From a9af98fa5cc684c261ea99f42eec77f876db099f Mon Sep 17 00:00:00 2001 From: Dave Richardson Date: Mon, 31 Aug 2026 18:55:55 -0700 Subject: [PATCH] fix(weave): append versioned first-Knop MeshInventory facts - preserve the complete carried MeshInventory as the exact output prefix - request only first-Knop membership, page, and next-state facts through the shared append planner - fail closed on conflicting locators and legacy inventory-owned progression before writes - remove the legacy first-Knop block-replacement fallback and cover Alice fixture/runtime behavior --- documentation/notes/wd.codebase-overview.md | 2 +- documentation/notes/wd.todo.md | 1 + src/core/weave/mesh_inventory_renderers.ts | 311 +++++------------- .../weave/mesh_inventory_renderers_test.ts | 98 ++++++ src/core/weave/weave_test.ts | 69 +++- tests/integration/weave_test.ts | 128 ++++++- 6 files changed, 345 insertions(+), 264 deletions(-) diff --git a/documentation/notes/wd.codebase-overview.md b/documentation/notes/wd.codebase-overview.md index 3a59bae..1d1594f 100644 --- a/documentation/notes/wd.codebase-overview.md +++ b/documentation/notes/wd.codebase-overview.md @@ -35,7 +35,7 @@ Core owns portable Semantic Flow behavior: request/result types, RDF/Turtle help - `src/core/weave` owns shared weave/version/generate contracts and planner helpers. - `src/core/weave/inventory_append_planner.ts` prepares current inventory bytes and parsed default-graph quads as one opaque value, plans append/no-op/conflict outcomes, and renders planner-approved append facts through one shared compact renderer. The prepared current bytes remain the exact output prefix; a self-contained suffix-only proof checks rendered facts against `plan.missing`, avoiding a second parse of carried blank nodes. `knop.create` MeshInventory growth, FoundingReferentData settlement, current-only ReferenceCatalog and PageDefinition weave, current-shape extract and integrate, import source-registry insertion, first `knop add-reference` catalog registration, extracted-term versioned MeshInventory growth, and shared current-only payload-like page claims use the planner. - `src/core/knop/add_reference.ts` retains its unwoven/current-only/versioned shape validation and known support-family consistency checks, but treats the shape renderer only as the requested ReferenceCatalog fact set. The original KnopInventory is the append planner's prepared input and exact output prefix, so unknown facts, comments, prefixes, and carried blank nodes survive; single-valued conflicts refuse before the runtime stages writes. -- `src/core/weave/mesh_inventory_renderers.ts` builds only the term, Knop, page, membership, and optional history/state facts owned by each migrated path, then appends them against the original MeshInventory. Homogeneous extracted batches, versioned sequential extracted weave, and the shared current-only first-payload/extracted helper preserve carried bytes and graph; current-only semantic no-ops return exact input bytes and create no history progression. +- `src/core/weave/mesh_inventory_renderers.ts` builds only the term, Knop, page, membership, and optional history/state facts owned by each migrated path, then appends them against the original MeshInventory. Versioned first-Knop weave, homogeneous extracted batches, versioned sequential extracted weave, and the shared current-only first-payload/extracted helper preserve carried bytes and graph. First-Knop weave rejects legacy inventory-owned mutable progression before planning instead of silently deleting or retaining it; current-only semantic no-ops return exact input bytes and create no history progression. - `src/core/weave/mesh_support_pages.ts` keeps initial support-history construction separate. Initial versioned support history writes only settled artifact/history/state/manifestation membership into MeshInventory and writes current/latest/next progression for every versioned mesh-owned support artifact into MeshMetadata. Its no-initial-history page-only arm prepares the carried MeshInventory and appends only named support-page claims/types. Runtime ResourcePage history/raw-source assembly combines inventory structure with metadata progression. Exact page-claim duplicates produce no inventory update; comments, repeated blocks, opaque facts, trailing bytes, and carried blank nodes remain untouched prefix data. - `src/core/weave/knop_inventory_renderers.ts` uses the same append path for current-only ReferenceCatalog and ResourcePageDefinition page claims. PageDefinition working-file validation is RDF-semantic rather than substring/block based, so unrelated mentions cannot hide a conflicting locator; exact duplicates return the original KnopInventory bytes. - `src/core/extract/extract.ts` keeps its legacy-carried-shape renderer separate, but normal current-shape MeshInventory registration prepares and preserves the original bytes before appending only `_mesh` membership, Knop type/working-inventory, and inventory-file facts. Carried source-locator blank nodes remain prefix data and are not part of the named-node-only append request. diff --git a/documentation/notes/wd.todo.md b/documentation/notes/wd.todo.md index e2ef010..37b87db 100644 --- a/documentation/notes/wd.todo.md +++ b/documentation/notes/wd.todo.md @@ -37,6 +37,7 @@ Groomed on 2026-05-27 from all `wa.task.*` notes in the Weave archive. Updated a - [ ] Keep Stagecraft's persisted roleplaying-data use case in view when prioritizing runtime work: prefer slices that improve stable identifiers, exact state citation, append-onlyish histories, source provenance, and generated inspection pages for ordinary application data.- [ ] Remove the remaining first-payload planner blockers from [[wa.task.2026.2026-05-04-refactor-planFirstPayloadWeave]]. Multi-pending first-payload weave shipped in `v0.7.0` (PR #33), extracted-term sets joined the batch path post-regroom (PR #41), and condition-specific diagnostics landed (PR #42). Still open: current-mode extracted-term weave, and the SFLO/URPX docs pass to drop the old failure-mode warnings. - [ ] Implement append-onlyish inventory writes through [[wa.plan.2026.2026-05-17-append-onlyish-inventory]]: normal inventory operations append new settled facts, no-op existing facts, and fail closed on conflicts; current/latest/next progression belongs in metadata or explicit repair/regeneration/retraction modes. `planInventoryAppend` now has consistency-by-construction prepared current input and one exactness-checked shared append renderer; current-only ReferenceCatalog and PageDefinition, first/later `knop.create`, FoundingReferentData, import source-registry insertion, first `knop add-reference` catalog registration, extracted/current-only MeshInventory weave growth, current-shape extract/integrate, and mesh-support page-only growth use it, while ResourcePage policy deletion is fixed. Mesh-support progression producers and affected fixture tails are corrected. Remaining in sequence: first-Knop append, other versioned/KnopInventory writers, then fixture/docs closure. +- [ ] In the next Weave release notes, call out the first-Knop MeshInventory output change from [[wa.task.2026.2026-08-31_1111-versioned-first-knop-mesh-inventory-append]]: carried bytes/facts now survive as the exact prefix, stale inventory-owned progression refuses pending explicit repair, and the affected live fixture refs were regenerated. - [ ] Harden the shared append renderer against an internally mismatched prepared-inventory/plan pair, even though current production callers construct and pass the pair together and the final G1 review found no public reachability. Keep this separate from FoundingReferentData unless that work touches the renderer. - [ ] Stop reusing one stateful N3 Parser across multiple planned Turtle files in import, integrate, version execution, and payload update validators; instantiate per document so `@base`/`@prefix` state cannot leak between files. Found by [[wa.review.2026-08-22_2303-stagecraft-phase1-g1-claude]], outside G1. - [ ] Replace FoundingReferentData history progression's compact-`sflo:` regex rewrite with the shared subject-block/progression machinery or an equivalently representation-independent path, preserving fail-closed diagnostics for foreign-but-equivalent inventory serialization. Accepted as a non-landing G2 residual in [[wa.review.2026-08-23_0939-stagecraft-phase2-final-claude]]. diff --git a/src/core/weave/mesh_inventory_renderers.ts b/src/core/weave/mesh_inventory_renderers.ts index 2ecaafb..8686a2b 100644 --- a/src/core/weave/mesh_inventory_renderers.ts +++ b/src/core/weave/mesh_inventory_renderers.ts @@ -11,6 +11,7 @@ import { WeaveInputError } from "./errors.ts"; import { planInventoryAppend, prepareCurrentInventory, + type PreparedCurrentInventory, renderInventoryAppendPlan, } from "./inventory_append_planner.ts"; import type { MeshInventoryProgression } from "./progression_models.ts"; @@ -30,6 +31,12 @@ import { } from "./turtle_blocks.ts"; const SFLO_HAS_KNOP_IRI = `${SFLO_NAMESPACE}hasKnop`; +const FIRST_KNOP_LEGACY_INVENTORY_PROGRESSION_PREDICATES = new Set([ + `${SFLO_NAMESPACE}currentArtifactHistory`, + `${SFLO_NAMESPACE}nextHistoryOrdinal`, + `${SFLO_NAMESPACE}latestHistoricalState`, + `${SFLO_NAMESPACE}nextStateOrdinal`, +]); const BATCHED_EXTRACTED_SINGLE_VALUED_PREDICATES = [ `${SFLO_NAMESPACE}currentArtifactHistory`, `${SFLO_NAMESPACE}hasWorkingKnopInventoryFile`, @@ -48,120 +55,68 @@ export function renderFirstKnopWovenMeshInventoryTurtle( designatorPath: string, meshInventoryProgression: MeshInventoryProgression, ): string { - const knopPath = toKnopPath(designatorPath); - const designatorPagePath = toDesignatorResourcePagePath(designatorPath); - const historyPath = meshInventoryProgression.historyPath; - const latestManifestationPath = - meshInventoryProgression.latestManifestationPath; - const nextStatePath = meshInventoryProgression.nextStatePath; - const nextStateOrdinal = meshInventoryProgression.nextStateOrdinal; - const nextManifestationPath = `${nextStatePath}/ttl`; - const initialBlocks = normalizeMeshInventoryHeader( - splitTurtleBlocks(currentMeshInventoryTurtle), + const preparedCurrentInventory = prepareCurrentInventory({ + baseIri: meshBase, + currentInventoryTurtle: currentMeshInventoryTurtle, + currentInventoryLabel: + `current MeshInventory for first-Knop weave ${designatorPath}`, + }); + assertNoLegacyFirstKnopInventoryProgression( + preparedCurrentInventory, + designatorPath, ); - if ( - findSubjectBlockIndex(initialBlocks, "_mesh") === -1 || - findSubjectBlockIndex( - initialBlocks, - latestManifestationPath, - ) === -1 || - findSubjectBlockIndex(initialBlocks, "_mesh/index.html") === -1 - ) { - return renderLegacyFirstKnopWovenMeshInventoryTurtle( - meshBase, - designatorPath, + const plan = planInventoryAppend({ + preparedCurrentInventory, + requestedSettledFactsTurtle: + renderFirstKnopMeshInventoryRequestedFactsTurtle( + meshBase, + designatorPath, + meshInventoryProgression, + ), + singleValuedSettledPredicates: BATCHED_EXTRACTED_SINGLE_VALUED_PREDICATES, + currentInventoryLabel: + `current MeshInventory for first-Knop weave ${designatorPath}`, + requestedFactsLabel: + `versioned first-Knop MeshInventory facts for ${designatorPath}`, + }); + if (plan.kind === "conflict") { + throw new WeaveInputError( + `Could not append versioned first-Knop MeshInventory facts for ${designatorPath}: ${ + plan.conflicts.map((conflict) => conflict.message).join(" ") + }`, ); } - let blocks = initialBlocks; - const knopPaths = resolveMeshRootKnopPaths( - meshBase, - currentMeshInventoryTurtle, - ); - if (!knopPaths.includes(knopPath)) { - knopPaths.push(knopPath); - } + return renderInventoryAppendPlan({ + preparedCurrentInventory, + plan, + outputLabel: + `versioned first-Knop MeshInventory append for ${designatorPath}`, + }); +} - blocks = replaceSubjectBlock( - blocks, - "_mesh", - renderMeshRootBlock(meshBase, knopPaths), - ); - blocks = replaceSubjectBlock( - blocks, - "_mesh/_inventory", - renderMeshInventoryArtifactBlock(historyPath), - ); - blocks = upsertSubjectBlockAfter( - blocks, - "_mesh", - designatorPath, - renderMeshIdentifierBlock(designatorPath), - ); - blocks = replaceSubjectBlock( - blocks, - knopPath, - renderMeshKnopBlockWithResourcePage(knopPath), - ); - blocks = replaceSubjectBlock( - blocks, - historyPath, - renderMeshInventoryHistoryBlock( - historyPath, - nextStateOrdinal, - nextStatePath, - ), - ); - blocks = upsertSubjectBlockAfter( - blocks, - latestManifestationPath, - nextStatePath, - renderMeshInventoryStateBlock( - nextStatePath, - nextStateOrdinal, - meshInventoryProgression.latestStatePath, - ), - ); - blocks = upsertSubjectBlockAfter( - blocks, - nextStatePath, - nextManifestationPath, - renderMeshInventoryStateManifestationBlock(nextStatePath), - ); - blocks = upsertSubjectBlockAfter( - blocks, - `${latestManifestationPath}/inventory.ttl`, - `${nextManifestationPath}/inventory.ttl`, - renderLocatedFileBlock(`${nextManifestationPath}/inventory.ttl`), - ); - blocks = upsertSubjectBlockAfter( - blocks, - "_mesh/index.html", - designatorPagePath, - renderResourcePageLocatedFileBlock(designatorPagePath), - ); - blocks = upsertSubjectBlockAfter( - blocks, - designatorPagePath, - `${knopPath}/index.html`, - renderResourcePageLocatedFileBlock(`${knopPath}/index.html`), - ); - blocks = upsertSubjectBlockAfter( - blocks, - `${latestManifestationPath}/index.html`, - `${nextStatePath}/index.html`, - renderResourcePageLocatedFileBlock(`${nextStatePath}/index.html`), - ); - blocks = upsertSubjectBlockAfter( - blocks, - `${nextStatePath}/index.html`, - `${nextManifestationPath}/index.html`, - renderResourcePageLocatedFileBlock( - `${nextManifestationPath}/index.html`, +function assertNoLegacyFirstKnopInventoryProgression( + preparedCurrentInventory: PreparedCurrentInventory, + designatorPath: string, +): void { + const carriedPredicates = [ + ...new Set( + preparedCurrentInventory.quads + .map((quad) => quad.predicate.value) + .filter((predicate) => + FIRST_KNOP_LEGACY_INVENTORY_PROGRESSION_PREDICATES.has(predicate) + ), ), - ); + ].sort((left, right) => left.localeCompare(right)); + if (carriedPredicates.length === 0) { + return; + } - return `${blocks.join("\n\n")}\n`; + throw new WeaveInputError( + `Could not append versioned first-Knop MeshInventory facts for ${designatorPath} because the current MeshInventory contains legacy inventory-owned mutable progression predicates: ${ + carriedPredicates.map((predicate) => `<${predicate}>`).join(", ") + }. Regenerate the fixture or use an explicit repair path before retrying weave.`, + ); } export function renderMeshMetadataWithMeshInventoryProgression( @@ -483,6 +438,23 @@ ${blocks.join("\n\n")} `; } +function renderFirstKnopMeshInventoryRequestedFactsTurtle( + meshBase: string, + designatorPath: string, + meshInventoryProgression: MeshInventoryProgression, +): string { + return `${ + renderExtractedKnopMeshInventoryRequestedFactsTurtle( + meshBase, + [designatorPath], + meshInventoryProgression, + ).trimEnd() + } + +<_mesh> sflo:hasKnop <${toKnopPath(designatorPath)}> . +`; +} + /* * Batched extracted-term rendering intentionally has no block-replacement * fallback. The append planner owns duplicate detection, conflict refusal, @@ -608,131 +580,6 @@ function renderMeshInventoryHistoryMetaProgressionBlock( }"^^xsd:nonNegativeInteger .`; } -function renderLegacyFirstKnopWovenMeshInventoryTurtle( - meshBase: string, - designatorPath: string, -): string { - const knopPath = toKnopPath(designatorPath); - const designatorPagePath = toDesignatorResourcePagePath(designatorPath); - - return `@base <${meshBase}> . -${SFLO_TURTLE_PREFIX_DECLARATION} -@prefix xsd: . - -<_mesh> a sflo:SemanticMesh ; - sflo:meshBase "${meshBase}"^^xsd:anyURI ; - sflo:hasMeshMetadata <_mesh/_meta> ; - sflo:hasMeshInventory <_mesh/_inventory> ; - sflo:hasKnop <${knopPath}> ; - sflo:hasResourcePage <_mesh/index.html> . - -<${designatorPath}> - sflo:hasResourcePage <${designatorPagePath}> . - -<${knopPath}> a sflo:Knop ; - sflo:hasWorkingKnopInventoryFile <${knopPath}/_inventory/inventory.ttl> ; - sflo:hasResourcePage <${knopPath}/index.html> . - -<_mesh/_meta> a sflo:MeshMetadata, sflo:DigitalArtifact, sflo:RdfDocument ; - sflo:hasArtifactHistory <_mesh/_meta/_history001> ; - sflo:currentArtifactHistory <_mesh/_meta/_history001> ; - sflo:nextHistoryOrdinal "2"^^xsd:nonNegativeInteger ; - sflo:hasWorkingLocatedFile <_mesh/_meta/meta.ttl> ; - sflo:hasResourcePage <_mesh/_meta/index.html> . - -<_mesh/_meta/_history001> a sflo:ArtifactHistory ; - sflo:historyOrdinal "1"^^xsd:nonNegativeInteger ; - sflo:hasHistoricalState <_mesh/_meta/_history001/_s0001> ; - sflo:latestHistoricalState <_mesh/_meta/_history001/_s0001> ; - sflo:nextStateOrdinal "2"^^xsd:nonNegativeInteger ; - sflo:hasResourcePage <_mesh/_meta/_history001/index.html> . - -<_mesh/_meta/_history001/_s0001> a sflo:HistoricalState ; - sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ; - sflo:hasManifestation <_mesh/_meta/_history001/_s0001/ttl> ; - sflo:locatedFileForState <_mesh/_meta/_history001/_s0001/ttl/meta.ttl> ; - sflo:hasResourcePage <_mesh/_meta/_history001/_s0001/index.html> . - -<_mesh/_meta/_history001/_s0001/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ; - sflo:locatedFileForManifestation <_mesh/_meta/_history001/_s0001/ttl/meta.ttl> ; - sflo:hasResourcePage <_mesh/_meta/_history001/_s0001/ttl/index.html> . - -<_mesh/_inventory> a sflo:MeshInventory, sflo:DigitalArtifact, sflo:RdfDocument ; - sflo:hasArtifactHistory <_mesh/_inventory/_history001> ; - sflo:currentArtifactHistory <_mesh/_inventory/_history001> ; - sflo:nextHistoryOrdinal "2"^^xsd:nonNegativeInteger ; - sflo:hasWorkingLocatedFile <_mesh/_inventory/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/index.html> . - -<_mesh/_inventory/_history001> a sflo:ArtifactHistory ; - sflo:historyOrdinal "1"^^xsd:nonNegativeInteger ; - sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0001> ; - sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0002> ; - sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0002> ; - sflo:nextStateOrdinal "3"^^xsd:nonNegativeInteger ; - sflo:hasResourcePage <_mesh/_inventory/_history001/index.html> . - -<_mesh/_inventory/_history001/_s0001> a sflo:HistoricalState ; - sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ; - sflo:hasManifestation <_mesh/_inventory/_history001/_s0001/ttl> ; - sflo:locatedFileForState <_mesh/_inventory/_history001/_s0001/ttl/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/_history001/_s0001/index.html> . - -<_mesh/_inventory/_history001/_s0001/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ; - sflo:locatedFileForManifestation <_mesh/_inventory/_history001/_s0001/ttl/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/_history001/_s0001/ttl/index.html> . - -<_mesh/_inventory/_history001/_s0002> a sflo:HistoricalState ; - sflo:stateOrdinal "2"^^xsd:nonNegativeInteger ; - sflo:previousHistoricalState <_mesh/_inventory/_history001/_s0001> ; - sflo:hasManifestation <_mesh/_inventory/_history001/_s0002/ttl> ; - sflo:locatedFileForState <_mesh/_inventory/_history001/_s0002/ttl/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/_history001/_s0002/index.html> . - -<_mesh/_inventory/_history001/_s0002/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ; - sflo:locatedFileForManifestation <_mesh/_inventory/_history001/_s0002/ttl/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/_history001/_s0002/ttl/index.html> . - -<_mesh/_meta/meta.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -<_mesh/_inventory/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -<_mesh/_meta/_history001/_s0001/ttl/meta.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -<_mesh/_inventory/_history001/_s0001/ttl/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -<_mesh/_inventory/_history001/_s0002/ttl/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -<${knopPath}/_inventory/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -<_mesh/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<${designatorPagePath}> a sflo:ResourcePage, sflo:LocatedFile . - -<${knopPath}/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_meta/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_meta/_history001/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_meta/_history001/_s0001/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_meta/_history001/_s0001/ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/_history001/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/_history001/_s0001/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/_history001/_s0001/ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/_history001/_s0002/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/_history001/_s0002/ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile . -`; -} - function renderLegacyFirstPayloadWovenMeshInventoryTurtle( meshBase: string, designatorPath: string, diff --git a/src/core/weave/mesh_inventory_renderers_test.ts b/src/core/weave/mesh_inventory_renderers_test.ts index 59a26d4..29f7877 100644 --- a/src/core/weave/mesh_inventory_renderers_test.ts +++ b/src/core/weave/mesh_inventory_renderers_test.ts @@ -3,6 +3,7 @@ import { Parser, type Quad, type Term } from "n3"; import { WeaveInputError } from "./errors.ts"; import { renderBatchedFirstExtractedKnopWovenMeshInventoryTurtle, + renderFirstKnopWovenMeshInventoryTurtle, renderFirstPayloadWovenCurrentOnlyMeshInventoryTurtle, renderGenericFirstExtractedKnopWovenMeshInventoryTurtle, } from "./mesh_inventory_renderers.ts"; @@ -235,6 +236,95 @@ Deno.test("current-only payload-like MeshInventory returns exact bytes for a sem ); }); +Deno.test("versioned first-Knop MeshInventory appends owned facts with an exact carried prefix", () => { + const currentInventory = renderVersionedCurrentInventory().replace( + "sflo:hasKnop , ;", + "sflo:hasKnop ;", + ); + const rendered = renderFirstKnopWovenMeshInventoryTurtle( + currentInventory, + MESH_BASE, + "term-a", + meshInventoryProgression, + ); + + assert(rendered.startsWith(currentInventory)); + assertTurtleGraphsEqual( + rendered, + `${currentInventory}\n${ + renderTargetFacts(["term-a"]) + }\n${renderNextProgressionFacts()}\n${ + renderMeshKnopMembershipFact("term-a") + }`, + ); +}); + +Deno.test("versioned first-Knop MeshInventory returns exact bytes for a semantic no-op", () => { + const currentInventory = renderVersionedCurrentInventory(); + const alreadyWovenInventory = `${currentInventory}\n${ + renderTargetFacts(["term-a"]) + }\n${renderNextProgressionFacts()}\n${ + renderMeshKnopMembershipFact("term-a") + }`; + + assertEquals( + renderFirstKnopWovenMeshInventoryTurtle( + alreadyWovenInventory, + MESH_BASE, + "term-a", + meshInventoryProgression, + ), + alreadyWovenInventory, + ); +}); + +Deno.test("versioned first-Knop MeshInventory rejects a conflicting Knop inventory locator", () => { + const conflictingInventory = renderVersionedCurrentInventory().replace( + " .", + " .", + ); + + assertThrows( + () => + renderFirstKnopWovenMeshInventoryTurtle( + conflictingInventory, + MESH_BASE, + "term-a", + meshInventoryProgression, + ), + WeaveInputError, + "Requested settled inventory fact . conflicts with existing fact .", + ); +}); + +Deno.test("versioned first-Knop MeshInventory rejects legacy inventory-owned progression", () => { + const legacyInventory = `${renderVersionedCurrentInventory()} +@base <${MESH_BASE}> . +@prefix sflo: <${SFLO}> . +@prefix xsd: . + +<_mesh/_inventory> + sflo:currentArtifactHistory <_mesh/_inventory/_history001> ; + sflo:nextHistoryOrdinal "2"^^xsd:nonNegativeInteger . + +<_mesh/_inventory/_history001> + sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0001> ; + sflo:nextStateOrdinal "2"^^xsd:nonNegativeInteger . +`; + + assertThrows( + () => + renderFirstKnopWovenMeshInventoryTurtle( + legacyInventory, + MESH_BASE, + "term-a", + meshInventoryProgression, + ), + WeaveInputError, + "legacy inventory-owned mutable progression predicates: , , , ", + ); +}); + function renderVersionedCurrentInventory(): string { return currentOnlyPendingInventory.replace( " sflo:hasWorkingLocatedFile <_mesh/_inventory/inventory.ttl> ;", @@ -303,6 +393,14 @@ function renderCurrentOnlyPayloadLikeFacts(designatorPath: string): string { `; } +function renderMeshKnopMembershipFact(designatorPath: string): string { + return `@base <${MESH_BASE}> . +@prefix sflo: <${SFLO}> . + +<_mesh> sflo:hasKnop <${designatorPath}/_knop> . +`; +} + function renderNextProgressionFacts(): string { return `@base <${MESH_BASE}> . @prefix sflo: <${SFLO}> . diff --git a/src/core/weave/weave_test.ts b/src/core/weave/weave_test.ts index f78314a..b5fa0df 100644 --- a/src/core/weave/weave_test.ts +++ b/src/core/weave/weave_test.ts @@ -68,16 +68,12 @@ const firstWeaveMeshInventoryTurtle = <_mesh/_inventory> a sflo:MeshInventory, sflo:DigitalArtifact, sflo:RdfDocument ; sflo:hasArtifactHistory <_mesh/_inventory/_history001> ; - sflo:currentArtifactHistory <_mesh/_inventory/_history001> ; - sflo:nextHistoryOrdinal "2"^^xsd:nonNegativeInteger ; sflo:hasWorkingLocatedFile <_mesh/_inventory/inventory.ttl> ; sflo:hasResourcePage <_mesh/_inventory/index.html> . <_mesh/_inventory/_history001> a sflo:ArtifactHistory ; sflo:historyOrdinal "1"^^xsd:nonNegativeInteger ; sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0001> ; - sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0001> ; - sflo:nextStateOrdinal "2"^^xsd:nonNegativeInteger ; sflo:hasResourcePage <_mesh/_inventory/_history001/index.html> . `; @@ -1386,9 +1382,21 @@ Deno.test("planWeave renders the first alice knop-created-woven slice", () => { path: "alice/index.html", designatorPath: "alice", }); - assertStringIncludes( - plan.updatedFiles[0]?.contents ?? "", - "\n sflo:hasResourcePage .", + const meshInventory = plan.updatedFiles[0]?.contents ?? ""; + assert(meshInventory.startsWith(firstWeaveMeshInventoryTurtle)); + const meshInventoryQuads = parseWeaveShapeQuads( + "https://semantic-flow.github.io/mesh-alice-bio/", + meshInventory, + "Could not parse first-Knop MeshInventory output.", + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "alice", + `${SFLO_NAMESPACE}hasResourcePage`, + "alice/index.html", + ), ); assertStringIncludes( plan.updatedFiles[1]?.contents ?? "", @@ -2713,13 +2721,39 @@ Deno.test("planWeave supports a later first root Knop weave against a carried me plan.updatedFiles[2]?.contents ?? "", "sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0006> ;", ); - assertStringIncludes( - plan.updatedFiles[0]?.contents ?? "", - "<>\n sflo:hasResourcePage .", + const meshInventory = plan.updatedFiles[0]?.contents ?? ""; + assert(meshInventory.startsWith(createPlan.updatedFiles[0]!.contents)); + const meshInventoryQuads = parseWeaveShapeQuads( + "https://semantic-flow.github.io/mesh-alice-bio/", + meshInventory, + "Could not parse first root Knop MeshInventory output.", ); - assertStringIncludes( - plan.updatedFiles[0]?.contents ?? "", - "<_knop> a sflo:Knop ;\n sflo:hasWorkingKnopInventoryFile <_knop/_inventory/inventory.ttl> ;\n sflo:hasResourcePage <_knop/index.html> .", + assert( + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "", + `${SFLO_NAMESPACE}hasResourcePage`, + "index.html", + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "_knop", + `${SFLO_NAMESPACE}hasWorkingKnopInventoryFile`, + "_knop/_inventory/inventory.ttl", + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "_knop", + `${SFLO_NAMESPACE}hasResourcePage`, + "_knop/index.html", + ), ); }); @@ -4245,8 +4279,13 @@ Deno.test("planWeave renders an extracted term from a nested source without a ro ) .replace( ` a sflo:Knop ; - sflo:hasWorkingKnopInventoryFile ; - sflo:hasResourcePage . + sflo:hasWorkingKnopInventoryFile . + +`, + "", + ) + .replace( + ` sflo:hasResourcePage . `, "", diff --git a/tests/integration/weave_test.ts b/tests/integration/weave_test.ts index 57eb4a8..4b94367 100644 --- a/tests/integration/weave_test.ts +++ b/tests/integration/weave_test.ts @@ -353,6 +353,11 @@ Deno.test("executeWeave refreshes ancestor ResourcePages when a child designator Deno.test("executeWeave matches the settled alice knop-created-woven fixture", async () => { const workspaceRoot = await createTestTmpDir("weave-weave-first-"); await materializeMeshAliceBioBranch("04-alice-knop-created", workspaceRoot); + const meshInventoryPath = join( + workspaceRoot, + "_mesh/_inventory/inventory.ttl", + ); + const meshInventoryBefore = await Deno.readTextFile(meshInventoryPath); const result = await executeWeave({ meshRoot: workspaceRoot, @@ -361,14 +366,20 @@ Deno.test("executeWeave matches the settled alice knop-created-woven fixture", a assertEquals(result.wovenDesignatorPaths, ["alice"]); assert(result.updatedPaths.includes("_mesh/_inventory/inventory.ttl")); assert(result.updatedPaths.includes("alice/_knop/_inventory/inventory.ttl")); + const meshInventoryAfter = await Deno.readTextFile(meshInventoryPath); + assert(meshInventoryAfter.startsWith(meshInventoryBefore)); assertEquals( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), - ), - await readMeshAliceBioBranchFile( - "05-alice-knop-created-woven", - "_mesh/_inventory/inventory.ttl", - ), + await compareRdfContent({ + left: new TextEncoder().encode(meshInventoryAfter), + right: new TextEncoder().encode( + await readMeshAliceBioBranchFile( + "05-alice-knop-created-woven", + "_mesh/_inventory/inventory.ttl", + ), + ), + path: "_mesh/_inventory/inventory.ttl", + }), + true, ); assertEquals( await Deno.readTextFile( @@ -389,6 +400,70 @@ Deno.test("executeWeave matches the settled alice knop-created-woven fixture", a ); }); +Deno.test("executeWeave first-Knop conflict writes no files and preserves MeshInventory bytes", async () => { + const workspaceRoot = await createTestTmpDir( + "weave-weave-first-knop-conflict-", + ); + await materializeMeshAliceBioBranch("04-alice-knop-created", workspaceRoot); + const inventoryPath = join( + workspaceRoot, + "_mesh/_inventory/inventory.ttl", + ); + const currentInventory = await Deno.readTextFile(inventoryPath); + const conflictingInventory = currentInventory.replace( + " .", + " .", + ); + assert(conflictingInventory !== currentInventory); + await Deno.writeTextFile(inventoryPath, conflictingInventory); + const workspaceBefore = await snapshotWorkspaceFiles(workspaceRoot); + + await assertRejects( + () => executeWeave({ meshRoot: workspaceRoot }), + Error, + "Requested settled inventory fact", + ); + + assertEquals(await Deno.readTextFile(inventoryPath), conflictingInventory); + assertEquals(await snapshotWorkspaceFiles(workspaceRoot), workspaceBefore); +}); + +Deno.test("executeWeave rejects legacy first-Knop inventory progression without writes", async () => { + const workspaceRoot = await createTestTmpDir( + "weave-weave-first-knop-legacy-progression-", + ); + await materializeMeshAliceBioBranch("04-alice-knop-created", workspaceRoot); + const inventoryPath = join( + workspaceRoot, + "_mesh/_inventory/inventory.ttl", + ); + const currentInventory = await Deno.readTextFile(inventoryPath); + const legacyInventory = `${currentInventory} +@base . +@prefix sflo: . +@prefix xsd: . + +<_mesh/_inventory> + sflo:currentArtifactHistory <_mesh/_inventory/_history001> ; + sflo:nextHistoryOrdinal "2"^^xsd:nonNegativeInteger . + +<_mesh/_inventory/_history001> + sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0001> ; + sflo:nextStateOrdinal "2"^^xsd:nonNegativeInteger . +`; + await Deno.writeTextFile(inventoryPath, legacyInventory); + const workspaceBefore = await snapshotWorkspaceFiles(workspaceRoot); + + await assertRejects( + () => executeWeave({ meshRoot: workspaceRoot }), + Error, + "legacy inventory-owned mutable progression predicates", + ); + + assertEquals(await Deno.readTextFile(inventoryPath), legacyInventory); + assertEquals(await snapshotWorkspaceFiles(workspaceRoot), workspaceBefore); +}); + Deno.test("executeWeave supports the exact root target", async () => { const workspaceRoot = await createTestTmpDir("weave-weave-root-"); await materializeMeshAliceBioBranch( @@ -440,6 +515,11 @@ Deno.test("executeWeave supports a later first root Knop weave against a carried designatorPath: "", }, }); + const meshInventoryPath = join( + workspaceRoot, + "_mesh/_inventory/inventory.ttl", + ); + const meshInventoryBeforeWeave = await Deno.readTextFile(meshInventoryPath); const result = await executeWeave({ meshRoot: workspaceRoot, @@ -458,17 +538,33 @@ Deno.test("executeWeave supports a later first root Knop weave against a carried assert(result.updatedPaths.includes("_knop/_inventory/inventory.ttl")); await Deno.stat(join(workspaceRoot, "index.html")); await Deno.stat(join(workspaceRoot, "_knop/index.html")); - assertStringIncludes( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), - ), - "sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0006> ;", + const meshInventoryAfterWeave = await Deno.readTextFile(meshInventoryPath); + assert(meshInventoryAfterWeave.startsWith(meshInventoryBeforeWeave)); + assertEquals( + await compareRdfContent({ + left: new TextEncoder().encode(meshInventoryAfterWeave), + right: new TextEncoder().encode( + await readMeshAliceBioBranchFile( + "23-root-knop-created-woven", + "_mesh/_inventory/inventory.ttl", + ), + ), + path: "_mesh/_inventory/inventory.ttl", + }), + true, ); - assertStringIncludes( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + const meshInventoryQuads = new Parser({ + baseIRI: MESH_ALICE_BIO_BASE, + }).parse(meshInventoryAfterWeave); + assert( + meshInventoryQuads.some((quad: Quad) => + quad.subject.termType === "NamedNode" && + quad.subject.value === MESH_ALICE_BIO_BASE && + quad.predicate.value === + "https://semantic-flow.github.io/sflo/ontology/hasResourcePage" && + quad.object.termType === "NamedNode" && + quad.object.value === new URL("index.html", MESH_ALICE_BIO_BASE).href ), - "<>\n sflo:hasResourcePage .", ); });