diff --git a/documentation/notes/wd.codebase-overview.md b/documentation/notes/wd.codebase-overview.md index 1d1594f..2eed7bd 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. 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_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, singular/delegated-batch versioned first-payload weave, homogeneous extracted batches, versioned sequential extracted weave, and the shared current-only first-payload/extracted helper preserve carried bytes and graph. Versioned first-Knop/first-payload weave rejects legacy inventory-owned mutable progression before planning; first-payload also rejects conflicting values or locator kinds. 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 f31a084..0367e1f 100644 --- a/documentation/notes/wd.todo.md +++ b/documentation/notes/wd.todo.md @@ -37,7 +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/versioned first-Knop 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. The fresh audit cut [[wa.task.2026.2026-08-31_1905-versioned-first-payload-mesh-inventory-append]] next; payload/other KnopInventory writers and fixture/docs closure follow. -- [ ] In the next Weave release notes, call out the first-Knop MeshInventory output change from [[wa.completed.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 Alice live fixture refs were regenerated. +- [ ] In the next Weave release notes, call out the first-Knop and versioned first-payload MeshInventory output changes from [[wa.completed.2026.2026-08-31_1111-versioned-first-knop-mesh-inventory-append]] and [[wa.task.2026.2026-08-31_1905-versioned-first-payload-mesh-inventory-append]]: carried bytes/facts now survive as the exact prefix, stale inventory-owned progression and conflicting first-payload locator kinds refuse pending explicit repair, and the affected Alice 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/knop/create_test.ts b/src/core/knop/create_test.ts index 17c4bb9..f8db1e1 100644 --- a/src/core/knop/create_test.ts +++ b/src/core/knop/create_test.ts @@ -218,9 +218,12 @@ Deno.test( "https://semantic-flow.github.io/sflo/ontology/hasKnop", "_knop", ); - assertStringIncludes( + assertNamedNodeFact( plan.updatedFiles[0]?.contents ?? "", - "sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0005> ;", + "https://semantic-flow.github.io/mesh-alice-bio/", + "_mesh/_inventory/_history001", + "https://semantic-flow.github.io/sflo/ontology/hasHistoricalState", + "_mesh/_inventory/_history001/_s0005", ); assertNamedNodeFact( plan.updatedFiles[0]?.contents ?? "", diff --git a/src/core/weave/mesh_inventory_renderers.ts b/src/core/weave/mesh_inventory_renderers.ts index 8686a2b..91169c6 100644 --- a/src/core/weave/mesh_inventory_renderers.ts +++ b/src/core/weave/mesh_inventory_renderers.ts @@ -1,7 +1,6 @@ import { toDesignatorResourcePagePath, toKnopPath, - toPayloadSourceRepositoryFloatingLocatorPath, } from "../designator_segments.ts"; import { SFLO_NAMESPACE, @@ -15,23 +14,21 @@ import { renderInventoryAppendPlan, } from "./inventory_append_planner.ts"; import type { MeshInventoryProgression } from "./progression_models.ts"; -import { parseWeaveShapeQuads } from "./rdf_helpers.ts"; import { renderCurrentWorkingFileDeclaration, renderCurrentWorkingFileLocator, - renderRepositorySourceFloatingLocatorNamedBlock, + resolveCurrentWorkingFileLocatorTerm, } from "./source_locator_renderers.ts"; import type { RepositorySourceFloatingLocator } from "./source_models.ts"; -import { - findSubjectBlockIndex, - normalizeMeshInventoryHeader, - replaceSubjectBlock, - splitTurtleBlocks, - upsertSubjectBlockAfter, -} from "./turtle_blocks.ts"; - -const SFLO_HAS_KNOP_IRI = `${SFLO_NAMESPACE}hasKnop`; -const FIRST_KNOP_LEGACY_INVENTORY_PROGRESSION_PREDICATES = new Set([ +import { splitTurtleBlocks, upsertSubjectBlockAfter } from "./turtle_blocks.ts"; + +const SFLO_HAS_REPOSITORY_SOURCE_FLOATING_LOCATOR_IRI = + `${SFLO_NAMESPACE}hasRepositorySourceFloatingLocator`; +const SFLO_HAS_WORKING_LOCATED_FILE_IRI = + `${SFLO_NAMESPACE}hasWorkingLocatedFile`; +const SFLO_WORKING_LOCAL_RELATIVE_PATH_IRI = + `${SFLO_NAMESPACE}workingLocalRelativePath`; +const LEGACY_INVENTORY_PROGRESSION_PREDICATES = new Set([ `${SFLO_NAMESPACE}currentArtifactHistory`, `${SFLO_NAMESPACE}nextHistoryOrdinal`, `${SFLO_NAMESPACE}latestHistoricalState`, @@ -48,6 +45,13 @@ const BATCHED_EXTRACTED_SINGLE_VALUED_PREDICATES = [ `${SFLO_NAMESPACE}previousHistoricalState`, `${SFLO_NAMESPACE}stateOrdinal`, ] as const; +const FIRST_PAYLOAD_SINGLE_VALUED_PREDICATES = [ + ...BATCHED_EXTRACTED_SINGLE_VALUED_PREDICATES, + SFLO_HAS_REPOSITORY_SOURCE_FLOATING_LOCATOR_IRI, + `${SFLO_NAMESPACE}sourceRepositoryUrl`, + `${SFLO_NAMESPACE}sourceRepositoryPathFromRoot`, + SFLO_WORKING_LOCAL_RELATIVE_PATH_IRI, +] as const; export function renderFirstKnopWovenMeshInventoryTurtle( currentMeshInventoryTurtle: string, @@ -61,9 +65,9 @@ export function renderFirstKnopWovenMeshInventoryTurtle( currentInventoryLabel: `current MeshInventory for first-Knop weave ${designatorPath}`, }); - assertNoLegacyFirstKnopInventoryProgression( + assertNoLegacyMeshInventoryProgression( preparedCurrentInventory, - designatorPath, + `append versioned first-Knop MeshInventory facts for ${designatorPath}`, ); const plan = planInventoryAppend({ preparedCurrentInventory, @@ -95,16 +99,16 @@ export function renderFirstKnopWovenMeshInventoryTurtle( }); } -function assertNoLegacyFirstKnopInventoryProgression( +function assertNoLegacyMeshInventoryProgression( preparedCurrentInventory: PreparedCurrentInventory, - designatorPath: string, + operation: string, ): void { const carriedPredicates = [ ...new Set( preparedCurrentInventory.quads .map((quad) => quad.predicate.value) .filter((predicate) => - FIRST_KNOP_LEGACY_INVENTORY_PROGRESSION_PREDICATES.has(predicate) + LEGACY_INVENTORY_PROGRESSION_PREDICATES.has(predicate) ), ), ].sort((left, right) => left.localeCompare(right)); @@ -113,7 +117,7 @@ function assertNoLegacyFirstKnopInventoryProgression( } throw new WeaveInputError( - `Could not append versioned first-Knop MeshInventory facts for ${designatorPath} because the current MeshInventory contains legacy inventory-owned mutable progression predicates: ${ + `Could not ${operation} 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.`, ); @@ -157,159 +161,96 @@ export function renderFirstPayloadWovenMeshInventoryTurtle( repositorySourceFloatingLocator?: RepositorySourceFloatingLocator, payloadIsRdfDocument = true, ): string { - const knopPath = toKnopPath(designatorPath); - const rootDesignatorPath = toRootDesignatorPath(designatorPath); - const rootKnopPath = toKnopPath(rootDesignatorPath); - const designatorPagePath = toDesignatorResourcePagePath(designatorPath); - const rootPagePath = toDesignatorResourcePagePath(rootDesignatorPath); - const preferredDesignatorPageAnchorPath = - rootDesignatorPath === designatorPath ? "_mesh/index.html" : rootPagePath; - const historyPath = meshInventoryProgression.historyPath; - const nextStatePath = meshInventoryProgression.nextStatePath; - const nextStateManifestationPath = `${nextStatePath}/ttl`; - const initialBlocks = normalizeMeshInventoryHeader( - splitTurtleBlocks(currentMeshInventoryTurtle), - ); - if ( - findSubjectBlockIndex(initialBlocks, "_mesh") === -1 || - findSubjectBlockIndex( - initialBlocks, - meshInventoryProgression.latestManifestationPath, - ) === -1 - ) { - if (meshInventoryProgression.latestStateOrdinal !== 2) { - throw new WeaveInputError( - "Could not extend the current mesh inventory for a later first payload weave because the required current-state subject blocks were missing.", - ); - } - return renderLegacyFirstPayloadWovenMeshInventoryTurtle( - meshBase, - designatorPath, - workingLocalRelativePath, - repositorySourceFloatingLocator, - payloadIsRdfDocument, - ); - } - let blocks = initialBlocks; - const knopPaths = resolveMeshRootKnopPaths( - meshBase, - currentMeshInventoryTurtle, - ); - - if (!knopPaths.includes(knopPath)) { - knopPaths.push(knopPath); - } - const designatorPageAnchorPath = findSubjectBlockIndex( - blocks, - preferredDesignatorPageAnchorPath, - ) === -1 - ? "_mesh/index.html" - : preferredDesignatorPageAnchorPath; - const preferredKnopPageAnchorPath = rootDesignatorPath === designatorPath - ? designatorPagePath - : `${rootKnopPath}/index.html`; - const knopPageAnchorPath = findSubjectBlockIndex( - blocks, - preferredKnopPageAnchorPath, - ) === -1 - ? designatorPagePath - : preferredKnopPageAnchorPath; - - blocks = replaceSubjectBlock( - blocks, - "_mesh", - renderMeshRootBlock(meshBase, knopPaths), - ); - blocks = replaceSubjectBlock( - blocks, - "_mesh/_inventory", - renderMeshInventoryArtifactBlock(historyPath), + const preparedCurrentInventory = prepareCurrentInventory({ + baseIri: meshBase, + currentInventoryTurtle: currentMeshInventoryTurtle, + currentInventoryLabel: + `current MeshInventory for first-payload weave ${designatorPath}`, + }); + assertNoLegacyMeshInventoryProgression( + preparedCurrentInventory, + `append versioned first-payload MeshInventory facts for ${designatorPath}`, ); - blocks = replaceSubjectBlock( - blocks, + assertCompatibleFirstPayloadWorkingLocator( + preparedCurrentInventory, designatorPath, - renderMeshPayloadArtifactBlockWithResourcePage( - designatorPath, - workingLocalRelativePath, - repositorySourceFloatingLocator, - payloadIsRdfDocument, - ), + workingLocalRelativePath, + repositorySourceFloatingLocator, ); - if (repositorySourceFloatingLocator !== undefined) { - blocks = upsertSubjectBlockAfter( - blocks, - designatorPath, - toPayloadSourceRepositoryFloatingLocatorPath(designatorPath), - renderRepositorySourceFloatingLocatorNamedBlock( + const plan = planInventoryAppend({ + preparedCurrentInventory, + requestedSettledFactsTurtle: + renderFirstPayloadMeshInventoryRequestedFactsTurtle( + meshBase, designatorPath, + workingLocalRelativePath, + meshInventoryProgression, repositorySourceFloatingLocator, + payloadIsRdfDocument, ), + singleValuedSettledPredicates: FIRST_PAYLOAD_SINGLE_VALUED_PREDICATES, + currentInventoryLabel: + `current MeshInventory for first-payload weave ${designatorPath}`, + requestedFactsLabel: + `versioned first-payload MeshInventory facts for ${designatorPath}`, + }); + if (plan.kind === "conflict") { + throw new WeaveInputError( + `Could not append versioned first-payload MeshInventory facts for ${designatorPath}: ${ + plan.conflicts.map((conflict) => conflict.message).join(" ") + }`, ); } - blocks = replaceSubjectBlock( - blocks, - knopPath, - renderMeshKnopBlockWithResourcePage(knopPath), - ); - blocks = replaceSubjectBlock( - blocks, - historyPath, - renderMeshInventoryHistoryBlock( - historyPath, - meshInventoryProgression.nextStateOrdinal, - nextStatePath, - ), - ); - blocks = upsertSubjectBlockAfter( - blocks, - meshInventoryProgression.latestManifestationPath, - nextStatePath, - renderMeshInventoryStateBlock( - nextStatePath, - meshInventoryProgression.nextStateOrdinal, - meshInventoryProgression.latestStatePath, - ), - ); - blocks = upsertSubjectBlockAfter( - blocks, - nextStatePath, - nextStateManifestationPath, - renderMeshInventoryStateManifestationBlock(nextStatePath), - ); - blocks = upsertSubjectBlockAfter( - blocks, - `${meshInventoryProgression.latestManifestationPath}/inventory.ttl`, - `${nextStateManifestationPath}/inventory.ttl`, - renderLocatedFileBlock(`${nextStateManifestationPath}/inventory.ttl`), - ); - blocks = upsertSubjectBlockAfter( - blocks, - designatorPageAnchorPath, - designatorPagePath, - renderResourcePageLocatedFileBlock(designatorPagePath), - ); - blocks = upsertSubjectBlockAfter( - blocks, - knopPageAnchorPath, - `${knopPath}/index.html`, - renderResourcePageLocatedFileBlock(`${knopPath}/index.html`), - ); - blocks = upsertSubjectBlockAfter( - blocks, - `${meshInventoryProgression.latestManifestationPath}/index.html`, - `${nextStatePath}/index.html`, - renderResourcePageLocatedFileBlock(`${nextStatePath}/index.html`), + + return renderInventoryAppendPlan({ + preparedCurrentInventory, + plan, + outputLabel: + `versioned first-payload MeshInventory append for ${designatorPath}`, + }); +} + +function assertCompatibleFirstPayloadWorkingLocator( + preparedCurrentInventory: PreparedCurrentInventory, + designatorPath: string, + workingLocalRelativePath: string, + repositorySourceFloatingLocator?: RepositorySourceFloatingLocator, +): void { + const subjectIri = new URL(designatorPath, preparedCurrentInventory.baseIri) + .href; + const expectedLocator = resolveCurrentWorkingFileLocatorTerm( + designatorPath, + workingLocalRelativePath, + repositorySourceFloatingLocator, ); - blocks = upsertSubjectBlockAfter( - blocks, - `${nextStatePath}/index.html`, - `${nextStateManifestationPath}/index.html`, - renderResourcePageLocatedFileBlock( - `${nextStateManifestationPath}/index.html`, - ), + const expectedValue = expectedLocator.objectTermType === "NamedNode" + ? new URL(expectedLocator.objectValue, preparedCurrentInventory.baseIri) + .href + : expectedLocator.objectValue; + const carriedLocators = preparedCurrentInventory.quads.filter((quad) => + quad.subject.termType === "NamedNode" && + quad.subject.value === subjectIri && + (quad.predicate.value === SFLO_HAS_WORKING_LOCATED_FILE_IRI || + quad.predicate.value === SFLO_WORKING_LOCAL_RELATIVE_PATH_IRI || + quad.predicate.value === + SFLO_HAS_REPOSITORY_SOURCE_FLOATING_LOCATOR_IRI) ); + if (carriedLocators.length === 0) { + return; + } + if ( + carriedLocators.every((quad) => + quad.predicate.value === expectedLocator.predicateIri && + quad.object.termType === expectedLocator.objectTermType && + quad.object.value === expectedValue + ) + ) { + return; + } - return `${blocks.join("\n\n")}\n`; + throw new WeaveInputError( + `Could not append versioned first-payload MeshInventory facts for ${designatorPath} because the current MeshInventory has a conflicting working locator for <${designatorPath}>. Keep exactly the requested working locator before retrying weave.`, + ); } export interface BatchedFirstPayloadMeshInventoryTarget { @@ -406,26 +347,9 @@ function renderExtractedKnopMeshInventoryRequestedFactsTurtle( ]; }); if (meshInventoryProgression !== undefined) { - const historyPath = meshInventoryProgression.historyPath; - const nextStatePath = meshInventoryProgression.nextStatePath; - const nextManifestationPath = `${nextStatePath}/ttl`; blocks.push( - renderMeshInventoryArtifactBlock(historyPath), - renderMeshInventoryHistoryBlock( - historyPath, - meshInventoryProgression.nextStateOrdinal, - nextStatePath, - ), - renderMeshInventoryStateBlock( - nextStatePath, - meshInventoryProgression.nextStateOrdinal, - meshInventoryProgression.latestStatePath, - ), - renderMeshInventoryStateManifestationBlock(nextStatePath), - renderLocatedFileBlock(`${nextManifestationPath}/inventory.ttl`), - renderResourcePageLocatedFileBlock(`${nextStatePath}/index.html`), - renderResourcePageLocatedFileBlock( - `${nextManifestationPath}/index.html`, + ...renderMeshInventoryProgressionRequestedBlocks( + meshInventoryProgression, ), ); } @@ -438,6 +362,77 @@ ${blocks.join("\n\n")} `; } +function renderFirstPayloadMeshInventoryRequestedFactsTurtle( + meshBase: string, + designatorPath: string, + workingLocalRelativePath: string, + meshInventoryProgression: MeshInventoryProgression, + repositorySourceFloatingLocator?: RepositorySourceFloatingLocator, + payloadIsRdfDocument = true, +): string { + const knopPath = toKnopPath(designatorPath); + const designatorPagePath = toDesignatorResourcePagePath(designatorPath); + const currentWorkingFileDeclaration = renderCurrentWorkingFileDeclaration( + designatorPath, + workingLocalRelativePath, + repositorySourceFloatingLocator, + { locatedFileIsRdfDocument: payloadIsRdfDocument }, + ); + const blocks = [ + `<_mesh> sflo:hasKnop <${knopPath}> .`, + renderMeshPayloadArtifactBlockWithResourcePage( + designatorPath, + workingLocalRelativePath, + repositorySourceFloatingLocator, + payloadIsRdfDocument, + ), + renderMeshKnopBlockWithResourcePage(knopPath), + renderLocatedFileBlock(`${knopPath}/_inventory/inventory.ttl`), + ...(currentWorkingFileDeclaration.length === 0 + ? [] + : [currentWorkingFileDeclaration]), + renderResourcePageLocatedFileBlock(designatorPagePath), + renderResourcePageLocatedFileBlock(`${knopPath}/index.html`), + ...renderMeshInventoryProgressionRequestedBlocks( + meshInventoryProgression, + ), + ]; + + return `@base <${meshBase}> . +${SFLO_TURTLE_PREFIX_DECLARATION} +@prefix xsd: . + +${blocks.join("\n\n")} +`; +} + +function renderMeshInventoryProgressionRequestedBlocks( + meshInventoryProgression: MeshInventoryProgression, +): readonly string[] { + const historyPath = meshInventoryProgression.historyPath; + const nextStatePath = meshInventoryProgression.nextStatePath; + const nextManifestationPath = `${nextStatePath}/ttl`; + return [ + renderMeshInventoryArtifactBlock(historyPath), + renderMeshInventoryHistoryBlock( + historyPath, + meshInventoryProgression.nextStateOrdinal, + nextStatePath, + ), + renderMeshInventoryStateBlock( + nextStatePath, + meshInventoryProgression.nextStateOrdinal, + meshInventoryProgression.latestStatePath, + ), + renderMeshInventoryStateManifestationBlock(nextStatePath), + renderLocatedFileBlock(`${nextManifestationPath}/inventory.ttl`), + renderResourcePageLocatedFileBlock(`${nextStatePath}/index.html`), + renderResourcePageLocatedFileBlock( + `${nextManifestationPath}/index.html`, + ), + ]; +} + function renderFirstKnopMeshInventoryRequestedFactsTurtle( meshBase: string, designatorPath: string, @@ -580,227 +575,12 @@ function renderMeshInventoryHistoryMetaProgressionBlock( }"^^xsd:nonNegativeInteger .`; } -function renderLegacyFirstPayloadWovenMeshInventoryTurtle( - meshBase: string, - designatorPath: string, - workingLocalRelativePath: string, - repositorySourceFloatingLocator?: RepositorySourceFloatingLocator, - payloadIsRdfDocument = true, -): string { - const knopPath = toKnopPath(designatorPath); - const designatorPagePath = toDesignatorResourcePagePath(designatorPath); - const rootDesignatorPath = toRootDesignatorPath(designatorPath); - const rootKnopPath = toKnopPath(rootDesignatorPath); - const rootPagePath = toDesignatorResourcePagePath(rootDesignatorPath); - const distinctKnopPaths = rootKnopPath === knopPath - ? [knopPath] - : [rootKnopPath, knopPath]; - const meshRootKnopLines = distinctKnopPaths.map((path) => - ` sflo:hasKnop <${path}> ;` - ).join("\n"); - const rootIdentifierBlock = rootDesignatorPath === designatorPath - ? "" - : `<${rootDesignatorPath}> - sflo:hasResourcePage <${rootPagePath}> . -`; - const rootKnopBlock = rootKnopPath === knopPath - ? "" - : `<${rootKnopPath}> a sflo:Knop ; - sflo:hasWorkingKnopInventoryFile <${rootKnopPath}/_inventory/inventory.ttl> ; - sflo:hasResourcePage <${rootKnopPath}/index.html> . -`; - const rootKnopInventoryFileBlock = rootKnopPath === knopPath - ? "" - : `<${rootKnopPath}/_inventory/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument .`; - const rootPageFileBlock = rootDesignatorPath === designatorPath - ? "" - : `<${rootPagePath}> a sflo:ResourcePage, sflo:LocatedFile .`; - const rootKnopPageFileBlock = rootKnopPath === knopPath - ? "" - : `<${rootKnopPath}/index.html> a sflo:ResourcePage, sflo:LocatedFile .`; - const currentWorkingFileLocator = renderCurrentWorkingFileLocator( - designatorPath, - workingLocalRelativePath, - repositorySourceFloatingLocator, - ); - const currentWorkingFileDeclaration = renderCurrentWorkingFileDeclaration( - designatorPath, - workingLocalRelativePath, - repositorySourceFloatingLocator, - { locatedFileIsRdfDocument: payloadIsRdfDocument }, - ); - const payloadTypes = renderPayloadArtifactTypes(payloadIsRdfDocument); - - 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> ; -${meshRootKnopLines} - sflo:hasResourcePage <_mesh/index.html> . - -${rootIdentifierBlock} -${rootKnopBlock} - -<${designatorPath}> a ${payloadTypes} ; - ${currentWorkingFileLocator} - 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:hasHistoricalState <_mesh/_inventory/_history001/_s0003> ; - sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0003> ; - sflo:nextStateOrdinal "4"^^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/_inventory/_history001/_s0003> a sflo:HistoricalState ; - sflo:stateOrdinal "3"^^xsd:nonNegativeInteger ; - sflo:previousHistoricalState <_mesh/_inventory/_history001/_s0002> ; - sflo:hasManifestation <_mesh/_inventory/_history001/_s0003/ttl> ; - sflo:locatedFileForState <_mesh/_inventory/_history001/_s0003/ttl/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/_history001/_s0003/index.html> . - -<_mesh/_inventory/_history001/_s0003/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ; - sflo:locatedFileForManifestation <_mesh/_inventory/_history001/_s0003/ttl/inventory.ttl> ; - sflo:hasResourcePage <_mesh/_inventory/_history001/_s0003/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 . - -<_mesh/_inventory/_history001/_s0003/ttl/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -${rootKnopInventoryFileBlock} - -<${knopPath}/_inventory/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . - -${currentWorkingFileDeclaration} - -<_mesh/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -${rootPageFileBlock} - -<${designatorPagePath}> a sflo:ResourcePage, sflo:LocatedFile . - -${rootKnopPageFileBlock} - -<${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 . - -<_mesh/_inventory/_history001/_s0003/index.html> a sflo:ResourcePage, sflo:LocatedFile . - -<_mesh/_inventory/_history001/_s0003/ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile . -`; -} - function renderMeshIdentifierBlock(designatorPath: string): string { const designatorPagePath = toDesignatorResourcePagePath(designatorPath); return `<${designatorPath}> sflo:hasResourcePage <${designatorPagePath}> .`; } -function renderMeshRootBlock( - meshBase: string, - knopPaths: readonly string[], -): string { - const knopLines = knopPaths.map((knopPath) => - ` sflo:hasKnop <${knopPath}> ;` - ).join("\n"); - - return `<_mesh> a sflo:SemanticMesh ; - sflo:meshBase "${meshBase}"^^xsd:anyURI ; - sflo:hasMeshMetadata <_mesh/_meta> ; - sflo:hasMeshInventory <_mesh/_inventory> ; -${knopLines} - sflo:hasResourcePage <_mesh/index.html> .`; -} - function renderMeshKnopBlockWithResourcePage(knopPath: string): string { return `<${knopPath}> a sflo:Knop ; sflo:hasWorkingKnopInventoryFile <${knopPath}/_inventory/inventory.ttl> ; @@ -895,54 +675,6 @@ function renderResourcePageLocatedFileBlock(path: string): string { return `<${path}> a sflo:ResourcePage, sflo:LocatedFile .`; } -function resolveMeshRootKnopPaths( - meshBase: string, - currentMeshInventoryTurtle: string, -): string[] { - const quads = parseWeaveShapeQuads( - meshBase, - currentMeshInventoryTurtle, - "Could not parse the current MeshInventory while preserving mesh knop entries.", - ); - const meshIri = new URL("_mesh", meshBase).href; - const knopPaths: string[] = []; - - for (const quad of quads) { - if ( - quad.subject.termType !== "NamedNode" || - quad.subject.value !== meshIri || - quad.predicate.value !== SFLO_HAS_KNOP_IRI || - quad.object.termType !== "NamedNode" - ) { - continue; - } - - const objectPath = tryToMeshPath(meshBase, quad.object.value); - if (!objectPath || knopPaths.includes(objectPath)) { - continue; - } - knopPaths.push(objectPath); - } - - return knopPaths; -} - -function tryToMeshPath(meshBase: string, iri: string): string | undefined { - if (!iri.startsWith(meshBase)) { - return undefined; - } - - const suffix = iri.slice(meshBase.length); - return suffix.length === 0 ? undefined : suffix; -} - -function toRootDesignatorPath(designatorPath: string): string { - const firstSlash = designatorPath.indexOf("/"); - return firstSlash === -1 - ? designatorPath - : designatorPath.slice(0, firstSlash); -} - function toStateSegment(stateOrdinal: number): string { return `_s${stateOrdinal.toString().padStart(4, "0")}`; } diff --git a/src/core/weave/mesh_inventory_renderers_test.ts b/src/core/weave/mesh_inventory_renderers_test.ts index 29f7877..ef63372 100644 --- a/src/core/weave/mesh_inventory_renderers_test.ts +++ b/src/core/weave/mesh_inventory_renderers_test.ts @@ -3,8 +3,10 @@ import { Parser, type Quad, type Term } from "n3"; import { WeaveInputError } from "./errors.ts"; import { renderBatchedFirstExtractedKnopWovenMeshInventoryTurtle, + renderBatchedFirstPayloadWovenMeshInventoryTurtle, renderFirstKnopWovenMeshInventoryTurtle, renderFirstPayloadWovenCurrentOnlyMeshInventoryTurtle, + renderFirstPayloadWovenMeshInventoryTurtle, renderGenericFirstExtractedKnopWovenMeshInventoryTurtle, } from "./mesh_inventory_renderers.ts"; import type { MeshInventoryProgression } from "./progression_models.ts"; @@ -325,6 +327,168 @@ Deno.test("versioned first-Knop MeshInventory rejects legacy inventory-owned pro ); }); +Deno.test("versioned first-payload MeshInventory appends owned facts with an exact carried prefix", () => { + const currentInventory = renderVersionedCurrentInventory(); + const rendered = renderFirstPayloadWovenMeshInventoryTurtle( + currentInventory, + MESH_BASE, + "term-a", + "term-a.ttl", + meshInventoryProgression, + ); + + assert(rendered.startsWith(currentInventory)); + assertTurtleGraphsEqual( + rendered, + `${currentInventory}\n${ + renderFirstPayloadFacts("term-a", "term-a.ttl") + }\n${renderNextProgressionFacts()}`, + ); +}); + +Deno.test("versioned first-payload MeshInventory returns exact bytes for a semantic no-op", () => { + const currentInventory = renderVersionedCurrentInventory(); + const alreadyWovenInventory = `${currentInventory}\n${ + renderFirstPayloadFacts("term-a", "term-a.ttl") + }\n${renderNextProgressionFacts()}`; + + assertEquals( + renderFirstPayloadWovenMeshInventoryTurtle( + alreadyWovenInventory, + MESH_BASE, + "term-a", + "term-a.ttl", + meshInventoryProgression, + ), + alreadyWovenInventory, + ); +}); + +Deno.test("versioned first-payload MeshInventory rejects a conflicting named floating locator", () => { + const locatorPath = "term-a/_knop/_sources#payload-source-repository-locator"; + const conflictingInventory = `${renderVersionedCurrentInventory()} +@base <${MESH_BASE}> . +@prefix sflo: <${SFLO}> . + + sflo:hasRepositorySourceFloatingLocator <${locatorPath}> . + +<${locatorPath}> a sflo:RepositorySourceFloatingLocator ; + sflo:sourceRepositoryUrl "https://example.test/other.git" ; + sflo:sourceRepositoryPathFromRoot "data/term-a.ttl" . +`; + + assertThrows( + () => + renderFirstPayloadWovenMeshInventoryTurtle( + conflictingInventory, + MESH_BASE, + "term-a", + "../source/data/term-a.ttl", + meshInventoryProgression, + { + repositoryUrl: "https://example.test/source.git", + repositoryPathFromRoot: "data/term-a.ttl", + }, + ), + WeaveInputError, + "sourceRepositoryUrl", + ); +}); + +Deno.test("versioned first-payload MeshInventory rejects conflicting locator kinds", () => { + const conflictingInventory = `${renderVersionedCurrentInventory()} +@base <${MESH_BASE}> . +@prefix sflo: <${SFLO}> . + + sflo:hasWorkingLocatedFile . +`; + + assertThrows( + () => + renderFirstPayloadWovenMeshInventoryTurtle( + conflictingInventory, + MESH_BASE, + "term-a", + "../source/data/term-a.ttl", + meshInventoryProgression, + { + repositoryUrl: "https://example.test/source.git", + repositoryPathFromRoot: "data/term-a.ttl", + }, + ), + WeaveInputError, + "current MeshInventory has a conflicting working locator", + ); +}); + +Deno.test("versioned first-payload 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( + () => + renderFirstPayloadWovenMeshInventoryTurtle( + legacyInventory, + MESH_BASE, + "term-a", + "term-a.ttl", + meshInventoryProgression, + ), + WeaveInputError, + "legacy inventory-owned mutable progression predicates", + ); +}); + +Deno.test("batched versioned first-payload MeshInventory accumulates targets on one carried prefix", () => { + const currentInventory = renderVersionedCurrentInventory(); + const rendered = renderBatchedFirstPayloadWovenMeshInventoryTurtle( + currentInventory, + MESH_BASE, + [ + { + designatorPath: "term-b", + workingLocalRelativePath: "term-b.md", + payloadIsRdfDocument: false, + }, + { + designatorPath: "term-a", + workingLocalRelativePath: "term-a.ttl", + }, + ], + meshInventoryProgression, + ); + + assert(rendered.startsWith(currentInventory)); + assertTurtleGraphsEqual( + rendered, + `${currentInventory}\n${ + renderFirstPayloadFacts("term-b", "term-b.md", false) + }\n${ + renderFirstPayloadFacts("term-a", "term-a.ttl") + }\n${renderNextProgressionFacts()}`, + ); + assertEquals( + countNamedNodeFact( + rendered, + "_mesh/_inventory/_history001", + `${SFLO}hasHistoricalState`, + "_mesh/_inventory/_history001/_s0002", + ), + 1, + ); +}); + function renderVersionedCurrentInventory(): string { return currentOnlyPendingInventory.replace( " sflo:hasWorkingLocatedFile <_mesh/_inventory/inventory.ttl> ;", @@ -393,6 +557,41 @@ function renderCurrentOnlyPayloadLikeFacts(designatorPath: string): string { `; } +function renderFirstPayloadFacts( + designatorPath: string, + workingLocalRelativePath: string, + payloadIsRdfDocument = true, +): string { + const knopPath = `${designatorPath}/_knop`; + const payloadTypes = payloadIsRdfDocument + ? "sflo:PayloadArtifact, sflo:DigitalArtifact, sflo:RdfDocument" + : "sflo:PayloadArtifact, sflo:DigitalArtifact"; + const locatedFileTypes = payloadIsRdfDocument + ? "sflo:LocatedFile, sflo:RdfDocument" + : "sflo:LocatedFile"; + return `@base <${MESH_BASE}> . +@prefix sflo: <${SFLO}> . + +<_mesh> sflo:hasKnop <${knopPath}> . + +<${designatorPath}> a ${payloadTypes} ; + sflo:hasWorkingLocatedFile <${workingLocalRelativePath}> ; + sflo:hasResourcePage <${designatorPath}/index.html> . + +<${knopPath}> a sflo:Knop ; + sflo:hasWorkingKnopInventoryFile <${knopPath}/_inventory/inventory.ttl> ; + sflo:hasResourcePage <${knopPath}/index.html> . + +<${knopPath}/_inventory/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument . + +<${workingLocalRelativePath}> a ${locatedFileTypes} . + +<${designatorPath}/index.html> a sflo:ResourcePage, sflo:LocatedFile . + +<${knopPath}/index.html> a sflo:ResourcePage, sflo:LocatedFile . +`; +} + function renderMeshKnopMembershipFact(designatorPath: string): string { return `@base <${MESH_BASE}> . @prefix sflo: <${SFLO}> . diff --git a/src/core/weave/source_locator_renderers.ts b/src/core/weave/source_locator_renderers.ts index d15c76f..96a30ee 100644 --- a/src/core/weave/source_locator_renderers.ts +++ b/src/core/weave/source_locator_renderers.ts @@ -1,7 +1,42 @@ import { toPayloadSourceRepositoryFloatingLocatorPath } from "../designator_segments.ts"; +import { SFLO_NAMESPACE } from "../rdf/namespaces.ts"; import type { RepositorySourceFloatingLocator } from "./source_models.ts"; import { usesMeshLocalWorkingLocatedFile } from "./working_file_paths.ts"; +export interface CurrentWorkingFileLocatorTerm { + predicateIri: string; + objectTermType: "NamedNode" | "Literal"; + objectValue: string; +} + +export function resolveCurrentWorkingFileLocatorTerm( + designatorPath: string, + workingLocalRelativePath: string, + repositorySourceFloatingLocator?: RepositorySourceFloatingLocator, +): CurrentWorkingFileLocatorTerm { + if (repositorySourceFloatingLocator !== undefined) { + return { + predicateIri: `${SFLO_NAMESPACE}hasRepositorySourceFloatingLocator`, + objectTermType: "NamedNode", + objectValue: toPayloadSourceRepositoryFloatingLocatorPath( + designatorPath, + ), + }; + } + if (usesMeshLocalWorkingLocatedFile(workingLocalRelativePath)) { + return { + predicateIri: `${SFLO_NAMESPACE}hasWorkingLocatedFile`, + objectTermType: "NamedNode", + objectValue: workingLocalRelativePath, + }; + } + return { + predicateIri: `${SFLO_NAMESPACE}workingLocalRelativePath`, + objectTermType: "Literal", + objectValue: workingLocalRelativePath, + }; +} + export function renderCurrentWorkingFileLocator( designatorPath: string, workingLocalRelativePath: string, @@ -9,16 +44,16 @@ export function renderCurrentWorkingFileLocator( options: { terminal?: "." | ";" } = {}, ): string { const terminal = options.terminal ?? ";"; - if (repositorySourceFloatingLocator !== undefined) { - return `sflo:hasRepositorySourceFloatingLocator <${ - toPayloadSourceRepositoryFloatingLocatorPath(designatorPath) - }> ${terminal}`; - } - return usesMeshLocalWorkingLocatedFile(workingLocalRelativePath) - ? `sflo:hasWorkingLocatedFile <${workingLocalRelativePath}> ${terminal}` - : `sflo:workingLocalRelativePath ${ - JSON.stringify(workingLocalRelativePath) - } ${terminal}`; + const locator = resolveCurrentWorkingFileLocatorTerm( + designatorPath, + workingLocalRelativePath, + repositorySourceFloatingLocator, + ); + const predicate = `sflo:${locator.predicateIri.slice(SFLO_NAMESPACE.length)}`; + const object = locator.objectTermType === "NamedNode" + ? `<${locator.objectValue}>` + : JSON.stringify(locator.objectValue); + return `${predicate} ${object} ${terminal}`; } export function renderCurrentWorkingFileDeclaration( diff --git a/src/core/weave/weave_test.ts b/src/core/weave/weave_test.ts index b5fa0df..4569a7c 100644 --- a/src/core/weave/weave_test.ts +++ b/src/core/weave/weave_test.ts @@ -373,8 +373,6 @@ const firstPayloadWeaveMeshInventoryTurtle = <_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> . @@ -382,8 +380,6 @@ const firstPayloadWeaveMeshInventoryTurtle = 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> . a sflo:PayloadArtifact, sflo:DigitalArtifact, sflo:RdfDocument ; @@ -983,8 +979,6 @@ const laterFirstPayloadWeaveMeshInventoryTurtle = <_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> . @@ -994,8 +988,6 @@ const laterFirstPayloadWeaveMeshInventoryTurtle = sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0002> ; sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0003> ; sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0004> ; - sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0004> ; - sflo:nextStateOrdinal "5"^^xsd:nonNegativeInteger ; sflo:hasResourcePage <_mesh/_inventory/_history001/index.html> . <_mesh/_inventory/_history001/_s0001> a sflo:HistoricalState ; @@ -1126,8 +1118,6 @@ const firstReferenceCatalogWeaveMeshInventoryTurtle = <_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> . @@ -1557,8 +1547,6 @@ function firstPayloadBatchInput( <_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> . @@ -1566,8 +1554,6 @@ function firstPayloadBatchInput( 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/_s0002> a sflo:HistoricalState ; @@ -1675,12 +1661,13 @@ function firstPayloadBatchInput( } Deno.test("planWeave batches explicit first payload targets with one merged MeshInventory progression", () => { - const plan = planWeave(firstPayloadBatchInput({ + const input = firstPayloadBatchInput({ targets: [ { designatorPath: "bob/data" }, { designatorPath: "alice/data" }, ], - })); + }); + const plan = planWeave(input); assertEquals(plan.wovenDesignatorPaths, ["alice/data", "bob/data"]); assertEquals( @@ -1699,21 +1686,40 @@ Deno.test("planWeave batches explicit first payload targets with one merged Mesh plan.updatedFiles.find((file) => file.path === "_mesh/_inventory/inventory.ttl" )?.contents ?? ""; - assertStringIncludes( - meshInventory, - "sflo:hasKnop ;\n sflo:hasKnop ;", - ); - assertStringIncludes( - meshInventory, - " a sflo:PayloadArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;", - ); - assertStringIncludes( + assert(meshInventory.startsWith(input.currentMeshInventoryTurtle)); + const meshInventoryQuads = parseWeaveShapeQuads( + input.meshBase, meshInventory, - " a sflo:PayloadArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;", + "Could not parse explicit first-payload batch MeshInventory output.", ); - assertStringIncludes( - meshInventory, - "sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0003> ;", + for (const designatorPath of ["alice/data", "bob/data"]) { + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "_mesh", + `${SFLO_NAMESPACE}hasKnop`, + `${designatorPath}/_knop`, + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + designatorPath, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + `${SFLO_NAMESPACE}PayloadArtifact`, + ), + ); + } + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "_mesh/_inventory/_history001", + `${SFLO_NAMESPACE}hasHistoricalState`, + "_mesh/_inventory/_history001/_s0003", + ), ); const meshMetadata = plan.updatedFiles.find((file) => file.path === "_mesh/_meta/meta.ttl") @@ -1729,7 +1735,8 @@ Deno.test("planWeave batches explicit first payload targets with one merged Mesh }); Deno.test("planWeave batches untargeted first payload candidates in canonical order", () => { - const plan = planWeave(firstPayloadBatchInput({})); + const input = firstPayloadBatchInput({}); + const plan = planWeave(input); assertEquals(plan.wovenDesignatorPaths, ["alice/data", "bob/data"]); assertEquals( @@ -1762,14 +1769,30 @@ Deno.test("planWeave batches untargeted first payload candidates in canonical or plan.updatedFiles.find((file) => file.path === "_mesh/_inventory/inventory.ttl" )?.contents ?? ""; - assertStringIncludes( + assert(meshInventory.startsWith(input.currentMeshInventoryTurtle)); + const meshInventoryQuads = parseWeaveShapeQuads( + input.meshBase, meshInventory, - "sflo:hasKnop ;\n sflo:hasKnop ;", + "Could not parse untargeted first-payload batch MeshInventory output.", ); for (const designatorPath of ["alice/data", "bob/data"]) { - assertStringIncludes( - meshInventory, - `<${designatorPath}> a sflo:PayloadArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;`, + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "_mesh", + `${SFLO_NAMESPACE}hasKnop`, + `${designatorPath}/_knop`, + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + designatorPath, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + `${SFLO_NAMESPACE}PayloadArtifact`, + ), ); const knopInventory = plan.updatedFiles.find((file) => file.path === `${designatorPath}/_knop/_inventory/inventory.ttl` @@ -2354,22 +2377,41 @@ Deno.test("planWeave renders a later first payload weave slice against a carried designatorPath: "alice/page-main", workingLocalRelativePath: "alice-page-main.md", }); - assertStringIncludes( - plan.updatedFiles[0]?.contents ?? "", - `sflo:hasHistoricalState <_mesh/_inventory/_history001/release-candidate> ;`, + const meshInventory = plan.updatedFiles[0]?.contents ?? ""; + const meshInventoryQuads = parseWeaveShapeQuads( + "https://semantic-flow.github.io/mesh-alice-bio/", + meshInventory, + "Could not parse later first-payload MeshInventory output.", + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "_mesh/_inventory/_history001", + `${SFLO_NAMESPACE}hasHistoricalState`, + "_mesh/_inventory/_history001/release-candidate", + ), ); assertFalse( - (plan.updatedFiles[0]?.contents ?? "").includes( - "sflo:latestHistoricalState <_mesh/_inventory/_history001/release-candidate>", + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "_mesh/_inventory/_history001", + `${SFLO_NAMESPACE}latestHistoricalState`, + "_mesh/_inventory/_history001/release-candidate", ), ); assertFalse( - (plan.updatedFiles[0]?.contents ?? "").includes( - "sflo:currentArtifactHistory <_mesh/_inventory/_history001>", + hasNamedNodeFact( + meshInventoryQuads, + "https://semantic-flow.github.io/mesh-alice-bio/", + "_mesh/_inventory", + `${SFLO_NAMESPACE}currentArtifactHistory`, + "_mesh/_inventory/_history001", ), ); assertFalse( - (plan.updatedFiles[0]?.contents ?? "").includes( + meshInventory.includes( `<_mesh/_inventory> a sflo:MeshInventory, sflo:DigitalArtifact, sflo:RdfDocument ; sflo:hasArtifactHistory <_mesh/_inventory/_history001> ; sflo:nextHistoryOrdinal`, @@ -4314,27 +4356,75 @@ Deno.test("planWeave renders an extracted term from a nested source without a ro plan.updatedFiles.find((file) => file.path === "_mesh/_inventory/inventory.ttl" )?.contents ?? ""; + const meshInventoryQuads = parseWeaveShapeQuads( + input.meshBase, + meshInventory, + "Could not parse nested-source MeshInventory output.", + ); assertEquals(plan.wovenDesignatorPaths, ["bob"]); - assertStringIncludes( - meshInventory, - ` a sflo:PayloadArtifact, sflo:DigitalArtifact, sflo:RdfDocument ; - sflo:hasWorkingLocatedFile ; - sflo:hasResourcePage .`, + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "alice/data", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + `${SFLO_NAMESPACE}PayloadArtifact`, + ), ); - assertStringIncludes( - meshInventory, - ` a sflo:Knop ; - sflo:hasWorkingKnopInventoryFile ; - sflo:hasResourcePage .`, + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "alice/data", + `${SFLO_NAMESPACE}hasWorkingLocatedFile`, + "alice-data.ttl", + ), ); - assertStringIncludes( - meshInventory, - " sflo:hasResourcePage .", + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "alice/data", + `${SFLO_NAMESPACE}hasResourcePage`, + "alice/data/index.html", + ), ); - assertStringIncludes( - meshInventory, - " sflo:hasResourcePage .", + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "alice/data/_knop", + `${SFLO_NAMESPACE}hasWorkingKnopInventoryFile`, + "alice/data/_knop/_inventory/inventory.ttl", + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "alice/data/_knop", + `${SFLO_NAMESPACE}hasResourcePage`, + "alice/data/_knop/index.html", + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "bob", + `${SFLO_NAMESPACE}hasResourcePage`, + "bob/index.html", + ), + ); + assert( + hasNamedNodeFact( + meshInventoryQuads, + input.meshBase, + "bob/_knop", + `${SFLO_NAMESPACE}hasResourcePage`, + "bob/_knop/index.html", + ), ); assert( plan.createdFiles.some((file) => file.path === "bob/index.html"), @@ -4362,8 +4452,8 @@ Deno.test("planWeave accepts extracted terms from floating repository source pay const repositorySourceFloatingLocatorPath = "alice/data/_knop/_sources#payload-source-repository-locator"; input.currentMeshInventoryTurtle = input.currentMeshInventoryTurtle.replace( - "sflo:hasWorkingLocatedFile ;", - `sflo:hasRepositorySourceFloatingLocator <${repositorySourceFloatingLocatorPath}> ;`, + "sflo:hasWorkingLocatedFile .", + `sflo:hasRepositorySourceFloatingLocator <${repositorySourceFloatingLocatorPath}> .`, ) + ` <${repositorySourceFloatingLocatorPath}> a sflo:RepositorySourceFloatingLocator ; diff --git a/tests/integration/knop_create_test.ts b/tests/integration/knop_create_test.ts index 636d5f8..95c3e5f 100644 --- a/tests/integration/knop_create_test.ts +++ b/tests/integration/knop_create_test.ts @@ -212,9 +212,18 @@ Deno.test("executeKnopCreate supports the root Knop in a later carried mesh stat "https://semantic-flow.github.io/mesh-alice-bio/_knop" ), ); - assertStringIncludes( - meshInventoryTurtle, - "sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0005> ;", + assert( + new Parser({ + baseIRI: "https://semantic-flow.github.io/mesh-alice-bio/", + }).parse(meshInventoryTurtle).some((quad: Quad) => + quad.subject.value === + "https://semantic-flow.github.io/mesh-alice-bio/_mesh/_inventory/_history001" && + quad.predicate.value === + "https://semantic-flow.github.io/sflo/ontology/hasHistoricalState" && + quad.object.termType === "NamedNode" && + quad.object.value === + "https://semantic-flow.github.io/mesh-alice-bio/_mesh/_inventory/_history001/_s0005" + ), ); }); diff --git a/tests/integration/validate_version_generate_test.ts b/tests/integration/validate_version_generate_test.ts index 16d5f4a..7f161db 100644 --- a/tests/integration/validate_version_generate_test.ts +++ b/tests/integration/validate_version_generate_test.ts @@ -1402,18 +1402,33 @@ Deno.test("executeVersion batches recursive targets through staged current state "alice/data/_history001/_s0001/ttl/alice-data.ttl", ), ); - assertStringIncludes( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + const meshInventoryTurtle = await Deno.readTextFile( + join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + ); + const meshInventoryQuads = new Parser({ + baseIRI: "https://semantic-flow.github.io/mesh-alice-bio/", + }).parse(meshInventoryTurtle) as Quad[]; + assert( + meshInventoryQuads.some((quad) => + quad.subject.value === + "https://semantic-flow.github.io/mesh-alice-bio/_mesh" && + quad.predicate.value === + "https://semantic-flow.github.io/sflo/ontology/hasKnop" && + quad.object.termType === "NamedNode" && + quad.object.value === + "https://semantic-flow.github.io/mesh-alice-bio/alice/data/_knop" ), - `sflo:hasKnop ; - sflo:hasResourcePage <_mesh/index.html> .`, ); - assertStringIncludes( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + assert( + meshInventoryQuads.some((quad) => + quad.subject.value === + "https://semantic-flow.github.io/mesh-alice-bio/_mesh/_inventory/_history001" && + quad.predicate.value === + "https://semantic-flow.github.io/sflo/ontology/hasHistoricalState" && + quad.object.termType === "NamedNode" && + quad.object.value === + "https://semantic-flow.github.io/mesh-alice-bio/_mesh/_inventory/_history001/_s0003" ), - `sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0003> ;`, ); assertStringIncludes( await Deno.readTextFile( diff --git a/tests/integration/weave_test.ts b/tests/integration/weave_test.ts index 4b94367..d2bf0f2 100644 --- a/tests/integration/weave_test.ts +++ b/tests/integration/weave_test.ts @@ -45,6 +45,23 @@ function replaceFixturePaths( ); } +function hasMeshNamedNodeFact( + quads: readonly Quad[], + subjectPath: string, + predicateIri: string, + objectPath: string, +): boolean { + const subjectIri = new URL(subjectPath, MESH_ALICE_BIO_BASE).href; + const objectIri = new URL(objectPath, MESH_ALICE_BIO_BASE).href; + return quads.some((quad) => + quad.subject.termType === "NamedNode" && + quad.subject.value === subjectIri && + quad.predicate.value === predicateIri && + quad.object.termType === "NamedNode" && + quad.object.value === objectIri + ); +} + function meshScopedSourceTermPathsFromQuads(quads: readonly Quad[]): string[] { const paths = new Set(); for (const quad of quads) { @@ -291,6 +308,84 @@ Deno.test("executeWeave batches a docs-rooted multi-pending first payload weave" ); }); +Deno.test("executeWeave first-payload locator conflict writes no files and preserves MeshInventory bytes", async () => { + const workspaceRoot = await createTestTmpDir( + "weave-weave-first-payload-conflict-", + ); + await materializeMeshAliceBioBranch( + "06-alice-bio-integrated", + workspaceRoot, + ); + const inventoryPath = join( + workspaceRoot, + "_mesh/_inventory/inventory.ttl", + ); + const currentInventory = await Deno.readTextFile(inventoryPath); + const conflictingInventory = currentInventory.replace( + "sflo:hasWorkingLocatedFile .", + "sflo:hasWorkingLocatedFile .", + ); + assert(conflictingInventory !== currentInventory); + await Deno.writeTextFile(inventoryPath, conflictingInventory); + const workspaceBefore = await snapshotWorkspaceFiles(workspaceRoot); + + await assertRejects( + () => + executeWeave({ + meshRoot: workspaceRoot, + request: { targets: [{ designatorPath: "alice/data" }] }, + }), + Error, + "current MeshInventory has a conflicting working locator", + ); + + assertEquals(await Deno.readTextFile(inventoryPath), conflictingInventory); + assertEquals(await snapshotWorkspaceFiles(workspaceRoot), workspaceBefore); +}); + +Deno.test("executeWeave rejects legacy first-payload inventory progression without writes", async () => { + const workspaceRoot = await createTestTmpDir( + "weave-weave-first-payload-legacy-progression-", + ); + await materializeMeshAliceBioBranch( + "06-alice-bio-integrated", + 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/_s0002> ; + sflo:nextStateOrdinal "3"^^xsd:nonNegativeInteger . +`; + await Deno.writeTextFile(inventoryPath, legacyInventory); + const workspaceBefore = await snapshotWorkspaceFiles(workspaceRoot); + + await assertRejects( + () => + executeWeave({ + meshRoot: workspaceRoot, + request: { targets: [{ designatorPath: "alice/data" }] }, + }), + Error, + "legacy inventory-owned mutable progression predicates", + ); + + assertEquals(await Deno.readTextFile(inventoryPath), legacyInventory); + assertEquals(await snapshotWorkspaceFiles(workspaceRoot), workspaceBefore); +}); + Deno.test("executeWeave refreshes ancestor ResourcePages when a child designator is woven", async () => { const workspaceRoot = await createTestTmpDir( "weave-weave-refresh-ancestor-pages-", @@ -486,12 +581,19 @@ Deno.test("executeWeave supports the exact root target", async () => { 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"), + const meshInventory = await Deno.readTextFile( + join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + ); + const meshInventoryQuads = new Parser({ + baseIRI: MESH_ALICE_BIO_BASE, + }).parse(meshInventory) as Quad[]; + assert( + hasMeshNamedNodeFact( + meshInventoryQuads, + "_mesh", + "https://semantic-flow.github.io/sflo/ontology/hasKnop", + "_knop", ), - `sflo:hasKnop <_knop> ; - sflo:hasResourcePage <_mesh/index.html> .`, ); assertStringIncludes( await Deno.readTextFile( @@ -689,18 +791,27 @@ Deno.test("executeWeave batches recursive targets through validate, version, and }); assertEquals(result.wovenDesignatorPaths, ["alice", "alice/data"]); - assertStringIncludes( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + const meshInventory = await Deno.readTextFile( + join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + ); + const meshInventoryQuads = new Parser({ + baseIRI: MESH_ALICE_BIO_BASE, + }).parse(meshInventory) as Quad[]; + assert( + hasMeshNamedNodeFact( + meshInventoryQuads, + "_mesh", + "https://semantic-flow.github.io/sflo/ontology/hasKnop", + "alice/data/_knop", ), - `sflo:hasKnop ; - sflo:hasResourcePage <_mesh/index.html> .`, ); - assertStringIncludes( - await Deno.readTextFile( - join(workspaceRoot, "_mesh/_inventory/inventory.ttl"), + assert( + hasMeshNamedNodeFact( + meshInventoryQuads, + "_mesh/_inventory/_history001", + "https://semantic-flow.github.io/sflo/ontology/hasHistoricalState", + "_mesh/_inventory/_history001/_s0003", ), - `sflo:hasHistoricalState <_mesh/_inventory/_history001/_s0003> ;`, ); assertStringIncludes( await Deno.readTextFile(