diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2a8d2e0..16ec698 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,9 +41,9 @@ jobs:
run: git -C dependencies/github.com/semantic-flow/mesh-alice-bio fetch origin "+refs/heads/*:refs/remotes/origin/*"
- name: Set up Deno
- uses: denoland/setup-deno@v2
+ uses: denoland/setup-deno@v2.0.4
with:
- deno-version: 2.7.12
+ deno-version: 2.7.14
- name: Check formatting
run: deno task fmt:check
@@ -60,18 +60,23 @@ jobs:
- name: Generate coverage artifacts
run: deno task coverage:lcov
+ - name: Upload lcov artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: coverage-lcov
+ path: coverage/lcov.info
+ if-no-files-found: error
+
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@v6
with:
- disable_search: true
- fail_ci_if_error: false
files: coverage/lcov.info
+ fail_ci_if_error: true
use_oidc: true
- verbose: true
- name: Upload coverage artifacts
if: ${{ always() }}
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage/
diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml
new file mode 100644
index 0000000..8fffb27
--- /dev/null
+++ b/.github/workflows/release-manual.yml
@@ -0,0 +1,415 @@
+name: Release Manual
+
+on:
+ workflow_dispatch:
+ inputs:
+ npm_publish_mode:
+ description: What to do with the assembled npm packages
+ required: true
+ default: skip
+ type: choice
+ options:
+ - skip
+ - dry-run
+ - publish
+ npm_tag:
+ description: npm dist-tag to use for npm publish or dry-run
+ required: true
+ default: latest
+ type: string
+ github_release_mode:
+ description: What to do with the GitHub Release for this version
+ required: true
+ default: skip
+ type: choice
+ options:
+ - skip
+ - draft
+ - publish
+
+jobs:
+ build-binaries:
+ name: Build Binaries (${{ matrix.label }})
+ runs-on: ${{ matrix.runs_on }}
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - label: linux-x64
+ runs_on: ubuntu-latest
+ expected_os: linux
+ expected_arch: x86_64
+ executable: weave
+ - label: windows-x64
+ runs_on: windows-latest
+ expected_os: windows
+ expected_arch: x86_64
+ executable: weave.exe
+ - label: macos-x64
+ runs_on: macos-15-intel
+ expected_os: darwin
+ expected_arch: x86_64
+ executable: weave
+ - label: macos-arm64
+ runs_on: macos-latest
+ expected_os: darwin
+ expected_arch: aarch64
+ executable: weave
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2.0.4
+ with:
+ deno-version: 2.7.14
+
+ - name: Verify native runner architecture
+ env:
+ EXPECTED_OS: ${{ matrix.expected_os }}
+ EXPECTED_ARCH: ${{ matrix.expected_arch }}
+ run: |
+ deno eval '
+ const os = Deno.env.get("EXPECTED_OS");
+ const arch = Deno.env.get("EXPECTED_ARCH");
+ if (Deno.build.os !== os || Deno.build.arch !== arch) {
+ console.error(`Expected ${os}/${arch}, got ${Deno.build.os}/${Deno.build.arch}`);
+ Deno.exit(1);
+ }
+ console.log(`Runner verified: ${Deno.build.os}/${Deno.build.arch}`);
+ '
+
+ - name: Build native binary
+ run: deno task build:binaries -- --platform ${{ matrix.label }} --out-dir .test-tmp/release-binaries
+
+ - name: Smoke native binary
+ run: ./.test-tmp/release-binaries/${{ matrix.label }}/${{ matrix.executable }} --version
+
+ - name: Package release archive
+ run: deno task package:binaries -- --platform ${{ matrix.label }} --build-dir .test-tmp/release-binaries --out-dir .test-tmp/release-assets/${{ matrix.label }}
+
+ - name: Copy bundle metadata into release assets
+ env:
+ PLATFORM_LABEL: ${{ matrix.label }}
+ run: |
+ deno eval '
+ const label = Deno.env.get("PLATFORM_LABEL");
+ if (label === undefined) {
+ throw new Error("PLATFORM_LABEL is required");
+ }
+ await Deno.copyFile(
+ `.test-tmp/release-binaries/${label}/bundle-metadata.json`,
+ `.test-tmp/release-assets/${label}/bundle-metadata.json`,
+ );
+ '
+
+ - name: Upload binary build artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: weave-binary-${{ matrix.label }}
+ path: .test-tmp/release-binaries/${{ matrix.label }}/**
+ if-no-files-found: error
+ compression-level: 0
+
+ - name: Upload release asset artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: weave-release-${{ matrix.label }}
+ path: .test-tmp/release-assets/${{ matrix.label }}/**
+ if-no-files-found: error
+ compression-level: 0
+
+ assemble-npm-packages:
+ name: Assemble npm Packages
+ needs: build-binaries
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2.0.4
+ with:
+ deno-version: 2.7.14
+
+ - name: Download binary build artifacts
+ uses: actions/download-artifact@v8
+ with:
+ pattern: weave-binary-*
+ path: .test-tmp/downloaded-binaries
+ merge-multiple: false
+
+ - name: Prepare downloaded binary build directory
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ shopt -s nullglob
+ artifact_dirs=(.test-tmp/downloaded-binaries/weave-binary-*)
+ if [ "${#artifact_dirs[@]}" -eq 0 ]; then
+ echo "No binary build artifacts were downloaded"
+ exit 1
+ fi
+
+ mkdir -p .test-tmp/release-binaries
+ for artifact_dir in "${artifact_dirs[@]}"; do
+ label="${artifact_dir##*/weave-binary-}"
+ mkdir -p ".test-tmp/release-binaries/$label"
+ cp -R "$artifact_dir"/. ".test-tmp/release-binaries/$label/"
+ done
+
+ - name: Assemble npm packages
+ run: deno task assemble:npm-packages -- --build-dir .test-tmp/release-binaries --out-dir .test-tmp/npm-packages/release
+
+ - name: Upload npm package assembly artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: weave-npm-packages
+ path: .test-tmp/npm-packages/release/**
+ if-no-files-found: error
+ compression-level: 0
+
+ smoke-npm-install:
+ name: Smoke npm Install (${{ matrix.label }})
+ needs: assemble-npm-packages
+ runs-on: ${{ matrix.runs_on }}
+ timeout-minutes: 10
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - label: linux-x64
+ runs_on: ubuntu-latest
+ expected_os: linux
+ expected_arch: x86_64
+ - label: windows-x64
+ runs_on: windows-latest
+ expected_os: windows
+ expected_arch: x86_64
+ - label: macos-x64
+ runs_on: macos-15-intel
+ expected_os: darwin
+ expected_arch: x86_64
+ - label: macos-arm64
+ runs_on: macos-latest
+ expected_os: darwin
+ expected_arch: aarch64
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2.0.4
+ with:
+ deno-version: 2.7.14
+
+ - name: Set up Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ package-manager-cache: false
+
+ - name: Verify native runner architecture
+ env:
+ EXPECTED_OS: ${{ matrix.expected_os }}
+ EXPECTED_ARCH: ${{ matrix.expected_arch }}
+ run: |
+ deno eval '
+ const os = Deno.env.get("EXPECTED_OS");
+ const arch = Deno.env.get("EXPECTED_ARCH");
+ if (Deno.build.os !== os || Deno.build.arch !== arch) {
+ console.error(`Expected ${os}/${arch}, got ${Deno.build.os}/${Deno.build.arch}`);
+ Deno.exit(1);
+ }
+ console.log(`Runner verified: ${Deno.build.os}/${Deno.build.arch}`);
+ '
+
+ - name: Download npm package assembly artifact
+ uses: actions/download-artifact@v8
+ with:
+ name: weave-npm-packages
+ path: .test-tmp/downloaded-npm-packages
+
+ - name: Smoke npm install
+ run: deno task smoke:npm-install -- --input-dir .test-tmp/downloaded-npm-packages --work-dir .test-tmp/npm-install-smoke --npm-bin npm
+
+ publish-npm-packages:
+ name: Publish npm Packages
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.npm_publish_mode != 'skip' }}
+ needs: smoke-npm-install
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+ id-token: write
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2.0.4
+ with:
+ deno-version: 2.7.14
+
+ - name: Set up Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ registry-url: https://registry.npmjs.org
+ package-manager-cache: false
+
+ - name: Download npm package assembly artifact
+ uses: actions/download-artifact@v8
+ with:
+ name: weave-npm-packages
+ path: .test-tmp/downloaded-npm-packages
+
+ - name: Publish npm packages
+ shell: bash
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: |
+ set -euo pipefail
+
+ args=(
+ --input-dir .test-tmp/downloaded-npm-packages
+ --npm-bin npm
+ --tag "${{ inputs.npm_tag }}"
+ )
+ if [ "${{ inputs.npm_publish_mode }}" = "dry-run" ]; then
+ args+=(--dry-run)
+ else
+ args+=(--provenance)
+ fi
+
+ deno task publish:npm-packages -- "${args[@]}"
+
+ manage-github-release:
+ name: Manage GitHub Release
+ if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.github_release_mode != 'skip' && needs.smoke-npm-install.result == 'success' && (inputs.npm_publish_mode == 'skip' || needs.publish-npm-packages.result == 'success') }}
+ needs:
+ - smoke-npm-install
+ - publish-npm-packages
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: write
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2.0.4
+ with:
+ deno-version: 2.7.14
+
+ - name: Download release asset artifacts
+ uses: actions/download-artifact@v8
+ with:
+ pattern: weave-release-*
+ path: .test-tmp/downloaded-release-assets
+ merge-multiple: false
+
+ - name: Prepare release metadata
+ id: prepare-release
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ mapfile -t metadata_files < <(find .test-tmp/downloaded-release-assets -mindepth 2 -maxdepth 2 -type f -name bundle-metadata.json | sort)
+ if [ "${#metadata_files[@]}" -eq 0 ]; then
+ echo "No bundle-metadata.json files were downloaded"
+ exit 1
+ fi
+
+ version=$(deno eval 'const versions = await Promise.all(Deno.args.map(async (path) => JSON.parse(await Deno.readTextFile(path)).version)); const unique = [...new Set(versions)].sort(); if (unique.length !== 1) { throw new Error(`Expected one bundled release version, got: ${unique.join(", ")}`); } console.log(unique[0]);' -- "${metadata_files[@]}")
+ tag="v${version}"
+ title="${tag}"
+ notes_source="documentation/notes/release-notes.v${version}.md"
+ if [ ! -f "$notes_source" ]; then
+ echo "Release notes file not found: $notes_source"
+ exit 1
+ fi
+
+ notes_body="$RUNNER_TEMP/release-notes-${tag}.md"
+ awk '
+ BEGIN { in_frontmatter = 0; frontmatter_done = 0 }
+ NR == 1 && $0 == "---" { in_frontmatter = 1; next }
+ in_frontmatter && $0 == "---" { in_frontmatter = 0; frontmatter_done = 1; next }
+ !in_frontmatter && frontmatter_done { print }
+ ' "$notes_source" > "$notes_body"
+
+ if [ ! -s "$notes_body" ]; then
+ echo "Release notes body is empty after stripping frontmatter: $notes_source"
+ exit 1
+ fi
+
+ assets_file="$RUNNER_TEMP/release-assets-${tag}.txt"
+ find .test-tmp/downloaded-release-assets -mindepth 2 -maxdepth 2 -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.sha256' \) | sort > "$assets_file"
+ if [ ! -s "$assets_file" ]; then
+ echo "No release archives or checksum assets were found"
+ exit 1
+ fi
+
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
+ echo "title=$title" >> "$GITHUB_OUTPUT"
+ echo "notes_body=$notes_body" >> "$GITHUB_OUTPUT"
+ echo "assets_file=$assets_file" >> "$GITHUB_OUTPUT"
+
+ - name: Create or update GitHub Release
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ RELEASE_ASSETS_FILE: ${{ steps.prepare-release.outputs.assets_file }}
+ RELEASE_MODE: ${{ inputs.github_release_mode }}
+ RELEASE_NOTES_BODY: ${{ steps.prepare-release.outputs.notes_body }}
+ RELEASE_TAG: ${{ steps.prepare-release.outputs.tag }}
+ RELEASE_TITLE: ${{ steps.prepare-release.outputs.title }}
+ run: |
+ set -euo pipefail
+
+ mapfile -t assets < "$RELEASE_ASSETS_FILE"
+ if [ "${#assets[@]}" -eq 0 ]; then
+ echo "No release assets were prepared"
+ exit 1
+ fi
+
+ if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
+ gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber
+
+ edit_args=(
+ "$RELEASE_TAG"
+ --title "$RELEASE_TITLE"
+ --notes-file "$RELEASE_NOTES_BODY"
+ --target "$GITHUB_SHA"
+ )
+ if [ "$RELEASE_MODE" = "draft" ]; then
+ edit_args+=(--draft)
+ else
+ edit_args+=(--draft=false --latest)
+ fi
+ gh release edit "${edit_args[@]}"
+ else
+ create_args=(
+ "$RELEASE_TAG"
+ --title "$RELEASE_TITLE"
+ --notes-file "$RELEASE_NOTES_BODY"
+ --target "$GITHUB_SHA"
+ )
+ if [ "$RELEASE_MODE" = "draft" ]; then
+ create_args+=(--draft)
+ else
+ create_args+=(--latest)
+ fi
+ create_args+=("${assets[@]}")
+ gh release create "${create_args[@]}"
+ fi
diff --git a/.gitignore b/.gitignore
index ba17753..1f9421b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ node_modules
.test-tmp
.weave
coverage/
+dist/
# Dendron
.dendron.*
diff --git a/defaults/application.ttl b/defaults/application.ttl
index ba7bab3..b5aa8f7 100644
--- a/defaults/application.ttl
+++ b/defaults/application.ttl
@@ -16,30 +16,17 @@
], [
a sfcfg:ArtifactRolePolicy ;
sfcfg:hasArtifactRole sfcfg:artifactRole_meshInventory ;
- sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_required
+ sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly
], [
a sfcfg:ArtifactRolePolicy ;
sfcfg:hasArtifactRole sfcfg:artifactRole_knopInventory ;
- sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_slimHistory
+ sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly
], [
a sfcfg:ArtifactRolePolicy ;
sfcfg:hasArtifactRole sfcfg:artifactRole_runtimeMeta ;
sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly
] ;
- sfcfg:hasDefaultResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_suppress ;
- sfcfg:hasResourcePageGenerationDefault [
- a sfcfg:ArtifactRolePolicy ;
- sfcfg:hasArtifactRole sfcfg:artifactRole_payload ;
- sfcfg:hasResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_generate
- ], [
- a sfcfg:ArtifactRolePolicy ;
- sfcfg:hasArtifactRole sfcfg:artifactRole_meshInventory ;
- sfcfg:hasResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_generate
- ], [
- a sfcfg:ArtifactRolePolicy ;
- sfcfg:hasArtifactRole sfcfg:artifactRole_config ;
- sfcfg:hasResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_suppress
- ] ;
+ sfcfg:hasDefaultResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_generate ;
sfcfg:hasResourcePageRegenerationConfigPolicy sfcfg:resourcePageRegenerationConfigPolicy_configAtTheTime ;
sfcfg:hasHistoryNamingPolicy sfcfg:historyNamingPolicy_ordinal ;
sfcfg:hasStateNamingPolicy sfcfg:stateNamingPolicy_ordinal ;
diff --git a/defaults/config-resolution.ttl b/defaults/config-resolution.ttl
index bf3232e..673e8b4 100644
--- a/defaults/config-resolution.ttl
+++ b/defaults/config-resolution.ttl
@@ -1,12 +1,16 @@
@base .
@prefix sflo: .
@prefix sfcfg: .
+@prefix rdfs: .
@prefix xsd: .
a sfcfg:ConfigResolutionConfig, sfcfg:ConfigArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sfcfg:hasConfigPrecedenceProfile ;
+ sfcfg:hasConfigMergeProfile ;
sfcfg:hasUnknownConfigTermPolicy sfcfg:unknownConfigTermPolicy_reject ;
sfcfg:hasConfigCyclePolicy sfcfg:configCyclePolicy_reject ;
sfcfg:hasConfigReferencePolicy sfcfg:configReferencePolicy_pinnedOnly ;
+ sfcfg:hasOperationRequestOverridePolicy sfcfg:operationRequestOverridePolicy_warnAndApply ;
sfcfg:hasResolvedConfigCachePolicy sfcfg:resolvedConfigCachePolicy_cacheForProcess ;
sfcfg:hasPortableResolverHintPolicy sfcfg:portableResolverHintPolicy_honorWithinTrustedBoundary ;
sfcfg:maxConfigReferenceDepth "8"^^xsd:nonNegativeInteger ;
@@ -30,16 +34,36 @@
a sfcfg:ConfigLayer ;
sfcfg:hasConfigLayerRole sfcfg:configLayerRole_meshLocal ;
sfcfg:layerOrder "50"^^xsd:nonNegativeInteger
+ ], [
+ a sfcfg:ConfigLayer ;
+ sfcfg:hasConfigLayerRole sfcfg:configLayerRole_meshInheritable ;
+ sfcfg:layerOrder "55"^^xsd:nonNegativeInteger
], [
a sfcfg:ConfigLayer ;
sfcfg:hasConfigLayerRole sfcfg:configLayerRole_knopInherited ;
sfcfg:layerOrder "60"^^xsd:nonNegativeInteger
+ ], [
+ a sfcfg:ConfigLayer ;
+ sfcfg:hasConfigLayerRole sfcfg:configLayerRole_reusableConfig ;
+ sfcfg:layerOrder "65"^^xsd:nonNegativeInteger ;
+ rdfs:comment "Provenance role for reusable config sources. Merge happens at the attachment point that referenced the reusable config, not as one universal global layer."
], [
a sfcfg:ConfigLayer ;
sfcfg:hasConfigLayerRole sfcfg:configLayerRole_knopLocal ;
sfcfg:layerOrder "70"^^xsd:nonNegativeInteger
+ ], [
+ a sfcfg:ConfigLayer ;
+ sfcfg:hasConfigLayerRole sfcfg:configLayerRole_knopInheritable ;
+ sfcfg:layerOrder "75"^^xsd:nonNegativeInteger ;
+ rdfs:comment "Authored outbound offer role for Knop inheritable config. It is projected into descendant scopes as Knop inherited config unless propagation policy stops it."
], [
a sfcfg:ConfigLayer ;
sfcfg:hasConfigLayerRole sfcfg:configLayerRole_commandOverride ;
sfcfg:layerOrder "90"^^xsd:nonNegativeInteger
] .
+
+ a sfcfg:ConfigPrecedenceProfile ;
+ rdfs:label "Weave default config precedence profile" .
+
+ a sfcfg:ConfigMergeProfile ;
+ rdfs:label "Weave default config merge profile" .
diff --git a/deno.json b/deno.json
index 38b9900..c6f7115 100644
--- a/deno.json
+++ b/deno.json
@@ -1,10 +1,18 @@
{
+ "version": "0.1.0",
"tasks": {
"dev:root": "deno run --allow-read --allow-write --allow-env src/main.ts",
- "fmt": "deno fmt deno.json src tests",
- "fmt:check": "deno fmt --check deno.json src tests",
- "lint": "deno lint src tests",
- "check": "deno check src/**/*.ts tests/**/*.ts",
+ "bump:version": "deno run --allow-read --allow-write scripts/bump-version.ts",
+ "build:binaries": "deno run --allow-read --allow-write --allow-run=deno scripts/build-binaries.ts",
+ "package:binaries": "deno run --allow-read --allow-write scripts/package-binaries.ts",
+ "assemble:npm-packages": "deno run --allow-read --allow-write scripts/assemble-npm-packages.ts",
+ "smoke:npm-install": "deno run --allow-read --allow-write --allow-run --allow-env scripts/smoke-npm-install.ts",
+ "publish:npm-packages": "deno run --allow-read --allow-write --allow-run --allow-env scripts/publish-npm-packages.ts",
+ "fixture:ladder": "deno run --allow-read --allow-write --allow-run=git,deno --allow-env scripts/fixture-ladder.ts",
+ "fmt": "deno fmt deno.json scripts src tests",
+ "fmt:check": "deno fmt --check deno.json scripts src tests",
+ "lint": "deno lint scripts src tests",
+ "check": "deno check scripts/**/*.ts src/**/*.ts tests/**/*.ts",
"test": "WEAVE_GENERATED_AT=2026-05-03T00:00:00.000Z deno test --preload=tests/support/test_tmp_harness.ts --allow-read --allow-write --allow-run=git,deno --allow-env src tests",
"test:coverage": "WEAVE_GENERATED_AT=2026-05-03T00:00:00.000Z deno test --preload=tests/support/test_tmp_harness.ts --allow-read --allow-write --allow-run=git,deno --allow-env --coverage=coverage src tests",
"coverage:lcov": "deno coverage --lcov --output=coverage/lcov.info --exclude='^file:.*/dependencies/' --exclude='^file:.*/tests/' coverage",
@@ -22,7 +30,8 @@
},
"exclude": [
".test-tmp/**",
- "documentation/notes/**"
+ "documentation/notes/**",
+ "**/*.md"
],
"fmt": {
"proseWrap": "preserve",
diff --git a/documentation/notes/dev.release-runbook.md b/documentation/notes/dev.release-runbook.md
index 5f253ef..20c6d91 100644
--- a/documentation/notes/dev.release-runbook.md
+++ b/documentation/notes/dev.release-runbook.md
@@ -10,76 +10,155 @@ created: 1778685955558
Current developer-facing release process for Weave.
-Weave is still pre-package and pre-v1. The current release path is a reviewed source checkpoint: tag a commit, create a GitHub Release from that tag, and use the release notes in `documentation/notes/release-notes.v.md` as the public summary. This is intentionally smaller than Kato's release pipeline because Weave does not yet build native binaries, assemble npm packages, publish to JSR/npm, or carry durable in-repo version metadata.
+Weave is moving from the `v0.0.2` source-checkpoint release model toward the first packaged `v0.1.0` release. This runbook documents the current packaged release path: durable root version metadata, `weave --version`, release-note stubs, native binary builds, binary archive/checksum packaging, npm package assembly, npm install smoke tests, ordered npm dry-run/publish support, and a manual GitHub Actions release workflow.
## Current Model
-- The release version is represented by the Git tag, for example `v0.0.2`.
+- The authored release version lives in root `deno.json` as `version`.
+- Runtime version reporting uses the same root version: `weave --version`.
+- Use `deno task bump:version` to change the root version and create or verify `documentation/notes/release-notes.v.md`.
- Release notes live at `documentation/notes/release-notes.v.md`.
-- GitHub Actions CI and `deno task ci` are the intended quality gates, but `v0.0.2` is allowed as an explicit checkpoint exception while the full CI/CD task restores a real release gate for `v0.1.0`.
-- There is no automated release workflow yet. Create the GitHub Release manually or with `gh release create`.
-- There is no package publication step yet.
-- There is no `weave --version` or package version file yet, so do not claim runtime version reporting until a later CI/CD task adds it.
+- `deno task build:binaries` compiles native `weave` binaries and writes per-platform `bundle-metadata.json`.
+- `deno task package:binaries` turns built platform directories into `.tar.gz` or `.zip` archives plus `.sha256` files.
+- `deno task assemble:npm-packages` creates the npm wrapper package and selected platform packages from built platform directories, including package `publishConfig` metadata and an aggregate `npm-packages-metadata.json` manifest.
+- `deno task smoke:npm-install` reads `npm-packages-metadata.json`, runs `npm pack`, installs the wrapper and host platform package tarballs into a temporary project, and verifies `weave --version`.
+- `deno task publish:npm-packages` reads `npm-packages-metadata.json` and publishes platform packages before the wrapper package, with dry-run, dist-tag, and provenance options.
+- `.github/workflows/release-manual.yml` is the primary release path for packaged releases. It builds native binaries on native Linux, Windows, macOS x64, and macOS arm64 runners; packages release archives/checksums; assembles npm packages; smoke-tests npm installation on native runners; optionally dry-runs or publishes npm packages; and optionally drafts or publishes the GitHub Release.
+- GitHub Actions CI and `deno task ci` are the intended quality gates. Fixture tests that inspect branch-published generated output read explicit Git refs; deterministic source assets are also checked from source-bearing refs so local preview checkouts such as `gh-pages` do not change the test meaning.
+- The manual release workflow defaults to no npm publication and no GitHub Release mutation. Rehearsal and publication both require explicit workflow inputs.
## Pre-Release
-1. Confirm the release scope and version. For the current checkpoint, use `v0.0.2`.
-2. Update `documentation/notes/release-notes.v.md`. Do not leave the note empty.
-3. Make sure the release notes describe what is actually in the release commit, not work planned immediately afterward.
-4. Run the local quality gate when feasible, or record the known failure if the checkpoint is intentionally proceeding:
+1. Confirm the release scope and version. For the first full packaged release target, use `v0.1.0` unless the task scope changes.
+2. Bump or verify the release version:
+
+```bash
+deno task bump:version -- --version 0.1.0
+```
+
+Use `--patch`, `--minor`, or `--major` instead when advancing from an existing release.
+
+3. Fill `documentation/notes/release-notes.v.md`. Do not leave generated TODO placeholders in release notes for a real release.
+4. Make sure the release notes describe what is actually in the release commit, not work planned immediately afterward.
+5. Run the current focused release-tooling checks:
+
+```bash
+deno task fmt:check
+deno task lint
+deno task check
+deno test --allow-read --allow-write tests/scripts/bump_version_test.ts tests/scripts/release_metadata_test.ts tests/scripts/package_binaries_test.ts tests/scripts/assemble_npm_packages_test.ts tests/scripts/publish_npm_packages_test.ts tests/scripts/smoke_npm_install_test.ts src/version_test.ts
+deno test --allow-read --allow-write --allow-run=deno --allow-env tests/e2e/weave_cli_test.ts --filter "weave --version reports"
+```
+
+6. Run the full quality gate when feasible:
```bash
deno task ci
```
-5. Inspect the worktree:
+If `deno task ci` fails, record the failing command and reason explicitly in the release notes and do not call the release CI-clean.
+
+7. Build at least the local platform binary as a release-script smoke test:
+
+```bash
+deno task build:binaries -- --platform linux-x64 --out-dir /tmp/weave-binaries
+deno task package:binaries -- --platform linux-x64 --build-dir /tmp/weave-binaries --out-dir /tmp/weave-release
+deno task assemble:npm-packages -- --platform linux-x64 --build-dir /tmp/weave-binaries --out-dir /tmp/weave-npm/node_modules
+deno task smoke:npm-install -- --input-dir /tmp/weave-npm/node_modules --work-dir /tmp/weave-npm-smoke
+/tmp/weave-binaries/linux-x64/weave --version
+ls /tmp/weave-release
+```
+
+Adjust the platform label to match the runner when validating elsewhere. Supported labels are `linux-x64`, `windows-x64`, `macos-x64`, and `macos-arm64`.
+
+8. Inspect the worktree:
```bash
git status --short
git diff --check
```
-6. Commit the release preparation changes with a message that names the release, for example:
+9. Commit the release preparation changes with a message that names the release, for example:
```text
-docs: prepare v0.0.2 release checkpoint
+release: prepare v0.1.0 packaging groundwork
-- add Weave source-release runbook
-- add v0.0.2 release notes
-- record the pre-config-synthesis checkpoint scope
+- add canonical version metadata and version reporting
+- add release-note bump tooling
+- add native binary build and packaging scripts
+- add local npm package assembly
+- add local npm install smoke testing
```
-7. Push the branch. Prefer a green GitHub CI run before tagging, but if this is an explicit checkpoint exception, make sure the release notes do not claim green validation.
+10. Push the branch. Prefer a green GitHub CI run before tagging, but if this is an explicit checkpoint exception, make sure the release notes do not claim green validation.
## Release
-Use a reviewed commit on `main`. Green CI is preferred; for a deliberate source-checkpoint exception, the GitHub Release notes must say that the quality gate is known follow-up work.
+Use a reviewed commit on `main`. Green CI is preferred; for a deliberate checkpoint exception, the GitHub Release notes must say that the quality gate is known follow-up work.
-Create and push the tag:
+The primary release path is the manual GitHub Actions workflow:
-```bash
-git tag -a v0.0.2 -m v0.0.2
-git push origin v0.0.2
+1. Open the `Release Manual` workflow on the release commit.
+2. Run a rehearsal first:
+
+```text
+npm_publish_mode: dry-run
+npm_tag: latest
+github_release_mode: draft
```
-Create the GitHub Release. The release body should be the release notes content without the Dendron frontmatter. Either paste the body through the GitHub UI, or use a temporary body file and `gh`:
+3. Inspect the workflow artifacts, npm dry-run logs, draft GitHub Release body, uploaded archives, and checksum assets.
+4. If the rehearsal is good, rerun the same workflow on the same commit for publication:
-```bash
-sed '1,/^---$/d; 1,/^---$/d' documentation/notes/release-notes.v0.0.2.md > /tmp/weave-release-notes.v0.0.2.md
-gh release create v0.0.2 --title v0.0.2 --notes-file /tmp/weave-release-notes.v0.0.2.md
+```text
+npm_publish_mode: publish
+npm_tag: latest
+github_release_mode: publish
```
-For a checkpoint that should be reviewed before publication, create the release as a draft:
+The workflow derives the release tag from downloaded bundle metadata. Do not add a free-form tag input unless the workflow also proves the tag matches root `deno.json`, binary bundle metadata, npm package versions, and release notes.
+
+The workflow strips Dendron frontmatter from `documentation/notes/release-notes.v.md` and fails if the stripped body is empty. The workflow creates or updates the GitHub Release, uploads `.tar.gz`/`.zip` archives and `.sha256` files, and sets the release target to the workflow commit.
+
+The npm publish job publishes platform packages before the wrapper package. Real publish runs use `--provenance` and `NODE_AUTH_TOKEN` from the `NPM_TOKEN` secret. Confirm the npm package scope, package ownership, and token/trusted-publishing settings before the first real publish.
+
+### Manual Fallback
+
+Use the script-by-script path only for local debugging or emergency release repair. Build and package every supported platform before claiming a full release.
```bash
-gh release create v0.0.2 --title v0.0.2 --draft --notes-file /tmp/weave-release-notes.v0.0.2.md
+deno task build:binaries -- --platform linux-x64 --out-dir /tmp/weave-binaries
+deno task package:binaries -- --platform linux-x64 --build-dir /tmp/weave-binaries --out-dir /tmp/weave-release
+deno task assemble:npm-packages -- --platform linux-x64 --build-dir /tmp/weave-binaries --out-dir /tmp/weave-npm/node_modules
+deno task smoke:npm-install -- --input-dir /tmp/weave-npm/node_modules --work-dir /tmp/weave-npm-smoke
+deno task publish:npm-packages -- --input-dir /tmp/weave-npm/node_modules --dry-run --tag latest
```
+Manual GitHub Release creation should still use the release notes body without Dendron frontmatter. Prefer the workflow because it already validates version consistency and uploads the expected asset set.
+
## Post-Release
- Confirm the GitHub Release exists and points at the intended commit.
-- Confirm the release body matches `documentation/notes/release-notes.v0.0.2.md` after frontmatter removal.
-- Confirm no binary/package assets are expected for this release.
+- Confirm the release body matches `documentation/notes/release-notes.v.md` after frontmatter removal.
+- Confirm any uploaded binary archives have matching `.sha256` files and match the release notes.
+- Confirm npm packages exist under the expected version and dist-tag:
+
+```bash
+npm view @semantic-flow/weave@0.1.0 version dist-tags
+npm view @semantic-flow/weave-linux-x64@0.1.0 version dist-tags
+npm view @semantic-flow/weave-windows-x64@0.1.0 version dist-tags
+npm view @semantic-flow/weave-macos-x64@0.1.0 version dist-tags
+npm view @semantic-flow/weave-macos-arm64@0.1.0 version dist-tags
+```
+
+- Confirm a normal npm install works on at least one machine:
+
+```bash
+npm install -g @semantic-flow/weave@0.1.0
+weave --version
+npm uninstall -g @semantic-flow/weave
+```
+
- If another clone needs the new tag, run:
```bash
@@ -88,23 +167,19 @@ git fetch --tags origin
## Current Caveats
-- Weave does not yet have a release workflow like Kato's `Release Manual`.
-- Weave does not yet publish CLI binaries or npm/JSR packages.
-- Weave does not yet have a version bump task.
-- Weave does not yet have a runtime `--version` surface.
+- The `Release Manual` workflow exists, but still needs a real rehearsal run on GitHub Actions before it should be considered battle-tested.
+- The workflow uses `macos-15-intel` for macOS x64 and `macos-latest` for macOS arm64. If GitHub-hosted runner labels change, update the workflow before release.
+- The workflow uses `NPM_TOKEN` plus npm provenance for real package publication. Confirm npm organization settings before first publish.
+- Local fixture tests should be ref-based rather than checkout-state-based. If a fixture test fails only when a sibling fixture checkout is on a preview/publication branch, treat that as a test coupling bug before treating it as release code drift.
- Release notes are Dendron notes, so any GitHub Release body must omit frontmatter.
-## Future CI/CD Task
+## Future Release Workflow
-Create a dedicated Weave CI/CD task before treating releases as distributable product releases. That task should decide:
+Before treating `v0.1.0` as a distributable product release, finish the remaining release-workflow pieces tracked in [[wd.task.2026.2026-05-13-full-ci-cd]]:
-- where durable version metadata lives
-- whether `weave --version` is supported and how it reads version metadata
-- whether releases publish source-only checkpoints, Deno tasks, JSR packages, npm wrappers, native binaries, or some combination
-- whether to add a `deno task bump:version`
-- whether GitHub Releases are created by a manual workflow
-- whether release notes are transformed automatically from Dendron notes
-- what smoke tests prove a packaged CLI actually runs
-- how fixture repositories and Accord manifests are validated before a release
+- run the manual workflow in rehearsal mode
+- inspect the generated archives/checksums and draft release
+- decide whether the known fixture/config test failures block `v0.1.0`
+- publish only after npm scope ownership and registry credentials are confirmed
-Until that task lands, keep releases explicit and boring: reviewed commit, annotated tag, GitHub Release, no packaging claims, and no false CI claims.
+Until the rehearsal run is reviewed, keep releases explicit and boring: reviewed commit, authored version, release notes, manual workflow rehearsal, no false CI claims, and no real npm publish without registry confirmation.
diff --git a/documentation/notes/release-notes.v0.1.0.md b/documentation/notes/release-notes.v0.1.0.md
new file mode 100644
index 0000000..25a6087
--- /dev/null
+++ b/documentation/notes/release-notes.v0.1.0.md
@@ -0,0 +1,71 @@
+---
+id: 42f757dc89584d51810974bb8dede8a0
+title: 'release notes v0.1.0'
+desc: ''
+updated: 1778730578767
+created: 1778730578767
+---
+
+## Summary
+
+`v0.1.0` is the first package-oriented Weave release. It turns the earlier source-checkpoint work into an installable CLI release path, while carrying the current Semantic Flow mesh-generation runtime through Alice Bio, Sidecar Fantasy Rules, and Branch-Published Fantasy Rules fixture coverage.
+
+This release is still early: the daemon and web surfaces are not packaged as supported products, and parts of config resolution remain design/runtime follow-up work. The supported surface is the local `weave` CLI.
+
+## Highlights
+
+- `weave --version` now reports the canonical version from root `deno.json`.
+- Native binary build and archive packaging scripts are in place for Linux x64, Windows x64, macOS x64, and macOS arm64.
+- npm package assembly, local npm-install smoke testing, and ordered npm dry-run/publish scripting are in place for `@semantic-flow/weave` plus platform packages.
+- A manual GitHub Actions release workflow can build native binaries, package archives/checksums, assemble npm packages, smoke-test installs, optionally publish npm packages, and optionally create or update the GitHub Release.
+- Branch-published GitHub Pages mesh generation is covered by the `mesh-branch-fantasy-rules` fixture, including source-clean `main`, generated `gh-pages`, repository source provenance, extraction source registries, all-term ResourcePages, and representative current-mode references.
+- ResourcePages now surface source provenance, grouped references, direct RDF properties, source registries, raw RDF panels, history/state views, and branch-published release-state pages more coherently.
+- Config/runtime groundwork now includes policy-valued history/page defaults, Weave default profile RDF, `HostLocalOperationalConfig`, `ConfigResolutionConfig`, `ResolvedConfig`, inherited config propagation primitives, and current-only support-artifact history policy slices.
+
+## Breaking Or Changed Behavior
+
+- The canonical Semantic Flow core namespace is `https://semantic-flow.github.io/sflo/ontology/`; stale `semantic-flow-ontology` expectations are retired.
+- Config vocabulary uses flat namespace-local policy individuals such as `sfcfg:historyTrackingPolicy_currentOnly`; old slash-shaped and boolean config terms such as `generateResourcePages` / `createHistoricalStatesOnWeave` are retired.
+- `LocalConfig` has been replaced by `HostLocalOperationalConfig` for host-local operational trust policy.
+- Extraction provenance and repository source provenance live in Knop-owned `_knop/_sources/sources.ttl` registries; mesh config is not used as a provenance bucket.
+- Branch-published publication output is local-only by default. Weave can create local publication commits, but it does not push them.
+- Plain fixture planning remains non-mutating. Regeneration/execution requires explicit `--execute`, and branch updates are local unless pushed separately.
+
+## Artifacts
+
+- Git tag: `v0.1.0`
+- GitHub Release: `v0.1.0`
+- Native binary archives:
+ - `weave-v0.1.0-linux-x64.tar.gz`
+ - `weave-v0.1.0-windows-x64.zip`
+ - `weave-v0.1.0-macos-x64.tar.gz`
+ - `weave-v0.1.0-macos-arm64.tar.gz`
+- Matching `.sha256` checksum files for native archives.
+- npm packages:
+ - `@semantic-flow/weave`
+ - `@semantic-flow/weave-linux-x64`
+ - `@semantic-flow/weave-windows-x64`
+ - `@semantic-flow/weave-macos-x64`
+ - `@semantic-flow/weave-macos-arm64`
+
+## Validation
+
+- `deno task ci` passed locally during release preparation: 422 tests passed.
+- `deno task fmt:check`, `deno task lint`, `deno task check`, and `deno task test` are the intended source quality gate.
+- Release tooling has focused coverage for version metadata, binary metadata, archive packaging, npm package assembly, npm install smoke setup, npm publish ordering, and `weave --version`.
+- The manual release workflow should still be rehearsed in GitHub Actions before a real npm publish.
+
+## Known Limitations
+
+- The supported packaged surface is the local `weave` CLI. Daemon and web surfaces remain scaffolded/deferred.
+- The branch-published fixture repo is often left on `gh-pages` for local preview. Tests should remain meaningful there: generated mesh assertions read generated refs, while deterministic source-asset checks read the asset-bearing source ref.
+- Historical ResourcePage regeneration policy is parsed into effective config, but the full config-at-the-time/current/hybrid regeneration behavior remains follow-up work.
+- Durable next history/state segment hint APIs are not part of this release. Friendly histories and states are available through explicit `historySegment`, `stateSegment`, and `manifestationSegment` request fields.
+- npm publication requires npm organization/package access and a valid publish token or trusted publishing setup.
+
+## Next
+
+- Rehearse the manual release workflow on GitHub Actions.
+- Confirm npm scope ownership and publish credentials.
+- Use `v0.1.0` to build a real branch-published ontology such as URPX and feed any release-blocking ergonomics back into focused follow-up tasks.
+- Continue config work on durable next-segment hints, historical ResourcePage regeneration, and fuller config-source resolution only where real fixture or ontology publication pressure demands it.
diff --git a/documentation/notes/roadmap.md b/documentation/notes/roadmap.md
index c1ef747..87d0b77 100644
--- a/documentation/notes/roadmap.md
+++ b/documentation/notes/roadmap.md
@@ -18,6 +18,7 @@ created: 1773889263552
- `[importance: high] [how-soon: next]` Extend operational config from local-boundary policy into the remaining remote/runtime questions: explicit gating for `workingAccessUrl` and `targetAccessUrl`, selective command/runtime consumption, and the remaining `integrate` boundary about whether remote-origin association belongs there or stays centered on `import`. See [[wd.task.2026.2026-04-11_1723-operational-config-for-runtime-resolution]].
- `[importance: high] [how-soon: next]` Define the import, security, and resolution policy for outside-the-tree and extra-mesh content used by pages: allowed origin schemes, import triggers, pinning requirements, caching, offline behavior, HTML/script safety, and fail-closed error handling. See [[wd.task.2026.2026-04-08_1545-resource-page-definition-and-sources]].
- `[importance: medium] [how-soon: later]` Support policy-gated HTTP request shaping for remote RDF sites that do not expose direct file/export URLs cleanly, including custom `Accept` headers and related fetch metadata, without making content-negotiation-heavy endpoints a prerequisite for the first carried import-boundary fixtures.
+- `[importance: medium] [how-soon: later]` Add an API surface for deriving candidate `ReferenceLink`s from extraction provenance, likely as an explicit proposal/curation operation rather than automatic extraction side effect. For now, matching references should be created manually so source provenance and curated references stay distinct while the ontology, role defaults, and current-vs-pinned reference semantics settle.
- `[importance: medium] [how-soon: later]` Add a transformation/extraction layer for using imported RDF datasets as page-region content. The current customizable identifier-page slice renders imported authored text as Markdown; it does not yet turn imported Turtle/JSON-LD datasets into good page-body content directly.
- `[importance: high] [how-soon: next]` Keep `_knop/_assets` as a local ahistorical support area even if helper metadata is added in ontology/config. If an asset needs independent versioning, publication, or reuse, make it a separate payload artifact and reference it from the page definition rather than trying to version `_assets` directly. See [[wd.task.2026.2026-04-08_1545-resource-page-definition-and-sources]].
- `[importance: high] [how-soon: next]` Move page HTML construction fully into runtime rendering seams. Core weave planning should emit page models only, and the existing `alice/index.html` special-case builders should be retired in favor of a more general page model and renderer.
diff --git a/documentation/notes/wd.codebase-overview.md b/documentation/notes/wd.codebase-overview.md
index a1c4772..937174a 100644
--- a/documentation/notes/wd.codebase-overview.md
+++ b/documentation/notes/wd.codebase-overview.md
@@ -13,6 +13,8 @@ created: 1773673181726
mesh create, knop create, integrate, payload update, version, validate, generate, extract, weave
request/result types shared by all callers
shared designator normalization now treats `/` as a CLI-only root sentinel and `""` as the internal root designator path, including root-aware target selection and support-artifact path derivation
+ `core/weave` has started splitting focused planners out of the large façade module; mesh support ResourcePage catch-up planning now lives in `mesh_support_pages.ts`, while `weave.ts` keeps the public re-export surface for existing runtime, CLI, and test imports
+ first Knop, first payload, and first extracted-Knop weave planning now resolve MeshInventory current/latest/next progression from `_mesh/_meta` instead of mutable current pointers in `_mesh/_inventory`; `_mesh/_inventory` keeps stable artifact-history and historical-state membership facts while `_mesh/_meta` advances `sflo:latestHistoricalState`, `sflo:nextStateOrdinal`, and consumed next-state hints
current carried slices: `mesh create` request validation/support-artifact rendering, `knop create` planning over an existing mesh inventory, the first narrow `integrate` planning slice for `05-alice-knop-created-woven` -> `06-alice-bio-integrated`, the first narrow `knop add-reference` planning slice for `07-alice-bio-integrated-woven` -> `08-alice-bio-referenced`, the first narrow `payload.update` planning slice for `09-alice-bio-referenced-woven` -> `10-alice-bio-updated`, `extract` planning for both Alice Bio `11-alice-bio-v2-woven` -> `12-bob-extracted` and Fantasy Rules sidecar `07-shacl-integrated-woven` -> `08-ontology-and-shacl-terms-extracted`, and carried `weave` planning slices through Alice Bio `13-bob-extracted-woven` plus Fantasy Rules sidecar `15-first-release-woven`
### runtime
@@ -21,7 +23,11 @@ created: 1773673181726
job execution primitives, but not HTTP
includes first-pass Deno-native structured operational and audit logging
persistent config direction is RDF, probably JSON-LD, and should remain queryable via SPARQL
+ `runtime/config` now carries the first default effective-config seam: it loads Weave default RDF, resolves artifact-role history and ResourcePage policies, parses historical ResourcePage regeneration policy, parses default payload history/state/manifestation naming policies, parses the default config-resolution profile, and models first-pass Knop inherited-config propagation controls without changing fixture-backed behavior yet
runtime inventory discovery, workspace loaders, and page rendering now carry the root designator path as a first-class resource when a root Knop exists at `_knop`
+ runtime weave planning now passes Weave's default effective support-history and payload naming policies into version planning, letting first Knop and first payload weave outputs keep `_knop/_meta` current-only while preserving payload and inventory history behavior and keeping ordinal payload paths as the configured default
+ runtime weave planning now passes current MeshMetadata into version planning so core can use `_mesh/_meta` as the MeshInventory progression source for the first `_mesh/_meta` migration seam
+ runtime page generation and versioned inventory rendering now filter `sflo:hasResourcePage` candidates through the effective resource-page generation policy by owning artifact role, using stable history/state membership rather than mutable current/latest pointers, so `generate`, `suppress`, `defer`, and explicit-target `onRequest` have a materialization seam independent of history policy without leaving suppressed page promises in inventory
current carried slices: local filesystem materialization for `mesh create`, `knop create`, `knop add-reference`, the first local `integrate` pass over an existing workspace payload file, the first local `payload.update` pass over an already woven payload artifact, local `extract` passes that can either fail closed against one inferred woven payload source or use an explicit source designator for docs-rooted sidecar meshes, the first local `validate` / `version` / `generate` runtime seams under `runtime/weave`, and carried local `weave` passes over existing workspaces with a shared runtime ResourcePage renderer seam. Extracted-resource weave now covers Bob plus the Fantasy Rules sidecar term set; named-release weave covers the Fantasy Rules `releases/v0.0.1/ttl` paths by starting explicit payload histories on already-versioned artifacts while preserving ordinal history counters and state fallback counters. Named-state histories fail closed on later omitted state naming, while broad payload segment defaults can still be supplied for all included payload artifacts.
current logging slice: narrow Kato-inspired `LogRecord` / sink / `StructuredLogger` / `AuditLogger` JSONL layer
@@ -65,12 +71,12 @@ created: 1773673181726
- The current sidecar extract extension matches the settled Fantasy Rules `07-shacl-integrated-woven` -> `08-ontology-and-shacl-terms-extracted` fixture state.
- The current sidecar extracted-term weave extension matches the Fantasy Rules `08-ontology-and-shacl-terms-extracted` -> `09-ontology-and-shacl-terms-extracted-woven` fixture state.
- The current sidecar named-release weave extension matches the Fantasy Rules `14-first-release` -> `15-first-release-woven` fixture state.
-- The current carried `weave` slices are the local Alice Bio paths through `13-bob-extracted-woven` plus the Fantasy Rules sidecar `15-first-release-woven` path, including first-history creation for Knop support artifacts, first payload-artifact history creation, first ReferenceCatalog history creation on an already-versioned Knop surface, second payload-history creation on an already-versioned payload surface, extracted-resource support-artifact weave through inventory `sfc:ExtractionSource`, recursive multi-target sidecar term weave, named payload histories on already-versioned payload artifacts, and generated HTML pages rendered through a shared runtime page seam.
+- The current carried `weave` slices are the local Alice Bio paths through `13-bob-extracted-woven` plus the Fantasy Rules sidecar `15-first-release-woven` path, including first-history creation for Knop support artifacts, first payload-artifact history creation, first ReferenceCatalog history creation on an already-versioned Knop surface, second payload-history creation on an already-versioned payload surface, extracted-resource support-artifact weave through source-registry `sfc:ExtractionSource`, recursive multi-target sidecar term weave, named payload histories on already-versioned payload artifacts, and generated HTML pages rendered through a shared runtime page seam.
- `mesh create` now has a manifest-scoped black-box CLI acceptance test and thin framework example payloads.
- `knop create` now resolves `meshBase` from existing mesh metadata, creates the first Knop support artifacts, and has a manifest-scoped black-box CLI acceptance test.
- `knop add-reference` now resolves `meshBase` from existing mesh metadata, requires an explicit local `referenceRole`, creates the first Knop-owned `ReferenceCatalog` working file, updates the existing Knop inventory, and has manifest-scoped black-box CLI acceptance coverage for `08-alice-bio-referenced`.
- `integrate` now resolves a local source path or `file:` URL into a mesh-relative working file path, creates the first payload-Knop support artifacts, updates MeshInventory, and has manifest-scoped black-box CLI acceptance coverage together with thin framework examples.
- `payload.update` now resolves the existing working payload file from an already woven payload surface, stages replacement bytes from a local path or `file:` URL without changing the semantic mesh path, updates only `alice-bio.ttl` for the carried `10` slice, and has manifest-scoped black-box CLI acceptance coverage together with thin framework examples.
-- `extract` now resolves the target designator against exactly one woven payload artifact already present in the workspace, against an explicit current source payload designator with `--source`, or against an explicit pinned historical source state with `--source-state`. It creates a new minimal Knop with an inventory `sfc:ExtractionSource`, defaults that source binding to current resolution, leaves source payload bytes and existing source surfaces unchanged, preserves multi-payload mesh inventories by appending the extracted Knop facts, and has coverage for Alice Bio `12-bob-extracted` plus Fantasy Rules sidecar `08-ontology-and-shacl-terms-extracted`.
-- `weave` now runs as the top-level local CLI action, versions the first Alice Knop support artifacts, the first Alice Bio payload history surface, the first Alice ReferenceCatalog history surface, the second Alice Bio payload historical state, the first Bob extracted-support surface, and the first Fantasy Rules sidecar extracted-term support surfaces. It advances MeshInventory only where the public current surface changed, batches recursive target sets before writes, and generated extracted-term pages read source RDF from inventory `sfc:ExtractionSource` contracts using either current or pinned source resolution, including cases where a term such as `ontology/CharacterShape` is sourced from `shacl`.
+- `extract` now resolves the target designator against exactly one woven payload artifact already present in the workspace, against an explicit current source payload designator with `--source`, or against an explicit pinned historical source state with `--source-state`. It creates a new minimal Knop whose inventory points to `_knop/_sources/sources.ttl` for the `sfc:ExtractionSource` details, defaults that source binding to current resolution, leaves source payload bytes and existing source surfaces unchanged, preserves multi-payload mesh inventories by appending the extracted Knop facts, and has coverage for Alice Bio `12-bob-extracted` plus Fantasy Rules sidecar `08-ontology-and-shacl-terms-extracted`.
+- `weave` now runs as the top-level local CLI action, versions the first Alice Knop support artifacts, the first Alice Bio payload history surface, the first Alice ReferenceCatalog history surface, the second Alice Bio payload historical state, the first Bob extracted-support surface, and the first Fantasy Rules sidecar extracted-term support surfaces. It advances MeshInventory only where the public current surface changed, batches recursive target sets before writes, and generated extracted-term pages read source RDF from source-registry `sfc:ExtractionSource` contracts using either current or pinned source resolution, including cases where a term such as `ontology/CharacterShape` is sourced from `shacl`.
- Root designator support now treats `/` as the CLI spelling and `""` as the internal runtime/core value, including exact and recursive `--target` handling plus root-owned `_knop`, `_history001`, and `index.html` paths without leading slashes.
diff --git a/documentation/notes/wd.decision-log.md b/documentation/notes/wd.decision-log.md
index 0ec518e..61a54c9 100644
--- a/documentation/notes/wd.decision-log.md
+++ b/documentation/notes/wd.decision-log.md
@@ -190,7 +190,7 @@ created: 1773630801215
- References: [[wa.completed.2026.2026-04-05_1004-extract-bob]], [[sf.spec.2026-04-05-extract-behavior]]
- Why:
- The carried `12` fixture proves a narrow current-surface extraction boundary, not a generic source-selection or graph-rewrite API.
- - Pinning Bob's inventory-carried `sfc:ExtractionSource` to the source payload artifact's latest historical state preserves the non-woven semantic step while keeping broader payload splitting and Bob weaving out of scope.
+ - Pinning Bob's extraction-source contract to the source payload artifact's latest historical state preserves the non-woven semantic step while keeping broader payload splitting and Bob weaving out of scope.
### 2026-04-06: Fifth Local weave Slice Targets Bob 12 -> 13
@@ -203,7 +203,7 @@ created: 1773630801215
### 2026-05-04: Sidecar Term Extraction Uses Explicit Source Selection
- Decision: Extend local `extract` for the Fantasy Rules sidecar `07-shacl-integrated-woven` -> `08-ontology-and-shacl-terms-extracted` transition by supporting docs-rooted `--mesh-root` execution, explicit `--source-designator-path` selection for ambiguous multi-payload term mentions, and append-only mesh-inventory updates for new term Knops.
-- References: [[wd.task.2026.2026-05-03-term-extraction]], [[wd.task.2026.2026-05-02-fantasy-rules-sidecar]], [[sf.spec.2026-04-05-extract-behavior]]
+- References: [[wd.task.2026.2026-05-03-term-extraction]], [[wa.completed.2026.2026-05-02-fantasy-rules-sidecar]], [[sf.spec.2026-04-05-extract-behavior]]
- Why:
- Ontology terms such as `ontology/AbilityScore` are legitimately mentioned by both the ontology and SHACL payloads, so fail-closed inference still needs an explicit source selector for the intended extraction source.
- The sidecar mesh inventory already carries ontology, SHACL, config, and support artifacts; reconstructing a single-payload Bob-shaped inventory would be the wrong abstraction. Appending only the new term Knop facts preserves unrelated mesh state while keeping `08` non-woven.
@@ -219,8 +219,8 @@ created: 1773630801215
### 2026-05-04: Sidecar Extracted-Term Weave Uses Pinned Source States
-- Decision: Extend local `weave` for the Fantasy Rules sidecar `08-ontology-and-shacl-terms-extracted` -> `09-ontology-and-shacl-terms-extracted-woven` transition so extracted term Knops can be woven in a recursive multi-target batch, generated term pages read source RDF from pinned inventory `sfc:ExtractionSource` states, and term path anchoring follows the term namespace rather than the source artifact designator.
-- References: [[wd.task.2026.2026-05-03-term-extraction]], [[wd.task.2026.2026-05-02-fantasy-rules-sidecar]], [[sf.spec.2026-04-03-weave-behavior]]
+- Decision: Extend local `weave` for the Fantasy Rules sidecar `08-ontology-and-shacl-terms-extracted` -> `09-ontology-and-shacl-terms-extracted-woven` transition so extracted term Knops can be woven in a recursive multi-target batch, generated term pages read source RDF from pinned `sfc:ExtractionSource` states, and term path anchoring follows the term namespace rather than the source artifact designator.
+- References: [[wd.task.2026.2026-05-03-term-extraction]], [[wa.completed.2026.2026-05-02-fantasy-rules-sidecar]], [[sf.spec.2026-04-03-weave-behavior]]
- Why:
- `ontology/CharacterShape` is intentionally sourced from the `shacl` artifact while remaining an `ontology/...` term. The authored SHACL Turtle uses the `fant:` prefix for that ontology namespace, so path-prefix inference would pick the wrong source.
- Multiple extracted terms advance MeshInventory one state per term while preserving the previously woven sidecar mesh state.
@@ -232,11 +232,21 @@ created: 1773630801215
- Why:
- Extraction source binding is part of the extracted identifier surface's provenance and resolution contract, not a user-authored or cataloged reference about the resource.
- Fragment IRIs let the inventory page preserve dereferenceability for the extraction source relator without adding an otherwise empty `_references` support artifact.
+- Status: Superseded by [[#2026-05-16 Extraction Source Details Live In Knop Source Registries]]. The runtime no longer preserves a compatibility reader for this inventory-rooted shape; stale fixture refs should be regenerated.
+
+### 2026-05-16: Extraction Source Details Live In Knop Source Registries
+
+- Decision: Keep `sflo:hasExtractionSource` on the extracted Knop inventory as the compact pointer, but store the `sfc:ExtractionSource` details in `D/_knop/_sources/sources.ttl` at `D/_knop/_sources#extraction-source`. The Knop inventory links that supporting artifact with `sflo:hasKnopSourceRegistry`.
+- References: [[wd.task.2026.2026-05-04-extraction-improvements]], [[wa.completed.2026.2026-05-15_1113-mesh-branch-fantasy-rules]], [[ont.decision-log]]
+- Why:
+ - Extraction provenance is source information, so `_sources` is a better support-artifact home than `_inventory` once Knops can have a general source registry.
+ - Keeping the Knop-level `sflo:hasExtractionSource` pointer preserves the simple runtime lookup and lets SHACL constrain one primary extraction source without making inventory carry the relator details.
+ - The source registry can grow to include repository payload sources and extraction/term sources without bloating MeshConfig or conflating operational config with provenance.
### 2026-05-04: Named Release Histories Do Not Consume Ordinal Counters
- Decision: Let payload weave start an explicitly named ArtifactHistory such as `releases` on an already versioned payload artifact, while leaving `sflo:nextHistoryOrdinal` unchanged as the next auto `_historyNNN` counter. Semver-style HistoricalState names such as `v0.0.1` are explicitly requested and do not receive `sflo:stateOrdinal`; the named history still carries `sflo:nextStateOrdinal`, but later auto-versioning fails closed after a named state unless the caller supplies the next `stateSegment` or explicitly requests an ordinal fallback segment.
-- References: [[wd.task.2026.2026-05-02-fantasy-rules-sidecar]]
+- References: [[wa.completed.2026.2026-05-02-fantasy-rules-sidecar]]
- Why:
- Named histories and ordinal histories are different naming policies. Creating `releases` should not make a future omitted history become `_history003` when `_history002` has never existed.
- Weave does not yet have a semver increment policy or interactive release prompt, so `nextStateOrdinal` remains an ordinal fallback counter, not a semver successor.
@@ -245,7 +255,7 @@ created: 1773630801215
### 2026-05-04: Extraction Sources Default To Current Resolution
- Decision: Make `Current` the default `sfc:ExtractionSource` resolution for newly extracted terms, keep pinned resolution explicit through `--source-state`, replace `--source-designator-path` with `--source`, replace `--yes` with `--accept-preview`, and add `weave set extraction-source` as the maintenance command for changing an existing extracted Knop's source-resolution contract.
-- References: [[wd.task.2026.2026-05-04-extraction-improvements]], [[wd.task.2026.2026-05-02-fantasy-rules-sidecar]], [[wu.cli-reference]]
+- References: [[wd.task.2026.2026-05-04-extraction-improvements]], [[wa.completed.2026.2026-05-02-fantasy-rules-sidecar]], [[wu.cli-reference]]
- Why:
- Ontology and SHACL term pages should normally refresh from the source artifact's current state after a release advances; pinning is still available when reproducibility against a historical source state is the intended contract.
- `extract --all-terms` remains a creation operation that skips existing Knops, so migrating already-created term surfaces needs an explicit update command.
@@ -290,3 +300,19 @@ created: 1773630801215
- Why:
- An explicit `/` sentinel is clearer and safer than overloading an omitted or blank designator-path value to mean root.
- Normalizing root once at the CLI boundary keeps target resolution, path derivation, and user-facing display coherent across commands.
+
+### 2026-05-14: Branch-Published Fantasy Rules Fixture
+
+- Decision: Treat Fantasy Rules as the branch-published ontology fixture for the next rerung, with authored ontology/source files on the source branch and all generated mesh output on the publication branch.
+- References: [[wd.task.2026.2026-05-13_1655-support-gh-pages-branch-based-deployments]], [[wa.completed.2026.2026-05-07-fixture-ladder-generator]]
+- Why:
+ - This proves the clean-source-branch story that motivated branch-published meshes: no generated `_mesh`, config, pages, histories, or local sibling paths need to live on the source branch.
+ - The older `docs/` sidecar topology remains valid, but it no longer needs to be the primary Fantasy Rules fixture once branch-published deployment is available.
+
+### 2026-05-14: Fixture Branches Are Generated Outputs
+
+- Decision: Treat fixture branch ladders as disposable generated golden outputs produced from ordered scenario definitions plus Accord manifests, rather than hand-maintained source material.
+- References: [[wa.completed.2026.2026-05-07-fixture-ladder-generator]]
+- Why:
+ - Current fixture branches carry stale namespace and progression shapes, and pre-v1 Weave should regenerate them against the current contract rather than add compatibility shims.
+ - Broad fixture rerungs should be intentional, reviewable generated-output passes with branch writes behind an explicit flag.
diff --git a/documentation/notes/wd.task.2026.2026-04-14_0018-configurable-test-tmp.md b/documentation/notes/wd.task.2026.2026-04-14_0018-configurable-test-tmp.md
new file mode 100644
index 0000000..790663f
--- /dev/null
+++ b/documentation/notes/wd.task.2026.2026-04-14_0018-configurable-test-tmp.md
@@ -0,0 +1,102 @@
+---
+id: aqslrdergnqejj5mulkfim2
+title: 2026 04 14_0018 Configurable Test Tmp
+desc: ''
+updated: 1778743130406
+created: 1778743130406
+---
+
+## Goals
+
+- Make Weave's shared test temporary workspace root configurable.
+- Stop default `deno task test` and `deno task test:coverage` runs from writing temporary workspaces under the repository-local `.test-tmp/` directory.
+- Keep the existing per-test cleanup behavior, including `WEAVE_KEEP_TEST_TMP`, so preserved temp workspaces remain an intentional debugging choice rather than an accidental leak.
+- Migrate direct `.test-tmp` writers to the shared test temp helper or another clearly justified temp strategy.
+- Update developer testing documentation so contributors know where preserved test workspaces go and how to override that location.
+
+## Summary
+
+The current test harness writes `createTestTmpDir()` workspaces under `repoRoot/.test-tmp`. That was convenient when the test suite was smaller, but stale directories now accumulate when tests abort, when cleanup is intentionally disabled, or when test code bypasses the shared helper. Because `.test-tmp/` lives inside the repository, those leaks can still affect editor watching, search, file indexing, and mental noise even though the workspace settings hide the directory.
+
+This task should introduce a configurable test temp root named `WEAVE_TEST_TMP_ROOT` and configure the repository's normal test tasks to place test workspaces outside the repository. The shared helper remains responsible for registering created directories and cleaning them after each test unless `WEAVE_KEEP_TEST_TMP=1` or `WEAVE_KEEP_TEST_TMP=true` is set.
+
+This is not a substitute for cleanup correctness. We should still treat leftover temp workspaces as a signal that some test path bypassed the harness, crashed before cleanup, or was run with an explicit keep flag. The change is meant to keep those leftovers out of the repo and make the temp location intentional.
+
+## Discussion
+
+Current state:
+
+- `tests/support/test_tmp.ts` hardcodes `const testTmpRoot = join(repoRoot, ".test-tmp")`.
+- `deno task test` and `deno task test:coverage` preload `tests/support/test_tmp_harness.ts`, which wraps `Deno.test` and cleans registered `createTestTmpDir()` paths after each test.
+- `WEAVE_KEEP_TEST_TMP` already preserves registered temp paths for debugging.
+- `deno.json` excludes `.test-tmp/**`, and `weave.code-workspace` hides/excludes `.test-tmp/**`, but those are mitigations rather than cleanup.
+- `tests/scripts/publish_npm_packages_test.ts` currently constructs a `.test-tmp/publish-npm-packages/...` path directly instead of using `createTestTmpDir()`.
+- Some focused tests use plain `Deno.makeTempDir()` without a `dir`; those already go to the platform temp area and do not contribute to repo-local `.test-tmp` growth.
+
+The implementation should centralize temp-root resolution in `tests/support/test_tmp.ts`. The helper should read a dedicated env var only after checking env permission, matching the existing `WEAVE_KEEP_TEST_TMP` pattern. Normal test tasks already run with `--allow-env`, so this should not require a permission expansion.
+
+Recommended behavior:
+
+- `WEAVE_TEST_TMP_ROOT` sets the parent directory used by `createTestTmpDir(prefix)`.
+- If `WEAVE_TEST_TMP_ROOT` is relative, resolve it relative to the repository root, not whatever a test temporarily uses as `Deno.cwd()`.
+- If `WEAVE_TEST_TMP_ROOT` is unset, use the platform temp directory through `Deno.makeTempDir({ prefix })` rather than falling back to repository-local `.test-tmp`.
+- `deno task test` and `deno task test:coverage` should set `WEAVE_TEST_TMP_ROOT` to a stable path outside the repository, for example `../.weave-test-tmp`.
+- `createTestTmpDir()` should continue to create uniquely named child directories with the caller-provided prefix and register them in the active cleanup scope.
+- Preserve the existing cleanup order and aggregate-error behavior.
+
+There is a small tradeoff between stable grouping and platform defaults. A stable external root such as `../.weave-test-tmp` is easier to inspect after `WEAVE_KEEP_TEST_TMP=1`; direct fallback to the platform temp directory is less likely to pollute the repo when someone runs `deno test` manually without using the configured task. The task-level env var gives us both.
+
+The existing repository-local `.test-tmp/` directory can be deleted manually after this lands. Test code should not perform a broad automatic cleanup of old repo-local temp workspaces because that could erase a developer's preserved debugging output without an explicit request.
+
+## Open Issues
+
+- Should the configured external root be `../.weave-test-tmp`, `../.test-tmp/weave`, or an OS-temp-based absolute path? Recommendation: use `../.weave-test-tmp` for now because it is stable, outside the repo, easy to inspect, and simple to express in the existing Deno task style.
+- Should `WEAVE_TEST_TMP_ROOT` be documented as accepting relative paths? Recommendation: yes, but define them as repo-root-relative to avoid surprises from tests that change process cwd.
+- Should `.test-tmp/**` stay in `deno.json` and `weave.code-workspace` after the move? Recommendation: keep it for now because old local leftovers and ad hoc debugging directories may still exist.
+
+## Decisions
+
+- Use a test-harness env var for this rather than production config. This is developer infrastructure, not a Weave runtime behavior.
+- Keep `WEAVE_KEEP_TEST_TMP` as the preservation switch; do not overload the new root setting to imply preservation.
+- Keep cleanup scoped to the exact directories created through `createTestTmpDir()`; do not recursively sweep the whole configured root at the end of a run.
+- Direct hardcoded `.test-tmp` paths in tests should be treated as bypasses and migrated.
+
+## Contract Changes
+
+- No Semantic Flow API, CLI, mesh, or runtime contract changes.
+- Developer/test harness contract change: `createTestTmpDir()` will honor `WEAVE_TEST_TMP_ROOT`.
+- Developer workflow change: normal test tasks will write temp workspaces outside the repository.
+- Documentation change: update [[wd.testing]] to describe `WEAVE_TEST_TMP_ROOT`, the default task location, and its relationship to `WEAVE_KEEP_TEST_TMP`.
+
+## Testing
+
+- Add focused unit-style coverage for temp-root resolution if it can be tested without replacing process globals.
+- Cover unset `WEAVE_TEST_TMP_ROOT` creating a temp directory outside `repoRoot/.test-tmp`.
+- Cover relative `WEAVE_TEST_TMP_ROOT` resolving relative to `repoRoot`.
+- Cover absolute `WEAVE_TEST_TMP_ROOT` being honored.
+- Cover `WEAVE_KEEP_TEST_TMP` still preserving registered directories.
+- Add or adjust an integration-level test that creates a temp directory through `createTestTmpDir()` with `WEAVE_TEST_TMP_ROOT` set to a test-controlled parent, then verifies cleanup removes the registered child when keep is not set.
+- Run `deno task test` after implementation and verify no new directories are created under repository-local `.test-tmp/`.
+- Run a targeted keep-mode check, for example `WEAVE_KEEP_TEST_TMP=1 deno task test --filter `, and verify the preserved workspace appears under the configured external root.
+- Run `deno task lint` because the change touches shared test harness code and test task wiring.
+
+## Non-Goals
+
+- Do not introduce production runtime temp-directory configuration.
+- Do not change Weave CLI behavior for user-provided workspaces or mesh roots.
+- Do not automatically delete existing repository-local `.test-tmp/` contents as part of the test harness.
+- Do not convert every use of `Deno.makeTempDir()` in the codebase; only migrate repo-local `.test-tmp` writers and tests that should participate in the shared cleanup harness.
+- Do not remove `.test-tmp` editor or Deno excludes in this slice.
+
+## Implementation Plan
+
+- [ ] Add a `WEAVE_TEST_TMP_ROOT` constant and temp-root resolver in `tests/support/test_tmp.ts`.
+- [ ] Change `createTestTmpDir()` so it uses the configured root when present and otherwise falls back to platform temp space.
+- [ ] Preserve active-scope registration, reverse-order cleanup, `WEAVE_KEEP_TEST_TMP`, and aggregate cleanup/test error behavior.
+- [ ] Configure `deno task test` and `deno task test:coverage` to set `WEAVE_TEST_TMP_ROOT` to an external stable path.
+- [ ] Replace direct `.test-tmp` construction in `tests/scripts/publish_npm_packages_test.ts` with `createTestTmpDir()` or another registered helper path.
+- [ ] Search for remaining repo-local `.test-tmp` writers and migrate any that create files during tests.
+- [ ] Update [[wd.testing]] with the new temp-root behavior and debugging workflow.
+- [ ] Add focused coverage for configured temp-root behavior.
+- [ ] Run `deno task lint` and `deno task test`.
+- [ ] Manually confirm a normal test run does not add new entries under repository-local `.test-tmp/`.
diff --git a/documentation/notes/wd.task.2026.2026-05-02-fantasy-rules-sidecar.md b/documentation/notes/wd.task.2026.2026-05-02-fantasy-rules-sidecar.md
deleted file mode 100644
index 548fb9e..0000000
--- a/documentation/notes/wd.task.2026.2026-05-02-fantasy-rules-sidecar.md
+++ /dev/null
@@ -1,370 +0,0 @@
----
-id: 6wjbum23c4rli8cojvtcp0i
-title: 2026 05 02 Fantasy Rules Sidecar
-desc: sidecar mesh fixture for dereferenceable ontology and SHACL publishing
-updated: 1777878162292
-created: 1777705655304
----
-
-## Goals
-
-- Build out the new fixture repository named `mesh-sidecar-fantasy-rules`.
-- Use the fixture to prove the docs-rooted sidecar mesh pattern for an ontology project: source files live outside the mesh root, while public identifiers, generated pages, and historical snapshots live under `docs/`.
-- Exercise the dereferenceable ontology publishing use case described in [[ont.use-case.dereferenceable-ontology]] with a small fantasy-rules ontology and SHACL graph.
-- Use the [System Reference Document 5.2.1](https://media.dndbeyond.com/compendium-images/srd/5.2/SRD_CC_v5.2.1.pdf) as the source-reference boundary for fantasy-rules vocabulary work, subject to its CC-BY-4.0 attribution requirements.
-- Use the [SRD 5.2 Markdown transcription](https://github.com/springbov/dndsrd5.2_markdown/blob/main/DND-SRD-5.2-CC.md) as a working convenience source for review and extraction, while keeping the official SRD source and attribution statement authoritative.
-- Keep the domain intentionally small: enough to feel real, not enough to become a fantasy rules knowledge-graph project.
-- Use the fixture to improve Weave's sidecar ergonomics while keeping general resource-page presentation behavior in the separate renderer task.
-- Include the raw RDF content on `RdfDocument` resource pages, not just links to Turtle files.
-- Evaluate a safe JavaScript URL-polish behavior for generated resource pages where the browser URL can display the canonical IRI without a trailing slash.
-- Capture reusable conventions for future ontology projects such as URPX without putting URPX-specific complexity into the fixture.
-
-## Summary
-
-`mesh-alice-bio` has been a good whole-repo reference mesh, but it is no longer the right fixture for the next publication topology problem: sidecar meshes. The next useful fixture should be a normal project repo whose primary source files are not themselves the public mesh root.
-
-`mesh-sidecar-fantasy-rules` should be that fixture. It should look like a small ontology project:
-
-- authored ontology source under `ontology/`
-- authored SHACL source under `shacl/`
-- optional examples/tests under `examples/` or `test/`
-- a Semantic Flow sidecar mesh under `docs/`
-
-The public GitHub Pages surface would be `docs/`, with stable artifact IRIs such as:
-
-- `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/ontology`
-- `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/shacl`
-- `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/ontology/releases/v0.0.1`
-- `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/shacl/releases/v0.0.1`
-
-The first versioned Turtle bytes should use the artifact-local Semantic Flow chain with custom segments: ArtifactHistory segment `releases`, HistoricalState segment `v0.0.1`, ArtifactManifestation segment `ttl`, then the source filename. For example, the first ontology release located file should be `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/ontology/releases/v0.0.1/ttl/fantasy-rules-ontology.ttl`.
-
-The authored source files should remain in the project-appropriate source tree, while `docs/` carries the public mesh, generated resource pages, and copied historical release bytes.
-
-The fixture should still use the Alice Bio branch-ladder pattern, but with one important clarification from the completed Alice Bio work: branches are the inspectable carrier for state, while Accord manifests are the acceptance contract for transitions. The branches are useful for human review and for automated comparison: a test can check out a source branch, run the intended operation or `weave`, and compare the produced workspace against the destination branch using the corresponding Accord manifest. The manifests should be created alongside the branch ladder as soon as each transition is settled, not deferred until final documentation cleanup.
-
-The first ladder should stay focused on the core sidecar path:
-
-- `00-blank-slate`
-- `01-source-only`
-- `02-sidecar-mesh-created`
-- `03-sidecar-mesh-created-woven`
-- `04-ontology-integrated`
-- `05-ontology-integrated-woven`
-- `06-shacl-integrated`
-- `07-shacl-integrated-woven`
-- `08-ontology-and-shacl-terms-extracted`
-- `09-ontology-and-shacl-terms-extracted-woven`
-- `10-root-knop`
-- `11-root-knop-woven`
-- `12-gunaar-example-dataset`
-- `13-gunaar-example-dataset-woven`
-- `14-first-release`
-- `15-first-release-woven`
-- `16-version-bump`
-- `17-version-bump-woven`
-
-The first named release pair should come after the root/examples collection surface and the Gunaar dataset pair, so the release slice exercises multiple histories in a richer mesh rather than only the two primary RDF documents.
-
-The first follow-up release pair should immediately exercise the same named release histories with `v0.0.2`, including at least one authored ontology or SHACL source change that affects an already extracted term page and at least one new mesh-scoped term discoverable by all-terms extraction. This makes the version bump a practical test that extracted pages refresh from changed source RDF rather than only testing copied release bytes, while also bringing the fixture forward from the intentionally narrow `08` extraction slice to source-scoped all-terms extraction.
-
-## Discussion
-
-This fixture should carry several related questions at once, but they should not all be treated as one inseparable implementation slice.
-
-The first question is sidecar topology:
-
-- can Weave create and operate a mesh rooted at `docs/`?
-- can a mesh artifact use `workingLocalRelativePath` to point at adjacent repo-local source such as `../ontology/fantasy-rules-ontology.ttl`?
-- can operational config allow that adjacent path while still rejecting arbitrary traversal?
-- can weave copy historical snapshots into the public mesh under `docs/`?
-- can generated pages and support artifacts stay under `docs/` without exposing the entire repo as a public Pages surface?
-
-The second question is ontology publication shape:
-
-- the ontology artifact should be a `DigitalArtifact`, likely a `PayloadArtifact`, an `RdfDocument`, and an `owl:Ontology`
-- the SHACL artifact should be a `DigitalArtifact`, likely a `PayloadArtifact`, an `RdfDocument`, and a `sh:ShapesGraph`; it may also be an `owl:Ontology` if it carries ontology-style metadata
-- release history should use artifact-local custom path segments such as `ontology/releases/v0.0.1` and `shacl/releases/v0.0.1`, where `releases` is the ArtifactHistory segment and `v0.0.1` is the HistoricalState segment
-- Turtle release bytes should sit under a `ttl` ArtifactManifestation segment, producing located-file paths such as `ontology/releases/v0.0.1/ttl/fantasy-rules-ontology.ttl`
-- `owl:versionIRI` should point to versioned bytes, such as `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/ontology/releases/v0.0.1/ttl/fantasy-rules-ontology.ttl`
-- `dcterms:hasVersion` should point from the ontology artifact to the Semantic Flow `HistoricalState`, but only once that state has actually been woven
-- richer Semantic Flow artifact/history/manifestation/located-file detail can live in mesh inventory and support artifacts rather than being forced into the ontology source document
-
-The ontology source should eventually publish a small Semantic Flow-compliant version metadata slice, but not in `01-source-only` and not speculatively before the historical state and located Turtle bytes exist. For the first source/integration branches, avoid `dcterms:hasVersion` and `owl:versionIRI`. Add them when the release/weave branch materializes the corresponding `HistoricalState`, `ArtifactManifestation`, and `LocatedFile`.
-
-The source ontology is its own meaningful workstream. The SRD 5.2.1 document is large, even before deciding what should become classes, controlled values, examples, SHACL shapes, labels, definitions, and attribution. This task should not pretend that `fantasy-rules-ontology.ttl` is just a quick fixture stub. The first ontology slice should be curated deliberately, with enough domain structure to exercise ontology publishing without dragging the mesh-sidecar work into a full SRD modeling project.
-
-The first ontology seed should stay small: `AbilityScore`, `Alignment`, `Character`, and a few representative controlled values or examples are enough. Larger SRD modeling work belongs in later task notes.
-
-The Markdown transcription makes the small seed review tractable. It has directly relevant sections for the six abilities and ability scores, character-creation ability score assignment, alignment, and glossary definitions for ability score/modifier, alignment, and player character. That is enough to justify the first seed slice without modeling the larger SRD.
-
-Use slash term IRIs first, not hash IRIs. The fixture should eventually support both patterns, but slash IRIs are the better first proof because term resource pages can stand independently and deprecated or removed terms do not force the ontology document page to keep carrying every old term description forever. The first-pass term path should be ontology-root `ontology/...`, for example `https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/ontology/AbilityScore`. The local rationale is also captured in `/home/djradon/hub/djradon/dendron-workspace/public-notes/vs.hash-vs-slash.md`.
-
-The authored Turtle prefix should use `fant:` for the fantasy-rules ontology namespace, with `@prefix fant: .`. Full IRIs remain fine in generated mesh RDF where they make cross-artifact relations clearer.
-
-The third question is whether the new fixture can exercise the generic generated resource-page surface without making renderer behavior part of the fixture contract. The Alice Bio pages are useful, and the sidecar fixture should consume the shared renderer improvements from [[wa.completed.2026.2026-05-03-resource-page-renderer-refresh]], but conformance for this fixture should focus on Semantic Flow artifacts, inventories, path policy, generated page presence, and raw RDF availability rather than pixel-level or prose-level presentation expectations.
-
-- artifact landing pages exist for ontology and SHACL artifacts when those artifacts are integrated
-- generated pages exist for current source bytes, versioned bytes, history pages, and support artifacts
-- raw RDF rendering exists for `RdfDocument` pages, including ontology, SHACL, manifestation, and located-file pages where the bytes are locally available
-- page generation remains rooted in shared runtime seams rather than fixture-specific HTML strings
-
-For RDF documents, the page should let a reader inspect the actual triples without leaving the resource page. That can start as an escaped `
` block or a progressively enhanced source panel. The important contract is that `RdfDocument` resource pages are not only metadata about a file; they also expose the RDF document content when Weave has local bytes. The raw file URL should still exist for tools and copy/download workflows.
-
-The fourth question is URL presentation. Static hosting wants `ontology/index.html`, and browsers usually display `/ontology/`. The ontology IRI may be `/ontology` without a trailing slash. A small generated script could use `history.replaceState` to display the canonical IRI form after page load.
-
-That script is worth exploring, but it has a trap: changing the displayed URL from `/ontology/` to `/ontology` can change how relative links resolve unless the page uses absolute/root-relative links or an explicit safe `` URL. The task should therefore treat trailing-slash removal as a page-rendering feature with tests, not a quick snippet pasted into every page.
-
-This task also intersects with the open layout question in [[sf.todo]]: `mesh-content/` probably should not remain a top-level sibling forever. For this sidecar fixture, mesh-owned helper content should start under `docs/_mesh/content/`.
-
-### Alice Bio Precedent
-
-The closest precedent is [[wa.completed.2026.2026-03-25-mesh-alice-bio]] together with the framework conformance task [[sf.completed.2026.2026-03-29-conformance-for-mesh-alice-bio]] and the examples index [[sf.api.examples]].
-
-The reusable parts are:
-
-- keep a numbered, human-readable branch ladder for manual inspection and comparison
-- use branch pairs as test refs: apply the operation from the source branch, then compare the generated result to the destination branch under the transition manifest
-- distinguish non-woven semantic-operation branches from `*-woven` branches
-- treat `weave` as version, validate, and generate, not as integrate or mesh creation
-- write one Accord manifest per transition, even when the filename follows the destination branch for convenience
-- store conformance manifests in the framework examples tree, not in each fixture branch
-- add manifests while the transition is being authored, so they stay normative instead of being reverse-engineered after the fact
-
-For this fixture, the corresponding framework example area should be `semantic-flow-framework/examples/sidecar-fantasy-rules/`, with API payloads under `api/` when needed and Accord manifests under `conformance/`.
-
-The first ladder should be branch-based unless implementation pressure proves a generated-only fixture is more useful. A generated fixture can still be added later as a comparison output, but the hand-authored branch ladder is the reviewable design surface.
-
-## Open Issues
-
-- How should RDF datasets such as `examples/gunaar.ttl` declare the ontology version or compatibility line they were authored against?
-- Should Semantic Flow define a small metadata property for dataset-to-ontology compatibility, or reuse an existing vocabulary pattern where the dataset announces the ontology artifact or ontology major version it expects?
-
-## Decisions
-
-- The fixture repository name should be `mesh-sidecar-fantasy-rules`.
-- The mesh root should be `docs/`.
-- Creating `docs/` as a sidecar mesh root should be an expansion of `weave mesh create`, not a separate fixture-only scaffold command. The expected command shape is `weave mesh create --workspace . --mesh-root docs --mesh-base https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/`, where `--workspace` names the repo/workspace root and `--mesh-root` names the mesh location on disk inside that workspace.
-- The `weave mesh create` CLI should resolve both `--workspace` and `--mesh-root` from the command working directory, then require the resolved mesh root to stay inside the resolved workspace root.
-- `weave mesh create` should include `.nojekyll` by default for GitHub Pages publishing targets, with an explicit opt-out switch for users who do not want that file.
-- The fixture should be fantasy-rules inspired, not a full rules ontology.
-- The first carried domain should be tiny and stable: classes such as `AbilityScore`, `Alignment`, `Character`, and perhaps a small number of representative individuals or controlled values are enough.
-- The SRD 5.2.1 PDF should be the initial source-reference boundary for rules vocabulary decisions. It is published under CC-BY-4.0 and requires attribution.
-- The SRD 5.2 Markdown transcription can be used as a convenience source for source review and extraction, but the official SRD source and attribution statement remain authoritative.
-- The fixture should avoid relying on trademarked branding or copied prose beyond what is intentionally and properly attributed from SRD 5.2.1.
-- SRD attribution should live in `NOTICE.md`; ontology metadata should include source/provenance for SRD-derived vocabulary, but `dcterms:license` on the fantasy-rules ontology should identify the fantasy-rules ontology's own license, not the SRD license.
-- The first-pass authored ontology, SHACL, and example files already exist in the fixture repo, so no separate source-authoring task note is needed before the sidecar mesh ladder proceeds.
-- Use slash IRIs for ontology terms first, with term resources under ontology-root `ontology/...` paths such as `ontology/AbilityScore`. Hash-term support can be proven later.
-- Use the authored Turtle prefix `fant:` for the fantasy-rules ontology namespace.
-- The fixture should carry ontology and SHACL as separate artifacts with independent histories.
-- Release paths should be artifact-local: `ontology/releases/v0.0.1` and `shacl/releases/v0.0.1`, not a single repo-global `releases/v0.0.1/...` path.
-- The first release should use custom version path segments: ArtifactHistory `releases`, HistoricalState `v0.0.1`, and ArtifactManifestation `ttl`.
-- The first ontology located file should be `ontology/releases/v0.0.1/ttl/fantasy-rules-ontology.ttl`, with the SHACL equivalent under `shacl/releases/v0.0.1/ttl/fantasy-rules-shacl.ttl`.
-- Version naming belongs on version/weave requests, not on generic targeting; unsupported custom segment requests should fail closed rather than silently producing default `_history001`, `_s0001`, or filename-derived manifestation paths.
-- The default payload manifestation segment should migrate from filename-derived segments such as `fantasy-rules-ontology-ttl` to extension-derived segments such as `ttl`.
-- The no-extension manifestation fallback still needs an implementation-level decision, but the normal RDF publishing path should be extension-backed by default.
-- The Alice Bio ladder should be re-laddered for the extension-backed manifestation default rather than hidden behind fixture normalization.
-- The first sidecar release should still request `manifestationSegment: "ttl"` explicitly until the default migration has landed everywhere, so this fixture does not depend on a half-migrated default.
-- `dcterms:hasVersion` and `owl:versionIRI` should be deferred until the release/weave branch that materializes the target `HistoricalState` and versioned located Turtle bytes.
-- Once release metadata is added, `dcterms:hasVersion` should point at the Semantic Flow `HistoricalState` and `owl:versionIRI` should point at versioned located Turtle bytes for OWL/RDF tool compatibility.
-- Semantic Flow-specific release-state detail should be present in the mesh, but the authored ontology file should stay mostly normal OWL/RDF.
-- If the meaning of a term needs to change, publish a new term instead of changing the meaning behind the existing IRI.
-- Incompatible ontology changes should generally be treated as a new ontology artifact or compatibility line, not as a silent semantic rewrite of the same term set.
-- Historical located files should be copied into the mesh by default when versioning is enabled.
-- Use a numbered branch ladder for the hand-authored fixture, following the Alice Bio comparison pattern.
-- The first sidecar ladder should continue past `07-shacl-integrated-woven` through ontology and SHACL term extraction, root/examples collection Knops, Gunaar example dataset integration, the first named ontology/SHACL release pair, and a follow-up version-bump pair.
-- `10-root-knop` should add a friendly root Knop for the repository Resource Page and an `examples/` Knop to act as the collection surface for example datasets.
-- `11-root-knop-woven` should weave the root and `examples/` collection Knops into history and pages before adding the Gunaar dataset.
-- `12-gunaar-example-dataset` should integrate `examples/gunaar.ttl` as public artifact `examples/gunaar`; `13-gunaar-example-dataset-woven` should weave that dataset into history and pages.
-- `14-first-release` and `15-first-release-woven` should publish the first named release histories for ontology and SHACL after the Gunaar dataset pair.
-- `16-version-bump` and `17-version-bump-woven` should publish the next paired ontology and SHACL release under the existing `releases` ArtifactHistories with `stateSegment=v0.0.2`, should include a source change that proves extracted term pages are refreshed by the woven output, and should run source-scoped all-terms extraction for both ontology and SHACL after the `v0.0.2` source states exist.
-- Ontology and SHACL should normally be bumped together in the fixture, even if only one source file has semantic changes, because they are published as a compatibility pair for this small ontology project.
-- Named ArtifactHistory paths such as `releases` should not consume or advance `sflo:nextHistoryOrdinal`; that property remains the next auto-generated `_historyNNN` counter for the artifact.
-- Semver-style HistoricalState paths such as `v0.0.1` should be explicitly requested and should not receive `sflo:stateOrdinal`; the containing named ArtifactHistory should still carry `sflo:nextStateOrdinal` for fallback default `_sNNNN` allocation if a later state request omits an explicit name.
-- Once a payload history has established a named HistoricalState such as `v0.0.1`, later `weave` or `version` operations must fail closed when that payload would be versioned without an explicit next `stateSegment`. A caller may continue semver naming with `v0.0.2` or explicitly opt into ordinal fallback with a segment such as `_s0001`; Weave should not silently choose between those policies.
-- Payload version segment defaults may apply broadly to all included payload artifacts, but target-specific segment fields should override the broad defaults. Support artifacts keep system-controlled history and state names until a separate support-artifact naming contract is defined.
-- Because `weave extract --all-terms` now creates current-tracking term surfaces by default but still skips already registered Knops, the `17` extraction step should run after the `v0.0.2` weave and should also run `weave set extraction-source --all-terms` for the existing extracted ontology and SHACL terms. This makes the fixture explicitly migrate the older pinned `08/09` term inventories before proving already extracted pages refresh from current source RDF.
-- The version-bump branch should include dataset compatibility metadata in `examples/gunaar.ttl` once the project settles how datasets announce the ontology version or compatibility line they target; do not block the first extracted-page refresh test on that still-open modeling decision.
-- Use branch refs as test fixtures: source refs define operation input, destination refs define expected output, and Accord manifests define the transition assertions.
-- Treat Accord manifests as transition contracts for the ladder, not as branch metadata or late acceptance paperwork.
-- Store Fantasy Rules Sidecar conformance manifests in `semantic-flow-framework/examples/sidecar-fantasy-rules/conformance/`.
-- Author the first manifest for each transition as that transition settles, before a runner or generated fixture is allowed to define the expected behavior.
-- Treat extracted term paths and source artifact designators as independent. `ontology/CharacterShape` intentionally remains under the ontology namespace while its source facts are pinned to the `shacl` artifact state whose Turtle uses the `fant:` prefix.
-- Generated pages for extracted terms should load source-derived RDF facts through the term Knop inventory's `sfc:ExtractionSource`, not by inferring the source artifact from the term path prefix.
-- Sidecar support should remain fail-closed. A `workingLocalRelativePath` outside the mesh root is allowed only when operational config explicitly permits that adjacent repo-local path.
-- For this fixture, adjacent source allowances should be added when the corresponding sidecar artifact is integrated, not when the mesh is merely created. Those allowances should use the existing config ontology terms `sfcfg:MeshConfig` and `sfcfg:hasLocalPathAccessRule`, with `sfcfg:meshRootPathBase`, `sfcfg:workingLocalRelativePathLocatorKind`, and explicit `sfcfg:pathPrefix` values such as `../ontology/`, `../shacl/`, and, once example datasets are integrated, `../examples/`.
-- The desired portable config surface should live under mesh-owned config such as `docs/_mesh/_config/`, not as a repo-root `.sf-repo-access.ttl` file.
-- `weave mesh create` should create a mesh config support artifact at `_mesh/_config/config.ttl` when the caller specifies a workspace root that differs from the mesh root. For this sidecar fixture, that is `docs/_mesh/_config/config.ttl`.
-- The sidecar `MeshConfig` should record `sfcfg:workspaceRootRelativeToMeshRoot "../"` for `--workspace . --mesh-root docs`, giving tools the portable relationship from mesh root to containing workspace root without recording an absolute host path.
-- The sidecar `MeshConfig` produced by `weave mesh create` should grant no extra-mesh access. Constrained `sfcfg:hasLocalPathAccessRule` entries belong to later integration steps that actually introduce adjacent-source artifacts.
-- Mesh-owned helper page content should live under `docs/_mesh/content/` in this fixture.
-- Improved resource-page look-and-feel belongs to [[wa.completed.2026.2026-05-03-resource-page-renderer-refresh]]; this task should only depend on resource-page behavior needed for the sidecar fixture contract.
-- `RdfDocument` resource pages should include raw RDF content when the document bytes are locally available.
-- Knop support artifacts such as `KnopMetadata` and `KnopInventory` should use their declared `sflo:hasWorkingLocatedFile` or `sflo:workingLocalRelativePath` values to show current raw RDF on their generated resource pages; they should not need separate page-definition working-file metadata.
-- The trailing-slash URL script should only run on generated resource pages whose canonical IRI is explicitly slashless, which is expected to include most resource pages. It must not break relative links, canonical links, copied IRI controls, or no-JavaScript page usability.
-- Generated resource pages should preserve link behavior after slashless URL polish by using project-root-relative links for mesh navigation, resource pages, support assets, and local previews.
-
-## Contract Changes
-
-- Add or clarify a sidecar mesh creation/use contract where the mesh root is not the repository root.
-- Expand `weave mesh create` so it separates the local workspace root from the mesh root path, can create a docs-rooted mesh surface, and keeps whole-repo meshes as the default `--mesh-root .` case.
-- Expand `weave mesh create` so sidecar creation emits `_mesh/_config/config.ttl` support RDF with `sfcfg:workspaceRootRelativeToMeshRoot`, without seeding mesh-adjacent path grants.
-- Include GitHub Pages `.nojekyll` defaults and an explicit opt-out in `weave mesh create`.
-- Ensure `workingLocalRelativePath` can be resolved relative to a mesh root such as `docs/` while obeying explicit local path access policy.
-- Use `MeshConfig` and `hasLocalPathAccessRule` from the Semantic Flow config ontology as the first-pass sidecar path-policy contract.
-- Ensure weaving can copy historical snapshots from adjacent source files into mesh-owned release paths under `docs/`.
-- Define expected artifact-local release path handling for non-ordinal histories such as `ontology/releases` and named states such as `v0.0.1`.
-- Define version/weave request fields for custom ArtifactHistory, HistoricalState, and ArtifactManifestation path segments, including `releases`, `v0.0.1`, and `ttl`.
-- Migrate the default manifestation-segment rule from filename-derived defaults to extension-derived defaults, while preserving explicit request overrides.
-- Define the no-extension manifestation-segment fallback before making the extension-backed default normative.
-- Define how ontology and SHACL artifacts advertise current working bytes, versioned located bytes, and generated resource pages in inventory.
-- Define how example datasets such as `examples/gunaar.ttl` can advertise the ontology version or compatibility line they were authored against.
-- Define the sidecar fixture's transition-manifest convention in the framework examples tree, reusing the Alice Bio one-manifest-per-transition approach.
-- Define how branch refs, operation execution, and Accord manifests are combined into acceptance tests for the sidecar fixture.
-- Define the resource-page behavior needed for ontology/SHACL landing pages, release pages, manifestation pages, and located-file pages, without making renderer-specific prose, layout, or visual styling part of sidecar fixture conformance.
-- Define how `RdfDocument` resource pages obtain and render raw RDF bytes from current working files and historical located files.
-- Defer shared page presentation/template improvements unless they are required for the sidecar fixture contract.
-- Potentially introduce a generated page script contract for slashless canonical IRI URL display, including when it may call `history.replaceState` and how generated project-root-relative links remain safe.
-- Decide whether mesh-owned helper page content should move from top-level `mesh-content/` to `_mesh`-owned content paths.
-
-## Testing
-
-- Add fixture-level Accord acceptance coverage for `mesh-sidecar-fantasy-rules` as each branch transition is settled.
-- Add branch-ref comparison tests that check out the source branch, run the intended operation or `weave`, and compare the resulting workspace to the destination branch under the matching Accord manifest.
-- Add tests that run Weave against a workspace whose mesh root is `docs/`.
-- Add tests proving `workingLocalRelativePath` can read explicitly allowed adjacent source files such as `../ontology/fantasy-rules-ontology.ttl` and `../shacl/fantasy-rules-shacl.ttl`.
-- Add fail-closed tests for disallowed `workingLocalRelativePath` traversal outside the configured repo-local boundary.
-- Add tests proving woven release snapshots are materialized inside `docs/` and remain byte-identical to the source bytes for that release.
-- Add tests proving custom version path segments produce `ontology/releases/v0.0.1/ttl/...` and `shacl/releases/v0.0.1/ttl/...`, not the ordinal or filename-derived defaults.
-- Add tests for `owl:versionIRI` pointing to versioned located Turtle files.
-- Add tests that generated resource pages link to current artifact pages, histories, states, manifestations, and located files without assuming a whole-repo mesh root.
-- Add tests that `RdfDocument` resource pages include escaped raw RDF content from the correct current or historical located file.
-- Add browser-oriented or HTML-level tests for the trailing-slash script if it lands, including relative-link behavior after `history.replaceState`.
-- Keep visual/regression checks for the generated resource-page template baseline in the renderer task, not in the Fantasy Rules Sidecar fixture acceptance layer.
-
-## Non-Goals
-
-- Building a complete fantasy rules ontology.
-- Modeling the full SRD 5.2.1.
-- Modeling spells, monsters, equipment, classes, species, conditions, or combat systems in the first fixture.
-- Making the fixture depend on URPX-specific terms or release policy.
-- Replacing Alice Bio as the whole-repo reference mesh.
-- Enabling arbitrary remote current-byte fetching.
-- Treating `workingAccessUrl` or `targetAccessUrl` as live fetch inputs unless a separate operational-policy slice explicitly enables them.
-- Making the generated resource pages a full client-side app.
-- Requiring JavaScript for dereferenceability or basic navigation.
-
-## Implementation Plan
-
-### Phase 0: Fixture Shape, Ladder, And Source Policy
-
-- [x] Confirm the first public base IRI for `mesh-sidecar-fantasy-rules`.
-- [x] Draft the small first ontology slice around `AbilityScore`, `Alignment`, `Character`, and representative controlled values or examples.
-- [x] Review SRD 5.2.1 source for the small seed slice and defer larger SRD modeling.
-- [x] Plan the SRD CC-BY-4.0 attribution boundary for `NOTICE.md` and choose source/provenance metadata for the ontology.
-- [x] Use slash IRIs for first-pass ontology terms.
-- [x] Draft the initial numbered branch ladder through `07-shacl-integrated-woven`, preserving the Alice Bio distinction between non-woven operation branches and `*-woven` branches.
-- [x] Create the first `semantic-flow-framework/examples/sidecar-fantasy-rules/conformance/README.md` plan before the first non-seed transition is treated as settled.
-- [x] Define the branch-ref testing loop for source branch, operation execution, destination branch, and Accord manifest comparison.
-- [x] Put first-pass mesh-owned helper page content under `docs/_mesh/content/`.
-
-### Phase 1: Build Out The Sidecar Fixture Repo
-
-- [x] Initialize or update `mesh-sidecar-fantasy-rules` as the sidecar fixture repo.
-- [x] Add authored ontology source under `ontology/`.
-- [x] Add authored SHACL source under `shacl/`.
-- [x] Add a first-pass example under `examples/`.
-- [x] Add `NOTICE.md` with the SRD 5.2.1 CC-BY-4.0 attribution boundary.
-- [x] Expand `weave mesh create` so `--workspace . --mesh-root docs --mesh-base https://semantic-flow.github.io/mesh-sidecar-fantasy-rules/` creates the docs-rooted mesh support surface.
-- [x] Add `.nojekyll` from `weave mesh create` by default for GitHub Pages publishing targets, with an opt-out switch.
-- [x] Have `weave mesh create` create sidecar mesh-owned config at `docs/_mesh/_config/config.ttl` with `sfcfg:workspaceRootRelativeToMeshRoot "../"`.
-- [x] Author the `01-source-only` to `02-sidecar-mesh-created` Accord manifest before generating the branch output.
-- [x] Generate the `02-sidecar-mesh-created` fixture branch by running the current docs-rooted `weave mesh create` command from `01-source-only`.
-- [x] Verify the generated `02-sidecar-mesh-created` branch output against the transition manifest.
-- [x] Make `weave`, `weave validate`, `weave version`, and `weave generate` resolve from a mesh root, infer workspace root from mesh config when present, and otherwise treat the mesh root as the workspace root.
-- [x] Add the first mesh-support-only weave transition so `02-sidecar-mesh-created` can produce the `03-sidecar-mesh-created-woven` current support ResourcePages without requiring an application Knop or payload candidate, including the sidecar config support artifact when present.
-- [x] Make existing-mesh CLI operations mesh-root centered: use `--mesh-root` for operations after mesh creation, infer workspace root from mesh config, and keep `--workspace` only on `weave mesh create`.
-
-### Phase 2: Integrate Ontology And SHACL Artifacts
-
-- [x] Add an explicit `weave integrate --grant-source-directory` path so sidecar artifact integration can add the corresponding constrained mesh-carried source-directory rule while keeping ungranted extra-mesh source access fail-closed.
-- [x] Integrate the ontology artifact at public path `ontology`.
-- [x] Add the constrained `sfcfg:hasLocalPathAccessRule` entry for `../ontology/` as part of ontology artifact integration.
-- [x] Integrate the SHACL artifact at public path `shacl`.
-- [x] Ensure SHACL integration adds the constrained `sfcfg:hasLocalPathAccessRule` entry for `../shacl/`; the grant should be created by the integration operation that introduces the adjacent SHACL source artifact.
-- [x] Add the constrained `sfcfg:hasLocalPathAccessRule` entry for `../examples/` only when example datasets are integrated as sidecar artifacts.
-- [x] Use `workingLocalRelativePath` to associate the ontology artifact with its adjacent authored source file.
-- [x] Use `workingLocalRelativePath` to associate the SHACL artifact with its adjacent authored source file.
-- [x] Keep `hasWorkingLocatedFile` usage semantically consistent with the current located-byte story.
-- [x] Add current resource pages for root, ontology, and relevant support artifacts.
-- [x] Add current resource pages for SHACL and relevant support artifacts.
-- [x] Add the Accord manifest for the ontology integration transition.
-- [x] Add Accord manifests for the remaining SHACL integration transitions as they settle.
-
-### Phase 3: Weave The First Release
-
-- [x] Complete `10-root-knop`, including a root Knop and an `examples/` collection Knop, before the first named release pair.
-- [x] Weave the root and `examples/` collection Knops in `11-root-knop-woven`.
-- [x] Integrate the Gunaar example dataset at public path `examples/gunaar` in `12-gunaar-example-dataset`.
-- [x] Ensure Gunaar dataset integration adds the constrained `sfcfg:hasLocalPathAccessRule` entry for `../examples/`; the grant should be created by the integration operation that introduces the adjacent example source artifact.
-- [x] Use `workingLocalRelativePath` to associate the Gunaar dataset artifact with `../examples/gunaar.ttl`.
-- [x] Weave the Gunaar dataset in `13-gunaar-example-dataset-woven`.
-- [x] Weave ontology release `v0.0.1` under `ontology/releases/v0.0.1`.
-- [x] Weave SHACL release `v0.0.1` under `shacl/releases/v0.0.1`.
-- [x] Treat ontology and SHACL release bumps as a pair in the fixture, even when only one source file changes.
-- [x] Materialize Turtle manifestations under each release state using the `ttl` manifestation segment.
-- [x] Exercise multiple ArtifactHistories by creating or selecting the named `releases` history while preserving earlier ordinal publication histories.
-- [x] Ensure `owl:versionIRI` points at the versioned located Turtle file.
-- [x] Ensure working source bytes and latest historical located bytes match where the release is current.
-- [x] Add Accord manifests for the first ontology and SHACL release/weave transitions as they settle.
-
-### Phase 3B: Version-Bump Follow-Up Pair
-
-- [ ] Add `16-version-bump` and `17-version-bump-woven` as the follow-up ontology and SHACL version-bump pair after the first release ladder is working.
-- [ ] In `16-version-bump`, update the authored ontology and SHACL sources with `v0.0.2` release metadata, at least one changed term that already has an extracted page, and at least one new mesh-scoped term that all-terms extraction should discover.
-- [ ] In `17-version-bump-woven`, weave the paired `v0.0.2` ontology and SHACL releases first, then run `weave extract --all-terms --source ontology --accept-preview`, `weave extract --all-terms --source shacl --accept-preview`, `weave set extraction-source --all-terms --source ontology --accept-preview`, and `weave set extraction-source --all-terms --source shacl --accept-preview`, and then regenerate/weave pages as needed.
-- [ ] Use the follow-up pair to prove extracted term pages update when their source RDF changes by migrating the existing pinned term inventories to current-tracking source resolution before page generation.
-- [ ] Use the follow-up pair to test how datasets, ontology files, SHACL files, release histories, extracted term pages, and generated pages behave when only part of the source content has semantic changes but the published compatibility pair advances together.
-- [ ] Use the follow-up pair to prove broad payload state naming for ontology and SHACL together, such as one request-level/default state segment applied to both selected payload artifacts.
-
-### Phase 3C: Explicit Return To Ordinal Sequencing
-
-- [ ] Add a later pair for explicitly returning from named release state sequencing to default ordinal state or history sequencing.
-- [d] Defer the previously considered immediate ordinal-return branch pair; `16/17` are now reserved for the version-bump pair.
-- [ ] Alternatively use a named-history state fallback pair if the more important behavior is explicitly requesting `stateSegment=_s0001` under the existing `releases` history.
-- [x] Keep this pair explicit; a broad weave with omitted state naming after `v0.0.1` should fail closed with a message explaining how to provide `stateSegment` or choose ordinal fallback.
-
-Settled API/CLI surface: there is no separate "return to ordinal sequencing" command. The operator uses the existing payload version naming fields on `weave` or `weave version`. To continue semver in the current named history, provide `stateSegment=v0.0.2`. To explicitly fall back to ordinal states inside the current `releases` history, provide `stateSegment=_s0001`. To start a fresh ordinal history after `releases`, provide `historySegment=_history002`; if no state segment is supplied for that new history, the default state is `_s0001`, though the fixture may choose to pass `stateSegment=_s0001` as documentation-by-command. Request-level defaults such as `--payload-state-segment _s0001` and `--payload-history-segment _history002` may apply broadly to selected payload artifacts, while target-specific `historySegment` and `stateSegment` values override those defaults.
-
-### Phase 4: Resource Page Behavior
-
-- [d] Keep the target page model for ontology and SHACL artifact pages in the renderer task unless the sidecar fixture exposes a missing Semantic Flow behavior requirement.
-- [d] Defer broader resource-page look-and-feel improvements to [[wa.completed.2026.2026-05-03-resource-page-renderer-refresh]].
-- [x] Keep current artifact pages sufficient to show identity, current bytes, histories, and support resources for the fixture contract.
-- [x] Show every `sflo:hasArtifactHistory` on artifact pages, not only `sflo:currentArtifactHistory`, ordered with the current/latest history first so named `releases` histories do not hide earlier ordinal histories.
-- [x] Keep historical-state and located-file pages sufficient for navigating existing woven history without reading raw Turtle first.
-- [x] Add raw RDF panels to `RdfDocument` resource pages for locally available current and historical bytes.
-- [x] Move reusable page HTML/CSS rendering toward shared runtime seams rather than fixture-specific builders.
-- [x] Add generic History-section truncation for repeated lists longer than 10 items: show the first 2 and last 7 with a vertical ellipsis gap marker.
-- [d] Add or update specs for resource-page presentation in the renderer task if that contract changes materially.
-- [d] Do not make renderer-specific prose, layout, or visual expectations part of Fantasy Rules Sidecar Accord manifests.
-
-### Phase 5: URL Polish Experiment
-
-- [x] Design the canonical-IRI display script for generated `index.html` pages.
-- [x] Require an explicit canonical IRI signal before trimming a trailing slash.
-- [x] Preserve relative-link behavior with root-relative/absolute links or an explicit safe `` strategy.
-- [x] Keep pages usable with JavaScript disabled.
-- [x] Add tests for slashful load URL, slashless displayed URL, canonical link, and link navigation.
-
-Settled behavior: `sflo:meshBase` stays trailing-slash for RDF and URL resolution, while generated resource-page canonical links use slashless resource IRIs where the resource path is slashless, including the mesh repo root page. The default page script reads the explicit canonical link and only calls `history.replaceState` when the current slashful path exactly matches the canonical slashless path, with no query or hash. Generated mesh navigation links remain root-relative, so link behavior survives URL polish and pages remain usable with JavaScript disabled.
-
-Root resource-page titles and visible root designator labels should use the mesh segment, such as `mesh-sidecar-fantasy-rules`, when Weave can derive it from `sflo:meshBase`; bare `/` remains an input/path sentinel, not the default public label.
-
-### Phase 6: Acceptance And Documentation
-
-- [x] Add Weave integration/e2e tests for docs-rooted sidecar operation.
-- [ ] Migrate the default manifestation segment derivation to extension-backed segments and re-ladder `mesh-alice-bio` for the new default.
-- [ ] Update [[wu.repository-options]] if the fixture changes the sidecar recommendation.
-- [x] Update [[wd.codebase-overview]] once implementation lands.
-- [ ] Update [[wd.decision-log]] with settled sidecar, release-path, and resource-page decisions before closing the task.
diff --git a/documentation/notes/wd.task.2026.2026-05-04-extraction-improvements.md b/documentation/notes/wd.task.2026.2026-05-04-extraction-improvements.md
index 57a2412..b6bda3f 100644
--- a/documentation/notes/wd.task.2026.2026-05-04-extraction-improvements.md
+++ b/documentation/notes/wd.task.2026.2026-05-04-extraction-improvements.md
@@ -25,7 +25,7 @@ The improved model is:
- `Pinned` source resolution is explicit and requires a historical source state.
- `extract` creates missing extracted identifier surfaces and skips already-created surfaces in batch mode.
- a dedicated `set extraction-source` command updates the source-resolution contract for an existing extracted Knop.
-- generated pages resolve source-derived RDF facts through the extracted Knop's inventory-carried `sfc:ExtractionSource`, using current or pinned semantics as recorded there.
+- generated pages resolve source-derived RDF facts through the extracted Knop's linked source registry and its `sfc:ExtractionSource`, using current or pinned semantics as recorded there.
This task should update the CLI, runtime extraction planning, generated page source loading, tests, and user-facing docs. It should also create the behavior needed before the Fantasy Rules sidecar `17-version-bump-woven` rung can prove existing extracted term pages update after the ontology or SHACL source advances to `v0.0.2`.
@@ -33,7 +33,7 @@ This task should update the CLI, runtime extraction planning, generated page sou
The important distinction is between extraction as creation and extraction-source management as later maintenance.
-`weave extract` is allowed to skip existing extracted Knops. That is sensible for `--all-terms`, where the operator wants to mint missing term surfaces from a source RDF graph without rewriting every already-governed identifier. But skipping existing Knops means `extract --all-terms` cannot be the only operation that changes an already extracted term from a pinned source to a current-tracking source. That change should be a deliberate update to the Knop inventory's source-resolution contract.
+`weave extract` is allowed to skip existing extracted Knops. That is sensible for `--all-terms`, where the operator wants to mint missing term surfaces from a source RDF graph without rewriting every already-governed identifier. But skipping existing Knops means `extract --all-terms` cannot be the only operation that changes an already extracted term from a pinned source to a current-tracking source. That change should be a deliberate update to the Knop source registry's source-resolution contract.
The CLI should avoid vague confirmation flags. `--yes` does not say what is being accepted. For all-terms extraction, the operator is accepting the previewed list of identifiers to create, so the noninteractive flag should be `--accept-preview`. Because Weave is pre-v1, this task can remove or replace `--yes` rather than preserving it as a compatibility alias unless a short transition proves necessary.
@@ -89,7 +89,7 @@ This task is related to but distinct from forced unchanged state creation. The s
- Add `weave set extraction-source` as the dedicated command for changing an existing extracted Knop's source-resolution contract.
- `weave set extraction-source --all-terms` should update every already extracted term discovered in the selected source graph after preview acceptance. The project has release history, so an operator can revert if a broad migration is wrong.
- Treat it as an error when `weave extract --source-state ` or `weave set extraction-source --source-state ` selects a pinned historical state that does not mention the target term.
-- Enforce one primary extraction source per extracted Knop inventory. SHACL should express this as `sh:maxCount 1` for `sfc:hasExtractionSource` where the shape applies.
+- Enforce one primary extraction source per extracted Knop. SHACL should express this as `sh:maxCount 1` for `sfc:hasExtractionSource` where the shape applies, while the extraction-source details live in the Knop's `_sources` registry.
- Rename all-terms noninteractive confirmation from `--yes` to `--accept-preview`.
- Remove `--source-designator-path` immediately instead of keeping it as a deprecated alias for `--source`.
- Remove `--yes` immediately instead of keeping it as a deprecated alias for `--accept-preview`.
@@ -107,10 +107,10 @@ This task is related to but distinct from forced unchanged state creation. The s
- `weave set extraction-source --source-state ` replaces the existing extracted Knop's source binding with a pinned source binding.
- `weave set extraction-source --all-terms --source --accept-preview` previews and updates existing extracted terms discovered in the selected source graph to current-tracking source resolution.
- `weave set extraction-source --all-terms --source-state --accept-preview` previews and updates existing extracted terms discovered in the selected source graph to pinned source resolution.
-- Generated resource pages for extracted terms must read source-derived RDF facts using the recorded `ExtractionSource` resolution mode.
+- Generated resource pages for extracted terms must read source-derived RDF facts using the recorded `ExtractionSource` resolution mode from the Knop's linked source registry.
- Current-tracking generated pages use the source artifact's current latest state at generation time.
- Pinned generated pages use the recorded `sfc:hasRequestedTargetState`.
-- Existing `Pinned` inventories remain valid.
+- Inventory-rooted extraction-source records are obsolete; stale fixture refs should be regenerated to the `_sources` registry shape rather than carried as a compatibility mode.
## Testing
@@ -126,7 +126,7 @@ This task is related to but distinct from forced unchanged state creation. The s
- Add runtime and CLI tests for `weave set extraction-source --source `.
- Add runtime and CLI tests for `weave set extraction-source --source-state `.
- Add tests proving `weave set extraction-source` replaces the existing binding rather than appending a second `sfc:hasExtractionSource`.
-- Add SHACL or ontology validation coverage for at-most-one `sfc:hasExtractionSource` where that shape is maintained in this repository or the ontology dependency.
+- Add SHACL or ontology validation coverage for at-most-one `sfc:hasExtractionSource` where that shape is maintained in this repository or the ontology dependency, plus source-registry validation for the owned source binding.
- Update sidecar integration coverage so the `16/17` version-bump pair can migrate existing extracted terms to current source resolution and then verify refreshed extracted pages.
## Non-Goals
@@ -141,7 +141,7 @@ This task is related to but distinct from forced unchanged state creation. The s
## Implementation Plan
-- [ ] Update this task note and related sidecar/all-terms notes with the settled extraction-source resolution contract.
+- [x] Update this task note and related sidecar/all-terms notes with the settled extraction-source resolution contract.
- [x] Update [[wu.cli-reference]] with `--source`, `--source-state`, `--accept-preview`, and `weave set extraction-source`.
- [ ] Update the relevant Semantic Flow behavior spec, likely [[sf.spec.2026-04-05-extract-behavior]], for current and pinned source-resolution semantics.
- [x] Extend extraction planning so `ExtractionSource` can be rendered in current or pinned mode.
@@ -150,7 +150,7 @@ This task is related to but distinct from forced unchanged state creation. The s
- [x] Update generated page source loading so current-mode extraction sources resolve the source artifact's current latest state at generation time.
- [x] Add `weave set extraction-source` runtime support for one target.
- [x] Add `weave set extraction-source --all-terms` runtime support for batch migration/update.
-- [ ] Add or update SHACL constraints so an extracted Knop inventory has at most one `sfc:hasExtractionSource`.
+- [x] Add or update SHACL constraints so an extracted Knop has at most one `sfc:hasExtractionSource` and extraction-source details are accepted in `_sources`.
- [x] Update unit, integration, and e2e tests for the new contract.
- [x] Decide whether to retrofit existing Fantasy Rules `08/09` rungs or keep them pinned and migrate in `16/17`.
- [x] Update [[wd.decision-log]] once behavior is implemented and the fixture policy is settled.
diff --git a/documentation/notes/wd.task.2026.2026-05-04-refactor-planFirstPayloadWeave.md b/documentation/notes/wd.task.2026.2026-05-04-refactor-planFirstPayloadWeave.md
index e728e8f..de75412 100644
--- a/documentation/notes/wd.task.2026.2026-05-04-refactor-planFirstPayloadWeave.md
+++ b/documentation/notes/wd.task.2026.2026-05-04-refactor-planFirstPayloadWeave.md
@@ -52,10 +52,17 @@ The failure mode appears when MeshInventory contains more than the one exact fix
That is a reasonable pre-weave state. The mesh inventory has registered two Knops and two payload artifacts; neither payload has a history yet. The user intent is clear: version both first payloads and produce pages. The current planner instead behaves as though first-payload weave can only ever occur against one settled branch shape.
-After working around that first-payload limitation, the URPX run also showed a stale extracted-Knop assertion. For an extracted term such as `ontology/A`, `docs/ontology/A/_knop/_inventory/inventory.ttl` can legitimately contain:
+After working around that first-payload limitation, the URPX run also showed a stale extracted-Knop assertion. For an extracted term such as `ontology/A`, `docs/ontology/A/_knop/_inventory/inventory.ttl` can legitimately link a source registry:
```turtle
- a sfc:ExtractionSource ;
+ sfc:hasExtractionSource ;
+ sfc:hasKnopSourceRegistry .
+```
+
+and `docs/ontology/A/_knop/_sources/sources.ttl` can carry:
+
+```turtle
+ a sfc:ExtractionSource ;
sfc:hasTargetArtifact ;
sfc:hasArtifactResolutionMode .
```
diff --git a/documentation/notes/wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default.md b/documentation/notes/wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default.md
index dfd3010..0701722 100644
--- a/documentation/notes/wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default.md
+++ b/documentation/notes/wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default.md
@@ -31,11 +31,25 @@ That suggests a better target: move mutable current/progression facts out of inv
Historical resource-page regeneration should not require re-weaving from old mutable current pointers. If historical pages need to be regenerated, the durable input should be a generation manifest, render manifest, checkpoint, or source-state bundle that records the concrete source artifact states, page definition state, reference catalog state, renderer/config state, and output paths used when the page was generated. For testing, mutable values needed to reproduce a weave can also be captured in the fixture manifest. Re-weaving old mesh states is not a general user-facing requirement.
-Payloads should keep history by default. `_mesh/_inventory` and `_knop/_inventory` should keep their current history behavior in the immediate quick fix because the current implementation still reads inventory history/progression shape to plan later weaves. But the longer direction is to stop using full inventory history as the blunt tool for mutable progression and historical page regeneration. After the mutable-state split, inventory can likely become current-only, delta/checkpoint based, or metadata-only historical by default, with historical inventory HTML pages suppressed or deferred.
+Payloads should keep history by default. `_mesh/_inventory` and `_knop/_inventory` are current-only by default in the Weave default profile, but many current implementation paths still read inventory history/progression shape to plan later weaves. The longer direction is to stop using full inventory history as the blunt tool for mutable progression and historical page regeneration. After the mutable-state split, inventory can be current-only, delta/checkpoint based, or metadata-only historical by default, with historical inventory HTML pages suppressed or deferred.
-The first slim-history pass should target support artifacts whose history is mostly noise: `_mesh/_meta`, `_knop/_meta`, and probably `_mesh/_config`. These can remain current DigitalArtifacts with working located files and current resource pages, but omit initial `ArtifactHistory`, `HistoricalState`, manifestation, snapshot, and history-page generation by default. If `_meta` becomes the home for mutable current/progression facts, it is still allowed to be current-only by default; those facts are working state, not necessarily historical source material.
+The first slim-history pass should target support artifacts whose history is mostly noise: `_mesh/_meta`, `_knop/_meta`, and inventory paths where the planner no longer depends on inventory history shape. These can remain current DigitalArtifacts with working located files and current resource pages, but omit initial `ArtifactHistory`, `HistoricalState`, manifestation, snapshot, and history-page generation by default. Authored config artifacts are different: after the grand config synthesis decision, `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_local-config`, `_knop/_inheritable-config`, and reusable config artifacts are versioned by default. If `_meta` becomes the home for mutable current/progression facts, it is still allowed to be current-only by default; those facts are working state, not necessarily historical source material.
-Do not solve full config first. Introduce a small internal policy seam now, default it conservatively, and let later config work feed that seam. Do not combine this with a general "turn off resource page generation" feature yet; that is related but has a different contract around dereferenceability and `sflo:hasResourcePage` facts.
+Do not wait for the entire config resolver before landing bridge slices. Use the default effective config and keep the seam narrow. Do not combine this with a general "turn off resource page generation" feature yet; that is related but has a different contract around dereferenceability and `sflo:hasResourcePage` facts.
+
+### Current Alignment With Grand Config Synthesis
+
+[[wd.task.2026.2026-05-06-grand-config-synthesis]] now supersedes the pre-config parts of this note. We no longer need to invent a separate temporary policy model here: Weave's checked-in default profile under `defaults/` is the policy source for the first runtime slices.
+
+Important deltas from the original quick-fix sketch:
+
+- `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_local-config`, `_knop/_inheritable-config`, and reusable authored config artifacts are versioned by default, with current ResourcePages generated by default.
+- `_mesh/_meta`, `_knop/_meta`, `_mesh/_inventory`, and `_knop/_inventory` default current-only unless overridden, but inventory remains transitional because current weave planners still read inventory progression/history shape in many paths.
+- The first implemented bridge slice wires the default effective config into mesh support ResourcePage catch-up: `_mesh/_meta` and `_mesh/_inventory` remain current-only there while `_mesh/_config` is versioned.
+- The second bridge slice generalizes that support-history policy seam into core weave planning and applies default effective config to first Knop and first payload weave outputs: `_knop/_meta` remains current-only while payload history and `_knop/_inventory` history stay unchanged.
+- The broad fixture-visible implementation should still wait until the config vocabulary, default profile, inherited propagation semantics, and fixture-ladder generator are stable enough to avoid rerunning fixture repair twice.
+
+So the answer is: start implementing narrow bridge slices now, but do not perform the full slim-support fixture migration until the config synthesis first pass is settled.
## Discussion
@@ -64,6 +78,31 @@ Right now `_mesh/_inventory` is the durable mesh-level snapshot because the impl
That is different from preserving payload history. Payload historical states are user-facing resources. Mutable pointers to the latest payload state are working state.
+### Page-generation manifest contract
+
+Historical ResourcePage regeneration should be driven by a render/provenance manifest, checkpoint, or equivalent source-state bundle. The manifest is derived runtime evidence, not authored portable config. It can be stored outside the mesh by default, bundled with fixture manifests for tests, or deliberately promoted into a mesh artifact when a project wants auditable page-render provenance.
+
+The minimal manifest contract should record:
+
+- the generated page path and the resource IRI/path that page represented
+- page kind, at least current artifact/resource, `ArtifactHistory`, `HistoricalState`, and `ArtifactManifestation`
+- generated timestamp, renderer identifier/version, and Weave version or renderer implementation digest
+- selected `ResourcePageRegenerationConfigPolicy` mode
+- source artifact states used for semantic content, including payload/source state, inventory state, reference catalog state, extraction-source target state, and any other required source snapshots
+- presentation inputs, including ResourcePageDefinition, template, stylesheet, presentation config, and built-in renderer/theme identifiers or digests
+- relevant `ResolvedConfig` digest plus the config-source fingerprints or pinned states needed to explain that resolved config
+- output digest for the generated HTML file
+- warnings for missing optional inputs and failures for missing required inputs
+
+The regeneration modes from [[wd.task.2026.2026-05-06-grand-config-synthesis]] need different required inputs:
+
+- `configAtTheTime` requires enough manifest data to recover the page definition, template/stylesheet/presentation config, relevant resolved config, renderer identity, and semantic source snapshots from the original render. If those required inputs are missing, regeneration should warn and fail that page rather than silently rendering with current defaults.
+- `currentPresentation` uses historical semantic/source snapshots but current page-definition/template/stylesheet/presentation config. It still requires pinned historical content/source inputs.
+- `currentFullConfig` uses current config wherever compatible with the historical source state. It is useful for administrative rebuilds but should be labeled less faithful because config drift can change output.
+- `historicalSemanticsCurrentPresentation` preserves historical source-resolution/semantic config while applying current layout/chrome config.
+
+The manifest should never rely on an old inventory's mutable `sflo:latestHistoricalState`, `sflo:currentArtifactHistory`, `sflo:nextStateOrdinal`, or `sflo:nextHistoryOrdinal` facts as unqualified truth. If an old value matters, the manifest should name the concrete state or digest that was used. That lets `_mesh/_inventory` and `_knop/_inventory` move toward current-only or checkpoint-style behavior without making historical page regeneration guess from stale "current" pointers.
+
### Move mutable progression facts out of inventory
Inventory should not be the hot path for every mutable allocator/current pointer if it is also the potentially huge public mesh map.
@@ -76,7 +115,36 @@ Candidate facts to move to `_mesh/_meta`, `_knop/_meta`, or a future explicit wo
- current progression facts for support artifacts
- possibly current working-file pointers, if those are better treated as current working state than public map data
-`_meta` is a reasonable first landing place because it is small and already support-oriented. The long-term ontology should decide whether these are truly metadata facts or whether Weave needs a more specific working-state/progression artifact. Either way, the design should avoid requiring a full inventory snapshot whenever only a small mutable pointer changes.
+`_meta` is the first landing place because it is small and already support-oriented. It should hold current/progression facts, not the whole history. Stable membership stays in inventory/history; `_meta` says where Weave should continue from.
+
+Use the split progression shape:
+
+```ttl
+<_mesh/_inventory>
+ sflo:currentArtifactHistory <_mesh/_inventory/_history001> ;
+ sflo:nextHistoryOrdinal "2"^^xsd:nonNegativeInteger ;
+ sfcfg:hasNextHistorySegmentHint "_history002" .
+
+<_mesh/_inventory/_history001>
+ sflo:latestHistoricalState <_mesh/_inventory/_history001/_s0007> ;
+ sflo:nextStateOrdinal "8"^^xsd:nonNegativeInteger ;
+ sfcfg:hasNextStateSegmentHint "_s0008" .
+```
+
+Segment hints are candidate names for the next minted history or state. They are not a substitute for the ordinal counters. When an operation supplies an explicit segment, that explicit segment controls the actual minted path. When no operation segment is supplied, a segment hint controls the minted path if present. Otherwise, Weave derives the anonymous ordinal segment from `sflo:nextHistoryOrdinal` or `sflo:nextStateOrdinal`. The ordinal counter always keeps counting monotonically even when a named segment is used, so a later anonymous state does not reuse an ordinal that was skipped by a named state.
+
+The API and CLI need explicit set and clear operations for these next-segment hints. Setting a hint should validate it as a legal unused path segment for the targeted artifact/history and persist it in the relevant `_meta` progression record. Clearing a hint should remove only the hint, not rewind or recalculate the ordinal counter. Operation-supplied segments remain one-shot request values; set/clear hint commands are the durable way to prepare or remove the next default name before a future weave.
+
+Current code audit:
+
+- `sflo:currentArtifactHistory` is read from current inventory by runtime artifact resolvers and version planning to choose the active history for payloads, ReferenceCatalogs, ResourcePageDefinitions, mesh support artifacts, and Knop support artifacts. It is a current selector, not historical evidence. Target home: `_mesh/_meta` or `_knop/_meta` for support artifacts and a future artifact working-state/progression record for payload/config-like governed artifacts.
+- `sflo:latestHistoricalState` is read from the current active history to choose source bytes, validate named-state progression, and plan the next historical state. ResourcePage policy ownership no longer uses it as an ownership edge; it follows stable `sflo:hasHistoricalState` ownership instead. Target home: same progression record as `currentArtifactHistory`.
+- `sflo:nextHistoryOrdinal` and `sflo:nextStateOrdinal` are allocator state. They are not source facts for historical reconstruction and should move out first once each planner has a stable current/progression source outside inventory. Target home: `_mesh/_meta`, `_knop/_meta`, or a dedicated working-state artifact.
+- `sflo:hasWorkingLocatedFile` and `sflo:workingLocalRelativePath` are current source locators used by runtime loaders and page raw-source panels. Mesh-local public located files may remain useful public map facts, but extra-mesh `workingLocalRelativePath` literals are operational/trust-gated current inputs rather than durable historical facts. Target home: current artifact working state, with historical manifests pinning the actual state/manifestation used for old pages.
+- `sflo:hasArtifactHistory`, `sflo:hasHistoricalState`, `sflo:hasManifestation`, historical `sflo:hasLocatedFile`, and truthful `sflo:hasResourcePage` facts should remain inventory/history facts because they describe durable resource membership and generated/public surfaces rather than mutable "current" pointers.
+- Extraction-source bindings and reference-target bindings need a separate pass: pinned source-state bindings are durable page-generation inputs, while current-following bindings are mutable resolution instructions and should be captured in render manifests when they influence historical pages.
+
+The immediate implementation consequence is modest: do not make `_mesh/_inventory` or `_knop/_inventory` fully current-only in broad fixture output until version planning can read current selectors and allocator state from `_meta` or a working-state artifact. But the audit removes the mystery: the blockers are current selectors, allocator counters, and current working locators, not the stable history/state membership facts.
### Artifact classes by default policy
@@ -86,7 +154,7 @@ Recommended default policy:
- `_mesh/_inventory`: keep history on in the immediate implementation because current planner/progression code depends on it; target current-only, delta/checkpoint, or metadata-only history after mutable facts move out and page-regeneration manifests exist.
- `_knop/_inventory`: keep history on in the immediate implementation because current weave progression depends on it; target current-only or slim history once Knop progression facts move to `_knop/_meta` or a dedicated working-state artifact.
- `_mesh/_meta`, `_knop/_meta`: history off by default.
-- `_mesh/_config`: history off by default for the quick fix, unless a future config story explicitly chooses to version mesh policy changes.
+- `_mesh/_config` and `_mesh/_knop-inheritable-config`: versioned by default under the grand config synthesis defaults, because portable authored config should preserve config-at-the-time for diagnostics, historical page regeneration, and audit.
- `_knop/_assets`: no history by default; if an asset needs independent publication or versioning, model it as its own payload artifact. This aligns with [[wd.task.2026.2026-04-08_1735-page-definition-ontology-and-config]].
- `ResourcePageDefinition` (`_knop/_page`) and `ReferenceCatalog` (`_knop/_references`): behavior-bearing support artifacts. They can become current-only by default only if generated page manifests or page-output durability preserve enough information to regenerate historical pages. Until that contract is explicit, keep them versioned or treat them as a separate policy class.
@@ -94,9 +162,9 @@ The last bullet is the main pushback on "everything else can avoid history." Pag
### Config first?
-Do not block this quick fix on full config.
+Do not block every implementation slice on full config, but do not create a second temporary policy surface either.
-Full config needs mesh/submesh/Knop/artifact inheritance, operational versus portable config boundaries, validation, CLI/runtime loading behavior, and ontology vocabulary. That is too large for this cleanup, and it risks freezing a config surface before the history policy is settled.
+Full config needs mesh/submesh/Knop/artifact inheritance, operational versus portable config boundaries, validation, CLI/runtime loading behavior, and ontology vocabulary. That is too large for this cleanup, and it risks freezing a config surface before the history policy is settled. The grand config synthesis task now owns that vocabulary and default-profile work, so this task should consume those policies through internal seams rather than defining competing defaults.
The implementation should still be shaped for config later:
@@ -105,7 +173,7 @@ The implementation should still be shaped for config later:
- leave current request/CLI surfaces unchanged
- add TODOs or internal types that make it obvious where later config should enter
-Later config can then decide defaults such as `historyPolicy current-only` or `historyPolicy versioned` at mesh, submesh, Knop, artifact-kind, or artifact-specific scope.
+Later resolver work can then decide scoped overrides such as `historyPolicy current-only` or `historyPolicy versioned` at mesh, submesh, Knop, artifact-kind, or artifact-specific scope.
### Resource page generation toggle?
@@ -128,24 +196,24 @@ The safe order is:
- Should historical generated pages be reproducible from mesh state, or is the generated HTML/file output itself the durable historical artifact?
- Can `ResourcePageDefinition` and `ReferenceCatalog` become current-only by default, or do they need history whenever their facts influence historical page output?
-- Which mutable current/progression facts should move from inventory into `_mesh/_meta`, `_knop/_meta`, or a dedicated working-state artifact?
+- Should mutable current/progression facts live directly in `_mesh/_meta` / `_knop/_meta`, or should Weave introduce a more explicit working-state/progression artifact?
- Is `_knop/_inventory` conceptually required to have history, or is that only a current implementation dependency that should be replaced by a more explicit Knop progression model?
- Can `_mesh/_inventory` become current-only by default once historical page regeneration is driven by manifests/checkpoints rather than full inventory snapshots?
-- What should a page-generation manifest record: source artifact states, page definition state, reference catalog state, renderer version, config/effective policy, output path, checksums, or full source snapshots?
-- Should `_mesh/_config` ever be historical by default, or should config changes be tracked through repository history and current mesh state unless explicitly opted in?
-- What is the future policy vocabulary: boolean flags, a small enum such as `current-only` / `versioned`, or an artifact-class default with per-artifact overrides?
-- Where should inheritable policy live once config is ready: mesh config, Knop config, artifact-local config, operational config, or some combination?
+- Which page-generation manifest fields should be mandatory for each page kind, and when should Weave store a full source snapshot instead of only state/digest references?
+- Can `_mesh/_config` history ever be safely suppressed for tiny/local-only meshes, or is versioned config always the safer default?
+- How should the first resolver surface scoped overrides for default history policy without letting portable config weaken trusted runtime invariants?
+- Where should inheritable history policy live in practice: `_mesh/_knop-inheritable-config`, `_knop/_inheritable-config`, artifact-local config, operational config, or some combination?
- If resource page generation becomes configurable, what exact RDF should be emitted when a page is intentionally not generated?
## Decisions
- Payload artifacts keep history by default.
-- Keep `_mesh/_inventory` history on in the immediate quick fix because current weave planning depends on the existing inventory history/progression shape.
-- Keep `_knop/_inventory` history on in the immediate quick fix because current weave progression depends on it.
+- `_mesh/_inventory` and `_knop/_inventory` are current-only by default in the Weave default profile, but applying that behavior everywhere is blocked by current weave planning paths that still depend on inventory history/progression shape.
+- Keep `_knop/_inventory` history rendering unchanged until Knop progression facts move out of inventory or the planner can resolve them from a narrower working-state source.
- Longer-term direction: move mutable current/progression facts out of inventory into `_meta` or a dedicated working-state artifact, then make inventory history slim, current-only, delta/checkpoint based, or metadata-only by default.
- Historical resource-page regeneration should be driven by explicit page/render manifests, source-state bundles, generated output durability, or checkpoints rather than relying on stale mutable current pointers in old inventory snapshots.
-- First slim-history implementation should default `_mesh/_meta`, `_knop/_meta`, and `_mesh/_config` to current-only support artifacts.
-- Do not wait for full config before implementing the first default cleanup; create an internal policy seam that later config can drive.
+- First slim-history implementation should default `_mesh/_meta` and `_knop/_meta` to current-only support artifacts, while authored config artifacts remain versioned by default.
+- Do not wait for the full resolver before implementing bridge slices; do wait before broad fixture migration that would bake temporary policy behavior into examples.
- Do not implement a broad resource-page generation toggle in the same first pass.
## Contract Changes
@@ -153,15 +221,15 @@ The safe order is:
- Current-only support artifacts are valid DigitalArtifacts when they have current working-file facts and resource-page facts but no `sflo:hasArtifactHistory`, `sflo:currentArtifactHistory`, `sflo:nextHistoryOrdinal`, `ArtifactHistory`, `HistoricalState`, or manifestation snapshot for that support artifact itself.
- For artifacts whose history is disabled, Weave should not emit history/state/manifestation resource pages or `sflo:hasResourcePage` facts for those omitted historical resources.
- Payload artifact history behavior is unchanged.
-- `_mesh/_inventory` and `_knop/_inventory` history behavior is unchanged in the first pass, but this is now treated as an implementation dependency rather than a permanent conceptual requirement.
+- `_mesh/_inventory` and `_knop/_inventory` history behavior is transitional. The mesh support ResourcePage catch-up path can already honor current-only mesh inventory policy, and the first Knop/payload/extracted weave planners now read MeshInventory current/latest/next progression from `_mesh/_meta`; broader inventory and Knop-inventory current-only behavior still waits on the remaining progression seams.
- Future page regeneration contracts should prefer explicit generation manifests/checkpoints that pin concrete source states over full copied inventory snapshots.
- Future inventory contracts should distinguish public map facts from mutable current/progression facts.
-- No public config, CLI flag, or request-field contract is introduced in the quick fix.
+- No CLI flag or request-field contract is introduced in the quick fix. Default behavior comes from Weave's checked-in default config profile as that profile becomes wired into runtime paths.
## Testing
- Core planner tests should assert that new first-weave outputs omit `_mesh/_meta` and `_knop/_meta` history triples, snapshot files, and history/state/manifestation pages when the default policy is current-only.
-- Mesh support resource-page tests should assert `_mesh/_config` does not get default history when present, while its current page and working file remain represented.
+- Mesh support resource-page tests should assert `_mesh/_meta` and `_mesh/_inventory` can keep current pages without new support history while `_mesh/_config` remains versioned by default.
- Existing payload tests should continue to assert payload `ArtifactHistory`, first `HistoricalState`, manifestation, snapshot, and history pages.
- Existing mesh and Knop inventory tests should continue to assert inventory history advancement and next-state ordinal behavior.
- Runtime/integration tests should verify generated current pages do not link to omitted support-history pages.
@@ -172,25 +240,29 @@ The safe order is:
## Non-Goals
- Do not disable payload history by default.
-- Do not disable `_mesh/_inventory` history in this task.
+- Do not force all `_mesh/_inventory` planning paths current-only in this task; apply it only where the planner no longer depends on inventory history shape.
- Do not disable `_knop/_inventory` history in the first implementation pass.
- Do not require full inventory snapshots forever as the only way to regenerate historical resource pages.
- Do not promise general re-weaving of old mesh states as a user-facing feature; test fixtures may capture additional mutable state in manifests when needed.
-- Do not design or expose the full inheritable config surface here.
+- Do not design or expose the full inheritable config surface here; consume the terms and defaults from [[wd.task.2026.2026-05-06-grand-config-synthesis]].
- Do not add a general resource-page generation toggle here.
- Do not migrate or delete already-generated historical support artifacts in existing carried fixtures unless a fixture refresh explicitly requires it.
- Do not treat `_knop/_assets` files as governed artifacts; assets remain helper files unless separately modeled as payload artifacts.
## Implementation Plan
-- [ ] Introduce an internal support-history policy helper that can answer whether a candidate artifact role should create history by default.
-- [ ] Classify at least `_mesh/_meta`, `_mesh/_config`, `_knop/_meta`, `_mesh/_inventory`, `_knop/_inventory`, payload artifacts, `ResourcePageDefinition`, and `ReferenceCatalog`.
-- [ ] Audit mutable current/progression facts currently stored in `_mesh/_inventory` and `_knop/_inventory`, and classify which should move to `_mesh/_meta`, `_knop/_meta`, or a future working-state artifact.
-- [ ] Sketch a page-generation manifest/checkpoint contract for historical page regeneration that pins source artifact states instead of relying on old inventory current pointers.
-- [ ] Refactor mesh-support page planning so `_mesh/_meta` and `_mesh/_config` can keep current pages without creating support history.
-- [ ] Refactor first Knop and first payload weave renderers so `_knop/_meta` remains current-only by default.
-- [ ] Keep `_mesh/_inventory` and `_knop/_inventory` history rendering unchanged.
+- [x] Introduce an internal support-history policy seam that can answer whether a candidate artifact role should create history by default for mesh support ResourcePage catch-up.
+- [x] Generalize the support-history policy seam beyond mesh support ResourcePage catch-up.
+- [x] Classify at least `_mesh/_meta`, `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_meta`, `_mesh/_inventory`, `_knop/_inventory`, payload artifacts, `ResourcePageDefinition`, and `ReferenceCatalog`.
+- [x] Audit mutable current/progression facts currently stored in `_mesh/_inventory` and `_knop/_inventory`, and classify which should move to `_mesh/_meta`, `_knop/_meta`, or a future working-state artifact.
+- [x] Sketch a page-generation manifest/checkpoint contract for historical page regeneration that pins source artifact states instead of relying on old inventory current pointers.
+- [x] Refactor mesh-support page planning so `_mesh/_meta` and `_mesh/_inventory` can keep current pages without creating support history when default policy says current-only.
+- [x] Keep `_mesh/_config` versioned in mesh-support page planning unless an explicit future policy overrides it.
+- [x] Refactor first Knop and first payload weave renderers so `_knop/_meta` remains current-only by default.
+- [x] Move the first MeshInventory current/latest/next progression reads and writes from `_mesh/_inventory` into `_mesh/_meta` for first Knop, first payload, and first extracted-Knop weave planning while keeping stable MeshInventory history/state membership in inventory.
+- [x] Keep `_knop/_inventory` history rendering unchanged until a Knop-local progression seam exists.
- [ ] Audit generated page models and hand-rendered pages so current support pages do not link to omitted support histories.
-- [ ] Update focused core and integration tests for the new default output shape.
+- [x] Update focused core tests for the first `_mesh/_meta` MeshInventory progression seam, including hinted named state minting and later ordinal advancement after a named latest state.
+- [ ] Update focused integration tests for the new default output shape after fixture regeneration removes legacy ontology IRI assumptions.
- [ ] Run the relevant Deno validation tasks after code changes, at minimum `deno task test` and `deno task lint` for a broad renderer/planner change.
- [ ] Leave clear TODOs for later config-driven policy and resource-page generation policy.
diff --git a/documentation/notes/wd.task.2026.2026-05-06-grand-config-synthesis.md b/documentation/notes/wd.task.2026.2026-05-06-grand-config-synthesis.md
index 7efec55..8e65af6 100644
--- a/documentation/notes/wd.task.2026.2026-05-06-grand-config-synthesis.md
+++ b/documentation/notes/wd.task.2026.2026-05-06-grand-config-synthesis.md
@@ -27,7 +27,7 @@ The config line needs a deliberate consolidation pass. The current active config
The synthesized direction is:
-- portable authored config belongs in mesh-managed config artifacts such as `_mesh/_config`, `_knop/_local-config`, and `_knop/_inheritable-config`
+- portable authored config belongs in mesh-managed config artifacts such as `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_local-config`, and `_knop/_inheritable-config`
- operational/runtime config supplies trusted runtime inputs and gates, such as host access policy and bootstrap resolver policy
- `ResolvedConfig` is derived resolver output, while effective config is the operation-specific runtime policy object derived from it
- reusable config is a first-class `ConfigArtifact` / `DigitalArtifact` with its own IRI, working file, optional history, and resource page policy
@@ -52,25 +52,25 @@ This task should supersede the older "replace local/inheritable config with mesh
[[wd.task.2026.2026-04-11_1723-operational-config-for-runtime-resolution]] established operational config as a first-class runtime concern for CLI, daemon, and other execution surfaces. It distinguishes mesh-carried expectations from machine-local trust policy and uses deny-by-default local/remote access allow rules. This task keeps that split and adds `ResolvedConfig` for the resolver's derived behavior policy. `ResolvedConfig` may include history, page generation, presentation, naming, and the trust gates that were applied during resolution, but it is derived output rather than the operational input itself.
-[[wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default]] established the need for policy-valued history tracking and page-generation control. Payloads keep history by default. Low-value support surfaces should be slim by default, especially their generated pages. The current Weave planner still reads `_mesh/_inventory` and `_knop/_inventory` history/progression facts to decide the next weave, so the first cleanup should not remove inventory history until those reads are moved. That is a short-term code dependency, not the target model. The longer direction is to move mutable current/progression facts into `_mesh/_meta` and `_knop/_meta`, then make inventory history slim, delta/checkpoint based, or metadata-only by default where full snapshots are unnecessary. Historical page regeneration should be driven by explicit page/render manifests, pinned source states, output durability, checkpoints, or source-state bundles rather than stale mutable current pointers in old inventory snapshots.
+[[wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default]] established the need for policy-valued history tracking and page-generation control. Payloads keep history by default. Low-value support surfaces should be slim by default in their history behavior, but current ResourcePages should still be generated by default so artifacts are dereferenceable unless a config explicitly suppresses or defers a page. Inventory should not keep history by default; `_mesh/_inventory` and `_knop/_inventory` should be current-only unless explicitly configured otherwise. The current Weave planner still reads `_mesh/_inventory` and `_knop/_inventory` history/progression facts to decide the next weave, so the runtime migration needs to move those mutable current/progression facts into `_mesh/_meta` and `_knop/_meta` before the default inventory policy can be applied without fixture churn. Historical page regeneration should be driven by explicit page/render manifests, pinned source states, output durability, checkpoints, or source-state bundles rather than stale mutable current pointers in old inventory snapshots.
### Authored Config Layers
The authored portable config layers should be explicit:
- `_mesh/_config`: mesh-level config for the mesh surface and defaults that apply across the mesh
-- mesh-level inheritable config: defaults the mesh offers to Knops, submeshes, or descendant scopes inside the mesh boundary
+- `_mesh/_knop-inheritable-config`: mesh-level defaults the mesh offers into Knop inheritance inside the mesh boundary
- `_knop/_local-config`: local config for the Knop, its resource page, and artifacts governed at that Knop
- `_knop/_inheritable-config`: defaults the Knop offers to descendant Knops and subtrees
- reusable named config artifacts: ordinary named `ConfigArtifact` resources that may live anywhere in a mesh, such as `alice/alices-favorite-sf-config-setting`, and may be referenced by mesh, Knop, local, or inheritable config
-`_knop/_local-config` and `_knop/_inheritable-config` should be separate `DigitalArtifact`s because they have different semantics, lifecycle, history policy, and attachment behavior. The preferred default is to version them while suppressing noisy ResourcePages unless useful; meshes can still opt specific config artifacts into current-only, checkpoint-only, or metadata-only policy.
+`_knop/_local-config` and `_knop/_inheritable-config` should be separate `DigitalArtifact`s because they have different semantics, lifecycle, history policy, and attachment behavior. The preferred default is to version them and generate current ResourcePages for dereferenceability; meshes can still opt specific config artifacts or historical support pages into suppressed, deferred, current-only, checkpoint-only, or metadata-only policy.
Do not model every authored config layer as a disjoint class. A config artifact can cross layer boundaries: the same reusable artifact might be referenced as a mesh default in one place, a Knop-local override in another, and an inherited policy fragment somewhere else. The durable semantics should come from attachment properties, `ConfigLayerRole` values, and resolution context. Layer-specific classes are optional conveniences only when they add validation value without preventing reuse.
`KnopConfig` is a useful optional scope marker for a portable config artifact attached to a Knop. That is separate from the active ontology's machine/user-local `LocalConfig` meaning, which should be renamed or specialized toward host-local operational config. Knop-local versus Knop-inheritable behavior should be expressed by attachment properties or layer roles.
-Inheritable config should be an attachment/layer role rather than a single class. A mesh can attach inheritable defaults for scopes inside the mesh, and a Knop can attach inheritable defaults for descendant Knops. The source artifact may still just be a `ConfigArtifact`, optionally also a `MeshConfig` or `KnopConfig` when that marker helps validation.
+Inheritable config should be an attachment/layer role rather than a single class. A mesh can attach inheritable defaults for Knop scopes inside the mesh through the canonical support artifact `_mesh/_knop-inheritable-config`, and a Knop can attach inheritable defaults for descendant Knops. The source artifact may still just be a `ConfigArtifact`, optionally also a `MeshConfig` or `KnopConfig` when that marker helps validation.
### Operational Config And Effective Config
@@ -134,11 +134,11 @@ The default behavior config should include defaults such as:
- default history policy is current-only unless an artifact role or artifact-specific policy says otherwise
- payload artifacts are versioned by default
-- portable authored config artifacts are versioned by default, while their ResourcePages may be suppressed by default
-- `_mesh/_inventory` and `_knop/_inventory` keep versioned/slim history only as a transitional Weave implementation default until mutable progression facts move into `_meta`
-- `_mesh/_meta` and `_knop/_meta` may be current-only or slim-history by default
-- current payload resource pages are generated by default
-- support artifact history pages may be suppressed by default even when the support artifact itself is versioned
+- portable authored config artifacts are versioned by default and get current ResourcePages by default unless explicitly suppressed or deferred
+- `_mesh/_inventory` and `_knop/_inventory` are current-only by default unless a mesh explicitly opts inventory into history
+- `_mesh/_meta` and `_knop/_meta` own mutable current/progression facts and may be current-only or slim-history by default
+- current ResourcePages are generated by default for artifacts
+- support artifact history pages may be suppressed or deferred by explicit policy even when the support artifact itself is versioned
- default history segment strategy is ordinal
- default state segment strategy is ordinal
- default manifestation segment strategy is filename/content-kind derived unless explicitly configured
@@ -157,6 +157,7 @@ The practical implementation should keep TTL files as the source of truth and th
<> a sfcfg:ApplicationConfig ;
sfcfg:hasConfigResolutionConfig ;
sfcfg:hasDefaultHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly ;
+ sfcfg:hasDefaultResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_generate ;
sfcfg:hasHistoryTrackingDefault [
a sfcfg:ArtifactRolePolicy ;
sfcfg:hasArtifactRole sfcfg:artifactRole_payload ;
@@ -164,16 +165,15 @@ The practical implementation should keep TTL files as the source of truth and th
], [
a sfcfg:ArtifactRolePolicy ;
sfcfg:hasArtifactRole sfcfg:artifactRole_meshInventory ;
- sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_required
+ sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly
], [
a sfcfg:ArtifactRolePolicy ;
- sfcfg:hasArtifactRole sfcfg:artifactRole_knopMetadata ;
+ sfcfg:hasArtifactRole sfcfg:artifactRole_knopInventory ;
sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly
- ] ;
- sfcfg:hasResourcePageGenerationDefault [
+ ], [
a sfcfg:ArtifactRolePolicy ;
- sfcfg:hasArtifactRole sfcfg:artifactRole_payload ;
- sfcfg:hasResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_generate
+ sfcfg:hasArtifactRole sfcfg:artifactRole_knopMetadata ;
+ sfcfg:hasHistoryTrackingPolicy sfcfg:historyTrackingPolicy_currentOnly
] .
```
@@ -217,6 +217,38 @@ Phase 0 should include an inventory of current implicit defaults in code, API de
- explicit operation request: one-shot target, input, or command intent
- derived `ResolvedConfig` / effective config: runtime output only, not authored input
+### Current Implementation Default Inventory
+
+Current Weave code still carries several fixture-shaped defaults that should become explicit config, operational config, or request data as the resolver lands. This inventory is intentionally descriptive rather than normative; it documents what the code currently does so the first resolver slice can avoid changing behavior accidentally.
+
+Weave default behavior config candidates:
+
+- `defaults/application.ttl` is now the source RDF for the intended default profile: default history is current-only, payload and config artifacts are versioned, mesh and Knop inventory are current-only, runtime meta is current-only, current ResourcePages are generated by default, historical page regeneration defaults to config-at-the-time, history/state naming defaults to ordinal, and manifestation naming defaults to filename-derived.
+- Runtime ResourcePage materialization and versioned inventory rendering now read Weave's default resource-page generation policy by artifact role. `generate` preserves current behavior, `suppress` and `defer` omit matching `sflo:hasResourcePage` facts and generated HTML, and `onRequest` materializes only when the operation has explicit targets. Historical support page regeneration policy still needs its fuller config-at-the-time/current/hybrid behavior; the manifest/checkpoint prerequisite is sketched in [[wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default]].
+- Payload versioning now reads Weave's default naming policies from `defaults/application.ttl`: ordinal history/state policies preserve `_history001` and `_s0001`/`sflo:nextStateOrdinal` behavior, filename-derived manifestation policy preserves extension-based manifestation paths, non-ordinal history/state policies require explicit request segments when Weave cannot infer a concrete segment, and semver/date state policies lightly validate explicit state segments.
+- `mesh create` includes `.nojekyll` by default only when the mesh base host is `github.io` or ends with `.github.io`. That is a publishing-environment default, not portable mesh behavior.
+- ResourcePage rendering defaults to the built-in theme, hides the generated Semantic Flow metadata section unless requested, truncates long history lists with a fixed head/tail policy, and inlines raw source panels only under the current byte limit.
+- Current fixture expectations assume support artifacts are historical and include historical pages. Those history/backfill expectations are migration targets, not policy targets; fixture regeneration should wait until the resolver and inheritance semantics are stable.
+
+Operational config and trusted runtime inputs:
+
+- CLI `--mesh-root` defaults to the current directory and selects the runtime context. It is not portable config.
+- Workspace root inference is currently operational: `_mesh/_config/config.ttl` may carry `sfcfg:workspaceRootRelativeToMeshRoot`, and `loadOperationalLocalPathPolicy` treats it as project-local layout inside the active trust boundary.
+- Local path access is deny-by-default outside the mesh root. Mesh-carried rules, user/machine-local `.sf-local-access.ttl`, and the current `--grant-source-directory` flow are operational access policy, not portable behavior defaults.
+- Mesh-carried path rules may describe project-local workspace access but cannot grant arbitrary host traversal; broader access such as user-home or absolute-path allowances requires higher-trust local config.
+- CLI/runtime logging currently writes under the inferred workspace `.weave/logs` directory and marks commands as `localMode: true`. Log location and local/service mode are operational runtime state.
+
+Explicit operation requests:
+
+- Positional designator paths, `--target`, `--source`, `--source-state`, `--reference-role`, `--all-terms`, `--payload-history-segment`, `--payload-state-segment`, `--payload-manifestation-segment`, and `--mesh-base` describe one operation's subject, source, semantic payload, or requested names.
+- `--accept-preview`, interactive confirmations, `--no-nojekyll`, and future force/dry-run-style controls are safety or publishing-environment request controls.
+- `--include-semantic-flow-metadata` is currently a request flag. It may later become a ResourcePage presentation policy, but until the presentation model is ready it remains request-only.
+
+Derived runtime output:
+
+- `ValidateResult`, `VersionPlan`, `GenerateResult`, `WeaveResult`, resolved local file paths, ResourcePage render models, extracted-source resolution, computed next paths, and generated audit/operational log records are derived output.
+- A future `ResolvedConfig` may record the policy decisions behind those outputs, but the generated outputs themselves must not become authored source config or trust-granting input.
+
### Meta-Config And The Bootstrap Problem
We need config about how config is resolved, but that introduces a bootstrap problem: if ordinary config can decide which config sources are trusted, then untrusted config can grant itself authority.
@@ -323,7 +355,7 @@ A single global precedence order is not enough for every property. The `ConfigPr
- Host trust gates: trusted operational config is not a normal behavior override. If machine-local policy disallows remote config references and mesh config requests them, the result is disallowed. If machine-local config allows only the workspace and workspace-local config points at a project-local config file, the file can participate only inside that boundary.
- Resolver safety caps: stricter policy wins. If Weave defaults allow reference depth 8, machine-local operational config caps it at 4, and portable mesh config requests 12, the result is 4. If mesh config asks to reject unknown terms while defaults merely warn, the stricter rejection can apply.
- Scope-specific behavior defaults: nearest applicable scope wins after trust gates. For resource page presentation, a Knop-local template can override a mesh default template for that Knop, while an artifact-specific presentation policy can override both. A parent Knop's inheritable stylesheet can provide a fallback for descendants that do not specify their own.
-- Required invariants: required or fail-closed policies can dominate ordinary overrides. If `_mesh/_inventory` is marked required for the current implementation but mesh config asks for current-only history, the resolver should either keep the required policy or fail closed rather than silently weakening the ledger.
+- Required invariants: required or fail-closed policies can dominate ordinary overrides. If an artifact role is marked required by the current implementation but mesh config asks for current-only history, the resolver should either keep the required policy or fail closed rather than silently weakening that invariant.
- Operation request fields: explicit command/API fields select what this invocation is asking Weave to do, including targets, source bindings, concrete requested names, and backfill/generation requests. They are not durable config, but they can narrow or specialize a single operation. A `--target` can narrow the operation even if config defaults describe all Knops. A requested payload state segment can override a naming default or hint when it is legal for the current artifact history; Weave should warn when the request overrides a resolved config hint.
- Additive values: some properties merge by union or append rather than winner-takes-all. Page support assets, diagnostic tags, or local presentation affordances may accumulate across mesh and inherited Knop config, subject to deduplication and explicit remove/block rules.
- Reusable config references: reusable config is merged where it is referenced, not at one universal global layer. A reusable presentation profile referenced from mesh config behaves like mesh config; the same profile referenced from Knop-local config behaves like Knop-local config.
@@ -479,6 +511,115 @@ Digest lifecycle should distinguish authoring from verification:
The current CLI/API surface only exposes one concrete resolution-target maintenance flow: extraction source creation and update through `weave extract --source`, `weave extract --source-state`, and `weave set extraction-source`. There is not yet a generic command for authoring config-source resolution targets. The config implementation should define that surface rather than assuming operators will hand-edit RDF forever, but the complete operator-facing command set can follow after the first fixture-visible config pass. A future surface could be shaped like `weave config source add|set|pin|unpin|remove`, with options for target artifact, target state, located file or URL, current versus pinned mode, fallback policy, and optional `--expected-digest`; when omitted, the digest should be computed from resolved bytes during the trusted authoring operation.
+First-pass config-source target management should use the existing `sflo:ArtifactResolutionTarget` contract directly. `weave config source add` creates a new target attached through the requested role-specific property such as `sfcfg:hasMeshConfigSource`, `sfcfg:hasKnopLocalConfigSource`, or `sfcfg:hasKnopInheritableConfigSource`. `set` replaces the attachment for that role/scope, `pin` converts an existing current-following target to a pinned target by recording the resolved state and expected digest, `unpin` removes the requested state and expected digest only when trusted resolver policy allows current-following config, and `remove` deletes the attachment while leaving the reusable `ConfigArtifact` itself untouched. Authoring commands must resolve paths relative to the declaring config file unless an explicit base is modeled, must reject external/current-following targets unless trusted operational config allows them, and must fail when a policy-required digest cannot be computed or explicitly supplied.
+
+### First-Pass Example Bundle
+
+These examples are Weave developer-note implementation sketches for now. They are not yet normative `sflo` examples, and they are not broader Semantic Flow Framework tutorial examples. Once the resolver names and fixture-visible behavior settle, compact normative examples should move into the `sflo` ontology repo, while scenario meshes such as `mesh-sidecar-fantasy-rules` and `mesh-alice-bio` should stay with the framework/examples material.
+
+Mesh config and mesh-inheritable config source attachments:
+
+```turtle
+@base .
+@prefix sflo: .
+@prefix sfcfg: .
+
+<_mesh> a sflo:SemanticMesh ;
+ sfcfg:hasMeshConfigSource [
+ a sflo:ArtifactResolutionTarget ;
+ sflo:hasTargetArtifact <_mesh/_config> ;
+ sflo:hasArtifactResolutionMode sflo:artifactResolutionMode_current
+ ] ;
+ sfcfg:hasMeshInheritableConfigSource [
+ a sflo:ArtifactResolutionTarget ;
+ sflo:hasTargetArtifact <_mesh/_knop-inheritable-config> ;
+ sflo:hasArtifactResolutionMode sflo:artifactResolutionMode_pinned ;
+ sflo:hasRequestedTargetState <_mesh/_knop-inheritable-config/_history001/_s0003> ;
+ sflo:expectsContentDigest "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ] .
+
+<_mesh/_config> a sfcfg:MeshConfig, sfcfg:ConfigArtifact, sflo:DigitalArtifact, sflo:RdfDocument .
+
+<_mesh/_knop-inheritable-config> a sfcfg:MeshConfig, sfcfg:ConfigArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sfcfg:hasDefaultResourcePageGenerationPolicy sfcfg:resourcePageGenerationPolicy_generate .
+```
+
+Knop-local config, Knop-inheritable config, and reusable config imported at the attachment point:
+
+```turtle
+@base .
+@prefix sflo: .
+@prefix sfcfg: .
+
+ a sflo:Knop ;
+ sfcfg:hasKnopLocalConfigSource [
+ a sflo:ArtifactResolutionTarget ;
+ sflo:hasTargetArtifact ;
+ sflo:hasArtifactResolutionMode sflo:artifactResolutionMode_current
+ ] ;
+ sfcfg:hasKnopInheritableConfigSource [
+ a sflo:ArtifactResolutionTarget ;
+ sflo:hasTargetArtifact ;
+ sflo:hasArtifactResolutionMode sflo:artifactResolutionMode_current
+ ] .
+
+ a sfcfg:KnopConfig, sfcfg:ConfigArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sfcfg:hasConfigSource [
+ a sflo:ArtifactResolutionTarget ;
+ sflo:hasTargetArtifact ;
+ sflo:hasArtifactResolutionMode sflo:artifactResolutionMode_pinned ;
+ sflo:hasRequestedTargetState ;
+ sflo:expectsContentDigest "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ ] .
+
+ a sfcfg:KnopConfig, sfcfg:ConfigArtifact, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sfcfg:hasConfigInheritancePolicy sfcfg:configInheritancePolicy_offerDescendantsOnly ;
+ sfcfg:hasDefaultHistoryTrackingPolicy sfcfg:historyTrackingPolicy_versioned .
+```
+
+Host-local operational config remains trusted runtime input:
+
+```turtle
+@prefix sfcfg: .
+
+<> a sfcfg:HostLocalOperationalConfig ;
+ sfcfg:hasLocalPathAccessRule [
+ a sfcfg:LocalPathAccessRule ;
+ sfcfg:hasLocalPathBase sfcfg:localPathBase_userHome ;
+ sfcfg:pathPrefix "semantic-flow/shared-config/" ;
+ sfcfg:hasLocalPathLocatorKind sfcfg:localPathLocatorKind_workingLocalRelativePath
+ ] .
+```
+
+Derived `ResolvedConfig` and resolution records:
+
+```turtle
+@base .
+@prefix sflo: .
+@prefix sfcfg: .
+@prefix xsd: .
+
+<#resolved-alice-knop> a sfcfg:ResolvedConfig ;
+ sfcfg:hasResolvedConfigFor ;
+ sfcfg:resolvedFromConfig , <_mesh/_config>, ;
+ sfcfg:resolvedAt "2026-05-13T12:00:00Z"^^xsd:dateTimeStamp ;
+ sfcfg:hasResolverProfileDigest "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" ;
+ sfcfg:hasTrustPolicyDigest "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ;
+ sfcfg:hasResolvedConfigDigest "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" ;
+ sfcfg:hasConfigResolutionRecord [
+ a sfcfg:ConfigResolutionRecord ;
+ sfcfg:hasConfigLayerRole sfcfg:configLayerRole_knopLocal ;
+ sfcfg:resolvedFromConfig ;
+ sfcfg:hasConfigResolutionStatus sfcfg:configResolutionStatus_accepted ;
+ sfcfg:hasConfigSourceFingerprint "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ ], [
+ a sfcfg:ConfigResolutionRecord ;
+ sfcfg:hasConfigLayerRole sfcfg:configLayerRole_reusableConfig ;
+ sfcfg:resolvedFromConfig ;
+ sfcfg:hasConfigResolutionStatus sfcfg:configResolutionStatus_rejected
+ ] .
+```
+
### Config Resolution Logs And Caches
Runtime logs should remain append-only JSONL, but each record should be shaped as single-line compact JSON-LD rather than plain uncontextualized JSON. The standards-compliant baseline is to put a compact `@context` IRI on each line, not an inline context object. That keeps every line independently parseable as JSON-LD while avoiding a large repeated context. If the repeated context IRI still becomes annoying, Weave may define a non-standard "Weave JSON-LD Lines" profile where the log stream has a sibling context or manifest file and each JSON line carries compact terms plus a context/profile identifier. General JSON-LD consumers would need a small preprocessing step that injects the context before expansion; tools that require standalone JSON-LD records should use the repeated context-IRI form.
@@ -493,6 +634,96 @@ Cache keys should include the resolver/profile digest, Weave version or resolver
Runtime logs, config-resolution records, and `ResolvedConfig` caches should live outside the mesh by default. They can still become in-mesh `DigitalArtifact`s by deliberate integration, just like any other file, but that promotion should be explicit and should consider redaction of host paths, user-local config locations, and security decisions.
+### First-Pass Weave Resolver Contract
+
+The first runtime resolver should be deliberately small but strict enough to keep fixture regeneration from encoding another temporary model.
+
+Trusted bootstrap profile:
+
+- The trusted bootstrap profile is the union of implementation emergency defaults, the checked-in Weave defaults RDF under `defaults/`, explicit runtime inputs, machine-local operational config, and daemon/session profile state.
+- Weave's checked-in defaults are trusted because they ship with the application. Portable mesh config may include `sfcfg:hasConfigResolutionConfig` and resolver hints, but those hints are accepted only after the trusted bootstrap profile says they are legal.
+- Mesh-root-local and workspace-local operational config can describe project layout and project-local config sources inside the active workspace boundary. They cannot grant access outside the workspace, enable remote config, enable external current-following references, or choose machine-local config files.
+- Machine-local operational config and explicit runtime inputs are the first places that may broaden local or remote access, permit external config references, or choose persistent cache/log locations.
+
+Portable resolver hints:
+
+- Legal portable hints include role-specific config attachments, expected config artifacts, mesh-local and Knop-local/inheritable config sources, reusable config targets already inside the trusted boundary, stricter unknown-term/cycle/reference policies, lower reference-depth caps, and inheritance preferences for portable behavior config.
+- Portable hints are capped by trusted policy. They cannot raise max reference depth above the trusted cap, loosen unknown-term or cycle policy, turn pinned-only into current-following, write caches outside approved operational locations, grant filesystem/network access, or make `ResolvedConfig` an input source.
+- When a portable hint conflicts with trusted policy, the resolver applies the stricter policy or rejects the source with a `config.resolution.source.rejected` event.
+
+Defaults source and diagnostics:
+
+- Source RDF for Weave's default profile lives directly under top-level `defaults/`, currently `defaults/application.ttl` and `defaults/config-resolution.ttl`.
+- Runtime packaging may embed parsed defaults, but tests and diagnostics should keep comparing the embedded/default in-memory representation against those TTL files. A future diagnostic command should be able to emit the active default profile and the digest of each default source file.
+- Defaults under `defaults/` are source artifacts for Weave's sidecar defaults mesh, but they are not yet placed under generated `_mesh` support paths. The future packaging command should integrate them into a sidecar mesh rather than making hand-maintained `_mesh` files authoritative.
+
+Discovery and layer order:
+
+- Evaluate trust gates before portable config discovery: implementation emergency defaults, Weave default `ConfigResolutionConfig`, explicit runtime arguments, machine-local operational config, workspace-local operational config, and mesh-root-local operational config.
+- Discover behavior config after the active trust boundary is known: Weave application defaults, mesh-local config, mesh-inheritable config, ancestor Knop inheritable config projected as `configLayerRole_knopInherited`, current Knop local config, and reusable config at each attachment point.
+- Apply request/command overrides last for the current operation only, then validate the resulting effective operation config against hard invariants and current artifact/history RDF.
+- Reusable config is not a single global layer. It is merged where the source is referenced: a reusable config imported from mesh config behaves as mesh config, while the same reusable config imported from Knop-local config behaves as Knop-local config.
+
+Mesh-local config remains a normal mesh-scoped behavior layer in that order. It is not walked through Knop inheritance controls. Mesh-inheritable config is the mesh-scope offer that enters the inherited Knop stream and can then be stopped or blocked by Knop inheritance policy.
+
+Property-family merge rules:
+
+- Trust gates merge by intersection/deny-by-default. A lower-trust source may narrow access but cannot widen it.
+- Safety caps use stricter-wins semantics, including unknown-term policy, cycle policy, external reference policy, current-following permission, and max reference depth.
+- Scoped behavior defaults use nearest applicable scope after trust gates: artifact-specific policy beats Knop-local policy, which beats inherited Knop policy, which beats mesh policy, which beats application defaults.
+- Required invariants dominate ordinary overrides. If implementation policy requires a ledger artifact or rejects an invalid name, config and request fields cannot silently weaken that requirement.
+- Additive values such as page support assets or diagnostic tags merge by stable union with deduplication. Explicit block/remove vocabulary should be added before additive values can be safely subtracted.
+- Operation request values can specialize one invocation. They may override hints with warnings, satisfy strict policies without warnings, or fail against non-overrideable policies.
+
+Segment override behavior:
+
+- If config supplies a next segment hint and the command supplies a different legal segment, use the command segment and emit a warning tied to the target, property, resolved hint, and requested value.
+- If config supplies a naming policy and the command segment satisfies it, use the command value without conflict warning.
+- If the command segment violates a hard naming invariant or current artifact/history RDF, fail before writing.
+- If the command segment violates a strict but overrideable policy, require an explicit operation-level acknowledgement and a trusted resolver profile that allows that override class. Until such a CLI/API acknowledgement exists, fail closed.
+
+Inheritance traversal:
+
+- For a target Knop, collect mesh-inheritable config, then walk ancestor Knops from root to parent and collect each ancestor's inheritable offers that are still propagating.
+- Default inbound inheritance is `configInheritancePolicy_acceptAndPropagate` inside one mesh boundary. `acceptDoNotPropagate` applies incoming inherited config to the current scope but stops it from reaching descendants. `blockInherited` rejects incoming inherited config for the current scope and descendants.
+- Default outbound Knop-inheritable config is `offerDescendantsOnly`. `offerSelfAndDescendants` projects the offer into the authored Knop as well, but Knop-local config for that same Knop still wins for nearest-scope behavior defaults.
+- Submesh boundaries stop inheritance unless an explicit config source crosses the boundary and trusted operational policy allows that source to be read.
+
+Validation:
+
+- All config sources must parse as RDF before participation. Malformed config fails the source; if the source was required by trusted policy, resolution fails the scope.
+- Policy-valued properties must point to known individuals of the expected policy class. Unknown `sfcfg:` policy values fail under the default reject policy.
+- Singleton resolver policy properties such as max reference depth and cache policy must have exactly one effective value after merge unless a property-specific merge rule says otherwise.
+- Naming hints must be syntactically legal path segments and must still be validated against current artifact/history RDF immediately before write.
+- Config-source targets must resolve to a `ConfigArtifact`, RDF-bearing `DigitalArtifact`, trusted `LocatedFile`, or trusted external bytes according to active reference policy. Unresolved required targets fail closed.
+
+Digest validation:
+
+- Pinned, external, and `LocatedFile`-based config sources should carry `sflo:expectsContentDigest` when the runtime can compute or know the bytes. Pinned config without a required digest fails creation/pinning under strict policy.
+- Resolution verifies loaded bytes before parsing or merging. Digest mismatch on a pinned or external config source is a resolution failure by default.
+- Current-following sources inside the trusted boundary may record observed content digest in `ConfigResolutionRecord` and cache keys. Whether a mismatch with an authored expected digest warns or fails is controlled by trusted policy; default should fail for config sources.
+- Repinning recomputes the expected digest from trusted resolved bytes or requires an explicitly supplied digest.
+
+Cycle, depth, cache, and lock behavior:
+
+- The resolver walks config-source targets with a visited stack and rejects cycles by default.
+- The Weave default maximum config reference depth is 8. Trusted operational config may lower it. Portable config may lower it but not raise it above the trusted cap.
+- Stand-alone CLI invocations may use a process-local cache only for the current command. They may read a persistent diagnostic cache only after verifying resolver profile digest, trust-policy digest, source fingerprints, mesh identity, and scope key.
+- Service-backed mode may keep watcher-backed scoped caches warm across invocations. Watchers are invalidation hints; cache correctness still depends on fingerprints, digests, pinned states, ETags, or equivalent freshness tokens.
+- Default cache and lock files belong in operational storage such as `.weave/cache` under the trusted workspace container, not in semantic mesh history and not as implicit `DigitalArtifact`s.
+
+Config-resolution logs:
+
+- Emit compact single-line JSON-LD events with an `@context` IRI per line for standards-compliant logs. A future sidecar-context profile must be explicitly labeled as a Weave-specific optimization.
+- First-pass event names should include `config.resolution.started`, `config.resolution.source.discovered`, `config.resolution.source.accepted`, `config.resolution.source.ignored`, `config.resolution.source.rejected`, `config.resolution.digest.verified`, `config.resolution.digest.mismatch`, `config.resolution.cache.hit`, `config.resolution.cache.miss`, and `config.resolution.completed`.
+- Log event payloads should include source kind, layer role, declaring scope, declared location, resolved location, resolution mode, trust tier, decision status, source fingerprint or digest, resolver profile digest, trust-policy digest, scope key, `ResolvedConfig` digest when available, warning/error codes, and cache status. Do not log full config content by default.
+
+Representing many scopes:
+
+- `ResolvedConfig` cache entries are scoped by mesh root/base identity plus optional submesh path, Knop path, artifact role, artifact path, and operation kind.
+- Compute scoped `ResolvedConfig` lazily. A mesh-level resolved profile can seed lower scopes, but Knop/artifact scopes must remain distinguishable because inheritance, local overrides, and artifact-role defaults can differ.
+- A daemon may manage many mesh roots. Each mesh root gets its own cache container and scope namespace; reusable config shared across meshes can be cached by source digest, but its merged effect is still scope-specific.
+
### No Boolean Policy Flags
The old `generateResourcePages` and `createHistoricalStatesOnWeave` booleans name real needs, but booleans are the wrong core contract. They are too narrow for inventory history, deferred page generation, and policy inheritance.
@@ -576,7 +807,7 @@ Generic `hasConfig` remains useful, but the public model should include role-spe
Inheritance should be scoped and explicit:
- mesh-level config supplies mesh defaults
-- mesh-level inheritable config supplies defaults for Knops, submeshes, or descendant scopes inside the mesh boundary
+- `_mesh/_knop-inheritable-config` supplies defaults into Knop inheritance inside the mesh boundary
- a parent Knop's inheritable config supplies defaults for descendants
- a Knop's local config overrides inherited defaults for that Knop
- reusable config artifacts may be imported/referenced into either layer
@@ -584,13 +815,15 @@ Inheritance should be scoped and explicit:
By default, a Knop's inheritable config should be an offer to descendants, not an implicit local override for the Knop itself. If a Knop also needs the policy locally, attach the same config artifact through its local config role or use a policy that explicitly makes inheritance self-inclusive.
+The mesh-local versus mesh-inheritable distinction is not about whether mesh config participates in resolution. Both participate. Mesh-local config is applied as a mesh-scoped layer, while mesh-inheritable config is projected into Knop scopes as inherited input before ancestor Knop offers. Knop inheritance stop/block controls apply to that inherited stream, not to all mesh-scoped policy.
+
Implement a minimal inherited-config propagation control in the first config pass, before fixture ladder regeneration. The fixture ladders will otherwise encode a propagation model implicitly, and we would pay the rerung cost twice when the explicit control lands.
Do not revive the full old "configuration firewall" machinery yet. The first-pass control should be policy-valued and scoped:
- default normal Knop inheritance accepts inherited config and propagates it to descendants inside the current mesh boundary
- a scope can accept inherited config locally but stop propagation to descendants
-- a scope can block inherited config entirely for itself and descendants
+- a scope can block incoming inherited config entirely for itself and descendants
- a scope can make its own inheritable config descendant-only or explicitly self-inclusive
- submesh boundary behavior should be explicit rather than accidentally inherited through path traversal
@@ -605,9 +838,9 @@ Options:
- current-only config artifacts keep support surfaces quiet, but make historical replay and page regeneration depend on whatever config exists now
- versioned config artifacts preserve "config at the time" and make historical rendering/debugging more reproducible, but add support-history artifacts
- checkpointed or metadata-only config histories preserve fingerprints and selected snapshots without recording every minor edit as a full public surface
-- versioned config with suppressed ResourcePages records history without making every config state dereferenceable or visible in the generated site
+- versioned config with explicit suppressed ResourcePages records history without making every config state dereferenceable or visible in the generated site
-The preferred default is: all portable authored config artifacts are versioned by default, but their ResourcePages are suppressible by default. That includes `_mesh/_config`, mesh-level inheritable config, `_knop/_local-config`, `_knop/_inheritable-config`, reusable named config artifacts, page presentation config, template artifacts, and stylesheet artifacts when they are modeled as mesh artifacts. The reason is reproducibility: historical ResourcePage regeneration, diagnostics, and audits need to know which config was in force when a state was created or when a page was rendered.
+The preferred default is: all portable authored config artifacts are versioned by default and their current ResourcePages are generated by default. They remain suppressible or deferrable by explicit policy. That includes `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_local-config`, `_knop/_inheritable-config`, reusable named config artifacts, page presentation config, template artifacts, and stylesheet artifacts when they are modeled as mesh artifacts. The reason is reproducibility and dereferenceability: historical ResourcePage regeneration, diagnostics, and audits need to know which config was in force when a state was created or when a page was rendered, and current config artifacts should remain inspectable unless a mesh deliberately hides or defers them.
This does not mean every operational or derived config file participates in mesh history. Machine-local operational config, daemon state, runtime logs, `ResolvedConfig` caches, and config-resolution diagnostics stay outside normal mesh history unless explicitly represented or integrated as mesh `DigitalArtifact`s.
@@ -643,7 +876,9 @@ The config model must answer:
Suppressed pages should omit `sflo:hasResourcePage`. Do not leave an unfulfilled `hasResourcePage` promise for a page that policy says should not exist. This can mean an older historical state has a resource page while the current resource does not, or vice versa after policy changes. That is acceptable because each inventory/state describes the page facts for that state. If a page is generated later by explicit request/backfill, the corresponding current or historical inventory update can add the `sflo:hasResourcePage` fact at that time.
-The default should preserve dereferenceability for public payloads and important mesh navigation, but slim support artifacts should be able to suppress noisy current and historical pages by policy.
+The default should preserve dereferenceability for artifacts by generating current ResourcePages. Slim support artifacts should still be able to suppress or defer noisy current or historical pages by explicit policy.
+
+First-pass suppression granularity can be coarse. The resolver should support the default generate/suppress/defer/on-request policy at least at the default, artifact-role, and named-artifact/config-source level. It does not need to block on separate suppression controls for every generated page surface, such as Knop identifier pages, arbitrary IRI/term pages, `ArtifactHistory` pages, `HistoricalState` pages, and `ArtifactManifestation` pages. For the first runtime slice, it is acceptable for those pages to follow the owning artifact's page policy or the current implementation's bundled page-generation behavior. More precise page-kind policy can be added after the resolver exists and after fixture regeneration shows where the broad policy is too blunt.
### Config Ontology Overhaul Scope
@@ -692,7 +927,7 @@ Drop or avoid reviving:
## Sequencing
-The next-step sequencing is still good: implement the grand config synthesis next, while keeping [[wd.task.2026.2026-05-07-fixture-ladder-generator]] close enough that we do not hand-repair generated branches during the migration.
+The next-step sequencing is still good: implement the grand config synthesis next, while keeping [[wa.completed.2026.2026-05-07-fixture-ladder-generator]] close enough that we do not hand-repair generated branches during the migration.
Recommended order:
@@ -714,6 +949,7 @@ Use this section for items that are real, but should not block the first config
- Persistent diagnostic cache storage beyond source-fingerprint-safe cache keys and log records.
- Complete `weave config source add|set|pin|unpin|remove` authoring surface, if the first pass can use compact RDF examples or a smaller internal API.
- Rich render/provenance manifests for all historical ResourcePage regeneration modes.
+- Fine-grained page-kind suppression controls for Knop/IRI pages, `ArtifactHistory`, `HistoricalState`, and `ArtifactManifestation` pages beyond the first-pass default/role/artifact-level ResourcePage policy.
- Scheduled or automatic historical ResourcePage backfill.
- Package-manager-style config dependency resolution across external meshes.
- Publishing and governance workflow for the future `sflo` sidecar mesh.
@@ -763,10 +999,10 @@ Use this section for items that are real, but should not block the first config
- Let explicit CLI/API segment arguments override config defaults and hints for one operation, with a warning when they differ from a resolved hint.
- Fail closed when a CLI/API segment argument violates a hard invariant, trust gate, or non-overrideable policy. For strict but overrideable policies, require an explicit operation override acknowledgement plus resolver policy allowing that class of override.
- Keep payload artifacts historical by default.
-- Version portable authored config artifacts by default, including mesh config, mesh inheritable config, Knop local config, Knop inheritable config, reusable named config artifacts, and presentation/template/style config artifacts when they are represented as mesh artifacts.
-- Suppress or defer ResourcePages for config support artifacts by default when they are not useful to publish; versioning config history does not require generating a visible page for every config state.
-- Keep `_mesh/_inventory` historical by default because it is the settled mesh-state ledger.
-- Keep `_knop/_inventory` historical for now because current weave progression depends on it.
+- Version portable authored config artifacts by default, including mesh config, `_mesh/_knop-inheritable-config`, Knop local config, Knop inheritable config, reusable named config artifacts, and presentation/template/style config artifacts when they are represented as mesh artifacts.
+- Allow explicit suppression or deferral of ResourcePages for config support artifacts when they are not useful to publish; versioning config history does not require generating a visible page for every config state when policy says otherwise.
+- Do not keep `_mesh/_inventory` or `_knop/_inventory` history by default; inventory defaults to current-only unless a mesh explicitly opts in.
+- Treat current runtime reads of inventory history/progression facts as transitional implementation debt to remove before applying the current-only inventory default to fixture-backed behavior.
- Default low-value working/progression artifacts such as `_mesh/_meta` and `_knop/_meta` toward current-only, slim-history, checkpoint-only, or metadata-only history policy unless overridden.
- Treat operational/runtime config as a trusted runtime input and trust gate, not as `ResolvedConfig` or the application's effective config.
- Treat `ResolvedConfig` as derived resolver output produced from Weave defaults, operational gates, resolver policy, authored config, reusable config artifacts, and validated request-level config inputs.
@@ -813,6 +1049,7 @@ Use this section for items that are real, but should not block the first config
- Add policy-valued ResourcePage regeneration config modes for config-at-the-time, current presentation config, current full config, and hybrid historical-semantic/current-presentation regeneration.
- Add config naming-default and naming-hint vocabulary that is separate from core ordinal allocator state.
- Add operation-request override policy vocabulary for warning/applying, rejecting conflicts, or requiring explicit acknowledgement when request fields conflict with resolved config.
+- Define API/CLI affordances for setting and clearing durable next history/state segment hints without rewinding ordinal allocator counters.
- Add config attachment and config-source resolution vocabulary for reusable named config artifacts.
- Add content digest vocabulary for byte-bearing resources and expected resolved targets, at least `hasContentDigest` and `expectsContentDigest`.
- Define digest lifecycle for target creation, pinning, repinning, user-supplied expected digests, and weave-time verification.
@@ -837,7 +1074,7 @@ Use this section for items that are real, but should not block the first config
## Testing
- Ontology validation should cover the revised config ontology and examples.
-- Add example RDF for `_mesh/_config`, `_knop/_local-config`, `_knop/_inheritable-config`, and a reusable named config artifact.
+- Add example RDF for `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_local-config`, `_knop/_inheritable-config`, and a reusable named config artifact.
- Add example RDF showing a Knop inheriting defaults from a parent and overriding them locally.
- Add example RDF and resolver tests for inherited config propagation controls: default accept/propagate, accept-but-stop, block inherited config, descendant-only inheritable config, and explicitly self-inclusive inheritable config.
- Add example RDF showing a reusable config artifact referenced through a pinned config-source target.
@@ -887,12 +1124,12 @@ Use this section for items that are real, but should not block the first config
- [x] Finish or at least settle the ontology enum-instance naming task enough that config policy values can be minted once, using the flat underscore-separated convention.
- [x] Record the synthesized decisions from the current task and the four source tasks.
-- [ ] Inventory current implicit TypeScript, API, CLI, page planning, history planning, and fixture defaults.
-- [ ] Classify each current default as Weave default profile config, operational config, explicit operation request, or derived effective config.
-- [ ] Draft compact example RDF for mesh config, Knop local config, Knop inheritable config, reusable config artifacts, operational config, and derived `ResolvedConfig`.
-- [ ] Draft compact example RDF for config-resolution / meta-config, including pinned reusable config and a rejected current-following config source.
+- [x] Inventory current implicit TypeScript, API, CLI, page planning, history planning, and fixture defaults.
+- [x] Classify each current default as Weave default profile config, operational config, explicit operation request, or derived effective config.
+- [x] Draft compact example RDF for mesh config, Knop local config, Knop inheritable config, reusable config artifacts, operational config, and derived `ResolvedConfig`.
+- [x] Draft compact example RDF for config-resolution / meta-config, including pinned reusable config and a rejected current-following config source.
- [x] Draft compact example RDF for the Weave default profile mesh.
-- [ ] Classify draft examples as normative sflo examples, Semantic Flow Framework scenario examples, or Weave developer-note implementation examples.
+- [x] Classify draft examples as normative sflo examples, Semantic Flow Framework scenario examples, or Weave developer-note implementation examples.
- [x] Decide initial names for policy classes and controlled policy values.
### Phase 1: Config Ontology Overhaul
@@ -900,8 +1137,8 @@ Use this section for items that are real, but should not block the first config
- [x] Update `dependencies/github.com/semantic-flow/sflo/semantic-flow-config-ontology.ttl` with Knop local/inheritable config attachment properties and layer-role values.
- [x] Add or refine reusable config-source resolution vocabulary by directly reusing `sflo:ArtifactResolutionTarget`.
- [x] Add content digest vocabulary for `LocatedFile`, `ArtifactManifestation`, and `ArtifactResolutionTarget`.
-- [ ] Add SHACL expectations for content digest use.
-- [ ] Define generic config-source target management API/CLI behavior, including add, set, pin, unpin, remove, and expected-digest handling.
+- [x] Add SHACL expectations for content digest use.
+- [x] Define generic config-source target management API/CLI behavior, including add, set, pin, unpin, remove, and expected-digest handling.
- [x] Add config-resolution / meta-config classes for layers, layer roles, precedence, merge behavior, reference policy, cycle policy, unknown-term policy, and cache policy.
- [x] Add Weave default profile properties and examples for default policies currently implicit in code/API/CLI defaults, without minting a `WeaveDefaultConfig` class.
- [x] Add `KnopConfig` only if it helps validation, and keep Knop local/inheritable behavior on attachment properties and layer roles.
@@ -918,28 +1155,37 @@ Use this section for items that are real, but should not block the first config
### Phase 2: Weave Config Discovery And Resolution Design
-- [ ] Define the trusted bootstrap resolver profile and which sources can supply it.
-- [ ] Define which portable resolver hints are legal and which are capped by trusted bootstrap policy.
-- [ ] Define the Weave-owned defaults mesh, where Weave default profile artifacts are loaded from, and how they can be inspected in tests and diagnostics.
-- [ ] Define config discovery order for `_mesh/_config`, `_knop/_local-config`, `_knop/_inheritable-config`, reusable config artifacts, machine-local operational config, and command-line overrides.
-- [ ] Define property-family merge and precedence rules for trust gates, safety caps, scoped behavior defaults, required invariants, operation request fields, additive values, and reusable config attachment points.
-- [ ] Define warning and failure behavior for CLI/API segment arguments that override hints, satisfy strict policies, or conflict with overrideable versus non-overrideable policies.
-- [ ] Define inheritance traversal rules for Knop hierarchy and submesh boundaries, including default accept/propagate behavior and explicit stop/block/self-inclusive propagation policies.
-- [ ] Define validation rules for config policies, naming hints, reusable config targets, and unknown terms.
-- [ ] Define digest validation rules for external, pinned, and `LocatedFile`-based config sources.
-- [ ] Define when config-source target commands compute, persist, recompute, or require expected content digests.
-- [ ] Define cycle detection, maximum config reference depth, and cache/lock semantics for resolved config.
-- [ ] Define stand-alone CLI re-resolution behavior versus service-backed watcher/cache behavior.
-- [ ] Define the `config.resolution.*` log event schema, JSON-LD context strategy, cache key shape, and watcher/fingerprint invalidation model.
-- [ ] Define how resolved runtime config can represent many meshes, submeshes, Knops, and artifacts.
+- [x] Define the trusted bootstrap resolver profile and which sources can supply it.
+- [x] Define which portable resolver hints are legal and which are capped by trusted bootstrap policy.
+- [x] Define the Weave-owned defaults mesh, where Weave default profile artifacts are loaded from, and how they can be inspected in tests and diagnostics.
+- [x] Define config discovery order for `_mesh/_config`, `_mesh/_knop-inheritable-config`, `_knop/_local-config`, `_knop/_inheritable-config`, reusable config artifacts, machine-local operational config, and command-line overrides.
+- [x] Define property-family merge and precedence rules for trust gates, safety caps, scoped behavior defaults, required invariants, operation request fields, additive values, and reusable config attachment points.
+- [x] Define warning and failure behavior for CLI/API segment arguments that override hints, satisfy strict policies, or conflict with overrideable versus non-overrideable policies.
+- [x] Define inheritance traversal rules for Knop hierarchy and submesh boundaries, including default accept/propagate behavior and explicit stop/block/self-inclusive propagation policies.
+- [x] Define validation rules for config policies, naming hints, reusable config targets, and unknown terms.
+- [x] Define digest validation rules for external, pinned, and `LocatedFile`-based config sources.
+- [x] Define when config-source target commands compute, persist, recompute, or require expected content digests.
+- [x] Define cycle detection, maximum config reference depth, and cache/lock semantics for resolved config.
+- [x] Define stand-alone CLI re-resolution behavior versus service-backed watcher/cache behavior.
+- [x] Define the `config.resolution.*` log event schema, JSON-LD context strategy, cache key shape, and watcher/fingerprint invalidation model.
+- [x] Define how resolved runtime config can represent many meshes, submeshes, Knops, and artifacts.
### Phase 3: Runtime Implementation Slices
-- [ ] Implement an internal effective-config model that can answer history policy and resource-page policy for a target artifact role.
-- [ ] Implement minimal inherited config propagation controls before fixture ladder regeneration, covering normal propagation, accept-but-stop, block inherited config, and descendant-only versus self-inclusive offers.
-- [ ] Wire history policy into the slim-support-artifact work from [[wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default]].
-- [ ] Wire naming defaults and hints into payload versioning without bypassing current RDF validation.
-- [ ] Wire resource-page generation policy into page planning separately from history policy.
+- [x] Implement an internal effective-config model that can answer history policy and resource-page policy for a target artifact role.
+- [x] Implement minimal inherited config propagation controls before fixture ladder regeneration, covering normal propagation, accept-but-stop, block inherited config, and descendant-only versus self-inclusive offers.
+- [x] Wire the first support-artifact history-policy slice into mesh support ResourcePage catch-up: `_mesh/_meta` and `_mesh/_inventory` use current-only history by default while `_mesh/_config` remains versioned.
+- [x] Wire history policy into the first slim-support-artifact bridge slice from [[wd.task.2026.2026-05-05-optional-history-and-slim-support-artifacts-by-default]]: `_knop/_meta` is current-only in first Knop and first payload weave planning, while payload and inventory histories remain unchanged.
+- [x] Wire configured naming policies into payload versioning without bypassing current RDF validation.
+- [x] Decide that `_meta` progression uses split artifact/history facts plus optional next-segment hints, with explicit or hinted names controlling minted paths while ordinals keep counting monotonically.
+- [x] Record that API/CLI needs set and clear operations for durable next-segment hints without rewinding ordinal counters.
+- [x] Implement the first `_mesh/_meta` MeshInventory progression seam for first Knop, first payload, and first extracted-Knop weave planning: read current/latest/next progression plus optional `sfcfg:hasNextStateSegmentHint` from `_mesh/_meta`, mint hinted names before ordinal fallback, advance the ordinal monotonically, clear consumed hints, and keep inventory history blocks focused on stable state membership.
+- [ ] Complete concrete default-segment and next-segment hint runtime behavior beyond this first MeshInventory state seam, including history hints, Knop-local progression, and API/CLI set/clear commands.
+- [ ] Reassess durable next-segment hint APIs after `v0.1.0` and the first URPX publication pass. Explicit `historySegment`, `stateSegment`, and `manifestationSegment` request fields are good enough for friendly release histories in the immediate npm release path.
+- [x] Wire resource-page generation policy into runtime page materialization separately from history policy.
+- [x] Omit `sflo:hasResourcePage` facts from versioned RDF when resource-page policy suppresses or defers a page.
+- [x] Keep ResourcePage policy ownership on stable history/state membership facts rather than mutable current/latest pointers.
+- [x] Parse and validate the default `ResourcePageRegenerationConfigPolicy` into effective runtime config.
- [ ] Wire historical ResourcePage regeneration to select config-at-the-time, current presentation config, current full config, or hybrid regeneration policy.
- [ ] Keep path/URL trust policy integration aligned with [[wd.task.2026.2026-04-11_1723-operational-config-for-runtime-resolution]].
- [ ] Update non-fixture unit and integration tests alongside each runtime slice so parser, resolver, naming, history-policy, and page-policy expectations move with the implementation.
diff --git a/documentation/notes/wd.task.2026.2026-05-07-fixture-ladder-generator.md b/documentation/notes/wd.task.2026.2026-05-07-fixture-ladder-generator.md
deleted file mode 100644
index 4068d0b..0000000
--- a/documentation/notes/wd.task.2026.2026-05-07-fixture-ladder-generator.md
+++ /dev/null
@@ -1,151 +0,0 @@
----
-id: 9xybywi58iqffsh96al5yhl
-title: 2026 05 07 Fixture Ladder Generator
-desc: ''
-updated: 1778219880393
-created: 1778219880393
----
-
-## Goals
-
-- Replace hand-carried fixture branch ladders with a reproducible fixture-ladder generation workflow.
-- Treat fixture repository branches as disposable golden outputs that can be regenerated after ontology, config, planner, renderer, or manifest changes.
-- Keep Accord transition manifests as the durable behavior contract, while fixture branches remain convenient test and inspection material.
-- Support the existing Alice Bio and Sidecar Fantasy Rules fixture repositories without forcing a generalized scenario engine in the first pass.
-- Make rerunging from an early branch boring: run one command, replay transitions in order, validate each step, and report drift.
-- Preserve the ability for tests to compare Weave output against settled fixture refs.
-- Keep GitHub Pages publication focused on the final SemanticSite unless a specific task needs intermediate states.
-- Coordinate the generator with the enum-instance migration in [[ont.task.2026.2026-05-03-enumeration-type-instances]] and the config synthesis in [[wd.task.2026.2026-05-06-grand-config-synthesis]].
-
-## Summary
-
-The current fixture repositories use numbered branch ladders such as Alice Bio's `00-blank-slate` through `25-root-page-customized-woven` and Sidecar Fantasy Rules' `00-blank-slate` through `15-first-release-woven`. Those ladders are useful because they make each operation transition inspectable and give tests stable refs to compare against. They are also expensive: when an early rung changes, every later rung must be recreated, and the recreation process currently depends too much on human/agent memory.
-
-The better model is to keep the ladder shape but change ownership. The durable source should be the transition journal and Accord manifests. The branch ladder should be generated output. A fixture generator should materialize a fixture repo from a known starting point, run each declared transition with the current Weave CLI/runtime, validate the result against the matching Accord manifest, and update the branch ref for that rung.
-
-This task belongs in Weave because the generator orchestrates Weave commands, integrates with Weave tests, and manages local fixture repositories under `dependencies/github.com/semantic-flow/`. The portable transition manifests can remain in the Semantic Flow Framework examples tree where they already live.
-
-## Discussion
-
-### Current Shape
-
-Weave tests currently read fixture branch contents from helper modules such as `tests/support/mesh_alice_bio_fixture.ts` and `tests/support/mesh_sidecar_fantasy_rules_fixture.ts`. The helpers resolve local or remote refs, read files via `git show`, list branch files via `git ls-tree`, and materialize branch contents into temporary directories. Integration and e2e tests then run Weave operations and compare the generated workspace to the expected fixture branch or to manifest-scoped expectations.
-
-That structure is basically right for tests. The weak part is fixture maintenance. Branches are being used both as acceptance snapshots and as authored historical examples. The first use is valuable. The second is where the maintenance cost comes from.
-
-### Disposable Golden Outputs
-
-For this task, "disposable golden output" means:
-
-- a fixture branch may be force-updated during an intentional regeneration
-- a branch's contents are not independently authored once the transition source and manifest are settled
-- review should focus on manifest changes, generator changes, and the generated diff, not on preserving branch commit history
-- if an early rung changes, later rungs should be regenerated from the new state instead of patched manually
-
-This does not make the fixtures less important. It makes their provenance clearer. The generated branch state is still the black-box expected output for tests. It is just no longer the source of truth for how to produce that state.
-
-### Source Of Truth
-
-The intended source layers are:
-
-- fixture scenario definition: ordered list of transitions, branch names, commands, source refs, destination refs, and manifest names
-- Accord manifests: durable per-transition expected behavior and file expectations
-- Weave implementation: current operation behavior
-- fixture repo branches: generated expected outputs used by tests and local inspection
-
-The first implementation can encode the scenario definition in TypeScript if that keeps the tool simple. A later pass can move it to JSON, JSON-LD, YAML, or an Accord-adjacent manifest if the shape stabilizes.
-
-### Publication
-
-We do not need every intermediate branch to publish through GitHub Pages at the same time. The fixture repos mainly demonstrate a mesh. Publishing the final SemanticSite is enough by default.
-
-If intermediate states become useful for documentation or demos, the generator can later copy selected rung outputs into a single Pages deployment tree such as `/alice-bio/07-alice-bio-integrated-woven/`. That should be a separate publishing enhancement, not part of the first generator.
-
-### Relationship To Config Synthesis
-
-The config synthesis will probably invalidate most existing fixture outputs. It will introduce explicit Weave defaults, config artifacts, local/inheritable Knop config, inherited propagation controls, changed support-artifact history policy, and likely updated generated pages/manifests. That is exactly the sort of change a generator should absorb.
-
-The generator should be designed alongside the next config pass and used before repairing fixture repos. Otherwise we will spend the config migration doing another manual ladder repair and then still need the generator afterward.
-
-That does not mean the generator has to be perfect before config synthesis begins. The minimum useful version is a deterministic replay tool for one fixture repo, probably Alice Bio, with clear dry-run/status output and validation hooks. Config design can proceed concurrently, but fixture repo repair should wait until the enum and config vocabulary changes can be regenerated together.
-
-### Relationship To Enumeration Migration
-
-The enum-instance migration in [[ont.task.2026.2026-05-03-enumeration-type-instances]] should not be blocked on a finished fixture generator. The enum task is ontology-level vocabulary cleanup and should settle before the config ontology mints many new controlled values.
-
-Fixture regeneration for enum fallout is deferred until after the next config pass. A pragmatic order is:
-
-1. Settle the enum naming convention and update ontology/code references.
-2. Take the next config synthesis pass using the settled flat underscore-separated enum naming convention, including the minimal inherited config propagation controls that affect fixture output.
-3. Build or refine the fixture generator concurrently enough to replay affected branches.
-4. Rerung fixtures once for the combined enum and config fallout using the generator/replay path.
-
-If config work exposes fixture-generator requirements, fold those requirements back into this task instead of doing one-off manual ladder repair.
-
-### Initial Scope
-
-Start with Alice Bio because it has the longest ladder and exercises mesh create, Knop create, integrate, weave, reference addition, payload update, extract, page customization, and root lifecycle behavior. Once Alice Bio can be regenerated, adapt the same machinery for Sidecar Fantasy Rules.
-
-The generator should be intentionally concrete at first. It does not need to infer operations from arbitrary manifests. It can have explicit transition definitions that name the command to run, the source branch, the target branch, the manifest, and any path replacements or known comparison exclusions already used by tests.
-
-## Open Issues
-
-- Should scenario definitions live as TypeScript in Weave, as data files in Weave, or beside Accord manifests in the Semantic Flow Framework examples tree?
-- Should generated fixture branch commits be one commit per rung, or should the generator only update branch tips without caring about branch-local history?
-- Should the generator force-update branches by default, or require an explicit `--force` / `--write-branches` flag after a dry run?
-- How should the generator handle intentionally hand-authored source-only branches such as `01-source-only`?
-- Should manifest validation compare full tree contents, manifest-scoped expectations only, or both depending on transition type?
-- How much should generated HTML be normalized before comparison, especially as renderer behavior changes?
-- Should final SemanticSite publication be handled by this generator later or by a separate release/publish task?
-
-## Decisions
-
-- The fixture generator is a Weave developer-tooling task, not part of the portable Semantic Flow ontology work.
-- Fixture branch ladders should become disposable generated outputs.
-- Accord manifests and ordered transition definitions are the durable contract.
-- Keep the existing fixture branch comparison tests for now; update their assumptions only where needed to support generated refs.
-- Publish only the final SemanticSite by default; intermediate Pages publication is out of scope for the first pass.
-- Do not rename completed task notes or fixture branches as part of this task unless explicitly requested.
-- Do not build a fully generic fixture scenario engine in the first pass.
-
-## Contract Changes
-
-- No immediate external Semantic Flow API contract changes.
-- Weave's internal fixture maintenance contract changes: generated fixture branches are no longer treated as hand-maintained source material.
-- Test fixtures may gain a declared scenario/replay contract that names transition order, expected source refs, expected target refs, commands, and manifests.
-- Future fixture branch diffs should be reviewed as generated outputs from a declared replay, not as standalone authored examples.
-
-## Testing
-
-- Add focused unit tests for scenario definition parsing/validation if the scenario becomes data-driven.
-- Add dry-run tests for command planning so transition order, source branch, target branch, manifest path, and command arguments are validated without mutating fixture repos.
-- Add at least one integration-style test that regenerates a small temporary fixture ladder from a minimal scenario.
-- Use existing e2e and integration fixture comparisons as the main acceptance check after branch regeneration.
-- Run `deno task lint` after significant implementation changes, per repo guidance.
-- For actual fixture rerunging, run the relevant Accord manifest checks and the affected Weave fixture tests before accepting generated branches.
-
-## Non-Goals
-
-- Publishing every intermediate branch via GitHub Pages.
-- Preserving old fixture branch commit histories during intentional regeneration.
-- Designing a universal workflow engine for arbitrary Semantic Flow examples.
-- Solving the config ontology overhaul directly.
-- Solving the enum-instance migration directly.
-- Rewriting Accord manifest semantics unless generator implementation exposes a concrete gap.
-- Moving source-of-truth user-facing README content into fixture branches.
-
-## Implementation Plan
-
-- [ ] Inventory the current Alice Bio and Sidecar Fantasy Rules branch ladders, manifest names, transition commands, and existing test expectations.
-- [ ] Decide the first scenario-definition format, favoring a simple TypeScript definition unless a data file is clearly better.
-- [ ] Implement a dry-run planner that prints transition order, source branch, target branch, manifest path, command, and expected validation steps.
-- [ ] Implement local materialization for a source branch into a temporary workspace using the existing fixture helper behavior as a reference.
-- [ ] Implement execution for the first Alice Bio transition that runs the intended Weave command and validates the result against its Accord manifest.
-- [ ] Add branch update support behind an explicit write flag so dry runs remain the default while the tool is being proven.
-- [ ] Extend the generator through the full Alice Bio ladder.
-- [ ] Update or add documentation for the Alice Bio regeneration workflow.
-- [ ] Extend the generator to Sidecar Fantasy Rules.
-- [ ] Update Accord manifests, fixture-backed Weave tests, and conformance expectations after generated branches are rerung for the combined enum/config changes.
-- [ ] Record the expected workflow for large ontology/config churn: update manifests, run generator, inspect generated branch diffs, run fixture tests, commit/push branch updates intentionally.
-- [x] Update [[wd.task.2026.2026-05-06-grand-config-synthesis]] to reference this task as the intended fixture regeneration path before the config-driven fixture rebuild.
-- [ ] Update [[wd.decision-log]] with the decision to treat fixture branches as disposable generated outputs once the implementation path is accepted.
diff --git a/documentation/notes/wd.task.2026.2026-05-13-full-ci-cd.md b/documentation/notes/wd.task.2026.2026-05-13-full-ci-cd.md
index 99819bf..cd2c804 100644
--- a/documentation/notes/wd.task.2026.2026-05-13-full-ci-cd.md
+++ b/documentation/notes/wd.task.2026.2026-05-13-full-ci-cd.md
@@ -34,18 +34,48 @@ Weave currently has:
- root `deno.json` tasks for `fmt`, `lint`, `check`, `test`, `test:coverage`, `coverage:lcov`, and `ci`
- GitHub Actions CI on pull requests and pushes to `main`
- Codecov upload from coverage
-- Deno 2.7.12 in CI
-- a source-checkpoint release runbook in [[dev.release-runbook]]
+- Deno 2.7.14 in CI
+- a release runbook in [[dev.release-runbook]] that now documents the transitional `v0.1.0` path
- `documentation/notes/release-notes.v0.0.2.md` as the first release-notes note
+- `documentation/notes/release-notes.v0.1.0.md` as the first full-release stub
- no release workflow
-- no package build scripts
-- no npm package assembly or publishing
-- no binary archive/checksum generation
-- no `weave --version`
-- no durable Weave version metadata
+- `deno task build:binaries` for native binary compilation and per-platform bundle metadata
+- `deno task package:binaries` for Deno-native `.tar.gz`/`.zip` archive generation and `.sha256` checksum files
+- `deno task assemble:npm-packages` for local npm wrapper/platform package directory assembly and `npm-packages-metadata.json` generation
+- `deno task smoke:npm-install` for local `npm pack`, temp-project install, and installed `weave --version` smoke testing
+- `deno task publish:npm-packages` for ordered npm dry-run/publish execution from assembled package directories
+- `.github/workflows/release-manual.yml` for manual native binary builds, archive packaging, npm package assembly, native npm smoke testing, optional npm dry-run/publish, and optional GitHub Release draft/publish handling
+- root `deno.json` version metadata and `weave --version`
That is enough for `v0.0.2`, especially as a deliberate checkpoint with known CI debt, but not enough for a release that users can install.
+### Current Release-Gate Inventory
+
+Earlier local validation after the first version-plumbing slice showed broad fixture-backed drift. After the fixture ladder regeneration, source-registry cleanup, ResourcePage/property work, release tooling slices, and ontology/config guardrails, the release gate is now much closer to the target:
+
+- `deno task fmt:check` passes.
+- `deno task lint` passes.
+- `deno task check` passes.
+- focused `weave --version` e2e coverage passes.
+- focused release metadata and build-script argument tests pass.
+- focused binary packaging helper tests pass.
+- focused npm package assembly tests pass.
+- focused npm install smoke setup tests pass.
+- focused npm publish ordering and argument tests pass.
+- `deno task ci` passes locally with 422 tests; branch-published fixture assertions read explicit Git refs rather than relying on the dependency checkout branch.
+
+The branch-published Fantasy Rules dependency checkout is often left on `gh-pages` for preview, and that branch intentionally does not carry deterministic `.assets`. That should not affect fixture meaning: generated mesh assertions read generated refs, and the fixture-ladder source-asset contract reads `.assets` from the source-bearing `main` ref.
+
+The old failures were not caused by version metadata. They clustered around pre-release fixture and contract drift that has now mostly been repaired:
+
+- stale `https://semantic-flow.github.io/semantic-flow-ontology/` expectations versus the canonical `https://semantic-flow.github.io/sflo/ontology/` namespace
+- stale enum/value shapes such as old reference-role IRIs versus flat namespace-local values
+- stale config ontology IRIs such as `https://semantic-flow.github.io/ontology/config/meshRootPathBase`
+- stale carried mesh and Knop inventory shapes after current config/progression changes
+- fixture-backed CLI and integration tests reading old branch-ladder states that need regeneration through [[wa.completed.2026.2026-05-07-fixture-ladder-generator]]
+
+That means the release pipeline can now move from infrastructure-building to release rehearsal. The main remaining release blockers are authored release notes, a manual workflow rehearsal on GitHub Actions, and confirmation of npm scope/package access before any real publish.
+
### Kato Release Pattern To Adapt
Kato's current release model is a good template:
@@ -135,9 +165,11 @@ The build script should:
- read the canonical release version
- build into a supplied output directory
- produce platform-native executable names, including `.exe` on Windows
-- include only permissions needed by the CLI, or explicitly document why `-A` is temporarily required
+- compile with explicit broad CLI permissions for the first pass: read, write, env, and run for `git`/`deno`
- fail if invoked from a dirty or mismatched version state when release mode requires strictness
+The first implementation slice adds `scripts/build-binaries.ts`, `deno task build:binaries`, a shared release metadata module, and tests for the platform matrix, archive names, npm package names, and build-script arguments. The build script writes `bundle-metadata.json` beside each platform executable so packaging can consume a stable contract.
+
Add `scripts/package-binaries.ts` to turn build outputs into platform bundles. Each bundle should include:
- executable
@@ -149,6 +181,8 @@ Add `scripts/package-binaries.ts` to turn build outputs into platform bundles. E
The package script should produce `.tar.gz` for Unix platforms and `.zip` for Windows.
+The second implementation slice adds `scripts/package-binaries.ts`, `deno task package:binaries`, Deno-native archive writers, SHA-256 checksum generation, archive-local install notes, license inclusion when `LICENSE` is present, and validation that build-time `bundle-metadata.json` still matches the canonical root version and platform metadata. Generated release outputs default under `dist/`, which is ignored.
+
### npm Integration
Add npm package assembly and publishing scripts modeled on Kato:
@@ -172,6 +206,12 @@ The platform packages should:
- be marked with appropriate `os` and `cpu` constraints
- avoid lifecycle scripts when practical; prefer static bin dispatch from the wrapper
+The third implementation slice adds `scripts/assemble-npm-packages.ts`, `deno task assemble:npm-packages`, npm package metadata helpers, a Node bin dispatcher for the wrapper package, platform packages with `os` and `cpu` constraints, copied native binaries, license/readme files, and tests for metadata, optional dependencies, bin dispatch contents, platform constraints, executable modes, and stale bundle metadata rejection. Follow-up publish metadata work adds scoped package `publishConfig`, repository/homepage/bugs metadata, and an aggregate `npm-packages-metadata.json` manifest for smoke/publish/workflow consumers. This is local package-directory assembly; publish behavior remains a separate slice.
+
+The fourth implementation slice adds `scripts/smoke-npm-install.ts`, `deno task smoke:npm-install`, host platform package selection from `npm-packages-metadata.json`, `npm pack` for the wrapper and host platform package, temporary project install from local tarballs, installed `weave --version` verification, and tests for CLI argument parsing, Node platform naming, host platform matching, and npm bin shim path handling. This intentionally verifies the wrapper/platform package resolution path without introducing publish behavior yet.
+
+The fifth implementation slice adds `scripts/publish-npm-packages.ts`, `deno task publish:npm-packages`, ordered platform-before-wrapper publication, downloaded artifact path resolution, executable mode restoration after artifact download, npm dry-run/publish argument construction, and tests for the publish ordering and rehearsal/publish flags.
+
The publish script should support:
- dry-run mode
@@ -195,7 +235,7 @@ The workflow should have jobs similar to:
- `publish-npm-packages`: optional dry-run or publish
- `manage-github-release`: optional draft or published GitHub Release with binary archives and checksum assets
-The release workflow should derive the release tag from bundled release metadata, not from a free-form workflow input. That reduces accidental tag/package mismatch.
+The release workflow derives the release tag from downloaded `bundle-metadata.json` files, not from a free-form workflow input. That reduces accidental tag/package mismatch.
The workflow should support a rehearsal pass:
@@ -261,12 +301,8 @@ The runbook should include:
## Open Issues
-- Confirm the npm package scope and names. Proposed names are `@semantic-flow/weave` and platform packages under the same scope.
-- Decide whether npm publishing uses `NPM_TOKEN`, npm trusted publishing, or both. Kato currently uses `NPM_TOKEN` plus provenance from GitHub Actions.
-- Decide whether `deno.json` can be imported safely for runtime version reporting in compiled binaries, or whether a generated TypeScript version module is cleaner.
-- Decide whether `deno compile` permissions can be narrowed for `weave` in `v0.1.0`, or whether the first binary uses broad permissions with a documented follow-up.
-- Decide whether to pin an exact Deno version for release builds or use `v2.x` as Kato does.
-- Decide whether `v0.1.0` should publish a draft GitHub Release first by default, or whether the first workflow run can publish directly after a dry-run rehearsal.
+- Confirm whether the implemented npm package scope and names need any change before publish. The current metadata default is `@semantic-flow/weave` plus platform packages under the same scope.
+- Decide whether `deno compile` permissions should be narrowed before `v0.1.0` publish, or whether explicit broad CLI permissions are acceptable for the first packaged release.
- Decide whether release artifacts should include SBOM or provenance metadata beyond npm provenance and SHA-256 checksums.
- Decide whether fixture-ladder regeneration must be complete before `v0.1.0`, or whether `v0.1.0` can be a full pipeline release with known fixture-generator work still pending.
@@ -276,14 +312,20 @@ The runbook should include:
- Keep `v0.0.2` as a source-checkpoint release and do not retrofit it into the full pipeline.
- Use root `deno.json` as the preferred authored version source unless implementation proves that impractical.
- Add `deno task bump:version` so humans do not hand-edit release version metadata and release-note stubs.
+- Import root `deno.json` for runtime version reporting and release metadata; generated version modules are not needed yet.
- Ship a native `weave` binary for `v0.1.0`.
- Do not ship separate daemon or web binaries until those surfaces are real release targets.
- Use GitHub Release archives plus `.sha256` checksum files as binary distribution artifacts.
- Use npm wrapper/platform packages as the first package-manager integration.
+- Start with `@semantic-flow/weave` as the wrapper package name and `@semantic-flow/weave-` as the platform package naming convention.
- Model the release workflow on Kato's manual release workflow with rehearsal and publish modes.
- Keep full release packaging in a manual workflow rather than adding automatic publish-on-tag behavior for the first pass.
- Keep release notes as Dendron notes and strip frontmatter for GitHub Release bodies.
- Update [[dev.release-runbook]] as part of this task, after the actual scripts/workflow behavior is known.
+- Use `NPM_TOKEN` through `NODE_AUTH_TOKEN` plus npm provenance for the first publish workflow, matching the current Kato pattern. npm trusted publishing can replace or supplement this later if the package settings are configured for it.
+- Pin release workflow Deno setup to `2.7.14`, matching ordinary CI, until we intentionally choose a floating `v2.x` release lane.
+- Make the manual workflow default to no npm publish and no GitHub Release mutation. Rehearsal is an explicit npm dry-run plus draft GitHub Release run; publication is a later explicit rerun.
+- Use native GitHub-hosted runners for all supported package platforms, with `macos-15-intel` for macOS x64 and `macos-latest` for macOS arm64.
## Contract Changes
@@ -330,29 +372,34 @@ The runbook should include:
## Implementation Plan
-- [ ] Confirm npm package names, release artifact names, and supported platform matrix.
-- [ ] Inventory current `deno task ci` failures and decide which are release-pipeline blockers versus separate product/test debt.
+- [x] Confirm npm package names, release artifact names, and supported platform matrix as implementation defaults.
+- [x] Inventory current `deno task ci` failures and decide which are release-pipeline blockers versus separate product/test debt.
- [ ] Restore the ordinary `deno task ci` quality gate before treating `v0.1.0` as releasable.
-- [ ] Add canonical version metadata to root `deno.json`.
-- [ ] Add runtime version-reporting support and expose `weave --version`.
-- [ ] Add `scripts/bump-version.ts` and root `deno task bump:version`.
-- [ ] Make the bump script create or verify `documentation/notes/release-notes.v.md`.
-- [ ] Add tests for version metadata and bump behavior.
-- [ ] Add `scripts/build-binaries.ts` and root `deno task build:binaries`.
-- [ ] Add `scripts/package-binaries.ts` and root `deno task package:binaries`.
-- [ ] Add bundle metadata, archive naming, and `.sha256` generation.
-- [ ] Add tests for bundle metadata and packaging helpers.
-- [ ] Add `scripts/assemble-npm-packages.ts` and root `deno task assemble:npm-packages`.
-- [ ] Add npm wrapper package and platform package generation.
-- [ ] Add `scripts/smoke-npm-install.ts` and root `deno task smoke:npm-install`.
-- [ ] Add `scripts/publish-npm-packages.ts` and root `deno task publish:npm-packages`.
-- [ ] Add tests for npm package assembly and smoke-test setup.
-- [ ] Add `.github/workflows/release-manual.yml`.
-- [ ] Add native binary smoke tests to the release workflow.
-- [ ] Add npm install smoke tests to the release workflow.
-- [ ] Add optional npm dry-run/publish and GitHub draft/publish jobs to the release workflow.
-- [ ] Ensure GitHub Release creation strips Dendron frontmatter and uploads archives plus checksums.
-- [ ] Update `documentation/notes/release-notes.v0.1.0.md` convention or stub.
-- [ ] Update [[dev.release-runbook]] to make the release workflow the primary path.
+- [x] Add canonical version metadata to root `deno.json`.
+- [x] Add runtime version-reporting support and expose `weave --version`.
+- [x] Add `scripts/bump-version.ts` and root `deno task bump:version`.
+- [x] Make the bump script create or verify `documentation/notes/release-notes.v.md`.
+- [x] Add tests for version metadata and bump behavior.
+- [x] Add `scripts/build-binaries.ts` and root `deno task build:binaries`.
+- [x] Add `scripts/package-binaries.ts` and root `deno task package:binaries`.
+- [x] Add bundle metadata, archive naming, and `.sha256` generation.
+- [x] Add tests for bundle metadata and packaging helpers.
+- [x] Add tests for release platform metadata, archive naming, and build-script arguments.
+- [x] Add `scripts/assemble-npm-packages.ts` and root `deno task assemble:npm-packages`.
+- [x] Add npm wrapper package and platform package generation.
+- [x] Add npm package publish metadata and aggregate package manifest generation.
+- [x] Add `scripts/smoke-npm-install.ts` and root `deno task smoke:npm-install`.
+- [x] Add `scripts/publish-npm-packages.ts` and root `deno task publish:npm-packages`.
+- [x] Add tests for npm package assembly.
+- [x] Add tests for npm package smoke-test setup.
+- [x] Add tests for npm publish ordering and dry-run/provenance arguments.
+- [x] Add `.github/workflows/release-manual.yml`.
+- [x] Add native binary smoke tests to the release workflow.
+- [x] Add npm install smoke tests to the release workflow.
+- [x] Add optional npm dry-run/publish and GitHub draft/publish jobs to the release workflow.
+- [x] Ensure GitHub Release creation strips Dendron frontmatter and uploads archives plus checksums.
+- [x] Update `documentation/notes/release-notes.v0.1.0.md` convention or stub.
+- [x] Update [[dev.release-runbook]] for the current version/binary-build and package state.
+- [x] Update [[dev.release-runbook]] again after the release workflow becomes the primary path.
- [ ] Run `deno task ci`.
- [ ] Run a release rehearsal with npm dry-run and draft GitHub Release before publishing `v0.1.0`.
diff --git a/documentation/notes/wd.task.2026.2026-05-13_1142-refactor.md b/documentation/notes/wd.task.2026.2026-05-13_1142-refactor.md
new file mode 100644
index 0000000..0878baa
--- /dev/null
+++ b/documentation/notes/wd.task.2026.2026-05-13_1142-refactor.md
@@ -0,0 +1,56 @@
+---
+id: jd5zknfphxed645cfk8u1x8
+title: 2026 05 13_1142 Refactor
+desc: ''
+updated: 1778697781645
+created: 1778697752752
+---
+
+## Goals
+
+- some files, like weave.ts have gotten extremely long. Let's refactor to more-manageable files.
+
+## Summary
+
+- Started the `src/core/weave/weave.ts` split by moving mesh support ResourcePage planning into `src/core/weave/mesh_support_pages.ts`.
+- Pulled `WeaveInputError` and `VersionPlan` into small shared modules so the public `./weave.ts` API can continue re-exporting the same names while extracted planners avoid circular imports.
+
+## Discussion
+
+- This is intentionally a narrow first extraction. The moved planner keeps private Turtle-block helpers for now instead of introducing a broad helper module taxonomy while config synthesis is still in motion.
+
+## Open Issues
+
+- Several older fixture-backed tests still fail after the canonical namespace/config-default changes. This refactor preserves the focused mesh support ResourcePage behavior, but the broader fixture ladder still needs regeneration or targeted updates.
+
+## Decisions
+
+- Keep `src/core/weave/weave.ts` as the public façade for existing imports.
+- Move mesh support ResourcePage planning to a dedicated core module first because it is a current config-synthesis seam and has focused test coverage.
+- Keep `VersionPlan` in a shared module so `planVersion` and extracted version-style planners can share the same structural result type without importing through the façade.
+
+## Contract Changes
+
+- No CLI/runtime contract change intended.
+- Existing imports from `src/core/weave/weave.ts` for `WeaveInputError`, `VersionPlan`, `MeshSupportHistoryPolicies`, and `planMeshSupportResourcePages` remain valid through re-exports.
+
+## Testing
+
+- `deno test --allow-read --allow-env src/core/weave/weave_test.ts --filter planMeshSupportResourcePages` passes.
+- `deno test --allow-read --allow-write --allow-env tests/integration/weave_test.ts --filter "executeWeave materializes current support ResourcePages"` passes.
+- `deno task lint` passes.
+- `deno task check` passes.
+- `deno task test` currently fails broadly: 157 passed, 145 failed. Failures match the active fixture/config drift around canonical `sflo` namespace, retired config names, and older generated mesh shapes rather than this extraction's focused behavior.
+
+## Non-Goals
+
+- Do not split every `weave.ts` concern in this pass.
+- Do not regenerate the fixture ladder in this pass.
+
+## Implementation Plan
+
+- [x] Extract mesh support ResourcePage planning from `src/core/weave/weave.ts`.
+- [x] Preserve the public `./weave.ts` export surface.
+- [x] Run focused support-page tests.
+- [x] Run lint and type checks.
+- [ ] Regenerate or update the broader fixture ladder after config synthesis settles.
diff --git a/documentation/notes/wd.task.2026.2026-05-13_1655-support-gh-pages-branch-based-deployments.md b/documentation/notes/wd.task.2026.2026-05-13_1655-support-gh-pages-branch-based-deployments.md
new file mode 100644
index 0000000..8230adf
--- /dev/null
+++ b/documentation/notes/wd.task.2026.2026-05-13_1655-support-gh-pages-branch-based-deployments.md
@@ -0,0 +1,433 @@
+---
+id: whl83xf5i9tlp39wceay5cf
+title: 2026 05 13_1655 Support Gh Pages Branch Based Deployments
+desc: ''
+updated: 1778736548328
+created: 1778716598190
+---
+
+## Goals
+
+- For people for whom a sidecar mesh (in docs) is too much clutter, we need to be able to support the gh-pages publication route
+- update [[wu.repository-options]]
+- Support ontology and software repositories that want dereferenceable Semantic Flow pages without checking generated mesh support artifacts into their normal source branch.
+- Keep the public URL shape stable: branch-based publication should still publish canonical mesh IRIs such as `https://example.github.io/repo/term`, not branch-flavored IRIs.
+- Preserve Weave's fail-closed local path behavior while adding an intentional workflow for reading source files from one checkout/worktree and writing mesh output into another.
+- Record the decision that Fantasy Rules becomes the branch-published ontology fixture once fixture branches are regenerated.
+- Keep branch deployment separate from full fixture ladder generation unless an implementation detail genuinely belongs to both.
+
+## Summary
+
+Some repositories should not carry the generated mesh tree in their normal source branch. Ontology repositories are the immediate pressure point: a repo such as Klaar's URPX ontology may want canonical GitHub Pages publication from `gh-pages`, but may not want a `docs/` directory full of generated histories, inventories, and pages next to the authored ontology source.
+
+The current sidecar pattern assumes a single checkout where the mesh root is a directory inside the source workspace, usually `docs/`. That gives Weave a simple local path story: payload sources can live outside `docs/` but still inside the source repository, and `_mesh/_config/config.ttl` can record `sfcfg:workspaceRootRelativeToMeshRoot ".."` plus constrained `workingLocalRelativePath` grants.
+
+A branch-based deployment is different. The source branch and the publication branch are different filesystem trees, often represented by sibling git worktrees during local generation. Weave needs to read authored source from the source checkout and write the generated mesh into the `gh-pages` checkout, without making the published branch depend on a developer's local sibling-directory layout. This task is about designing and implementing that deployment mode cleanly.
+
+## Discussion
+
+### Repository Topologies
+
+We currently describe two topologies in [[wu.repository-options]]:
+
+- whole-repo mesh: the repository itself is the mesh
+- sidecar mesh: the public mesh lives in a directory such as `docs/` inside the source checkout
+
+Branch-based publication should be the third topology:
+
+- branch-published mesh: authored source remains on the normal source branch, generated mesh output lives on a publication branch such as `gh-pages`
+
+This is not merely a naming variant of `docs/` sidecar. It shares the same conceptual goal as sidecar publication, but its operational shape is different because the mesh root is not a subdirectory of the source checkout.
+
+The user-facing docs should probably position branch-published meshes as the best fit for repositories where the source branch should stay clean: ontology repos, vocabulary repos, compact spec repos, and software repos that publish semantic documentation as a projection rather than as checked-in source material.
+
+### Why This Is Not Just `--mesh-root ../repo-gh-pages`
+
+The current runtime can technically be coaxed into reading sibling paths if the mesh config sets a workspace root above both worktrees and grants a relative path such as `../source/ontology/`. That would be a bad public contract.
+
+Those paths are host-local operational facts, not semantic facts about the published mesh. If `_mesh/_config/config.ttl` in the `gh-pages` branch says the source is at `../urpx/ontology/`, the public branch has encoded one developer's checkout layout. A different contributor, CI runner, or downstream clone may not have that sibling directory at all.
+
+The branch-based mode should therefore distinguish:
+
+- durable publication data carried in the `gh-pages` branch
+- host-local generation settings that say where the source checkout and publication checkout live today
+- semantic source provenance that can name the source repository, source branch, source path, and optionally source commit/ref without implying a local filesystem path
+
+This is the main departure from the existing file-based permission scoping. We should keep file access fail-closed, but the operator's local access grant should not be confused with the published mesh's source provenance.
+
+### Working Source Locators
+
+Current payload metadata leans heavily on `sflo:workingLocalRelativePath` or `sflo:hasWorkingLocatedFile`. That is natural when the source file is inside or adjacent to the mesh root in the same repository checkout. Branch-published meshes need a cleaner distinction.
+
+Possible approaches:
+
+- keep `workingLocalRelativePath` for local generation only, but use host-local config or command options to resolve it against a source checkout that is not published
+- introduce a target-neutral source-repository locator shape for branch-published inputs, such as source repository URL, source branch/ref, source path, and expected content digest
+- copy current source bytes into a mesh-carried source cache before weaving, then version from that local cache
+- require branch-published workflows to integrate from materialized source snapshots and treat the source branch as provenance rather than as the runtime working file
+
+The locator should not be payload-specific. The same general source locator idea should be able to point at authored payload bytes, page-source Markdown, stylesheet assets, local/default config inputs, or any other target material that the publication branch needs to materialize. The binding can be target-specific, but the source-addressing shape should be general.
+
+Raw URLs may be enough for some remote inputs, especially when the URL is immutable and digest-pinned. GitHub raw URLs are not a complete replacement for a branch/ref/path locator, though. A raw branch URL is mutable, loses some repository/ref/path structure unless we parse GitHub-specific URL conventions, and is awkward for private repos, local worktrees, and CI checkouts. A git-oriented locator can still render or resolve through a raw URL when that is useful, but the durable source binding should be able to say "repo + ref + path + digest" directly.
+
+The first implementation should use the initial core repository-source locator vocabulary for the durable RDF shape, even while local source resolution remains command/profile-scoped. We should not hard-code sibling paths into generated public artifacts as though that were a durable source reference.
+
+### Ontology Shape
+
+The existing target-relator pattern is close to what branch-published meshes need. `ArtifactResolutionTarget` already provides the generic relator boundary, with specialized subclasses such as `ExtractionSource` and `ResourcePageSource`, and properties such as `targetLocalRelativePath`, `targetAccessUrl`, `hasTargetArtifact`, `hasTargetLocatedFile`, `hasTargetDistribution`, `hasRequestedTargetHistory`, `hasRequestedTargetState`, `hasArtifactResolutionMode`, `hasArtifactResolutionFallbackPolicy`, and `expectsContentDigest`.
+
+Repo/ref/path/digest support should extend that pattern rather than introduce a payload-only side channel. The missing piece is a durable source locator that can name a version-control repository, a ref or commit, a path inside that ref, and an expected digest. That locator should be usable from any target relator that needs source bytes, including config materialization, payload integration, page-source Markdown, and assets.
+
+The shape probably belongs in core `sflo` if it describes durable source provenance and target byte identity. Operational trust rules for fetching or reading those sources should remain in config/runtime policy, not in the core source locator itself.
+
+### Source Binding Contract
+
+A branch-published source binding has two deliberately separate halves:
+
+- operation-local resolution: deploy receives a trusted local `sourceRoot` plus repository-relative `sourcePath` from CLI flags, a deploy profile, CI checkout layout, or host-local runtime state. This local root may be a sibling git worktree, but it is command-scoped input to the deploy operation, not durable mesh config.
+- durable publication binding: `_mesh/_config/config.ttl` records the target and source identity using `ArtifactResolutionTarget` plus `RepositorySourceLocator` vocabulary. The durable facts are target designator, target publication-relative path, expected digest, source repository URL, source ref, optional source commit, source repository-relative path, and content digest.
+
+The publication branch should not persist any of the following merely because a deploy read from a sibling source checkout:
+
+- `sfcfg:workspaceRootRelativeToMeshRoot` that expands the publication workspace to include the source checkout
+- `sfcfg:hasLocalPathAccessRule` granting access back to the source checkout
+- `sflo:workingLocalRelativePath` pointing at `../source/...` or another host-local layout
+- absolute local paths, file URLs, or other machine-specific checkout locations
+
+Host-local grants such as `.sf-local-access.ttl` remain valid for lower-level operations that intentionally resolve extra-mesh `workingLocalRelativePath` values, but branch-published deploy should not require or mint such grants for its normal `sourceRoot` path. Deploy materializes source bytes into a publication-root relative target path first, then integrates/weaves from that publication-local copy. The durable config records provenance and byte identity, not the operator's checkout topology.
+
+The minimum durable source binding shape for the first implementation is:
+
+- `ArtifactResolutionTarget` for the binding relator
+- `sflo:hasTargetArtifact` for the designator being materialized
+- `sflo:targetLocalRelativePath` for the publication-root relative target path
+- `sflo:expectsContentDigest` for the target bytes Weave expects to publish
+- `sflo:hasTargetRepositorySource` pointing to a `RepositorySourceLocator`
+- `sflo:sourceRepositoryUrl` for the durable repository identity or equivalent repository access URL
+- `sflo:sourceRepositoryRef` for the symbolic or explicit ref used as source provenance
+- `sflo:sourceRepositoryCommit` when an exact commit is known and honestly describes the materialized bytes
+- `sflo:sourceRepositoryPath` for the repository-relative source path
+- `sflo:hasContentDigest` for the source bytes that were actually materialized
+
+Raw URLs are acceptable as access/rendering hints, or as the repository/source URL for non-git immutable resources that are digest-pinned. For git-hosted source material, URL-only bindings are too lossy as the primary durable identity because branch raw URLs are mutable and do not clearly preserve repository/ref/path structure. A GitHub raw commit URL can be useful, but the structured repo/ref-or-commit/path/digest locator should remain the canonical binding when the source is in git.
+
+### Clean Source Branch
+
+It should be possible for the normal source branch to contain no Semantic Flow or Weave files at all. In that shape:
+
+- authored ontology/source files live on the normal source branch
+- the publication branch carries `_mesh/`, generated pages, histories, inventories, and Weave mesh config
+- branch-published config records source bindings using repo/ref/path/digest-style provenance rather than local checkout paths
+- host-local checkout paths are supplied by CLI flags, deploy profile state, CI checkout layout, or `.sf-local-access.ttl`
+
+This means `_mesh/_config/config.ttl` can live in `gh-pages` and still be the mesh's durable config. The source branch stays clean. The main caveat is bootstrap: before the `gh-pages` branch exists, Weave needs enough command/profile input to create the first publication branch and seed its config. After that, source-to-target mappings can be maintained on the publication branch.
+
+There is a review tradeoff. If config lives only on `gh-pages`, adding a new target or changing source bindings is a publication-branch change rather than a source-branch change. That may be acceptable for clean source repos, but the workflow should make it visible and reviewable.
+
+### Bootstrap Inputs
+
+API and CLI inputs can provide everything needed for first bootstrap if they are allowed to carry a structured publication request. There are two bootstrap levels that should stay distinct:
+
+- publication-branch bootstrap: create or locate the publication worktree/branch and seed the empty mesh/config shell
+- first materialization: bind one or more source inputs to mesh targets and run the first integrate/weave/generate pass
+
+The publication-branch bootstrap can be small. It needs:
+
+- source checkout root or source repository URL
+- source ref or commit when the operator wants an explicit pin; otherwise Weave can infer only clean, inspectable git facts such as the checked-out branch and exact `HEAD` commit
+- publication checkout root or publication branch name
+- mesh base IRI, either supplied explicitly or inferred from GitHub remote metadata when the default project-site URL is appropriate
+- publication controls such as branch-create policy, `.nojekyll`, optional `CNAME`, local commit policy, and preserved-file policy
+
+Initial target bindings are not required just to bootstrap the branch. They are required for the first useful materialization because Weave otherwise does not know which source file should become which mesh target, which source files are config inputs, which page-source assets should be materialized, or what designator paths should be integrated/extracted/generated.
+
+Digests are also not required as operator-supplied bootstrap inputs. If the source ref is mutable or omitted, Weave can compute and record digests during materialization. Digest requirements become more important for deterministic replay, remote-source refresh, and provenance validation.
+
+For the API, this can be a normal structured object. For the CLI, pure flags are possible for publication-branch bootstrap. They get noisy once first materialization involves multiple targets. A first CLI slice can support explicit flags for one or a few targets, but a profile input is likely needed before this is pleasant:
+
+```bash
+weave deploy gh-pages --bootstrap-profile publish.weave.json
+```
+
+The profile does not have to live in the source repository. It can be provided from outside the repo, from CI configuration, or from an operator's local working directory. If the goal is a completely clean source branch, the bootstrap profile should be treated as an operational input that seeds durable config into the publication branch, not as a file Weave requires on the source branch.
+
+In this task, "publication root" means the local checkout/worktree directory where Weave writes the published mesh. GitHub Pages branch publishing currently serves either the selected branch root or that branch's `/docs` folder. For a `gh-pages` branch deployment, Weave should default to the branch root as both the publication source folder and mesh root, while leaving room for an explicit `/docs` override if a user deliberately chooses that Pages setting.
+
+### Inference Rules
+
+Inference should be conservative, inspectable, and visible in dry-run output. Weave should infer only values it can derive from the supplied source/publication roots without network magic or ambiguous repository conventions. Any inferred value should be overrideable by CLI flag or deploy profile, and host-local paths must never be persisted as durable mesh facts.
+
+Publication worktree location is not inferred. In non-interactive runs, deploy requires `--publish-root` or a deploy profile value. In interactive runs, Weave may offer a conventional sibling default such as `../-gh-pages`, but the operator must accept or edit that path before Weave uses it.
+
+Publication source folder defaults to the branch root for `gh-pages` branch publishing. A future explicit override can support repositories configured to serve `/docs` from the publication branch, but the first branch-published surface should treat publication root as the mesh root and generated Pages source.
+
+Source repository URL can be inferred from the source checkout only when git metadata is available and one durable remote is unambiguous. Prefer `origin` when present; otherwise accept a single configured remote. If there are multiple plausible remotes, no remote, or a local-only remote that is not a durable publication identity, require `--source-repository-url` or a profile value.
+
+Source repository ref can be inferred from a git source checkout when `git symbolic-ref --short HEAD` returns a branch name. Detached `HEAD` should not silently become a symbolic source ref; in that case Weave should require an explicit source ref or use an explicit commit-like ref only when the operator/profile asks for that behavior. Tags, full refs, and commit SHAs supplied by the operator should be preserved rather than rewritten.
+
+Source repository commit can be inferred with `git rev-parse HEAD` only when the source checkout is clean enough for the commit to honestly describe the source bytes being materialized. If the source checkout has uncommitted changes that affect materialized inputs, Weave should either omit `sourceRepositoryCommit` and rely on the digest, or require an explicit override that makes the provenance policy visible. Recording `HEAD` as the source commit for dirty working-tree bytes would be misleading.
+
+Mesh base can be inferred only for clear GitHub Pages project-site cases, such as an unambiguous GitHub remote for `owner/repo` plus the default project-site base `https://.github.io//`. Custom domains, user/organization sites, enterprise GitHub hosts, non-GitHub remotes, and multiple remotes should require `--mesh-base` or a profile value until richer Pages metadata support exists.
+
+Publication branch name may default to `gh-pages` for worktree creation/planning, but the public mesh base must not include or expose the branch name. Commit and push behavior should never be inferred from branch names or remotes; it remains explicit operator or CI policy.
+
+### Command Shape
+
+There are two plausible command surfaces:
+
+```bash
+weave deploy gh-pages --source-root . --publish-root ../repo-gh-pages --mesh-base https://semantic-flow.github.io/repo/
+```
+
+or a more general profile-driven form:
+
+```bash
+weave deploy --profile gh-pages
+```
+
+The profile-driven shape is nicer long term, but the first slice can be explicit if that gets the path semantics and tests right. Important inputs are:
+
+- source checkout root
+- publication checkout root
+- publication branch name, usually `gh-pages`
+- mesh base IRI
+- source paths or target designator paths to integrate/version/generate when the command is doing first materialization rather than only branch bootstrap
+- whether to initialize/reset the publication branch
+- whether to commit and/or push
+
+The command should default to dry-run or no-push behavior until the branch state is inspectable. Creating or force-updating a publication branch should require an explicit flag.
+
+If the publication worktree location is not supplied, the interactive CLI should prompt for it rather than silently guessing a sibling path. It may offer a conventional default such as `../-gh-pages`, but the operator needs to accept or edit that value before Weave creates or uses the worktree. In non-interactive mode, CI, or when stdin is not a TTY, an omitted publication root should fail with a clear message that points to `--publish-root` or a deploy profile value.
+
+The prompt should be for the local publication worktree path, not for the public base IRI. The mesh base can still be inferred from GitHub remote metadata when that inference is enabled, but a host filesystem path is too consequential to infer and persist without explicit operator confirmation.
+
+### Dry-Run Surface
+
+The first deploy workflow surface is local-only and inspectable:
+
+```bash
+weave deploy gh-pages --dry-run --source-root . --publish-root ../repo-gh-pages --mesh-base https://example.github.io/repo/
+```
+
+Dry-run performs the same input validation as the write path, including source/publication root checks, dirty publication worktree enforcement unless explicitly skipped, stale output rejection, and generated-RDF local-path leakage checks. It then simulates the deploy in an isolated temporary copy of the publication root so the reported path set comes from the same mesh create, source materialization, integrate, payload update, and weave operations that the real deploy would use.
+
+The human-facing plan should print the source root, publication root, mesh base, mesh IRI, paths that would be created, paths that would be updated, existing files that would be preserved unchanged, materialized source provenance including digest when a source binding is supplied, validation checks, and git operations. For the first slice, git operations are intentionally limited to worktree inspection; Weave writes local files but does not commit unless an explicit commit flag is added, and does not push.
+
+### Git Worktree Model
+
+The likely implementation path is to use git worktrees rather than checking out branches in-place:
+
+- source branch remains checked out at the normal repository root
+- publication branch is checked out into a sibling temporary or configured worktree
+- Weave writes generated mesh files into the publication worktree
+- optional local commit happens from that publication worktree; push remains an explicit operator or CI action outside the first commit-support slice
+
+The workflow should handle:
+
+- missing `gh-pages` branch
+- existing `gh-pages` branch
+- dirty publication worktree
+- stale generated files that should be removed before regeneration
+- preserving intentionally carried files such as `CNAME`, `.nojekyll`, or deployment metadata
+
+We should be very conservative about deletes. A publication branch reset is acceptable only behind an explicit flag and after preserving or re-creating known publication control files.
+
+### Local Generation Workflow
+
+The local workflow should be split into phases that make the branch boundary visible:
+
+1. Resolve deploy inputs from CLI flags, deploy profile, CI environment, or prompts. Required roots are the source checkout root and publication root. The mesh base and source binding facts may be explicit or inferred only under the conservative rules above.
+2. Inspect the source checkout. Confirm it exists, is distinct from the publication root, and contains each requested repository-relative source path. If source repository URL, ref, or commit are inferred, record whether they came from git remote metadata, symbolic `HEAD`, or exact `HEAD` commit. Dirty source checkouts are allowed for local byte materialization, but they should prevent silently recording `HEAD` as the source commit for changed bytes.
+3. Resolve the publication worktree. If `--publish-root` names an existing directory, use that directory after root-overlap and dirty-worktree checks. If a future profile/flag asks Weave to create the worktree, require an explicit branch/create policy before running `git worktree add`; do not switch the source checkout in-place.
+4. Inspect publication state before writing. Reject dirty publication git worktrees by default, reject partial branch-published mesh bootstrap state, reject stale local/publication clutter such as `.weave`, `.sf-local-access.ttl`, or old `docs/_mesh`, and report preserved non-generated files. Dry-run should perform the same checks before simulating writes in a temporary copy.
+5. Bootstrap or reuse the publication mesh. If no `_mesh/_meta`, `_mesh/_inventory`, and `_mesh/_config` bootstrap exists, create the minimal mesh shell in the publication root. If all exist, reuse them only when the requested mesh base matches. If only some exist, fail closed rather than guessing how to repair them.
+6. Materialize source bindings. For each binding, read bytes from `sourceRoot/sourcePath`, compute the digest, copy/update the publication-root relative target path, upsert the `RepositorySourceLocator` block in publication config, and run existing integrate/payload-update/weave operations from the publication-local target bytes. This keeps normal generation inside the publication workspace after the initial command-scoped read.
+7. Validate generated publication output. Scan generated RDF for source/publication root paths and parent traversal, preserve publication controls such as `.nojekyll` and configured `CNAME`, and report created, updated, preserved, and woven paths. Later Accord checks can sit after this phase for fixture or CI acceptance.
+8. Leave git publication actions explicit. The local write path stops after validated file writes unless `--commit` is supplied. When commit support is requested, Weave stages the publication worktree after validation, creates a local commit only when there is a publication diff, and prints a reminder that the publication branch still needs to be pushed for GitHub Pages to update. Push support should remain a separate explicit operator/CI policy.
+
+The guardrails are intentionally stricter than a generic static-site build:
+
+- never infer or persist the local publication worktree path
+- never broaden the publication mesh workspace to include the sibling source checkout
+- never create source-branch `_mesh`, `.weave`, `docs`, or `.sf-local-access.ttl` state as part of branch-published deploy
+- never create, reset, or force-update a publication branch without an explicit branch policy flag or profile setting
+- never delete unknown publication files during normal incremental deploy
+- never record an exact source commit for bytes that are not actually represented by that commit
+- never let commit or push happen as a side effect of a command whose surface only promised local generation
+
+The first implementation now covers the local-write subset and explicit local commit slice of this workflow. Worktree creation and source-ref/mesh-base inference should remain separate guarded slices rather than being folded into the basic materialization path. Push stays out of the first commit-support slice; when Weave creates a local publication commit, the CLI clearly tells the operator that the commit must still be pushed for GitHub Pages to go live. Rebuild-from-scratch belongs in [[wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild]].
+
+### Workspace Model
+
+Branch-published deployment should not broaden the existing workspace concept so that one workspace casually spans both sibling worktrees. That is exactly the move that would make `../source-repo/...` feel natural in persisted RDF, and that is the shape we are trying to avoid.
+
+For the first implementation, treat the publication root as the active mesh root and publication workspace. It owns `_mesh/`, `_mesh/_config/config.ttl`, generated pages, histories, inventories, validation output, and any publication-branch-local runtime state. Treat the source checkout as a trusted operation input root supplied by CLI, deploy profile, CI layout, or machine-local operational config. The deploy operation can create an in-memory resolver binding from durable source locator facts to that local source root, but the local sibling path must not become a durable mesh fact.
+
+This means branch publication introduces a deploy context with at least two local roots: source root and publication root. That is not the same as redefining every Weave workspace as multi-root. If later daemon or multi-mesh work needs a general multi-root workspace model, it should be designed there; this task only needs enough context to keep clean source branches and fail-closed local access compatible.
+
+### Incremental Publication
+
+Branch-published meshes should be updated incrementally by default rather than overwritten on every run. The publication branch is not just disposable build output once it carries mesh histories, current-state progression, config, inventories, and release pages. Treating it as stateful is unusual for GitHub Pages, but it matches the Semantic Flow model better than rebuilding the branch from scratch every time.
+
+The workflow should still support an explicit rebuild mode for disaster recovery, fixture regeneration, or intentional model churn. That mode should be loud and guarded, for example `--rebuild-from-scratch` plus a dirty-worktree check and an explicit preserved-file list. It is deferred to [[wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild]] so ordinary deploy can remain incremental by default. Default `deploy` should read the existing publication branch, compute the next semantic update, validate it, and optionally create a local commit only when requested.
+
+### Fixture Implications
+
+The current Fantasy Rules fixture branch ladder demonstrates a `docs/` sidecar mesh. For the next generated ladder, Fantasy Rules should demonstrate branch-published ontology delivery because Alice Bio already exercises a whole-repo reference mesh and branch-published deployment is the more urgent ontology case.
+
+This does not mean the `docs/` sidecar pattern goes away. It means the fixture corpus has better coverage if:
+
+- Alice Bio remains the whole-repo/reference mesh fixture
+- Fantasy Rules becomes the branch-published ontology fixture
+- docs-rooted sidecar behavior is covered by focused tests or a smaller fixture rather than by the main long ladder
+
+[[wa.completed.2026.2026-05-07-fixture-ladder-generator]] records this topology before rerunging branches.
+
+Accord now honors `ignorePaths` in whole-tree transition completeness checks. That is useful for branch-generated fixtures: manifests can assert that no unexpected source or publication tree paths changed while still ignoring intentional local-only assets, fixture setup material, or other declared non-contract paths. Branch-published manifests should use this for source-branch cleanliness and publication-branch completeness, and should rely on Accord's conflict checks to reject manifests that both ignore and explicitly expect the same path.
+
+The ordering should be: settle the Semantic Flow Framework Fantasy Rules branch-published spec/example first, prove the branch-published clean-source behavior in a focused temporary-git integration slice, and build enough fixture-generator support to replay the chosen topology. Do not spend a full regeneration pass on the current `docs/` sidecar ladder immediately before replacing that ladder. The actual fixture branch rerung should happen later, once the branch-published topology, repository-source locator RDF, and near-term config/ontology churn are all stable enough to regenerate in one intentional pass.
+
+### GitHub Pages Details
+
+Branch-based GitHub Pages usually serves the root of the selected branch. Weave should make sure the generated branch contains the usual publication affordances:
+
+- `.nojekyll` unless disabled
+- optional `CNAME`
+- generated `index.html` for the mesh root when the root Knop/page exists
+- generated resource pages and historical pages
+- no accidental source branch clutter
+
+The canonical base IRI should be independent of the branch name. For GitHub Pages project sites, it is typically `https://.github.io//`; for custom domains, it may be the custom origin.
+
+### CI/Automation
+
+The branch-published workflow should be scriptable in GitHub Actions:
+
+- checkout source branch
+- checkout or create `gh-pages` worktree/branch
+- run Weave generation
+- run validation
+- commit generated changes only when there is a diff
+- push `gh-pages` explicitly from CI or the operator's release workflow
+
+This should eventually support CI permissions that are narrower than a blanket token with arbitrary write access. The task can start locally, but the design should not preclude a safe Action later.
+
+## Open Issues And Working Answers
+
+- Topology name: use `branch-published mesh` in user docs. `gh-pages mesh` is too GitHub-specific, `publication branch mesh` is accurate but clunky, and calling it only a sidecar mesh hides the important operational difference. The docs can describe it as a sidecar-like publication topology implemented through a publication branch.
+- Source locators: branch-published targets should not use `workingLocalRelativePath` as their durable source provenance. The first implementation may use command/profile-scoped source-root resolution to read local files, but any persisted source binding should use core `sflo` repository-source locator vocabulary such as `RepositorySourceLocator`, `hasTargetRepositorySource`, `sourceRepositoryUrl`, `sourceRepositoryRef`, `sourceRepositoryCommit`, `sourceRepositoryPath`, and `hasContentDigest` / `expectsContentDigest`.
+- Core versus config: repo/ref/path/digest identity belongs in core `sflo` as reusable source locator vocabulary that composes with `ArtifactResolutionTarget`; this vocabulary should land early, if not first, so the branch-published proof slice does not grow around temporary path-shaped RDF. Operational policy for resolving that locator, deciding whether network or local git access is allowed, and mapping it to a local checkout belongs in config/runtime policy.
+- Clean source branch: `_mesh/_config/config.ttl` on the publication branch should be enough to support a source branch with no Semantic Flow or Weave files. This is the point of the topology. The source branch may still opt into carrying a bootstrap profile or authored config later, but that must not be required.
+- Bootstrap surface: keep explicit flags for the first narrow CLI slice, but design the API around a structured request and make a deploy profile the pleasant path before target bindings become numerous. A completely clean source branch means the profile can live outside the source repo and seed durable config into the publication branch.
+- Inference defaults: infer only values that are conventional and inspectable. Source repository URL can come from an unambiguous durable git remote, source ref can come from a symbolic checked-out branch, source commit can come from `HEAD` only when it honestly describes the materialized bytes, mesh base can come from GitHub remote/project Pages metadata when unambiguous, and `gh-pages` should default to branch-root publication. The publication worktree path should be prompted for interactively or required non-interactively, not silently guessed.
+- Bootstrap versus materialization: model publication-branch bootstrap and first materialization as separate phases. One CLI command may perform both when target bindings are supplied, but the planner and tests should prove the phases independently.
+- Host-local paths: allow CLI flags, deploy profile values, CI environment/request data, and higher-trust local config to supply source and publication roots. Do not write those roots, or grants derived from their sibling relationship, into the public `gh-pages` branch.
+- Cross-worktree access: cross-worktree source access should be host-local and command-scoped for the first implementation. A publication branch may carry durable source provenance and project-local expectations, but it should not grant itself arbitrary sibling checkout access.
+- Local generation workflow: resolve/inspect source and publication roots, reject unsafe publication state, bootstrap or reuse the publication mesh, materialize source bindings into publication-local target files, validate output, and stop before git commit unless explicit future flags request that action. Push remains external to the first commit-support slice.
+- Durable source binding model: use git repository/ref/path/digest as the default durable model, with raw URLs as optional access/rendering forms. URL-first bindings are too lossy for private repos, local worktrees, branch/ref semantics, and digest-pinned replay.
+- Preserved files: normal incremental deployment should preserve unknown non-generated files by default, and always preserve or recreate configured publication control files such as `.nojekyll` and `CNAME`. Reset/rebuild mode needs an explicit preserved-file policy and should refuse a dirty publication worktree unless forced.
+- Command composition: the deploy command should orchestrate existing mesh create, integrate/version/weave/generate seams rather than invent a parallel generator. It can expose a higher-level workflow because the branch-published operator experience is different, but the internal semantic operations should remain recognizable and testable.
+- No semantic payload change: if the source branch changes but resolved source bytes or semantic output do not change, Weave should validate, report no publication diff, skip local commit by default, and never push implicitly. Provenance-only updates, such as recording a new source commit for identical bytes, should be explicit policy rather than accidental churn.
+- Default history policy: the branch-published proof path should float with the current default effective config. In particular, it must not create `_mesh/_inventory`, `_knop/_meta`, or `_knop/_inventory` history merely to preserve old fixture-ladder shapes. Legacy/versioned inventory shapes belong behind explicit non-default policy and can remain covered by Alice Bio or other compatibility fixtures.
+- Default-history proof: the focused branch-published materialization slice now keeps MeshInventory, KnopMetadata, and KnopInventory current-only under runtime defaults while preserving the old explicit/versioned core shape when non-default policies are supplied.
+- Dirty publication roots: branch-published deploy now refuses a dirty publication git worktree root by default. Operators can explicitly opt into dirty-root deployment for local experimentation, but the default path requires committed/stashed/clean publication state before Weave writes generated output.
+- Publication controls: branch-published deploy preserves unknown files by leaving them alone, recreates `.nojekyll` when GitHub Pages protection is enabled, and can create or update a configured `CNAME` without persisting local checkout paths.
+- Stale output validation: branch-published deploy rejects known stale local/publication clutter such as `.weave`, `.sf-local-access.ttl`, and old `docs/_mesh` sidecar output, then scans generated RDF support files for local source/publication root paths or parent-directory traversal before reporting success.
+- Rebuild mode: rebuild-from-scratch should exist, but only as a separate guarded task after incremental update behavior is proven. Track it in [[wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild]] rather than bundling it into the first branch-published deploy path.
+- Fixture placement: prefer converting Fantasy Rules to the branch-published ontology fixture if we keep only two main fixture repos. If that creates too much churn during fixture ladder regeneration, create focused temporary-git integration coverage first and defer the fixture move through [[wa.completed.2026.2026-05-07-fixture-ladder-generator]].
+- Fixture placement decision: Fantasy Rules is the branch-published ontology fixture for the next rerung. The existing `docs/` sidecar pattern remains valid, but Fantasy Rules no longer needs to preserve that topology as its primary durable example.
+- Fixture regeneration timing: rewrite the Semantic Flow Framework Fantasy Rules spec/example and build focused branch-published proof coverage before rerunging fixture branches. Build fixture-generator machinery early enough to avoid manual repair, but defer full branch-ladder regeneration until the topology and vocabulary are stable.
+- Git automation boundary: Weave owns safe local planning, dirty-state checks, generation, validation, and explicit optional local commit creation. Worktree discovery/creation remains a future guarded slice. Push policy and CI credentials should remain explicit operator/CI concerns, with documented snippets rather than hidden automation. If Weave creates a local publication commit, the CLI warns that the operator or CI still needs to push it before the site updates.
+- Vocabulary timing: the durable design needs core ontology vocabulary for repo/ref/path/digest source locators early, preferably before the first branch-published materialization slice. The proof slice can still take local source roots from runtime/deploy request data, but the RDF shape for persisted source provenance should already be the core locator shape rather than a throwaway branch-deploy special case.
+- Workspace concept: do not re-address the general workspace model for this task. Define a branch deploy context with source root plus publication root, keep the publication root as the active mesh workspace, and treat the source root as a trusted operation input. Re-open the broader workspace concept only if daemon, multi-mesh, or long-lived multi-root use cases demand it.
+- First implementation acceptance slice: prove the clean-source-branch story before adding fancy publishing automation. The source branch should contain only ontology/source files, the `gh-pages` branch should carry all `_mesh`, config, generated pages, histories, and inventories, no local sibling paths should appear in public RDF or generated config, and a second run should update incrementally.
+
+## Decisions
+
+- Branch-based publication is a first-class repository topology, not merely an accidental use of `--mesh-root` with a sibling path.
+- User-facing docs should call the topology `branch-published mesh`.
+- The public mesh base IRI must not include or expose the publication branch name.
+- Do not encode developer-specific sibling checkout paths as durable public mesh facts.
+- The design should allow the normal source branch to remain free of Semantic Flow and Weave files; durable mesh config may live on the publication branch.
+- API/CLI bootstrap inputs may provide everything needed to create the first publication branch and seed its durable mesh config.
+- Source repository URL, source ref, source commit, mesh base, and publication source folder may be inferred only when local git/Pages conventions make them unambiguous and honest, while remaining explicit/overrideable.
+- Branch-published source bindings separate operation-local `sourceRoot` resolution from durable repo/ref/path/digest publication facts; deploy must not persist sibling worktree paths or local path grants as source provenance.
+- Raw URLs are secondary access/rendering hints for git-hosted source material; the canonical durable binding for git sources is structured repository/ref-or-commit/path/digest provenance.
+- Branch-published local generation should use git worktrees rather than in-place branch switching, reject dirty or partial publication state by default, reserve branch creation and commit creation for explicit guarded flags or profile settings, defer rebuild mode to [[wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild]], and leave push as an explicit external action.
+- The interactive CLI should prompt for the publication worktree path when it is omitted; non-interactive runs should require `--publish-root` or a deploy profile value.
+- Branch-published deployment uses a deploy context with a source root and a publication root; it does not redefine the general workspace model. The publication root is the active mesh workspace, while the source root is a trusted operation input.
+- Default branch-published deployment should update the existing publication branch incrementally rather than overwrite it from scratch.
+- Branch-published deployment should follow default current-only MeshInventory, KnopMetadata, and KnopInventory behavior unless the operator/config explicitly requests versioned support history.
+- Keep write and git behavior explicit; branch publication should be dry-run or local-only until the operator opts into local commit creation, and push remains outside the first commit-support slice.
+- Preserve the existing `docs/` sidecar pattern as valid even though the Fantasy Rules fixture moves to branch-published publication.
+
+## Contract Changes
+
+- Weave should gain a documented branch-published mesh workflow for source repos that publish generated mesh output from a dedicated branch.
+- User-facing repository topology docs should describe whole-repo, directory sidecar, and branch-published options.
+- Branch-published source bindings should be target-neutral rather than payload-only, so config inputs, payload bytes, page sources, and assets can use the same addressing model.
+- Core ontology includes initial repo/ref/path/digest source locator vocabulary that extends the existing target-relator pattern.
+- Runtime/deploy config may need to distinguish host-local source checkout access from durable mesh-carried source provenance.
+- CLI/API surface may gain a deploy command or profile that accepts source root, publication root/branch, mesh base, and safe local write/commit flags.
+- Interactive CLI execution should prompt for a missing publication worktree path; CI and other non-interactive execution should fail closed unless the path is supplied.
+- Fixture expectations will change because the Fantasy Rules fixture stops using `docs/` sidecar output as its primary topology and becomes the branch-published ontology fixture.
+- The Semantic Flow Framework Fantasy Rules example/spec should be rewritten around the branch-published ontology shape before the fixture ladder is rerung.
+- Full fixture branch regeneration should be a later generated-output pass, not a prerequisite for the first branch-published implementation slice.
+
+## Testing
+
+- Add focused unit tests for deploy/profile argument parsing once the command shape is selected.
+- Add path-policy tests proving cross-worktree source access is fail-closed unless explicitly granted by host-local config or command-scoped options.
+- Add tests proving public mesh config does not serialize developer-specific sibling checkout paths into publication output.
+- Add tests proving the source branch can remain free of `_mesh`, `.weave`, `docs`, or other Weave/Semantic Flow generated files while the publication branch carries the mesh.
+- Add tests proving bootstrap API/CLI inputs can seed a publication branch from a clean source branch.
+- Add tests for bootstrap inference: omitted source ref uses default-branch `HEAD`, GitHub remote metadata can infer the default mesh base, and `gh-pages` defaults to branch-root publication.
+- Add tests proving normal deployment updates an existing publication branch incrementally, while rebuild/reset behavior requires an explicit guarded flag.
+- Add local integration coverage using a temporary git repo with a source branch and a `gh-pages` worktree.
+- Add CLI coverage proving omitted publication root prompts interactively and fails closed in non-interactive mode.
+- Verify generation preserves `.nojekyll` and configured `CNAME`, removes stale generated files only when requested, and refuses dirty publication worktrees by default.
+- Add fixture or focused coverage for a branch-published ontology source where authored source stays off the publication branch.
+- Because Fantasy Rules moves to branch-published output, update its Accord manifests and fixture helper assumptions through [[wa.completed.2026.2026-05-07-fixture-ladder-generator]], using whole-tree completeness checks plus `ignorePaths` for intentional non-contract paths.
+- Rewrite the Semantic Flow Framework Fantasy Rules example/spec so the conformance story names source-branch authored ontology files, publication-branch mesh output, and repository-source locator provenance.
+- Run `deno task lint` after significant implementation changes.
+
+## Non-Goals
+
+- Replacing ordinary `docs/` sidecar publication.
+- Requiring every repository to use git branches or GitHub Pages.
+- Designing a universal static-site deploy system for all hosts.
+- Force-pushing or deleting publication branch content by default.
+- Solving fixture ladder regeneration directly.
+- Hiding source provenance; branch-published meshes still need to say where their source material came from, just not as host-local checkout paths.
+
+## Implementation Plan
+
+- [x] Confirm terminology and update [[wu.repository-options]] with a branch-published topology section.
+- [x] Add initial core `sflo` repository-source locator vocabulary for durable repo/ref/path/digest provenance.
+- [x] Confirm first implementation source-binding scope: command/profile-scoped local resolution is allowed for the proof slice, but any persisted binding needs target-neutral repo/ref/path/digest rather than `workingLocalRelativePath`.
+- [x] Define the minimum source binding shape for repo/ref/path/digest inputs, including when raw URLs are acceptable.
+- [x] Draft the core ontology change for a repo/ref/path/digest locator that composes with `ArtifactResolutionTarget`.
+- [x] Define bootstrap API/CLI inputs for creating the first publication branch from a clean source branch.
+- [x] Split publication-branch bootstrap from first materialization in the deploy model, even if one CLI command can perform both.
+- [x] Define inference rules and override flags for source ref, mesh base, and publication source folder.
+- [x] Add interactive prompting for missing publication worktree path and non-interactive fail-closed behavior when no path/profile is supplied.
+- [x] Define the branch deploy context as source root plus publication root without broadening the general workspace model.
+- [x] Draft the local generation workflow for source checkout plus publication worktree, including dirty-worktree and branch initialization guardrails.
+- [x] Add a dry-run planner for the branch-published workflow that prints source root, publication root, mesh base, generated paths, preserved files, and git operations that would run.
+- [x] Add path-policy tests for cross-worktree source access and host-local grants.
+- [x] Create the first branch-published Fantasy Rules source-only proof ref and Accord manifest (`bp-01-source-only`) in the existing fixture repo/SFF conformance area.
+- [x] Implement local-only branch-published publication-root bootstrap through `weave deploy gh-pages`.
+- [x] Add focused bootstrap tests proving the source root stays free of `_mesh`/`.weave`, publication root carries `_mesh` plus config, public config has no sibling path leakage, and a second bootstrap run is a no-op.
+- [x] Implement local-only branch-published materialization/generation for one simple ontology source from command-scoped source and publication roots.
+- [x] Prove the first clean-source-branch slice in focused tests: source root contains only authored source, publication root carries `_mesh` and generated state, public RDF has no sibling path leakage, reruns are incremental, and default MeshInventory, KnopMetadata, and KnopInventory support histories remain current-only.
+- [x] Add local integration coverage using an actual temporary git repo with source and `gh-pages` worktrees.
+- [x] Update [[wa.completed.2026.2026-05-07-fixture-ladder-generator]] to make fixture-generator work early but full fixture branch rerunging later, after branch-published topology and vocabulary are stable.
+- [x] Add `.nojekyll` and optional `CNAME` preservation behavior.
+- [x] Add validation that generated public mesh output does not include stale source-branch clutter or developer-specific sibling checkout paths.
+- [x] Implement incremental publication-branch updates as the default behavior.
+- [d] Add a guarded rebuild-from-scratch mode only after incremental updates are proven; deferred to [[wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild]].
+- [x] Add explicit local commit support after local generation is proven, and print a CLI reminder that the publication branch still needs to be pushed for GitHub Pages to update.
+- [x] Decide whether to convert the Fantasy Rules fixture from `docs/` sidecar to branch-published output before the next fixture rerung.
+- [x] Rewrite the Semantic Flow Framework Fantasy Rules example/spec for branch-published ontology delivery before rerunging fixture branches.
+- [x] Update [[wa.completed.2026.2026-05-07-fixture-ladder-generator]] if the fixture topology changes.
+- [x] Update [[wd.decision-log]] once the topology and path-provenance decisions are accepted.
diff --git a/documentation/notes/wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild.md b/documentation/notes/wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild.md
new file mode 100644
index 0000000..7b1b7e8
--- /dev/null
+++ b/documentation/notes/wd.task.2026.2026-05-14_1105-guarded-branch-published-rebuild.md
@@ -0,0 +1,78 @@
+---
+id: yzu9g1n4ppf9lpf17xahj3u
+title: 2026 05 14_1105 Guarded Branch Published Rebuild
+desc: ''
+updated: 1778766300000
+created: 1778766300000
+---
+
+## Goals
+
+- Add an explicit rebuild-from-scratch mode for branch-published meshes after incremental publication is the proven default.
+- Keep rebuild behavior loud, guarded, and separate from ordinary `weave deploy gh-pages`.
+- Preserve intentional publication controls such as `.nojekyll`, `CNAME`, and declared manual files when rebuilding.
+- Prevent accidental source-branch writes, branch resets, force updates, or publication file deletion.
+
+## Summary
+
+[[wd.task.2026.2026-05-13_1655-support-gh-pages-branch-based-deployments]] proves the clean-source, incremental branch-published path. Rebuild-from-scratch is still useful for disaster recovery, fixture regeneration, and intentional model churn, but it has a different risk profile. It should not ride along as a casual flag on the first local deploy implementation.
+
+This task is for the later guarded rebuild path. The mode should require an explicit operator decision, validate preserved-file policy before deleting anything, and make the planned deletion/write set inspectable before it mutates a publication worktree.
+
+## Discussion
+
+Normal branch-published deploy treats the publication branch as stateful Semantic Flow output. It reuses the existing mesh shell, preserves unknown non-generated files, updates source bindings, and advances generated state incrementally. Rebuild mode temporarily treats generated output as disposable and therefore needs stronger guardrails.
+
+The dangerous operations are deleting existing publication files, resetting generated mesh state, and potentially recreating semantic histories. Those operations should be separate from local generation, local commit support, and push support.
+
+The dry-run planner should grow enough detail to show:
+
+- paths that would be deleted
+- paths that would be preserved
+- paths that would be recreated
+- publication control files that would be carried forward
+- whether semantic histories would be reset
+- whether the publication worktree is clean enough for a rebuild
+
+## Open Issues
+
+- Should rebuild preserve unknown files by default, or require an explicit preserved-file allowlist?
+- Should rebuild require both `--rebuild-from-scratch` and `--confirm-rebuild`, or is one loud flag plus dry-run enough?
+- Should rebuild be allowed against a dirty publication worktree under any circumstances?
+- Should rebuild delete generated histories, or should history reset be an even louder sub-mode?
+- Should fixture-ladder regeneration use this mode directly, or use fixture-specific checkout replacement instead?
+
+## Decisions
+
+- Rebuild mode is deferred from the first branch-published deploy task.
+- Rebuild mode must be explicit and guarded; it is not the default deploy path.
+- Incremental deploy remains the default for branch-published meshes.
+- Push support is out of scope for rebuild mode until local commit behavior is settled.
+
+## Contract Changes
+
+- Future branch-published deploy may gain a guarded rebuild mode that deletes/recreates generated publication output only after an explicit rebuild request.
+- Rebuild dry-run output should include deletion and preservation plans.
+
+## Testing
+
+- Add dry-run tests showing rebuild deletion/preservation plans without mutating the publication root.
+- Add integration tests proving normal deploy does not delete unknown publication files.
+- Add integration tests proving rebuild refuses dirty publication worktrees by default.
+- Add tests for `.nojekyll`, `CNAME`, and declared preserved-file handling.
+- Add tests proving rebuild does not touch the source checkout.
+
+## Non-Goals
+
+- Adding push support.
+- Replacing incremental deploy as the default branch-published workflow.
+- Solving fixture ladder regeneration directly.
+- Force-pushing or deleting publication branch content without explicit operator intent.
+
+## Implementation Plan
+
+- [ ] Define the rebuild request shape and preserved-file policy.
+- [ ] Extend dry-run output with deletion and preservation plans.
+- [ ] Add guarded rebuild validation for clean publication worktrees.
+- [ ] Implement local rebuild without commit/push.
+- [ ] Add focused integration tests.
diff --git a/documentation/notes/wu.cli-reference.md b/documentation/notes/wu.cli-reference.md
index 42bd33f..9ae75e7 100644
--- a/documentation/notes/wu.cli-reference.md
+++ b/documentation/notes/wu.cli-reference.md
@@ -182,6 +182,60 @@ weave mesh create --mesh-base 'https://semantic-flow.github.io/my-mesh/' --no-no
weave mesh create --interactive
```
+### `weave deploy gh-pages`
+
+Creates or updates a branch-published GitHub Pages mesh in a publication worktree. Use this when authored source files stay in a normal source checkout and generated mesh output lives in a separate publication branch checkout such as `gh-pages`.
+
+The command reads source bytes from `--source-root`, writes generated mesh output to `--publish-root`, and keeps host-local checkout paths out of the published RDF. `--source-root` defaults to the current directory. `--publish-root` is required in noninteractive runs; interactive runs can prompt for it.
+
+Repository source bindings are recorded beside the target Knop rather than in `_mesh/_config/config.ttl`. When a deploy materializes a source file, Weave writes a source registry at `_knop/_sources/sources.ttl`, links it from the Knop inventory with `sflo:hasKnopSourceRegistry`, and records the repository URL, source ref, resolved commit, repository-relative path, and content digest. This keeps publication output portable without preserving a developer's checkout path.
+
+Dry-run prints the planned writes, preserved files, validation checks, and git operations without mutating the publication worktree:
+
+```sh
+weave deploy gh-pages \
+ --dry-run \
+ --source-root . \
+ --publish-root ../my-repo-gh-pages \
+ --mesh-base 'https://example.github.io/my-repo/'
+```
+
+Materialize one repository source file into the publication mesh:
+
+```sh
+weave deploy gh-pages \
+ --source-root . \
+ --publish-root ../my-repo-gh-pages \
+ --mesh-base 'https://example.github.io/my-repo/' \
+ --source-path ontology/fantasy-rules-ontology.ttl \
+ --designator-path ontology \
+ --source-repository-url 'https://github.com/example/my-repo.git' \
+ --source-ref main
+```
+
+Create a local publication commit after a successful deploy:
+
+```sh
+weave deploy gh-pages \
+ --source-root . \
+ --publish-root ../my-repo-gh-pages \
+ --mesh-base 'https://example.github.io/my-repo/' \
+ --commit \
+ --commit-message 'Publish mesh'
+```
+
+Constraints:
+
+- `--publish-root` must be a distinct publication worktree, not the source checkout or a directory inside it
+- publication git worktrees must be clean by default before Weave writes
+- `--allow-dirty-publish-root` is available for local experimentation, but cannot be combined with `--commit`
+- `--commit` creates a local commit only when the publication worktree has changes
+- Weave does not push; after a local commit, push the publication branch yourself for GitHub Pages to update
+- `--commit-message` requires `--commit`
+- `--source-commit` records an exact source commit in the source locator when supplied, but it should name bytes that the commit actually represents
+
+For local preview or regeneration runs, set `WEAVE_LOG_DIR` to a temporary directory such as `/tmp/weave-logs` when you want runtime logs kept outside a publication checkout.
+
### `weave integrate`
Integrates a local source file into a designator path as a payload artifact, including policy-approved extra-mesh local sources.
@@ -217,7 +271,7 @@ weave extract [--mesh-root ] [--source | --source-state ) [--mesh-root ] [--accept-preview]
```
-`` is the resource or term surface to create. `--source ` selects the already woven payload artifact that describes that target and records a current-tracking `sfc:ExtractionSource`. `--source-state ` pins the extraction source to a historical source state and resolves the owning source artifact from mesh inventory.
+`` is the resource or term surface to create. `--source ` selects the already woven payload artifact that describes that target and records a current-tracking `sflo:ExtractionSource` in the Knop's `_sources` registry. `--source-state ` pins the extraction source to a historical source state and resolves the owning source artifact from mesh inventory.
`--source` and `--source-state` are mutually exclusive. If neither is supplied for single-target extraction, Weave resolves the unique current woven payload artifact that mentions the target. `--all-terms` requires an explicit `--source` or `--source-state`, previews the identifiers that will be created, and asks for confirmation before writing; `--accept-preview` accepts that preview for noninteractive runs. Existing Knops, blank nodes, support artifact paths, and generated page/file artifact paths are skipped.
@@ -250,7 +304,7 @@ weave set extraction-source [--mesh-root ] (--s
weave set extraction-source --all-terms [--mesh-root ] (--source | --source-state ) [--accept-preview]
```
-`--source` records a current-tracking source binding. `--source-state` records a pinned source binding and fails if the historical source bytes do not mention the target term. The command replaces the existing `sfc:hasExtractionSource` binding; it does not append a second primary extraction source.
+`--source` records a current-tracking source binding. `--source-state` records a pinned source binding and fails if the historical source bytes do not mention the target term. The command replaces the existing source-registry `sflo:ExtractionSource` details; it does not append a second primary extraction source.
The `--all-terms` form discovers named mesh-scoped terms from the selected source graph, previews the existing extracted terms that will be updated, and updates all listed terms after confirmation or `--accept-preview`.
diff --git a/documentation/notes/wu.repository-options.md b/documentation/notes/wu.repository-options.md
index 980ff72..0b6282c 100644
--- a/documentation/notes/wu.repository-options.md
+++ b/documentation/notes/wu.repository-options.md
@@ -2,7 +2,6 @@
id: 96vumpc760psizhzvrw4y29
title: Repository Options
desc: 'publication topology options for a semantic mesh'
-updated: 1777703721069
created: 1775529630513
---
@@ -25,3 +24,15 @@ This should usually be the default for repos that are not primarily meshes: soft
Sidecar meshes fit the practical rule that mesh paths should be relatively stable. Most project source trees are allowed to move around as the project evolves; the public mesh should not have to churn every time the authoring layout changes. In a sidecar layout, working payload files can remain in project-appropriate source locations while the mesh keeps stable public identifiers, generated resource pages, and historical snapshots under a publishable root such as `docs/`.
This also limits accidental publication. A whole-repo mesh tends to make the whole repo feel like the public page surface, while a sidecar mesh keeps the public mesh boundary explicit.
+
+## Branch-published semantic mesh:
+
+Use this when the authored source branch should stay clean, but the project still wants stable dereferenceable mesh pages from a publication branch such as `gh-pages`.
+
+A branch-published mesh is sidecar-like in purpose: the public mesh is a generated projection of (some of) the source repository rather than the main authoring layout. The difference is operational. Instead of storing generated `_mesh/`, histories, inventories, and pages in a `docs/` directory on the source branch, the generated mesh lives in a separate publication branch.
+
+This is a strong fit for ontology and vocabulary repositories where maintainers want the normal branch to contain only source artifacts such as Turtle, SHACL, Markdown, or examples, while GitHub Pages serves the generated Semantic Flow surface from a dedicated branch.
+
+Branch-published meshes should record durable source provenance, such as repository, ref, source path, and content digest. They should not record one contributor's local sibling checkout path as public RDF. Local paths belong to the deploy operation that reads the source checkout and writes the publication checkout; the published mesh should describe the source material, not the workstation layout.
+
+Choose this option when generated mesh state would be too noisy for the source branch, when review of generated publication output can happen on the publication branch, and when the project can tolerate a slightly more explicit deploy workflow.
diff --git a/scripts/assemble-npm-packages.ts b/scripts/assemble-npm-packages.ts
new file mode 100644
index 0000000..8dc5c24
--- /dev/null
+++ b/scripts/assemble-npm-packages.ts
@@ -0,0 +1,290 @@
+import { fromFileUrl, isAbsolute, join } from "@std/path";
+import {
+ createPlatformPackageJson,
+ createWrapperPackageJson,
+ NPM_COMMAND_NAME,
+ NPM_PACKAGES_METADATA_FILENAME,
+ npmPackagePath,
+ type NpmPackagesMetadata,
+ type NpmPlatformPackageMetadata,
+ renderPlatformReadme,
+ renderWrapperBinScript,
+ renderWrapperReadme,
+} from "./release/npm.ts";
+import {
+ assertBinaryBundleMetadata,
+ createBinaryBundleMetadata,
+ NPM_WRAPPER_PACKAGE_NAME,
+ readBinaryBundleMetadata,
+ readRootVersionFrom,
+ type ReleasePlatform,
+ selectReleasePlatforms,
+} from "./release/metadata.ts";
+
+export interface AssembleNpmPackagesOptions {
+ root: string;
+ buildDir: string;
+ outDir: string;
+ platformLabels: string[];
+}
+
+export interface AssembleNpmPackagesResult {
+ wrapperPackageDir: string;
+ platformPackageDirs: string[];
+ packagesMetadataPath: string;
+}
+
+const defaultRoot = fromFileUrl(new URL("..", import.meta.url));
+const defaultBuildDir = "dist/binaries";
+const defaultOutDir = "dist/npm";
+
+if (import.meta.main) {
+ try {
+ const result = await assembleNpmPackages(
+ parseAssembleNpmPackagesArgs(Deno.args),
+ );
+ console.log(`Assembled wrapper package: ${result.wrapperPackageDir}`);
+ for (const packageDir of result.platformPackageDirs) {
+ console.log(`Assembled platform package: ${packageDir}`);
+ }
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parseAssembleNpmPackagesArgs(
+ args: readonly string[],
+): AssembleNpmPackagesOptions {
+ let root = defaultRoot;
+ let buildDir = defaultBuildDir;
+ let outDir = defaultOutDir;
+ const platformLabels: string[] = [];
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+
+ switch (arg) {
+ case "--":
+ break;
+ case "--root":
+ index += 1;
+ root = requireArgumentValue(args[index], "--root");
+ break;
+ case "--build-dir":
+ index += 1;
+ buildDir = requireArgumentValue(args[index], "--build-dir");
+ break;
+ case "--out-dir":
+ index += 1;
+ outDir = requireArgumentValue(args[index], "--out-dir");
+ break;
+ case "--platform":
+ index += 1;
+ platformLabels.push(requireArgumentValue(args[index], "--platform"));
+ break;
+ default:
+ if (arg.startsWith("--root=")) {
+ root = requireArgumentValue(arg.slice("--root=".length), "--root");
+ break;
+ }
+ if (arg.startsWith("--build-dir=")) {
+ buildDir = requireArgumentValue(
+ arg.slice("--build-dir=".length),
+ "--build-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--out-dir=")) {
+ outDir = requireArgumentValue(
+ arg.slice("--out-dir=".length),
+ "--out-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--platform=")) {
+ platformLabels.push(
+ requireArgumentValue(arg.slice("--platform=".length), "--platform"),
+ );
+ break;
+ }
+ throw new Error(`Unsupported assemble:npm-packages argument: ${arg}`);
+ }
+ }
+
+ return { root, buildDir, outDir, platformLabels };
+}
+
+export async function assembleNpmPackages(
+ options: AssembleNpmPackagesOptions,
+): Promise {
+ const version = await readRootVersionFrom(options.root);
+ const platforms = selectReleasePlatforms(options.platformLabels);
+ const buildDir = resolveRootPath(options.root, options.buildDir);
+ const outDir = resolveRootPath(options.root, options.outDir);
+
+ const wrapperPackageDir = await writeWrapperPackage({
+ outDir,
+ platforms,
+ root: options.root,
+ version,
+ });
+ const platformResults: PlatformPackageAssemblyResult[] = [];
+
+ for (const platform of platforms) {
+ platformResults.push(
+ await writePlatformPackage({
+ buildDir,
+ outDir,
+ platform,
+ root: options.root,
+ version,
+ }),
+ );
+ }
+
+ const packagesMetadataPath = join(outDir, NPM_PACKAGES_METADATA_FILENAME);
+ const packagesMetadata = createNpmPackagesMetadata({
+ platformPackages: platformResults.map((result) => result.publishMetadata),
+ version,
+ wrapperPackageDir,
+ });
+ await writeJsonFile(packagesMetadataPath, packagesMetadata);
+
+ return {
+ wrapperPackageDir,
+ platformPackageDirs: platformResults.map((result) => result.packageDir),
+ packagesMetadataPath,
+ };
+}
+
+async function writeWrapperPackage(options: {
+ outDir: string;
+ platforms: readonly ReleasePlatform[];
+ root: string;
+ version: string;
+}): Promise {
+ const packageDir = npmPackagePath(options.outDir, NPM_WRAPPER_PACKAGE_NAME);
+ await Deno.mkdir(join(packageDir, "bin"), { recursive: true });
+ await writeJsonFile(
+ join(packageDir, "package.json"),
+ createWrapperPackageJson(options.version, options.platforms),
+ );
+ const binPath = join(packageDir, "bin", "weave.js");
+ await Deno.writeTextFile(
+ binPath,
+ renderWrapperBinScript(options.platforms),
+ );
+ await chmodExecutable(binPath);
+ await Deno.writeTextFile(
+ join(packageDir, "README.md"),
+ renderWrapperReadme(options.version),
+ );
+ await copyLicenseIfPresent(options.root, packageDir);
+ return packageDir;
+}
+
+interface PlatformPackageAssemblyResult {
+ packageDir: string;
+ publishMetadata: NpmPlatformPackageMetadata;
+}
+
+function createNpmPackagesMetadata(options: {
+ platformPackages: NpmPlatformPackageMetadata[];
+ version: string;
+ wrapperPackageDir: string;
+}): NpmPackagesMetadata {
+ return {
+ createdAt: new Date().toISOString(),
+ version: options.version,
+ wrapperPackageName: NPM_WRAPPER_PACKAGE_NAME,
+ wrapperPackageDir: options.wrapperPackageDir,
+ wrapperPackageJsonPath: join(options.wrapperPackageDir, "package.json"),
+ commandName: NPM_COMMAND_NAME,
+ platformPackages: options.platformPackages,
+ };
+}
+
+async function writePlatformPackage(options: {
+ buildDir: string;
+ outDir: string;
+ platform: ReleasePlatform;
+ root: string;
+ version: string;
+}): Promise {
+ const platformBuildDir = join(options.buildDir, options.platform.label);
+ const metadataPath = join(platformBuildDir, "bundle-metadata.json");
+ const metadata = await readBinaryBundleMetadata(metadataPath);
+ const expectedMetadata = createBinaryBundleMetadata(
+ options.version,
+ options.platform,
+ );
+ assertBinaryBundleMetadata(metadata, expectedMetadata, metadataPath);
+
+ const packageDir = npmPackagePath(options.outDir, metadata.packageName);
+ await Deno.mkdir(join(packageDir, "bin"), { recursive: true });
+ await writeJsonFile(
+ join(packageDir, "package.json"),
+ createPlatformPackageJson(metadata),
+ );
+ await Deno.copyFile(metadataPath, join(packageDir, "bundle-metadata.json"));
+ await Deno.writeTextFile(
+ join(packageDir, "README.md"),
+ renderPlatformReadme(metadata),
+ );
+ await copyLicenseIfPresent(options.root, packageDir);
+
+ const sourceBinaryPath = join(platformBuildDir, metadata.executableName);
+ const targetBinaryPath = join(packageDir, "bin", metadata.executableName);
+ await Deno.copyFile(sourceBinaryPath, targetBinaryPath);
+ await chmodExecutable(targetBinaryPath);
+
+ return {
+ packageDir,
+ publishMetadata: {
+ packageName: metadata.packageName,
+ platform: metadata.platform,
+ packageDir,
+ packageJsonPath: join(packageDir, "package.json"),
+ os: metadata.os,
+ cpu: metadata.cpu,
+ executableName: metadata.executableName,
+ executablePath: targetBinaryPath,
+ bundleMetadataPath: join(packageDir, "bundle-metadata.json"),
+ },
+ };
+}
+
+async function copyLicenseIfPresent(root: string, packageDir: string) {
+ try {
+ await Deno.copyFile(join(root, "LICENSE"), join(packageDir, "LICENSE"));
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ }
+}
+
+async function writeJsonFile(path: string, value: unknown): Promise {
+ await Deno.writeTextFile(path, `${JSON.stringify(value, null, 2)}\n`);
+}
+
+async function chmodExecutable(path: string): Promise {
+ if (Deno.build.os !== "windows") {
+ await Deno.chmod(path, 0o755);
+ }
+}
+
+function resolveRootPath(root: string, path: string): string {
+ if (isAbsolute(path)) {
+ return path;
+ }
+ return join(root, path);
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
diff --git a/scripts/build-binaries.ts b/scripts/build-binaries.ts
new file mode 100644
index 0000000..8c50310
--- /dev/null
+++ b/scripts/build-binaries.ts
@@ -0,0 +1,150 @@
+import { fromFileUrl, isAbsolute, join } from "@std/path";
+import {
+ createBinaryBundleMetadata,
+ readRootVersion,
+ type ReleasePlatform,
+ selectReleasePlatforms,
+} from "./release/metadata.ts";
+
+export interface BuildBinariesOptions {
+ outDir: string;
+ platformLabels: string[];
+}
+
+const DEFAULT_OUT_DIR = "dist/binaries";
+
+if (import.meta.main) {
+ try {
+ await buildBinaries(parseBuildBinariesArgs(Deno.args));
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parseBuildBinariesArgs(
+ args: readonly string[],
+): BuildBinariesOptions {
+ let outDir = DEFAULT_OUT_DIR;
+ const platformLabels: string[] = [];
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+
+ switch (arg) {
+ case "--":
+ break;
+ case "--out-dir":
+ index += 1;
+ outDir = requireArgumentValue(args[index], "--out-dir");
+ break;
+ case "--platform":
+ index += 1;
+ platformLabels.push(requireArgumentValue(args[index], "--platform"));
+ break;
+ default:
+ if (arg.startsWith("--out-dir=")) {
+ outDir = requireArgumentValue(
+ arg.slice("--out-dir=".length),
+ "--out-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--platform=")) {
+ platformLabels.push(
+ requireArgumentValue(arg.slice("--platform=".length), "--platform"),
+ );
+ break;
+ }
+ throw new Error(`Unsupported build:binaries argument: ${arg}`);
+ }
+ }
+
+ return { outDir, platformLabels };
+}
+
+export async function buildBinaries(
+ options: BuildBinariesOptions,
+): Promise {
+ const version = readRootVersion();
+ const platforms = selectReleasePlatforms(options.platformLabels);
+ const repoRoot = fromFileUrl(new URL("..", import.meta.url));
+ const entrypoint = join(repoRoot, "src", "main.ts");
+ const outDir = resolveRepoPath(repoRoot, options.outDir);
+
+ for (const platform of platforms) {
+ await buildPlatformBinary({
+ entrypoint,
+ outDir,
+ platform,
+ repoRoot,
+ version,
+ });
+ }
+}
+
+async function buildPlatformBinary(options: {
+ entrypoint: string;
+ outDir: string;
+ platform: ReleasePlatform;
+ repoRoot: string;
+ version: string;
+}): Promise {
+ const platformOutDir = join(options.outDir, options.platform.label);
+ await Deno.mkdir(platformOutDir, { recursive: true });
+
+ const executablePath = join(
+ platformOutDir,
+ options.platform.executableName,
+ );
+ const command = new Deno.Command("deno", {
+ args: [
+ "compile",
+ "--allow-read",
+ "--allow-write",
+ "--allow-env",
+ "--allow-run=git,deno",
+ "--target",
+ options.platform.denoTarget,
+ "--output",
+ executablePath,
+ options.entrypoint,
+ ],
+ cwd: options.repoRoot,
+ stdout: "inherit",
+ stderr: "inherit",
+ });
+
+ console.log(
+ `Building ${options.platform.label} binary with ${options.platform.denoTarget}`,
+ );
+ const status = await command.spawn().status;
+ if (!status.success) {
+ throw new Error(
+ `deno compile failed for ${options.platform.label} with exit code ${status.code}`,
+ );
+ }
+
+ const metadata = createBinaryBundleMetadata(
+ options.version,
+ options.platform,
+ );
+ await Deno.writeTextFile(
+ join(platformOutDir, "bundle-metadata.json"),
+ `${JSON.stringify(metadata, null, 2)}\n`,
+ );
+}
+
+function resolveRepoPath(repoRoot: string, path: string): string {
+ if (isAbsolute(path)) {
+ return path;
+ }
+ return join(repoRoot, path);
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts
new file mode 100644
index 0000000..c521723
--- /dev/null
+++ b/scripts/bump-version.ts
@@ -0,0 +1,270 @@
+import { join } from "@std/path";
+
+export type VersionIncrement = "major" | "minor" | "patch";
+
+export interface BumpVersionOptions {
+ root: string;
+ increment?: VersionIncrement;
+ version?: string;
+ releaseNoteId?: string;
+ timestamp?: number;
+}
+
+export interface BumpVersionResult {
+ previousVersion: string;
+ nextVersion: string;
+ denoConfigPath: string;
+ releaseNotesPath: string;
+ releaseNotesCreated: boolean;
+}
+
+interface DenoConfigWithVersion {
+ version?: unknown;
+ [key: string]: unknown;
+}
+
+if (import.meta.main) {
+ try {
+ const result = await bumpVersion(parseBumpVersionArgs(Deno.args));
+ console.log(
+ `Updated version ${result.previousVersion} -> ${result.nextVersion}`,
+ );
+ console.log(
+ result.releaseNotesCreated
+ ? `Created ${result.releaseNotesPath}`
+ : `Verified ${result.releaseNotesPath}`,
+ );
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parseBumpVersionArgs(
+ args: readonly string[],
+): BumpVersionOptions {
+ let root = Deno.cwd();
+ let increment: VersionIncrement | undefined;
+ let version: string | undefined;
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+
+ switch (arg) {
+ case "--":
+ break;
+ case "--root":
+ index += 1;
+ root = requireArgumentValue(args[index], "--root");
+ break;
+ case "--major":
+ case "--minor":
+ case "--patch":
+ increment = setSingleIncrement(increment, arg.slice(2));
+ break;
+ case "--version":
+ index += 1;
+ version = requireArgumentValue(args[index], "--version");
+ break;
+ default:
+ if (arg.startsWith("--root=")) {
+ root = requireArgumentValue(arg.slice("--root=".length), "--root");
+ break;
+ }
+ if (arg.startsWith("--version=")) {
+ version = requireArgumentValue(
+ arg.slice("--version=".length),
+ "--version",
+ );
+ break;
+ }
+ throw new Error(`Unsupported bump:version argument: ${arg}`);
+ }
+ }
+
+ if ((increment === undefined) === (version === undefined)) {
+ throw new Error(
+ "bump:version requires exactly one of --major, --minor, --patch, or --version ",
+ );
+ }
+
+ return { root, increment, version };
+}
+
+export async function bumpVersion(
+ options: BumpVersionOptions,
+): Promise {
+ if ((options.increment === undefined) === (options.version === undefined)) {
+ throw new Error(
+ "Either version or increment must be provided, but not both",
+ );
+ }
+
+ const denoConfigPath = join(options.root, "deno.json");
+ const denoConfig = JSON.parse(
+ await Deno.readTextFile(denoConfigPath),
+ ) as DenoConfigWithVersion;
+ const previousVersion = requireVersionString(denoConfig.version);
+ const nextVersion = options.version ??
+ incrementVersion(previousVersion, options.increment!);
+
+ if (!isSupportedVersion(nextVersion)) {
+ throw new Error(`Unsupported version: ${nextVersion}`);
+ }
+
+ if (denoConfig.version !== nextVersion) {
+ denoConfig.version = nextVersion;
+ await Deno.writeTextFile(
+ denoConfigPath,
+ `${JSON.stringify(denoConfig, null, 2)}\n`,
+ );
+ }
+
+ const releaseNotesResult = await ensureReleaseNotes({
+ root: options.root,
+ version: nextVersion,
+ id: options.releaseNoteId ?? crypto.randomUUID().replaceAll("-", ""),
+ timestamp: options.timestamp ?? Date.now(),
+ });
+
+ return {
+ previousVersion,
+ nextVersion,
+ denoConfigPath,
+ releaseNotesPath: releaseNotesResult.path,
+ releaseNotesCreated: releaseNotesResult.created,
+ };
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
+
+function setSingleIncrement(
+ current: VersionIncrement | undefined,
+ next: string,
+): VersionIncrement {
+ if (current !== undefined) {
+ throw new Error("bump:version accepts only one increment flag");
+ }
+ if (next !== "major" && next !== "minor" && next !== "patch") {
+ throw new Error(`Unsupported version increment: ${next}`);
+ }
+ return next;
+}
+
+function requireVersionString(value: unknown): string {
+ if (typeof value !== "string" || !isSupportedVersion(value)) {
+ throw new Error(
+ "root deno.json must declare a semver-compatible string version",
+ );
+ }
+ return value;
+}
+
+function incrementVersion(
+ currentVersion: string,
+ increment: VersionIncrement,
+): string {
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(currentVersion);
+ if (!match) {
+ throw new Error(`Cannot increment unsupported version: ${currentVersion}`);
+ }
+
+ const major = Number(match[1]);
+ const minor = Number(match[2]);
+ const patch = Number(match[3]);
+
+ switch (increment) {
+ case "major":
+ return `${major + 1}.0.0`;
+ case "minor":
+ return `${major}.${minor + 1}.0`;
+ case "patch":
+ return `${major}.${minor}.${patch + 1}`;
+ }
+}
+
+function isSupportedVersion(value: string): boolean {
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
+ value,
+ );
+}
+
+async function ensureReleaseNotes(options: {
+ root: string;
+ version: string;
+ id: string;
+ timestamp: number;
+}): Promise<{ path: string; created: boolean }> {
+ const notesDir = join(options.root, "documentation", "notes");
+ const path = join(notesDir, `release-notes.v${options.version}.md`);
+
+ try {
+ const existing = await Deno.readTextFile(path);
+ if (stripDendronFrontmatter(existing).trim().length === 0) {
+ throw new Error(
+ `Release notes body is empty after Dendron frontmatter: ${path}`,
+ );
+ }
+ return { path, created: false };
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ }
+
+ await Deno.mkdir(notesDir, { recursive: true });
+ await Deno.writeTextFile(path, renderReleaseNotesStub(options));
+ return { path, created: true };
+}
+
+function stripDendronFrontmatter(contents: string): string {
+ return contents.replace(/^---\n[\s\S]*?\n---\n?/, "");
+}
+
+function renderReleaseNotesStub(options: {
+ version: string;
+ id: string;
+ timestamp: number;
+}): string {
+ return `---
+id: ${options.id}
+title: 'release notes v${options.version}'
+desc: ''
+updated: ${options.timestamp}
+created: ${options.timestamp}
+---
+
+## Summary
+
+TODO: summarize v${options.version}.
+
+## Highlights
+
+- TODO
+
+## Breaking Or Changed Behavior
+
+- TODO
+
+## Artifacts
+
+- TODO
+
+## Validation
+
+- TODO
+
+## Known Limitations
+
+- TODO
+
+## Next
+
+- TODO
+`;
+}
diff --git a/scripts/fixture-ladder.ts b/scripts/fixture-ladder.ts
new file mode 100644
index 0000000..b60b808
--- /dev/null
+++ b/scripts/fixture-ladder.ts
@@ -0,0 +1,3849 @@
+import { dirname, isAbsolute, join, relative, resolve } from "@std/path";
+import * as pathPosix from "@std/path/posix";
+import {
+ compareBytes,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/checker/compare_bytes.ts";
+import {
+ compareRdfContent,
+ RdfCompareError,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/checker/compare_rdf.ts";
+import {
+ compareTextContents,
+ TextDecodeError,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/checker/compare_text.ts";
+import {
+ evaluatePresenceExpectation,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/checker/file_expectations.ts";
+import type {
+ FileChangeType,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/checker/file_expectations.ts";
+import {
+ runAskAssertion,
+ SparqlAskError,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/checker/sparql.ts";
+import {
+ readManifestSource,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/manifest/load_jsonld.ts";
+import type {
+ CommandInvocation,
+ FileExpectation,
+ InputMaterialization,
+ RdfExpectation,
+ ReplayProfile,
+ SourceProvenance,
+ SparqlAskAssertion,
+ TransitionCase,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/manifest/model.ts";
+import {
+ selectTransitionCase,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/manifest/select_case.ts";
+import {
+ CHECK_CODES,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/report/codes.ts";
+import {
+ countCheckStatuses,
+ deriveReportStatus,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/report/json_report.ts";
+import type {
+ CheckRecord,
+ JsonReport,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/report/json_report.ts";
+import {
+ renderTextReport,
+} from "../dependencies/github.com/spectacular-voyage/accord/src/report/text_report.ts";
+
+export type FixtureScenarioId =
+ | "alice-bio"
+ | "sidecar-fantasy-rules"
+ | "branch-fantasy-rules";
+export type FixturePlanFormat = "text" | "json";
+
+export interface FixtureLadderOptions {
+ root: string;
+ scenario: FixtureScenarioId;
+ format: FixturePlanFormat;
+ materializeTransitionId?: string;
+ executeTransitionId?: string;
+ dryRun?: boolean;
+ workspaceRoot?: string;
+}
+
+export interface FixtureLadderPlan {
+ scenario: FixtureLadderScenario;
+ root: string;
+ fixtureRepoPath: string;
+ manifestRoot: string;
+ assetRoot: string;
+ transitions: readonly FixtureTransitionPlan[];
+ writesBranches: false;
+}
+
+export interface MaterializeFixtureTransitionOptions {
+ root: string;
+ scenario: FixtureScenarioId;
+ transitionId: string;
+ workspaceRoot?: string;
+}
+
+export interface ExecuteFixtureTransitionOptions {
+ root: string;
+ scenario: FixtureScenarioId;
+ transitionId: string;
+ workspaceRoot?: string;
+ dryRun?: boolean;
+}
+
+export interface FixtureMaterializationResult {
+ scenario: FixtureScenarioId;
+ transitionId: string;
+ fromRef: string;
+ toRef: string;
+ operationId: string;
+ fixtureRepoPath: string;
+ manifestPath: string;
+ assetRoot: string;
+ workspaceRoot: string;
+ materializedPaths: readonly string[];
+ writesBranches: false;
+ nextAction: FixtureTransitionAction;
+}
+
+export interface FixtureCommandExecutionResult {
+ kind: "command";
+ command: readonly string[];
+ commands?: readonly FixtureCommandInvocationExecutionResult[];
+ cwd: string;
+ success: boolean;
+ code: number;
+ stdout: string;
+ stderr: string;
+}
+
+export interface FixtureCommandInvocationExecutionResult {
+ command: readonly string[];
+ cwd: string;
+ success: boolean;
+ code: number;
+ stdout: string;
+ stderr: string;
+}
+
+export interface FixtureFileOperationAppliedFile {
+ path: string;
+ assetPath: string;
+ provenance: string;
+ bytes: number;
+}
+
+export interface FixtureFileOperationMissingAsset {
+ path: string;
+ assetPath: string;
+ absolutePath: string;
+}
+
+export interface FixtureFileOperationExecutionResult {
+ kind: "fileOperation";
+ description: string;
+ success: boolean;
+ files: readonly FixtureFileOperationAppliedFile[];
+ missingAssets: readonly FixtureFileOperationMissingAsset[];
+}
+
+export interface FixtureBranchPublicationExecutionResult {
+ kind: "branchPublication";
+ description: string;
+ success: boolean;
+ code: number;
+ sourceWorkspaceRoot: string;
+ publicationWorkspaceRoot: string;
+ publicationBranch: string;
+ commands: readonly FixtureCommandInvocationExecutionResult[];
+ stdout: string;
+ stderr: string;
+}
+
+export type FixtureTransitionOperationResult =
+ | FixtureCommandExecutionResult
+ | FixtureFileOperationExecutionResult
+ | FixtureBranchPublicationExecutionResult;
+
+interface FixtureExecutionBase {
+ scenario: FixtureScenarioId;
+ transitionId: string;
+ fromRef: string;
+ toRef: string;
+ operationId: string;
+ fixtureRepoPath: string;
+ manifestPath: string;
+ assetRoot: string;
+ workspaceRoot: string;
+ materializedPaths: readonly string[];
+ operation: FixtureTransitionOperationResult;
+ validation: JsonReport;
+ writesBranches: boolean;
+ branchUpdate: FixtureBranchUpdateResult;
+}
+
+export type FixtureExecutionResult =
+ | FixtureCommandTransitionExecutionResult
+ | FixtureFileOperationTransitionExecutionResult
+ | FixtureBranchPublicationTransitionExecutionResult;
+
+export interface FixtureCommandTransitionExecutionResult
+ extends FixtureExecutionBase {
+ actionKind: "command";
+ operation: FixtureCommandExecutionResult;
+ command: FixtureCommandExecutionResult;
+ fileOperation?: undefined;
+}
+
+export interface FixtureFileOperationTransitionExecutionResult
+ extends FixtureExecutionBase {
+ actionKind: "fileOperation";
+ operation: FixtureFileOperationExecutionResult;
+ command?: undefined;
+ fileOperation: FixtureFileOperationExecutionResult;
+}
+
+export interface FixtureBranchPublicationTransitionExecutionResult
+ extends FixtureExecutionBase {
+ actionKind: "branchPublication";
+ operation: FixtureBranchPublicationExecutionResult;
+ command?: undefined;
+ fileOperation?: undefined;
+ branchPublication: FixtureBranchPublicationExecutionResult;
+ publicationBranchUpdate: FixturePublicationBranchUpdateResult;
+}
+
+export type FixturePublicationBranchUpdateResult =
+ | {
+ updated: false;
+ branch: string;
+ reason: string;
+ }
+ | {
+ updated: true;
+ branch: string;
+ branchRef: string;
+ commitSha: string;
+ };
+
+export interface UpdateFixtureBranchOptions {
+ fixtureRepoPath: string;
+ workspaceRoot: string;
+ targetRef: string;
+ parentRef?: string;
+ message: string;
+}
+
+export type FixtureBranchUpdateResult =
+ | {
+ dryRun: true;
+ updated: false;
+ targetRef: string;
+ branchRef: string;
+ localOnly: true;
+ reason: string;
+ }
+ | {
+ dryRun: false;
+ updated: false;
+ targetRef: string;
+ branchRef: string;
+ localOnly: true;
+ reason: string;
+ }
+ | {
+ dryRun: false;
+ updated: true;
+ targetRef: string;
+ branchRef: string;
+ localOnly: true;
+ commitSha: string;
+ treeSha: string;
+ parentRef?: string;
+ parentSha?: string;
+ pushed: false;
+ };
+
+export interface FixtureLadderScenario {
+ id: FixtureScenarioId;
+ label: string;
+ fixtureRepo: string;
+ fixtureRepoRelativePath: string;
+ manifestRootRelativePath: string;
+ assetRootRelativePath?: string;
+ branchPrefix: string;
+ transitions: readonly FixtureTransitionDefinition[];
+}
+
+export interface FixtureTransitionDefinition {
+ index: number;
+ id: string;
+ fromRef: string;
+ toRef: string;
+ manifestName: string;
+ operationId: string;
+ action: FixtureTransitionAction;
+ validation: FixtureTransitionValidation;
+}
+
+export interface FixtureTransitionPlan extends FixtureTransitionDefinition {
+ manifestPath: string;
+}
+
+export type FixtureTransitionAction =
+ | FixtureCommandAction
+ | FixtureFileOperationAction
+ | FixtureBranchPublicationAction;
+
+export interface FixtureCommandAction {
+ kind: "command";
+ executable: "weave";
+ argv: readonly string[];
+ inputs: readonly FixtureFileOperationSource[];
+ cwd: "workspace";
+ promptPolicy: "nonInteractive";
+ expectedRuntimeLogs: boolean;
+ invocations?: readonly FixtureCommandInvocationAction[];
+}
+
+export interface FixtureCommandInvocationAction {
+ executable: "weave";
+ argv: readonly string[];
+ inputs: readonly FixtureFileOperationSource[];
+ cwd: "workspace";
+ promptPolicy: "nonInteractive";
+ expectedRuntimeLogs: boolean;
+}
+
+export interface FixtureFileOperationAction {
+ kind: "fileOperation";
+ description: string;
+ sources: readonly FixtureFileOperationSource[];
+ inventoryPatches: readonly FixtureInventoryPatch[];
+}
+
+export interface FixtureBranchPublicationAction {
+ kind: "branchPublication";
+ description: string;
+ sourceRef: string;
+ publicationFromRef?: string;
+ publicationBranch: string;
+ invocations: readonly FixtureBranchPublicationCommandInvocation[];
+}
+
+export interface FixtureBranchPublicationCommandInvocation {
+ executable: "weave";
+ argv: readonly string[];
+ cwd: "workspace";
+ promptPolicy: "nonInteractive";
+ expectedRuntimeLogs: boolean;
+}
+
+export interface FixtureFileOperationSource {
+ path: string;
+ assetPath: string;
+ provenance: string;
+}
+
+interface FixtureFileOperationSourceInput {
+ path: string;
+ assetPath?: string;
+ provenance: string;
+}
+
+export type FixtureInventoryPatch = FixtureResourcePageDefinitionInventoryPatch;
+
+export interface FixtureResourcePageDefinitionInventoryPatch {
+ kind: "resourcePageDefinition";
+ inventoryPath: string;
+ knopPath: string;
+ pageDefinitionPath: string;
+ pageDefinitionFilePath: string;
+ assetBundlePath?: string;
+ provenance: string;
+}
+
+interface FixtureResourcePageDefinitionInventoryPatchInput {
+ kind: "resourcePageDefinition";
+ designatorPath: string;
+ hasAssetBundle?: boolean;
+ provenance: string;
+}
+
+export interface FixtureTransitionValidation {
+ accordManifest: true;
+ comparison: "manifestScoped";
+ guardrails: readonly string[];
+}
+
+const CANONICAL_OUTPUT_GUARDRAILS = [
+ "generated RDF uses the canonical sflo namespace",
+ "generated MeshInventory progression lives on _mesh/_meta",
+] as const;
+const FIXTURE_ASSET_ROOT_BASENAME = ".assets";
+const LADDER_BRANCH_PREFIX = "a.";
+const ALICE_BIO_LADDER_BRANCH_PREFIX = LADDER_BRANCH_PREFIX;
+const SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX = LADDER_BRANCH_PREFIX;
+const BRANCH_FANTASY_RULES_LADDER_BRANCH_PREFIX = LADDER_BRANCH_PREFIX;
+
+const ALICE_BIO_FIXTURE_REPO = "github.com/semantic-flow/mesh-alice-bio";
+const ALICE_BIO_FIXTURE_REPO_RELATIVE_PATH = join(
+ "dependencies",
+ "github.com",
+ "semantic-flow",
+ "mesh-alice-bio",
+);
+const ALICE_BIO_MANIFEST_ROOT_RELATIVE_PATH = join(
+ "dependencies",
+ "github.com",
+ "semantic-flow",
+ "semantic-flow-framework",
+ "examples",
+ "alice-bio",
+ "conformance",
+);
+const SIDECAR_FANTASY_RULES_FIXTURE_REPO =
+ "github.com/semantic-flow/mesh-sidecar-fantasy-rules";
+const SIDECAR_FANTASY_RULES_FIXTURE_REPO_RELATIVE_PATH = join(
+ "dependencies",
+ "github.com",
+ "semantic-flow",
+ "mesh-sidecar-fantasy-rules",
+);
+const SIDECAR_FANTASY_RULES_MANIFEST_ROOT_RELATIVE_PATH = join(
+ "dependencies",
+ "github.com",
+ "semantic-flow",
+ "semantic-flow-framework",
+ "examples",
+ "sidecar-fantasy-rules",
+ "conformance",
+);
+const BRANCH_FANTASY_RULES_FIXTURE_REPO =
+ "github.com/semantic-flow/mesh-branch-fantasy-rules";
+const BRANCH_FANTASY_RULES_FIXTURE_REPO_RELATIVE_PATH = join(
+ "dependencies",
+ "github.com",
+ "semantic-flow",
+ "mesh-branch-fantasy-rules",
+);
+const BRANCH_FANTASY_RULES_MANIFEST_ROOT_RELATIVE_PATH = join(
+ "dependencies",
+ "github.com",
+ "semantic-flow",
+ "semantic-flow-framework",
+ "examples",
+ "branch-fantasy-rules",
+ "conformance",
+);
+const FIXTURE_GENERATED_AT = "2026-05-03T00:00:00.000Z";
+const CANONICAL_SFLO_NAMESPACE =
+ "https://semantic-flow.github.io/sflo/ontology/";
+const OLD_SFLO_NAMESPACE =
+ "https://semantic-flow.github.io/semantic-flow-ontology/";
+const MESH_INVENTORY_HISTORY_PREFIX = "_mesh/_inventory/_history";
+const MESH_INVENTORY_FILE_PATH = "_mesh/_inventory/inventory.ttl";
+const MESH_METADATA_FILE_PATH = "_mesh/_meta/meta.ttl";
+const RDF_OUTPUT_EXTENSIONS = [
+ ".ttl",
+ ".jsonld",
+ ".nt",
+ ".nq",
+ ".trig",
+] as const;
+
+export const ALICE_BIO_FIXTURE_SCENARIO: FixtureLadderScenario = {
+ id: "alice-bio",
+ label: "Alice Bio",
+ fixtureRepo: ALICE_BIO_FIXTURE_REPO,
+ fixtureRepoRelativePath: ALICE_BIO_FIXTURE_REPO_RELATIVE_PATH,
+ manifestRootRelativePath: ALICE_BIO_MANIFEST_ROOT_RELATIVE_PATH,
+ branchPrefix: ALICE_BIO_LADDER_BRANCH_PREFIX,
+ transitions: [
+ fileTransition(1, "01-source-only", "00-blank-slate", {
+ description: "Seed the source-only Alice Bio fixture branch.",
+ sources: [
+ {
+ path: "alice-bio.ttl",
+ provenance:
+ "fixture-authored source RDF carried from the existing Alice Bio source-only fixture",
+ },
+ ],
+ }),
+ commandTransition(2, "02-mesh-created", "01-source-only", "mesh.create"),
+ commandTransition(
+ 3,
+ "03-mesh-created-woven",
+ "02-mesh-created",
+ "weave",
+ ),
+ commandTransition(
+ 4,
+ "04-alice-knop-created",
+ "03-mesh-created-woven",
+ "knop.create",
+ ),
+ commandTransition(
+ 5,
+ "05-alice-knop-created-woven",
+ "04-alice-knop-created",
+ "weave",
+ ),
+ commandTransition(
+ 6,
+ "06-alice-bio-integrated",
+ "05-alice-knop-created-woven",
+ "integrate",
+ ),
+ commandTransition(
+ 7,
+ "07-alice-bio-integrated-woven",
+ "06-alice-bio-integrated",
+ "weave",
+ ),
+ commandTransition(
+ 8,
+ "08-alice-bio-referenced",
+ "07-alice-bio-integrated-woven",
+ "knop.addReference",
+ ),
+ commandTransition(
+ 9,
+ "09-alice-bio-referenced-woven",
+ "08-alice-bio-referenced",
+ "weave",
+ ),
+ commandTransition(
+ 10,
+ "10-alice-bio-updated",
+ "09-alice-bio-referenced-woven",
+ "payload.update",
+ ),
+ commandTransition(
+ 11,
+ "11-alice-bio-v2-woven",
+ "10-alice-bio-updated",
+ "weave",
+ ),
+ commandTransition(
+ 12,
+ "12-bob-extracted",
+ "11-alice-bio-v2-woven",
+ "extract",
+ ),
+ commandTransition(
+ 13,
+ "13-bob-extracted-woven",
+ "12-bob-extracted",
+ "weave",
+ ),
+ fileTransition(14, "14-alice-page-customized", "13-bob-extracted-woven", {
+ description:
+ "Apply the hand-authored Alice page definition and local page assets.",
+ sources: [
+ {
+ path: "alice/_knop/_page/page.ttl",
+ provenance:
+ "fixture-authored canonical page definition adapted from the Alice Bio main branch page bytes",
+ },
+ {
+ path: "alice/alice.md",
+ provenance:
+ "fixture-authored Markdown copied from the Alice Bio main branch source bytes",
+ },
+ {
+ path: "mesh-content/sidebar.md",
+ provenance:
+ "fixture-authored sidebar Markdown copied from the Alice Bio main branch source bytes",
+ },
+ {
+ path: "alice/_knop/_assets/alice.css",
+ provenance:
+ "fixture-authored stylesheet copied from the Alice Bio main branch source bytes",
+ },
+ ],
+ inventoryPatches: [
+ {
+ kind: "resourcePageDefinition",
+ designatorPath: "alice",
+ hasAssetBundle: true,
+ provenance:
+ "register Alice's ResourcePageDefinition and KnopAssetBundle against the current generated Alice KnopInventory",
+ },
+ ],
+ }, "resourcePage.define"),
+ commandTransition(
+ 15,
+ "15-alice-page-customized-woven",
+ "14-alice-page-customized",
+ "weave",
+ ),
+ commandTransition(
+ 16,
+ "16-alice-page-main-integrated",
+ "15-alice-page-customized-woven",
+ "integrate",
+ ),
+ commandTransition(
+ 17,
+ "17-alice-page-main-integrated-woven",
+ "16-alice-page-main-integrated",
+ "weave",
+ ),
+ fileTransition(
+ 18,
+ "18-alice-page-artifact-source",
+ "17-alice-page-main-integrated-woven",
+ {
+ description:
+ "Repoint Alice's page definition to the governed page-main artifact.",
+ sources: [
+ {
+ path: "alice/_knop/_page/page.ttl",
+ provenance:
+ "fixture-authored canonical page definition adapted from the Alice Bio main branch artifact-backed page bytes",
+ },
+ ],
+ },
+ "resourcePage.define",
+ ),
+ commandTransition(
+ 19,
+ "19-alice-page-artifact-source-woven",
+ "18-alice-page-artifact-source",
+ "weave",
+ ),
+ fileTransition(
+ 20,
+ "20-bob-page-imported-source",
+ "19-alice-page-artifact-source-woven",
+ {
+ description:
+ "Import Bob page Markdown from the pinned outside-origin source fixture.",
+ sources: [
+ {
+ path: "bob-page-main.md",
+ provenance:
+ "checked-in bytes copied from the Alice Bio main branch's imported Markdown source; original outside-origin URL was https://raw.githubusercontent.com/djradon/public-notes/refs/heads/main/user.bob-newhart.md",
+ },
+ {
+ path: "bob/_knop/_page/page.ttl",
+ provenance:
+ "fixture-authored canonical page definition adapted from the Alice Bio main branch Bob page bytes",
+ },
+ ],
+ inventoryPatches: [
+ {
+ kind: "resourcePageDefinition",
+ designatorPath: "bob",
+ provenance:
+ "register Bob's ResourcePageDefinition against the current generated Bob KnopInventory",
+ },
+ ],
+ },
+ "import",
+ ),
+ commandTransition(
+ 21,
+ "21-bob-page-imported-source-woven",
+ "20-bob-page-imported-source",
+ "weave",
+ ),
+ commandTransition(
+ 22,
+ "22-root-knop-created",
+ "21-bob-page-imported-source-woven",
+ "knop.create",
+ ),
+ commandTransition(
+ 23,
+ "23-root-knop-created-woven",
+ "22-root-knop-created",
+ "weave",
+ ),
+ fileTransition(
+ 24,
+ "24-root-page-customized",
+ "23-root-knop-created-woven",
+ {
+ description:
+ "Apply the hand-authored root page definition and local page assets.",
+ sources: [
+ {
+ path: "_knop/_page/page.ttl",
+ provenance:
+ "fixture-authored canonical page definition adapted from the Alice Bio main branch root page bytes",
+ },
+ {
+ path: "home.md",
+ provenance:
+ "fixture-authored root Markdown copied from the Alice Bio main branch source bytes",
+ },
+ {
+ path: "mesh-content/root-sidebar.md",
+ provenance:
+ "fixture-authored root sidebar Markdown copied from the Alice Bio main branch source bytes",
+ },
+ {
+ path: "_knop/_assets/site.css",
+ provenance:
+ "fixture-authored root stylesheet copied from the Alice Bio main branch source bytes",
+ },
+ ],
+ inventoryPatches: [
+ {
+ kind: "resourcePageDefinition",
+ designatorPath: "",
+ hasAssetBundle: true,
+ provenance:
+ "register the root ResourcePageDefinition and KnopAssetBundle against the current generated root KnopInventory",
+ },
+ ],
+ },
+ "resourcePage.define",
+ ),
+ commandTransition(
+ 25,
+ "25-root-page-customized-woven",
+ "24-root-page-customized",
+ "weave",
+ ),
+ ],
+};
+
+export const SIDECAR_FANTASY_RULES_FIXTURE_SCENARIO: FixtureLadderScenario = {
+ id: "sidecar-fantasy-rules",
+ label: "Sidecar Fantasy Rules",
+ fixtureRepo: SIDECAR_FANTASY_RULES_FIXTURE_REPO,
+ fixtureRepoRelativePath: SIDECAR_FANTASY_RULES_FIXTURE_REPO_RELATIVE_PATH,
+ manifestRootRelativePath: SIDECAR_FANTASY_RULES_MANIFEST_ROOT_RELATIVE_PATH,
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ transitions: [
+ fileTransition(
+ 1,
+ "01-source-only",
+ "00-blank-slate",
+ {
+ description:
+ "Seed the authored source files for the docs-rooted Sidecar Fantasy Rules fixture branch.",
+ sources: [
+ {
+ path: "NOTICE.md",
+ provenance:
+ "fixture-authored NOTICE text carried from the existing Sidecar Fantasy Rules source-only branch",
+ },
+ {
+ path: "ontology/fantasy-rules-ontology.ttl",
+ provenance:
+ "fixture-authored ontology RDF carried from the existing Sidecar Fantasy Rules source-only branch",
+ },
+ {
+ path: "shacl/fantasy-rules-shacl.ttl",
+ provenance:
+ "fixture-authored SHACL RDF carried from the existing Sidecar Fantasy Rules source-only branch",
+ },
+ {
+ path: "examples/gunaar.ttl",
+ provenance:
+ "fixture-authored example RDF carried from the existing Sidecar Fantasy Rules source-only branch",
+ },
+ ],
+ },
+ "fixture.seedSourceOnly",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 2,
+ "02-sidecar-mesh-created",
+ "01-source-only",
+ "mesh.create",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 3,
+ "03-sidecar-mesh-created-woven",
+ "02-sidecar-mesh-created",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 4,
+ "04-ontology-integrated",
+ "03-sidecar-mesh-created-woven",
+ "integrate",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 5,
+ "05-ontology-integrated-woven",
+ "04-ontology-integrated",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 6,
+ "06-shacl-integrated",
+ "05-ontology-integrated-woven",
+ "integrate",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 7,
+ "07-shacl-integrated-woven",
+ "06-shacl-integrated",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 8,
+ "08-ontology-and-shacl-terms-extracted",
+ "07-shacl-integrated-woven",
+ "extract",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 9,
+ "09-ontology-and-shacl-terms-extracted-woven",
+ "08-ontology-and-shacl-terms-extracted",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 10,
+ "10-root-knop",
+ "09-ontology-and-shacl-terms-extracted-woven",
+ "knop.create",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(11, "11-root-knop-woven", "10-root-knop", "weave", {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ }),
+ commandTransition(
+ 12,
+ "12-gunaar-example-dataset",
+ "11-root-knop-woven",
+ "integrate",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 13,
+ "13-gunaar-example-dataset-woven",
+ "12-gunaar-example-dataset",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ fileTransition(
+ 14,
+ "14-first-release",
+ "13-gunaar-example-dataset-woven",
+ {
+ description:
+ "Replace authored first-release source bytes from deterministic assets.",
+ sources: [
+ {
+ path: "ontology/fantasy-rules-ontology.ttl",
+ provenance:
+ "fixture-authored ontology release source copied from the Sidecar Fantasy Rules main branch",
+ },
+ {
+ path: "shacl/fantasy-rules-shacl.ttl",
+ provenance:
+ "fixture-authored SHACL release source copied from the Sidecar Fantasy Rules main branch",
+ },
+ ],
+ },
+ "source.update",
+ { branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX },
+ ),
+ commandTransition(
+ 15,
+ "15-first-release-woven",
+ "14-first-release",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 16,
+ "16-all-remaining-terms-extracted",
+ "15-first-release-woven",
+ "extract",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ commandTransition(
+ 17,
+ "17-all-remaining-terms-woven",
+ "16-all-remaining-terms-extracted",
+ "weave",
+ {
+ branchPrefix: SIDECAR_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ ],
+};
+
+export const BRANCH_FANTASY_RULES_FIXTURE_SCENARIO: FixtureLadderScenario = {
+ id: "branch-fantasy-rules",
+ label: "Branch-Published Fantasy Rules",
+ fixtureRepo: BRANCH_FANTASY_RULES_FIXTURE_REPO,
+ fixtureRepoRelativePath: BRANCH_FANTASY_RULES_FIXTURE_REPO_RELATIVE_PATH,
+ manifestRootRelativePath: BRANCH_FANTASY_RULES_MANIFEST_ROOT_RELATIVE_PATH,
+ branchPrefix: BRANCH_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ transitions: [
+ fileTransition(
+ 1,
+ "01-source-only",
+ "00-blank-slate",
+ {
+ description:
+ "Seed the clean authored source files for the branch-published Fantasy Rules fixture.",
+ sources: [
+ {
+ path: "NOTICE.md",
+ provenance:
+ "fixture-authored NOTICE text adapted from the Sidecar Fantasy Rules source-only branch",
+ },
+ {
+ path: "ontology/fantasy-rules-ontology.ttl",
+ provenance:
+ "fixture-authored ontology RDF adapted from the Sidecar Fantasy Rules source-only branch with the branch fixture base IRI",
+ },
+ {
+ path: "shacl/fantasy-rules-shacl.ttl",
+ provenance:
+ "fixture-authored SHACL RDF adapted from the Sidecar Fantasy Rules source-only branch with the branch fixture base IRI",
+ },
+ {
+ path: "examples/gunaar.ttl",
+ provenance:
+ "fixture-authored example RDF adapted from the Sidecar Fantasy Rules source-only branch with the branch fixture base IRI",
+ },
+ ],
+ },
+ "fixture.seedSourceOnly",
+ {
+ branchPrefix: BRANCH_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ branchPublicationTransition(
+ 2,
+ "02-publication-bootstrapped-woven",
+ "01-source-only",
+ {
+ description:
+ "Bootstrap the branch-published GitHub Pages publication root and weave its support pages.",
+ publicationBranch: "gh-pages",
+ },
+ ),
+ branchPublicationTransition(
+ 3,
+ "03-ontology-integrated-woven",
+ "01-source-only",
+ {
+ description:
+ "Materialize the ontology source into the branch-published GitHub Pages root and weave its ResourcePages.",
+ publicationFromRef: "02-publication-bootstrapped-woven",
+ publicationBranch: "gh-pages",
+ },
+ ),
+ branchPublicationTransition(
+ 4,
+ "04-shacl-integrated-woven",
+ "01-source-only",
+ {
+ description:
+ "Materialize the SHACL source into the branch-published GitHub Pages root and weave its ResourcePages.",
+ publicationFromRef: "03-ontology-integrated-woven",
+ publicationBranch: "gh-pages",
+ },
+ ),
+ branchPublicationTransition(
+ 5,
+ "05-ontology-and-shacl-terms-extracted",
+ "01-source-only",
+ {
+ description:
+ "Extract selected ontology and SHACL terms in the branch-published GitHub Pages root without weaving their pages yet.",
+ publicationFromRef: "04-shacl-integrated-woven",
+ publicationBranch: "gh-pages",
+ },
+ "extract",
+ ),
+ branchPublicationTransition(
+ 6,
+ "06-ontology-and-shacl-terms-extracted-woven",
+ "01-source-only",
+ {
+ description:
+ "Weave selected extracted ontology and SHACL term ResourcePages in the branch-published GitHub Pages root.",
+ publicationFromRef: "05-ontology-and-shacl-terms-extracted",
+ publicationBranch: "gh-pages",
+ },
+ "weave",
+ ),
+ branchPublicationTransition(
+ 7,
+ "07-root-and-examples-knops",
+ "01-source-only",
+ {
+ description:
+ "Create root and examples collection Knops in the branch-published GitHub Pages root without weaving their pages yet.",
+ publicationFromRef: "06-ontology-and-shacl-terms-extracted-woven",
+ publicationBranch: "gh-pages",
+ },
+ "knop.create",
+ ),
+ branchPublicationTransition(
+ 8,
+ "08-root-and-examples-knops-woven",
+ "01-source-only",
+ {
+ description:
+ "Weave root and examples collection Knop ResourcePages in the branch-published GitHub Pages root.",
+ publicationFromRef: "07-root-and-examples-knops",
+ publicationBranch: "gh-pages",
+ },
+ "weave",
+ ),
+ branchPublicationTransition(
+ 9,
+ "09-gunaar-example-dataset-woven",
+ "01-source-only",
+ {
+ description:
+ "Materialize the Gunaar example dataset from the clean source ref into the branch-published GitHub Pages root and weave its ResourcePages.",
+ publicationFromRef: "08-root-and-examples-knops-woven",
+ publicationBranch: "gh-pages",
+ },
+ ),
+ fileTransition(
+ 10,
+ "10-first-release-source",
+ "01-source-only",
+ {
+ description:
+ "Replace authored first-release source bytes from deterministic assets on the clean source lane.",
+ sources: [
+ {
+ path: "ontology/fantasy-rules-ontology.ttl",
+ assetPath: "14-first-release/ontology/fantasy-rules-ontology.ttl",
+ provenance:
+ "fixture-authored ontology release source copied from deterministic branch fixture assets",
+ },
+ {
+ path: "shacl/fantasy-rules-shacl.ttl",
+ assetPath: "14-first-release/shacl/fantasy-rules-shacl.ttl",
+ provenance:
+ "fixture-authored SHACL release source copied from deterministic branch fixture assets",
+ },
+ {
+ path: "examples/gunaar.ttl",
+ assetPath: "14-first-release/examples/gunaar.ttl",
+ provenance:
+ "fixture-authored Gunaar release example source copied from deterministic branch fixture assets",
+ },
+ ],
+ },
+ "source.update",
+ {
+ branchPrefix: BRANCH_FANTASY_RULES_LADDER_BRANCH_PREFIX,
+ },
+ ),
+ branchPublicationTransition(
+ 11,
+ "11-first-release-woven",
+ "10-first-release-source",
+ {
+ description:
+ "Update branch-published source bindings from the first-release source ref and weave named ontology and SHACL release states.",
+ publicationFromRef: "09-gunaar-example-dataset-woven",
+ publicationBranch: "gh-pages",
+ },
+ "weave",
+ ),
+ branchPublicationTransition(
+ 12,
+ "12-all-remaining-terms-extracted",
+ "10-first-release-source",
+ {
+ description:
+ "Extract every remaining mesh-scoped IRI from the current ontology, SHACL, and Gunaar source artifacts in the branch-published publication root.",
+ publicationFromRef: "11-first-release-woven",
+ publicationBranch: "gh-pages",
+ },
+ "extract",
+ ),
+ branchPublicationTransition(
+ 13,
+ "13-all-remaining-terms-woven",
+ "10-first-release-source",
+ {
+ description:
+ "Run a broad publication-root weave so every mesh-scoped source term has a generated ResourcePage.",
+ publicationFromRef: "12-all-remaining-terms-extracted",
+ publicationBranch: "gh-pages",
+ },
+ "weave",
+ ),
+ branchPublicationTransition(
+ 14,
+ "14-extracted-term-references",
+ "10-first-release-source",
+ {
+ description:
+ "Add curated canonical ReferenceLinks from representative extracted terms to their current source artifacts.",
+ publicationFromRef: "13-all-remaining-terms-woven",
+ publicationBranch: "gh-pages",
+ },
+ "knop.addReference",
+ ),
+ branchPublicationTransition(
+ 15,
+ "15-extracted-term-references-woven",
+ "10-first-release-source",
+ {
+ description:
+ "Weave the extracted term ReferenceCatalogs and refresh their ResourcePages.",
+ publicationFromRef: "14-extracted-term-references",
+ publicationBranch: "gh-pages",
+ },
+ "weave",
+ ),
+ ],
+};
+
+if (import.meta.main) {
+ try {
+ const options = parseFixtureLadderArgs(Deno.args);
+ if (options.executeTransitionId !== undefined) {
+ const result = await executeFixtureTransition({
+ root: options.root,
+ scenario: options.scenario,
+ transitionId: options.executeTransitionId,
+ workspaceRoot: options.workspaceRoot,
+ dryRun: options.dryRun ?? false,
+ });
+ console.log(
+ options.format === "json"
+ ? JSON.stringify(result, null, 2)
+ : renderFixtureExecutionResult(result),
+ );
+ if (
+ !result.operation.success ||
+ (!result.branchUpdate.updated && result.validation.status !== "pass")
+ ) {
+ Deno.exit(1);
+ }
+ } else if (options.materializeTransitionId !== undefined) {
+ const result = await materializeFixtureTransitionSource({
+ root: options.root,
+ scenario: options.scenario,
+ transitionId: options.materializeTransitionId,
+ workspaceRoot: options.workspaceRoot,
+ });
+ console.log(
+ options.format === "json"
+ ? JSON.stringify(result, null, 2)
+ : renderFixtureMaterializationResult(result),
+ );
+ } else {
+ const plan = await planFixtureLadder(options);
+ console.log(
+ options.format === "json"
+ ? JSON.stringify(plan, null, 2)
+ : renderFixtureLadderPlan(plan),
+ );
+ }
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parseFixtureLadderArgs(
+ args: readonly string[],
+): FixtureLadderOptions {
+ let root = Deno.cwd();
+ let scenario: FixtureScenarioId = "alice-bio";
+ let format: FixturePlanFormat = "text";
+ let materializeTransitionId: string | undefined;
+ let executeTransitionId: string | undefined;
+ let dryRun = false;
+ let workspaceRoot: string | undefined;
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+ switch (arg) {
+ case "--":
+ break;
+ case "--root":
+ index += 1;
+ root = requireArgumentValue(args[index], "--root");
+ break;
+ case "--scenario":
+ index += 1;
+ scenario = parseScenarioId(
+ requireArgumentValue(args[index], "--scenario"),
+ );
+ break;
+ case "--format":
+ index += 1;
+ format = parsePlanFormat(
+ requireArgumentValue(args[index], "--format"),
+ );
+ break;
+ case "--materialize":
+ index += 1;
+ materializeTransitionId = requireArgumentValue(
+ args[index],
+ "--materialize",
+ );
+ break;
+ case "--execute":
+ index += 1;
+ executeTransitionId = requireArgumentValue(args[index], "--execute");
+ break;
+ case "--workspace-root":
+ index += 1;
+ workspaceRoot = requireArgumentValue(args[index], "--workspace-root");
+ break;
+ case "--json":
+ format = "json";
+ break;
+ case "--dry-run":
+ dryRun = true;
+ break;
+ default:
+ if (arg.startsWith("--root=")) {
+ root = requireArgumentValue(arg.slice("--root=".length), "--root");
+ break;
+ }
+ if (arg.startsWith("--scenario=")) {
+ scenario = parseScenarioId(
+ requireArgumentValue(
+ arg.slice("--scenario=".length),
+ "--scenario",
+ ),
+ );
+ break;
+ }
+ if (arg.startsWith("--format=")) {
+ format = parsePlanFormat(
+ requireArgumentValue(arg.slice("--format=".length), "--format"),
+ );
+ break;
+ }
+ if (arg.startsWith("--materialize=")) {
+ materializeTransitionId = requireArgumentValue(
+ arg.slice("--materialize=".length),
+ "--materialize",
+ );
+ break;
+ }
+ if (arg.startsWith("--execute=")) {
+ executeTransitionId = requireArgumentValue(
+ arg.slice("--execute=".length),
+ "--execute",
+ );
+ break;
+ }
+ if (arg.startsWith("--workspace-root=")) {
+ workspaceRoot = requireArgumentValue(
+ arg.slice("--workspace-root=".length),
+ "--workspace-root",
+ );
+ break;
+ }
+ throw new Error(`Unsupported fixture:ladder argument: ${arg}`);
+ }
+ }
+
+ if (
+ materializeTransitionId !== undefined && executeTransitionId !== undefined
+ ) {
+ throw new Error(
+ "fixture:ladder accepts only one of --materialize or --execute",
+ );
+ }
+
+ if (
+ workspaceRoot !== undefined && materializeTransitionId === undefined &&
+ executeTransitionId === undefined
+ ) {
+ throw new Error(
+ "fixture:ladder --workspace-root requires --materialize or --execute",
+ );
+ }
+
+ return {
+ root: resolve(root),
+ scenario,
+ format,
+ ...(dryRun ? { dryRun } : {}),
+ ...(materializeTransitionId !== undefined
+ ? { materializeTransitionId }
+ : {}),
+ ...(executeTransitionId !== undefined ? { executeTransitionId } : {}),
+ ...(workspaceRoot !== undefined
+ ? { workspaceRoot: resolve(workspaceRoot) }
+ : {}),
+ };
+}
+
+export async function planFixtureLadder(
+ options: FixtureLadderOptions,
+): Promise {
+ const scenario = resolveFixtureScenario(options.scenario);
+ const root = resolve(options.root);
+ const manifestRoot = join(root, scenario.manifestRootRelativePath);
+ const assetRoot = join(
+ root,
+ scenario.assetRootRelativePath ??
+ join(scenario.fixtureRepoRelativePath, FIXTURE_ASSET_ROOT_BASENAME),
+ );
+
+ const transitions = await Promise.all(
+ scenario.transitions.map((transition) =>
+ hydrateFixtureTransitionPlan({
+ transition,
+ manifestPath: join(manifestRoot, transition.manifestName),
+ })
+ ),
+ );
+
+ return {
+ scenario,
+ root,
+ fixtureRepoPath: join(root, scenario.fixtureRepoRelativePath),
+ manifestRoot,
+ assetRoot,
+ transitions,
+ writesBranches: false,
+ };
+}
+
+export function renderFixtureLadderPlan(plan: FixtureLadderPlan): string {
+ const lines = [
+ `Fixture ladder dry run: ${plan.scenario.label}`,
+ `Fixture repository: ${plan.scenario.fixtureRepo}`,
+ `Fixture repository path: ${plan.fixtureRepoPath}`,
+ `Manifest root: ${plan.manifestRoot}`,
+ `Asset root: ${plan.assetRoot}`,
+ "Branch writes: disabled",
+ `Transitions: ${plan.transitions.length}`,
+ ];
+
+ for (const transition of plan.transitions) {
+ lines.push("");
+ lines.push(
+ `${transition.index}. ${transition.id}: ${transition.fromRef} -> ${transition.toRef}`,
+ );
+ lines.push(` operation: ${transition.operationId}`);
+ lines.push(
+ ` manifest: ${relative(plan.root, transition.manifestPath)}`,
+ );
+ if (transition.action.kind === "command") {
+ const invocations = commandActionInvocations(transition.action);
+ for (const [index, invocation] of invocations.entries()) {
+ const label = invocations.length > 1
+ ? ` command ${index + 1}:`
+ : " command:";
+ lines.push(
+ `${label} ${
+ [
+ invocation.executable,
+ ...invocation.argv,
+ ].join(" ")
+ }`,
+ );
+ }
+ lines.push(` cwd: ${transition.action.cwd}`);
+ lines.push(` prompts: ${transition.action.promptPolicy}`);
+ lines.push(
+ ` runtime logs: ${transition.action.expectedRuntimeLogs}`,
+ );
+ for (const input of transition.action.inputs) {
+ lines.push(
+ ` input: ${input.path} <= ${
+ formatFixtureAssetPath(input)
+ } (${input.provenance})`,
+ );
+ }
+ } else if (transition.action.kind === "fileOperation") {
+ lines.push(` file operation: ${transition.action.description}`);
+ for (const source of transition.action.sources) {
+ lines.push(
+ ` source: ${source.path} <= ${
+ formatFixtureAssetPath(source)
+ } (${source.provenance})`,
+ );
+ }
+ for (const patch of transition.action.inventoryPatches) {
+ lines.push(
+ ` inventory patch: ${patch.inventoryPath} registers ${patch.pageDefinitionPath} (${patch.provenance})`,
+ );
+ }
+ } else {
+ lines.push(` branch publication: ${transition.action.description}`);
+ lines.push(` source ref: ${transition.action.sourceRef}`);
+ lines.push(
+ ` publication from ref: ${
+ transition.action.publicationFromRef ?? "(empty publication root)"
+ }`,
+ );
+ lines.push(
+ ` publication branch: ${transition.action.publicationBranch}`,
+ );
+ for (
+ const [index, invocation] of transition.action.invocations
+ .entries()
+ ) {
+ const label = transition.action.invocations.length > 1
+ ? ` command ${index + 1}:`
+ : " command:";
+ lines.push(
+ `${label} ${[invocation.executable, ...invocation.argv].join(" ")}`,
+ );
+ }
+ }
+ lines.push(
+ ` validation: ${transition.validation.comparison} via Accord manifest`,
+ );
+ for (const guardrail of transition.validation.guardrails) {
+ lines.push(` guardrail: ${guardrail}`);
+ }
+ }
+
+ return lines.join("\n");
+}
+
+export async function materializeFixtureTransitionSource(
+ options: MaterializeFixtureTransitionOptions,
+): Promise {
+ const plan = await planFixtureLadder({
+ root: options.root,
+ scenario: options.scenario,
+ format: "text",
+ });
+ const transition = findFixtureTransitionPlan(plan, options.transitionId);
+
+ const workspaceRoot = options.workspaceRoot === undefined
+ ? await Deno.makeTempDir({ prefix: "weave-fixture-ladder-" })
+ : resolve(options.workspaceRoot);
+ await ensureEmptyWorkspaceRoot(workspaceRoot);
+
+ const resolvedRef = await resolveGitCommitishIfExists(
+ plan.fixtureRepoPath,
+ transition.fromRef,
+ );
+ if (resolvedRef === undefined) {
+ throw unresolvedFixtureRefError(plan.fixtureRepoPath, transition.fromRef);
+ }
+
+ const materializedPaths = await materializeGitTree({
+ repoPath: plan.fixtureRepoPath,
+ ref: resolvedRef,
+ workspaceRoot,
+ });
+
+ return {
+ scenario: plan.scenario.id,
+ transitionId: transition.id,
+ fromRef: transition.fromRef,
+ toRef: transition.toRef,
+ operationId: transition.operationId,
+ fixtureRepoPath: plan.fixtureRepoPath,
+ manifestPath: transition.manifestPath,
+ assetRoot: plan.assetRoot,
+ workspaceRoot,
+ materializedPaths,
+ writesBranches: false,
+ nextAction: transition.action,
+ };
+}
+
+export async function executeFixtureTransition(
+ options: ExecuteFixtureTransitionOptions,
+): Promise {
+ const plan = await planFixtureLadder({
+ root: options.root,
+ scenario: options.scenario,
+ format: "text",
+ });
+ const transition = findFixtureTransitionPlan(plan, options.transitionId);
+
+ if (transition.action.kind === "branchPublication") {
+ return await executeBranchPublicationTransition({
+ options,
+ plan,
+ transition,
+ action: transition.action,
+ });
+ }
+
+ const materialization = await materializeFixtureTransitionSource(options);
+ const operation = transition.action.kind === "command"
+ ? await runFixtureCommand({
+ assetRoot: plan.assetRoot,
+ root: plan.root,
+ workspaceRoot: materialization.workspaceRoot,
+ action: transition.action,
+ })
+ : transition.action.kind === "fileOperation"
+ ? await applyFixtureFileOperation({
+ assetRoot: plan.assetRoot,
+ workspaceRoot: materialization.workspaceRoot,
+ action: transition.action,
+ })
+ : unreachableAction(transition.action);
+ const validation = await validateFixtureTransitionWorkspace({
+ fixtureRepoPath: plan.fixtureRepoPath,
+ manifestPath: transition.manifestPath,
+ workspaceRoot: materialization.workspaceRoot,
+ fallbackFromRef: transition.fromRef,
+ fallbackToRef: transition.toRef,
+ });
+ const branchUpdate = await maybeUpdateFixtureBranch({
+ fixtureRepoPath: plan.fixtureRepoPath,
+ workspaceRoot: materialization.workspaceRoot,
+ targetRef: transition.toRef,
+ parentRef: transition.fromRef,
+ dryRun: options.dryRun ?? false,
+ operation,
+ validation,
+ message: `Regenerate fixture branch ${transition.toRef}`,
+ });
+
+ const base = {
+ scenario: materialization.scenario,
+ transitionId: materialization.transitionId,
+ fromRef: materialization.fromRef,
+ toRef: materialization.toRef,
+ operationId: materialization.operationId,
+ fixtureRepoPath: materialization.fixtureRepoPath,
+ manifestPath: materialization.manifestPath,
+ assetRoot: materialization.assetRoot,
+ workspaceRoot: materialization.workspaceRoot,
+ materializedPaths: materialization.materializedPaths,
+ operation,
+ validation,
+ writesBranches: branchUpdate.updated,
+ branchUpdate,
+ };
+
+ if (operation.kind === "command") {
+ return {
+ ...base,
+ actionKind: "command",
+ operation,
+ command: operation,
+ };
+ }
+
+ return {
+ ...base,
+ actionKind: "fileOperation",
+ operation,
+ fileOperation: operation,
+ };
+}
+
+async function executeBranchPublicationTransition(options: {
+ options: ExecuteFixtureTransitionOptions;
+ plan: FixtureLadderPlan;
+ transition: FixtureTransitionPlan;
+ action: FixtureBranchPublicationAction;
+}): Promise {
+ const materialization = await materializeBranchPublicationWorkspaces({
+ plan: options.plan,
+ transition: options.transition,
+ action: options.action,
+ workspaceRoot: options.options.workspaceRoot,
+ });
+ const operation = await runBranchPublicationCommands({
+ root: options.plan.root,
+ sourceWorkspaceRoot: materialization.sourceWorkspaceRoot,
+ sourceRef: options.action.sourceRef,
+ sourceCommit: materialization.sourceCommit,
+ publicationWorkspaceRoot: materialization.publicationWorkspaceRoot,
+ action: options.action,
+ });
+ const validation = await validateFixtureTransitionWorkspace({
+ fixtureRepoPath: options.plan.fixtureRepoPath,
+ manifestPath: options.transition.manifestPath,
+ workspaceRoot: materialization.publicationWorkspaceRoot,
+ fallbackFromRef: options.action.publicationFromRef ??
+ options.transition.fromRef,
+ fallbackToRef: options.transition.toRef,
+ });
+ const branchUpdate = await maybeUpdateFixtureBranch({
+ fixtureRepoPath: options.plan.fixtureRepoPath,
+ workspaceRoot: materialization.publicationWorkspaceRoot,
+ targetRef: options.transition.toRef,
+ parentRef: options.action.publicationFromRef,
+ dryRun: options.options.dryRun ?? false,
+ operation,
+ validation,
+ message: `Regenerate fixture branch ${options.transition.toRef}`,
+ });
+ const publicationBranchUpdate = await maybeFastForwardPublicationBranch({
+ fixtureRepoPath: options.plan.fixtureRepoPath,
+ publicationBranch: options.action.publicationBranch,
+ branchUpdate,
+ });
+
+ return {
+ scenario: options.plan.scenario.id,
+ transitionId: options.transition.id,
+ fromRef: options.transition.fromRef,
+ toRef: options.transition.toRef,
+ operationId: options.transition.operationId,
+ fixtureRepoPath: options.plan.fixtureRepoPath,
+ manifestPath: options.transition.manifestPath,
+ assetRoot: options.plan.assetRoot,
+ workspaceRoot: materialization.publicationWorkspaceRoot,
+ materializedPaths: materialization.publicationMaterializedPaths,
+ operation,
+ validation,
+ writesBranches: branchUpdate.updated,
+ branchUpdate,
+ actionKind: "branchPublication",
+ branchPublication: operation,
+ publicationBranchUpdate,
+ };
+}
+
+export function renderFixtureMaterializationResult(
+ result: FixtureMaterializationResult,
+): string {
+ const lines = [
+ `Fixture source materialized: ${result.scenario}`,
+ `Transition: ${result.transitionId}`,
+ `Source ref: ${result.fromRef}`,
+ `Target ref: ${result.toRef}`,
+ `Asset root: ${result.assetRoot}`,
+ `Workspace root: ${result.workspaceRoot}`,
+ "Branch writes: disabled",
+ `Files materialized: ${result.materializedPaths.length}`,
+ ];
+ for (const path of result.materializedPaths) {
+ lines.push(`- ${path}`);
+ }
+ if (result.nextAction.kind === "command") {
+ const invocations = commandActionInvocations(result.nextAction);
+ lines.push(
+ `${invocations.length > 1 ? "Next commands" : "Next command"}: ${
+ invocations.map((invocation) =>
+ [invocation.executable, ...invocation.argv].join(" ")
+ ).join(" && ")
+ }`,
+ );
+ } else if (result.nextAction.kind === "fileOperation") {
+ lines.push(`Next file operation: ${result.nextAction.description}`);
+ } else {
+ lines.push(`Next branch publication: ${result.nextAction.description}`);
+ }
+ return lines.join("\n");
+}
+
+export function renderFixtureExecutionResult(
+ result: FixtureExecutionResult,
+): string {
+ const lines = [
+ `Fixture transition executed: ${result.scenario}`,
+ `Transition: ${result.transitionId}`,
+ `Source ref: ${result.fromRef}`,
+ `Target ref: ${result.toRef}`,
+ `Asset root: ${result.assetRoot}`,
+ `Workspace root: ${result.workspaceRoot}`,
+ `Branch writes: ${result.branchUpdate.updated ? "enabled" : "disabled"}`,
+ ];
+
+ if (result.actionKind === "command") {
+ const commands = result.command.commands ?? [{
+ command: result.command.command,
+ cwd: result.command.cwd,
+ success: result.command.success,
+ code: result.command.code,
+ stdout: result.command.stdout,
+ stderr: result.command.stderr,
+ }];
+ if (commands.length === 1) {
+ lines.push(`Command: ${result.command.command.join(" ")}`);
+ } else {
+ lines.push(`Commands: ${commands.length}`);
+ for (const [index, command] of commands.entries()) {
+ lines.push(`Command ${index + 1}: ${command.command.join(" ")}`);
+ lines.push(`Command ${index + 1} exit code: ${command.code}`);
+ }
+ }
+ lines.push(`Command cwd: ${result.command.cwd}`);
+ lines.push(`Command exit code: ${result.command.code}`);
+
+ if (result.command.stdout.trim().length > 0) {
+ lines.push("Command stdout:");
+ lines.push(result.command.stdout.trimEnd());
+ }
+
+ if (result.command.stderr.trim().length > 0) {
+ lines.push("Command stderr:");
+ lines.push(result.command.stderr.trimEnd());
+ }
+ } else if (result.actionKind === "fileOperation") {
+ lines.push(`File operation: ${result.fileOperation.description}`);
+ lines.push(`File operation success: ${result.fileOperation.success}`);
+ lines.push(`Files applied: ${result.fileOperation.files.length}`);
+ for (const file of result.fileOperation.files) {
+ lines.push(
+ `- ${file.path} <= ${
+ formatFixtureAssetPath(file)
+ } (${file.bytes} bytes)`,
+ );
+ }
+ if (result.fileOperation.missingAssets.length > 0) {
+ lines.push("Missing assets:");
+ for (const missing of result.fileOperation.missingAssets) {
+ lines.push(
+ `- ${missing.path} <= ${
+ formatFixtureAssetPath(missing)
+ } (${missing.absolutePath})`,
+ );
+ }
+ }
+ } else {
+ lines.push(
+ `Branch publication: ${result.branchPublication.description}`,
+ );
+ lines.push(
+ `Source workspace: ${result.branchPublication.sourceWorkspaceRoot}`,
+ );
+ lines.push(
+ `Publication workspace: ${result.branchPublication.publicationWorkspaceRoot}`,
+ );
+ lines.push(
+ `Publication branch: ${result.branchPublication.publicationBranch}`,
+ );
+ lines.push(`Commands: ${result.branchPublication.commands.length}`);
+ for (
+ const [index, command] of result.branchPublication.commands
+ .entries()
+ ) {
+ lines.push(`Command ${index + 1}: ${command.command.join(" ")}`);
+ lines.push(`Command ${index + 1} exit code: ${command.code}`);
+ }
+ if (result.branchPublication.stdout.trim().length > 0) {
+ lines.push("Command stdout:");
+ lines.push(result.branchPublication.stdout.trimEnd());
+ }
+ if (result.branchPublication.stderr.trim().length > 0) {
+ lines.push("Command stderr:");
+ lines.push(result.branchPublication.stderr.trimEnd());
+ }
+ }
+
+ lines.push("Validation:");
+ lines.push(renderTextReport(result.validation));
+ lines.push("Branch update:");
+ if (result.branchUpdate.updated) {
+ lines.push(
+ `updated ${result.branchUpdate.branchRef} to ${result.branchUpdate.commitSha}`,
+ );
+ lines.push(
+ `Push ${result.branchUpdate.targetRef} from ${result.fixtureRepoPath} separately for the regenerated fixture to leave this checkout.`,
+ );
+ } else {
+ lines.push(`skipped: ${result.branchUpdate.reason}`);
+ }
+ if (result.actionKind === "branchPublication") {
+ lines.push("Publication branch update:");
+ if (result.publicationBranchUpdate.updated) {
+ lines.push(
+ `fast-forwarded ${result.publicationBranchUpdate.branchRef} to ${result.publicationBranchUpdate.commitSha}`,
+ );
+ } else {
+ lines.push(`skipped: ${result.publicationBranchUpdate.reason}`);
+ }
+ }
+ return lines.join("\n");
+}
+
+function resolveFixtureScenario(id: FixtureScenarioId): FixtureLadderScenario {
+ switch (id) {
+ case "alice-bio":
+ return ALICE_BIO_FIXTURE_SCENARIO;
+ case "sidecar-fantasy-rules":
+ return SIDECAR_FANTASY_RULES_FIXTURE_SCENARIO;
+ case "branch-fantasy-rules":
+ return BRANCH_FANTASY_RULES_FIXTURE_SCENARIO;
+ }
+}
+
+function findFixtureTransitionPlan(
+ plan: FixtureLadderPlan,
+ transitionId: string,
+): FixtureTransitionPlan {
+ const transition = plan.transitions.find((candidate) =>
+ candidate.id === transitionId
+ );
+ if (transition === undefined) {
+ throw new Error(
+ `Unknown ${plan.scenario.label} transition: ${transitionId}`,
+ );
+ }
+ return transition;
+}
+
+async function hydrateFixtureTransitionPlan(options: {
+ transition: FixtureTransitionDefinition;
+ manifestPath: string;
+}): Promise {
+ const base = {
+ ...options.transition,
+ manifestPath: options.manifestPath,
+ };
+
+ if (options.transition.action.kind === "fileOperation") {
+ return base;
+ }
+
+ if (!await pathExists(options.manifestPath)) {
+ return base;
+ }
+
+ const manifest = await readManifestSource(options.manifestPath);
+ const transitionCase = selectTransitionCase(manifest.document);
+ if (options.transition.action.kind === "branchPublication") {
+ return {
+ ...base,
+ operationId: transitionCase.operationId ?? options.transition.operationId,
+ action: hydrateBranchPublicationActionFromReplayProfile({
+ action: options.transition.action,
+ transitionId: options.transition.id,
+ manifestPath: options.manifestPath,
+ replayProfile: transitionCase.hasReplayProfile,
+ }),
+ };
+ }
+
+ return {
+ ...base,
+ operationId: transitionCase.operationId ?? options.transition.operationId,
+ action: hydrateCommandActionFromReplayProfile({
+ transitionId: options.transition.id,
+ manifestPath: options.manifestPath,
+ replayProfile: transitionCase.hasReplayProfile,
+ }),
+ };
+}
+
+function replayProfileCommandInvocations(options: {
+ transitionId: string;
+ manifestPath: string;
+ replayProfile?: ReplayProfile;
+}): readonly CommandInvocation[] {
+ const replayProfile = options.replayProfile;
+ if (replayProfile === undefined) {
+ throw new Error(
+ `Manifest ${options.manifestPath} is missing hasReplayProfile for command transition ${options.transitionId}`,
+ );
+ }
+
+ const invocations = replayProfile.hasCommandSequence?.length
+ ? replayProfile.hasCommandSequence
+ : replayProfile.hasCommandInvocation === undefined
+ ? []
+ : [replayProfile.hasCommandInvocation];
+ if (invocations.length === 0) {
+ throw new Error(
+ `Manifest ${options.manifestPath} is missing hasReplayProfile.hasCommandInvocation or hasCommandSequence for command transition ${options.transitionId}`,
+ );
+ }
+
+ validateReplayProfile(options.transitionId, replayProfile);
+ for (const invocation of invocations) {
+ validateCommandInvocation(options.transitionId, invocation);
+ }
+
+ return invocations;
+}
+
+function hydrateCommandActionFromReplayProfile(options: {
+ transitionId: string;
+ manifestPath: string;
+ replayProfile?: ReplayProfile;
+}): FixtureCommandAction {
+ const replayProfile = options.replayProfile;
+ const invocations = replayProfileCommandInvocations(options);
+
+ const hydratedInvocations = invocations.map((invocation) =>
+ hydrateCommandInvocationAction({
+ transitionId: options.transitionId,
+ replayProfile: replayProfile!,
+ invocation,
+ })
+ );
+ const firstInvocation = hydratedInvocations[0];
+ if (firstInvocation === undefined) {
+ throw new Error(
+ `Manifest ${options.manifestPath} has an empty command sequence for ${options.transitionId}`,
+ );
+ }
+
+ return {
+ kind: "command",
+ ...firstInvocation,
+ ...(hydratedInvocations.length > 1
+ ? { invocations: hydratedInvocations }
+ : {}),
+ };
+}
+
+function hydrateBranchPublicationActionFromReplayProfile(options: {
+ action: FixtureBranchPublicationAction;
+ transitionId: string;
+ manifestPath: string;
+ replayProfile?: ReplayProfile;
+}): FixtureBranchPublicationAction {
+ const invocations = replayProfileCommandInvocations(options);
+
+ return {
+ ...options.action,
+ invocations: invocations.map((invocation) => ({
+ executable: "weave",
+ argv: invocation.argv ?? [],
+ cwd: "workspace",
+ promptPolicy: "nonInteractive",
+ expectedRuntimeLogs: invocation.expectsOperationalLogs === true ||
+ invocation.expectsAuditLogs === true,
+ })),
+ };
+}
+
+function hydrateCommandInvocationAction(options: {
+ transitionId: string;
+ replayProfile: ReplayProfile;
+ invocation: CommandInvocation;
+}): FixtureCommandInvocationAction {
+ return {
+ executable: "weave",
+ argv: options.invocation.argv ?? [],
+ inputs: resolveReplayInputMaterializations(
+ options.transitionId,
+ options.replayProfile.hasInputMaterialization ?? [],
+ ),
+ cwd: "workspace",
+ promptPolicy: "nonInteractive",
+ expectedRuntimeLogs: options.invocation.expectsOperationalLogs === true ||
+ options.invocation.expectsAuditLogs === true,
+ };
+}
+
+function validateReplayProfile(
+ transitionId: string,
+ replayProfile: ReplayProfile,
+): void {
+ if (
+ replayProfile.workspaceRoot !== undefined &&
+ replayProfile.workspaceRoot !== "."
+ ) {
+ throw new Error(
+ `Unsupported replay workspaceRoot for ${transitionId}: ${replayProfile.workspaceRoot}`,
+ );
+ }
+
+ if (replayProfile.meshRoot !== undefined && replayProfile.meshRoot !== ".") {
+ try {
+ normalizeGitTreePath(replayProfile.meshRoot);
+ } catch {
+ throw new Error(
+ `Unsupported replay meshRoot for ${transitionId}: ${replayProfile.meshRoot}`,
+ );
+ }
+ }
+}
+
+function validateCommandInvocation(
+ transitionId: string,
+ invocation: CommandInvocation,
+): void {
+ if (invocation.executable !== "weave") {
+ throw new Error(
+ `Unsupported replay executable for ${transitionId}: ${invocation.executable}`,
+ );
+ }
+
+ if (
+ invocation.workingDirectory !== undefined &&
+ invocation.workingDirectory !== "workspace"
+ ) {
+ throw new Error(
+ `Unsupported replay workingDirectory for ${transitionId}: ${invocation.workingDirectory}`,
+ );
+ }
+
+ if (
+ invocation.promptPolicy !== undefined &&
+ invocation.promptPolicy !== "nonInteractive"
+ ) {
+ throw new Error(
+ `Unsupported replay promptPolicy for ${transitionId}: ${invocation.promptPolicy}`,
+ );
+ }
+
+ if (
+ invocation.expectedExitCode !== undefined &&
+ invocation.expectedExitCode !== 0
+ ) {
+ throw new Error(
+ `Unsupported replay expectedExitCode for ${transitionId}: ${invocation.expectedExitCode}`,
+ );
+ }
+
+ if ((invocation.hasEnvironmentOverride ?? []).length > 0) {
+ throw new Error(
+ `Unsupported replay environment overrides for ${transitionId}`,
+ );
+ }
+}
+
+function resolveReplayInputMaterializations(
+ transitionId: string,
+ materializations: readonly InputMaterialization[],
+): FixtureFileOperationSource[] {
+ return materializations.map((materialization) => {
+ if (materialization.targetPath === undefined) {
+ throw new Error(
+ `Replay input materialization for ${transitionId} is missing targetPath`,
+ );
+ }
+
+ const targetPath = normalizeGitTreePath(materialization.targetPath);
+ const provenance = materialization.hasSourceProvenance;
+ const assetPath = provenance?.sourcePath === undefined
+ ? pathPosix.join(transitionId, targetPath)
+ : normalizeGitTreePath(provenance.sourcePath);
+
+ return {
+ path: targetPath,
+ assetPath,
+ provenance: describeSourceProvenance(provenance),
+ };
+ });
+}
+
+function describeSourceProvenance(provenance?: SourceProvenance): string {
+ return provenance?.derivationNote ??
+ provenance?.sourceUrl ??
+ provenance?.sourceRef ??
+ provenance?.sourceKind ??
+ "manifest-declared fixture input";
+}
+
+async function pathExists(path: string): Promise {
+ try {
+ await Deno.stat(path);
+ return true;
+ } catch (error) {
+ if (error instanceof Deno.errors.NotFound) {
+ return false;
+ }
+ throw error;
+ }
+}
+
+function commandTransition(
+ index: number,
+ id: string,
+ fromRef: string,
+ operationId: string,
+ options: {
+ branchPrefix?: string;
+ } = {},
+): FixtureTransitionDefinition {
+ const branchPrefix = options.branchPrefix ?? ALICE_BIO_LADDER_BRANCH_PREFIX;
+ return {
+ index,
+ id,
+ fromRef: toLadderBranchRef(branchPrefix, fromRef),
+ toRef: toLadderBranchRef(branchPrefix, id),
+ manifestName: `${id}.jsonld`,
+ operationId,
+ action: {
+ kind: "command",
+ executable: "weave",
+ argv: [],
+ inputs: [],
+ cwd: "workspace",
+ promptPolicy: "nonInteractive",
+ expectedRuntimeLogs: true,
+ },
+ validation: defaultValidation(),
+ };
+}
+
+function fileTransition(
+ index: number,
+ id: string,
+ fromRef: string,
+ action: {
+ description: string;
+ sources: readonly FixtureFileOperationSourceInput[];
+ inventoryPatches?:
+ readonly FixtureResourcePageDefinitionInventoryPatchInput[];
+ },
+ operationId = "fixture.fileOperation",
+ options: {
+ branchPrefix?: string;
+ } = {},
+): FixtureTransitionDefinition {
+ const branchPrefix = options.branchPrefix ?? ALICE_BIO_LADDER_BRANCH_PREFIX;
+ return {
+ index,
+ id,
+ fromRef: toLadderBranchRef(branchPrefix, fromRef),
+ toRef: toLadderBranchRef(branchPrefix, id),
+ manifestName: `${id}.jsonld`,
+ operationId,
+ action: {
+ kind: "fileOperation",
+ description: action.description,
+ sources: resolveFixtureAssetSources(id, action.sources),
+ inventoryPatches: (action.inventoryPatches ?? []).map(
+ resolveResourcePageDefinitionInventoryPatch,
+ ),
+ },
+ validation: defaultValidation(),
+ };
+}
+
+function branchPublicationTransition(
+ index: number,
+ id: string,
+ sourceFromRef: string,
+ action: {
+ description: string;
+ publicationFromRef?: string;
+ publicationBranch: string;
+ invocations?: readonly {
+ argv: readonly string[];
+ }[];
+ },
+ operationId = "deploy.ghPages",
+ options: {
+ branchPrefix?: string;
+ } = {},
+): FixtureTransitionDefinition {
+ const branchPrefix = options.branchPrefix ??
+ BRANCH_FANTASY_RULES_LADDER_BRANCH_PREFIX;
+ const sourceRef = toLadderBranchRef(branchPrefix, sourceFromRef);
+ const publicationFromRef = action.publicationFromRef === undefined
+ ? undefined
+ : toLadderBranchRef(branchPrefix, action.publicationFromRef);
+ return {
+ index,
+ id,
+ fromRef: publicationFromRef ?? sourceRef,
+ toRef: toLadderBranchRef(branchPrefix, id),
+ manifestName: `${id}.jsonld`,
+ operationId,
+ action: {
+ kind: "branchPublication",
+ description: action.description,
+ sourceRef,
+ ...(publicationFromRef === undefined ? {} : { publicationFromRef }),
+ publicationBranch: action.publicationBranch,
+ invocations: (action.invocations ?? []).map((invocation) => ({
+ executable: "weave",
+ argv: invocation.argv,
+ cwd: "workspace",
+ promptPolicy: "nonInteractive",
+ expectedRuntimeLogs: true,
+ })),
+ },
+ validation: defaultValidation(),
+ };
+}
+
+function resolveFixtureAssetSources(
+ transitionId: string,
+ sources: readonly FixtureFileOperationSourceInput[],
+): FixtureFileOperationSource[] {
+ return sources.map((source) => ({
+ path: source.path,
+ assetPath: source.assetPath ??
+ pathPosix.join(transitionId, normalizeGitTreePath(source.path)),
+ provenance: source.provenance,
+ }));
+}
+
+function resolveResourcePageDefinitionInventoryPatch(
+ input: FixtureResourcePageDefinitionInventoryPatchInput,
+): FixtureResourcePageDefinitionInventoryPatch {
+ const knopPath = toKnopPath(input.designatorPath);
+ const pageDefinitionPath = pathPosix.join(knopPath, "_page");
+ return {
+ kind: "resourcePageDefinition",
+ inventoryPath: pathPosix.join(knopPath, "_inventory/inventory.ttl"),
+ knopPath,
+ pageDefinitionPath,
+ pageDefinitionFilePath: pathPosix.join(pageDefinitionPath, "page.ttl"),
+ ...(input.hasAssetBundle
+ ? { assetBundlePath: pathPosix.join(knopPath, "_assets") }
+ : {}),
+ provenance: input.provenance,
+ };
+}
+
+function toKnopPath(designatorPath: string): string {
+ return designatorPath.length === 0
+ ? "_knop"
+ : pathPosix.join(normalizeGitTreePath(designatorPath), "_knop");
+}
+
+function toLadderBranchRef(branchPrefix: string, rungId: string): string {
+ return `${branchPrefix}${rungId}`;
+}
+
+function formatFixtureAssetPath(options: { assetPath: string }): string {
+ return pathPosix.join(FIXTURE_ASSET_ROOT_BASENAME, options.assetPath);
+}
+
+function defaultValidation(): FixtureTransitionValidation {
+ return {
+ accordManifest: true,
+ comparison: "manifestScoped",
+ guardrails: CANONICAL_OUTPUT_GUARDRAILS,
+ };
+}
+
+function commandActionInvocations(
+ action: FixtureCommandAction,
+): readonly FixtureCommandInvocationAction[] {
+ return action.invocations ?? [action];
+}
+
+function unreachableAction(action: never): never {
+ throw new Error(`Unsupported fixture action: ${JSON.stringify(action)}`);
+}
+
+async function runFixtureCommand(options: {
+ assetRoot: string;
+ root: string;
+ workspaceRoot: string;
+ action: FixtureCommandAction;
+}): Promise {
+ const invocations = commandActionInvocations(options.action);
+ const results: FixtureCommandInvocationExecutionResult[] = [];
+
+ for (const invocation of invocations) {
+ const result = await runFixtureCommandInvocation({
+ ...options,
+ invocation,
+ });
+ results.push(result);
+ if (!result.success) {
+ return summarizeFixtureCommandResults(results);
+ }
+ }
+
+ return summarizeFixtureCommandResults(results);
+}
+
+async function runFixtureCommandInvocation(options: {
+ assetRoot: string;
+ root: string;
+ workspaceRoot: string;
+ invocation: FixtureCommandInvocationAction;
+}): Promise {
+ if (options.invocation.executable !== "weave") {
+ throw new Error(
+ `Unsupported fixture command executable: ${options.invocation.executable}`,
+ );
+ }
+
+ const command = [
+ "deno",
+ "run",
+ "--allow-read",
+ "--allow-write",
+ "--allow-run=git",
+ "--allow-env",
+ join(options.root, "src/main.ts"),
+ ...options.invocation.argv,
+ ];
+ const stagedInputs = await stageFixtureAssetSources({
+ assetRoot: options.assetRoot,
+ workspaceRoot: options.workspaceRoot,
+ sources: options.invocation.inputs,
+ });
+ if (stagedInputs.missingAssets.length > 0) {
+ return {
+ command,
+ cwd: options.workspaceRoot,
+ success: false,
+ code: 1,
+ stdout: "",
+ stderr: stagedInputs.missingAssets.map((missing) =>
+ `Missing fixture command input ${missing.path} from ${
+ formatFixtureAssetPath(missing)
+ } (${missing.absolutePath})`
+ ).join("\n"),
+ };
+ }
+
+ const output = await new Deno.Command("deno", {
+ cwd: options.workspaceRoot,
+ args: command.slice(1),
+ env: {
+ WEAVE_GENERATED_AT: FIXTURE_GENERATED_AT,
+ },
+ stdout: "piped",
+ stderr: "piped",
+ }).output();
+
+ return {
+ command,
+ cwd: options.workspaceRoot,
+ success: output.success,
+ code: output.code,
+ stdout: new TextDecoder().decode(output.stdout),
+ stderr: new TextDecoder().decode(output.stderr),
+ };
+}
+
+function summarizeFixtureCommandResults(
+ results: readonly FixtureCommandInvocationExecutionResult[],
+): FixtureCommandExecutionResult {
+ const first = results[0];
+ const last = results.at(-1);
+ if (first === undefined || last === undefined) {
+ throw new Error("Fixture command action must contain at least one command");
+ }
+
+ return {
+ kind: "command",
+ command: first.command,
+ commands: results,
+ cwd: first.cwd,
+ success: results.every((result) => result.success),
+ code: last.code,
+ stdout: results.map((result) => result.stdout).join(""),
+ stderr: results.map((result) => result.stderr).join(""),
+ };
+}
+
+async function materializeBranchPublicationWorkspaces(options: {
+ plan: FixtureLadderPlan;
+ transition: FixtureTransitionPlan;
+ action: FixtureBranchPublicationAction;
+ workspaceRoot?: string;
+}): Promise<{
+ workspaceRoot: string;
+ sourceWorkspaceRoot: string;
+ sourceCommit: string;
+ publicationWorkspaceRoot: string;
+ sourceMaterializedPaths: readonly string[];
+ publicationMaterializedPaths: readonly string[];
+}> {
+ const workspaceRoot = options.workspaceRoot === undefined
+ ? await Deno.makeTempDir({ prefix: "weave-fixture-ladder-" })
+ : resolve(options.workspaceRoot);
+ await ensureEmptyWorkspaceRoot(workspaceRoot);
+
+ const sourceWorkspaceRoot = join(workspaceRoot, "source");
+ const publicationWorkspaceRoot = join(workspaceRoot, "publication");
+ await Deno.mkdir(sourceWorkspaceRoot, { recursive: true });
+ await Deno.mkdir(publicationWorkspaceRoot, { recursive: true });
+
+ const resolvedSourceRef = await resolveGitCommitishIfExists(
+ options.plan.fixtureRepoPath,
+ options.action.sourceRef,
+ );
+ if (resolvedSourceRef === undefined) {
+ throw unresolvedFixtureRefError(
+ options.plan.fixtureRepoPath,
+ options.action.sourceRef,
+ );
+ }
+ const sourceMaterializedPaths = await materializeGitTree({
+ repoPath: options.plan.fixtureRepoPath,
+ ref: resolvedSourceRef,
+ workspaceRoot: sourceWorkspaceRoot,
+ });
+
+ const publicationFromRef = options.action.publicationFromRef;
+ const publicationMaterializedPaths = publicationFromRef === undefined
+ ? []
+ : await materializePublicationRef({
+ fixtureRepoPath: options.plan.fixtureRepoPath,
+ publicationFromRef,
+ publicationWorkspaceRoot,
+ });
+
+ return {
+ workspaceRoot,
+ sourceWorkspaceRoot,
+ sourceCommit: resolvedSourceRef,
+ publicationWorkspaceRoot,
+ sourceMaterializedPaths,
+ publicationMaterializedPaths,
+ };
+}
+
+async function materializePublicationRef(options: {
+ fixtureRepoPath: string;
+ publicationFromRef: string;
+ publicationWorkspaceRoot: string;
+}): Promise {
+ const resolvedPublicationRef = await resolveGitCommitishIfExists(
+ options.fixtureRepoPath,
+ options.publicationFromRef,
+ );
+ if (resolvedPublicationRef === undefined) {
+ throw unresolvedFixtureRefError(
+ options.fixtureRepoPath,
+ options.publicationFromRef,
+ );
+ }
+ return await materializeGitTree({
+ repoPath: options.fixtureRepoPath,
+ ref: resolvedPublicationRef,
+ workspaceRoot: options.publicationWorkspaceRoot,
+ });
+}
+
+async function runBranchPublicationCommands(options: {
+ root: string;
+ sourceWorkspaceRoot: string;
+ sourceRef: string;
+ sourceCommit: string;
+ publicationWorkspaceRoot: string;
+ action: FixtureBranchPublicationAction;
+}): Promise {
+ const results: FixtureCommandInvocationExecutionResult[] = [];
+
+ for (const invocation of options.action.invocations) {
+ const result = await runBranchPublicationCommandInvocation({
+ root: options.root,
+ sourceWorkspaceRoot: options.sourceWorkspaceRoot,
+ sourceRef: options.sourceRef,
+ sourceCommit: options.sourceCommit,
+ publicationWorkspaceRoot: options.publicationWorkspaceRoot,
+ invocation,
+ });
+ results.push(result);
+ if (!result.success) {
+ break;
+ }
+ }
+
+ const last = results.at(-1);
+ return {
+ kind: "branchPublication",
+ description: options.action.description,
+ success: results.every((result) => result.success),
+ code: last?.code ?? 1,
+ sourceWorkspaceRoot: options.sourceWorkspaceRoot,
+ publicationWorkspaceRoot: options.publicationWorkspaceRoot,
+ publicationBranch: options.action.publicationBranch,
+ commands: results,
+ stdout: results.map((result) => result.stdout).join(""),
+ stderr: results.map((result) => result.stderr).join(""),
+ };
+}
+
+async function runBranchPublicationCommandInvocation(options: {
+ root: string;
+ sourceWorkspaceRoot: string;
+ sourceRef: string;
+ sourceCommit: string;
+ publicationWorkspaceRoot: string;
+ invocation: FixtureBranchPublicationCommandInvocation;
+}): Promise {
+ const commandCwd = dirname(options.sourceWorkspaceRoot);
+ const logDir = join(commandCwd, "runtime-logs");
+ const command = [
+ "deno",
+ "run",
+ "--allow-read",
+ "--allow-write",
+ "--allow-run=git",
+ "--allow-env",
+ join(options.root, "src/main.ts"),
+ ...options.invocation.argv.map((arg) =>
+ substituteBranchPublicationArg({
+ arg,
+ sourceWorkspaceRoot: options.sourceWorkspaceRoot,
+ sourceRef: options.sourceRef,
+ sourceCommit: options.sourceCommit,
+ publicationWorkspaceRoot: options.publicationWorkspaceRoot,
+ })
+ ),
+ ];
+ const output = await new Deno.Command("deno", {
+ cwd: commandCwd,
+ args: command.slice(1),
+ env: {
+ WEAVE_GENERATED_AT: FIXTURE_GENERATED_AT,
+ WEAVE_LOG_DIR: logDir,
+ },
+ stdout: "piped",
+ stderr: "piped",
+ }).output();
+
+ return {
+ command,
+ cwd: commandCwd,
+ success: output.success,
+ code: output.code,
+ stdout: new TextDecoder().decode(output.stdout),
+ stderr: new TextDecoder().decode(output.stderr),
+ };
+}
+
+function substituteBranchPublicationArg(options: {
+ arg: string;
+ sourceWorkspaceRoot: string;
+ sourceRef: string;
+ sourceCommit: string;
+ publicationWorkspaceRoot: string;
+}): string {
+ return options.arg
+ .replaceAll("{sourceRoot}", options.sourceWorkspaceRoot)
+ .replaceAll("{sourceRef}", options.sourceRef)
+ .replaceAll("{sourceCommit}", options.sourceCommit)
+ .replaceAll("{publicationRoot}", options.publicationWorkspaceRoot);
+}
+
+async function applyFixtureFileOperation(options: {
+ assetRoot: string;
+ workspaceRoot: string;
+ action: FixtureFileOperationAction;
+}): Promise {
+ const stagedSources = await stageFixtureAssetSources({
+ assetRoot: options.assetRoot,
+ workspaceRoot: options.workspaceRoot,
+ sources: options.action.sources,
+ });
+ if (stagedSources.missingAssets.length > 0) {
+ return {
+ kind: "fileOperation",
+ description: options.action.description,
+ success: false,
+ files: [],
+ missingAssets: stagedSources.missingAssets,
+ };
+ }
+
+ for (const patch of options.action.inventoryPatches) {
+ await applyFixtureInventoryPatch({
+ workspaceRoot: options.workspaceRoot,
+ patch,
+ });
+ }
+
+ return {
+ kind: "fileOperation",
+ description: options.action.description,
+ success: true,
+ files: stagedSources.files,
+ missingAssets: [],
+ };
+}
+
+async function stageFixtureAssetSources(options: {
+ assetRoot: string;
+ workspaceRoot: string;
+ sources: readonly FixtureFileOperationSource[];
+}): Promise<{
+ files: FixtureFileOperationAppliedFile[];
+ missingAssets: FixtureFileOperationMissingAsset[];
+}> {
+ const pendingFiles: Array<
+ FixtureFileOperationAppliedFile & {
+ absoluteTargetPath: string;
+ contents: Uint8Array;
+ }
+ > = [];
+ const missingAssets: FixtureFileOperationMissingAsset[] = [];
+ const seenTargets = new Set();
+
+ for (const source of options.sources) {
+ const targetPath = normalizeGitTreePath(source.path);
+ const assetPath = normalizeGitTreePath(source.assetPath);
+ if (seenTargets.has(targetPath)) {
+ throw new Error(
+ `Duplicate fixture file-operation target path: ${targetPath}`,
+ );
+ }
+ seenTargets.add(targetPath);
+
+ const absoluteAssetPath = join(options.assetRoot, assetPath);
+ const absoluteTargetPath = join(options.workspaceRoot, targetPath);
+ try {
+ const contents = await Deno.readFile(absoluteAssetPath);
+ pendingFiles.push({
+ path: targetPath,
+ assetPath,
+ provenance: source.provenance,
+ bytes: contents.byteLength,
+ absoluteTargetPath,
+ contents,
+ });
+ } catch (error) {
+ if (error instanceof Deno.errors.NotFound) {
+ missingAssets.push({
+ path: targetPath,
+ assetPath,
+ absolutePath: absoluteAssetPath,
+ });
+ continue;
+ }
+ throw error;
+ }
+ }
+
+ if (missingAssets.length > 0) {
+ return {
+ files: [],
+ missingAssets,
+ };
+ }
+
+ for (const file of pendingFiles) {
+ await Deno.mkdir(dirname(file.absoluteTargetPath), { recursive: true });
+ await Deno.writeFile(file.absoluteTargetPath, file.contents);
+ }
+
+ return {
+ files: pendingFiles.map((
+ { absoluteTargetPath: _absoluteTargetPath, contents: _contents, ...file },
+ ) => file),
+ missingAssets,
+ };
+}
+
+async function applyFixtureInventoryPatch(options: {
+ workspaceRoot: string;
+ patch: FixtureInventoryPatch;
+}): Promise {
+ switch (options.patch.kind) {
+ case "resourcePageDefinition":
+ await applyResourcePageDefinitionInventoryPatch(options);
+ return;
+ }
+}
+
+async function applyResourcePageDefinitionInventoryPatch(options: {
+ workspaceRoot: string;
+ patch: FixtureResourcePageDefinitionInventoryPatch;
+}): Promise {
+ const inventoryPath = normalizeGitTreePath(options.patch.inventoryPath);
+ const absoluteInventoryPath = join(options.workspaceRoot, inventoryPath);
+ const existing = await Deno.readTextFile(absoluteInventoryPath);
+ if (
+ existing.includes(
+ `sflo:hasResourcePageDefinition <${options.patch.pageDefinitionPath}>`,
+ ) &&
+ existing.includes(`<${options.patch.pageDefinitionPath}>`)
+ ) {
+ return;
+ }
+
+ const block = renderResourcePageDefinitionInventoryPatch(options.patch);
+ await Deno.writeTextFile(
+ absoluteInventoryPath,
+ `${existing.trimEnd()}\n\n${block}\n`,
+ );
+}
+
+function renderResourcePageDefinitionInventoryPatch(
+ patch: FixtureResourcePageDefinitionInventoryPatch,
+): string {
+ const assetBundleLink = patch.assetBundlePath === undefined ? "" : ` ;
+ sflo:hasKnopAssetBundle <${patch.assetBundlePath}>`;
+ const assetBundleBlock = patch.assetBundlePath === undefined
+ ? ""
+ : `\n\n<${patch.assetBundlePath}> a sflo:KnopAssetBundle .`;
+
+ return `<${patch.knopPath}> sflo:hasResourcePageDefinition <${patch.pageDefinitionPath}>${assetBundleLink} .
+
+<${patch.pageDefinitionPath}> a sflo:ResourcePageDefinition, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sflo:workingLocalRelativePath "${patch.pageDefinitionFilePath}" .
+
+<${patch.pageDefinitionFilePath}> a sflo:LocatedFile, sflo:RdfDocument .${assetBundleBlock}`;
+}
+
+async function maybeUpdateFixtureBranch(options: {
+ fixtureRepoPath: string;
+ workspaceRoot: string;
+ targetRef: string;
+ parentRef?: string;
+ dryRun: boolean;
+ operation: FixtureTransitionOperationResult;
+ validation: JsonReport;
+ message: string;
+}): Promise {
+ const branchRef = toLocalBranchRef(options.targetRef);
+
+ if (options.dryRun) {
+ return {
+ dryRun: true,
+ updated: false,
+ targetRef: options.targetRef,
+ branchRef,
+ localOnly: true,
+ reason: "dry run requested",
+ };
+ }
+
+ if (!options.operation.success) {
+ return {
+ dryRun: false,
+ updated: false,
+ targetRef: options.targetRef,
+ branchRef,
+ localOnly: true,
+ reason: `${options.operation.kind} failed`,
+ };
+ }
+
+ const failingGuardrail = findFailingGeneratedOutputGuardrail(
+ options.validation,
+ );
+ if (failingGuardrail !== undefined) {
+ return {
+ dryRun: false,
+ updated: false,
+ targetRef: options.targetRef,
+ branchRef,
+ localOnly: true,
+ reason: `generated-output guardrail failed: ${failingGuardrail.message}`,
+ };
+ }
+
+ return await updateFixtureBranchFromWorkspace({
+ fixtureRepoPath: options.fixtureRepoPath,
+ workspaceRoot: options.workspaceRoot,
+ targetRef: options.targetRef,
+ parentRef: options.parentRef,
+ message: options.message,
+ });
+}
+
+async function maybeFastForwardPublicationBranch(options: {
+ fixtureRepoPath: string;
+ publicationBranch: string;
+ branchUpdate: FixtureBranchUpdateResult;
+}): Promise {
+ if (!options.branchUpdate.updated) {
+ return {
+ updated: false,
+ branch: options.publicationBranch,
+ reason: "fixture checkpoint branch was not updated",
+ };
+ }
+
+ await assertValidBranchName(
+ options.fixtureRepoPath,
+ options.publicationBranch,
+ );
+ const branchRef = toLocalBranchRef(options.publicationBranch);
+ const currentSha = await resolveGitCommitishIfExists(
+ options.fixtureRepoPath,
+ options.publicationBranch,
+ );
+ if (currentSha !== undefined) {
+ const ancestor = await runGit(options.fixtureRepoPath, [
+ "merge-base",
+ "--is-ancestor",
+ currentSha,
+ options.branchUpdate.commitSha,
+ ]);
+ if (!ancestor.success) {
+ throw new Error(
+ `Refusing to move ${options.publicationBranch}; current ${currentSha} is not an ancestor of ${options.branchUpdate.commitSha}`,
+ );
+ }
+ }
+
+ const updateResult = await runGit(options.fixtureRepoPath, [
+ "update-ref",
+ branchRef,
+ options.branchUpdate.commitSha,
+ ]);
+ if (!updateResult.success) {
+ throw new Error(
+ `Failed to update ${branchRef}: ${updateResult.stderr.trim()}`,
+ );
+ }
+
+ return {
+ updated: true,
+ branch: options.publicationBranch,
+ branchRef,
+ commitSha: options.branchUpdate.commitSha,
+ };
+}
+
+export async function updateFixtureBranchFromWorkspace(
+ options: UpdateFixtureBranchOptions,
+): Promise {
+ const branchRef = toLocalBranchRef(options.targetRef);
+ await assertValidBranchName(options.fixtureRepoPath, options.targetRef);
+
+ const resolvedParentRef = options.parentRef ?? options.targetRef;
+ const parentSha = await resolveGitCommitishIfExists(
+ options.fixtureRepoPath,
+ resolvedParentRef,
+ );
+ const treeSha = await writeWorkspaceTreeToFixtureRepo({
+ fixtureRepoPath: options.fixtureRepoPath,
+ workspaceRoot: options.workspaceRoot,
+ });
+ const commitArgs = [
+ "commit-tree",
+ treeSha,
+ ...(parentSha === undefined ? [] : ["-p", parentSha]),
+ "-m",
+ options.message,
+ ];
+ const commitResult = await runGit(options.fixtureRepoPath, commitArgs, {
+ env: gitAuthorEnv(),
+ });
+ if (!commitResult.success) {
+ throw new Error(
+ `Failed to create fixture branch commit: ${commitResult.stderr.trim()}`,
+ );
+ }
+
+ const commitSha = commitResult.stdout.trim();
+ const updateResult = await runGit(options.fixtureRepoPath, [
+ "update-ref",
+ branchRef,
+ commitSha,
+ ]);
+ if (!updateResult.success) {
+ throw new Error(
+ `Failed to update ${branchRef}: ${updateResult.stderr.trim()}`,
+ );
+ }
+
+ return {
+ dryRun: false,
+ updated: true,
+ targetRef: options.targetRef,
+ branchRef,
+ localOnly: true,
+ commitSha,
+ treeSha,
+ ...(parentSha === undefined ? {} : {
+ parentRef: resolvedParentRef,
+ parentSha,
+ }),
+ pushed: false,
+ };
+}
+
+async function writeWorkspaceTreeToFixtureRepo(options: {
+ fixtureRepoPath: string;
+ workspaceRoot: string;
+}): Promise {
+ const indexFile = await Deno.makeTempFile({
+ prefix: "weave-fixture-index-",
+ });
+ const excludesFile = await Deno.makeTempFile({
+ prefix: "weave-fixture-excludes-",
+ });
+ await Deno.writeTextFile(excludesFile, ".git/\n.weave/\n");
+ const gitDir = join(options.fixtureRepoPath, ".git");
+ const env = { GIT_INDEX_FILE: indexFile };
+ const gitArgs = [
+ "-c",
+ `core.excludesFile=${excludesFile}`,
+ "--git-dir",
+ gitDir,
+ "--work-tree",
+ options.workspaceRoot,
+ ];
+
+ try {
+ await runRequiredGit(options.fixtureRepoPath, [
+ ...gitArgs,
+ "read-tree",
+ "--empty",
+ ], env);
+ await runRequiredGit(options.fixtureRepoPath, [
+ ...gitArgs,
+ "add",
+ "-A",
+ "--",
+ ".",
+ ], env);
+ const result = await runGit(options.fixtureRepoPath, [
+ ...gitArgs,
+ "write-tree",
+ ], { env });
+ if (!result.success) {
+ throw new Error(
+ `Failed to write generated fixture tree: ${result.stderr.trim()}`,
+ );
+ }
+ return result.stdout.trim();
+ } finally {
+ await Deno.remove(indexFile).catch(() => {});
+ await Deno.remove(excludesFile).catch(() => {});
+ }
+}
+
+async function runRequiredGit(
+ cwd: string,
+ args: readonly string[],
+ env?: Record,
+): Promise {
+ const result = await runGit(cwd, args, { env });
+ if (!result.success) {
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim()}`);
+ }
+}
+
+async function validateFixtureTransitionWorkspace(options: {
+ fixtureRepoPath: string;
+ manifestPath: string;
+ workspaceRoot: string;
+ fallbackFromRef: string;
+ fallbackToRef: string;
+}): Promise {
+ const manifest = await readManifestSource(options.manifestPath);
+ const transitionCase = selectTransitionCase(manifest.document);
+ const fromRef = options.fallbackFromRef;
+ const toRef = options.fallbackToRef;
+ const fileExpectations = transitionCase.hasFileExpectation ?? [];
+ const actualBytesByPath = new Map();
+ const checks: CheckRecord[] = [];
+ const resolvedFromRef = await resolveGitCommitishIfExists(
+ options.fixtureRepoPath,
+ fromRef,
+ );
+ const resolvedToRef = await resolveGitCommitishIfExists(
+ options.fixtureRepoPath,
+ toRef,
+ );
+
+ if (resolvedFromRef === undefined) {
+ checks.push(gitRefUnresolvedRecord({
+ ref: fromRef,
+ role: "fromRef",
+ fixtureRepoPath: options.fixtureRepoPath,
+ }));
+ }
+ if (resolvedToRef === undefined) {
+ checks.push(gitRefUnresolvedRecord({
+ ref: toRef,
+ role: "toRef",
+ fixtureRepoPath: options.fixtureRepoPath,
+ }));
+ }
+
+ for (const fileExpectation of fileExpectations) {
+ checks.push(
+ ...await evaluateWorkspaceFileExpectation({
+ fixtureRepoPath: options.fixtureRepoPath,
+ fromRef: resolvedFromRef,
+ toRef: resolvedToRef,
+ expectedRefLabel: toRef,
+ workspaceRoot: options.workspaceRoot,
+ transitionCase,
+ fileExpectation,
+ actualBytesByPath,
+ }),
+ );
+ }
+
+ checks.push(
+ ...await evaluateWorkspaceRdfExpectations({
+ workspaceRoot: options.workspaceRoot,
+ transitionCase,
+ fileExpectations,
+ actualBytesByPath,
+ }),
+ );
+ checks.push(
+ ...await evaluateGeneratedOutputGuardrails(options.workspaceRoot),
+ );
+
+ const summary = countCheckStatuses(checks);
+ return {
+ manifestPath: options.manifestPath,
+ caseId: transitionCase.resolvedId ?? transitionCase.id ?? "(anonymous)",
+ fixtureRepoPath: options.fixtureRepoPath,
+ status: deriveReportStatus(checks),
+ summary,
+ checks,
+ };
+}
+
+async function evaluateWorkspaceFileExpectation(options: {
+ fixtureRepoPath: string;
+ fromRef: string | undefined;
+ toRef: string | undefined;
+ expectedRefLabel: string;
+ workspaceRoot: string;
+ transitionCase: TransitionCase;
+ fileExpectation: FileExpectation;
+ actualBytesByPath: Map;
+}): Promise {
+ const path = options.fileExpectation.path;
+ const changeType = options.fileExpectation.changeType as
+ | FileChangeType
+ | undefined;
+ const compareMode = options.fileExpectation.compareMode;
+
+ if (path === undefined || changeType === undefined) {
+ return [{
+ kind: "file_presence",
+ status: "error",
+ code: CHECK_CODES.FILE_PRESENCE_MISMATCH,
+ message: "File expectation is missing path or changeType.",
+ path,
+ }];
+ }
+
+ const safePath = normalizeGitTreePath(path);
+ const fromBytes = options.fromRef === undefined
+ ? undefined
+ : await readGitBlobIfExists(
+ options.fixtureRepoPath,
+ options.fromRef,
+ safePath,
+ );
+ const expectedBytes = options.toRef === undefined
+ ? undefined
+ : await readGitBlobIfExists(
+ options.fixtureRepoPath,
+ options.toRef,
+ safePath,
+ );
+ const actualBytes = await readWorkspaceFileIfExists(
+ options.workspaceRoot,
+ safePath,
+ );
+ options.actualBytesByPath.set(safePath, actualBytes);
+
+ const checks: CheckRecord[] = [
+ filePresenceRecord({
+ path: safePath,
+ changeType,
+ fromExists: fromBytes !== undefined,
+ actualExists: actualBytes !== undefined,
+ }),
+ ];
+
+ if (actualBytes === undefined || expectedBytes === undefined) {
+ if (actualBytes !== undefined && expectedBytes === undefined) {
+ checks.push({
+ kind: compareMode === "rdfCanonical" ? "rdf_compare" : "file_compare",
+ status: "fail",
+ code: compareMode === "rdfCanonical"
+ ? CHECK_CODES.RDF_GRAPH_MISMATCH
+ : CHECK_CODES.FILE_CONTENT_MISMATCH,
+ message:
+ `Expected fixture ref ${options.expectedRefLabel} to contain ${safePath} for ${compareMode} comparison.`,
+ path: safePath,
+ });
+ }
+ return checks;
+ }
+
+ if (compareMode === "bytes") {
+ checks.push(fileCompareRecord({
+ path: safePath,
+ compareMode,
+ contentsEqual: compareBytes(actualBytes, expectedBytes),
+ }));
+ return checks;
+ }
+
+ if (compareMode === "text") {
+ try {
+ checks.push(fileCompareRecord({
+ path: safePath,
+ compareMode,
+ contentsEqual: compareTextContents(actualBytes, expectedBytes),
+ }));
+ } catch (error) {
+ if (error instanceof TextDecodeError) {
+ checks.push({
+ kind: "file_compare",
+ status: "error",
+ code: CHECK_CODES.TEXT_DECODE_ERROR,
+ message: error.message,
+ path: safePath,
+ });
+ return checks;
+ }
+ throw error;
+ }
+ return checks;
+ }
+
+ if (compareMode === "rdfCanonical") {
+ const rdfExpectation = resolveTargetRdfExpectation(
+ options.fileExpectation,
+ options.transitionCase.hasRdfExpectation ?? [],
+ );
+ try {
+ const contentsEqual = await compareRdfContent({
+ left: actualBytes,
+ right: expectedBytes,
+ path: safePath,
+ ignorePredicates: rdfExpectation?.ignorePredicate,
+ });
+ checks.push({
+ kind: "rdf_compare",
+ status: contentsEqual ? "pass" : "fail",
+ code: contentsEqual
+ ? CHECK_CODES.RDF_GRAPH_OK
+ : CHECK_CODES.RDF_GRAPH_MISMATCH,
+ message:
+ `Expected workspace contents to match ${options.expectedRefLabel} under rdfCanonical comparison.`,
+ path: safePath,
+ });
+ } catch (error) {
+ if (error instanceof RdfCompareError) {
+ checks.push({
+ kind: "rdf_compare",
+ status: "error",
+ code: error.code,
+ message: error.message,
+ path: safePath,
+ });
+ return checks;
+ }
+ throw error;
+ }
+ return checks;
+ }
+
+ if (compareMode !== undefined) {
+ checks.push({
+ kind: "file_compare",
+ status: "error",
+ code: CHECK_CODES.FILE_CONTENT_MISMATCH,
+ message: `Unsupported compare mode for file expectation: ${compareMode}`,
+ path: safePath,
+ });
+ }
+
+ return checks;
+}
+
+async function evaluateWorkspaceRdfExpectations(options: {
+ workspaceRoot: string;
+ transitionCase: TransitionCase;
+ fileExpectations: readonly FileExpectation[];
+ actualBytesByPath: Map;
+}): Promise {
+ const checks: CheckRecord[] = [];
+
+ for (const rdfExpectation of options.transitionCase.hasRdfExpectation ?? []) {
+ const fileExpectation = resolveTargetFileExpectation(
+ rdfExpectation,
+ options.fileExpectations,
+ );
+ const path = fileExpectation?.path;
+ if (
+ fileExpectation === undefined || path === undefined ||
+ fileExpectation.compareMode !== "rdfCanonical"
+ ) {
+ continue;
+ }
+
+ const safePath = normalizeGitTreePath(path);
+ const actualBytes = options.actualBytesByPath.get(safePath) ??
+ await readWorkspaceFileIfExists(options.workspaceRoot, safePath);
+ if (actualBytes === undefined) {
+ continue;
+ }
+
+ for (const askAssertion of rdfExpectation.hasAskAssertion ?? []) {
+ checks.push(
+ await evaluateWorkspaceSparqlAskAssertion({
+ path: safePath,
+ actualBytes,
+ askAssertion,
+ }),
+ );
+ }
+ }
+
+ return checks;
+}
+
+async function evaluateWorkspaceSparqlAskAssertion(options: {
+ path: string;
+ actualBytes: Uint8Array;
+ askAssertion: SparqlAskAssertion;
+}): Promise {
+ const assertionId = options.askAssertion.id ??
+ options.askAssertion.resolvedId;
+
+ if (
+ typeof options.askAssertion.query !== "string" ||
+ options.askAssertion.query === ""
+ ) {
+ return {
+ kind: "sparql_ask",
+ status: "error",
+ code: CHECK_CODES.SPARQL_QUERY_ERROR,
+ message: "SPARQL ASK assertion is missing a query string.",
+ path: options.path,
+ assertionId,
+ };
+ }
+
+ if (typeof options.askAssertion.expectedBoolean !== "boolean") {
+ return {
+ kind: "sparql_ask",
+ status: "error",
+ code: CHECK_CODES.SPARQL_QUERY_ERROR,
+ message: "SPARQL ASK assertion is missing expectedBoolean.",
+ path: options.path,
+ assertionId,
+ };
+ }
+
+ try {
+ const actual = await runAskAssertion({
+ dataset: options.actualBytes,
+ path: options.path,
+ query: options.askAssertion.query,
+ });
+ const passed = actual === options.askAssertion.expectedBoolean;
+ return {
+ kind: "sparql_ask",
+ status: passed ? "pass" : "fail",
+ code: passed
+ ? CHECK_CODES.SPARQL_ASK_OK
+ : CHECK_CODES.SPARQL_ASK_MISMATCH,
+ message: passed
+ ? "SPARQL ASK result matched expectedBoolean."
+ : `Expected SPARQL ASK to return ${options.askAssertion.expectedBoolean}, but it returned ${actual}.`,
+ path: options.path,
+ assertionId,
+ };
+ } catch (error) {
+ if (error instanceof RdfCompareError || error instanceof SparqlAskError) {
+ return {
+ kind: "sparql_ask",
+ status: "error",
+ code: error.code,
+ message: error.message,
+ path: options.path,
+ assertionId,
+ };
+ }
+ throw error;
+ }
+}
+
+export async function evaluateGeneratedOutputGuardrails(
+ workspaceRoot: string,
+): Promise {
+ const paths = await listWorkspaceFiles(workspaceRoot);
+ const meshSupportRoots = findMeshSupportRoots(paths);
+ const progressionRoots = meshSupportRoots.length === 0
+ ? [""]
+ : meshSupportRoots;
+ const checks = [
+ await evaluateCanonicalNamespaceGuardrail(
+ workspaceRoot,
+ paths,
+ ),
+ ];
+
+ for (const meshSupportRoot of progressionRoots) {
+ checks.push(
+ await evaluateInventoryOwnedProgressionGuardrail(
+ workspaceRoot,
+ meshSupportRoot,
+ ),
+ );
+ checks.push(
+ await evaluateMeshInventoryMetadataProgressionGuardrail(
+ workspaceRoot,
+ paths,
+ meshSupportRoot,
+ ),
+ );
+ }
+
+ return checks;
+}
+
+async function evaluateCanonicalNamespaceGuardrail(
+ workspaceRoot: string,
+ paths: readonly string[],
+): Promise {
+ for (const path of paths.filter(isRdfOutputPath)) {
+ const contents = await Deno.readTextFile(join(workspaceRoot, path));
+ if (contents.includes(OLD_SFLO_NAMESPACE)) {
+ return guardrailRecord({
+ assertionId: "generated-output.guardrail.canonicalNamespace",
+ passed: false,
+ path,
+ message:
+ `Generated RDF must use ${CANONICAL_SFLO_NAMESPACE}; found retired namespace ${OLD_SFLO_NAMESPACE}.`,
+ });
+ }
+ }
+
+ return guardrailRecord({
+ assertionId: "generated-output.guardrail.canonicalNamespace",
+ passed: true,
+ message:
+ `Generated RDF uses the canonical sflo namespace ${CANONICAL_SFLO_NAMESPACE}.`,
+ });
+}
+
+async function evaluateInventoryOwnedProgressionGuardrail(
+ workspaceRoot: string,
+ meshSupportRoot: string,
+): Promise {
+ const inventoryPath = meshSupportPath(
+ meshSupportRoot,
+ MESH_INVENTORY_FILE_PATH,
+ );
+ const inventory = await readWorkspaceTextFileIfExists(
+ workspaceRoot,
+ inventoryPath,
+ );
+ if (inventory === undefined) {
+ return guardrailRecord({
+ assertionId: "generated-output.guardrail.inventoryOwnedProgression",
+ passed: true,
+ path: inventoryPath,
+ message:
+ "MeshInventory current file is absent at this mesh root; no inventory-owned progression facts found.",
+ });
+ }
+
+ const hasInventoryOwnedProgression =
+ findStaleInventoryProgressionBlock(inventory) !== undefined;
+ const metadataPath = meshSupportPath(
+ meshSupportRoot,
+ MESH_METADATA_FILE_PATH,
+ );
+ const metadata = await readWorkspaceTextFileIfExists(
+ workspaceRoot,
+ metadataPath,
+ );
+ const passed = !hasInventoryOwnedProgression ||
+ hasMeshInventoryMetadataProgressionAnchor(metadata);
+
+ return guardrailRecord({
+ assertionId: "generated-output.guardrail.inventoryOwnedProgression",
+ passed,
+ path: inventoryPath,
+ message: !hasInventoryOwnedProgression
+ ? "MeshInventory progression facts are not owned by _mesh/_inventory/inventory.ttl."
+ : passed
+ ? `MeshInventory progression facts are anchored in ${metadataPath}.`
+ : `Stale MeshInventory progression facts found in ${inventoryPath} without a matching ${metadataPath} anchor.`,
+ });
+}
+
+function findStaleInventoryProgressionBlock(
+ inventory: string,
+): string | undefined {
+ const progressionPredicates = [
+ "hasArtifactHistory",
+ "currentArtifactHistory",
+ "nextHistoryOrdinal",
+ "latestHistoricalState",
+ "nextStateOrdinal",
+ ] as const;
+
+ return inventory.split(/\n\s*\n/).find((block) => {
+ const trimmed = block.trimStart();
+ if (
+ !trimmed.startsWith("<_mesh/_inventory>") &&
+ !trimmed.startsWith("<_mesh/_inventory/_history")
+ ) {
+ return false;
+ }
+
+ return progressionPredicates.some((predicate) =>
+ block.includes(`sflo:${predicate}`) ||
+ block.includes(`<${CANONICAL_SFLO_NAMESPACE}${predicate}>`)
+ );
+ });
+}
+
+async function evaluateMeshInventoryMetadataProgressionGuardrail(
+ workspaceRoot: string,
+ paths: readonly string[],
+ meshSupportRoot: string,
+): Promise {
+ const historyPrefix = meshSupportPath(
+ meshSupportRoot,
+ MESH_INVENTORY_HISTORY_PREFIX,
+ );
+ const hasMeshInventoryHistoryOutput = paths.some((path) =>
+ path.startsWith(historyPrefix)
+ );
+ const metadataPath = meshSupportPath(
+ meshSupportRoot,
+ MESH_METADATA_FILE_PATH,
+ );
+ if (!hasMeshInventoryHistoryOutput) {
+ return guardrailRecord({
+ assertionId:
+ "generated-output.guardrail.meshInventoryMetadataProgression",
+ passed: true,
+ path: metadataPath,
+ message:
+ "No MeshInventory history output is present; metadata progression facts are not required.",
+ });
+ }
+
+ const metadata = await readWorkspaceTextFileIfExists(
+ workspaceRoot,
+ metadataPath,
+ );
+ const passed = hasMeshInventoryMetadataProgressionAnchor(metadata);
+
+ return guardrailRecord({
+ assertionId: "generated-output.guardrail.meshInventoryMetadataProgression",
+ passed,
+ path: metadataPath,
+ message: passed
+ ? `MeshInventory progression facts are anchored in ${metadataPath}.`
+ : `MeshInventory history output exists, but ${metadataPath} does not anchor current/latest MeshInventory progression.`,
+ });
+}
+
+function hasMeshInventoryMetadataProgressionAnchor(
+ metadata: string | undefined,
+): boolean {
+ return metadata !== undefined &&
+ metadata.includes(
+ "sflo:currentArtifactHistory <_mesh/_inventory/_history",
+ ) &&
+ metadata.includes("sflo:latestHistoricalState <_mesh/_inventory/_history");
+}
+
+function guardrailRecord(options: {
+ assertionId: string;
+ passed: boolean;
+ message: string;
+ path?: string;
+}): CheckRecord {
+ return {
+ kind: "setup",
+ status: options.passed ? "pass" : "fail",
+ code: options.passed
+ ? CHECK_CODES.FILE_CONTENT_OK
+ : CHECK_CODES.FILE_CONTENT_MISMATCH,
+ message: options.message,
+ path: options.path,
+ assertionId: options.assertionId,
+ };
+}
+
+function findMeshSupportRoots(paths: readonly string[]): string[] {
+ const roots = new Set();
+ const rootMarker = "_mesh/";
+ const nestedMarker = "/_mesh/";
+
+ for (const path of paths) {
+ if (path.startsWith(rootMarker)) {
+ roots.add("");
+ continue;
+ }
+
+ const markerIndex = path.indexOf(nestedMarker);
+ if (markerIndex >= 0) {
+ roots.add(path.slice(0, markerIndex));
+ }
+ }
+
+ return [...roots].sort((left, right) => left.localeCompare(right));
+}
+
+function meshSupportPath(meshSupportRoot: string, path: string): string {
+ return meshSupportRoot.length === 0
+ ? path
+ : pathPosix.join(meshSupportRoot, path);
+}
+
+function gitRefUnresolvedRecord(options: {
+ ref: string;
+ role: "fromRef" | "toRef";
+ fixtureRepoPath: string;
+}): CheckRecord {
+ return {
+ kind: "setup",
+ status: "fail",
+ code: CHECK_CODES.GIT_REF_UNRESOLVED,
+ message:
+ `Could not resolve manifest ${options.role} ${options.ref} in ${options.fixtureRepoPath}; reporting fixture drift without blocking generated-output guardrails.`,
+ };
+}
+
+function findFailingGeneratedOutputGuardrail(
+ validation: JsonReport,
+): CheckRecord | undefined {
+ return validation.checks.find((check) =>
+ check.kind === "setup" &&
+ check.status !== "pass" &&
+ check.assertionId?.startsWith("generated-output.guardrail.") === true
+ );
+}
+
+function filePresenceRecord(options: {
+ path: string;
+ changeType: FileChangeType;
+ fromExists: boolean;
+ actualExists: boolean;
+}): CheckRecord {
+ const presence = evaluatePresenceExpectation(
+ options.changeType,
+ options.fromExists,
+ options.actualExists,
+ );
+ return {
+ kind: "file_presence",
+ status: presence.passed ? "pass" : "fail",
+ code: presence.passed
+ ? CHECK_CODES.FILE_PRESENCE_OK
+ : CHECK_CODES.FILE_PRESENCE_MISMATCH,
+ message: presence.reason,
+ path: options.path,
+ };
+}
+
+function fileCompareRecord(options: {
+ path: string;
+ compareMode: string;
+ contentsEqual: boolean;
+}): CheckRecord {
+ return {
+ kind: "file_compare",
+ status: options.contentsEqual ? "pass" : "fail",
+ code: options.contentsEqual
+ ? CHECK_CODES.FILE_CONTENT_OK
+ : CHECK_CODES.FILE_CONTENT_MISMATCH,
+ message:
+ `Expected workspace contents to match toRef under ${options.compareMode} comparison.`,
+ path: options.path,
+ };
+}
+
+function resolveTargetFileExpectation(
+ rdfExpectation: RdfExpectation,
+ fileExpectations: readonly FileExpectation[],
+): FileExpectation | undefined {
+ const target = rdfExpectation.targetsFileExpectation;
+ return fileExpectations.find((candidate) =>
+ candidate.id === target || candidate.resolvedId === target
+ );
+}
+
+function resolveTargetRdfExpectation(
+ fileExpectation: FileExpectation,
+ rdfExpectations: readonly RdfExpectation[],
+): RdfExpectation | undefined {
+ return rdfExpectations.find((candidate) =>
+ candidate.targetsFileExpectation === fileExpectation.id ||
+ candidate.targetsFileExpectation === fileExpectation.resolvedId
+ );
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
+
+function parseScenarioId(value: string): FixtureScenarioId {
+ if (
+ value === "alice-bio" || value === "sidecar-fantasy-rules" ||
+ value === "branch-fantasy-rules"
+ ) {
+ return value;
+ }
+ throw new Error(`Unsupported fixture scenario: ${value}`);
+}
+
+function parsePlanFormat(value: string): FixturePlanFormat {
+ if (value === "text" || value === "json") {
+ return value;
+ }
+ throw new Error(`Unsupported fixture plan format: ${value}`);
+}
+
+async function ensureEmptyWorkspaceRoot(path: string): Promise {
+ try {
+ const stat = await Deno.stat(path);
+ if (!stat.isDirectory) {
+ throw new Error(`workspace root is not a directory: ${path}`);
+ }
+ for await (const _entry of Deno.readDir(path)) {
+ throw new Error(
+ `workspace root must be empty before materialization: ${path}`,
+ );
+ }
+ } catch (error) {
+ if (error instanceof Deno.errors.NotFound) {
+ await Deno.mkdir(path, { recursive: true });
+ return;
+ }
+ throw error;
+ }
+}
+
+async function resolveGitCommitishIfExists(
+ repoPath: string,
+ ref: string,
+): Promise {
+ const candidates = fixtureRefCandidates(ref);
+ for (const candidate of candidates) {
+ const result = await runGit(repoPath, [
+ "rev-parse",
+ "--verify",
+ "--quiet",
+ `${candidate}^{commit}`,
+ ]);
+ if (result.success) {
+ return result.stdout.trim();
+ }
+ }
+ return undefined;
+}
+
+function fixtureRefCandidates(ref: string): string[] {
+ return [ref, `origin/${ref}`];
+}
+
+function unresolvedFixtureRefError(repoPath: string, ref: string): Error {
+ return new Error(
+ `Failed to resolve fixture ref ${ref} in ${repoPath}; checked ${
+ fixtureRefCandidates(ref).join(", ")
+ }.`,
+ );
+}
+
+async function assertValidBranchName(
+ repoPath: string,
+ branchName: string,
+): Promise {
+ const result = await runGit(repoPath, [
+ "check-ref-format",
+ "--branch",
+ branchName,
+ ]);
+ if (!result.success) {
+ throw new Error(`Invalid fixture branch name: ${branchName}`);
+ }
+}
+
+function toLocalBranchRef(branchName: string): string {
+ return `refs/heads/${branchName}`;
+}
+
+function gitAuthorEnv(): Record {
+ return {
+ GIT_AUTHOR_NAME: "Weave fixture ladder",
+ GIT_AUTHOR_EMAIL: "weave-fixture-ladder@example.invalid",
+ GIT_COMMITTER_NAME: "Weave fixture ladder",
+ GIT_COMMITTER_EMAIL: "weave-fixture-ladder@example.invalid",
+ };
+}
+
+async function materializeGitTree(options: {
+ repoPath: string;
+ ref: string;
+ workspaceRoot: string;
+}): Promise {
+ const listResult = await runGit(options.repoPath, [
+ "ls-tree",
+ "-r",
+ "--name-only",
+ "-z",
+ options.ref,
+ ]);
+ if (!listResult.success) {
+ throw new Error(
+ `Failed to list fixture files for ${options.ref}: ${listResult.stderr.trim()}`,
+ );
+ }
+
+ const paths = listResult.stdout.split("\0").filter((path) => path.length > 0);
+ for (const path of paths) {
+ const safePath = normalizeGitTreePath(path);
+ const absolutePath = join(options.workspaceRoot, safePath);
+ await Deno.mkdir(dirname(absolutePath), { recursive: true });
+ const fileResult = await runGitBytes(options.repoPath, [
+ "show",
+ `${options.ref}:${safePath}`,
+ ]);
+ if (!fileResult.success) {
+ throw new Error(
+ `Failed to read fixture file ${options.ref}:${safePath}: ${fileResult.stderr.trim()}`,
+ );
+ }
+ await Deno.writeFile(absolutePath, fileResult.stdout);
+ }
+
+ return paths.map(normalizeGitTreePath).sort((left, right) =>
+ left.localeCompare(right)
+ );
+}
+
+async function readGitBlobIfExists(
+ repoPath: string,
+ ref: string,
+ path: string,
+): Promise {
+ const result = await runGitBytes(repoPath, ["show", `${ref}:${path}`]);
+ return result.success ? result.stdout : undefined;
+}
+
+async function readWorkspaceFileIfExists(
+ workspaceRoot: string,
+ path: string,
+): Promise {
+ try {
+ return await Deno.readFile(join(workspaceRoot, normalizeGitTreePath(path)));
+ } catch (error) {
+ if (error instanceof Deno.errors.NotFound) {
+ return undefined;
+ }
+ throw error;
+ }
+}
+
+async function readWorkspaceTextFileIfExists(
+ workspaceRoot: string,
+ path: string,
+): Promise {
+ const bytes = await readWorkspaceFileIfExists(workspaceRoot, path);
+ return bytes === undefined ? undefined : new TextDecoder().decode(bytes);
+}
+
+async function listWorkspaceFiles(
+ workspaceRoot: string,
+ basePath = ".",
+): Promise {
+ const directory = basePath === "."
+ ? workspaceRoot
+ : join(workspaceRoot, basePath);
+ const paths: string[] = [];
+
+ for await (const entry of Deno.readDir(directory)) {
+ if (entry.name === ".git" || entry.name === ".weave") {
+ continue;
+ }
+
+ const childPath = basePath === "."
+ ? entry.name
+ : pathPosix.join(basePath, entry.name);
+
+ if (entry.isDirectory) {
+ paths.push(...await listWorkspaceFiles(workspaceRoot, childPath));
+ } else if (entry.isFile) {
+ paths.push(normalizeGitTreePath(childPath));
+ }
+ }
+
+ return paths.sort((left, right) => left.localeCompare(right));
+}
+
+function isRdfOutputPath(path: string): boolean {
+ return RDF_OUTPUT_EXTENSIONS.some((extension) => path.endsWith(extension));
+}
+
+function normalizeGitTreePath(path: string): string {
+ if (path.includes("\\") || isAbsolute(path) || /^[A-Za-z]:/.test(path)) {
+ throw new Error(`Unsafe git tree path: ${path}`);
+ }
+ const normalized = pathPosix.normalize(path);
+ if (
+ normalized === "." || normalized === ".." || normalized.startsWith("../")
+ ) {
+ throw new Error(`Unsafe git tree path: ${path}`);
+ }
+ return normalized;
+}
+
+async function runGit(
+ cwd: string,
+ args: readonly string[],
+ options: { env?: Record } = {},
+): Promise<{ success: boolean; stdout: string; stderr: string }> {
+ const result = await runGitBytes(cwd, args, options);
+ return {
+ success: result.success,
+ stdout: new TextDecoder().decode(result.stdout),
+ stderr: result.stderr,
+ };
+}
+
+async function runGitBytes(
+ cwd: string,
+ args: readonly string[],
+ options: { env?: Record } = {},
+): Promise<{ success: boolean; stdout: Uint8Array; stderr: string }> {
+ try {
+ const output = await new Deno.Command("git", {
+ cwd,
+ args: [...args],
+ env: options.env,
+ }).output();
+ return {
+ success: output.success,
+ stdout: output.stdout,
+ stderr: new TextDecoder().decode(output.stderr),
+ };
+ } catch (error) {
+ if (error instanceof Deno.errors.NotFound) {
+ return {
+ success: false,
+ stdout: new Uint8Array(),
+ stderr: "git executable was not found",
+ };
+ }
+ throw error;
+ }
+}
diff --git a/scripts/package-binaries.ts b/scripts/package-binaries.ts
new file mode 100644
index 0000000..7960161
--- /dev/null
+++ b/scripts/package-binaries.ts
@@ -0,0 +1,245 @@
+import { fromFileUrl, isAbsolute, join } from "@std/path";
+import {
+ type ArchiveEntry,
+ createTarGzArchive,
+ createZipArchive,
+ renderChecksumFile,
+ sha256Hex,
+} from "./release/archive.ts";
+import {
+ assertBinaryBundleMetadata,
+ type BinaryBundleMetadata,
+ createBinaryBundleMetadata,
+ readBinaryBundleMetadata,
+ readRootVersionFrom,
+ type ReleasePlatform,
+ selectReleasePlatforms,
+} from "./release/metadata.ts";
+
+export interface PackageBinariesOptions {
+ root: string;
+ buildDir: string;
+ outDir: string;
+ platformLabels: string[];
+}
+
+export interface PackageBinaryResult {
+ platform: string;
+ archivePath: string;
+ checksumPath: string;
+ checksum: string;
+}
+
+const defaultRoot = fromFileUrl(new URL("..", import.meta.url));
+const defaultBuildDir = "dist/binaries";
+const defaultOutDir = "dist/release";
+const textEncoder = new TextEncoder();
+
+if (import.meta.main) {
+ try {
+ const results = await packageBinaries(parsePackageBinariesArgs(Deno.args));
+ for (const result of results) {
+ console.log(`Packaged ${result.platform}: ${result.archivePath}`);
+ console.log(`Checksum: ${result.checksumPath}`);
+ }
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parsePackageBinariesArgs(
+ args: readonly string[],
+): PackageBinariesOptions {
+ let root = defaultRoot;
+ let buildDir = defaultBuildDir;
+ let outDir = defaultOutDir;
+ const platformLabels: string[] = [];
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+
+ switch (arg) {
+ case "--":
+ break;
+ case "--root":
+ index += 1;
+ root = requireArgumentValue(args[index], "--root");
+ break;
+ case "--build-dir":
+ index += 1;
+ buildDir = requireArgumentValue(args[index], "--build-dir");
+ break;
+ case "--out-dir":
+ index += 1;
+ outDir = requireArgumentValue(args[index], "--out-dir");
+ break;
+ case "--platform":
+ index += 1;
+ platformLabels.push(requireArgumentValue(args[index], "--platform"));
+ break;
+ default:
+ if (arg.startsWith("--root=")) {
+ root = requireArgumentValue(arg.slice("--root=".length), "--root");
+ break;
+ }
+ if (arg.startsWith("--build-dir=")) {
+ buildDir = requireArgumentValue(
+ arg.slice("--build-dir=".length),
+ "--build-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--out-dir=")) {
+ outDir = requireArgumentValue(
+ arg.slice("--out-dir=".length),
+ "--out-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--platform=")) {
+ platformLabels.push(
+ requireArgumentValue(arg.slice("--platform=".length), "--platform"),
+ );
+ break;
+ }
+ throw new Error(`Unsupported package:binaries argument: ${arg}`);
+ }
+ }
+
+ return { root, buildDir, outDir, platformLabels };
+}
+
+export async function packageBinaries(
+ options: PackageBinariesOptions,
+): Promise {
+ const version = await readRootVersionFrom(options.root);
+ const platforms = selectReleasePlatforms(options.platformLabels);
+ const buildDir = resolveRootPath(options.root, options.buildDir);
+ const outDir = resolveRootPath(options.root, options.outDir);
+
+ await Deno.mkdir(outDir, { recursive: true });
+
+ const results: PackageBinaryResult[] = [];
+ for (const platform of platforms) {
+ results.push(
+ await packagePlatformBinary({
+ buildDir,
+ outDir,
+ platform,
+ root: options.root,
+ version,
+ }),
+ );
+ }
+ return results;
+}
+
+async function packagePlatformBinary(options: {
+ buildDir: string;
+ outDir: string;
+ platform: ReleasePlatform;
+ root: string;
+ version: string;
+}): Promise {
+ const platformBuildDir = join(options.buildDir, options.platform.label);
+ const expectedMetadata = createBinaryBundleMetadata(
+ options.version,
+ options.platform,
+ );
+ const metadataPath = join(platformBuildDir, "bundle-metadata.json");
+ const metadata = await readBinaryBundleMetadata(metadataPath);
+ assertBinaryBundleMetadata(metadata, expectedMetadata, metadataPath);
+
+ const entries = await createArchiveEntries({
+ metadata,
+ platformBuildDir,
+ root: options.root,
+ });
+ const archiveBytes = options.platform.archiveExtension === ".zip"
+ ? createZipArchive(entries)
+ : await createTarGzArchive(entries);
+ const archivePath = join(options.outDir, metadata.archiveName);
+ const checksum = await sha256Hex(archiveBytes);
+ const checksumPath = join(options.outDir, metadata.checksumName);
+
+ await Deno.writeFile(archivePath, archiveBytes);
+ await Deno.writeTextFile(
+ checksumPath,
+ renderChecksumFile(checksum, metadata.archiveName),
+ );
+
+ return {
+ platform: options.platform.label,
+ archivePath,
+ checksumPath,
+ checksum,
+ };
+}
+
+async function createArchiveEntries(options: {
+ metadata: BinaryBundleMetadata;
+ platformBuildDir: string;
+ root: string;
+}): Promise {
+ const bundlePrefix = options.metadata.bundleDirectoryName;
+ const binaryPath = join(
+ options.platformBuildDir,
+ options.metadata.executableName,
+ );
+ const metadataPath = join(options.platformBuildDir, "bundle-metadata.json");
+ const licensePath = join(options.root, "LICENSE");
+
+ const entries: ArchiveEntry[] = [
+ {
+ name: `${bundlePrefix}/${options.metadata.executableName}`,
+ data: await Deno.readFile(binaryPath),
+ executable: true,
+ },
+ {
+ name: `${bundlePrefix}/bundle-metadata.json`,
+ data: await Deno.readFile(metadataPath),
+ },
+ {
+ name: `${bundlePrefix}/README.md`,
+ data: textEncoder.encode(renderArchiveReadme(options.metadata)),
+ },
+ ];
+
+ try {
+ entries.push({
+ name: `${bundlePrefix}/LICENSE`,
+ data: await Deno.readFile(licensePath),
+ });
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ }
+
+ return entries;
+}
+
+function renderArchiveReadme(metadata: BinaryBundleMetadata): string {
+ const runPrefix = metadata.os === "win32" ? "" : "./";
+ return `# Weave ${metadata.version} ${metadata.platform}
+
+This archive contains the \`${metadata.executableName}\` CLI for ${metadata.platform}.
+
+Run \`${runPrefix}${metadata.executableName} --version\` after extracting on a matching platform.
+`;
+}
+
+function resolveRootPath(root: string, path: string): string {
+ if (isAbsolute(path)) {
+ return path;
+ }
+ return join(root, path);
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
diff --git a/scripts/publish-npm-packages.ts b/scripts/publish-npm-packages.ts
new file mode 100644
index 0000000..6821808
--- /dev/null
+++ b/scripts/publish-npm-packages.ts
@@ -0,0 +1,300 @@
+import { fromFileUrl, isAbsolute, join } from "@std/path";
+import { readRootVersionFrom } from "./release/metadata.ts";
+import {
+ NPM_COMMAND_NAME,
+ NPM_PACKAGES_METADATA_FILENAME,
+ npmPackagePath,
+ type NpmPackagesMetadata,
+ type NpmPlatformPackageMetadata,
+ readNpmPackagesMetadata,
+} from "./release/npm.ts";
+
+export interface PublishNpmPackagesOptions {
+ root: string;
+ inputDir: string;
+ npmBin: string;
+ tag: string;
+ dryRun: boolean;
+ provenance: boolean;
+}
+
+export interface NpmPublishTarget {
+ packageName: string;
+ packageDir: string;
+}
+
+const defaultRoot = fromFileUrl(new URL("..", import.meta.url));
+const defaultInputDir = "dist/npm";
+
+if (import.meta.main) {
+ try {
+ await publishNpmPackages(parsePublishNpmPackagesArgs(Deno.args));
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parsePublishNpmPackagesArgs(
+ args: readonly string[],
+): PublishNpmPackagesOptions {
+ let root = defaultRoot;
+ let inputDir = defaultInputDir;
+ let npmBin = "npm";
+ let tag = "latest";
+ let dryRun = false;
+ let provenance = false;
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+
+ switch (arg) {
+ case "--":
+ break;
+ case "--root":
+ index += 1;
+ root = requireArgumentValue(args[index], "--root");
+ break;
+ case "--input-dir":
+ index += 1;
+ inputDir = requireArgumentValue(args[index], "--input-dir");
+ break;
+ case "--npm-bin":
+ index += 1;
+ npmBin = requireArgumentValue(args[index], "--npm-bin");
+ break;
+ case "--tag":
+ index += 1;
+ tag = requireArgumentValue(args[index], "--tag");
+ break;
+ case "--dry-run":
+ dryRun = true;
+ break;
+ case "--provenance":
+ provenance = true;
+ break;
+ default:
+ if (arg.startsWith("--root=")) {
+ root = requireArgumentValue(arg.slice("--root=".length), "--root");
+ break;
+ }
+ if (arg.startsWith("--input-dir=")) {
+ inputDir = requireArgumentValue(
+ arg.slice("--input-dir=".length),
+ "--input-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--npm-bin=")) {
+ npmBin = requireArgumentValue(
+ arg.slice("--npm-bin=".length),
+ "--npm-bin",
+ );
+ break;
+ }
+ if (arg.startsWith("--tag=")) {
+ tag = requireArgumentValue(arg.slice("--tag=".length), "--tag");
+ break;
+ }
+ throw new Error(`Unsupported publish:npm-packages argument: ${arg}`);
+ }
+ }
+
+ return { root, inputDir, npmBin, tag, dryRun, provenance };
+}
+
+export async function publishNpmPackages(
+ options: PublishNpmPackagesOptions,
+): Promise {
+ const version = await readRootVersionFrom(options.root);
+ const inputDir = resolveRootPath(options.root, options.inputDir);
+ const metadata = await readNpmPackagesMetadata(
+ join(inputDir, NPM_PACKAGES_METADATA_FILENAME),
+ );
+ assertNpmPackagesVersion(metadata, version);
+
+ const targets = await resolvedPublicationOrder(metadata, inputDir);
+ const packageDirsByName = new Map(
+ targets.map((target) => [target.packageName, target.packageDir]),
+ );
+
+ const wrapperDir = packageDirsByName.get(metadata.wrapperPackageName);
+ if (wrapperDir === undefined) {
+ throw new Error(
+ `Resolved publication order did not include wrapper package ${metadata.wrapperPackageName}`,
+ );
+ }
+ await restoreWrapperPackageExecutableModes(wrapperDir);
+
+ for (const platformPackage of metadata.platformPackages) {
+ const packageDir = packageDirsByName.get(platformPackage.packageName);
+ if (packageDir === undefined) {
+ throw new Error(
+ `Resolved publication order did not include platform package ${platformPackage.packageName}`,
+ );
+ }
+ await restorePlatformPackageExecutableModes(packageDir, platformPackage);
+ }
+
+ for (const target of targets) {
+ await runCommand({
+ args: npmPublishArgs(options),
+ command: options.npmBin,
+ cwd: target.packageDir,
+ });
+ }
+
+ return targets;
+}
+
+export function publicationOrder(
+ metadata: NpmPackagesMetadata,
+): NpmPublishTarget[] {
+ return [
+ ...metadata.platformPackages
+ .slice()
+ .sort((left, right) => left.packageName.localeCompare(right.packageName))
+ .map((entry) => ({
+ packageName: entry.packageName,
+ packageDir: entry.packageDir,
+ })),
+ {
+ packageName: metadata.wrapperPackageName,
+ packageDir: metadata.wrapperPackageDir,
+ },
+ ];
+}
+
+export async function resolvedPublicationOrder(
+ metadata: NpmPackagesMetadata,
+ inputDir: string,
+): Promise {
+ const ordered = publicationOrder(metadata);
+ const resolvedTargets: NpmPublishTarget[] = [];
+
+ for (const target of ordered) {
+ resolvedTargets.push({
+ packageName: target.packageName,
+ packageDir: await resolvePackageDir(
+ inputDir,
+ target.packageName,
+ target.packageDir,
+ ),
+ });
+ }
+
+ return resolvedTargets;
+}
+
+export function npmPublishArgs(options: {
+ tag: string;
+ dryRun: boolean;
+ provenance: boolean;
+}): string[] {
+ const args = ["publish", "--tag", options.tag];
+ if (options.dryRun) {
+ args.push("--dry-run");
+ } else if (options.provenance) {
+ args.push("--provenance");
+ }
+ return args;
+}
+
+async function resolvePackageDir(
+ inputDir: string,
+ packageName: string,
+ preferredPath: string,
+): Promise {
+ const candidates = [
+ preferredPath,
+ npmPackagePath(inputDir, packageName),
+ ];
+
+ for (const candidate of candidates) {
+ try {
+ const stat = await Deno.stat(candidate);
+ if (stat.isDirectory) {
+ return candidate;
+ }
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ }
+ }
+
+ throw new Error(
+ `Could not resolve assembled npm package ${packageName} under ${inputDir}`,
+ );
+}
+
+async function restoreWrapperPackageExecutableModes(
+ packageDir: string,
+): Promise {
+ await chmodExecutable(join(packageDir, "bin", `${NPM_COMMAND_NAME}.js`));
+}
+
+async function restorePlatformPackageExecutableModes(
+ packageDir: string,
+ platformPackage: NpmPlatformPackageMetadata,
+): Promise {
+ await chmodExecutable(
+ join(packageDir, "bin", platformPackage.executableName),
+ );
+}
+
+async function chmodExecutable(path: string): Promise {
+ if (Deno.build.os !== "windows") {
+ await Deno.chmod(path, 0o755);
+ }
+}
+
+async function runCommand(options: {
+ command: string;
+ args: string[];
+ cwd: string;
+}): Promise {
+ console.log(
+ `$ (cd ${options.cwd} && ${options.command} ${options.args.join(" ")})`,
+ );
+ const command = new Deno.Command(options.command, {
+ args: options.args,
+ cwd: options.cwd,
+ stdin: "inherit",
+ stdout: "inherit",
+ stderr: "inherit",
+ });
+ const status = await command.spawn().status;
+ if (!status.success) {
+ throw new Error(
+ `Command failed with exit code ${status.code}: ${options.command} ${
+ options.args.join(" ")
+ }`,
+ );
+ }
+}
+
+function assertNpmPackagesVersion(
+ metadata: NpmPackagesMetadata,
+ expectedVersion: string,
+): void {
+ if (metadata.version !== expectedVersion) {
+ throw new Error(
+ `npm package metadata version ${metadata.version} does not match root version ${expectedVersion}`,
+ );
+ }
+}
+
+function resolveRootPath(root: string, path: string): string {
+ if (isAbsolute(path)) {
+ return path;
+ }
+ return join(root, path);
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
diff --git a/scripts/release/archive.ts b/scripts/release/archive.ts
new file mode 100644
index 0000000..06011a7
--- /dev/null
+++ b/scripts/release/archive.ts
@@ -0,0 +1,238 @@
+export interface ArchiveEntry {
+ name: string;
+ data: Uint8Array;
+ executable?: boolean;
+}
+
+const encoder = new TextEncoder();
+const tarBlockSize = 512;
+const zipLocalFileHeaderSignature = 0x04034b50;
+const zipCentralDirectoryHeaderSignature = 0x02014b50;
+const zipEndOfCentralDirectorySignature = 0x06054b50;
+const zipDosDate = (1 << 5) | 1;
+const zipDosTime = 0;
+
+let crc32Table: Uint32Array | undefined;
+
+export async function createTarGzArchive(
+ entries: readonly ArchiveEntry[],
+): Promise {
+ const tarArchive = createTarArchive(entries);
+ const gzipStream = new Blob([toArrayBuffer(tarArchive)]).stream()
+ .pipeThrough(
+ new CompressionStream("gzip"),
+ );
+ return new Uint8Array(await new Response(gzipStream).arrayBuffer());
+}
+
+export function createZipArchive(
+ entries: readonly ArchiveEntry[],
+): Uint8Array {
+ const chunks: Uint8Array[] = [];
+ const centralDirectoryChunks: Uint8Array[] = [];
+ let offset = 0;
+
+ for (const entry of entries) {
+ const name = normalizeArchiveEntryName(entry.name);
+ const nameBytes = encoder.encode(name);
+ const crc = crc32(entry.data);
+ const mode = entry.executable ? 0o755 : 0o644;
+
+ const localHeader = new Uint8Array(30 + nameBytes.length);
+ const localView = new DataView(localHeader.buffer);
+ localView.setUint32(0, zipLocalFileHeaderSignature, true);
+ localView.setUint16(4, 20, true);
+ localView.setUint16(6, 0, true);
+ localView.setUint16(8, 0, true);
+ localView.setUint16(10, zipDosTime, true);
+ localView.setUint16(12, zipDosDate, true);
+ localView.setUint32(14, crc, true);
+ localView.setUint32(18, entry.data.length, true);
+ localView.setUint32(22, entry.data.length, true);
+ localView.setUint16(26, nameBytes.length, true);
+ localView.setUint16(28, 0, true);
+ localHeader.set(nameBytes, 30);
+
+ const centralHeader = new Uint8Array(46 + nameBytes.length);
+ const centralView = new DataView(centralHeader.buffer);
+ centralView.setUint32(0, zipCentralDirectoryHeaderSignature, true);
+ centralView.setUint16(4, 0x0314, true);
+ centralView.setUint16(6, 20, true);
+ centralView.setUint16(8, 0, true);
+ centralView.setUint16(10, 0, true);
+ centralView.setUint16(12, zipDosTime, true);
+ centralView.setUint16(14, zipDosDate, true);
+ centralView.setUint32(16, crc, true);
+ centralView.setUint32(20, entry.data.length, true);
+ centralView.setUint32(24, entry.data.length, true);
+ centralView.setUint16(28, nameBytes.length, true);
+ centralView.setUint16(30, 0, true);
+ centralView.setUint16(32, 0, true);
+ centralView.setUint16(34, 0, true);
+ centralView.setUint16(36, 0, true);
+ centralView.setUint32(38, (mode & 0xffff) << 16, true);
+ centralView.setUint32(42, offset, true);
+ centralHeader.set(nameBytes, 46);
+
+ chunks.push(localHeader, entry.data);
+ centralDirectoryChunks.push(centralHeader);
+ offset += localHeader.length + entry.data.length;
+ }
+
+ const centralDirectoryOffset = offset;
+ const centralDirectorySize = sumByteLengths(centralDirectoryChunks);
+ chunks.push(...centralDirectoryChunks);
+
+ const endOfCentralDirectory = new Uint8Array(22);
+ const endView = new DataView(endOfCentralDirectory.buffer);
+ endView.setUint32(0, zipEndOfCentralDirectorySignature, true);
+ endView.setUint16(4, 0, true);
+ endView.setUint16(6, 0, true);
+ endView.setUint16(8, entries.length, true);
+ endView.setUint16(10, entries.length, true);
+ endView.setUint32(12, centralDirectorySize, true);
+ endView.setUint32(16, centralDirectoryOffset, true);
+ endView.setUint16(20, 0, true);
+ chunks.push(endOfCentralDirectory);
+
+ return concatBytes(chunks);
+}
+
+export async function sha256Hex(data: Uint8Array): Promise {
+ const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
+ return Array.from(
+ new Uint8Array(digest),
+ (byte) => byte.toString(16).padStart(2, "0"),
+ ).join("");
+}
+
+export function renderChecksumFile(
+ checksum: string,
+ archiveName: string,
+): string {
+ return `${checksum} ${archiveName}\n`;
+}
+
+function createTarArchive(entries: readonly ArchiveEntry[]): Uint8Array {
+ const chunks: Uint8Array[] = [];
+
+ for (const entry of entries) {
+ const name = normalizeArchiveEntryName(entry.name);
+ const data = entry.data;
+ const header = createTarHeader({
+ name,
+ mode: entry.executable ? 0o755 : 0o644,
+ size: data.length,
+ });
+ chunks.push(header, data, createPadding(data.length, tarBlockSize));
+ }
+
+ chunks.push(new Uint8Array(tarBlockSize * 2));
+ return concatBytes(chunks);
+}
+
+function createTarHeader(options: {
+ name: string;
+ mode: number;
+ size: number;
+}): Uint8Array {
+ const header = new Uint8Array(tarBlockSize);
+ const nameBytes = encoder.encode(options.name);
+ if (nameBytes.length > 100) {
+ throw new Error(
+ `Archive entry name is too long for ustar: ${options.name}`,
+ );
+ }
+
+ header.set(nameBytes, 0);
+ writeOctal(header, 100, 8, options.mode);
+ writeOctal(header, 108, 8, 0);
+ writeOctal(header, 116, 8, 0);
+ writeOctal(header, 124, 12, options.size);
+ writeOctal(header, 136, 12, 0);
+ header.fill(0x20, 148, 156);
+ header[156] = "0".charCodeAt(0);
+ header.set(encoder.encode("ustar\0"), 257);
+ header.set(encoder.encode("00"), 263);
+
+ let checksum = 0;
+ for (const byte of header) {
+ checksum += byte;
+ }
+
+ const checksumText = checksum.toString(8).padStart(6, "0");
+ header.set(encoder.encode(checksumText), 148);
+ header[154] = 0;
+ header[155] = 0x20;
+
+ return header;
+}
+
+function writeOctal(
+ target: Uint8Array,
+ offset: number,
+ length: number,
+ value: number,
+): void {
+ const text = value.toString(8).padStart(length - 1, "0");
+ target.set(encoder.encode(text), offset);
+ target[offset + length - 1] = 0;
+}
+
+function createPadding(length: number, blockSize: number): Uint8Array {
+ const remainder = length % blockSize;
+ return remainder === 0
+ ? new Uint8Array()
+ : new Uint8Array(blockSize - remainder);
+}
+
+function normalizeArchiveEntryName(name: string): string {
+ if (
+ name.length === 0 || name.startsWith("/") || name.includes("..") ||
+ name.includes("\\")
+ ) {
+ throw new Error(`Invalid archive entry name: ${name}`);
+ }
+ return name;
+}
+
+function crc32(data: Uint8Array): number {
+ const table = crc32Table ??= createCrc32Table();
+ let crc = 0xffffffff;
+ for (const byte of data) {
+ crc = table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
+ }
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+function createCrc32Table(): Uint32Array {
+ const table = new Uint32Array(256);
+ for (let index = 0; index < 256; index += 1) {
+ let crc = index;
+ for (let bit = 0; bit < 8; bit += 1) {
+ crc = (crc & 1) !== 0 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
+ }
+ table[index] = crc >>> 0;
+ }
+ return table;
+}
+
+function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
+ const output = new Uint8Array(sumByteLengths(chunks));
+ let offset = 0;
+ for (const chunk of chunks) {
+ output.set(chunk, offset);
+ offset += chunk.length;
+ }
+ return output;
+}
+
+function sumByteLengths(chunks: readonly Uint8Array[]): number {
+ return chunks.reduce((total, chunk) => total + chunk.length, 0);
+}
+
+function toArrayBuffer(data: Uint8Array): ArrayBuffer {
+ const copy = new ArrayBuffer(data.byteLength);
+ new Uint8Array(copy).set(data);
+ return copy;
+}
diff --git a/scripts/release/metadata.ts b/scripts/release/metadata.ts
new file mode 100644
index 0000000..b511964
--- /dev/null
+++ b/scripts/release/metadata.ts
@@ -0,0 +1,213 @@
+import denoConfig from "../../deno.json" with { type: "json" };
+import { join } from "@std/path";
+
+export type ReleasePlatformLabel =
+ | "linux-x64"
+ | "windows-x64"
+ | "macos-x64"
+ | "macos-arm64";
+
+export interface ReleasePlatform {
+ label: ReleasePlatformLabel;
+ denoTarget: string;
+ os: "linux" | "darwin" | "win32";
+ cpu: "x64" | "arm64";
+ archiveExtension: ".tar.gz" | ".zip";
+ executableName: "weave" | "weave.exe";
+ npmPackageName: string;
+}
+
+export interface BinaryBundleMetadata {
+ packageName: string;
+ wrapperPackageName: string;
+ version: string;
+ platform: ReleasePlatformLabel;
+ os: ReleasePlatform["os"];
+ cpu: ReleasePlatform["cpu"];
+ denoTarget: string;
+ executableName: ReleasePlatform["executableName"];
+ bundleDirectoryName: string;
+ archiveName: string;
+ checksumName: string;
+}
+
+export const NPM_WRAPPER_PACKAGE_NAME = "@semantic-flow/weave";
+
+export const RELEASE_PLATFORMS: readonly ReleasePlatform[] = [
+ {
+ label: "linux-x64",
+ denoTarget: "x86_64-unknown-linux-gnu",
+ os: "linux",
+ cpu: "x64",
+ archiveExtension: ".tar.gz",
+ executableName: "weave",
+ npmPackageName: "@semantic-flow/weave-linux-x64",
+ },
+ {
+ label: "windows-x64",
+ denoTarget: "x86_64-pc-windows-msvc",
+ os: "win32",
+ cpu: "x64",
+ archiveExtension: ".zip",
+ executableName: "weave.exe",
+ npmPackageName: "@semantic-flow/weave-windows-x64",
+ },
+ {
+ label: "macos-x64",
+ denoTarget: "x86_64-apple-darwin",
+ os: "darwin",
+ cpu: "x64",
+ archiveExtension: ".tar.gz",
+ executableName: "weave",
+ npmPackageName: "@semantic-flow/weave-macos-x64",
+ },
+ {
+ label: "macos-arm64",
+ denoTarget: "aarch64-apple-darwin",
+ os: "darwin",
+ cpu: "arm64",
+ archiveExtension: ".tar.gz",
+ executableName: "weave",
+ npmPackageName: "@semantic-flow/weave-macos-arm64",
+ },
+] as const;
+
+export function readRootVersion(): string {
+ return requireSupportedVersion(denoConfig.version);
+}
+
+export async function readRootVersionFrom(root: string): Promise {
+ const denoConfigPath = join(root, "deno.json");
+ const config = JSON.parse(
+ await Deno.readTextFile(denoConfigPath),
+ ) as { version?: unknown };
+ return requireSupportedVersion(config.version);
+}
+
+export function createBundleDirectoryName(
+ version: string,
+ platform: ReleasePlatform,
+): string {
+ if (!isSupportedVersion(version)) {
+ throw new Error(`Unsupported release version: ${version}`);
+ }
+ return `weave-v${version}-${platform.label}`;
+}
+
+export function createArchiveName(
+ version: string,
+ platform: ReleasePlatform,
+): string {
+ return `${
+ createBundleDirectoryName(version, platform)
+ }${platform.archiveExtension}`;
+}
+
+export function createBinaryBundleMetadata(
+ version: string,
+ platform: ReleasePlatform,
+): BinaryBundleMetadata {
+ if (!isSupportedVersion(version)) {
+ throw new Error(`Unsupported release version: ${version}`);
+ }
+
+ const bundleDirectoryName = createBundleDirectoryName(version, platform);
+ const archiveName = `${bundleDirectoryName}${platform.archiveExtension}`;
+
+ return {
+ packageName: platform.npmPackageName,
+ wrapperPackageName: NPM_WRAPPER_PACKAGE_NAME,
+ version,
+ platform: platform.label,
+ os: platform.os,
+ cpu: platform.cpu,
+ denoTarget: platform.denoTarget,
+ executableName: platform.executableName,
+ bundleDirectoryName,
+ archiveName,
+ checksumName: `${archiveName}.sha256`,
+ };
+}
+
+export async function readBinaryBundleMetadata(
+ path: string,
+): Promise {
+ return JSON.parse(await Deno.readTextFile(path)) as BinaryBundleMetadata;
+}
+
+export function assertBinaryBundleMetadata(
+ actual: BinaryBundleMetadata,
+ expected: BinaryBundleMetadata,
+ path: string,
+): void {
+ const fields: readonly (keyof BinaryBundleMetadata)[] = [
+ "packageName",
+ "wrapperPackageName",
+ "version",
+ "platform",
+ "os",
+ "cpu",
+ "denoTarget",
+ "executableName",
+ "bundleDirectoryName",
+ "archiveName",
+ "checksumName",
+ ];
+
+ for (const field of fields) {
+ if (actual[field] !== expected[field]) {
+ throw new Error(
+ `Bundle metadata field ${field} does not match expected release metadata: ${path}`,
+ );
+ }
+ }
+}
+
+export function getReleasePlatform(
+ label: string,
+): ReleasePlatform | undefined {
+ return RELEASE_PLATFORMS.find((platform) => platform.label === label);
+}
+
+export function selectReleasePlatforms(
+ labels: readonly string[],
+): ReleasePlatform[] {
+ if (labels.length === 0) {
+ return [...RELEASE_PLATFORMS];
+ }
+
+ const seen = new Set();
+ return labels.map((label) => {
+ if (seen.has(label)) {
+ throw new Error(`Release platform selected more than once: ${label}`);
+ }
+ seen.add(label);
+
+ const platform = getReleasePlatform(label);
+ if (platform === undefined) {
+ const supported = RELEASE_PLATFORMS.map((entry) => entry.label).join(
+ ", ",
+ );
+ throw new Error(
+ `Unsupported release platform: ${label}. Supported platforms: ${supported}`,
+ );
+ }
+
+ return platform;
+ });
+}
+
+function requireSupportedVersion(value: unknown): string {
+ if (typeof value !== "string" || !isSupportedVersion(value)) {
+ throw new Error(
+ "root deno.json must declare a semver-compatible string version",
+ );
+ }
+ return value;
+}
+
+function isSupportedVersion(value: string): boolean {
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
+ value,
+ );
+}
diff --git a/scripts/release/npm.ts b/scripts/release/npm.ts
new file mode 100644
index 0000000..f9ac114
--- /dev/null
+++ b/scripts/release/npm.ts
@@ -0,0 +1,228 @@
+import { join } from "@std/path";
+import {
+ type BinaryBundleMetadata,
+ NPM_WRAPPER_PACKAGE_NAME,
+ type ReleasePlatform,
+} from "./metadata.ts";
+
+export const NPM_PACKAGES_METADATA_FILENAME = "npm-packages-metadata.json";
+export const NPM_COMMAND_NAME = "weave";
+export const NPM_REPOSITORY_URL =
+ "git+https://github.com/semantic-flow/weave.git";
+export const NPM_BUGS_URL = "https://github.com/semantic-flow/weave/issues";
+export const NPM_HOMEPAGE_URL = "https://github.com/semantic-flow/weave#readme";
+
+export interface NpmPackageJson {
+ name: string;
+ version: string;
+ description: string;
+ license: "Apache-2.0";
+ homepage: string;
+ repository: {
+ type: "git";
+ url: string;
+ };
+ bugs: {
+ url: string;
+ };
+ publishConfig?: {
+ access: "public";
+ };
+ files: string[];
+ bin?: Record;
+ os?: string[];
+ cpu?: string[];
+ optionalDependencies?: Record;
+ engines?: Record;
+}
+
+export interface NpmPlatformPackageMetadata {
+ packageName: string;
+ platform: string;
+ packageDir: string;
+ packageJsonPath: string;
+ os: string;
+ cpu: string;
+ executableName: string;
+ executablePath: string;
+ bundleMetadataPath: string;
+}
+
+export interface NpmPackagesMetadata {
+ createdAt: string;
+ version: string;
+ wrapperPackageName: string;
+ wrapperPackageDir: string;
+ wrapperPackageJsonPath: string;
+ commandName: string;
+ platformPackages: NpmPlatformPackageMetadata[];
+}
+
+export function npmPackagePath(outDir: string, packageName: string): string {
+ const match = /^@([^/]+)\/([^/]+)$/.exec(packageName);
+ if (match !== null) {
+ return join(outDir, `@${match[1]}`, match[2]);
+ }
+ return join(outDir, packageName);
+}
+
+export function createWrapperPackageJson(
+ version: string,
+ platforms: readonly ReleasePlatform[],
+): NpmPackageJson {
+ const optionalDependencies = Object.fromEntries(
+ platforms.map((platform) => [platform.npmPackageName, version]),
+ );
+
+ return {
+ name: NPM_WRAPPER_PACKAGE_NAME,
+ version,
+ description: "Semantic Flow Weave CLI.",
+ license: "Apache-2.0",
+ homepage: NPM_HOMEPAGE_URL,
+ repository: {
+ type: "git",
+ url: NPM_REPOSITORY_URL,
+ },
+ bugs: {
+ url: NPM_BUGS_URL,
+ },
+ publishConfig: packagePublishConfig(NPM_WRAPPER_PACKAGE_NAME),
+ bin: {
+ [NPM_COMMAND_NAME]: "bin/weave.js",
+ },
+ files: [
+ "bin/",
+ "README.md",
+ "LICENSE",
+ ],
+ optionalDependencies,
+ engines: {
+ node: ">=18",
+ },
+ };
+}
+
+export function createPlatformPackageJson(
+ metadata: BinaryBundleMetadata,
+): NpmPackageJson {
+ return {
+ name: metadata.packageName,
+ version: metadata.version,
+ description: `Native Weave CLI binary for ${metadata.platform}.`,
+ license: "Apache-2.0",
+ homepage: NPM_HOMEPAGE_URL,
+ repository: {
+ type: "git",
+ url: NPM_REPOSITORY_URL,
+ },
+ bugs: {
+ url: NPM_BUGS_URL,
+ },
+ publishConfig: packagePublishConfig(metadata.packageName),
+ os: [metadata.os],
+ cpu: [metadata.cpu],
+ files: [
+ "bin/",
+ "bundle-metadata.json",
+ "README.md",
+ "LICENSE",
+ ],
+ };
+}
+
+export function renderWrapperBinScript(
+ platforms: readonly ReleasePlatform[],
+): string {
+ const entries = platforms.map((platform) => {
+ const key = `${platform.os}-${platform.cpu}`;
+ return ` ${JSON.stringify(key)}: ${
+ JSON.stringify({
+ packageName: platform.npmPackageName,
+ executableName: platform.executableName,
+ label: platform.label,
+ })
+ },`;
+ }).join("\n");
+ const supportedLabels = platforms.map((platform) => platform.label).join(
+ ", ",
+ );
+
+ return `#!/usr/bin/env node
+"use strict";
+
+const { spawnSync } = require("node:child_process");
+const path = require("node:path");
+
+const platformPackages = {
+${entries}
+};
+
+const currentPlatform = \`\${process.platform}-\${process.arch}\`;
+const platformPackage = platformPackages[currentPlatform];
+
+if (platformPackage === undefined) {
+ console.error(
+ \`Unsupported Weave platform: \${process.platform}/\${process.arch}. Supported package platforms: ${supportedLabels}.\`,
+ );
+ process.exit(1);
+}
+
+let packageJsonPath;
+try {
+ packageJsonPath = require.resolve(\`\${platformPackage.packageName}/package.json\`);
+} catch (_error) {
+ console.error(
+ \`Missing Weave native package \${platformPackage.packageName}. Try reinstalling ${NPM_WRAPPER_PACKAGE_NAME}.\`,
+ );
+ process.exit(1);
+}
+
+const executablePath = path.join(
+ path.dirname(packageJsonPath),
+ "bin",
+ platformPackage.executableName,
+);
+const result = spawnSync(executablePath, process.argv.slice(2), {
+ stdio: "inherit",
+});
+
+if (result.error) {
+ console.error(\`Failed to execute Weave binary: \${result.error.message}\`);
+ process.exit(1);
+}
+
+if (result.signal) {
+ console.error(\`Weave terminated by signal \${result.signal}.\`);
+ process.exit(1);
+}
+
+process.exit(result.status ?? 1);
+`;
+}
+
+export function renderWrapperReadme(version: string): string {
+ return `# Weave ${version}
+
+This package installs the Semantic Flow Weave CLI and dispatches to the native package for the current platform.
+`;
+}
+
+export function renderPlatformReadme(metadata: BinaryBundleMetadata): string {
+ return `# Weave ${metadata.version} ${metadata.platform}
+
+This package contains the native Weave CLI binary for ${metadata.platform}.
+`;
+}
+
+export async function readNpmPackagesMetadata(
+ path: string,
+): Promise {
+ return JSON.parse(await Deno.readTextFile(path)) as NpmPackagesMetadata;
+}
+
+function packagePublishConfig(
+ packageName: string,
+): { access: "public" } | undefined {
+ return packageName.startsWith("@") ? { access: "public" } : undefined;
+}
diff --git a/scripts/smoke-npm-install.ts b/scripts/smoke-npm-install.ts
new file mode 100644
index 0000000..2215cf9
--- /dev/null
+++ b/scripts/smoke-npm-install.ts
@@ -0,0 +1,352 @@
+import { fromFileUrl, isAbsolute, join } from "@std/path";
+import { readRootVersionFrom } from "./release/metadata.ts";
+import {
+ NPM_PACKAGES_METADATA_FILENAME,
+ npmPackagePath,
+ type NpmPackagesMetadata,
+ type NpmPlatformPackageMetadata,
+ readNpmPackagesMetadata,
+} from "./release/npm.ts";
+
+export interface SmokeNpmInstallOptions {
+ root: string;
+ inputDir: string;
+ workDir: string;
+ npmBin: string;
+}
+
+export interface SmokeNpmInstallResult {
+ projectDir: string;
+ wrapperTarball: string;
+ platformTarball: string;
+ versionOutput: string;
+}
+
+const defaultRoot = fromFileUrl(new URL("..", import.meta.url));
+const defaultInputDir = "dist/npm";
+const defaultWorkDir = "dist/npm-install-smoke";
+
+if (import.meta.main) {
+ try {
+ const result = await smokeNpmInstall(parseSmokeNpmInstallArgs(Deno.args));
+ console.log(result.versionOutput.trim());
+ console.log(`npm install smoke passed in ${result.projectDir}`);
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ Deno.exit(1);
+ }
+}
+
+export function parseSmokeNpmInstallArgs(
+ args: readonly string[],
+): SmokeNpmInstallOptions {
+ let root = defaultRoot;
+ let inputDir = defaultInputDir;
+ let workDir = defaultWorkDir;
+ let npmBin = "npm";
+
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index];
+
+ switch (arg) {
+ case "--":
+ return { root, inputDir, workDir, npmBin };
+ case "--root":
+ index += 1;
+ root = requireArgumentValue(args[index], "--root");
+ break;
+ case "--input-dir":
+ index += 1;
+ inputDir = requireArgumentValue(args[index], "--input-dir");
+ break;
+ case "--work-dir":
+ index += 1;
+ workDir = requireArgumentValue(args[index], "--work-dir");
+ break;
+ case "--npm-bin":
+ index += 1;
+ npmBin = requireArgumentValue(args[index], "--npm-bin");
+ break;
+ default:
+ if (arg.startsWith("--root=")) {
+ root = requireArgumentValue(arg.slice("--root=".length), "--root");
+ break;
+ }
+ if (arg.startsWith("--input-dir=")) {
+ inputDir = requireArgumentValue(
+ arg.slice("--input-dir=".length),
+ "--input-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--work-dir=")) {
+ workDir = requireArgumentValue(
+ arg.slice("--work-dir=".length),
+ "--work-dir",
+ );
+ break;
+ }
+ if (arg.startsWith("--npm-bin=")) {
+ npmBin = requireArgumentValue(
+ arg.slice("--npm-bin=".length),
+ "--npm-bin",
+ );
+ break;
+ }
+ throw new Error(`Unsupported smoke:npm-install argument: ${arg}`);
+ }
+ }
+
+ return { root, inputDir, workDir, npmBin };
+}
+
+export async function smokeNpmInstall(
+ options: SmokeNpmInstallOptions,
+): Promise {
+ const version = await readRootVersionFrom(options.root);
+ const inputDir = resolveRootPath(options.root, options.inputDir);
+ const workDir = resolveRootPath(options.root, options.workDir);
+ const packagesMetadata = await readNpmPackagesMetadata(
+ join(inputDir, NPM_PACKAGES_METADATA_FILENAME),
+ );
+ assertNpmPackagesVersion(packagesMetadata, version);
+ const platformPackage = hostNpmPlatformPackage(packagesMetadata);
+ const wrapperDir = await resolvePackageDir(
+ inputDir,
+ packagesMetadata.wrapperPackageName,
+ packagesMetadata.wrapperPackageDir,
+ );
+ const platformDir = await resolvePackageDir(
+ inputDir,
+ platformPackage.packageName,
+ platformPackage.packageDir,
+ );
+
+ await ensurePackageDir(wrapperDir);
+ await ensurePackageDir(platformDir);
+ await restoreExecutableModes(wrapperDir, platformDir, platformPackage);
+
+ const wrapperTarball = await npmPack(options.npmBin, wrapperDir);
+ const platformTarball = await npmPack(options.npmBin, platformDir);
+ const projectDir = join(workDir, "project");
+
+ await resetDirectory(workDir);
+ await Deno.mkdir(projectDir, { recursive: true });
+ await Deno.writeTextFile(
+ join(projectDir, "package.json"),
+ `${JSON.stringify({ name: "weave-npm-install-smoke", private: true })}\n`,
+ );
+
+ await runCommand({
+ command: options.npmBin,
+ args: [
+ "install",
+ "--ignore-scripts",
+ "--no-audit",
+ "--no-fund",
+ "--no-package-lock",
+ wrapperTarball,
+ platformTarball,
+ ],
+ cwd: projectDir,
+ });
+
+ const versionOutput = await runCommand({
+ command: localProjectCommandPath(projectDir, packagesMetadata.commandName),
+ args: ["--version"],
+ cwd: projectDir,
+ stdout: "piped",
+ });
+ const expectedVersionOutput = `${packagesMetadata.commandName} ${version}`;
+ if (versionOutput.trim() !== expectedVersionOutput) {
+ throw new Error(
+ `Expected npm-installed ${packagesMetadata.commandName} --version to print ${expectedVersionOutput}, got ${versionOutput.trim()}`,
+ );
+ }
+
+ return {
+ projectDir,
+ wrapperTarball,
+ platformTarball,
+ versionOutput,
+ };
+}
+
+export function currentNodeArch(): string {
+ switch (Deno.build.arch) {
+ case "x86_64":
+ return "x64";
+ case "aarch64":
+ return "arm64";
+ default:
+ return Deno.build.arch;
+ }
+}
+
+export function currentNodePlatform(): string {
+ return Deno.build.os === "windows" ? "win32" : Deno.build.os;
+}
+
+export function hostNpmPlatformPackage(
+ metadata: NpmPackagesMetadata,
+ os: string = currentNodePlatform(),
+ cpu: string = currentNodeArch(),
+): NpmPlatformPackageMetadata {
+ const platform = metadata.platformPackages.find((entry) =>
+ entry.os === os && entry.cpu === cpu
+ );
+ if (platform === undefined) {
+ throw new Error(
+ `No Weave npm platform package supports host ${os}/${cpu}`,
+ );
+ }
+ return platform;
+}
+
+export function localProjectCommandPath(
+ projectDir: string,
+ command: string,
+ os: string = Deno.build.os,
+): string {
+ return join(
+ projectDir,
+ "node_modules",
+ ".bin",
+ os === "windows" ? `${command}.cmd` : command,
+ );
+}
+
+async function npmPack(
+ npmBin: string,
+ packageDir: string,
+): Promise {
+ const output = await runCommand({
+ command: npmBin,
+ args: ["pack", "--json"],
+ cwd: packageDir,
+ stdout: "piped",
+ });
+ const parsed = JSON.parse(output) as Array<{ filename?: string }>;
+ const filename = parsed[0]?.filename;
+ if (filename === undefined || filename.length === 0) {
+ throw new Error(`npm pack did not return a filename for ${packageDir}`);
+ }
+ return join(packageDir, filename);
+}
+
+async function runCommand(options: {
+ command: string;
+ args: string[];
+ cwd: string;
+ stdout?: "inherit" | "piped";
+}): Promise {
+ const command = new Deno.Command(options.command, {
+ args: options.args,
+ cwd: options.cwd,
+ stdin: "null",
+ stdout: options.stdout ?? "inherit",
+ stderr: "inherit",
+ });
+ const output = await command.output();
+ if (!output.success) {
+ throw new Error(
+ `Command failed with exit code ${output.code}: ${options.command} ${
+ options.args.join(" ")
+ }`,
+ );
+ }
+ return options.stdout === "piped"
+ ? new TextDecoder().decode(output.stdout)
+ : "";
+}
+
+async function restoreExecutableModes(
+ wrapperDir: string,
+ platformDir: string,
+ platformPackage: NpmPlatformPackageMetadata,
+): Promise {
+ await chmodExecutable(join(wrapperDir, "bin", "weave.js"));
+ await chmodExecutable(
+ join(platformDir, "bin", platformPackage.executableName),
+ );
+}
+
+async function chmodExecutable(path: string): Promise {
+ if (Deno.build.os !== "windows") {
+ await Deno.chmod(path, 0o755);
+ }
+}
+
+async function ensurePackageDir(path: string): Promise {
+ const stat = await Deno.stat(path).catch((error) => {
+ if (error instanceof Deno.errors.NotFound) {
+ throw new Error(`Missing assembled npm package directory: ${path}`);
+ }
+ throw error;
+ });
+ if (!stat.isDirectory) {
+ throw new Error(`Assembled npm package path is not a directory: ${path}`);
+ }
+}
+
+async function resolvePackageDir(
+ inputDir: string,
+ packageName: string,
+ preferredPath: string,
+): Promise {
+ const candidates = [
+ preferredPath,
+ npmPackagePath(inputDir, packageName),
+ ];
+
+ for (const candidate of candidates) {
+ try {
+ const stat = await Deno.stat(candidate);
+ if (stat.isDirectory) {
+ return candidate;
+ }
+ } catch (error) {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ }
+ }
+
+ throw new Error(
+ `Could not resolve assembled npm package ${packageName} under ${inputDir}`,
+ );
+}
+
+function assertNpmPackagesVersion(
+ metadata: NpmPackagesMetadata,
+ expectedVersion: string,
+): void {
+ if (metadata.version !== expectedVersion) {
+ throw new Error(
+ `npm package metadata version ${metadata.version} does not match root version ${expectedVersion}`,
+ );
+ }
+}
+
+async function resetDirectory(path: string): Promise {
+ await Deno.remove(path, { recursive: true }).catch((error) => {
+ if (!(error instanceof Deno.errors.NotFound)) {
+ throw error;
+ }
+ });
+ await Deno.mkdir(path, { recursive: true });
+}
+
+function resolveRootPath(root: string, path: string): string {
+ if (isAbsolute(path)) {
+ return path;
+ }
+ return join(root, path);
+}
+
+function requireArgumentValue(value: string | undefined, name: string): string {
+ if (value === undefined || value.trim().length === 0) {
+ throw new Error(`${name} requires a value`);
+ }
+ return value;
+}
diff --git a/src/cli/run.ts b/src/cli/run.ts
index 94676c8..5c9db0e 100644
--- a/src/cli/run.ts
+++ b/src/cli/run.ts
@@ -1,6 +1,6 @@
import { Command } from "@cliffy/command";
import { Confirm, Input } from "@cliffy/prompt";
-import { isAbsolute, join, relative, resolve } from "@std/path";
+import { basename, isAbsolute, join, relative, resolve } from "@std/path";
import { ExtractInputError } from "../core/extract/extract.ts";
import { IntegrateInputError } from "../core/integrate/integrate.ts";
import { KnopAddReferenceInputError } from "../core/knop/add_reference.ts";
@@ -11,6 +11,14 @@ import { normalizeCliDesignatorPath } from "../core/designator_segments.ts";
import type { TargetSpec, VersionTargetSpec } from "../core/targeting.ts";
import { WeaveInputError } from "../core/weave/weave.ts";
import { createRuntimeLoggers } from "../runtime/logging/factory.ts";
+import {
+ describeGHPagesDeployBootstrapPlan,
+ describeGHPagesDeployBootstrapResult,
+ executeGHPagesDeployBootstrap,
+ GHPagesDeployInputError,
+ GHPagesDeployRuntimeError,
+ planGHPagesDeployBootstrap,
+} from "../runtime/deploy/gh_pages.ts";
import {
describeExtractAllTermsResult,
describeExtractResult,
@@ -61,15 +69,27 @@ import {
WeaveRuntimeError,
} from "../runtime/weave/weave.ts";
import { loadOperationalLocalPathPolicy } from "../runtime/operational/local_path_policy.ts";
+import type { HistoryTrackingPolicy } from "../runtime/config/effective_config.ts";
+import { WEAVE_VERSION } from "../version.ts";
const TARGET_OPTION_DESCRIPTION =
"Target spec as comma-separated key=value fields. Supported keys: designatorPath, recursive. Versioning commands also accept historySegment, stateSegment, and manifestationSegment.";
+const HISTORY_TRACKING_POLICY_VALUES = [
+ "versioned",
+ "currentOnly",
+ "required",
+ "slimHistory",
+ "checkpointOnly",
+ "metadataOnly",
+] as const satisfies readonly HistoryTrackingPolicy[];
+const CLI_LOG_DIR_ENV_VAR = "WEAVE_LOG_DIR";
export async function runWeaveCli(args: string[]): Promise {
let exitCode = 0;
const command = new Command()
.name("weave")
+ .version(WEAVE_VERSION)
.description("Filesystem-oriented Semantic Flow tooling.")
.option(
"--mesh-root ",
@@ -93,6 +113,10 @@ export async function runWeaveCli(args: string[]): Promise {
"--payload-manifestation-segment ",
"Payload manifestation segment name to pass only to version for a single targeted payload weave.",
)
+ .option(
+ "--history-tracking-policy ",
+ "Override the history tracking policy for all artifact roles during this command.",
+ )
.action(async (
options: {
meshRoot: string;
@@ -100,12 +124,16 @@ export async function runWeaveCli(args: string[]): Promise {
payloadHistorySegment?: string;
payloadStateSegment?: string;
payloadManifestationSegment?: string;
+ historyTrackingPolicy?: string;
},
) => {
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
const targets = resolveVersionTargetSpecs(options, "weave");
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const historyTrackingPolicyOverride = resolveHistoryTrackingPolicyOption(
+ options.historyTrackingPolicy,
+ );
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -114,6 +142,7 @@ export async function runWeaveCli(args: string[]): Promise {
meshRoot,
workspaceRoot,
targets,
+ historyTrackingPolicyOverride,
localMode: true,
});
@@ -122,6 +151,7 @@ export async function runWeaveCli(args: string[]): Promise {
request: targets.length > 0 ? { targets } : undefined,
operationalLogger,
auditLogger,
+ historyTrackingPolicyOverride,
});
console.log(describeWeaveResult(result));
for (const path of result.createdPaths) {
@@ -154,7 +184,7 @@ export async function runWeaveCli(args: string[]): Promise {
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
const targets = resolveSharedTargetSpecs(options, "validate");
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { auditLogger } = createRuntimeLoggers({ logDir });
await auditLogger.command("validate", {
@@ -206,6 +236,10 @@ export async function runWeaveCli(args: string[]): Promise {
"--payload-manifestation-segment ",
"Payload manifestation segment name for a single targeted payload version.",
)
+ .option(
+ "--history-tracking-policy ",
+ "Override the history tracking policy for all artifact roles during this command.",
+ )
.action(async (
options: {
meshRoot: string;
@@ -213,24 +247,29 @@ export async function runWeaveCli(args: string[]): Promise {
payloadHistorySegment?: string;
payloadStateSegment?: string;
payloadManifestationSegment?: string;
+ historyTrackingPolicy?: string;
},
) => {
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
const targets = resolveVersionTargetSpecs(options, "version");
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const historyTrackingPolicyOverride =
+ resolveHistoryTrackingPolicyOption(options.historyTrackingPolicy);
+ const logDir = resolveCliLogDir(workspaceRoot);
const { auditLogger } = createRuntimeLoggers({ logDir });
await auditLogger.command("version", {
meshRoot,
workspaceRoot,
targets,
+ historyTrackingPolicyOverride,
localMode: true,
});
const result = await executeVersion({
meshRoot,
request: targets.length > 0 ? { targets } : undefined,
+ historyTrackingPolicyOverride,
});
console.log(describeVersionResult(result));
for (const path of result.createdPaths) {
@@ -261,17 +300,24 @@ export async function runWeaveCli(args: string[]): Promise {
"--include-semantic-flow-metadata",
"Include the generated Semantic Flow metadata section on ResourcePages.",
)
+ .option(
+ "--history-tracking-policy ",
+ "Override the history tracking policy for all artifact roles during this command.",
+ )
.action(async (
options: {
meshRoot: string;
target?: string[];
includeSemanticFlowMetadata?: boolean;
+ historyTrackingPolicy?: string;
},
) => {
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
const targets = resolveSharedTargetSpecs(options, "generate");
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const historyTrackingPolicyOverride =
+ resolveHistoryTrackingPolicyOption(options.historyTrackingPolicy);
+ const logDir = resolveCliLogDir(workspaceRoot);
const { auditLogger } = createRuntimeLoggers({ logDir });
await auditLogger.command("generate", {
@@ -280,6 +326,7 @@ export async function runWeaveCli(args: string[]): Promise {
targets,
includeSemanticFlowMetadata:
options.includeSemanticFlowMetadata === true,
+ historyTrackingPolicyOverride,
localMode: true,
});
@@ -288,6 +335,7 @@ export async function runWeaveCli(args: string[]): Promise {
request: targets.length > 0 ? { targets } : undefined,
includeSemanticFlowMetadata:
options.includeSemanticFlowMetadata === true,
+ historyTrackingPolicyOverride,
});
console.log(describeGenerateResult(result));
for (const path of result.createdPaths) {
@@ -339,7 +387,7 @@ export async function runWeaveCli(args: string[]): Promise {
) => {
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -505,7 +553,7 @@ export async function runWeaveCli(args: string[]): Promise {
) => {
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -639,7 +687,7 @@ export async function runWeaveCli(args: string[]): Promise {
);
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -700,7 +748,7 @@ export async function runWeaveCli(args: string[]): Promise {
options,
designatorPathArg,
);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -729,6 +777,180 @@ export async function runWeaveCli(args: string[]): Promise {
}),
),
)
+ .command(
+ "deploy",
+ new Command()
+ .description("Deployment operations.")
+ .command(
+ "gh-pages",
+ new Command()
+ .description(
+ "Bootstrap a branch-published GitHub Pages mesh in a publication worktree.",
+ )
+ .option(
+ "--source-root ",
+ "Source checkout root to read during branch-published deployment.",
+ { default: "." },
+ )
+ .option(
+ "--publish-root ",
+ "Publication branch worktree root to update.",
+ )
+ .option(
+ "--mesh-base ",
+ "Canonical base IRI for Semantic Flow identifiers in the published mesh.",
+ )
+ .option(
+ "--no-nojekyll",
+ "Do not create a GitHub Pages .nojekyll publishing guard.",
+ )
+ .option(
+ "--cname ",
+ "Create or update the publication branch CNAME file.",
+ )
+ .option(
+ "--allow-dirty-publish-root",
+ "Allow deployment even when the publication worktree has uncommitted changes.",
+ )
+ .option(
+ "--dry-run",
+ "Print the branch-published deploy plan without writing publication files.",
+ )
+ .option(
+ "--commit",
+ "Create a local publication commit after successful generation when the publication diff is non-empty.",
+ )
+ .option(
+ "--commit-message ",
+ "Commit message to use with --commit.",
+ )
+ .option(
+ "--source-path ",
+ "Repository-relative source path to materialize into the publication mesh.",
+ )
+ .option(
+ "--target-path ",
+ "Publication-root relative target path for the materialized source. Defaults to --source-path.",
+ )
+ .option(
+ "--designator-path ",
+ "Designator path for the materialized source artifact.",
+ )
+ .option(
+ "--source-repository-url ",
+ "Durable repository URL to record for the materialized source locator.",
+ )
+ .option(
+ "--source-ref ",
+ "Durable repository ref to record for the materialized source locator.",
+ )
+ .option(
+ "--source-commit ",
+ "Optional resolved commit to record for the materialized source locator.",
+ )
+ .option(
+ "--interactive",
+ "Prompt for missing branch-published deployment inputs.",
+ )
+ .action(async (
+ options: {
+ sourceRoot: string;
+ publishRoot?: string;
+ meshBase?: string;
+ nojekyll?: boolean;
+ cname?: string;
+ allowDirtyPublishRoot?: boolean;
+ dryRun?: boolean;
+ commit?: boolean;
+ commitMessage?: string;
+ sourcePath?: string;
+ targetPath?: string;
+ designatorPath?: string;
+ sourceRepositoryUrl?: string;
+ sourceRef?: string;
+ sourceCommit?: string;
+ interactive?: boolean;
+ },
+ ) => {
+ const sourceRoot = resolve(options.sourceRoot);
+ const promptForMissingInputs = options.interactive === true ||
+ Deno.stdin.isTerminal();
+ const publishRoot = await resolvePublishRootOption({
+ sourceRoot,
+ publishRoot: options.publishRoot,
+ interactive: promptForMissingInputs,
+ });
+ const meshBase = await resolveMeshBaseOption(
+ {
+ meshBase: options.meshBase,
+ interactive: promptForMissingInputs,
+ },
+ "deploy gh-pages",
+ "an interactive terminal",
+ );
+ const request = {
+ meshBase,
+ includeNoJekyll: options.nojekyll === false ? false : undefined,
+ ...(options.cname !== undefined
+ ? { cname: options.cname }
+ : {}),
+ ...(resolveGHPagesSourceBindingOption(options) ?? {}),
+ };
+ const allowDirtyPublicationRoot =
+ options.allowDirtyPublishRoot === true;
+ const commit = resolveGHPagesCommitOption({
+ commit: options.commit,
+ commitMessage: options.commitMessage,
+ });
+
+ if (options.dryRun === true) {
+ const plan = await planGHPagesDeployBootstrap({
+ sourceRoot,
+ publishRoot,
+ request,
+ allowDirtyPublicationRoot,
+ commit,
+ });
+ console.log(describeGHPagesDeployBootstrapPlan(plan));
+ return;
+ }
+
+ const { operationalLogger, auditLogger } = createRuntimeLoggers({
+ logDir: resolveOptionalCliLogDir(),
+ });
+
+ await auditLogger.command("deploy.ghPages", {
+ sourceRoot,
+ publishRoot,
+ meshBase,
+ localMode: true,
+ localCommit: commit !== undefined,
+ });
+
+ const result = await executeGHPagesDeployBootstrap({
+ sourceRoot,
+ publishRoot,
+ request,
+ allowDirtyPublicationRoot,
+ commit,
+ operationalLogger,
+ auditLogger,
+ });
+ console.log(describeGHPagesDeployBootstrapResult(result));
+ for (const path of result.createdPaths) {
+ console.log(path);
+ }
+ for (const path of result.updatedPaths) {
+ console.log(path);
+ }
+ if (result.materializedSource) {
+ for (const path of result.materializedSource.createdPaths) {
+ console.log(path);
+ }
+ }
+ }),
+ ),
+ )
.command(
"mesh",
new Command()
@@ -767,7 +989,7 @@ export async function runWeaveCli(args: string[]): Promise {
options.meshRoot,
);
const meshBase = await resolveMeshBaseOption(options);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -842,7 +1064,7 @@ export async function runWeaveCli(args: string[]): Promise {
"knop add-reference requires --reference-role",
(message) => new KnopAddReferenceInputError(message),
);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -896,7 +1118,7 @@ export async function runWeaveCli(args: string[]): Promise {
);
const meshRoot = resolve(options.meshRoot);
const workspaceRoot = await inferCliWorkspaceRoot(meshRoot);
- const logDir = join(workspaceRoot, ".weave", "logs");
+ const logDir = resolveCliLogDir(workspaceRoot);
const { operationalLogger, auditLogger } = createRuntimeLoggers({
logDir,
});
@@ -966,8 +1188,46 @@ async function inferCliWorkspaceRoot(meshRoot: string): Promise {
return (await loadOperationalLocalPathPolicy(meshRoot)).workspaceRoot;
}
+function resolveCliLogDir(workspaceRoot: string): string {
+ return resolveOptionalCliLogDir() ?? join(workspaceRoot, ".weave", "logs");
+}
+
+function resolveOptionalCliLogDir(): string | undefined {
+ let value: string | undefined;
+ try {
+ value = Deno.env.get(CLI_LOG_DIR_ENV_VAR);
+ } catch {
+ return undefined;
+ }
+
+ const trimmed = value?.trim();
+ return trimmed && trimmed.length > 0 ? resolve(trimmed) : undefined;
+}
+
+function resolveHistoryTrackingPolicyOption(
+ value: string | undefined,
+): HistoryTrackingPolicy | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ if (
+ HISTORY_TRACKING_POLICY_VALUES.includes(
+ value as HistoryTrackingPolicy,
+ )
+ ) {
+ return value as HistoryTrackingPolicy;
+ }
+
+ throw new WeaveInputError(
+ `Unsupported history tracking policy: ${value}`,
+ );
+}
+
async function resolveMeshBaseOption(
options: { meshBase?: string; interactive?: boolean },
+ commandName = "mesh create",
+ interactiveHint = "--interactive",
): Promise {
if (
typeof options.meshBase === "string" && options.meshBase.trim().length > 0
@@ -977,7 +1237,7 @@ async function resolveMeshBaseOption(
if (!options.interactive) {
throw new MeshCreateInputError(
- "mesh create requires --mesh-base or --interactive",
+ `${commandName} requires --mesh-base or ${interactiveHint}`,
);
}
@@ -989,6 +1249,140 @@ async function resolveMeshBaseOption(
});
}
+async function resolvePublishRootOption(
+ options: {
+ sourceRoot: string;
+ publishRoot?: string;
+ interactive?: boolean;
+ },
+): Promise {
+ if (
+ typeof options.publishRoot === "string" &&
+ options.publishRoot.trim().length > 0
+ ) {
+ return resolve(options.publishRoot);
+ }
+
+ if (!options.interactive) {
+ throw new GHPagesDeployInputError(
+ "deploy gh-pages requires --publish-root, a deploy profile value, or an interactive terminal",
+ );
+ }
+
+ const defaultPublishRoot = resolve(
+ options.sourceRoot,
+ "..",
+ `${basename(options.sourceRoot)}-gh-pages`,
+ );
+
+ const value = await Input.prompt({
+ message: "Publication worktree path",
+ default: defaultPublishRoot,
+ validate(value) {
+ return value.trim().length > 0 || "publishRoot is required";
+ },
+ });
+ return resolve(value);
+}
+
+function resolveGHPagesCommitOption(
+ options: {
+ commit?: boolean;
+ commitMessage?: string;
+ },
+): { message?: string } | undefined {
+ if (options.commit !== true) {
+ if (options.commitMessage !== undefined) {
+ throw new GHPagesDeployInputError(
+ "deploy gh-pages --commit-message requires --commit",
+ );
+ }
+ return undefined;
+ }
+
+ return options.commitMessage === undefined ? {} : {
+ message: options.commitMessage,
+ };
+}
+
+function resolveGHPagesSourceBindingOption(
+ options: {
+ sourcePath?: string;
+ targetPath?: string;
+ designatorPath?: string;
+ sourceRepositoryUrl?: string;
+ sourceRef?: string;
+ sourceCommit?: string;
+ },
+):
+ | {
+ source: {
+ sourcePath: string;
+ designatorPath: string;
+ targetPath?: string;
+ sourceRepositoryUrl: string;
+ sourceRepositoryRef: string;
+ sourceRepositoryCommit?: string;
+ };
+ }
+ | undefined {
+ const hasSourceBindingOption = [
+ options.sourcePath,
+ options.targetPath,
+ options.designatorPath,
+ options.sourceRepositoryUrl,
+ options.sourceRef,
+ options.sourceCommit,
+ ].some((value) => value !== undefined);
+
+ if (!hasSourceBindingOption) {
+ return undefined;
+ }
+
+ return {
+ source: {
+ sourcePath: resolveRequiredOptionValue(
+ options.sourcePath,
+ "deploy gh-pages materialization requires --source-path",
+ (message) => new GHPagesDeployInputError(message),
+ ),
+ designatorPath: resolveRequiredOptionValue(
+ options.designatorPath,
+ "deploy gh-pages materialization requires --designator-path",
+ (message) => new GHPagesDeployInputError(message),
+ ),
+ ...(options.targetPath
+ ? {
+ targetPath: resolveRequiredOptionValue(
+ options.targetPath,
+ "deploy gh-pages --target-path is required",
+ (message) => new GHPagesDeployInputError(message),
+ ),
+ }
+ : {}),
+ sourceRepositoryUrl: resolveRequiredOptionValue(
+ options.sourceRepositoryUrl,
+ "deploy gh-pages materialization requires --source-repository-url",
+ (message) => new GHPagesDeployInputError(message),
+ ),
+ sourceRepositoryRef: resolveRequiredOptionValue(
+ options.sourceRef,
+ "deploy gh-pages materialization requires --source-ref",
+ (message) => new GHPagesDeployInputError(message),
+ ),
+ ...(options.sourceCommit
+ ? {
+ sourceRepositoryCommit: resolveRequiredOptionValue(
+ options.sourceCommit,
+ "deploy gh-pages --source-commit is required",
+ (message) => new GHPagesDeployInputError(message),
+ ),
+ }
+ : {}),
+ },
+ };
+}
+
function printExtractAllTermsPreview(
designatorPaths: readonly string[],
): void {
@@ -1307,7 +1701,9 @@ function getCliErrorMessage(error: unknown): string {
error instanceof KnopCreateInputError ||
error instanceof KnopCreateRuntimeError ||
error instanceof MeshCreateInputError ||
- error instanceof MeshCreateRuntimeError
+ error instanceof MeshCreateRuntimeError ||
+ error instanceof GHPagesDeployInputError ||
+ error instanceof GHPagesDeployRuntimeError
) {
return error.message;
}
diff --git a/src/core/extract/extract.ts b/src/core/extract/extract.ts
index 94fe904..fae4e17 100644
--- a/src/core/extract/extract.ts
+++ b/src/core/extract/extract.ts
@@ -52,9 +52,19 @@ export interface ResolvedExtractRequest extends ExtractRequest {
sourceDesignatorPath: string;
sourceStatePath?: string;
sourceResolutionMode?: "current" | "pinned";
+ sourceEvidence?: ExtractionSourceEvidence;
sourceWorkingLocalRelativePath: string;
}
+export interface ExtractionSourceEvidence {
+ sourceStatePath?: string;
+ sourceManifestationPath?: string;
+ sourceLocatedFilePath?: string;
+ sourceLocalRelativePath?: string;
+ sourceDigest?: string;
+ observedAt?: string;
+}
+
export interface ExtractPlan {
meshBase: string;
designatorPath: string;
@@ -64,6 +74,7 @@ export interface ExtractPlan {
sourceStateIri?: string;
sourceStatePath?: string;
sourceResolutionMode: "current" | "pinned";
+ sourceEvidence?: ExtractionSourceEvidence;
createdFiles: readonly PlannedFile[];
updatedFiles: readonly PlannedFile[];
}
@@ -86,7 +97,7 @@ export function planExtract(request: ResolvedExtractRequest): ExtractPlan {
"sourceDesignatorPath",
);
const sourceResolutionMode = request.sourceResolutionMode === undefined
- ? request.sourceStatePath === undefined ? "current" : "pinned"
+ ? "current"
: normalizeSourceResolutionMode(request.sourceResolutionMode);
const sourceStatePath = request.sourceStatePath === undefined
? undefined
@@ -99,11 +110,9 @@ export function planExtract(request: ResolvedExtractRequest): ExtractPlan {
"sourceStatePath is required for pinned extraction",
);
}
- if (sourceResolutionMode === "current" && sourceStatePath !== undefined) {
- throw new ExtractInputError(
- "sourceStatePath is only valid for pinned extraction",
- );
- }
+ const sourceEvidence = normalizeExtractionSourceEvidence(
+ request.sourceEvidence,
+ );
const sourceWorkingLocalRelativePath = normalizeWorkingLocalRelativePath(
request.sourceWorkingLocalRelativePath,
);
@@ -122,7 +131,7 @@ export function planExtract(request: ResolvedExtractRequest): ExtractPlan {
meshBase,
designatorPath,
extractionSourceIri:
- new URL(`${knopPath}/_inventory#extraction-source`, meshBase).href,
+ new URL(`${knopPath}/_sources#extraction-source`, meshBase).href,
sourceArtifactIri: new URL(sourceDesignatorPath, meshBase).href,
sourceDesignatorPath,
...(sourceStatePath
@@ -130,6 +139,7 @@ export function planExtract(request: ResolvedExtractRequest): ExtractPlan {
: {}),
sourceStatePath,
sourceResolutionMode,
+ ...(sourceEvidence ? { sourceEvidence } : {}),
createdFiles: [
{
path: `${knopPath}/_meta/meta.ttl`,
@@ -143,9 +153,17 @@ export function planExtract(request: ResolvedExtractRequest): ExtractPlan {
contents: renderExtractKnopInventoryTurtle(
meshBase,
designatorPath,
+ ),
+ },
+ {
+ path: `${knopPath}/_sources/sources.ttl`,
+ contents: renderExtractKnopSourcesTurtle(
+ meshBase,
+ designatorPath,
sourceDesignatorPath,
sourceResolutionMode,
sourceStatePath,
+ sourceEvidence,
),
},
],
@@ -171,6 +189,61 @@ function normalizeSourceResolutionMode(
throw new ExtractInputError("sourceResolutionMode must be current or pinned");
}
+function normalizeExtractionSourceEvidence(
+ sourceEvidence: ExtractionSourceEvidence | undefined,
+): ExtractionSourceEvidence | undefined {
+ if (sourceEvidence === undefined) {
+ return undefined;
+ }
+
+ const normalized: ExtractionSourceEvidence = {};
+ if (sourceEvidence.sourceStatePath !== undefined) {
+ normalized.sourceStatePath = normalizeRelativeIriPath(
+ sourceEvidence.sourceStatePath,
+ "sourceEvidence.sourceStatePath",
+ );
+ }
+ if (sourceEvidence.sourceManifestationPath !== undefined) {
+ normalized.sourceManifestationPath = normalizeRelativeIriPath(
+ sourceEvidence.sourceManifestationPath,
+ "sourceEvidence.sourceManifestationPath",
+ );
+ }
+ if (sourceEvidence.sourceLocatedFilePath !== undefined) {
+ normalized.sourceLocatedFilePath = normalizeRelativeIriPath(
+ sourceEvidence.sourceLocatedFilePath,
+ "sourceEvidence.sourceLocatedFilePath",
+ );
+ }
+ if (sourceEvidence.sourceLocalRelativePath !== undefined) {
+ normalized.sourceLocalRelativePath = normalizeWorkingLocalRelativePath(
+ sourceEvidence.sourceLocalRelativePath,
+ );
+ }
+ if (sourceEvidence.sourceDigest !== undefined) {
+ normalized.sourceDigest = normalizeNonEmptyLiteral(
+ sourceEvidence.sourceDigest,
+ "sourceEvidence.sourceDigest",
+ );
+ }
+ if (sourceEvidence.observedAt !== undefined) {
+ normalized.observedAt = normalizeNonEmptyLiteral(
+ sourceEvidence.observedAt,
+ "sourceEvidence.observedAt",
+ );
+ }
+
+ return Object.keys(normalized).length === 0 ? undefined : normalized;
+}
+
+function normalizeNonEmptyLiteral(value: string, fieldName: string): string {
+ const trimmed = value.trim();
+ if (trimmed.length === 0) {
+ throw new ExtractInputError(`${fieldName} must not be empty`);
+ }
+ return trimmed;
+}
+
function normalizeMeshBase(meshBase: string): string {
const trimmed = meshBase.trim();
if (trimmed.length === 0) {
@@ -378,10 +451,10 @@ function renderLegacyExtractMeshInventoryTurtle(
const locatedFileDeclarations = renderLocatedFileDeclarations([
"_mesh/_meta/meta.ttl",
"_mesh/_inventory/inventory.ttl",
- "_mesh/_meta/_history001/_s0001/meta-ttl/meta.ttl",
- "_mesh/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl",
- "_mesh/_inventory/_history001/_s0002/inventory-ttl/inventory.ttl",
- "_mesh/_inventory/_history001/_s0003/inventory-ttl/inventory.ttl",
+ "_mesh/_meta/_history001/_s0001/ttl/meta.ttl",
+ "_mesh/_inventory/_history001/_s0001/ttl/inventory.ttl",
+ "_mesh/_inventory/_history001/_s0002/ttl/inventory.ttl",
+ "_mesh/_inventory/_history001/_s0003/ttl/inventory.ttl",
`${rootKnopPath}/_inventory/inventory.ttl`,
`${sourceKnopPath}/_inventory/inventory.ttl`,
`${knopPath}/_inventory/inventory.ttl`,
@@ -396,15 +469,15 @@ function renderLegacyExtractMeshInventoryTurtle(
"_mesh/_meta/index.html",
"_mesh/_meta/_history001/index.html",
"_mesh/_meta/_history001/_s0001/index.html",
- "_mesh/_meta/_history001/_s0001/meta-ttl/index.html",
+ "_mesh/_meta/_history001/_s0001/ttl/index.html",
"_mesh/_inventory/index.html",
"_mesh/_inventory/_history001/index.html",
"_mesh/_inventory/_history001/_s0001/index.html",
- "_mesh/_inventory/_history001/_s0001/inventory-ttl/index.html",
+ "_mesh/_inventory/_history001/_s0001/ttl/index.html",
"_mesh/_inventory/_history001/_s0002/index.html",
- "_mesh/_inventory/_history001/_s0002/inventory-ttl/index.html",
+ "_mesh/_inventory/_history001/_s0002/ttl/index.html",
"_mesh/_inventory/_history001/_s0003/index.html",
- "_mesh/_inventory/_history001/_s0003/inventory-ttl/index.html",
+ "_mesh/_inventory/_history001/_s0003/ttl/index.html",
]);
return `@base <${meshBase}> .
@@ -443,13 +516,13 @@ ${sourceKnopBlock}<${knopPath}> a sflo:Knop ;
<_mesh/_meta/_history001/_s0001> a sflo:HistoricalState ;
sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ;
- sflo:hasManifestation <_mesh/_meta/_history001/_s0001/meta-ttl> ;
- sflo:locatedFileForState <_mesh/_meta/_history001/_s0001/meta-ttl/meta.ttl> ;
+ 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/meta-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile <_mesh/_meta/_history001/_s0001/meta-ttl/meta.ttl> ;
- sflo:hasResourcePage <_mesh/_meta/_history001/_s0001/meta-ttl/index.html> .
+<_mesh/_meta/_history001/_s0001/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile <_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> ;
@@ -469,35 +542,35 @@ ${sourceKnopBlock}<${knopPath}> a sflo:Knop ;
<_mesh/_inventory/_history001/_s0001> a sflo:HistoricalState ;
sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ;
- sflo:hasManifestation <_mesh/_inventory/_history001/_s0001/inventory-ttl> ;
- sflo:locatedFileForState <_mesh/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl> ;
+ 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/inventory-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile <_mesh/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl> ;
- sflo:hasResourcePage <_mesh/_inventory/_history001/_s0001/inventory-ttl/index.html> .
+<_mesh/_inventory/_history001/_s0001/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile <_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/inventory-ttl> ;
- sflo:locatedFileForState <_mesh/_inventory/_history001/_s0002/inventory-ttl/inventory.ttl> ;
+ 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/inventory-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile <_mesh/_inventory/_history001/_s0002/inventory-ttl/inventory.ttl> ;
- sflo:hasResourcePage <_mesh/_inventory/_history001/_s0002/inventory-ttl/index.html> .
+<_mesh/_inventory/_history001/_s0002/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile <_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/inventory-ttl> ;
- sflo:locatedFileForState <_mesh/_inventory/_history001/_s0003/inventory-ttl/inventory.ttl> ;
+ 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/inventory-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile <_mesh/_inventory/_history001/_s0003/inventory-ttl/inventory.ttl> ;
- sflo:hasResourcePage <_mesh/_inventory/_history001/_s0003/inventory-ttl/index.html> .
+<_mesh/_inventory/_history001/_s0003/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile <_mesh/_inventory/_history001/_s0003/ttl/inventory.ttl> ;
+ sflo:hasResourcePage <_mesh/_inventory/_history001/_s0003/ttl/index.html> .
${locatedFileDeclarations}
@@ -636,7 +709,22 @@ function hasLegacyCarriedExtractMeshInventoryShape(
return payloadArtifactPaths.length === 1 &&
payloadArtifactPaths[0] === sourcePayloadDesignatorPath &&
meshKnopPaths.length === expectedMeshKnopPaths.length &&
- expectedMeshKnopPaths.every((path) => meshKnopPaths.includes(path));
+ expectedMeshKnopPaths.every((path) => meshKnopPaths.includes(path)) &&
+ hasNamedNodeFact(
+ quads,
+ meshBase,
+ "_mesh/_inventory/_history001",
+ SFLO_LATEST_HISTORICAL_STATE_IRI,
+ "_mesh/_inventory/_history001/_s0003",
+ ) &&
+ hasLiteralFact(
+ quads,
+ meshBase,
+ "_mesh/_inventory/_history001",
+ SFLO_NEXT_STATE_ORDINAL_IRI,
+ "4",
+ XSD_NON_NEGATIVE_INTEGER_IRI,
+ );
}
function uniquePaths(paths: readonly string[]): string[] {
@@ -664,17 +752,11 @@ function renderResourcePageDeclarations(paths: readonly string[]): string {
function renderExtractKnopInventoryTurtle(
meshBase: string,
designatorPath: string,
- sourceDesignatorPath: string,
- sourceResolutionMode: "current" | "pinned",
- sourceStatePath?: string,
): string {
const knopPath = toKnopPath(designatorPath);
- const extractionSourceFacts = sourceResolutionMode === "pinned"
- ? ` sflo:hasTargetArtifact <${sourceDesignatorPath}> ;
- sflo:hasRequestedTargetState <${sourceStatePath}> ;
- sflo:hasArtifactResolutionMode <${SFLO_ARTIFACT_RESOLUTION_MODE_PINNED_IRI}> .`
- : ` sflo:hasTargetArtifact <${sourceDesignatorPath}> ;
- sflo:hasArtifactResolutionMode <${SFLO_ARTIFACT_RESOLUTION_MODE_CURRENT_IRI}> .`;
+ const sourceRegistryPath = `${knopPath}/_sources`;
+ const sourcesFilePath = `${sourceRegistryPath}/sources.ttl`;
+ const extractionSourcePath = `${sourceRegistryPath}#extraction-source`;
return `@base <${meshBase}> .
${SFLO_TURTLE_PREFIX_DECLARATION}
@@ -682,24 +764,157 @@ ${SFLO_TURTLE_PREFIX_DECLARATION}
<${knopPath}> a sflo:Knop ;
sflo:hasKnopMetadata <${knopPath}/_meta> ;
sflo:hasKnopInventory <${knopPath}/_inventory> ;
- sflo:hasExtractionSource <${knopPath}/_inventory#extraction-source> ;
+ sflo:hasKnopSourceRegistry <${sourceRegistryPath}> ;
+ sflo:hasExtractionSource <${extractionSourcePath}> ;
sflo:hasWorkingKnopInventoryFile <${knopPath}/_inventory/inventory.ttl> .
-<${knopPath}/_inventory#extraction-source> a sflo:ExtractionSource ;
-${extractionSourceFacts}
-
<${knopPath}/_meta> a sflo:KnopMetadata, sflo:DigitalArtifact, sflo:RdfDocument ;
sflo:hasWorkingLocatedFile <${knopPath}/_meta/meta.ttl> .
<${knopPath}/_inventory> a sflo:KnopInventory, sflo:DigitalArtifact, sflo:RdfDocument ;
sflo:hasWorkingLocatedFile <${knopPath}/_inventory/inventory.ttl> .
+<${sourceRegistryPath}> a sflo:KnopSourceRegistry, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sflo:hasWorkingLocatedFile <${sourcesFilePath}> .
+
<${knopPath}/_meta/meta.ttl> a sflo:LocatedFile, sflo:RdfDocument .
<${knopPath}/_inventory/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument .
+
+<${sourcesFilePath}> a sflo:LocatedFile, sflo:RdfDocument .
+`;
+}
+
+function renderExtractKnopSourcesTurtle(
+ meshBase: string,
+ designatorPath: string,
+ sourceDesignatorPath: string,
+ sourceResolutionMode: "current" | "pinned",
+ sourceStatePath?: string,
+ sourceEvidence?: ExtractionSourceEvidence,
+): string {
+ const sourceRegistryPath = `${toKnopPath(designatorPath)}/_sources`;
+ const sourcesFilePath = `${sourceRegistryPath}/sources.ttl`;
+ const extractionSourcePath = `${sourceRegistryPath}#extraction-source`;
+ const extractionSourceFacts = renderExtractionSourceFacts(
+ sourceDesignatorPath,
+ sourceResolutionMode,
+ sourceStatePath,
+ sourceEvidence,
+ );
+
+ return `@base <${meshBase}> .
+${SFLO_TURTLE_PREFIX_DECLARATION}
+
+<${sourceRegistryPath}> a sflo:KnopSourceRegistry, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sflo:hasWorkingLocatedFile <${sourcesFilePath}> ;
+ sflo:hasSourceBinding <${extractionSourcePath}> .
+
+<${extractionSourcePath}> a sflo:ExtractionSource ;
+${extractionSourceFacts}
+
+<${sourcesFilePath}> a sflo:LocatedFile, sflo:RdfDocument .
`;
}
+function renderExtractionSourceFacts(
+ sourceDesignatorPath: string,
+ sourceResolutionMode: "current" | "pinned",
+ sourceStatePath: string | undefined,
+ sourceEvidence: ExtractionSourceEvidence | undefined,
+): string {
+ const facts: [string, string][] = [
+ ["sflo:hasTargetArtifact", `<${sourceDesignatorPath}>`],
+ ];
+ if (sourceResolutionMode === "pinned") {
+ facts.push(["sflo:hasRequestedTargetState", `<${sourceStatePath}>`]);
+ }
+ facts.push([
+ "sflo:hasArtifactResolutionMode",
+ `<${
+ sourceResolutionMode === "pinned"
+ ? SFLO_ARTIFACT_RESOLUTION_MODE_PINNED_IRI
+ : SFLO_ARTIFACT_RESOLUTION_MODE_CURRENT_IRI
+ }>`,
+ ]);
+ facts.push(...toExtractionSourceEvidenceFacts(sourceEvidence));
+
+ return facts.map(([predicate, object], index) =>
+ ` ${predicate} ${object}${index === facts.length - 1 ? " ." : " ;"}`
+ ).join("\n");
+}
+
+function toExtractionSourceEvidenceFacts(
+ sourceEvidence: ExtractionSourceEvidence | undefined,
+): [string, string][] {
+ if (!sourceEvidence) {
+ return [];
+ }
+
+ const facts: [string, string][] = [];
+ if (sourceEvidence.sourceStatePath !== undefined) {
+ facts.push([
+ "sflo:hasObservedSourceState",
+ `<${sourceEvidence.sourceStatePath}>`,
+ ]);
+ }
+ if (sourceEvidence.sourceManifestationPath !== undefined) {
+ facts.push([
+ "sflo:hasObservedSourceManifestation",
+ `<${sourceEvidence.sourceManifestationPath}>`,
+ ]);
+ }
+ if (sourceEvidence.sourceLocatedFilePath !== undefined) {
+ facts.push([
+ "sflo:hasObservedSourceLocatedFile",
+ `<${sourceEvidence.sourceLocatedFilePath}>`,
+ ]);
+ }
+ if (sourceEvidence.sourceLocalRelativePath !== undefined) {
+ facts.push([
+ "sflo:observedSourceLocalRelativePath",
+ `"${escapeTurtleString(sourceEvidence.sourceLocalRelativePath)}"`,
+ ]);
+ }
+ if (sourceEvidence.sourceDigest !== undefined) {
+ facts.push([
+ "sflo:observedSourceDigest",
+ `"${escapeTurtleString(sourceEvidence.sourceDigest)}"`,
+ ]);
+ }
+ if (sourceEvidence.observedAt !== undefined) {
+ facts.push([
+ "sflo:observedAt",
+ `"${escapeTurtleString(sourceEvidence.observedAt)}"`,
+ ]);
+ }
+
+ return facts;
+}
+
+function escapeTurtleString(value: string): string {
+ return value.replace(/[\b\t\n\f\r"\\]/g, (character) => {
+ switch (character) {
+ case "\b":
+ return "\\b";
+ case "\t":
+ return "\\t";
+ case "\n":
+ return "\\n";
+ case "\f":
+ return "\\f";
+ case "\r":
+ return "\\r";
+ case '"':
+ return '\\"';
+ case "\\":
+ return "\\\\";
+ default:
+ return character;
+ }
+ });
+}
+
function renderExtractKnopMetadataTurtle(
meshBase: string,
designatorPath: string,
diff --git a/src/core/extract/extract_test.ts b/src/core/extract/extract_test.ts
index d150e2b..c18849e 100644
--- a/src/core/extract/extract_test.ts
+++ b/src/core/extract/extract_test.ts
@@ -1,4 +1,10 @@
-import { assertEquals, assertStringIncludes, assertThrows } from "@std/assert";
+import {
+ assertEquals,
+ assertFalse,
+ assertStringIncludes,
+ assertThrows,
+} from "@std/assert";
+import { compareRdfContent } from "../../../dependencies/github.com/spectacular-voyage/accord/src/checker/compare_rdf.ts";
import { readMeshAliceBioBranchFile } from "../../../tests/support/mesh_alice_bio_fixture.ts";
import { ExtractInputError, planExtract } from "./extract.ts";
import { KnopCreateInputError } from "../knop/create.ts";
@@ -33,6 +39,9 @@ const rootSourcePreExtractMeshInventoryTurtle =
`;
Deno.test("planExtract renders the first non-woven bob extraction artifacts", async () => {
+ const sourceDigest = await sha256Digest(
+ await readMeshAliceBioBranchFile("11-alice-bio-v2-woven", "alice-bio.ttl"),
+ );
const plan = planExtract({
meshBase: "https://semantic-flow.github.io/mesh-alice-bio/",
currentMeshInventoryTurtle: await readMeshAliceBioBranchFile(
@@ -42,12 +51,17 @@ Deno.test("planExtract renders the first non-woven bob extraction artifacts", as
designatorPath: "bob",
sourceDesignatorPath: "alice/bio",
sourceStatePath: "alice/bio/_history001/_s0002",
+ sourceEvidence: {
+ sourceLocatedFilePath: "alice-bio.ttl",
+ sourceDigest,
+ observedAt: "2026-05-16T12:00:00Z\nmanual review",
+ },
sourceWorkingLocalRelativePath: "alice-bio.ttl",
});
assertEquals(
plan.extractionSourceIri,
- "https://semantic-flow.github.io/mesh-alice-bio/bob/_knop/_inventory#extraction-source",
+ "https://semantic-flow.github.io/mesh-alice-bio/bob/_knop/_sources#extraction-source",
);
assertEquals(
plan.sourceArtifactIri,
@@ -57,11 +71,13 @@ Deno.test("planExtract renders the first non-woven bob extraction artifacts", as
plan.sourceStateIri,
"https://semantic-flow.github.io/mesh-alice-bio/alice/bio/_history001/_s0002",
);
+ assertEquals(plan.sourceResolutionMode, "current");
assertEquals(
plan.createdFiles.map((file) => file.path),
[
"bob/_knop/_meta/meta.ttl",
"bob/_knop/_inventory/inventory.ttl",
+ "bob/_knop/_sources/sources.ttl",
],
);
assertEquals(
@@ -75,26 +91,42 @@ Deno.test("planExtract renders the first non-woven bob extraction artifacts", as
"bob/_knop/_meta/meta.ttl",
),
);
- assertEquals(
+ assertStringIncludes(
plan.createdFiles[1]?.contents ?? "",
- await readMeshAliceBioBranchFile(
- "12-bob-extracted",
- "bob/_knop/_inventory/inventory.ttl",
- ),
+ `sflo:hasKnopSourceRegistry ;
+ sflo:hasExtractionSource ;`,
);
- // Keep this explicit so future extract changes still pin the source binding
- // to a historical state in the rendered KnopInventory file.
assertStringIncludes(
- plan.createdFiles[1]?.contents ?? "",
- "sflo:hasRequestedTargetState ;",
+ plan.createdFiles[2]?.contents ?? "",
+ "sflo:hasArtifactResolutionMode ;",
);
- assertEquals(
- plan.updatedFiles[0]?.contents ?? "",
- await readMeshAliceBioBranchFile(
- "12-bob-extracted",
- "_mesh/_inventory/inventory.ttl",
+ assertStringIncludes(
+ plan.createdFiles[2]?.contents ?? "",
+ `sflo:hasObservedSourceLocatedFile ;
+ sflo:observedSourceDigest "${sourceDigest}" ;`,
+ );
+ assertStringIncludes(
+ plan.createdFiles[2]?.contents ?? "",
+ 'sflo:observedAt "2026-05-16T12:00:00Z\\nmanual review" .',
+ );
+ assertFalse(
+ (plan.createdFiles[2]?.contents ?? "").includes(
+ "sflo:hasRequestedTargetState",
),
);
+ assertEquals(
+ await compareRdfContent({
+ left: encode(plan.updatedFiles[0]?.contents ?? ""),
+ right: encode(
+ await readMeshAliceBioBranchFile(
+ "12-bob-extracted",
+ "_mesh/_inventory/inventory.ttl",
+ ),
+ ),
+ path: "_mesh/_inventory/inventory.ttl",
+ }),
+ true,
+ );
});
Deno.test("planExtract accepts a root source payload when the root and source knops are the same", () => {
@@ -103,6 +135,7 @@ Deno.test("planExtract accepts a root source payload when the root and source kn
currentMeshInventoryTurtle: rootSourcePreExtractMeshInventoryTurtle,
designatorPath: "alice/bio",
sourceDesignatorPath: "",
+ sourceResolutionMode: "pinned",
sourceStatePath: "_history001/_s0001",
sourceWorkingLocalRelativePath: "root-person.ttl",
});
@@ -112,6 +145,7 @@ Deno.test("planExtract accepts a root source payload when the root and source kn
[
"alice/bio/_knop/_meta/meta.ttl",
"alice/bio/_knop/_inventory/inventory.ttl",
+ "alice/bio/_knop/_sources/sources.ttl",
],
);
assertStringIncludes(
@@ -162,7 +196,7 @@ Deno.test("planExtract accepts a root source payload when the root and source kn
1,
);
assertStringIncludes(
- plan.createdFiles[1]?.contents ?? "",
+ plan.createdFiles[2]?.contents ?? "",
`sflo:hasTargetArtifact <> ;
sflo:hasRequestedTargetState <_history001/_s0001> ;`,
);
@@ -221,6 +255,9 @@ Deno.test("planExtract preserves the original knop-planning error as the cause",
});
Deno.test("planExtract accepts a semantically equivalent source payload LocatedFile block", async () => {
+ const sourceDigest = await sha256Digest(
+ await readMeshAliceBioBranchFile("11-alice-bio-v2-woven", "alice-bio.ttl"),
+ );
const currentMeshInventoryTurtle = withRdfPrefix(
await readMeshAliceBioBranchFile(
"11-alice-bio-v2-woven",
@@ -237,15 +274,25 @@ Deno.test("planExtract accepts a semantically equivalent source payload LocatedF
designatorPath: "bob",
sourceDesignatorPath: "alice/bio",
sourceStatePath: "alice/bio/_history001/_s0002",
+ sourceEvidence: {
+ sourceLocatedFilePath: "alice-bio.ttl",
+ sourceDigest,
+ },
sourceWorkingLocalRelativePath: "alice-bio.ttl",
});
assertEquals(
- plan.updatedFiles[0]?.contents ?? "",
- await readMeshAliceBioBranchFile(
- "12-bob-extracted",
- "_mesh/_inventory/inventory.ttl",
- ),
+ await compareRdfContent({
+ left: encode(plan.updatedFiles[0]?.contents ?? ""),
+ right: encode(
+ await readMeshAliceBioBranchFile(
+ "12-bob-extracted",
+ "_mesh/_inventory/inventory.ttl",
+ ),
+ ),
+ path: "_mesh/_inventory/inventory.ttl",
+ }),
+ true,
);
});
@@ -260,3 +307,24 @@ function withRdfPrefix(turtle: string): string {
function countOccurrences(haystack: string, needle: string): number {
return haystack.split(needle).length - 1;
}
+
+function encode(value: string): Uint8Array {
+ return new TextEncoder().encode(value);
+}
+
+async function sha256Digest(contents: string): Promise {
+ const digest = await crypto.subtle.digest(
+ "SHA-256",
+ toArrayBuffer(encode(contents)),
+ );
+ const hex = [...new Uint8Array(digest)]
+ .map((byte) => byte.toString(16).padStart(2, "0"))
+ .join("");
+ return `sha256:${hex}`;
+}
+
+function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
+ const buffer = new ArrayBuffer(bytes.byteLength);
+ new Uint8Array(buffer).set(bytes);
+ return buffer;
+}
diff --git a/src/core/integrate/integrate_test.ts b/src/core/integrate/integrate_test.ts
index 5564f81..67fc8da 100644
--- a/src/core/integrate/integrate_test.ts
+++ b/src/core/integrate/integrate_test.ts
@@ -137,8 +137,8 @@ Deno.test(
" rdf:type sflo:Knop ;",
)
.replace(
- "<_mesh/_inventory/_history001/_s0002/inventory-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;",
- "<_mesh/_inventory/_history001/_s0002/inventory-ttl> rdf:type sflo:RdfDocument, sflo:ArtifactManifestation ;",
+ /(<_mesh\/_inventory\/_history\d+\/_s\d+\/ttl>) a sflo:ArtifactManifestation, sflo:RdfDocument ;/,
+ "$1 rdf:type sflo:RdfDocument, sflo:ArtifactManifestation ;",
);
const plan = planIntegrate({
diff --git a/src/core/knop/add_reference.ts b/src/core/knop/add_reference.ts
index 64a6a82..a5a1d46 100644
--- a/src/core/knop/add_reference.ts
+++ b/src/core/knop/add_reference.ts
@@ -24,8 +24,11 @@ const SFLO_HAS_ARTIFACT_HISTORY_IRI = `${SFLO_NAMESPACE}hasArtifactHistory`;
const SFLO_HAS_HISTORICAL_STATE_IRI = `${SFLO_NAMESPACE}hasHistoricalState`;
const SFLO_HAS_KNOP_INVENTORY_IRI = `${SFLO_NAMESPACE}hasKnopInventory`;
const SFLO_HAS_KNOP_METADATA_IRI = `${SFLO_NAMESPACE}hasKnopMetadata`;
+const SFLO_HAS_EXTRACTION_SOURCE_IRI = `${SFLO_NAMESPACE}hasExtractionSource`;
const SFLO_HAS_LOCATED_FILE_IRI = `${SFLO_NAMESPACE}hasLocatedFile`;
const SFLO_HAS_MANIFESTATION_IRI = `${SFLO_NAMESPACE}hasManifestation`;
+const SFLO_HAS_KNOP_SOURCE_REGISTRY_IRI =
+ `${SFLO_NAMESPACE}hasKnopSourceRegistry`;
const SFLO_HAS_REFERENCE_CATALOG_IRI = `${SFLO_NAMESPACE}hasReferenceCatalog`;
const SFLO_HAS_RESOURCE_PAGE_IRI = `${SFLO_NAMESPACE}hasResourcePage`;
const SFLO_HAS_WORKING_KNOP_INVENTORY_FILE_IRI =
@@ -37,6 +40,7 @@ const SFLO_HISTORY_ORDINAL_IRI = `${SFLO_NAMESPACE}historyOrdinal`;
const SFLO_KNOP_IRI = `${SFLO_NAMESPACE}Knop`;
const SFLO_KNOP_INVENTORY_IRI = `${SFLO_NAMESPACE}KnopInventory`;
const SFLO_KNOP_METADATA_IRI = `${SFLO_NAMESPACE}KnopMetadata`;
+const SFLO_KNOP_SOURCE_REGISTRY_IRI = `${SFLO_NAMESPACE}KnopSourceRegistry`;
const SFLO_LATEST_HISTORICAL_STATE_IRI =
`${SFLO_NAMESPACE}latestHistoricalState`;
const SFLO_LOCATED_FILE_FOR_STATE_IRI = `${SFLO_NAMESPACE}locatedFileForState`;
@@ -89,6 +93,13 @@ export class KnopAddReferenceInputError extends Error {
type KnopInventoryShape = "unwoven" | "woven";
+interface CurrentKnopSourceFacts {
+ sourceRegistryPath?: string;
+ sourcesFilePath?: string;
+ extractionSourcePath?: string;
+ extractionSourceBlock?: string;
+}
+
export function planKnopAddReference(
request: ResolvedKnopAddReferenceRequest,
): KnopAddReferencePlan {
@@ -221,10 +232,259 @@ function renderUpdatedKnopInventoryTurtle(
currentKnopInventoryTurtle,
knopPath,
);
-
- return shape === "woven"
+ const rendered = shape === "woven"
? renderWovenKnopInventoryWithReferenceCatalog(meshBase, knopPath)
: renderUnwovenKnopInventoryWithReferenceCatalog(meshBase, knopPath);
+
+ return renderKnopInventoryWithPreservedSourceFacts({
+ meshBase,
+ currentKnopInventoryTurtle,
+ renderedKnopInventoryTurtle: rendered,
+ knopPath,
+ });
+}
+
+function renderKnopInventoryWithPreservedSourceFacts(options: {
+ meshBase: string;
+ currentKnopInventoryTurtle: string;
+ renderedKnopInventoryTurtle: string;
+ knopPath: string;
+}): string {
+ const sourceFacts = resolveCurrentKnopSourceFacts(options);
+ if (
+ sourceFacts.sourceRegistryPath === undefined &&
+ sourceFacts.extractionSourcePath === undefined
+ ) {
+ return options.renderedKnopInventoryTurtle;
+ }
+
+ let blocks = splitTurtleBlocks(options.renderedKnopInventoryTurtle);
+ const knopBlockIndex = findSubjectBlockIndex(blocks, options.knopPath);
+ if (knopBlockIndex === -1) {
+ throw new KnopAddReferenceInputError(
+ `rendered knop inventory is missing the ${options.knopPath} block`,
+ );
+ }
+
+ blocks = replaceSubjectBlock(
+ blocks,
+ options.knopPath,
+ renderKnopBlockWithSourceFacts(blocks[knopBlockIndex]!, sourceFacts),
+ );
+ if (
+ sourceFacts.sourceRegistryPath !== undefined &&
+ sourceFacts.sourcesFilePath !== undefined
+ ) {
+ blocks = upsertSubjectBlockAfter(
+ blocks,
+ `${options.knopPath}/_inventory`,
+ sourceFacts.sourceRegistryPath,
+ renderSubjectPredicateBlock(
+ sourceFacts.sourceRegistryPath,
+ "sflo:KnopSourceRegistry, sflo:DigitalArtifact, sflo:RdfDocument",
+ [
+ `sflo:hasWorkingLocatedFile <${sourceFacts.sourcesFilePath}>`,
+ ],
+ ),
+ );
+ blocks = upsertSubjectBlockAfter(
+ blocks,
+ sourceFacts.sourceRegistryPath,
+ sourceFacts.sourcesFilePath,
+ renderLocatedFileBlock(sourceFacts.sourcesFilePath),
+ );
+ }
+ if (
+ sourceFacts.extractionSourcePath !== undefined &&
+ sourceFacts.extractionSourceBlock !== undefined
+ ) {
+ blocks = upsertSubjectBlockAfter(
+ blocks,
+ sourceFacts.sourceRegistryPath ?? `${options.knopPath}/_inventory`,
+ sourceFacts.extractionSourcePath,
+ sourceFacts.extractionSourceBlock,
+ );
+ }
+
+ return `${blocks.join("\n\n")}\n`;
+}
+
+function resolveCurrentKnopSourceFacts(options: {
+ meshBase: string;
+ currentKnopInventoryTurtle: string;
+ knopPath: string;
+}): CurrentKnopSourceFacts {
+ const errorMessage =
+ `Could not resolve carried source facts from current KnopInventory for ${options.knopPath}.`;
+ const quads = parseKnopInventoryQuads(
+ options.meshBase,
+ options.currentKnopInventoryTurtle,
+ errorMessage,
+ );
+ const knopIri = new URL(options.knopPath, options.meshBase).href;
+ const sourceRegistryIri = requireOptionalNamedNodeObject(
+ quads,
+ knopIri,
+ SFLO_HAS_KNOP_SOURCE_REGISTRY_IRI,
+ errorMessage,
+ );
+ const extractionSourceIri = requireOptionalNamedNodeObject(
+ quads,
+ knopIri,
+ SFLO_HAS_EXTRACTION_SOURCE_IRI,
+ errorMessage,
+ );
+ const sourceRegistryPath = sourceRegistryIri === undefined
+ ? undefined
+ : toMeshPath(options.meshBase, sourceRegistryIri, errorMessage);
+ const extractionSourcePath = extractionSourceIri === undefined
+ ? undefined
+ : toMeshPath(options.meshBase, extractionSourceIri, errorMessage);
+ const sourcesFilePath = sourceRegistryIri === undefined
+ ? undefined
+ : requireOptionalNamedNodeObject(
+ quads,
+ sourceRegistryIri,
+ SFLO_HAS_WORKING_LOCATED_FILE_IRI,
+ errorMessage,
+ );
+ const sourcesFileRelativePath = sourcesFilePath === undefined
+ ? undefined
+ : toMeshPath(options.meshBase, sourcesFilePath, errorMessage);
+ const extractionSourceBlock = extractionSourcePath === undefined
+ ? undefined
+ : splitTurtleBlocks(options.currentKnopInventoryTurtle).find((block) =>
+ getSubjectPathFromBlock(block) === extractionSourcePath
+ );
+
+ if (
+ sourceRegistryPath !== undefined &&
+ !hasNamedNodeFact(
+ quads,
+ options.meshBase,
+ sourceRegistryPath,
+ RDF_TYPE_IRI,
+ SFLO_KNOP_SOURCE_REGISTRY_IRI,
+ )
+ ) {
+ throw new KnopAddReferenceInputError(errorMessage);
+ }
+ if (
+ sourceRegistryPath !== undefined && sourcesFileRelativePath === undefined
+ ) {
+ throw new KnopAddReferenceInputError(errorMessage);
+ }
+
+ return {
+ ...(sourceRegistryPath !== undefined ? { sourceRegistryPath } : {}),
+ ...(sourcesFileRelativePath !== undefined
+ ? { sourcesFilePath: sourcesFileRelativePath }
+ : {}),
+ ...(extractionSourcePath !== undefined ? { extractionSourcePath } : {}),
+ ...(extractionSourceBlock !== undefined ? { extractionSourceBlock } : {}),
+ };
+}
+
+function renderKnopBlockWithSourceFacts(
+ block: string,
+ sourceFacts: CurrentKnopSourceFacts,
+): string {
+ const carriedLines = [
+ ...(sourceFacts.sourceRegistryPath === undefined ? [] : [
+ ` sflo:hasKnopSourceRegistry <${sourceFacts.sourceRegistryPath}> ;`,
+ ]),
+ ...(sourceFacts.extractionSourcePath === undefined ? [] : [
+ ` sflo:hasExtractionSource <${sourceFacts.extractionSourcePath}> ;`,
+ ]),
+ ].filter((line) => !block.includes(line));
+ if (carriedLines.length === 0) {
+ return block;
+ }
+
+ const workingInventoryLine = " sflo:hasWorkingKnopInventoryFile ";
+ if (!block.includes(workingInventoryLine)) {
+ throw new KnopAddReferenceInputError(
+ "could not preserve source facts because the Knop block is missing hasWorkingKnopInventoryFile",
+ );
+ }
+
+ return block.replace(
+ workingInventoryLine,
+ `${carriedLines.join("\n")}\n${workingInventoryLine}`,
+ );
+}
+
+function splitTurtleBlocks(turtle: string): string[] {
+ return turtle.trimEnd().split("\n\n");
+}
+
+function replaceSubjectBlock(
+ blocks: readonly string[],
+ subjectPath: string,
+ replacementBlock: string,
+): string[] {
+ const index = findSubjectBlockIndex(blocks, subjectPath);
+ if (index === -1) {
+ throw new KnopAddReferenceInputError(
+ `rendered knop inventory did not contain subject block <${subjectPath}>`,
+ );
+ }
+
+ const nextBlocks = [...blocks];
+ nextBlocks[index] = replacementBlock;
+ return nextBlocks;
+}
+
+function upsertSubjectBlockAfter(
+ blocks: readonly string[],
+ anchorSubjectPath: string,
+ subjectPath: string,
+ block: string,
+): string[] {
+ const existingIndex = findSubjectBlockIndex(blocks, subjectPath);
+ if (existingIndex !== -1) {
+ const nextBlocks = [...blocks];
+ nextBlocks[existingIndex] = block;
+ return nextBlocks;
+ }
+
+ const anchorIndex = findSubjectBlockIndex(blocks, anchorSubjectPath);
+ if (anchorIndex === -1) {
+ throw new KnopAddReferenceInputError(
+ `rendered knop inventory did not contain anchor subject block <${anchorSubjectPath}>`,
+ );
+ }
+
+ const nextBlocks = [...blocks];
+ nextBlocks.splice(anchorIndex + 1, 0, block);
+ return nextBlocks;
+}
+
+function findSubjectBlockIndex(
+ blocks: readonly string[],
+ subjectPath: string,
+): number {
+ return blocks.findIndex((block) =>
+ getSubjectPathFromBlock(block) === subjectPath
+ );
+}
+
+function getSubjectPathFromBlock(block: string): string | undefined {
+ const match = block.match(/^<([^>]*)>/);
+ return match?.[1];
+}
+
+function renderSubjectPredicateBlock(
+ subjectPath: string,
+ typeList: string,
+ predicates: readonly string[],
+): string {
+ return `<${subjectPath}> a ${typeList} ;
+ ${predicates.join(" ;\n ")} .`;
+}
+
+function renderLocatedFileBlock(path: string): string {
+ return `<${path}> a sflo:LocatedFile, sflo:RdfDocument .`;
}
function classifyCurrentKnopInventoryShape(
@@ -429,12 +689,12 @@ function assertCurrentWovenKnopInventoryShape(
[
`${knopPath}/_meta/_history001/_s0001`,
SFLO_HAS_MANIFESTATION_IRI,
- `${knopPath}/_meta/_history001/_s0001/meta-ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl`,
],
[
`${knopPath}/_meta/_history001/_s0001`,
SFLO_LOCATED_FILE_FOR_STATE_IRI,
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl`,
],
[
`${knopPath}/_meta/_history001/_s0001`,
@@ -442,24 +702,24 @@ function assertCurrentWovenKnopInventoryShape(
`${knopPath}/_meta/_history001/_s0001/index.html`,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl`,
RDF_TYPE_IRI,
SFLO_ARTIFACT_MANIFESTATION_IRI,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl`,
RDF_TYPE_IRI,
SFLO_RDF_DOCUMENT_IRI,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl`,
SFLO_HAS_LOCATED_FILE_IRI,
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl`,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl`,
SFLO_HAS_RESOURCE_PAGE_IRI,
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/index.html`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/index.html`,
],
[
`${knopPath}/_inventory/_history001`,
@@ -489,12 +749,12 @@ function assertCurrentWovenKnopInventoryShape(
[
`${knopPath}/_inventory/_history001/_s0001`,
SFLO_HAS_MANIFESTATION_IRI,
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl`,
],
[
`${knopPath}/_inventory/_history001/_s0001`,
SFLO_LOCATED_FILE_FOR_STATE_IRI,
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl`,
],
[
`${knopPath}/_inventory/_history001/_s0001`,
@@ -502,24 +762,24 @@ function assertCurrentWovenKnopInventoryShape(
`${knopPath}/_inventory/_history001/_s0001/index.html`,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl`,
RDF_TYPE_IRI,
SFLO_ARTIFACT_MANIFESTATION_IRI,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl`,
RDF_TYPE_IRI,
SFLO_RDF_DOCUMENT_IRI,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl`,
SFLO_HAS_LOCATED_FILE_IRI,
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl`,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl`,
SFLO_HAS_RESOURCE_PAGE_IRI,
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/index.html`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/index.html`,
],
[`${knopPath}/index.html`, RDF_TYPE_IRI, SFLO_RESOURCE_PAGE_IRI],
[`${knopPath}/index.html`, RDF_TYPE_IRI, SFLO_LOCATED_FILE_IRI],
@@ -546,12 +806,12 @@ function assertCurrentWovenKnopInventoryShape(
SFLO_LOCATED_FILE_IRI,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/index.html`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/index.html`,
RDF_TYPE_IRI,
SFLO_RESOURCE_PAGE_IRI,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/index.html`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/index.html`,
RDF_TYPE_IRI,
SFLO_LOCATED_FILE_IRI,
],
@@ -578,32 +838,32 @@ function assertCurrentWovenKnopInventoryShape(
SFLO_LOCATED_FILE_IRI,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/index.html`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/index.html`,
RDF_TYPE_IRI,
SFLO_RESOURCE_PAGE_IRI,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/index.html`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/index.html`,
RDF_TYPE_IRI,
SFLO_LOCATED_FILE_IRI,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl`,
RDF_TYPE_IRI,
SFLO_LOCATED_FILE_IRI,
],
[
- `${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl`,
+ `${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl`,
RDF_TYPE_IRI,
SFLO_RDF_DOCUMENT_IRI,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl`,
RDF_TYPE_IRI,
SFLO_LOCATED_FILE_IRI,
],
[
- `${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl`,
+ `${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl`,
RDF_TYPE_IRI,
SFLO_RDF_DOCUMENT_IRI,
],
@@ -721,13 +981,13 @@ ${SFLO_TURTLE_PREFIX_DECLARATION}
<${knopPath}/_meta/_history001/_s0001> a sflo:HistoricalState ;
sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ;
- sflo:hasManifestation <${knopPath}/_meta/_history001/_s0001/meta-ttl> ;
- sflo:locatedFileForState <${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl> ;
+ sflo:hasManifestation <${knopPath}/_meta/_history001/_s0001/ttl> ;
+ sflo:locatedFileForState <${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl> ;
sflo:hasResourcePage <${knopPath}/_meta/_history001/_s0001/index.html> .
-<${knopPath}/_meta/_history001/_s0001/meta-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile <${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl> ;
- sflo:hasResourcePage <${knopPath}/_meta/_history001/_s0001/meta-ttl/index.html> .
+<${knopPath}/_meta/_history001/_s0001/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile <${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl> ;
+ sflo:hasResourcePage <${knopPath}/_meta/_history001/_s0001/ttl/index.html> .
<${knopPath}/_inventory> a sflo:KnopInventory, sflo:DigitalArtifact, sflo:RdfDocument ;
sflo:hasArtifactHistory <${knopPath}/_inventory/_history001> ;
@@ -748,13 +1008,13 @@ ${SFLO_TURTLE_PREFIX_DECLARATION}
<${knopPath}/_inventory/_history001/_s0001> a sflo:HistoricalState ;
sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ;
- sflo:hasManifestation <${knopPath}/_inventory/_history001/_s0001/inventory-ttl> ;
- sflo:locatedFileForState <${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl> ;
+ sflo:hasManifestation <${knopPath}/_inventory/_history001/_s0001/ttl> ;
+ sflo:locatedFileForState <${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl> ;
sflo:hasResourcePage <${knopPath}/_inventory/_history001/_s0001/index.html> .
-<${knopPath}/_inventory/_history001/_s0001/inventory-ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile <${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl> ;
- sflo:hasResourcePage <${knopPath}/_inventory/_history001/_s0001/inventory-ttl/index.html> .
+<${knopPath}/_inventory/_history001/_s0001/ttl> a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile <${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl> ;
+ sflo:hasResourcePage <${knopPath}/_inventory/_history001/_s0001/ttl/index.html> .
<${knopPath}/_meta/meta.ttl> a sflo:LocatedFile, sflo:RdfDocument .
@@ -762,9 +1022,9 @@ ${SFLO_TURTLE_PREFIX_DECLARATION}
<${knopPath}/_references/references.ttl> a sflo:LocatedFile, sflo:RdfDocument .
-<${knopPath}/_meta/_history001/_s0001/meta-ttl/meta.ttl> a sflo:LocatedFile, sflo:RdfDocument .
+<${knopPath}/_meta/_history001/_s0001/ttl/meta.ttl> a sflo:LocatedFile, sflo:RdfDocument .
-<${knopPath}/_inventory/_history001/_s0001/inventory-ttl/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument .
+<${knopPath}/_inventory/_history001/_s0001/ttl/inventory.ttl> a sflo:LocatedFile, sflo:RdfDocument .
<${knopPath}/index.html> a sflo:ResourcePage, sflo:LocatedFile .
@@ -774,7 +1034,7 @@ ${SFLO_TURTLE_PREFIX_DECLARATION}
<${knopPath}/_meta/_history001/_s0001/index.html> a sflo:ResourcePage, sflo:LocatedFile .
-<${knopPath}/_meta/_history001/_s0001/meta-ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile .
+<${knopPath}/_meta/_history001/_s0001/ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile .
<${knopPath}/_inventory/index.html> a sflo:ResourcePage, sflo:LocatedFile .
@@ -782,7 +1042,7 @@ ${SFLO_TURTLE_PREFIX_DECLARATION}
<${knopPath}/_inventory/_history001/_s0001/index.html> a sflo:ResourcePage, sflo:LocatedFile .
-<${knopPath}/_inventory/_history001/_s0001/inventory-ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile .
+<${knopPath}/_inventory/_history001/_s0001/ttl/index.html> a sflo:ResourcePage, sflo:LocatedFile .
`;
}
@@ -894,3 +1154,37 @@ function parseKnopInventoryQuads(
throw new KnopAddReferenceInputError(errorMessage);
}
}
+
+function requireOptionalNamedNodeObject(
+ quads: readonly Quad[],
+ subjectIri: string,
+ predicateIri: string,
+ errorMessage: string,
+): string | undefined {
+ const values = quads.flatMap((quad) =>
+ quad.subject.termType === "NamedNode" &&
+ quad.subject.value === subjectIri &&
+ quad.predicate.value === predicateIri &&
+ quad.object.termType === "NamedNode"
+ ? [quad.object.value]
+ : []
+ );
+
+ if (values.length > 1) {
+ throw new KnopAddReferenceInputError(errorMessage);
+ }
+
+ return values[0];
+}
+
+function toMeshPath(
+ meshBase: string,
+ iri: string,
+ errorMessage: string,
+): string {
+ if (!iri.startsWith(meshBase)) {
+ throw new KnopAddReferenceInputError(errorMessage);
+ }
+
+ return iri.slice(meshBase.length);
+}
diff --git a/src/core/knop/add_reference_test.ts b/src/core/knop/add_reference_test.ts
index a7a05f4..98e8a24 100644
--- a/src/core/knop/add_reference_test.ts
+++ b/src/core/knop/add_reference_test.ts
@@ -32,13 +32,13 @@ const wovenKnopInventory =
a sflo:HistoricalState ;
sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ;
- sflo:hasManifestation ;
- sflo:locatedFileForState ;
+ sflo:hasManifestation ;
+ sflo:locatedFileForState ;
sflo:hasResourcePage .
- a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile ;
- sflo:hasResourcePage .
+ a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile ;
+ sflo:hasResourcePage .
a sflo:KnopInventory, sflo:DigitalArtifact, sflo:RdfDocument ;
sflo:hasArtifactHistory ;
@@ -56,21 +56,21 @@ const wovenKnopInventory =
a sflo:HistoricalState ;
sflo:stateOrdinal "1"^^xsd:nonNegativeInteger ;
- sflo:hasManifestation ;
- sflo:locatedFileForState ;
+ sflo:hasManifestation ;
+ sflo:locatedFileForState ;
sflo:hasResourcePage .
- a sflo:ArtifactManifestation, sflo:RdfDocument ;
- sflo:hasLocatedFile ;
- sflo:hasResourcePage .
+ a sflo:ArtifactManifestation, sflo:RdfDocument ;
+ sflo:hasLocatedFile ;
+ sflo:hasResourcePage .
a sflo:LocatedFile, sflo:RdfDocument .
a sflo:LocatedFile, sflo:RdfDocument .
- a sflo:LocatedFile, sflo:RdfDocument .
+ a sflo:LocatedFile, sflo:RdfDocument .
- a sflo:LocatedFile, sflo:RdfDocument .
+ a sflo:LocatedFile, sflo:RdfDocument .
a sflo:ResourcePage, sflo:LocatedFile .
@@ -80,7 +80,7 @@ const wovenKnopInventory =
a sflo:ResourcePage, sflo:LocatedFile .
- a sflo:ResourcePage, sflo:LocatedFile .
+ a sflo:ResourcePage, sflo:LocatedFile .
a sflo:ResourcePage, sflo:LocatedFile .
@@ -88,7 +88,7 @@ const wovenKnopInventory =
a sflo:ResourcePage, sflo:LocatedFile .
- a sflo:ResourcePage, sflo:LocatedFile .
+ a sflo:ResourcePage, sflo:LocatedFile .
`;
const unwovenKnopInventory =
@@ -111,6 +111,33 @@ const unwovenKnopInventory =
a sflo:LocatedFile, sflo:RdfDocument .
`;
+const extractedKnopInventory =
+ `@base .
+@prefix sflo: .
+
+ a sflo:Knop ;
+ sflo:hasKnopMetadata ;
+ sflo:hasKnopInventory ;
+ sflo:hasKnopSourceRegistry ;
+ sflo:hasExtractionSource ;
+ sflo:hasWorkingKnopInventoryFile .
+
+ a sflo:KnopMetadata, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sflo:hasWorkingLocatedFile .
+
+ a sflo:KnopInventory, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sflo:hasWorkingLocatedFile .
+
+ a sflo:KnopSourceRegistry, sflo:DigitalArtifact, sflo:RdfDocument ;
+ sflo:hasWorkingLocatedFile .
+
+ a sflo:LocatedFile, sflo:RdfDocument .
+
+ a sflo:LocatedFile, sflo:RdfDocument .
+
+