From 95bb957052ff97d6b112b1a8eed921917021a9a0 Mon Sep 17 00:00:00 2001 From: "Claude (Emil)" Date: Tue, 3 Nov 2026 10:24:46 +0000 Subject: [PATCH 01/15] chore: remove the Rust tree from main (D1.24) The first rewrite pull request removes the Rust workspace from main, as the product definition rules in D1.24 (Draft 4, decision-driven-design fcda5ed). Every crate, the workspace files, schema/, spec/, conventions/, examples/, scripts/, docs/, the .product/, .decisions/ and .ddd/ stores, the agent configuration, the three Rust workflows, CLAUDE.md, .gitignore and .github/copilot-instructions.md leave in this one commit; nothing was moved, renamed or edited first. Everything removed stays readable at the tag rust-final (tag object d0cf1f1, commit 70395df). LICENSE and README.md stay here and are replaced in their own commits. Refs #165 Signed-off-by: Emil Okkels Klein Claude-Session: https://claude.ai/code/session_01NbWSYSVJASSN4tw4nMJ9kU --- .cargo/config.toml | 14 - .claude/skills/feature-e2e/SKILL.md | 189 -- .claude/skills/product-build/SKILL.md | 81 - .claude/skills/product-how/SKILL.md | 80 - .claude/skills/product-session-e2e/SKILL.md | 55 - .claude/skills/product-session/SKILL.md | 74 - .claude/skills/product-what/SKILL.md | 115 - .claude/skills/release/SKILL.md | 193 -- .ddd/claims/.gitkeep | 0 .ddd/claims/DDD-adapter-01.yaml | 61 - .ddd/claims/DDD-adapter-02.yaml | 14 - .ddd/claims/DDD-adapter-03.yaml | 32 - .ddd/claims/DDD-adapter-04.yaml | 71 - .ddd/claims/DDD-adapter-05.yaml | 46 - .ddd/claims/DDD-arch-01.yaml | 18 - .ddd/claims/DDD-arch-02.yaml | 14 - .ddd/claims/DDD-arch-03.yaml | 14 - .ddd/claims/DDD-arch-04.yaml | 14 - .ddd/claims/DDD-arch-05.yaml | 56 - .ddd/claims/DDD-arch-06.yaml | 28 - .ddd/claims/DDD-arch-07.yaml | 39 - .ddd/claims/DDD-arch-08.yaml | 25 - .ddd/claims/DDD-arch-09.yaml | 25 - .ddd/claims/DDD-bicep-def-01.yaml | 30 - .ddd/claims/DDD-bicep-diag-01.yaml | 30 - .ddd/claims/DDD-bicep-exist-01.yaml | 26 - .ddd/claims/DDD-bicep-idem-01.yaml | 33 - .ddd/claims/DDD-bicep-net-01.yaml | 27 - .ddd/claims/DDD-bicep-rbac-01.yaml | 31 - .ddd/claims/DDD-bicep-sec-01.yaml | 29 - .ddd/claims/DDD-bicep-sec-02.yaml | 29 - .ddd/claims/DDD-bicep-sku-01.yaml | 29 - .ddd/claims/DDD-bicep-ver-01.yaml | 28 - .ddd/claims/DDD-cs-authz-01.yaml | 31 - .ddd/claims/DDD-cs-conc-01.yaml | 28 - .ddd/claims/DDD-cs-conc-02.yaml | 33 - .ddd/claims/DDD-cs-disp-01.yaml | 26 - .ddd/claims/DDD-cs-disp-02.yaml | 31 - .ddd/claims/DDD-cs-disp-03.yaml | 33 - .ddd/claims/DDD-cs-disp-04.yaml | 31 - .ddd/claims/DDD-cs-disp-05.yaml | 34 - .ddd/claims/DDD-cs-disp-06.yaml | 37 - .ddd/claims/DDD-cs-ef-01.yaml | 31 - .ddd/claims/DDD-cs-fail-01.yaml | 28 - .ddd/claims/DDD-cs-map-01.yaml | 28 - .ddd/claims/DDD-cs-map-02.yaml | 32 - .ddd/claims/DDD-cs-nrt-01.yaml | 31 - .ddd/claims/DDD-cs-nrt-02.yaml | 31 - .ddd/claims/DDD-cs-nrt-03.yaml | 26 - .ddd/claims/DDD-cs-nrt-04.yaml | 35 - .ddd/claims/DDD-cs-nrt-05.yaml | 34 - .ddd/claims/DDD-cs-nrt-06.yaml | 36 - .ddd/claims/DDD-cs-nrt-07.yaml | 31 - .ddd/claims/DDD-cs-obs-01.yaml | 29 - .ddd/claims/DDD-cs-ord-01.yaml | 26 - .ddd/claims/DDD-cs-pol-01.yaml | 29 - .ddd/claims/DDD-cs-pol-02.yaml | 31 - .ddd/claims/DDD-cs-pol-03.yaml | 27 - .ddd/claims/DDD-cs-pol-04.yaml | 32 - .ddd/claims/DDD-cs-proc-01.yaml | 28 - .ddd/claims/DDD-cs-ser-01.yaml | 33 - .ddd/claims/DDD-cs-tc-01.yaml | 28 - .ddd/claims/DDD-cs-tc-02.yaml | 28 - .ddd/claims/DDD-cs-tc-03.yaml | 27 - .ddd/claims/DDD-cs-tc-04.yaml | 29 - .ddd/claims/DDD-cs-tc-05.yaml | 34 - .ddd/claims/DDD-cs-tx-01.yaml | 29 - .ddd/claims/DDD-detect-01.yaml | 19 - .ddd/claims/DDD-detect-02.yaml | 19 - .ddd/claims/DDD-friction-01.yaml | 16 - .ddd/claims/DDD-friction-02.yaml | 15 - .ddd/claims/DDD-gates-01.yaml | 18 - .ddd/claims/DDD-gates-02.yaml | 42 - .ddd/claims/DDD-method-01.yaml | 17 - .ddd/claims/DDD-method-02.yaml | 16 - .ddd/claims/DDD-method-03.yaml | 13 - .ddd/claims/DDD-method-04.yaml | 14 - .ddd/claims/DDD-method-05.yaml | 19 - .ddd/claims/DDD-method-06.yaml | 19 - .ddd/claims/DDD-method-07.yaml | 31 - .ddd/claims/DDD-scope-01.yaml | 16 - .ddd/claims/DDD-web-01.yaml | 28 - .ddd/claims/DDD-web-02.yaml | 22 - .ddd/claims/DDD-web-visual-01.yaml | 38 - .ddd/claims/DDD-what-01.yaml | 15 - .ddd/claims/DDD-what-02.yaml | 21 - .ddd/claims/DDD-what-03.yaml | 22 - .ddd/claims/DDD-what-04.yaml | 23 - .ddd/concordance.yaml | 295 -- .ddd/config.yaml | 47 - .ddd/decisions/.gitkeep | 0 .ddd/decisions/adapter-policy-tables.yaml | 37 - .../amend-explicit-evidence-frozen.yaml | 21 - .ddd/decisions/bicep-linter-at-error.yaml | 41 - .ddd/decisions/bicep-psrule-in-pr-stage.yaml | 38 - .ddd/decisions/ci-gates-report-skipped.yaml | 65 - .../correspondence-rows-are-stratified.yaml | 32 - .../cs-async-policy-rules-at-error.yaml | 29 - .../cs-authorization-fallback-deny.yaml | 30 - .../decisions/cs-disposal-rules-at-error.yaml | 37 - .ddd/decisions/cs-general-catch-at-error.yaml | 26 - .ddd/decisions/cs-policy-rules-at-error.yaml | 31 - .../cs-startup-assertions-required.yaml | 34 - .../cs-unsafe-reinterpretation-banned.yaml | 38 - .ddd/decisions/curation-over-mining.yaml | 18 - .../enforce-matching-tightens-to-symbol.yaml | 54 - .ddd/decisions/enum-member-gap-priced.yaml | 36 - .ddd/decisions/fixtures-not-sdk.yaml | 13 - .ddd/decisions/git-is-the-amend-trail.yaml | 29 - .ddd/decisions/interceptor-not-extension.yaml | 19 - .ddd/decisions/internal-not-surface.yaml | 52 - .ddd/decisions/lsp-as-seam.yaml | 59 - .ddd/decisions/m6-proceeds-no-flip.yaml | 98 - .ddd/decisions/m8-enforcement-closure.yaml | 29 - .ddd/decisions/no-unwrap-gate.yaml | 51 - .ddd/decisions/pins-at-m2.yaml | 15 - .ddd/decisions/prd-split.yaml | 27 - .../decisions/predicates-carry-no-status.yaml | 38 - .ddd/decisions/rejection-facts-prefilled.yaml | 16 - .ddd/decisions/rejection-payload-basis.yaml | 78 - .ddd/decisions/report-coverage-explicit.yaml | 17 - .ddd/decisions/rust-class-enforced-here.yaml | 65 - .ddd/decisions/rust-host-is-real.yaml | 38 - .ddd/decisions/sarif-unification.yaml | 14 - .../seed-lands-in-claims-not-shared.yaml | 34 - .ddd/decisions/typed-basis.yaml | 25 - .../decisions/undeclared-what-boundaries.yaml | 32 - .ddd/decisions/v1-scope.yaml | 20 - .ddd/decisions/web-htmlcss-enforce.yaml | 18 - .ddd/decisions/web-markup-validity.yaml | 15 - .ddd/decisions/web-plain-pair-scope.yaml | 17 - .ddd/decisions/web-render-status-palette.yaml | 21 - .ddd/decisions/web-token-discipline.yaml | 17 - .../what-boundaries-priced-not-paid.yaml | 62 - .ddd/decisions/what-policy-table.yaml | 21 - .ddd/decisions/what-published-qualifier.yaml | 19 - .ddd/decisions/why-resolves-three-ways.yaml | 17 - .ddd/decisions/workspace-member-delivery.yaml | 22 - .ddd/manifest/analyzers.yaml | 4 - .ddd/manifest/clippy.yaml | 10 - .ddd/manifest/htmlvalidate.yaml | 7 - .ddd/manifest/linter.yaml | 4 - .ddd/manifest/stylelint.yaml | 8 - .ddd/manifest/tokens.yaml | 33 - .ddd/patterns/.gitkeep | 0 .ddd/predicates/.gitkeep | 0 .ddd/predicates/api-version-currency.yaml | 45 - .../authorization-scope-declared.yaml | 44 - .ddd/predicates/composition-decorator.yaml | 22 - .ddd/predicates/convention-mapping.yaml | 53 - .ddd/predicates/deployment-idempotence.yaml | 43 - .../deployment-secret-containment.yaml | 46 - .ddd/predicates/deterministic-disposal.yaml | 54 - .../deterministic-sequence-order.yaml | 47 - .../diagnostic-retention-declared.yaml | 41 - .../endpoint-authorization-explicit.yaml | 45 - .ddd/predicates/explicit-policy-argument.yaml | 49 - .../external-dependency-declared.yaml | 44 - .../failure-behaviour-declared.yaml | 46 - .ddd/predicates/failure-policy-declared.yaml | 44 - .ddd/predicates/governance-resolvability.yaml | 21 - .../predicates/network-exposure-declared.yaml | 44 - .ddd/predicates/null-dereference-safety.yaml | 55 - .ddd/predicates/observable-completion.yaml | 46 - .../parameter-default-governed.yaml | 45 - .../persistence-defaults-declared.yaml | 49 - .../resource-capacity-governed.yaml | 44 - .../serialisation-contract-declared.yaml | 48 - .ddd/predicates/static-type-conformance.yaml | 51 - .../transaction-boundary-declared.yaml | 44 - .ddd/render.html | 714 ---- .ddd/seams/.gitkeep | 0 .ddd/seams/events/0001-default-command.yaml | 18 - .ddd/seams/events/0002-host-command.yaml | 18 - .ddd/seams/events/0003-default-command.yaml | 19 - .ddd/seams/events/0004-host-command.yaml | 19 - .ddd/seams/events/0005-readysignal.yaml | 18 - .ddd/seams/events/0006-readysignal.yaml | 19 - .ddd/seams/events/0007-raw-str.yaml | 18 - .ddd/seams/events/0008-raw-str.yaml | 19 - .ddd/seams/events/0009-bad.yaml | 19 - .ddd/seams/events/0010-card.yaml | 19 - .ddd/seams/events/0011-chip.yaml | 19 - .ddd/seams/events/0012-established.yaml | 19 - .ddd/seams/events/0013-muted.yaml | 19 - .ddd/seams/events/0014-ok.yaml | 19 - .ddd/seams/events/0015-projected.yaml | 19 - .ddd/seams/events/0016-reported.yaml | 19 - .ddd/seams/events/0017-retired.yaml | 19 - .ddd/seams/events/0018-warn.yaml | 19 - .ddd/seams/events/0019-bad.yaml | 20 - .ddd/seams/events/0020-card.yaml | 20 - .ddd/seams/events/0021-chip.yaml | 20 - .ddd/seams/events/0022-established.yaml | 20 - .ddd/seams/events/0023-muted.yaml | 20 - .ddd/seams/events/0024-ok.yaml | 20 - .ddd/seams/events/0025-projected.yaml | 20 - .ddd/seams/events/0026-reported.yaml | 20 - .ddd/seams/events/0027-retired.yaml | 20 - .ddd/seams/events/0028-warn.yaml | 20 - .ddd/seams/events/0029---ink.yaml | 19 - .ddd/seams/events/0030---ink-faint.yaml | 19 - .ddd/seams/events/0031---ink-muted.yaml | 19 - .ddd/seams/events/0032---on-chip.yaml | 19 - .ddd/seams/events/0033---status-bad.yaml | 19 - .ddd/seams/events/0034---status-ok.yaml | 19 - .ddd/seams/events/0035---status-warn.yaml | 19 - .ddd/seams/events/0036---ink.yaml | 20 - .ddd/seams/events/0037---ink-faint.yaml | 20 - .ddd/seams/events/0038---ink-muted.yaml | 20 - .ddd/seams/events/0039---on-chip.yaml | 20 - .ddd/seams/events/0040---status-bad.yaml | 20 - .ddd/seams/events/0041---status-ok.yaml | 20 - .ddd/seams/events/0042---status-warn.yaml | 20 - .ddd/seams/events/0043---surface.yaml | 19 - .ddd/seams/events/0044---surface-raised.yaml | 19 - .ddd/seams/events/0045---surface.yaml | 20 - .ddd/seams/events/0046---surface-raised.yaml | 20 - .ddd/seams/seam-ledger-acceptor-identity.yaml | 13 - .ddd/seams/seam-ledger-blame-attribution.yaml | 14 - .ddd/seams/seam-ledger-canonical-form.yaml | 14 - .../seams/seam-ledger-disposition-states.yaml | 13 - .ddd/seams/seam-ledger-graph-stage.yaml | 13 - .ddd/seams/seam-ledger-tolerance-floor.yaml | 13 - .ddd/seams/seam-ledger-ulid-mint.yaml | 13 - .ddd/seams/seam-ledger-verify-classes.yaml | 13 - .ddd/seams/seam-ledger-version-hash.yaml | 14 - .ddd/seams/seam-ledger-write-refusal.yaml | 14 - .ddd/seams/seam-mcp-raw-str.yaml | 14 - ...eam-rust-ddd-cli-src-commands-bind-rs.yaml | 14 - ...-ddd-cli-src-commands-content-hash-rs.yaml | 14 - ...dd-cli-src-commands-diff-contracts-rs.yaml | 20 - ...eam-rust-ddd-cli-src-commands-diff-rs.yaml | 14 - ...-ddd-cli-src-commands-migrate-plan-rs.yaml | 68 - ...-rust-ddd-cli-src-commands-migrate-rs.yaml | 14 - ...seam-rust-ddd-cli-src-commands-mod-rs.yaml | 38 - ...m-rust-ddd-cli-src-commands-report-rs.yaml | 14 - ...rust-ddd-cli-src-commands-validate-rs.yaml | 14 - .../seam-rust-ddd-core-src-basis-pin-rs.yaml | 50 - .../seam-rust-ddd-core-src-binding-rs.yaml | 86 - ...seam-rust-ddd-core-src-concordance-rs.yaml | 98 - .../seam-rust-ddd-core-src-contracts-rs.yaml | 164 - .../seam-rust-ddd-core-src-decision-rs.yaml | 20 - .../seam-rust-ddd-core-src-gitrev-rs.yaml | 56 - ...seam-rust-ddd-core-src-ledger-gate-rs.yaml | 152 - .ddd/seams/seam-rust-ddd-core-src-lib-rs.yaml | 50 - .../seam-rust-ddd-core-src-render-rs.yaml | 14 - .../seam-rust-ddd-core-src-report-rs.yaml | 32 - .../seam-rust-ddd-core-src-revisit-rs.yaml | 110 - .../seams/seam-rust-ddd-core-src-seam-rs.yaml | 14 - .../seam-rust-ddd-core-src-validate-rs.yaml | 14 - .../seam-rust-ddd-lsp-src-capability-rs.yaml | 152 - .../seam-rust-ddd-lsp-src-declaration-rs.yaml | 128 - .ddd/seams/seam-rust-ddd-lsp-src-host-rs.yaml | 20 - .ddd/seams/seam-rust-ddd-lsp-src-lib-rs.yaml | 26 - .ddd/seams/seam-rust-ddd-lsp-src-mock-rs.yaml | 14 - .../seam-rust-ddd-lsp-src-protocol-rs.yaml | 74 - .../seam-rust-ddd-lsp-src-revdiff-rs.yaml | 14 - .ddd/seams/seam-rust-ddd-mcp-src-bind-rs.yaml | 74 - .ddd/seams/seam-rust-ddd-mcp-src-lib-rs.yaml | 14 - .../seams/seam-rust-ddd-mcp-src-state-rs.yaml | 56 - .ddd/seams/seam-rust-default-command.yaml | 15 - .../seam-rust-ground-cli-src-extract-rs.yaml | 380 --- .../seam-rust-ground-cli-src-read-rs.yaml | 116 - .../seam-rust-ground-cli-src-symbols-rs.yaml | 176 - .ddd/seams/seam-rust-host-command.yaml | 10 - ...m-rust-ledger-cli-src-commands-add-rs.yaml | 32 - ...er-cli-src-commands-authority-enum-rs.yaml | 86 - ...-ledger-cli-src-commands-authority-rs.yaml | 50 - ...ledger-cli-src-commands-basis-text-rs.yaml | 38 - ...t-ledger-cli-src-commands-cli-enum-rs.yaml | 182 - ...ust-ledger-cli-src-commands-common-rs.yaml | 26 - ...ust-ledger-cli-src-commands-evolve-rs.yaml | 50 - ...ust-ledger-cli-src-commands-export-rs.yaml | 38 - ...dger-cli-src-commands-inbox-accept-rs.yaml | 104 - ...er-cli-src-commands-inbox-findings-rs.yaml | 32 - ...dger-cli-src-commands-inbox-render-rs.yaml | 32 - ...rust-ledger-cli-src-commands-inbox-rs.yaml | 224 -- ...-rust-ledger-cli-src-commands-init-rs.yaml | 14 - ...st-ledger-cli-src-commands-inspect-rs.yaml | 50 - ...m-rust-ledger-cli-src-commands-mod-rs.yaml | 146 - ...-rust-ledger-cli-src-commands-sign-rs.yaml | 80 - ...t-ledger-cli-src-commands-terminal-rs.yaml | 38 - ...ust-ledger-cli-src-commands-verify-rs.yaml | 26 - ...edger-cli-tests-common-export-only-rs.yaml | 62 - ...-rust-ledger-cli-tests-common-hand-rs.yaml | 188 -- ...m-rust-ledger-cli-tests-common-mod-rs.yaml | 188 -- ...ledger-cli-tests-inbox-fixture-mod-rs.yaml | 98 - ...am-rust-ledger-core-src-acceptance-rs.yaml | 44 - ...dger-core-src-author-authority-ops-rs.yaml | 152 - ...r-core-src-author-availability-ops-rs.yaml | 56 - ...edger-core-src-author-batch-accept-rs.yaml | 128 - ...st-ledger-core-src-author-decision-rs.yaml | 32 - ...ledger-core-src-author-genesis-key-rs.yaml | 26 - ...edger-core-src-author-identity-ops-rs.yaml | 68 - ...am-rust-ledger-core-src-author-mod-rs.yaml | 20 - ...-ledger-core-src-author-policy-ops-rs.yaml | 56 - ...st-ledger-core-src-author-sign-ops-rs.yaml | 20 - ...ledger-core-src-author-version-ops-rs.yaml | 26 - ...er-core-src-authority-availability-rs.yaml | 140 - ...-ledger-core-src-authority-binding-rs.yaml | 170 - ...st-ledger-core-src-authority-check-rs.yaml | 224 -- ...t-ledger-core-src-authority-choice-rs.yaml | 26 - ...t-ledger-core-src-authority-filing-rs.yaml | 20 - ...-ledger-core-src-authority-fixture-rs.yaml | 74 - ...st-ledger-core-src-authority-grant-rs.yaml | 266 -- ...edger-core-src-authority-key-close-rs.yaml | 74 - ...rust-ledger-core-src-authority-mod-rs.yaml | 98 - ...-ledger-core-src-authority-payload-rs.yaml | 146 - ...t-ledger-core-src-authority-policy-rs.yaml | 152 - ...dger-core-src-authority-references-rs.yaml | 14 - ...dger-core-src-authority-revocation-rs.yaml | 146 - ...ust-ledger-core-src-authority-role-rs.yaml | 224 -- ...-ledger-core-src-authority-signers-rs.yaml | 74 - ...edger-core-src-authority-structure-rs.yaml | 20 - ...ust-ledger-core-src-authority-view-rs.yaml | 158 - ...am-rust-ledger-core-src-batch-file-rs.yaml | 122 - .../seam-rust-ledger-core-src-batch-rs.yaml | 20 - .../seam-rust-ledger-core-src-canon-rs.yaml | 26 - ...eam-rust-ledger-core-src-changeset-rs.yaml | 62 - ...eam-rust-ledger-core-src-discharge-rs.yaml | 20 - .../seam-rust-ledger-core-src-finding-rs.yaml | 32 - .../seam-rust-ledger-core-src-format-rs.yaml | 50 - ...-rust-ledger-core-src-graph-export-rs.yaml | 68 - ...eam-rust-ledger-core-src-graph-mod-rs.yaml | 44 - ...ust-ledger-core-src-graph-ntriples-rs.yaml | 14 - ...-rust-ledger-core-src-graph-shapes-rs.yaml | 14 - .../seam-rust-ledger-core-src-hash-rs.yaml | 20 - .../seam-rust-ledger-core-src-id-rs.yaml | 56 - ...-rust-ledger-core-src-inbox-config-rs.yaml | 62 - ...eam-rust-ledger-core-src-inbox-git-rs.yaml | 188 -- ...m-rust-ledger-core-src-inbox-index-rs.yaml | 248 -- ...am-rust-ledger-core-src-inbox-list-rs.yaml | 182 - ...eam-rust-ledger-core-src-inbox-mod-rs.yaml | 38 - ...m-rust-ledger-core-src-inbox-terms-rs.yaml | 20 - .../seam-rust-ledger-core-src-key-rs.yaml | 56 - .../seam-rust-ledger-core-src-landed-rs.yaml | 32 - .../seam-rust-ledger-core-src-landing-rs.yaml | 224 -- .../seam-rust-ledger-core-src-lib-rs.yaml | 98 - ...seam-rust-ledger-core-src-revision-rs.yaml | 14 - .../seam-rust-ledger-core-src-revisit-rs.yaml | 50 - ...m-rust-ledger-core-src-show-render-rs.yaml | 20 - .../seam-rust-ledger-core-src-show-rs.yaml | 338 -- ...rust-ledger-core-src-signing-check-rs.yaml | 296 -- ...-rust-ledger-core-src-signing-dsse-rs.yaml | 50 - ...m-rust-ledger-core-src-signing-mod-rs.yaml | 98 - ...ust-ledger-core-src-signing-review-rs.yaml | 44 - ...m-rust-ledger-core-src-signing-ssh-rs.yaml | 116 - ...st-ledger-core-src-signing-subject-rs.yaml | 236 -- .../seam-rust-ledger-core-src-store-rs.yaml | 32 - .../seam-rust-ledger-core-src-testkit-rs.yaml | 14 - ...m-rust-ledger-core-src-verify-acts-rs.yaml | 20 - ...t-ledger-core-src-verify-authority-rs.yaml | 14 - ...ust-ledger-core-src-verify-history-rs.yaml | 20 - ...m-rust-ledger-core-src-verify-keys-rs.yaml | 20 - ...am-rust-ledger-core-src-verify-mod-rs.yaml | 128 - .../seam-rust-ledger-core-src-version-rs.yaml | 26 - ...roduct-cli-src-commands-how-fields-rs.yaml | 14 - ...-product-cli-src-commands-question-rs.yaml | 56 - ...-product-cli-src-commands-registry-rs.yaml | 26 - ...product-cli-src-commands-root-enum-rs.yaml | 20 - ...rust-product-core-src-ground-apply-rs.yaml | 38 - ...-rust-product-core-src-ground-axes-rs.yaml | 104 - ...rust-product-core-src-ground-batch-rs.yaml | 98 - ...t-product-core-src-ground-declared-rs.yaml | 200 -- ...product-core-src-ground-derivation-rs.yaml | 74 - ...ust-product-core-src-ground-entail-rs.yaml | 116 - ...rust-product-core-src-ground-facts-rs.yaml | 530 --- ...-rust-product-core-src-ground-mint-rs.yaml | 62 - ...m-rust-product-core-src-ground-mod-rs.yaml | 20 - ...-rust-product-core-src-ground-plan-rs.yaml | 356 -- ...product-core-src-ground-projection-rs.yaml | 50 - ...st-product-core-src-ground-propose-rs.yaml | 44 - ...ust-product-core-src-ground-report-rs.yaml | 20 - ...-rust-product-core-src-ground-rows-rs.yaml | 260 -- ...st-product-core-src-ground-sidecar-rs.yaml | 116 - ...product-core-src-ground-structural-rs.yaml | 26 - ...ust-product-core-src-ground-triple-rs.yaml | 200 -- ...rust-product-core-src-ground-vocab-rs.yaml | 110 - ...rust-product-core-src-ground-write-rs.yaml | 62 - .../seam-rust-product-core-src-lib-rs.yaml | 20 - .../seam-rust-product-core-src-pf-how-rs.yaml | 14 - .../seam-rust-product-core-src-pf-ids-rs.yaml | 26 - .../seam-rust-product-core-src-pf-mod-rs.yaml | 38 - ...product-core-src-pf-model-question-rs.yaml | 128 - ...eam-rust-product-core-src-pf-model-rs.yaml | 14 - ...-product-core-src-pf-question-gate-rs.yaml | 80 - ...-rust-product-core-src-pf-question-rs.yaml | 134 - ...product-core-src-pf-sparql-dataset-rs.yaml | 44 - ...st-product-core-src-registry-apply-rs.yaml | 50 - ...roduct-core-src-registry-check-mod-rs.yaml | 116 - ...t-product-core-src-registry-params-rs.yaml | 68 - ...ust-product-core-src-registry-plan-rs.yaml | 80 - ...oduct-core-src-registry-substitute-rs.yaml | 32 - ...product-core-src-registry-template-rs.yaml | 20 - ...t-product-core-src-registry-verify-rs.yaml | 20 - ...-product-mcp-src-question-handlers-rs.yaml | 56 - ...product-mcp-src-workflow-questions-rs.yaml | 38 - .ddd/seams/seam-rust-readysignal-debug.yaml | 14 - .ddd/seams/seam-web-render-bad.yaml | 15 - .ddd/seams/seam-web-render-card.yaml | 15 - .ddd/seams/seam-web-render-chip.yaml | 15 - .ddd/seams/seam-web-render-established.yaml | 15 - .ddd/seams/seam-web-render-muted.yaml | 15 - .ddd/seams/seam-web-render-ok.yaml | 15 - .ddd/seams/seam-web-render-projected.yaml | 15 - .ddd/seams/seam-web-render-reported.yaml | 15 - .ddd/seams/seam-web-render-retired.yaml | 15 - .ddd/seams/seam-web-render-warn.yaml | 15 - .ddd/seams/seam-web-token-ink-faint.yaml | 15 - .ddd/seams/seam-web-token-ink-muted.yaml | 15 - .ddd/seams/seam-web-token-ink.yaml | 15 - .ddd/seams/seam-web-token-on-chip.yaml | 15 - .ddd/seams/seam-web-token-status-bad.yaml | 15 - .ddd/seams/seam-web-token-status-ok.yaml | 15 - .ddd/seams/seam-web-token-status-warn.yaml | 15 - .ddd/seams/seam-web-token-surface-raised.yaml | 14 - .ddd/seams/seam-web-token-surface.yaml | 14 - .ddd/shared/.gitkeep | 0 .decisions/log/01KZNF77DG2RJPY7CA63HYTRJ1.yml | 94 - .decisions/log/01KZPMKQ18D1G3FXS0V0WT1F04.yml | 22 - .decisions/log/01KZPMKQD0BEDBD3HWCT4CJHA4.yml | 21 - .decisions/log/01KZPMKQE044SNFJSKFCTD8QJE.yml | 20 - .decisions/log/01KZPMKQF4BQA4HH2P4QEEYN8V.yml | 21 - .decisions/log/01KZPMKQGDB1MQC1HWY3HRQQ3G.yml | 22 - .decisions/log/01KZPMKQHRFFM2ERBBKZYR1EH2.yml | 21 - .decisions/log/01KZQT2D30HSX412BGC0SFKZKG.yml | 11 - .decisions/log/01KZQT2D483RSCKQYVT8X0P58S.yml | 11 - .decisions/log/01KZQT2D58N6NKVE99GP8C2D7N.yml | 11 - .decisions/log/01KZQT2D6ENP0YA2PSSB4NZX8S.yml | 11 - .decisions/log/01KZQT2D7DYYZ6AHMR1NDXHNHT.yml | 11 - .decisions/log/01KZQT2D8JE3GH0A9S4SQ6S3Y0.yml | 11 - .decisions/log/01KZQT2D9MX8PYZGVMC13TZVDD.yml | 11 - .decisions/log/01KZQT2DAWHXRP35YF345DEYS2.yml | 11 - .decisions/log/01KZQT2DC2G2BRDAGV9RFBG4TV.yml | 11 - .decisions/log/01KZR5P5MN3ZW61HP7ES1CXYFH.yml | 22 - .decisions/log/01KZR5PDTKFK9CNYJZRZV8BETY.yml | 22 - .decisions/log/01KZRA0SBDAF5JQBHC465SAVPX.yml | 11 - .decisions/log/01KZRA0XNDEH1EHQ0MTY60TFB2.yml | 11 - .decisions/log/01KZRBQ3YWP1VJ67E7QRCC7YYD.yml | 25 - .decisions/log/01KZRDFYPZTP1F2S1424N4FM7F.yml | 11 - .decisions/log/01KZTGGGS1TF32NE5ZREW3A55B.yml | 22 - .decisions/log/01KZTGGGXG6CVNYHVZWD4VSXXN.yml | 21 - .decisions/log/01KZTGGH25SFKCGSFCXJMTM7VC.yml | 21 - .decisions/log/01KZTGGH76Z3C69CJ6CBBXXZCZ.yml | 20 - .decisions/log/01KZTGGHCB2AY2GQ6B1JP50VH0.yml | 21 - .decisions/log/01KZTGGHHKFXC6APZ4GF0B20PE.yml | 20 - .decisions/log/01KZTGGHQ69HQR5N9TH579SWDM.yml | 20 - .decisions/log/01KZTGGHXCR4GMGPR87TJ7WNG9.yml | 21 - .decisions/log/01KZTGGJ34CHP029QZYF66YGXT.yml | 21 - .decisions/log/01KZTGGJ92TNGQSEVYW7ZK4Z11.yml | 20 - .decisions/log/01KZTGGJFED3R42CBXMT61QNEJ.yml | 21 - .decisions/log/01KZTGGJNQRC2V0Q4PYWZ5SFT0.yml | 20 - .decisions/log/01KZTGGJW5558M2Q2S9963F5SG.yml | 20 - .decisions/log/01KZTGGK2R76MG2SJY754BJ925.yml | 20 - .decisions/log/01KZTGGK9FH0FGZR7EE8HN4PS7.yml | 20 - .decisions/log/01KZTGGKGBCDMFK30X1K9FNZYA.yml | 20 - .decisions/log/01KZTGGKQDBWWQTNVC3XY937GD.yml | 20 - .decisions/log/01KZTGGKYNQKDYDBHQX2KW6KDS.yml | 22 - .decisions/log/01KZTGGM6BQ13WM22EBNYBPFEY.yml | 23 - .decisions/log/01KZTGGMEAJTX88HKBWPVYRQV9.yml | 20 - .decisions/log/01KZTGGMPHVMNZ6GGV8YX1N9EW.yml | 23 - .decisions/log/01KZTGGMYT8C43AZ5EXB6BZGY5.yml | 21 - .decisions/log/01KZTGGN7C2W9GMC2GYWY7NCPD.yml | 20 - .decisions/log/01KZTGGNGD5AYC7DPKGMSQ59EH.yml | 20 - .decisions/log/01KZTGGNSHJMZP78SAVPB09YBM.yml | 20 - .decisions/log/01KZTGGP8RB22MZ4ER7P7HX1N5.yml | 20 - .decisions/log/01KZTGGPHWM9SS6J446MGJKZWA.yml | 20 - .decisions/log/01KZTGGPV46ABRWGGQJYXEMF5R.yml | 20 - .decisions/log/01KZTGGQ4HT8J1V3A5RQ8HFBDC.yml | 21 - .decisions/log/01KZTGGQFE31C777RCBP4MVH5C.yml | 20 - .decisions/log/01KZTGGQS9G5FE0YATDHNC768B.yml | 20 - .decisions/log/01KZTGGR3DDYKDZYQ9BY56AMGM.yml | 20 - .decisions/log/01KZTGGRE2KWE90Q17NVBSHH3G.yml | 20 - .decisions/log/01KZTGGRSF2PN0YJ6W6YCXNXE7.yml | 21 - .decisions/log/01KZTGGS4B7BTEWB4C0N0Y3JV0.yml | 21 - .decisions/log/01KZTGGSFEM9JH6TYTENN4KKD3.yml | 20 - .decisions/log/01KZTGGSV7KVG13236BYWW28PV.yml | 20 - .decisions/log/01KZTGGT76A1ASWHEDXRBQ8CSK.yml | 20 - .decisions/log/01KZTGGTKM1NYXYNN2J5QX430D.yml | 24 - .decisions/log/01KZTGGV067R92981RZ569H0GT.yml | 20 - .decisions/log/01KZTGGVHB1Z680QE061XM1E5G.yml | 21 - .decisions/log/01KZTGGVXZSAY2ENMR2XPXWGWF.yml | 20 - .decisions/log/01KZTGGWARNTFQP017AERKJYS0.yml | 29 - .decisions/log/01KZTGGWQGRY25YZ01F8M3GG9P.yml | 21 - .decisions/log/01KZTGGX5BRMTH0XE4FRD8NQBN.yml | 21 - .decisions/log/01KZTGGXJBQ0FRGHGDP84KDM3Z.yml | 21 - .decisions/log/01KZTGGY0PM06CQ7XSDZP66ANX.yml | 21 - .decisions/log/01KZTGGYE3Y450AW44A69ZZDF0.yml | 21 - .decisions/log/01KZTGGYVSQ9M7F4DJ184E8D08.yml | 21 - .decisions/log/01KZTGGZ9PNSGSYFSPVADMEGAB.yml | 21 - .decisions/log/01KZTGGZQQJ5R0F156TJ6HARS6.yml | 21 - .decisions/log/01KZTGH06WMVQZTB9VWQJBNBPK.yml | 21 - .decisions/log/01KZTGH0TVH9JAXHG0H9K2VWWX.yml | 21 - .decisions/log/01KZTGH19V8KXVHBHTFAQDBN90.yml | 21 - .decisions/log/01KZTGH1RKH6AXJ0MP1MWGQVPW.yml | 21 - .decisions/log/01KZTGH27QWDBP6WHPRNPM3AHV.yml | 21 - .decisions/log/01KZTGH2Q22VA19V95VBDHV9MK.yml | 21 - .decisions/log/01KZTGH375X445T362JRNDTG8Q.yml | 21 - .decisions/log/01KZTGH3PYAVD7TQWV2ZSX0S86.yml | 21 - .decisions/log/01KZTGH46S7P81K5GG6FXSAZDV.yml | 21 - .decisions/log/01KZTGH4QJ902M3A5NM7CSK4ST.yml | 21 - .decisions/log/01KZTGH581BJJ0MMXYK4CAK7YR.yml | 21 - .decisions/log/01KZTGH5RQ1G66BPDM5N5Y2XCX.yml | 21 - .decisions/log/01KZTGH69VQ7NA131CY3GCGMXR.yml | 21 - .decisions/log/01KZTGH6TM60B2T8T2S2ZKSTG6.yml | 21 - .decisions/log/01KZTGH7BTQ2Z6NBPQGDHQRXYN.yml | 21 - .decisions/log/01KZTGH7XBNTWTZ253YWW2AQ5V.yml | 21 - .decisions/log/01KZTGH8FFCXV3M8Q70XCBNV8W.yml | 21 - .decisions/log/01KZTGH919RY8CY8XBJHJR9WZ6.yml | 21 - .decisions/log/01KZTGH9KHKTDGS8AA8FMA9GYA.yml | 21 - .decisions/log/01KZTGHA61JZQD5QSJKDHKWWEK.yml | 21 - .decisions/log/01KZTGHARQQW8F91DRNWSPNVHR.yml | 21 - .decisions/log/01KZTGHBB9ECEH8TJKTBA98F1X.yml | 21 - .decisions/log/01KZTGHBYPENHW4MFF2SR9GE56.yml | 21 - .decisions/log/01KZTGHCHVFKBJV8N7P8QHYBTT.yml | 21 - .decisions/log/01KZTGHD5CSJK413WZHGYVQ2T3.yml | 21 - .decisions/log/01KZTGHDS9YQ5H4F5FHFB28AWB.yml | 21 - .decisions/log/01KZTGHEE8M1R77RQR5CQ47MQW.yml | 21 - .decisions/log/01KZTGHF4B5HHKMR0QGBRBFD38.yml | 19 - .decisions/log/01KZX70EMPA47TBR0PFKX4M32Z.yml | 19 - .decisions/log/01KZX70EQGQCB1B190TS9FZ1A2.yml | 20 - .decisions/log/01KZX70ET1GMR2012XKEP5EWDW.yml | 19 - .decisions/log/01KZX70S86QGXVCA5GW5WSY6XA.yml | 17 - .decisions/log/01KZX7HW7JY5BKE3YPTXXX9BDH.yml | 21 - .decisions/log/01KZXCED6QA8EKJW5TX88H329G.yml | 11 - .decisions/log/01KZXGPY5GVCDA45QGGCTS2T5X.yml | 11 - .decisions/log/01KZXGRAK0P999C34NXQ3FPC27.yml | 11 - .decisions/log/01KZXGSFTA68J6PJZFKKCFZPNZ.yml | 11 - .decisions/log/01KZXHEF9JAERTPMYXGCAVK9HW.yml | 11 - .decisions/log/01KZXHEFD5NKJ1SXWTZ7A34066.yml | 11 - .decisions/log/01KZXHEFGXGD0KGZ4N2FX40D5F.yml | 11 - .decisions/log/01KZXHEFM9MCNTD3YPWT9EXGMS.yml | 11 - .decisions/log/01KZXHEFQH62RSGYTWTR83YMX1.yml | 11 - .decisions/log/01KZXHEFTWDK928D5X36X0TY01.yml | 11 - .decisions/log/01KZXHEFYBPHFW8WS6AQZ7EW2D.yml | 11 - .decisions/log/01KZXHEG2A1JQV8VQMKCT7BQSM.yml | 11 - .decisions/log/01KZXHEG6B4B4XZC078H6A0DR5.yml | 11 - .decisions/log/01KZXHEGA6VDB6WTNQHFN6174N.yml | 11 - .decisions/log/01KZXHEGD6WCBANRJZ76BRR1CE.yml | 11 - .decisions/log/01KZXHEGG8GMFWZV86AY1Q162C.yml | 11 - .decisions/log/01KZXHEGJXN7WM9Z32A0XAEZV9.yml | 11 - .decisions/log/01KZXHEGNH7JG54HY035X1BS7S.yml | 11 - .decisions/log/01KZXHEGRJYQ7SJ7N5RPEA9ANP.yml | 11 - .decisions/log/01KZXHEGV930X88CWEXBYEWE9V.yml | 11 - .decisions/log/01KZXHEGXWTEC1YTTCA66TXKAC.yml | 11 - .decisions/log/01KZXHEH0MXYTAW72VF4QJ33SB.yml | 11 - .decisions/log/01KZXHEH3ETEWJQA1HWE4W1ME8.yml | 11 - .decisions/log/01KZXHEH6ATHYBFC71CH2P0WE1.yml | 11 - .decisions/log/01KZXHEH9FK4EFSFPPHFHJCTS5.yml | 11 - .decisions/log/01KZXHEHCM5ABV9D5DC14QNZ28.yml | 11 - .decisions/log/01KZXHEHFANBFBMZJ39T1C5P9K.yml | 11 - .decisions/log/01KZXHEHHWF1TWC0DH6HKB3RA6.yml | 11 - .decisions/log/01KZXHEHN3N7HF0PDSMXVQPJWD.yml | 11 - .decisions/log/01KZXHEHR19GYD06J7C3KE312X.yml | 11 - .decisions/log/01KZXHEHV863ERYNZX2BWQRAHT.yml | 11 - .decisions/log/01KZXHEHY1ER3B6AQ6NRQYMNCX.yml | 11 - .decisions/log/01KZXHEJ0QJB6KJ7HEFG097DX9.yml | 11 - .decisions/log/01KZXHEJ3J4SFVH60EBSGSJS0C.yml | 11 - .decisions/log/01KZXHEJ6K605BPNWRMKR8DKSM.yml | 11 - .decisions/log/01KZXHEJ9K2Y0ZX57MCVAJWM3F.yml | 11 - .decisions/log/01KZXHEJCHN02VR0DVQWQYCYGY.yml | 11 - .decisions/log/01KZXHEJFTE7AF7WCE4PFX9T09.yml | 11 - .decisions/log/01KZXHEJKMK34FPVC09YPS8811.yml | 11 - .decisions/log/01KZXHEJQ46TT044PV8D7HF0DD.yml | 11 - .decisions/log/01KZXHEJV4XNJ1B1VN0S52MG9R.yml | 11 - .decisions/log/01KZXHEJYAYZR29E0YVRMHKT54.yml | 11 - .decisions/log/01KZXHEK1JEFGT6P65GA49VAMF.yml | 11 - .decisions/log/01KZXHEK4PZK8XZ4JZ4PQK2P1X.yml | 11 - .decisions/log/01KZXHEK87G21G6HDBARQSB397.yml | 11 - .decisions/log/01KZXHEKBWHRHNRVE1BS94BRMW.yml | 11 - .decisions/log/01KZXHEKF30H8Q1576AJBH7BJ6.yml | 11 - .decisions/log/01KZXHEKJBPV2DZKE0055M0GG1.yml | 11 - .decisions/log/01KZXHEKN8F6KKKAA3EHC3AWXQ.yml | 11 - .decisions/log/01KZXHEKRD7K84JVQQ37YAE22T.yml | 11 - .decisions/log/01KZXHEKVD61XMPEJR760DKVDV.yml | 11 - .decisions/log/01KZXHEKYFZA862VRB46KME6NM.yml | 11 - .decisions/log/01KZXHEM1CYPDR2MYP36ZANY05.yml | 11 - .decisions/log/01KZXHEM5XC319NKKT183Q1845.yml | 11 - .decisions/log/01KZXHEM97VBXFWNHM10ZK6JN0.yml | 11 - .decisions/log/01KZXHEMCMZND8ST8DBQ3877Y7.yml | 11 - .decisions/log/01KZXHEMG0VBZEBSZJZFJSDK5Z.yml | 11 - .decisions/log/01KZXHEMK0GWJQN8WYMD57TRPH.yml | 11 - .decisions/log/01KZXHEMP4MTJ1F07KC413MHJF.yml | 11 - .decisions/log/01KZXHEMSFZ8QK6MJW067NKYPF.yml | 11 - .decisions/log/01KZXHEMWR7PRCFFTWRKA915AM.yml | 11 - .decisions/log/01KZXHEN04RCXYA1EB98JT8WPE.yml | 11 - .decisions/log/01KZXHEN47W57WK8E5MBFJVBQ7.yml | 11 - .decisions/log/01KZXHEN8ZRASX4M10VC5814S3.yml | 11 - .decisions/log/01KZXHENDA4Y49QAE53FK97V41.yml | 11 - .decisions/log/01KZXHENGJ67KH8NFC7D67RXBP.yml | 11 - .decisions/log/01KZXHENKTATHVJHDGKMB59PHQ.yml | 11 - .decisions/log/01KZXHENQEBXRX0GSN90BTEB89.yml | 11 - .decisions/log/01KZXHENTSEEZY0T2N328FKB6N.yml | 11 - .decisions/log/01KZXHENYP68WAXVCVAQ3FY1MP.yml | 11 - .decisions/log/01KZXHEP2ERNT3A0P78RN5DEDZ.yml | 11 - .decisions/log/01KZXHEP5ZC4E9JPZYXA9744YF.yml | 11 - .decisions/log/01KZXHEP9M9H9NRTCW79AQ1Y89.yml | 11 - .decisions/log/01KZXHEPD4F8EWHAQDDV9A042S.yml | 11 - .decisions/log/01KZXHEPGKM61QD5JFZZ9FT5YN.yml | 11 - .decisions/log/01KZXHEPMAXJG39RCR2B18E2Q9.yml | 11 - .decisions/log/01KZXHEPRDW4XH1ESX7A2X8CF2.yml | 11 - .decisions/log/01KZXHEPWK8JYJE4CQ0170Z35Y.yml | 11 - .decisions/log/01KZXHEQ0QMMBZQ6NYEC3MSYC5.yml | 11 - .decisions/log/01KZXJX69AJYGN66DQG02Z27BH.yml | 28 - .decisions/log/01M3PHNSVZMME6XK72GNHHNKV6.yml | 16 - .decisions/sets/ddd-governance.yml | 8 - .decisions/sets/ledger-design.yml | 28 - .github/copilot-instructions.md | 119 - .github/workflows/product-ci.yml | 195 -- .github/workflows/publish-mcp.yml | 190 -- .github/workflows/release.yml | 296 -- .gitignore | 40 - .htmlvalidate.json | 5 - .mcp.json | 12 - .product/capabilities.yaml | 33 - .product/config.toml | 8 - .product/products/acme/acme.ttl | 322 -- .../acme-storefront/how-contract.yaml | 1 - .../blueprints/acme-storefront/layout.yaml | 21 - .../products/acme/deciders/cart-decider.yaml | 13 - .../products/acme/deciders/order-decider.yaml | 13 - .../acme/deliverables/del-checkout.yaml | 9 - .../acme/deliverables/del-refunds.yaml | 6 - .../acme/deployable-units/du-admin-web.yaml | 7 - .../acme/deployable-units/du-shop-ios.yaml | 7 - .../acme/deployable-units/du-shop-web.yaml | 7 - .../wire-ds/components/button.html | 3 - .../wire-ds/components/list.html | 3 - .../wire-ds/components/value.html | 3 - .../wire-ds/design-system.manifest.yaml | 53 - .../products/acme/features/feat-checkout.yaml | 2 - .../products/acme/features/feat-refunds.yaml | 2 - .product/products/acme/how-contract.yaml | 118 - .../acme/releases/rel-storefront.yaml | 2 - .product/products/acme/targets/tgt-2.yaml | 3 - .../product-cli/authoring-scopes/figma.yaml | 43 - .../product-cli/cells/add-mcp-tool.yaml | 50 - .../product-cli/cells/implement-slice.yaml | 45 - .../product-cli/cells/serve-web-view.yaml | 52 - .../blueprints/product-cli/how-contract.yaml | 5 - .../blueprints/product-cli/layout.yaml | 51 - .../work-units/cli-adapter-e-entity.yaml | 18 - .../work-units/core-slice-e-entity.yaml | 20 - .../work-units/handler-e-command.yaml | 20 - .../integration-tests-e-entity.yaml | 14 - .../work-units/registry-wiring-e-command.yaml | 13 - .../work-units/tool-def-e-command.yaml | 18 - .../work-units/tool-test-e-command.yaml | 14 - .../deciders/e-boundedcontext-decider.yaml | 8 - .../deciders/e-viewgraph-decider.yaml | 43 - .../deliverables/busiest-gate.yaml | 8 - .../deliverables/session-escalated.yaml | 8 - .../deliverables/session-start.yaml | 21 - .../deliverables/ship-ctx-view-live.yaml | 13 - .../deliverables/spec-depth-metric.yaml | 8 - .../product-cli/features/ctx-view-live.yaml | 4 - .../product-cli/features/session-start.yaml | 10 - .../features/spec-depth-metric.yaml | 5 - .../products/product-cli/how-contract.yaml | 207 -- .../product-cli/product-cli.provenance.json | 12 - .product/products/product-cli/product-cli.ttl | 1614 --------- .../product-cli/targets/what-1.2.yaml | 5 - .../work-units/view-client-rm-view-graph.yaml | 19 - .../work-units/view-routes-rm-view-graph.yaml | 19 - .../view-serializer-rm-view-graph.yaml | 21 - .../view-watcher-rm-view-graph.yaml | 19 - .product/role-bindings.yaml | 40 - .stylelintrc.json | 7 - CLAUDE.md | 856 ----- Cargo.toml | 35 - conventions/README.md | 118 - conventions/adr/ADR-0001-file-length-limit.md | 60 - conventions/adr/ADR-0002-no-unwrap-in-lib.md | 64 - conventions/adr/ADR-0003-cargo-deny.md | 58 - conventions/adr/ADR-0004-ctxid-newtype.md | 70 - conventions/adr/ADR-0005-fitness-checks.md | 73 - conventions/docs/CTX-DEPS.md | 55 - conventions/docs/CTX001.md | 77 - conventions/docs/CTX002.md | 73 - conventions/docs/CTX003.md | 86 - conventions/docs/CTX004.md | 69 - conventions/docs/CTX005.md | 84 - dagger/.gitattributes | 4 - dagger/.gitignore | 6 - dagger/dagger.json | 8 - dagger/go.mod | 53 - dagger/go.sum | 97 - dagger/main.go | 167 - ddd-cli/Cargo.toml | 40 - ddd-cli/src/commands/bind.rs | 109 - ddd-cli/src/commands/content_hash.rs | 40 - ddd-cli/src/commands/diff.rs | 45 - ddd-cli/src/commands/diff_contracts.rs | 96 - ddd-cli/src/commands/init.rs | 25 - ddd-cli/src/commands/migrate.rs | 204 -- ddd-cli/src/commands/migrate_plan.rs | 313 -- ddd-cli/src/commands/mod.rs | 179 - ddd-cli/src/commands/render.rs | 32 - ddd-cli/src/commands/report.rs | 234 -- ddd-cli/src/commands/serve.rs | 56 - ddd-cli/src/commands/validate.rs | 49 - ddd-cli/src/commands/what.rs | 68 - ddd-cli/src/commands/why.rs | 31 - ddd-cli/src/main.rs | 32 - ddd-cli/tests/contracts.rs | 244 -- ddd-cli/tests/corpus.rs | 344 -- .../corpus/htmlcss/01-token-added/after.css | 21 - .../corpus/htmlcss/01-token-added/before.css | 20 - .../corpus/htmlcss/01-token-added/labels.yaml | 6 - .../corpus/htmlcss/02-token-removed/after.css | 19 - .../htmlcss/02-token-removed/before.css | 20 - .../htmlcss/02-token-removed/labels.yaml | 6 - .../corpus/htmlcss/03-token-retype/after.css | 20 - .../corpus/htmlcss/03-token-retype/before.css | 20 - .../htmlcss/03-token-retype/labels.yaml | 6 - .../htmlcss/04-token-value-change/after.css | 20 - .../htmlcss/04-token-value-change/before.css | 20 - .../htmlcss/04-token-value-change/labels.yaml | 6 - .../corpus/htmlcss/05-class-added/after.css | 24 - .../corpus/htmlcss/05-class-added/before.css | 20 - .../corpus/htmlcss/05-class-added/labels.yaml | 6 - .../corpus/htmlcss/06-class-removed/after.css | 16 - .../htmlcss/06-class-removed/before.css | 20 - .../htmlcss/06-class-removed/labels.yaml | 6 - .../corpus/htmlcss/07-layer-order/after.css | 20 - .../corpus/htmlcss/07-layer-order/before.css | 20 - .../corpus/htmlcss/07-layer-order/labels.yaml | 6 - .../corpus/htmlcss/08-link-rewired/after.html | 10 - .../htmlcss/08-link-rewired/before.html | 10 - .../htmlcss/08-link-rewired/labels.yaml | 8 - .../htmlcss/09-mixed-theme-pass/after.css | 18 - .../htmlcss/09-mixed-theme-pass/before.css | 20 - .../htmlcss/09-mixed-theme-pass/labels.yaml | 12 - .../tests/corpus/rust/01-add-pub-fn/after.rs | 52 - .../tests/corpus/rust/01-add-pub-fn/before.rs | 48 - .../corpus/rust/01-add-pub-fn/labels.yaml | 10 - .../corpus/rust/02-add-private-fn/after.rs | 52 - .../corpus/rust/02-add-private-fn/before.rs | 48 - .../corpus/rust/02-add-private-fn/labels.yaml | 6 - .../rust/03-signature-change-pub-fn/after.rs | 48 - .../rust/03-signature-change-pub-fn/before.rs | 48 - .../03-signature-change-pub-fn/labels.yaml | 10 - .../04-signature-change-private-fn/after.rs | 48 - .../04-signature-change-private-fn/before.rs | 48 - .../labels.yaml | 6 - .../rust/05-promote-private-fn/after.rs | 48 - .../rust/05-promote-private-fn/before.rs | 48 - .../rust/05-promote-private-fn/labels.yaml | 6 - .../rust/06-add-pub-crate-method/after.rs | 52 - .../rust/06-add-pub-crate-method/before.rs | 48 - .../rust/06-add-pub-crate-method/labels.yaml | 6 - .../corpus/rust/07-add-trait-impl/after.rs | 54 - .../corpus/rust/07-add-trait-impl/before.rs | 48 - .../corpus/rust/07-add-trait-impl/labels.yaml | 11 - .../corpus/rust/08-remove-pub-fn/after.rs | 41 - .../corpus/rust/08-remove-pub-fn/before.rs | 48 - .../corpus/rust/08-remove-pub-fn/labels.yaml | 10 - .../corpus/rust/09-derive-change/after.rs | 48 - .../corpus/rust/09-derive-change/before.rs | 48 - .../corpus/rust/09-derive-change/labels.yaml | 6 - .../corpus/rust/10-add-trait-member/after.rs | 49 - .../corpus/rust/10-add-trait-member/before.rs | 48 - .../rust/10-add-trait-member/labels.yaml | 7 - .../corpus/rust/11-mixed-feature/after.rs | 56 - .../corpus/rust/11-mixed-feature/before.rs | 48 - .../corpus/rust/11-mixed-feature/labels.yaml | 14 - .../corpus/rust/12-mixed-refactor/after.rs | 56 - .../corpus/rust/12-mixed-refactor/before.rs | 48 - .../corpus/rust/12-mixed-refactor/labels.yaml | 14 - ddd-cli/tests/ddd.rs | 280 -- ddd-cli/tests/fixtures/bicep/bicep.sarif | 35 - ddd-cli/tests/fixtures/bicep/bicepconfig.json | 15 - ddd-cli/tests/fixtures/bicep/main.bicep | 19 - .../tests/fixtures/bicep/storage-module.bicep | 23 - ddd-cli/tests/fixtures/csharp/.editorconfig | 7 - ddd-cli/tests/fixtures/csharp/Library.cs | 42 - ddd-cli/tests/fixtures/csharp/Program.cs | 16 - ddd-cli/tests/fixtures/csharp/diag.sarif | 62 - ddd-cli/tests/fixtures/csharp/minimal.csproj | 22 - ddd-cli/tests/fixtures/rust/Cargo.toml.txt | 13 - ddd-cli/tests/fixtures/rust/lib.rs | 104 - ddd-cli/tests/fixtures/rust/other.rs | 9 - ddd-cli/tests/fixtures/visual/panel.html | 32 - .../tests/fixtures/webpair/.htmlvalidate.json | 5 - .../tests/fixtures/webpair/.stylelintrc.json | 5 - .../fixtures/webpair/htmlvalidate.out.json | 20 - ddd-cli/tests/fixtures/webpair/page.html | 5 - ddd-cli/tests/fixtures/webpair/style.css | 6 - .../tests/fixtures/webpair/stylelint.out.json | 20 - ddd-cli/tests/governance.rs | 502 --- ddd-cli/tests/mcp.rs | 41 - ddd-cli/tests/migrate.rs | 118 - ddd-cli/tests/render.rs | 111 - ddd-cli/tests/visual.rs | 85 - ddd-core/Cargo.toml | 30 - ddd-core/assets/render.css | 40 - ddd-core/src/basis_pin.rs | 198 -- ddd-core/src/bicepconfig.rs | 73 - ddd-core/src/binding.rs | 120 - ddd-core/src/cargolints.rs | 84 - ddd-core/src/claim.rs | 189 -- ddd-core/src/concordance.rs | 109 - ddd-core/src/config.rs | 218 -- ddd-core/src/configured.rs | 23 - ddd-core/src/contracts.rs | 253 -- ddd-core/src/decision.rs | 256 -- ddd-core/src/detect.rs | 310 -- ddd-core/src/detect_tests.rs | 139 - ddd-core/src/diff.rs | 233 -- ddd-core/src/diff_tests.rs | 173 - ddd-core/src/editorconfig.rs | 97 - ddd-core/src/gitrev.rs | 153 - ddd-core/src/htmlvalidateconfig.rs | 93 - ddd-core/src/init.rs | 107 - ddd-core/src/ledger_gate.rs | 255 -- ddd-core/src/lib.rs | 47 - ddd-core/src/manifest.rs | 91 - ddd-core/src/pattern.rs | 48 - ddd-core/src/predicate.rs | 129 - ddd-core/src/render.rs | 330 -- ddd-core/src/render_tests.rs | 71 - ddd-core/src/report.rs | 219 -- ddd-core/src/report_tests.rs | 185 - ddd-core/src/revisit.rs | 149 - ddd-core/src/revisit_tests.rs | 175 - ddd-core/src/rules.rs | 257 -- ddd-core/src/sarif.rs | 137 - ddd-core/src/seam.rs | 121 - ddd-core/src/seam_event.rs | 138 - ddd-core/src/store.rs | 163 - ddd-core/src/store_tests.rs | 100 - ddd-core/src/stylelintconfig.rs | 87 - ddd-core/src/surface.rs | 176 - ddd-core/src/tokenfile.rs | 109 - ddd-core/src/turtle.rs | 172 - ddd-core/src/validate.rs | 372 -- ddd-core/src/validate_format_tests.rs | 225 -- ddd-core/src/validate_tests.rs | 303 -- ddd-core/src/what.rs | 341 -- ddd-core/src/what_tests.rs | 242 -- ddd-core/src/why.rs | 253 -- ddd-core/src/why_tests.rs | 156 - ddd-lsp/Cargo.toml | 30 - ddd-lsp/src/adapter/bicep.rs | 203 -- ddd-lsp/src/adapter/csharp.rs | 170 - ddd-lsp/src/adapter/csharp_facts.rs | 257 -- ddd-lsp/src/adapter/htmlcss.rs | 151 - ddd-lsp/src/adapter/htmlcss_facts.rs | 358 -- ddd-lsp/src/adapter/htmlcss_facts_tests.rs | 91 - ddd-lsp/src/adapter/htmlcss_pair.rs | 364 -- ddd-lsp/src/adapter/htmlcss_pair_tests.rs | 143 - ddd-lsp/src/adapter/htmlcss_policy_tests.rs | 78 - ddd-lsp/src/adapter/mod.rs | 188 -- ddd-lsp/src/adapter/rust.rs | 264 -- ddd-lsp/src/adapter/rust_facts.rs | 357 -- ddd-lsp/src/adapter/rust_policy_tests.rs | 246 -- ddd-lsp/src/adapter/rust_tests.rs | 259 -- ddd-lsp/src/bin/ddd-mock-lsp.rs | 25 - ddd-lsp/src/capability.rs | 330 -- ddd-lsp/src/classify.rs | 127 - ddd-lsp/src/classify_tests.rs | 233 -- ddd-lsp/src/client.rs | 213 -- ddd-lsp/src/declaration.rs | 223 -- ddd-lsp/src/error.rs | 43 - ddd-lsp/src/host.rs | 358 -- ddd-lsp/src/jsonrpc.rs | 89 - ddd-lsp/src/lib.rs | 30 - ddd-lsp/src/manager.rs | 117 - ddd-lsp/src/mock.rs | 342 -- ddd-lsp/src/mock/hierarchy.rs | 201 -- ddd-lsp/src/mock/parse.rs | 208 -- ddd-lsp/src/protocol.rs | 326 -- ddd-lsp/src/revdiff.rs | 167 - ddd-lsp/src/revdiff_tests.rs | 112 - ddd-lsp/tests/declaration_subset.rs | 174 - ddd-lsp/tests/host.rs | 125 - ddd-lsp/tests/real_hosts.rs | 89 - ddd-lsp/tests/rust_host.rs | 196 -- ddd-mcp/Cargo.toml | 33 - ddd-mcp/src/bin/ddd-mcp-mock-lsp.rs | 22 - ddd-mcp/src/bind.rs | 203 -- ddd-mcp/src/dispatch.rs | 26 - ddd-mcp/src/edits.rs | 76 - ddd-mcp/src/govern_tools.rs | 399 --- ddd-mcp/src/intercept.rs | 367 -- ddd-mcp/src/lang_tools.rs | 299 -- ddd-mcp/src/lib.rs | 18 - ddd-mcp/src/serve.rs | 31 - ddd-mcp/src/state.rs | 74 - ddd-mcp/src/tools.rs | 225 -- ddd-mcp/tests/rust_governed.rs | 456 --- ddd-mcp/tests/serve.rs | 740 ---- ddd-mcp/tests/webpair_governed.rs | 209 -- deny.toml | 90 - dist-workspace.toml | 19 - docs/ai-foundations-conformance.md | 174 - docs/audits/basis-quality-2026-08.md | 585 ---- docs/audits/classifier-corpus-2026-08.md | 99 - docs/audits/provenance-2026-08.md | 649 ---- docs/ddd-adrs.md | 88 - docs/ddd-cli-prd.md | 67 - docs/ddd-format-migrations.md | 388 --- docs/ddd-m5-report.md | 192 -- docs/ddd-m6-experiment.md | 472 --- docs/ddd-m7-experiment.md | 493 --- docs/ddd-m8-report.md | 288 -- docs/ddd-research-protocol.md | 109 - docs/ddd-roadmap.md | 85 - docs/ddd-roslyn-host-plan.md | 101 - docs/ddd-v1-spec.md | 446 --- docs/decisions/hafeok.ddd.nt | 2990 ----------------- docs/decisions/hafeok.ledger.nt | 473 --- docs/eval-format-v1.md | 344 -- docs/eval-format-v1/context-digest.json | 61 - docs/examples/ddd-render-sample.html | 86 - docs/examples/product-cli-blueprint.md | 61 - .../g-dec-01-reading-vocabulary.yaml | 47 - .../g-dec-02-projection-reasoner.yaml | 25 - .../g0-entry-probes-v3-v5-2026-08-17.md | 616 ---- docs/g-track/g0-session-report-2026-08-19.md | 425 --- docs/g-track/g1-session-report-2026-08-17.md | 501 --- .../kotlin-lsp-workspace-import-2026-08-17.md | 510 --- docs/g-track/prd-ground-as-ontology.md | 1094 ------ .../registry-generator-design-2026-08-18.md | 380 --- .../registry-generator-session-2026-08-18.md | 259 -- .../.github/workflows/validate.yml | 108 - docs/g-track/registry-template/GENERATION.ttl | 42 - docs/g-track/registry-template/README.md | 63 - docs/g-track/registry-template/TEMPLATE.md | 129 - .../graphs/canonical/_exemplar-decision.ttl | 25 - .../graphs/canonical/_exemplar.ttl | 23 - .../scripts/build-projection.sh | 36 - .../registry-template/shapes/decision.ttl | 62 - .../registry-template/shapes/derivation.ttl | 112 - .../registry-template/shapes/reading.ttl | 51 - .../registry-template/shapes/structural.ttl | 30 - .../registry-template/vocabulary/reg.ttl | 135 - docs/g-track/two-axis-session-2026-08-20.md | 626 ---- docs/guide.md | 248 -- docs/live-wire-plan.md | 168 - docs/predicate-format.md | 96 - docs/product-framework-open.md | 1179 ------- docs/reviews/ddd-cli-prd-review-2026-08.md | 448 --- docs/skills/product-v1.md | 240 -- docs/spark-dispatch.md | 170 - docs/spec-flow-store-v1.md | 396 --- docs/two-pillars-conformance.md | 53 - ...-of-working-decision-allocated-delivery.md | 640 ---- docs/workshop-runbook.md | 84 - docs/workshop/README.md | 45 - docs/workshop/exercise-1-handout-da.md | 99 - docs/workshop/exercise-2-handout-da.md | 64 - docs/workshop/facilitator-spec-da.md | 237 -- eval-core/Cargo.toml | 28 - eval-core/src/backend.rs | 113 - eval-core/src/backend_tests.rs | 72 - eval-core/src/behaviour.rs | 169 - eval-core/src/behaviour_tests.rs | 106 - eval-core/src/blobs.rs | 118 - eval-core/src/blobs_tests.rs | 52 - eval-core/src/declaration.rs | 123 - eval-core/src/declaration_tests.rs | 34 - eval-core/src/digest.rs | 54 - eval-core/src/digest_tests.rs | 58 - eval-core/src/escape.rs | 167 - eval-core/src/escape_tests.rs | 100 - eval-core/src/judgement.rs | 96 - eval-core/src/judgement_tests.rs | 53 - eval-core/src/lib.rs | 46 - eval-core/src/pinned.rs | 55 - eval-core/src/pinned_tests.rs | 28 - eval-core/src/run.rs | 227 -- eval-core/src/run_tests.rs | 49 - eval-core/src/store.rs | 126 - eval-core/src/store_tests.rs | 94 - eval-core/tests/fixture.rs | 115 - .../tests/fixtures/dotnet-written-run.json | 56 - eval-core/tests/interop.rs | 77 - eval-dotnet/.gitignore | 2 - eval-dotnet/Directory.Build.props | 16 - eval-dotnet/Directory.Packages.props | 16 - eval-dotnet/Eval.slnx | 8 - eval-dotnet/README.md | 321 -- eval-dotnet/src/Eval/Backend.cs | 84 - eval-dotnet/src/Eval/Behaviour.cs | 172 - eval-dotnet/src/Eval/Blobs.cs | 82 - eval-dotnet/src/Eval/Declaration.cs | 90 - eval-dotnet/src/Eval/Escape.cs | 134 - eval-dotnet/src/Eval/Eval.csproj | 9 - eval-dotnet/src/Eval/EvalStore.cs | 103 - eval-dotnet/src/Eval/Evaluators.cs | 232 -- eval-dotnet/src/Eval/Judgement.cs | 170 - eval-dotnet/src/Eval/Judging.cs | 185 - eval-dotnet/src/Eval/RunContext.cs | 38 - eval-dotnet/src/Eval/RunObservation.cs | 162 - eval-dotnet/src/Eval/RunRecord.cs | 113 - .../tests/Eval.Tests/BehaviourTests.cs | 124 - .../Eval.Tests/ContextDigestFixtureTests.cs | 85 - .../tests/Eval.Tests/Eval.Tests.csproj | 14 - .../tests/Eval.Tests/EvaluationTests.cs | 345 -- eval-dotnet/tests/Eval.Tests/GlobalUsings.cs | 1 - eval-dotnet/tests/Eval.Tests/TempStore.cs | 29 - examples/checkout/content-store.manifest.yaml | 57 - examples/checkout/design-system.manifest.yaml | 72 - ground-cli/Cargo.toml | 31 - ground-cli/src/extract.rs | 292 -- ground-cli/src/extract_tests.rs | 77 - ground-cli/src/main.rs | 115 - ground-cli/src/read.rs | 261 -- ground-cli/src/read_tests.rs | 75 - ground-cli/src/symbols.rs | 159 - ground-cli/src/symbols_tests.rs | 129 - ledger-cli/Cargo.toml | 43 - ledger-cli/src/commands/add.rs | 49 - ledger-cli/src/commands/authority.rs | 145 - ledger-cli/src/commands/authority_enum.rs | 125 - ledger-cli/src/commands/basis_text.rs | 91 - ledger-cli/src/commands/cli_enum.rs | 360 -- ledger-cli/src/commands/common.rs | 155 - ledger-cli/src/commands/declare.rs | 29 - ledger-cli/src/commands/diff_cmd.rs | 59 - ledger-cli/src/commands/evolve.rs | 89 - ledger-cli/src/commands/export.rs | 57 - ledger-cli/src/commands/graph_cmds.rs | 43 - ledger-cli/src/commands/inbox.rs | 188 -- ledger-cli/src/commands/inbox_accept.rs | 242 -- ledger-cli/src/commands/inbox_findings.rs | 59 - ledger-cli/src/commands/inbox_render.rs | 121 - ledger-cli/src/commands/init.rs | 26 - ledger-cli/src/commands/inspect.rs | 71 - ledger-cli/src/commands/merge_cmd.rs | 131 - ledger-cli/src/commands/mod.rs | 161 - ledger-cli/src/commands/resolve_cmd.rs | 266 -- ledger-cli/src/commands/sign.rs | 145 - ledger-cli/src/commands/terminal.rs | 124 - .../src/commands/terminal_callers_tests.rs | 121 - ledger-cli/src/commands/terminal_tests.rs | 135 - ledger-cli/src/commands/verify.rs | 106 - ledger-cli/src/main.rs | 29 - ledger-cli/tests/accept_group.rs | 374 --- ledger-cli/tests/authority.rs | 390 --- ledger-cli/tests/batch.rs | 190 -- ledger-cli/tests/binding_accounting.rs | 148 - ledger-cli/tests/cli.rs | 98 - ledger-cli/tests/closed_key.rs | 280 -- ledger-cli/tests/common/export_only.rs | 237 -- ledger-cli/tests/common/hand.rs | 192 -- ledger-cli/tests/common/mod.rs | 280 -- ledger-cli/tests/diff.rs | 141 - ledger-cli/tests/digests.rs | 53 - ledger-cli/tests/export.rs | 118 - ledger-cli/tests/export_verifier.rs | 95 - .../log/01KZPMA8HF297MMN8232TM4FCE.yml | 14 - .../log/01KZPMA8J24KD92JZQTDC6Y6B4.yml | 17 - .../log/01KZPMA8JPQSTRM3X8E7R0S1KA.yml | 17 - .../log/01KZPMA8KCK8XJWJTB98QBFZ27.yml | 12 - .../log/01KZPMA8M3D597TGJJ8W2NA04B.yml | 17 - .../log/01KZPMA8MXG2CAYGBS3X827NAY.yml | 15 - .../log/01KZPMA8NTBFYDABGM1ZHFC4SG.yml | 17 - .../log/01KZPMA8PSTE42V7GE8H747PG7.yml | 15 - .../log/01KZPMA8QWE8B74CGR8M5RC1MX.yml | 17 - .../log/01KZPMA8S0K9SNDXXK4NS2ZWFA.yml | 12 - .../log/01KZPMA8T68ZYVCJZXG9Z07FYW.yml | 17 - .../log/01KZPMA8VERG1CGNA3JWWADRF1.yml | 17 - .../log/01KZPMA8WVA4P7GEDV915Y5GG8.yml | 17 - .../log/01KZPMA8YA7J3H1HQT79599BDJ.yml | 16 - .../log/01KZPMA8ZWBD1ADF7GP27HGH6S.yml | 16 - .../.decisions/sets/coverage-fixture.yml | 7 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 20 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDY0.yml | 14 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDY1.yml | 14 - .../forked/.decisions/sets/forked.yml | 8 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 15 - .../l001/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 19 - .../l002/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 24 - .../l003/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 19 - .../l004/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 25 - .../l005/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 25 - .../l006/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 19 - .../l007/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 24 - .../l008/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 21 - .../l010/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 27 - .../l013/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 30 - .../l014/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml | 24 - .../pass/.decisions/sets/ledger-fixture.yml | 9 - .../log/01K2000000000000000000000A.yml | 22 - .../log/01K2ZZZZZZZZZZZZZZZZZZZZZZ.yml | 19 - .../two-clocks/.decisions/sets/two-clocks.yml | 8 - ledger-cli/tests/format_history.rs | 91 - ledger-cli/tests/gate.rs | 308 -- ledger-cli/tests/genesis_key.rs | 237 -- ledger-cli/tests/git_identity.rs | 47 - ledger-cli/tests/graph.rs | 219 -- ledger-cli/tests/immutability.rs | 118 - ledger-cli/tests/inbox.rs | 153 - ledger-cli/tests/inbox_failures.rs | 159 - ledger-cli/tests/inbox_fixture/mod.rs | 118 - ledger-cli/tests/interactive.rs | 193 -- ledger-cli/tests/key_across_namespaces.rs | 246 -- ledger-cli/tests/key_ownership.rs | 125 - ledger-cli/tests/keys.rs | 76 - ledger-cli/tests/landed_header.rs | 55 - ledger-cli/tests/legacy_revocation.rs | 145 - ledger-cli/tests/merge.rs | 352 -- ledger-cli/tests/policy_authors.rs | 130 - ledger-cli/tests/pre_policy_binding.rs | 222 -- ledger-cli/tests/revisit_format.rs | 161 - ledger-cli/tests/role_position.rs | 68 - ledger-cli/tests/rotate_key.rs | 74 - ledger-cli/tests/scalars.rs | 65 - ledger-cli/tests/show.rs | 188 -- ledger-cli/tests/signing.rs | 315 -- ledger-cli/tests/trust.rs | 321 -- ledger-cli/tests/ungoverned_sidecar.rs | 72 - ledger-cli/tests/verbs.rs | 255 -- ledger-core/Cargo.toml | 38 - ledger-core/src/acceptance.rs | 129 - ledger-core/src/acceptance_tests.rs | 77 - ledger-core/src/allocation.rs | 191 -- ledger-core/src/allocation_tests.rs | 138 - ledger-core/src/author/acceptance_ops.rs | 196 -- ledger-core/src/author/authority_ops.rs | 387 --- ledger-core/src/author/availability_ops.rs | 107 - ledger-core/src/author/batch_accept.rs | 206 -- ledger-core/src/author/decision.rs | 172 - ledger-core/src/author/declare.rs | 62 - ledger-core/src/author/genesis_key.rs | 172 - ledger-core/src/author/group_accept.rs | 263 -- ledger-core/src/author/identity_ops.rs | 123 - ledger-core/src/author/mod.rs | 243 -- ledger-core/src/author/policy_ops.rs | 84 - ledger-core/src/author/report.rs | 148 - ledger-core/src/author/sign_ops.rs | 153 - ledger-core/src/author/sign_tests.rs | 199 -- ledger-core/src/author/tests.rs | 233 -- ledger-core/src/author/version_ops.rs | 209 -- ledger-core/src/authority/availability.rs | 94 - ledger-core/src/authority/binding.rs | 121 - ledger-core/src/authority/check.rs | 255 -- ledger-core/src/authority/check_tests.rs | 213 -- ledger-core/src/authority/choice.rs | 92 - ledger-core/src/authority/choice_tests.rs | 159 - ledger-core/src/authority/filing.rs | 111 - ledger-core/src/authority/filing_tests.rs | 113 - ledger-core/src/authority/fixture.rs | 112 - ledger-core/src/authority/grant.rs | 257 -- ledger-core/src/authority/key_close.rs | 80 - ledger-core/src/authority/key_close_tests.rs | 78 - ledger-core/src/authority/mod.rs | 40 - ledger-core/src/authority/payload.rs | 211 -- ledger-core/src/authority/payload_tests.rs | 130 - ledger-core/src/authority/policy.rs | 103 - ledger-core/src/authority/references.rs | 231 -- ledger-core/src/authority/revocation.rs | 183 - ledger-core/src/authority/role.rs | 225 -- ledger-core/src/authority/signers.rs | 116 - ledger-core/src/authority/structure.rs | 124 - ledger-core/src/authority/view.rs | 182 - ledger-core/src/batch.rs | 292 -- ledger-core/src/batch_drift.rs | 177 - ledger-core/src/batch_file.rs | 131 - ledger-core/src/batch_file_tests.rs | 79 - ledger-core/src/batch_render.rs | 94 - ledger-core/src/batch_tests.rs | 232 -- ledger-core/src/blame.rs | 73 - ledger-core/src/blame_tests.rs | 88 - ledger-core/src/canon.rs | 140 - ledger-core/src/canon_tests.rs | 328 -- ledger-core/src/changeset.rs | 178 - ledger-core/src/coverage.rs | 128 - ledger-core/src/diff.rs | 268 -- ledger-core/src/diff_tests.rs | 148 - ledger-core/src/discharge.rs | 157 - ledger-core/src/discharge_tests.rs | 68 - ledger-core/src/finding.rs | 253 -- ledger-core/src/format.rs | 161 - ledger-core/src/graph/authority.rs | 225 -- ledger-core/src/graph/export.rs | 257 -- ledger-core/src/graph/export_tests.rs | 200 -- ledger-core/src/graph/index.rs | 47 - ledger-core/src/graph/mod.rs | 139 - ledger-core/src/graph/ntriples.rs | 103 - ledger-core/src/graph/revocation_tests.rs | 95 - ledger-core/src/graph/shapes.rs | 291 -- ledger-core/src/graph/tests.rs | 249 -- ledger-core/src/graph/turtle.rs | 265 -- ledger-core/src/hash.rs | 154 - ledger-core/src/hold.rs | 79 - ledger-core/src/id.rs | 205 -- ledger-core/src/id_tests.rs | 78 - ledger-core/src/identity.rs | 144 - ledger-core/src/identity_tests.rs | 95 - ledger-core/src/inbox/config.rs | 66 - ledger-core/src/inbox/git.rs | 139 - ledger-core/src/inbox/index.rs | 126 - ledger-core/src/inbox/index_tests.rs | 37 - ledger-core/src/inbox/list.rs | 237 -- ledger-core/src/inbox/mod.rs | 19 - ledger-core/src/inbox/terms.rs | 50 - ledger-core/src/init.rs | 123 - ledger-core/src/key.rs | 110 - ledger-core/src/landed.rs | 82 - ledger-core/src/landed_tests.rs | 47 - ledger-core/src/landing.rs | 284 -- ledger-core/src/landing_tests.rs | 80 - ledger-core/src/lib.rs | 60 - ledger-core/src/merge/conflicts.rs | 183 - ledger-core/src/merge/driver.rs | 126 - ledger-core/src/merge/mod.rs | 71 - ledger-core/src/merge/plan.rs | 235 -- ledger-core/src/merge/resolve.rs | 227 -- ledger-core/src/merge/tests.rs | 349 -- ledger-core/src/mint.rs | 141 - ledger-core/src/render.rs | 172 - ledger-core/src/revision.rs | 142 - ledger-core/src/revisit.rs | 102 - ledger-core/src/scalars.rs | 87 - ledger-core/src/scalars_tests.rs | 52 - ledger-core/src/set.rs | 102 - ledger-core/src/show.rs | 349 -- ledger-core/src/show_render.rs | 165 - ledger-core/src/show_tests.rs | 161 - ledger-core/src/signing/check.rs | 392 --- ledger-core/src/signing/check_tests.rs | 121 - ledger-core/src/signing/dsse.rs | 137 - ledger-core/src/signing/dsse_tests.rs | 47 - ledger-core/src/signing/mod.rs | 89 - ledger-core/src/signing/review.rs | 80 - ledger-core/src/signing/ssh.rs | 207 -- ledger-core/src/signing/subject.rs | 151 - ledger-core/src/store.rs | 298 -- ledger-core/src/store_tests.rs | 210 -- ledger-core/src/testkit.rs | 199 -- ledger-core/src/tier.rs | 110 - ledger-core/src/tier_tests.rs | 52 - ledger-core/src/verify/acts.rs | 144 - ledger-core/src/verify/authority.rs | 86 - ledger-core/src/verify/authority_tests.rs | 151 - ledger-core/src/verify/disposition.rs | 136 - ledger-core/src/verify/duplicate_tests.rs | 91 - .../src/verify/escape_acceptor_tests.rs | 29 - ledger-core/src/verify/genesis_role.rs | 58 - ledger-core/src/verify/genesis_role_tests.rs | 74 - ledger-core/src/verify/history.rs | 175 - ledger-core/src/verify/integrity.rs | 109 - ledger-core/src/verify/keys.rs | 91 - ledger-core/src/verify/keys_tests.rs | 119 - ledger-core/src/verify/mod.rs | 283 -- ledger-core/src/verify/mod_tests.rs | 390 --- ledger-core/src/verify/order_grid.rs | 280 -- .../src/verify/order_regressions_tests.rs | 38 - ledger-core/src/verify/order_tests.rs | 154 - ledger-core/src/verify/state.rs | 170 - ledger-core/src/verify/state_tests.rs | 131 - ledger-core/src/verify/view.rs | 246 -- ledger-core/src/version.rs | 224 -- ledger-core/src/version_tests.rs | 99 - ledger-core/src/whoami.rs | 60 - ledger/README.md | 80 - ledger/audits/audit-2026-10.md | 547 --- ledger/prd/decision-ledger-prd.md | 583 ---- ledger/prd/decision-registry-prd.md | 123 - ledger/prd/ledger-cli-prd.md | 128 - ledger/prd/namespace-independence-prd.md | 1483 -------- .../absorption-replies-rulings-2026-10-07.md | 49 - ...basis-and-absorption-rulings-2026-10-06.md | 13 - .../ground-and-protocol-rulings-2026-10.md | 251 -- .../namespace-design-rulings-2026-10-07.md | 57 - .../order-from-the-acts-rulings-2026-10-09.md | 37 - .../rulings/signing-rulings-2026-10-d5-d9.md | 250 -- ledger/rulings/signing-rulings-2026-10.md | 169 - .../verification-rulings-2026-10-07.md | 27 - ledger/sessions/2026-10-namespace-design.md | 2386 ------------- .../sessions/2026-10-protocol-absorption.md | 786 ----- ledger/sessions/2026-10-repo-setup.md | 394 --- ledger/sessions/2026-10-session-a.md | 298 -- ledger/sessions/2026-10-session-b.md | 216 -- ledger/sessions/2026-10-verification-fixes.md | 220 -- ledger/sessions/2026-10-verification.md | 1174 ------- .../sessions/acceptance-worksheet-2026-08.md | 294 -- ledger/sessions/product-cli-v2-sessions.md | 140 - .../authority/ledger-authority-shapes.ttl | 177 - ledger/spec/authority/ledger-authority.ttl | 230 -- ledger/spec/authority/sample-invalid.ttl | 27 - ledger/spec/authority/sample-valid.ttl | 58 - ledger/spec/ledger-protocol.md | 2265 ------------- ledger/spec/server-client-protocol.md | 332 -- ledger/spec/tests/README.md | 23 - product-cli/Cargo.toml | 40 - product-cli/src/commands/author.rs | 137 - product-cli/src/commands/blueprint.rs | 192 -- product-cli/src/commands/build.rs | 383 --- product-cli/src/commands/build_guard.rs | 99 - product-cli/src/commands/build_lsp.rs | 150 - product-cli/src/commands/build_seam_emit.rs | 102 - product-cli/src/commands/build_session.rs | 81 - product-cli/src/commands/build_skip.rs | 35 - product-cli/src/commands/build_verify.rs | 131 - product-cli/src/commands/cell.rs | 227 -- product-cli/src/commands/codegen.rs | 387 --- product-cli/src/commands/codegen_how.rs | 112 - product-cli/src/commands/completions.rs | 22 - product-cli/src/commands/decider.rs | 285 -- product-cli/src/commands/deliverable.rs | 297 -- product-cli/src/commands/deployable_unit.rs | 223 -- product-cli/src/commands/design_system.rs | 237 -- product-cli/src/commands/dispatch.rs | 92 - product-cli/src/commands/domain.rs | 363 -- product-cli/src/commands/domain_data.rs | 120 - product-cli/src/commands/domain_fields.rs | 367 -- product-cli/src/commands/domain_fields_v16.rs | 93 - product-cli/src/commands/domain_rows.rs | 111 - product-cli/src/commands/feature.rs | 146 - product-cli/src/commands/guide.rs | 19 - product-cli/src/commands/hooks.rs | 38 - product-cli/src/commands/how.rs | 272 -- product-cli/src/commands/how_fields.rs | 150 - product-cli/src/commands/init.rs | 146 - product-cli/src/commands/init_helpers.rs | 127 - product-cli/src/commands/lsp.rs | 266 -- product-cli/src/commands/mcp_cmd.rs | 50 - product-cli/src/commands/mod.rs | 92 - product-cli/src/commands/output.rs | 152 - product-cli/src/commands/preview.rs | 137 - product-cli/src/commands/primitive.rs | 137 - product-cli/src/commands/product.rs | 124 - product-cli/src/commands/projector.rs | 239 -- product-cli/src/commands/question.rs | 250 -- product-cli/src/commands/registry.rs | 196 -- product-cli/src/commands/release.rs | 140 - product-cli/src/commands/root_enum.rs | 258 -- product-cli/src/commands/scope.rs | 290 -- product-cli/src/commands/seam.rs | 41 - product-cli/src/commands/session.rs | 135 - product-cli/src/commands/shared.rs | 71 - product-cli/src/commands/skills.rs | 94 - product-cli/src/commands/skills_tests.rs | 53 - product-cli/src/commands/target.rs | 157 - product-cli/src/commands/verdict.rs | 27 - product-cli/src/commands/work_unit.rs | 200 -- product-cli/src/commands/worker.rs | 337 -- product-cli/src/main.rs | 67 - product-cli/tests/agent_context.rs | 87 - product-cli/tests/code_quality_tests.rs | 796 ----- .../external-core-consumer/Cargo.toml | 18 - .../external-core-consumer/src/main.rs | 19 - .../tests/fixtures/onboard-large/Cargo.toml | 16 - .../fixtures/onboard-large/src/api/health.rs | 7 - .../fixtures/onboard-large/src/api/routes.rs | 7 - .../onboard-large/src/api/versioning.rs | 5 - .../fixtures/onboard-large/src/auth/jwt.rs | 9 - .../onboard-large/src/auth/middleware.rs | 6 - .../onboard-large/src/config/settings.rs | 8 - .../src/handlers/admin_handler.rs | 8 - .../src/handlers/order_handler.rs | 7 - .../src/handlers/user_handler.rs | 7 - .../onboard-large/src/middleware/logging.rs | 5 - .../onboard-large/src/models/order.rs | 9 - .../fixtures/onboard-large/src/models/user.rs | 8 - .../onboard-large/src/repo/order_repo.rs | 8 - .../onboard-large/src/repo/user_repo.rs | 7 - .../src/services/notification.rs | 5 - .../onboard-large/src/services/payment.rs | 5 - .../onboard-large/src/storage/migrations.rs | 7 - .../onboard-large/src/storage/postgres.rs | 7 - .../onboard-large/src/utils/errors.rs | 8 - .../tests/fixtures/onboard-sample/Cargo.toml | 8 - .../onboard-sample/src/api/handler.rs | 12 - .../onboard-sample/src/api/middleware.rs | 9 - .../fixtures/onboard-sample/src/api/routes.rs | 9 - .../fixtures/onboard-sample/src/error.rs | 13 - .../tests/fixtures/onboard-sample/src/main.rs | 7 - .../fixtures/onboard-sample/src/repo/mod.rs | 6 - .../onboard-sample/src/repo/orders.rs | 6 - .../fixtures/onboard-sample/src/repo/users.rs | 10 - .../onboard-sample/src/storage/cache.rs | 12 - product-cli/tests/fixtures/server.schema.json | 483 --- product-cli/tests/framework.rs | 1942 ----------- product-cli/tests/registry_generator.rs | 253 -- .../tests/registry_shacl_divergence.rs | 135 - product-cli/tests/registry_support/mod.rs | 165 - product-core/Cargo.toml | 40 - product-core/benches/graph_bench.rs | 258 -- product-core/src/author/domain.rs | 189 -- product-core/src/author/domain_prompt.md | 91 - product-core/src/author/mod.rs | 79 - product-core/src/author/workflow.rs | 339 -- product-core/src/author/workflow_prompt.md | 39 - product-core/src/config.rs | 259 -- product-core/src/config_sections.rs | 44 - product-core/src/demo.rs | 86 - product-core/src/error.rs | 395 --- product-core/src/fileops.rs | 312 -- product-core/src/ground/apply.rs | 140 - product-core/src/ground/axes.rs | 187 -- product-core/src/ground/batch.rs | 147 - product-core/src/ground/batch_tests.rs | 119 - product-core/src/ground/declared.rs | 207 -- product-core/src/ground/declared_tests.rs | 176 - product-core/src/ground/derivation.rs | 141 - product-core/src/ground/entail.rs | 322 -- product-core/src/ground/entail_tests.rs | 147 - product-core/src/ground/facts.rs | 192 -- product-core/src/ground/facts_tests.rs | 36 - product-core/src/ground/mint.rs | 221 -- product-core/src/ground/mod.rs | 53 - product-core/src/ground/plan.rs | 334 -- product-core/src/ground/plan_tests.rs | 249 -- product-core/src/ground/projection.rs | 143 - product-core/src/ground/projection_tests.rs | 222 -- product-core/src/ground/propose.rs | 154 - product-core/src/ground/report.rs | 222 -- product-core/src/ground/rows.rs | 273 -- product-core/src/ground/sidecar.rs | 281 -- product-core/src/ground/sidecar_tests.rs | 212 -- product-core/src/ground/structural.rs | 159 - product-core/src/ground/triple.rs | 163 - product-core/src/ground/vocab.rs | 123 - product-core/src/ground/write.rs | 165 - product-core/src/ground/write_tests.rs | 128 - product-core/src/guide/mod.rs | 183 - product-core/src/guide/plan.rs | 131 - product-core/src/guide/render.rs | 23 - product-core/src/guide/tests.rs | 87 - product-core/src/io/mod.rs | 3 - product-core/src/lib.rs | 17 - product-core/src/parse/mod.rs | 30 - product-core/src/pf/authoring_scope.rs | 158 - .../src/pf/authoring_scope_enforce.rs | 117 - product-core/src/pf/authoring_scope_join.rs | 75 - product-core/src/pf/authoring_scope_tests.rs | 207 -- product-core/src/pf/blueprint.rs | 197 -- product-core/src/pf/blueprint_tests.rs | 95 - product-core/src/pf/blueprint_turtle.rs | 54 - product-core/src/pf/build.rs | 96 - product-core/src/pf/build_metrics.rs | 122 - product-core/src/pf/build_metrics_tests.rs | 97 - product-core/src/pf/build_seam.rs | 270 -- product-core/src/pf/build_seam_convert.rs | 221 -- product-core/src/pf/build_spmc.rs | 98 - product-core/src/pf/build_spmc_tests.rs | 64 - product-core/src/pf/build_tests.rs | 31 - product-core/src/pf/bundle.rs | 253 -- product-core/src/pf/bundle_tests.rs | 59 - product-core/src/pf/capability.rs | 219 -- product-core/src/pf/capability_tests.rs | 69 - product-core/src/pf/cell.rs | 184 - product-core/src/pf/cell_validate.rs | 130 - product-core/src/pf/cell_validate_tests.rs | 90 - product-core/src/pf/codegen.rs | 379 --- product-core/src/pf/codegen_backend.rs | 219 -- product-core/src/pf/codegen_cell.rs | 145 - product-core/src/pf/codegen_decider.rs | 149 - product-core/src/pf/codegen_fixtures.rs | 215 -- product-core/src/pf/codegen_flow.rs | 212 -- product-core/src/pf/codegen_ident.rs | 126 - product-core/src/pf/codegen_infer.rs | 266 -- product-core/src/pf/codegen_kotlin.rs | 367 -- product-core/src/pf/codegen_kotlin_facts.rs | 235 -- product-core/src/pf/codegen_manifest.rs | 328 -- product-core/src/pf/codegen_openapi.rs | 140 - product-core/src/pf/codegen_oracle.rs | 310 -- product-core/src/pf/codegen_program.rs | 95 - product-core/src/pf/codegen_projector.rs | 241 -- product-core/src/pf/codegen_prov.rs | 46 - product-core/src/pf/codegen_runtime.rs | 210 -- product-core/src/pf/codegen_scenarios.rs | 143 - product-core/src/pf/codegen_screen.rs | 174 - product-core/src/pf/codegen_tests.rs | 398 --- product-core/src/pf/codegen_types.rs | 218 -- product-core/src/pf/codegen_web.rs | 191 -- product-core/src/pf/codegen_web_tests.rs | 103 - product-core/src/pf/data_check.rs | 200 -- product-core/src/pf/data_check_tests.rs | 112 - product-core/src/pf/decider.rs | 138 - product-core/src/pf/decider_cel.rs | 121 - product-core/src/pf/decider_conform.rs | 89 - product-core/src/pf/decider_conform_tests.rs | 82 - product-core/src/pf/decider_logic.rs | 191 -- product-core/src/pf/decider_sim.rs | 198 -- product-core/src/pf/decider_sim_tests.rs | 160 - product-core/src/pf/decider_tests.rs | 74 - product-core/src/pf/decider_turtle.rs | 55 - product-core/src/pf/deliverable.rs | 96 - product-core/src/pf/deliverable_tests.rs | 46 - product-core/src/pf/deployable_unit.rs | 162 - product-core/src/pf/deployable_unit_tests.rs | 81 - product-core/src/pf/dispatch.rs | 191 -- product-core/src/pf/dispatch_tests.rs | 140 - product-core/src/pf/done.rs | 184 - product-core/src/pf/done_tests.rs | 119 - product-core/src/pf/ds_store.rs | 169 - product-core/src/pf/edit.rs | 237 -- product-core/src/pf/edit_tests.rs | 81 - product-core/src/pf/feature.rs | 82 - product-core/src/pf/feature_tests.rs | 56 - product-core/src/pf/how.rs | 358 -- product-core/src/pf/how_edit.rs | 168 - product-core/src/pf/how_edit_tests.rs | 55 - product-core/src/pf/how_turtle.rs | 149 - product-core/src/pf/how_validate.rs | 129 - product-core/src/pf/how_validate_tests.rs | 111 - product-core/src/pf/ids.rs | 385 --- product-core/src/pf/kind_alias.rs | 31 - product-core/src/pf/layout.rs | 142 - product-core/src/pf/layout_check.rs | 123 - product-core/src/pf/layout_check_tests.rs | 70 - product-core/src/pf/layout_tests.rs | 58 - product-core/src/pf/lsp.rs | 269 -- product-core/src/pf/lsp_tests.rs | 91 - product-core/src/pf/manifest.rs | 193 -- product-core/src/pf/manifest_bundle.rs | 131 - product-core/src/pf/manifest_content.rs | 98 - product-core/src/pf/manifest_content_tests.rs | 53 - product-core/src/pf/manifest_tests.rs | 83 - product-core/src/pf/mod.rs | 127 - product-core/src/pf/model.rs | 297 -- product-core/src/pf/model_data.rs | 78 - product-core/src/pf/model_product.rs | 76 - product-core/src/pf/model_question.rs | 112 - product-core/src/pf/model_tests.rs | 17 - product-core/src/pf/model_ui.rs | 281 -- product-core/src/pf/ops.rs | 142 - product-core/src/pf/ops_tests.rs | 75 - product-core/src/pf/paths.rs | 153 - product-core/src/pf/primitive.rs | 102 - product-core/src/pf/primitive_tests.rs | 51 - product-core/src/pf/projector.rs | 149 - product-core/src/pf/projector_conform.rs | 70 - .../src/pf/projector_conform_tests.rs | 63 - product-core/src/pf/projector_logic.rs | 36 - product-core/src/pf/projector_sim.rs | 72 - product-core/src/pf/projector_sim_tests.rs | 68 - product-core/src/pf/projector_tests.rs | 51 - product-core/src/pf/provenance.rs | 49 - product-core/src/pf/query.rs | 179 - product-core/src/pf/question.rs | 184 - product-core/src/pf/question_gate.rs | 151 - product-core/src/pf/question_tests.rs | 157 - product-core/src/pf/question_turtle.rs | 73 - product-core/src/pf/questions.rs | 155 - product-core/src/pf/reify_ds.rs | 302 -- product-core/src/pf/reify_ds_tests.rs | 150 - product-core/src/pf/release.rs | 63 - product-core/src/pf/release_tests.rs | 32 - product-core/src/pf/render_contract.rs | 187 -- product-core/src/pf/render_contract_tests.rs | 54 - product-core/src/pf/rules_data.rs | 88 - product-core/src/pf/rules_data_tests.rs | 57 - product-core/src/pf/rules_decider.rs | 272 -- product-core/src/pf/rules_how.rs | 202 -- product-core/src/pf/rules_pattern.rs | 128 - product-core/src/pf/rules_pattern_tests.rs | 83 - product-core/src/pf/rules_reify.rs | 105 - product-core/src/pf/rules_reify_tests.rs | 65 - product-core/src/pf/rules_ui.rs | 308 -- product-core/src/pf/rules_what.rs | 147 - product-core/src/pf/run.rs | 84 - product-core/src/pf/schedule.rs | 60 - product-core/src/pf/schedule_tests.rs | 51 - product-core/src/pf/seam.rs | 108 - product-core/src/pf/seam_tests.rs | 74 - product-core/src/pf/seed.rs | 345 -- product-core/src/pf/seed_canon.rs | 125 - product-core/src/pf/seed_data.rs | 69 - product-core/src/pf/seed_tests.rs | 232 -- product-core/src/pf/seed_ui.rs | 222 -- product-core/src/pf/session.rs | 214 -- product-core/src/pf/sparql_dataset.rs | 89 - product-core/src/pf/sparql_rules.rs | 173 - product-core/src/pf/target.rs | 131 - product-core/src/pf/turtle.rs | 246 -- product-core/src/pf/turtle_data.rs | 54 - product-core/src/pf/turtle_product.rs | 69 - product-core/src/pf/turtle_ui.rs | 164 - product-core/src/pf/validate.rs | 363 -- product-core/src/pf/validate_product.rs | 211 -- product-core/src/pf/validate_tests.rs | 184 - product-core/src/pf/verify.rs | 55 - product-core/src/pf/verify_tests.rs | 51 - product-core/src/pf/viz.rs | 389 --- product-core/src/pf/viz_tests.rs | 112 - product-core/src/pf/wcag22.rs | 30 - product-core/src/pf/work_unit.rs | 120 - product-core/src/pf/work_unit_validate.rs | 113 - .../src/pf/work_unit_validate_tests.rs | 67 - product-core/src/pf/worker.rs | 262 -- product-core/src/pf/worker_tests.rs | 163 - product-core/src/pf/workflow.rs | 262 -- product-core/src/registry/apply.rs | 200 -- .../src/registry/check/check_tests.rs | 199 -- product-core/src/registry/check/eval.rs | 84 - product-core/src/registry/check/file_rule.rs | 39 - product-core/src/registry/check/mod.rs | 163 - product-core/src/registry/check/shapes.rs | 221 -- product-core/src/registry/mod.rs | 27 - product-core/src/registry/params.rs | 96 - product-core/src/registry/params_tests.rs | 66 - product-core/src/registry/plan.rs | 111 - product-core/src/registry/plan_tests.rs | 90 - product-core/src/registry/substitute.rs | 102 - product-core/src/registry/substitute_tests.rs | 69 - product-core/src/registry/template.rs | 69 - product-core/src/registry/template_tests.rs | 59 - product-core/src/registry/verify.rs | 178 - product-core/src/registry/verify_tests.rs | 109 - product-core/src/root.rs | 302 -- product-core/tests/graph_capability_tests.rs | 395 --- .../tests/property/fileops_properties.rs | 57 - .../init_properties.proptest-regressions | 7 - .../tests/property/init_properties.rs | 90 - product-core/tests/property/mod.rs | 5 - product-core/tests/property_tests.rs | 4 - product-mcp/Cargo.toml | 38 - product-mcp/src/assets/ui/AIOCatalogView.jsx | 103 - product-mcp/src/assets/ui/BuildViews.jsx | 192 -- product-mcp/src/assets/ui/ContentView.jsx | 116 - product-mcp/src/assets/ui/DataView.jsx | 161 - product-mcp/src/assets/ui/DecidersView.jsx | 168 - product-mcp/src/assets/ui/DomainGraph.jsx | 135 - product-mcp/src/assets/ui/FeaturesView.jsx | 411 --- product-mcp/src/assets/ui/FlowsTimeline.jsx | 142 - product-mcp/src/assets/ui/GraphView.jsx | 396 --- product-mcp/src/assets/ui/HowProcessView.jsx | 185 - product-mcp/src/assets/ui/HowViews.jsx | 595 ---- product-mcp/src/assets/ui/PageGraphView.jsx | 177 - product-mcp/src/assets/ui/ScenariosView.jsx | 111 - product-mcp/src/assets/ui/ScreenPreview.jsx | 441 --- product-mcp/src/assets/ui/SystemsMap.jsx | 272 -- product-mcp/src/assets/ui/UIStepsView.jsx | 231 -- product-mcp/src/assets/ui/VersionsView.jsx | 285 -- .../_adherence.oxlintrc.json | 427 --- .../_ds_bundle.js | 2320 ------------- .../_ds_manifest.json | 1 - .../readme.md | 194 -- .../styles.css | 12 - .../tokens/base.css | 36 - .../tokens/colors.css | 124 - .../tokens/effects.css | 54 - .../tokens/fonts.css | 16 - .../tokens/spacing.css | 29 - .../tokens/typography.css | 90 - product-mcp/src/assets/ui/app.jsx | 686 ---- .../src/assets/ui/assets/logo-mark.svg | 6 - .../assets/ui/assets/logo-wordmark-light.svg | 6 - product-mcp/src/assets/ui/data-graph.js | 144 - product-mcp/src/assets/ui/data-how.js | 416 --- product-mcp/src/assets/ui/data-ui.js | 343 -- product-mcp/src/assets/ui/data.js | 404 --- product-mcp/src/assets/ui/index.html | 74 - product-mcp/src/assets/ui/shared.jsx | 233 -- product-mcp/src/assets/ui/tweaks-panel.jsx | 541 --- product-mcp/src/assets/ui/vendor/babel.js | 4 - product-mcp/src/assets/ui/vendor/react-dom.js | 267 -- product-mcp/src/assets/ui/vendor/react.js | 31 - product-mcp/src/assets/view.html | 713 ---- product-mcp/src/build_handler.rs | 157 - product-mcp/src/codegen_handlers.rs | 214 -- product-mcp/src/decider_handlers.rs | 59 - product-mcp/src/decider_handlers_tests.rs | 41 - product-mcp/src/delivery_handlers.rs | 339 -- product-mcp/src/delivery_handlers_tests.rs | 67 - product-mcp/src/deployable_unit_handlers.rs | 100 - .../src/deployable_unit_handlers_tests.rs | 58 - product-mcp/src/design_system_handlers.rs | 117 - product-mcp/src/domain/args.rs | 33 - product-mcp/src/domain/handlers.rs | 157 - product-mcp/src/domain/mod.rs | 17 - product-mcp/src/domain/registry.rs | 127 - product-mcp/src/domain/registry_tests.rs | 100 - product-mcp/src/domain/session_handlers.rs | 79 - product-mcp/src/domain/stdio.rs | 42 - product-mcp/src/domain/tools.json | 274 -- product-mcp/src/domain/tools.rs | 60 - product-mcp/src/domain_handlers.rs | 177 - product-mcp/src/domain_handlers_tests.rs | 166 - product-mcp/src/framework_read_handlers.rs | 173 - .../src/framework_read_handlers_tests.rs | 73 - .../src/framework_scaffold_handlers.rs | 151 - .../src/framework_scaffold_handlers_tests.rs | 94 - product-mcp/src/framework_write_handlers.rs | 249 -- .../src/framework_write_handlers_tests.rs | 146 - product-mcp/src/http.rs | 170 - product-mcp/src/http_ui.rs | 94 - product-mcp/src/http_view.rs | 277 -- product-mcp/src/instructions.rs | 193 -- product-mcp/src/lib.rs | 129 - product-mcp/src/pf_mcp.rs | 89 - product-mcp/src/pf_view.rs | 357 -- product-mcp/src/pf_view/conformance.rs | 136 - product-mcp/src/pf_view/pf_build.rs | 36 - product-mcp/src/pf_view/pf_flows.rs | 174 - product-mcp/src/pf_view/pf_how.rs | 349 -- product-mcp/src/pf_view/pf_repo.rs | 146 - product-mcp/src/pf_view/pf_ui.rs | 127 - product-mcp/src/primitive_handlers.rs | 38 - product-mcp/src/primitive_handlers_tests.rs | 37 - product-mcp/src/product_handlers.rs | 106 - product-mcp/src/projector_handlers.rs | 59 - product-mcp/src/projector_handlers_tests.rs | 41 - product-mcp/src/question_handlers.rs | 162 - product-mcp/src/registry.rs | 378 --- product-mcp/src/scaffold.rs | 24 - product-mcp/src/scope_handlers.rs | 144 - product-mcp/src/stdio.rs | 68 - product-mcp/src/tests.rs | 73 - product-mcp/src/tools/build.rs | 64 - product-mcp/src/tools/codegen.rs | 75 - product-mcp/src/tools/decider.rs | 35 - product-mcp/src/tools/delivery.rs | 63 - product-mcp/src/tools/deployable_unit.rs | 47 - product-mcp/src/tools/design_system.rs | 93 - product-mcp/src/tools/domain.rs | 265 -- product-mcp/src/tools/how_author.rs | 90 - product-mcp/src/tools/legacy_pf.rs | 67 - product-mcp/src/tools/mod.rs | 61 - product-mcp/src/tools/primitive.rs | 23 - product-mcp/src/tools/product.rs | 28 - product-mcp/src/tools/projector.rs | 35 - product-mcp/src/tools/question.rs | 68 - product-mcp/src/tools/scope.rs | 51 - product-mcp/src/watch.rs | 49 - product-mcp/src/what_declare.rs | 92 - product-mcp/src/what_intercept.rs | 192 -- product-mcp/src/what_intercept_tests.rs | 193 -- product-mcp/src/workflow.rs | 338 -- product-mcp/src/workflow_questions.rs | 73 - product-mcp/src/workflow_tests.rs | 96 - rust-toolchain.toml | 9 - schema/README.md | 110 - schema/examples/delivery.example.yaml | 13 - schema/examples/how-contract.example.yaml | 88 - schema/examples/layout-model.example.yaml | 38 - .../task-type-definition.example.yaml | 73 - schema/examples/todo-product.ttl | 99 - schema/examples/work-unit.example.yaml | 17 - .../authoring-scope.schema.json | 65 - schema/json/build-seam/example.md | 112 - .../json/build-seam/verdictevent.schema.json | 56 - schema/json/build-seam/workunit.schema.json | 65 - schema/json/codegen/example.md | 134 - schema/json/codegen/file-plan.schema.json | 39 - schema/json/codegen/manifest.schema.json | 253 -- .../conformance/decision-request.schema.json | 72 - .../conformance/decision-response.schema.json | 49 - schema/json/conformance/example.md | 118 - .../projection-request.schema.json | 45 - .../projection-response.schema.json | 14 - schema/json/delivery.schema.json | 71 - schema/json/how-contract.schema.json | 147 - schema/json/layout-model.schema.json | 75 - schema/json/task-type-definition.schema.json | 91 - schema/json/work-unit.schema.json | 69 - schema/ontology/product-framework.ttl | 399 --- schema/shapes/how.shacl.ttl | 107 - schema/shapes/shapes.shacl.ttl | 167 - schema/validate.py | 92 - scripts/checks/file-length.sh | 55 - scripts/checks/function-length.sh | 73 - scripts/checks/module-structure.sh | 55 - scripts/checks/session-e2e.sh | 72 - scripts/checks/single-responsibility.sh | 43 - scripts/checks/spec-flow-e2e.sh | 144 - scripts/dispatch-to-spark.sh | 73 - scripts/generate-docs.sh | 116 - scripts/harness/author.sh | 22 - scripts/harness/implement.sh | 36 - scripts/implement-all.sh | 89 - scripts/showcase-acme-how.sh | 184 - scripts/showcase-acme.sh | 88 - spec-cli/Cargo.toml | 40 - spec-cli/src/commands.rs | 101 - spec-cli/src/exit.rs | 14 - spec-cli/src/main.rs | 33 - spec-cli/src/render.rs | 67 - spec-cli/src/verbs.rs | 51 - spec-cli/src/verbs/acts.rs | 61 - spec-cli/src/verbs/gate.rs | 143 - spec-cli/src/verbs/git.rs | 23 - spec-cli/src/verbs/host.rs | 217 -- spec-cli/src/verbs/host_tests.rs | 67 - spec-cli/src/verbs/policy.rs | 166 - spec-cli/src/verbs/ratify.rs | 220 -- spec-cli/src/verbs/record.rs | 159 - spec-cli/src/verbs/runs.rs | 219 -- spec-cli/src/verbs/trust.rs | 176 - spec-cli/src/verbs_tests.rs | 59 - spec-cli/tests/boundaries.rs | 173 - spec-cli/tests/flow.rs | 150 - spec-cli/tests/launcher.rs | 162 - spec-cli/tests/policy.rs | 179 - spec-cli/tests/ratify.rs | 198 -- spec-cli/tests/runs.rs | 144 - spec-cli/tests/signing.rs | 222 -- spec-core/Cargo.toml | 33 - spec-core/src/act.rs | 119 - spec-core/src/act_tests.rs | 78 - spec-core/src/check.rs | 161 - spec-core/src/check_tests.rs | 87 - spec-core/src/closure.rs | 72 - spec-core/src/closure_tests.rs | 35 - spec-core/src/digest.rs | 76 - spec-core/src/digest_tests.rs | 52 - spec-core/src/gate.rs | 364 -- spec-core/src/gate_signing_tests.rs | 158 - spec-core/src/gate_tests.rs | 208 -- spec-core/src/inventory.rs | 135 - spec-core/src/inventory_tests.rs | 100 - spec-core/src/lib.rs | 28 - spec-core/src/map.rs | 131 - spec-core/src/map_tests.rs | 75 - spec-core/src/metrics.rs | 107 - spec-core/src/metrics_tests.rs | 48 - spec-core/src/policy.rs | 229 -- spec-core/src/policy_check.rs | 228 -- spec-core/src/policy_check_tests.rs | 151 - spec-core/src/policy_tests.rs | 98 - spec-core/src/ratify.rs | 213 -- spec-core/src/ratify_tests.rs | 104 - spec-core/src/record.rs | 88 - spec-core/src/record_tests.rs | 59 - spec-core/src/signing.rs | 164 - spec-core/src/signing_tests.rs | 89 - spec-core/src/store.rs | 167 - spec-core/src/store_tests.rs | 155 - spec-flow/.gitignore | 2 - spec-flow/Directory.Build.props | 11 - spec-flow/Directory.Packages.props | 19 - spec-flow/README.md | 305 -- spec-flow/SpecFlow.slnx | 12 - spec-flow/src/SpecFlow.Cli/ActGround.cs | 83 - spec-flow/src/SpecFlow.Cli/ExitCodes.cs | 12 - .../src/SpecFlow.Cli/ImplementCommand.cs | 227 -- spec-flow/src/SpecFlow.Cli/ImportCommand.cs | 37 - spec-flow/src/SpecFlow.Cli/JudgeCommand.cs | 122 - spec-flow/src/SpecFlow.Cli/McpCommand.cs | 15 - spec-flow/src/SpecFlow.Cli/Options.cs | 63 - spec-flow/src/SpecFlow.Cli/Program.cs | 46 - .../src/SpecFlow.Cli/SpecFlow.Cli.csproj | 16 - .../src/SpecFlow.Flow/AgentSliceBuilder.cs | 249 -- spec-flow/src/SpecFlow.Flow/Executors.cs | 107 - .../src/SpecFlow.Flow/ImplementDriver.cs | 61 - .../src/SpecFlow.Flow/ImplementWorkflow.cs | 56 - spec-flow/src/SpecFlow.Flow/Messages.cs | 60 - spec-flow/src/SpecFlow.Flow/SpecCli.cs | 162 - .../src/SpecFlow.Flow/SpecFlow.Flow.csproj | 12 - .../src/SpecFlow.Import/AttributeScan.cs | 113 - .../src/SpecFlow.Import/CandidateBuild.cs | 56 - .../src/SpecFlow.Import/EntryPointScan.cs | 187 -- spec-flow/src/SpecFlow.Import/Importer.cs | 105 - spec-flow/src/SpecFlow.Import/Inventory.cs | 132 - .../SpecFlow.Import/SpecFlow.Import.csproj | 8 - spec-flow/src/SpecFlow.Import/SymbolScan.cs | 158 - spec-flow/src/SpecFlow.Mcp/GovernedTools.cs | 124 - spec-flow/src/SpecFlow.Mcp/Server.cs | 47 - .../src/SpecFlow.Mcp/SpecFlow.Mcp.csproj | 13 - spec-flow/src/SpecFlow.Mcp/SpecFlowOptions.cs | 23 - spec-flow/src/SpecFlow.Mcp/SpecFlowTools.cs | 171 - .../SpecFlow.Flow.Tests/BoundaryTests.cs | 63 - .../SpecFlow.Flow.Tests/DraftParsingTests.cs | 84 - .../tests/SpecFlow.Flow.Tests/GlobalUsings.cs | 1 - .../ImplementWorkflowTests.cs | 98 - .../SpecFlow.Flow.Tests/ImportFixture.cs | 116 - .../SpecFlow.Flow.Tests/ImporterTests.cs | 190 -- .../tests/SpecFlow.Flow.Tests/McpHarness.cs | 80 - .../SpecFlow.Flow.Tests/McpSurfaceTests.cs | 314 -- .../SpecFlow.Flow.Tests.csproj | 17 - .../tests/SpecFlow.Flow.Tests/SpecRepo.cs | 82 - spec-mcp/Cargo.toml | 35 - spec-mcp/src/bin/spec-mcp.rs | 13 - spec-mcp/src/dispatch.rs | 203 -- spec-mcp/src/dispatch_tests.rs | 126 - spec-mcp/src/lib.rs | 19 - spec-mcp/src/serve.rs | 19 - spec-mcp/src/tools.rs | 109 - spec-mcp/src/tools_tests.rs | 43 - spec/how-contract.example.yaml | 88 - spec/how-contract.schema.json | 147 - spec/how-schema/README.md | 110 - .../how-schema/examples/delivery.example.yaml | 13 - .../examples/how-contract.example.yaml | 88 - .../examples/layout-model.example.yaml | 38 - .../task-type-definition.example.yaml | 69 - spec/how-schema/examples/todo-product.ttl | 99 - .../examples/work-unit.example.yaml | 17 - spec/how-schema/json/delivery.schema.json | 71 - spec/how-schema/json/how-contract.schema.json | 147 - spec/how-schema/json/layout-model.schema.json | 75 - .../json/task-type-definition.schema.json | 90 - spec/how-schema/json/work-unit.schema.json | 69 - .../how-schema/ontology/product-framework.ttl | 307 -- spec/how-schema/shapes/how.shacl.ttl | 107 - spec/how-schema/shapes/shapes.shacl.ttl | 126 - .../tooling/product-author-domain.md | 186 - .../tooling/product-author-domain.tools.json | 274 -- spec/how-schema/validate.py | 92 - spec/how.shacl.ttl | 107 - spec/product-author-domain.md | 186 - spec/product-author-domain.tools.json | 274 -- spec/schema/README.md | 105 - spec/schema/examples/delivery.example.yaml | 13 - .../schema/examples/layout-model.example.yaml | 38 - .../task-type-definition.example.yaml | 69 - spec/schema/examples/todo-product.ttl | 99 - spec/schema/examples/work-unit.example.yaml | 17 - spec/schema/json/delivery.schema.json | 71 - spec/schema/json/layout-model.schema.json | 75 - .../json/task-type-definition.schema.json | 90 - spec/schema/json/work-unit.schema.json | 69 - spec/schema/ontology/product-framework.ttl | 307 -- spec/schema/shapes/shapes.shacl.ttl | 163 - spec/schema/tooling/product-author-domain.md | 186 - .../tooling/product-author-domain.tools.json | 274 -- spec/schema/validate.py | 82 - xtask/Cargo.toml | 26 - xtask/src/check_id.rs | 107 - xtask/src/checks/ctx001_file_length.rs | 99 - .../checks/ctx004_single_responsibility.rs | 130 - xtask/src/checks/ctx005_function_length.rs | 148 - xtask/src/checks/mod.rs | 55 - xtask/src/conventions.rs | 54 - xtask/src/diagnostic.rs | 151 - xtask/src/drift.rs | 198 -- xtask/src/lib.rs | 16 - xtask/src/main.rs | 94 - 1937 files changed, 211347 deletions(-) delete mode 100644 .cargo/config.toml delete mode 100644 .claude/skills/feature-e2e/SKILL.md delete mode 100644 .claude/skills/product-build/SKILL.md delete mode 100644 .claude/skills/product-how/SKILL.md delete mode 100644 .claude/skills/product-session-e2e/SKILL.md delete mode 100644 .claude/skills/product-session/SKILL.md delete mode 100644 .claude/skills/product-what/SKILL.md delete mode 100644 .claude/skills/release/SKILL.md delete mode 100644 .ddd/claims/.gitkeep delete mode 100644 .ddd/claims/DDD-adapter-01.yaml delete mode 100644 .ddd/claims/DDD-adapter-02.yaml delete mode 100644 .ddd/claims/DDD-adapter-03.yaml delete mode 100644 .ddd/claims/DDD-adapter-04.yaml delete mode 100644 .ddd/claims/DDD-adapter-05.yaml delete mode 100644 .ddd/claims/DDD-arch-01.yaml delete mode 100644 .ddd/claims/DDD-arch-02.yaml delete mode 100644 .ddd/claims/DDD-arch-03.yaml delete mode 100644 .ddd/claims/DDD-arch-04.yaml delete mode 100644 .ddd/claims/DDD-arch-05.yaml delete mode 100644 .ddd/claims/DDD-arch-06.yaml delete mode 100644 .ddd/claims/DDD-arch-07.yaml delete mode 100644 .ddd/claims/DDD-arch-08.yaml delete mode 100644 .ddd/claims/DDD-arch-09.yaml delete mode 100644 .ddd/claims/DDD-bicep-def-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-diag-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-exist-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-idem-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-net-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-rbac-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-sec-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-sec-02.yaml delete mode 100644 .ddd/claims/DDD-bicep-sku-01.yaml delete mode 100644 .ddd/claims/DDD-bicep-ver-01.yaml delete mode 100644 .ddd/claims/DDD-cs-authz-01.yaml delete mode 100644 .ddd/claims/DDD-cs-conc-01.yaml delete mode 100644 .ddd/claims/DDD-cs-conc-02.yaml delete mode 100644 .ddd/claims/DDD-cs-disp-01.yaml delete mode 100644 .ddd/claims/DDD-cs-disp-02.yaml delete mode 100644 .ddd/claims/DDD-cs-disp-03.yaml delete mode 100644 .ddd/claims/DDD-cs-disp-04.yaml delete mode 100644 .ddd/claims/DDD-cs-disp-05.yaml delete mode 100644 .ddd/claims/DDD-cs-disp-06.yaml delete mode 100644 .ddd/claims/DDD-cs-ef-01.yaml delete mode 100644 .ddd/claims/DDD-cs-fail-01.yaml delete mode 100644 .ddd/claims/DDD-cs-map-01.yaml delete mode 100644 .ddd/claims/DDD-cs-map-02.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-01.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-02.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-03.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-04.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-05.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-06.yaml delete mode 100644 .ddd/claims/DDD-cs-nrt-07.yaml delete mode 100644 .ddd/claims/DDD-cs-obs-01.yaml delete mode 100644 .ddd/claims/DDD-cs-ord-01.yaml delete mode 100644 .ddd/claims/DDD-cs-pol-01.yaml delete mode 100644 .ddd/claims/DDD-cs-pol-02.yaml delete mode 100644 .ddd/claims/DDD-cs-pol-03.yaml delete mode 100644 .ddd/claims/DDD-cs-pol-04.yaml delete mode 100644 .ddd/claims/DDD-cs-proc-01.yaml delete mode 100644 .ddd/claims/DDD-cs-ser-01.yaml delete mode 100644 .ddd/claims/DDD-cs-tc-01.yaml delete mode 100644 .ddd/claims/DDD-cs-tc-02.yaml delete mode 100644 .ddd/claims/DDD-cs-tc-03.yaml delete mode 100644 .ddd/claims/DDD-cs-tc-04.yaml delete mode 100644 .ddd/claims/DDD-cs-tc-05.yaml delete mode 100644 .ddd/claims/DDD-cs-tx-01.yaml delete mode 100644 .ddd/claims/DDD-detect-01.yaml delete mode 100644 .ddd/claims/DDD-detect-02.yaml delete mode 100644 .ddd/claims/DDD-friction-01.yaml delete mode 100644 .ddd/claims/DDD-friction-02.yaml delete mode 100644 .ddd/claims/DDD-gates-01.yaml delete mode 100644 .ddd/claims/DDD-gates-02.yaml delete mode 100644 .ddd/claims/DDD-method-01.yaml delete mode 100644 .ddd/claims/DDD-method-02.yaml delete mode 100644 .ddd/claims/DDD-method-03.yaml delete mode 100644 .ddd/claims/DDD-method-04.yaml delete mode 100644 .ddd/claims/DDD-method-05.yaml delete mode 100644 .ddd/claims/DDD-method-06.yaml delete mode 100644 .ddd/claims/DDD-method-07.yaml delete mode 100644 .ddd/claims/DDD-scope-01.yaml delete mode 100644 .ddd/claims/DDD-web-01.yaml delete mode 100644 .ddd/claims/DDD-web-02.yaml delete mode 100644 .ddd/claims/DDD-web-visual-01.yaml delete mode 100644 .ddd/claims/DDD-what-01.yaml delete mode 100644 .ddd/claims/DDD-what-02.yaml delete mode 100644 .ddd/claims/DDD-what-03.yaml delete mode 100644 .ddd/claims/DDD-what-04.yaml delete mode 100644 .ddd/concordance.yaml delete mode 100644 .ddd/config.yaml delete mode 100644 .ddd/decisions/.gitkeep delete mode 100644 .ddd/decisions/adapter-policy-tables.yaml delete mode 100644 .ddd/decisions/amend-explicit-evidence-frozen.yaml delete mode 100644 .ddd/decisions/bicep-linter-at-error.yaml delete mode 100644 .ddd/decisions/bicep-psrule-in-pr-stage.yaml delete mode 100644 .ddd/decisions/ci-gates-report-skipped.yaml delete mode 100644 .ddd/decisions/correspondence-rows-are-stratified.yaml delete mode 100644 .ddd/decisions/cs-async-policy-rules-at-error.yaml delete mode 100644 .ddd/decisions/cs-authorization-fallback-deny.yaml delete mode 100644 .ddd/decisions/cs-disposal-rules-at-error.yaml delete mode 100644 .ddd/decisions/cs-general-catch-at-error.yaml delete mode 100644 .ddd/decisions/cs-policy-rules-at-error.yaml delete mode 100644 .ddd/decisions/cs-startup-assertions-required.yaml delete mode 100644 .ddd/decisions/cs-unsafe-reinterpretation-banned.yaml delete mode 100644 .ddd/decisions/curation-over-mining.yaml delete mode 100644 .ddd/decisions/enforce-matching-tightens-to-symbol.yaml delete mode 100644 .ddd/decisions/enum-member-gap-priced.yaml delete mode 100644 .ddd/decisions/fixtures-not-sdk.yaml delete mode 100644 .ddd/decisions/git-is-the-amend-trail.yaml delete mode 100644 .ddd/decisions/interceptor-not-extension.yaml delete mode 100644 .ddd/decisions/internal-not-surface.yaml delete mode 100644 .ddd/decisions/lsp-as-seam.yaml delete mode 100644 .ddd/decisions/m6-proceeds-no-flip.yaml delete mode 100644 .ddd/decisions/m8-enforcement-closure.yaml delete mode 100644 .ddd/decisions/no-unwrap-gate.yaml delete mode 100644 .ddd/decisions/pins-at-m2.yaml delete mode 100644 .ddd/decisions/prd-split.yaml delete mode 100644 .ddd/decisions/predicates-carry-no-status.yaml delete mode 100644 .ddd/decisions/rejection-facts-prefilled.yaml delete mode 100644 .ddd/decisions/rejection-payload-basis.yaml delete mode 100644 .ddd/decisions/report-coverage-explicit.yaml delete mode 100644 .ddd/decisions/rust-class-enforced-here.yaml delete mode 100644 .ddd/decisions/rust-host-is-real.yaml delete mode 100644 .ddd/decisions/sarif-unification.yaml delete mode 100644 .ddd/decisions/seed-lands-in-claims-not-shared.yaml delete mode 100644 .ddd/decisions/typed-basis.yaml delete mode 100644 .ddd/decisions/undeclared-what-boundaries.yaml delete mode 100644 .ddd/decisions/v1-scope.yaml delete mode 100644 .ddd/decisions/web-htmlcss-enforce.yaml delete mode 100644 .ddd/decisions/web-markup-validity.yaml delete mode 100644 .ddd/decisions/web-plain-pair-scope.yaml delete mode 100644 .ddd/decisions/web-render-status-palette.yaml delete mode 100644 .ddd/decisions/web-token-discipline.yaml delete mode 100644 .ddd/decisions/what-boundaries-priced-not-paid.yaml delete mode 100644 .ddd/decisions/what-policy-table.yaml delete mode 100644 .ddd/decisions/what-published-qualifier.yaml delete mode 100644 .ddd/decisions/why-resolves-three-ways.yaml delete mode 100644 .ddd/decisions/workspace-member-delivery.yaml delete mode 100644 .ddd/manifest/analyzers.yaml delete mode 100644 .ddd/manifest/clippy.yaml delete mode 100644 .ddd/manifest/htmlvalidate.yaml delete mode 100644 .ddd/manifest/linter.yaml delete mode 100644 .ddd/manifest/stylelint.yaml delete mode 100644 .ddd/manifest/tokens.yaml delete mode 100644 .ddd/patterns/.gitkeep delete mode 100644 .ddd/predicates/.gitkeep delete mode 100644 .ddd/predicates/api-version-currency.yaml delete mode 100644 .ddd/predicates/authorization-scope-declared.yaml delete mode 100644 .ddd/predicates/composition-decorator.yaml delete mode 100644 .ddd/predicates/convention-mapping.yaml delete mode 100644 .ddd/predicates/deployment-idempotence.yaml delete mode 100644 .ddd/predicates/deployment-secret-containment.yaml delete mode 100644 .ddd/predicates/deterministic-disposal.yaml delete mode 100644 .ddd/predicates/deterministic-sequence-order.yaml delete mode 100644 .ddd/predicates/diagnostic-retention-declared.yaml delete mode 100644 .ddd/predicates/endpoint-authorization-explicit.yaml delete mode 100644 .ddd/predicates/explicit-policy-argument.yaml delete mode 100644 .ddd/predicates/external-dependency-declared.yaml delete mode 100644 .ddd/predicates/failure-behaviour-declared.yaml delete mode 100644 .ddd/predicates/failure-policy-declared.yaml delete mode 100644 .ddd/predicates/governance-resolvability.yaml delete mode 100644 .ddd/predicates/network-exposure-declared.yaml delete mode 100644 .ddd/predicates/null-dereference-safety.yaml delete mode 100644 .ddd/predicates/observable-completion.yaml delete mode 100644 .ddd/predicates/parameter-default-governed.yaml delete mode 100644 .ddd/predicates/persistence-defaults-declared.yaml delete mode 100644 .ddd/predicates/resource-capacity-governed.yaml delete mode 100644 .ddd/predicates/serialisation-contract-declared.yaml delete mode 100644 .ddd/predicates/static-type-conformance.yaml delete mode 100644 .ddd/predicates/transaction-boundary-declared.yaml delete mode 100644 .ddd/render.html delete mode 100644 .ddd/seams/.gitkeep delete mode 100644 .ddd/seams/events/0001-default-command.yaml delete mode 100644 .ddd/seams/events/0002-host-command.yaml delete mode 100644 .ddd/seams/events/0003-default-command.yaml delete mode 100644 .ddd/seams/events/0004-host-command.yaml delete mode 100644 .ddd/seams/events/0005-readysignal.yaml delete mode 100644 .ddd/seams/events/0006-readysignal.yaml delete mode 100644 .ddd/seams/events/0007-raw-str.yaml delete mode 100644 .ddd/seams/events/0008-raw-str.yaml delete mode 100644 .ddd/seams/events/0009-bad.yaml delete mode 100644 .ddd/seams/events/0010-card.yaml delete mode 100644 .ddd/seams/events/0011-chip.yaml delete mode 100644 .ddd/seams/events/0012-established.yaml delete mode 100644 .ddd/seams/events/0013-muted.yaml delete mode 100644 .ddd/seams/events/0014-ok.yaml delete mode 100644 .ddd/seams/events/0015-projected.yaml delete mode 100644 .ddd/seams/events/0016-reported.yaml delete mode 100644 .ddd/seams/events/0017-retired.yaml delete mode 100644 .ddd/seams/events/0018-warn.yaml delete mode 100644 .ddd/seams/events/0019-bad.yaml delete mode 100644 .ddd/seams/events/0020-card.yaml delete mode 100644 .ddd/seams/events/0021-chip.yaml delete mode 100644 .ddd/seams/events/0022-established.yaml delete mode 100644 .ddd/seams/events/0023-muted.yaml delete mode 100644 .ddd/seams/events/0024-ok.yaml delete mode 100644 .ddd/seams/events/0025-projected.yaml delete mode 100644 .ddd/seams/events/0026-reported.yaml delete mode 100644 .ddd/seams/events/0027-retired.yaml delete mode 100644 .ddd/seams/events/0028-warn.yaml delete mode 100644 .ddd/seams/events/0029---ink.yaml delete mode 100644 .ddd/seams/events/0030---ink-faint.yaml delete mode 100644 .ddd/seams/events/0031---ink-muted.yaml delete mode 100644 .ddd/seams/events/0032---on-chip.yaml delete mode 100644 .ddd/seams/events/0033---status-bad.yaml delete mode 100644 .ddd/seams/events/0034---status-ok.yaml delete mode 100644 .ddd/seams/events/0035---status-warn.yaml delete mode 100644 .ddd/seams/events/0036---ink.yaml delete mode 100644 .ddd/seams/events/0037---ink-faint.yaml delete mode 100644 .ddd/seams/events/0038---ink-muted.yaml delete mode 100644 .ddd/seams/events/0039---on-chip.yaml delete mode 100644 .ddd/seams/events/0040---status-bad.yaml delete mode 100644 .ddd/seams/events/0041---status-ok.yaml delete mode 100644 .ddd/seams/events/0042---status-warn.yaml delete mode 100644 .ddd/seams/events/0043---surface.yaml delete mode 100644 .ddd/seams/events/0044---surface-raised.yaml delete mode 100644 .ddd/seams/events/0045---surface.yaml delete mode 100644 .ddd/seams/events/0046---surface-raised.yaml delete mode 100644 .ddd/seams/seam-ledger-acceptor-identity.yaml delete mode 100644 .ddd/seams/seam-ledger-blame-attribution.yaml delete mode 100644 .ddd/seams/seam-ledger-canonical-form.yaml delete mode 100644 .ddd/seams/seam-ledger-disposition-states.yaml delete mode 100644 .ddd/seams/seam-ledger-graph-stage.yaml delete mode 100644 .ddd/seams/seam-ledger-tolerance-floor.yaml delete mode 100644 .ddd/seams/seam-ledger-ulid-mint.yaml delete mode 100644 .ddd/seams/seam-ledger-verify-classes.yaml delete mode 100644 .ddd/seams/seam-ledger-version-hash.yaml delete mode 100644 .ddd/seams/seam-ledger-write-refusal.yaml delete mode 100644 .ddd/seams/seam-mcp-raw-str.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-bind-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-content-hash-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-diff-contracts-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-diff-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-migrate-plan-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-migrate-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-report-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-cli-src-commands-validate-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-basis-pin-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-binding-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-concordance-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-contracts-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-decision-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-gitrev-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-ledger-gate-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-lib-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-render-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-report-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-revisit-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-seam-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-core-src-validate-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-capability-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-declaration-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-host-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-lib-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-mock-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-protocol-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-lsp-src-revdiff-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-mcp-src-bind-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-mcp-src-lib-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ddd-mcp-src-state-rs.yaml delete mode 100644 .ddd/seams/seam-rust-default-command.yaml delete mode 100644 .ddd/seams/seam-rust-ground-cli-src-extract-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ground-cli-src-read-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ground-cli-src-symbols-rs.yaml delete mode 100644 .ddd/seams/seam-rust-host-command.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-add-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-authority-enum-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-authority-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-basis-text-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-cli-enum-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-common-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-evolve-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-export-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-inbox-accept-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-inbox-findings-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-inbox-render-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-inbox-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-init-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-inspect-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-sign-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-terminal-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-src-commands-verify-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-tests-common-export-only-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-tests-common-hand-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-tests-common-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-cli-tests-inbox-fixture-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-acceptance-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-authority-ops-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-availability-ops-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-batch-accept-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-decision-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-genesis-key-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-identity-ops-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-policy-ops-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-sign-ops-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-author-version-ops-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-availability-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-binding-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-check-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-choice-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-filing-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-fixture-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-grant-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-key-close-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-payload-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-policy-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-references-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-revocation-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-role-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-signers-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-structure-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-authority-view-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-batch-file-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-batch-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-canon-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-changeset-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-discharge-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-finding-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-format-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-graph-export-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-graph-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-graph-ntriples-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-graph-shapes-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-hash-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-id-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-inbox-config-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-inbox-git-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-inbox-index-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-inbox-list-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-inbox-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-inbox-terms-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-key-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-landed-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-landing-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-lib-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-revision-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-revisit-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-show-render-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-show-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-signing-check-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-signing-dsse-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-signing-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-signing-review-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-signing-ssh-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-signing-subject-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-store-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-testkit-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-verify-acts-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-verify-authority-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-verify-history-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-verify-keys-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-verify-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-ledger-core-src-version-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-cli-src-commands-how-fields-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-cli-src-commands-question-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-cli-src-commands-registry-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-cli-src-commands-root-enum-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-apply-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-axes-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-batch-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-declared-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-derivation-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-entail-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-facts-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-mint-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-plan-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-projection-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-propose-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-report-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-rows-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-sidecar-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-structural-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-triple-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-vocab-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-ground-write-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-lib-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-how-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-ids-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-model-question-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-model-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-question-gate-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-question-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-pf-sparql-dataset-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-apply-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-check-mod-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-params-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-plan-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-substitute-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-template-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-core-src-registry-verify-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-mcp-src-question-handlers-rs.yaml delete mode 100644 .ddd/seams/seam-rust-product-mcp-src-workflow-questions-rs.yaml delete mode 100644 .ddd/seams/seam-rust-readysignal-debug.yaml delete mode 100644 .ddd/seams/seam-web-render-bad.yaml delete mode 100644 .ddd/seams/seam-web-render-card.yaml delete mode 100644 .ddd/seams/seam-web-render-chip.yaml delete mode 100644 .ddd/seams/seam-web-render-established.yaml delete mode 100644 .ddd/seams/seam-web-render-muted.yaml delete mode 100644 .ddd/seams/seam-web-render-ok.yaml delete mode 100644 .ddd/seams/seam-web-render-projected.yaml delete mode 100644 .ddd/seams/seam-web-render-reported.yaml delete mode 100644 .ddd/seams/seam-web-render-retired.yaml delete mode 100644 .ddd/seams/seam-web-render-warn.yaml delete mode 100644 .ddd/seams/seam-web-token-ink-faint.yaml delete mode 100644 .ddd/seams/seam-web-token-ink-muted.yaml delete mode 100644 .ddd/seams/seam-web-token-ink.yaml delete mode 100644 .ddd/seams/seam-web-token-on-chip.yaml delete mode 100644 .ddd/seams/seam-web-token-status-bad.yaml delete mode 100644 .ddd/seams/seam-web-token-status-ok.yaml delete mode 100644 .ddd/seams/seam-web-token-status-warn.yaml delete mode 100644 .ddd/seams/seam-web-token-surface-raised.yaml delete mode 100644 .ddd/seams/seam-web-token-surface.yaml delete mode 100644 .ddd/shared/.gitkeep delete mode 100644 .decisions/log/01KZNF77DG2RJPY7CA63HYTRJ1.yml delete mode 100644 .decisions/log/01KZPMKQ18D1G3FXS0V0WT1F04.yml delete mode 100644 .decisions/log/01KZPMKQD0BEDBD3HWCT4CJHA4.yml delete mode 100644 .decisions/log/01KZPMKQE044SNFJSKFCTD8QJE.yml delete mode 100644 .decisions/log/01KZPMKQF4BQA4HH2P4QEEYN8V.yml delete mode 100644 .decisions/log/01KZPMKQGDB1MQC1HWY3HRQQ3G.yml delete mode 100644 .decisions/log/01KZPMKQHRFFM2ERBBKZYR1EH2.yml delete mode 100644 .decisions/log/01KZQT2D30HSX412BGC0SFKZKG.yml delete mode 100644 .decisions/log/01KZQT2D483RSCKQYVT8X0P58S.yml delete mode 100644 .decisions/log/01KZQT2D58N6NKVE99GP8C2D7N.yml delete mode 100644 .decisions/log/01KZQT2D6ENP0YA2PSSB4NZX8S.yml delete mode 100644 .decisions/log/01KZQT2D7DYYZ6AHMR1NDXHNHT.yml delete mode 100644 .decisions/log/01KZQT2D8JE3GH0A9S4SQ6S3Y0.yml delete mode 100644 .decisions/log/01KZQT2D9MX8PYZGVMC13TZVDD.yml delete mode 100644 .decisions/log/01KZQT2DAWHXRP35YF345DEYS2.yml delete mode 100644 .decisions/log/01KZQT2DC2G2BRDAGV9RFBG4TV.yml delete mode 100644 .decisions/log/01KZR5P5MN3ZW61HP7ES1CXYFH.yml delete mode 100644 .decisions/log/01KZR5PDTKFK9CNYJZRZV8BETY.yml delete mode 100644 .decisions/log/01KZRA0SBDAF5JQBHC465SAVPX.yml delete mode 100644 .decisions/log/01KZRA0XNDEH1EHQ0MTY60TFB2.yml delete mode 100644 .decisions/log/01KZRBQ3YWP1VJ67E7QRCC7YYD.yml delete mode 100644 .decisions/log/01KZRDFYPZTP1F2S1424N4FM7F.yml delete mode 100644 .decisions/log/01KZTGGGS1TF32NE5ZREW3A55B.yml delete mode 100644 .decisions/log/01KZTGGGXG6CVNYHVZWD4VSXXN.yml delete mode 100644 .decisions/log/01KZTGGH25SFKCGSFCXJMTM7VC.yml delete mode 100644 .decisions/log/01KZTGGH76Z3C69CJ6CBBXXZCZ.yml delete mode 100644 .decisions/log/01KZTGGHCB2AY2GQ6B1JP50VH0.yml delete mode 100644 .decisions/log/01KZTGGHHKFXC6APZ4GF0B20PE.yml delete mode 100644 .decisions/log/01KZTGGHQ69HQR5N9TH579SWDM.yml delete mode 100644 .decisions/log/01KZTGGHXCR4GMGPR87TJ7WNG9.yml delete mode 100644 .decisions/log/01KZTGGJ34CHP029QZYF66YGXT.yml delete mode 100644 .decisions/log/01KZTGGJ92TNGQSEVYW7ZK4Z11.yml delete mode 100644 .decisions/log/01KZTGGJFED3R42CBXMT61QNEJ.yml delete mode 100644 .decisions/log/01KZTGGJNQRC2V0Q4PYWZ5SFT0.yml delete mode 100644 .decisions/log/01KZTGGJW5558M2Q2S9963F5SG.yml delete mode 100644 .decisions/log/01KZTGGK2R76MG2SJY754BJ925.yml delete mode 100644 .decisions/log/01KZTGGK9FH0FGZR7EE8HN4PS7.yml delete mode 100644 .decisions/log/01KZTGGKGBCDMFK30X1K9FNZYA.yml delete mode 100644 .decisions/log/01KZTGGKQDBWWQTNVC3XY937GD.yml delete mode 100644 .decisions/log/01KZTGGKYNQKDYDBHQX2KW6KDS.yml delete mode 100644 .decisions/log/01KZTGGM6BQ13WM22EBNYBPFEY.yml delete mode 100644 .decisions/log/01KZTGGMEAJTX88HKBWPVYRQV9.yml delete mode 100644 .decisions/log/01KZTGGMPHVMNZ6GGV8YX1N9EW.yml delete mode 100644 .decisions/log/01KZTGGMYT8C43AZ5EXB6BZGY5.yml delete mode 100644 .decisions/log/01KZTGGN7C2W9GMC2GYWY7NCPD.yml delete mode 100644 .decisions/log/01KZTGGNGD5AYC7DPKGMSQ59EH.yml delete mode 100644 .decisions/log/01KZTGGNSHJMZP78SAVPB09YBM.yml delete mode 100644 .decisions/log/01KZTGGP8RB22MZ4ER7P7HX1N5.yml delete mode 100644 .decisions/log/01KZTGGPHWM9SS6J446MGJKZWA.yml delete mode 100644 .decisions/log/01KZTGGPV46ABRWGGQJYXEMF5R.yml delete mode 100644 .decisions/log/01KZTGGQ4HT8J1V3A5RQ8HFBDC.yml delete mode 100644 .decisions/log/01KZTGGQFE31C777RCBP4MVH5C.yml delete mode 100644 .decisions/log/01KZTGGQS9G5FE0YATDHNC768B.yml delete mode 100644 .decisions/log/01KZTGGR3DDYKDZYQ9BY56AMGM.yml delete mode 100644 .decisions/log/01KZTGGRE2KWE90Q17NVBSHH3G.yml delete mode 100644 .decisions/log/01KZTGGRSF2PN0YJ6W6YCXNXE7.yml delete mode 100644 .decisions/log/01KZTGGS4B7BTEWB4C0N0Y3JV0.yml delete mode 100644 .decisions/log/01KZTGGSFEM9JH6TYTENN4KKD3.yml delete mode 100644 .decisions/log/01KZTGGSV7KVG13236BYWW28PV.yml delete mode 100644 .decisions/log/01KZTGGT76A1ASWHEDXRBQ8CSK.yml delete mode 100644 .decisions/log/01KZTGGTKM1NYXYNN2J5QX430D.yml delete mode 100644 .decisions/log/01KZTGGV067R92981RZ569H0GT.yml delete mode 100644 .decisions/log/01KZTGGVHB1Z680QE061XM1E5G.yml delete mode 100644 .decisions/log/01KZTGGVXZSAY2ENMR2XPXWGWF.yml delete mode 100644 .decisions/log/01KZTGGWARNTFQP017AERKJYS0.yml delete mode 100644 .decisions/log/01KZTGGWQGRY25YZ01F8M3GG9P.yml delete mode 100644 .decisions/log/01KZTGGX5BRMTH0XE4FRD8NQBN.yml delete mode 100644 .decisions/log/01KZTGGXJBQ0FRGHGDP84KDM3Z.yml delete mode 100644 .decisions/log/01KZTGGY0PM06CQ7XSDZP66ANX.yml delete mode 100644 .decisions/log/01KZTGGYE3Y450AW44A69ZZDF0.yml delete mode 100644 .decisions/log/01KZTGGYVSQ9M7F4DJ184E8D08.yml delete mode 100644 .decisions/log/01KZTGGZ9PNSGSYFSPVADMEGAB.yml delete mode 100644 .decisions/log/01KZTGGZQQJ5R0F156TJ6HARS6.yml delete mode 100644 .decisions/log/01KZTGH06WMVQZTB9VWQJBNBPK.yml delete mode 100644 .decisions/log/01KZTGH0TVH9JAXHG0H9K2VWWX.yml delete mode 100644 .decisions/log/01KZTGH19V8KXVHBHTFAQDBN90.yml delete mode 100644 .decisions/log/01KZTGH1RKH6AXJ0MP1MWGQVPW.yml delete mode 100644 .decisions/log/01KZTGH27QWDBP6WHPRNPM3AHV.yml delete mode 100644 .decisions/log/01KZTGH2Q22VA19V95VBDHV9MK.yml delete mode 100644 .decisions/log/01KZTGH375X445T362JRNDTG8Q.yml delete mode 100644 .decisions/log/01KZTGH3PYAVD7TQWV2ZSX0S86.yml delete mode 100644 .decisions/log/01KZTGH46S7P81K5GG6FXSAZDV.yml delete mode 100644 .decisions/log/01KZTGH4QJ902M3A5NM7CSK4ST.yml delete mode 100644 .decisions/log/01KZTGH581BJJ0MMXYK4CAK7YR.yml delete mode 100644 .decisions/log/01KZTGH5RQ1G66BPDM5N5Y2XCX.yml delete mode 100644 .decisions/log/01KZTGH69VQ7NA131CY3GCGMXR.yml delete mode 100644 .decisions/log/01KZTGH6TM60B2T8T2S2ZKSTG6.yml delete mode 100644 .decisions/log/01KZTGH7BTQ2Z6NBPQGDHQRXYN.yml delete mode 100644 .decisions/log/01KZTGH7XBNTWTZ253YWW2AQ5V.yml delete mode 100644 .decisions/log/01KZTGH8FFCXV3M8Q70XCBNV8W.yml delete mode 100644 .decisions/log/01KZTGH919RY8CY8XBJHJR9WZ6.yml delete mode 100644 .decisions/log/01KZTGH9KHKTDGS8AA8FMA9GYA.yml delete mode 100644 .decisions/log/01KZTGHA61JZQD5QSJKDHKWWEK.yml delete mode 100644 .decisions/log/01KZTGHARQQW8F91DRNWSPNVHR.yml delete mode 100644 .decisions/log/01KZTGHBB9ECEH8TJKTBA98F1X.yml delete mode 100644 .decisions/log/01KZTGHBYPENHW4MFF2SR9GE56.yml delete mode 100644 .decisions/log/01KZTGHCHVFKBJV8N7P8QHYBTT.yml delete mode 100644 .decisions/log/01KZTGHD5CSJK413WZHGYVQ2T3.yml delete mode 100644 .decisions/log/01KZTGHDS9YQ5H4F5FHFB28AWB.yml delete mode 100644 .decisions/log/01KZTGHEE8M1R77RQR5CQ47MQW.yml delete mode 100644 .decisions/log/01KZTGHF4B5HHKMR0QGBRBFD38.yml delete mode 100644 .decisions/log/01KZX70EMPA47TBR0PFKX4M32Z.yml delete mode 100644 .decisions/log/01KZX70EQGQCB1B190TS9FZ1A2.yml delete mode 100644 .decisions/log/01KZX70ET1GMR2012XKEP5EWDW.yml delete mode 100644 .decisions/log/01KZX70S86QGXVCA5GW5WSY6XA.yml delete mode 100644 .decisions/log/01KZX7HW7JY5BKE3YPTXXX9BDH.yml delete mode 100644 .decisions/log/01KZXCED6QA8EKJW5TX88H329G.yml delete mode 100644 .decisions/log/01KZXGPY5GVCDA45QGGCTS2T5X.yml delete mode 100644 .decisions/log/01KZXGRAK0P999C34NXQ3FPC27.yml delete mode 100644 .decisions/log/01KZXGSFTA68J6PJZFKKCFZPNZ.yml delete mode 100644 .decisions/log/01KZXHEF9JAERTPMYXGCAVK9HW.yml delete mode 100644 .decisions/log/01KZXHEFD5NKJ1SXWTZ7A34066.yml delete mode 100644 .decisions/log/01KZXHEFGXGD0KGZ4N2FX40D5F.yml delete mode 100644 .decisions/log/01KZXHEFM9MCNTD3YPWT9EXGMS.yml delete mode 100644 .decisions/log/01KZXHEFQH62RSGYTWTR83YMX1.yml delete mode 100644 .decisions/log/01KZXHEFTWDK928D5X36X0TY01.yml delete mode 100644 .decisions/log/01KZXHEFYBPHFW8WS6AQZ7EW2D.yml delete mode 100644 .decisions/log/01KZXHEG2A1JQV8VQMKCT7BQSM.yml delete mode 100644 .decisions/log/01KZXHEG6B4B4XZC078H6A0DR5.yml delete mode 100644 .decisions/log/01KZXHEGA6VDB6WTNQHFN6174N.yml delete mode 100644 .decisions/log/01KZXHEGD6WCBANRJZ76BRR1CE.yml delete mode 100644 .decisions/log/01KZXHEGG8GMFWZV86AY1Q162C.yml delete mode 100644 .decisions/log/01KZXHEGJXN7WM9Z32A0XAEZV9.yml delete mode 100644 .decisions/log/01KZXHEGNH7JG54HY035X1BS7S.yml delete mode 100644 .decisions/log/01KZXHEGRJYQ7SJ7N5RPEA9ANP.yml delete mode 100644 .decisions/log/01KZXHEGV930X88CWEXBYEWE9V.yml delete mode 100644 .decisions/log/01KZXHEGXWTEC1YTTCA66TXKAC.yml delete mode 100644 .decisions/log/01KZXHEH0MXYTAW72VF4QJ33SB.yml delete mode 100644 .decisions/log/01KZXHEH3ETEWJQA1HWE4W1ME8.yml delete mode 100644 .decisions/log/01KZXHEH6ATHYBFC71CH2P0WE1.yml delete mode 100644 .decisions/log/01KZXHEH9FK4EFSFPPHFHJCTS5.yml delete mode 100644 .decisions/log/01KZXHEHCM5ABV9D5DC14QNZ28.yml delete mode 100644 .decisions/log/01KZXHEHFANBFBMZJ39T1C5P9K.yml delete mode 100644 .decisions/log/01KZXHEHHWF1TWC0DH6HKB3RA6.yml delete mode 100644 .decisions/log/01KZXHEHN3N7HF0PDSMXVQPJWD.yml delete mode 100644 .decisions/log/01KZXHEHR19GYD06J7C3KE312X.yml delete mode 100644 .decisions/log/01KZXHEHV863ERYNZX2BWQRAHT.yml delete mode 100644 .decisions/log/01KZXHEHY1ER3B6AQ6NRQYMNCX.yml delete mode 100644 .decisions/log/01KZXHEJ0QJB6KJ7HEFG097DX9.yml delete mode 100644 .decisions/log/01KZXHEJ3J4SFVH60EBSGSJS0C.yml delete mode 100644 .decisions/log/01KZXHEJ6K605BPNWRMKR8DKSM.yml delete mode 100644 .decisions/log/01KZXHEJ9K2Y0ZX57MCVAJWM3F.yml delete mode 100644 .decisions/log/01KZXHEJCHN02VR0DVQWQYCYGY.yml delete mode 100644 .decisions/log/01KZXHEJFTE7AF7WCE4PFX9T09.yml delete mode 100644 .decisions/log/01KZXHEJKMK34FPVC09YPS8811.yml delete mode 100644 .decisions/log/01KZXHEJQ46TT044PV8D7HF0DD.yml delete mode 100644 .decisions/log/01KZXHEJV4XNJ1B1VN0S52MG9R.yml delete mode 100644 .decisions/log/01KZXHEJYAYZR29E0YVRMHKT54.yml delete mode 100644 .decisions/log/01KZXHEK1JEFGT6P65GA49VAMF.yml delete mode 100644 .decisions/log/01KZXHEK4PZK8XZ4JZ4PQK2P1X.yml delete mode 100644 .decisions/log/01KZXHEK87G21G6HDBARQSB397.yml delete mode 100644 .decisions/log/01KZXHEKBWHRHNRVE1BS94BRMW.yml delete mode 100644 .decisions/log/01KZXHEKF30H8Q1576AJBH7BJ6.yml delete mode 100644 .decisions/log/01KZXHEKJBPV2DZKE0055M0GG1.yml delete mode 100644 .decisions/log/01KZXHEKN8F6KKKAA3EHC3AWXQ.yml delete mode 100644 .decisions/log/01KZXHEKRD7K84JVQQ37YAE22T.yml delete mode 100644 .decisions/log/01KZXHEKVD61XMPEJR760DKVDV.yml delete mode 100644 .decisions/log/01KZXHEKYFZA862VRB46KME6NM.yml delete mode 100644 .decisions/log/01KZXHEM1CYPDR2MYP36ZANY05.yml delete mode 100644 .decisions/log/01KZXHEM5XC319NKKT183Q1845.yml delete mode 100644 .decisions/log/01KZXHEM97VBXFWNHM10ZK6JN0.yml delete mode 100644 .decisions/log/01KZXHEMCMZND8ST8DBQ3877Y7.yml delete mode 100644 .decisions/log/01KZXHEMG0VBZEBSZJZFJSDK5Z.yml delete mode 100644 .decisions/log/01KZXHEMK0GWJQN8WYMD57TRPH.yml delete mode 100644 .decisions/log/01KZXHEMP4MTJ1F07KC413MHJF.yml delete mode 100644 .decisions/log/01KZXHEMSFZ8QK6MJW067NKYPF.yml delete mode 100644 .decisions/log/01KZXHEMWR7PRCFFTWRKA915AM.yml delete mode 100644 .decisions/log/01KZXHEN04RCXYA1EB98JT8WPE.yml delete mode 100644 .decisions/log/01KZXHEN47W57WK8E5MBFJVBQ7.yml delete mode 100644 .decisions/log/01KZXHEN8ZRASX4M10VC5814S3.yml delete mode 100644 .decisions/log/01KZXHENDA4Y49QAE53FK97V41.yml delete mode 100644 .decisions/log/01KZXHENGJ67KH8NFC7D67RXBP.yml delete mode 100644 .decisions/log/01KZXHENKTATHVJHDGKMB59PHQ.yml delete mode 100644 .decisions/log/01KZXHENQEBXRX0GSN90BTEB89.yml delete mode 100644 .decisions/log/01KZXHENTSEEZY0T2N328FKB6N.yml delete mode 100644 .decisions/log/01KZXHENYP68WAXVCVAQ3FY1MP.yml delete mode 100644 .decisions/log/01KZXHEP2ERNT3A0P78RN5DEDZ.yml delete mode 100644 .decisions/log/01KZXHEP5ZC4E9JPZYXA9744YF.yml delete mode 100644 .decisions/log/01KZXHEP9M9H9NRTCW79AQ1Y89.yml delete mode 100644 .decisions/log/01KZXHEPD4F8EWHAQDDV9A042S.yml delete mode 100644 .decisions/log/01KZXHEPGKM61QD5JFZZ9FT5YN.yml delete mode 100644 .decisions/log/01KZXHEPMAXJG39RCR2B18E2Q9.yml delete mode 100644 .decisions/log/01KZXHEPRDW4XH1ESX7A2X8CF2.yml delete mode 100644 .decisions/log/01KZXHEPWK8JYJE4CQ0170Z35Y.yml delete mode 100644 .decisions/log/01KZXHEQ0QMMBZQ6NYEC3MSYC5.yml delete mode 100644 .decisions/log/01KZXJX69AJYGN66DQG02Z27BH.yml delete mode 100644 .decisions/log/01M3PHNSVZMME6XK72GNHHNKV6.yml delete mode 100644 .decisions/sets/ddd-governance.yml delete mode 100644 .decisions/sets/ledger-design.yml delete mode 100644 .github/copilot-instructions.md delete mode 100644 .github/workflows/product-ci.yml delete mode 100644 .github/workflows/publish-mcp.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .gitignore delete mode 100644 .htmlvalidate.json delete mode 100644 .mcp.json delete mode 100644 .product/capabilities.yaml delete mode 100644 .product/config.toml delete mode 100644 .product/products/acme/acme.ttl delete mode 100644 .product/products/acme/blueprints/acme-storefront/how-contract.yaml delete mode 100644 .product/products/acme/blueprints/acme-storefront/layout.yaml delete mode 100644 .product/products/acme/deciders/cart-decider.yaml delete mode 100644 .product/products/acme/deciders/order-decider.yaml delete mode 100644 .product/products/acme/deliverables/del-checkout.yaml delete mode 100644 .product/products/acme/deliverables/del-refunds.yaml delete mode 100644 .product/products/acme/deployable-units/du-admin-web.yaml delete mode 100644 .product/products/acme/deployable-units/du-shop-ios.yaml delete mode 100644 .product/products/acme/deployable-units/du-shop-web.yaml delete mode 100644 .product/products/acme/design-systems/wire-ds/components/button.html delete mode 100644 .product/products/acme/design-systems/wire-ds/components/list.html delete mode 100644 .product/products/acme/design-systems/wire-ds/components/value.html delete mode 100644 .product/products/acme/design-systems/wire-ds/design-system.manifest.yaml delete mode 100644 .product/products/acme/features/feat-checkout.yaml delete mode 100644 .product/products/acme/features/feat-refunds.yaml delete mode 100644 .product/products/acme/how-contract.yaml delete mode 100644 .product/products/acme/releases/rel-storefront.yaml delete mode 100644 .product/products/acme/targets/tgt-2.yaml delete mode 100644 .product/products/product-cli/authoring-scopes/figma.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/cells/add-mcp-tool.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/cells/implement-slice.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/cells/serve-web-view.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/how-contract.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/layout.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/cli-adapter-e-entity.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/core-slice-e-entity.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/handler-e-command.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/integration-tests-e-entity.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/registry-wiring-e-command.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/tool-def-e-command.yaml delete mode 100644 .product/products/product-cli/blueprints/product-cli/work-units/tool-test-e-command.yaml delete mode 100644 .product/products/product-cli/deciders/e-boundedcontext-decider.yaml delete mode 100644 .product/products/product-cli/deciders/e-viewgraph-decider.yaml delete mode 100644 .product/products/product-cli/deliverables/busiest-gate.yaml delete mode 100644 .product/products/product-cli/deliverables/session-escalated.yaml delete mode 100644 .product/products/product-cli/deliverables/session-start.yaml delete mode 100644 .product/products/product-cli/deliverables/ship-ctx-view-live.yaml delete mode 100644 .product/products/product-cli/deliverables/spec-depth-metric.yaml delete mode 100644 .product/products/product-cli/features/ctx-view-live.yaml delete mode 100644 .product/products/product-cli/features/session-start.yaml delete mode 100644 .product/products/product-cli/features/spec-depth-metric.yaml delete mode 100644 .product/products/product-cli/how-contract.yaml delete mode 100644 .product/products/product-cli/product-cli.provenance.json delete mode 100644 .product/products/product-cli/product-cli.ttl delete mode 100644 .product/products/product-cli/targets/what-1.2.yaml delete mode 100644 .product/products/product-cli/work-units/view-client-rm-view-graph.yaml delete mode 100644 .product/products/product-cli/work-units/view-routes-rm-view-graph.yaml delete mode 100644 .product/products/product-cli/work-units/view-serializer-rm-view-graph.yaml delete mode 100644 .product/products/product-cli/work-units/view-watcher-rm-view-graph.yaml delete mode 100644 .product/role-bindings.yaml delete mode 100644 .stylelintrc.json delete mode 100644 CLAUDE.md delete mode 100644 Cargo.toml delete mode 100644 conventions/README.md delete mode 100644 conventions/adr/ADR-0001-file-length-limit.md delete mode 100644 conventions/adr/ADR-0002-no-unwrap-in-lib.md delete mode 100644 conventions/adr/ADR-0003-cargo-deny.md delete mode 100644 conventions/adr/ADR-0004-ctxid-newtype.md delete mode 100644 conventions/adr/ADR-0005-fitness-checks.md delete mode 100644 conventions/docs/CTX-DEPS.md delete mode 100644 conventions/docs/CTX001.md delete mode 100644 conventions/docs/CTX002.md delete mode 100644 conventions/docs/CTX003.md delete mode 100644 conventions/docs/CTX004.md delete mode 100644 conventions/docs/CTX005.md delete mode 100644 dagger/.gitattributes delete mode 100644 dagger/.gitignore delete mode 100644 dagger/dagger.json delete mode 100644 dagger/go.mod delete mode 100644 dagger/go.sum delete mode 100644 dagger/main.go delete mode 100644 ddd-cli/Cargo.toml delete mode 100644 ddd-cli/src/commands/bind.rs delete mode 100644 ddd-cli/src/commands/content_hash.rs delete mode 100644 ddd-cli/src/commands/diff.rs delete mode 100644 ddd-cli/src/commands/diff_contracts.rs delete mode 100644 ddd-cli/src/commands/init.rs delete mode 100644 ddd-cli/src/commands/migrate.rs delete mode 100644 ddd-cli/src/commands/migrate_plan.rs delete mode 100644 ddd-cli/src/commands/mod.rs delete mode 100644 ddd-cli/src/commands/render.rs delete mode 100644 ddd-cli/src/commands/report.rs delete mode 100644 ddd-cli/src/commands/serve.rs delete mode 100644 ddd-cli/src/commands/validate.rs delete mode 100644 ddd-cli/src/commands/what.rs delete mode 100644 ddd-cli/src/commands/why.rs delete mode 100644 ddd-cli/src/main.rs delete mode 100644 ddd-cli/tests/contracts.rs delete mode 100644 ddd-cli/tests/corpus.rs delete mode 100644 ddd-cli/tests/corpus/htmlcss/01-token-added/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/01-token-added/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/01-token-added/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/02-token-removed/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/02-token-removed/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/02-token-removed/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/03-token-retype/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/03-token-retype/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/03-token-retype/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/04-token-value-change/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/04-token-value-change/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/04-token-value-change/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/05-class-added/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/05-class-added/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/05-class-added/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/06-class-removed/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/06-class-removed/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/06-class-removed/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/07-layer-order/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/07-layer-order/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/07-layer-order/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/08-link-rewired/after.html delete mode 100644 ddd-cli/tests/corpus/htmlcss/08-link-rewired/before.html delete mode 100644 ddd-cli/tests/corpus/htmlcss/08-link-rewired/labels.yaml delete mode 100644 ddd-cli/tests/corpus/htmlcss/09-mixed-theme-pass/after.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/09-mixed-theme-pass/before.css delete mode 100644 ddd-cli/tests/corpus/htmlcss/09-mixed-theme-pass/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/01-add-pub-fn/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/01-add-pub-fn/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/01-add-pub-fn/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/02-add-private-fn/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/02-add-private-fn/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/02-add-private-fn/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/03-signature-change-pub-fn/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/03-signature-change-pub-fn/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/03-signature-change-pub-fn/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/04-signature-change-private-fn/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/04-signature-change-private-fn/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/04-signature-change-private-fn/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/05-promote-private-fn/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/05-promote-private-fn/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/05-promote-private-fn/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/06-add-pub-crate-method/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/06-add-pub-crate-method/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/06-add-pub-crate-method/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/07-add-trait-impl/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/07-add-trait-impl/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/07-add-trait-impl/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/08-remove-pub-fn/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/08-remove-pub-fn/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/08-remove-pub-fn/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/09-derive-change/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/09-derive-change/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/09-derive-change/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/10-add-trait-member/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/10-add-trait-member/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/10-add-trait-member/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/11-mixed-feature/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/11-mixed-feature/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/11-mixed-feature/labels.yaml delete mode 100644 ddd-cli/tests/corpus/rust/12-mixed-refactor/after.rs delete mode 100644 ddd-cli/tests/corpus/rust/12-mixed-refactor/before.rs delete mode 100644 ddd-cli/tests/corpus/rust/12-mixed-refactor/labels.yaml delete mode 100644 ddd-cli/tests/ddd.rs delete mode 100644 ddd-cli/tests/fixtures/bicep/bicep.sarif delete mode 100644 ddd-cli/tests/fixtures/bicep/bicepconfig.json delete mode 100644 ddd-cli/tests/fixtures/bicep/main.bicep delete mode 100644 ddd-cli/tests/fixtures/bicep/storage-module.bicep delete mode 100644 ddd-cli/tests/fixtures/csharp/.editorconfig delete mode 100644 ddd-cli/tests/fixtures/csharp/Library.cs delete mode 100644 ddd-cli/tests/fixtures/csharp/Program.cs delete mode 100644 ddd-cli/tests/fixtures/csharp/diag.sarif delete mode 100644 ddd-cli/tests/fixtures/csharp/minimal.csproj delete mode 100644 ddd-cli/tests/fixtures/rust/Cargo.toml.txt delete mode 100644 ddd-cli/tests/fixtures/rust/lib.rs delete mode 100644 ddd-cli/tests/fixtures/rust/other.rs delete mode 100644 ddd-cli/tests/fixtures/visual/panel.html delete mode 100644 ddd-cli/tests/fixtures/webpair/.htmlvalidate.json delete mode 100644 ddd-cli/tests/fixtures/webpair/.stylelintrc.json delete mode 100644 ddd-cli/tests/fixtures/webpair/htmlvalidate.out.json delete mode 100644 ddd-cli/tests/fixtures/webpair/page.html delete mode 100644 ddd-cli/tests/fixtures/webpair/style.css delete mode 100644 ddd-cli/tests/fixtures/webpair/stylelint.out.json delete mode 100644 ddd-cli/tests/governance.rs delete mode 100644 ddd-cli/tests/mcp.rs delete mode 100644 ddd-cli/tests/migrate.rs delete mode 100644 ddd-cli/tests/render.rs delete mode 100644 ddd-cli/tests/visual.rs delete mode 100644 ddd-core/Cargo.toml delete mode 100644 ddd-core/assets/render.css delete mode 100644 ddd-core/src/basis_pin.rs delete mode 100644 ddd-core/src/bicepconfig.rs delete mode 100644 ddd-core/src/binding.rs delete mode 100644 ddd-core/src/cargolints.rs delete mode 100644 ddd-core/src/claim.rs delete mode 100644 ddd-core/src/concordance.rs delete mode 100644 ddd-core/src/config.rs delete mode 100644 ddd-core/src/configured.rs delete mode 100644 ddd-core/src/contracts.rs delete mode 100644 ddd-core/src/decision.rs delete mode 100644 ddd-core/src/detect.rs delete mode 100644 ddd-core/src/detect_tests.rs delete mode 100644 ddd-core/src/diff.rs delete mode 100644 ddd-core/src/diff_tests.rs delete mode 100644 ddd-core/src/editorconfig.rs delete mode 100644 ddd-core/src/gitrev.rs delete mode 100644 ddd-core/src/htmlvalidateconfig.rs delete mode 100644 ddd-core/src/init.rs delete mode 100644 ddd-core/src/ledger_gate.rs delete mode 100644 ddd-core/src/lib.rs delete mode 100644 ddd-core/src/manifest.rs delete mode 100644 ddd-core/src/pattern.rs delete mode 100644 ddd-core/src/predicate.rs delete mode 100644 ddd-core/src/render.rs delete mode 100644 ddd-core/src/render_tests.rs delete mode 100644 ddd-core/src/report.rs delete mode 100644 ddd-core/src/report_tests.rs delete mode 100644 ddd-core/src/revisit.rs delete mode 100644 ddd-core/src/revisit_tests.rs delete mode 100644 ddd-core/src/rules.rs delete mode 100644 ddd-core/src/sarif.rs delete mode 100644 ddd-core/src/seam.rs delete mode 100644 ddd-core/src/seam_event.rs delete mode 100644 ddd-core/src/store.rs delete mode 100644 ddd-core/src/store_tests.rs delete mode 100644 ddd-core/src/stylelintconfig.rs delete mode 100644 ddd-core/src/surface.rs delete mode 100644 ddd-core/src/tokenfile.rs delete mode 100644 ddd-core/src/turtle.rs delete mode 100644 ddd-core/src/validate.rs delete mode 100644 ddd-core/src/validate_format_tests.rs delete mode 100644 ddd-core/src/validate_tests.rs delete mode 100644 ddd-core/src/what.rs delete mode 100644 ddd-core/src/what_tests.rs delete mode 100644 ddd-core/src/why.rs delete mode 100644 ddd-core/src/why_tests.rs delete mode 100644 ddd-lsp/Cargo.toml delete mode 100644 ddd-lsp/src/adapter/bicep.rs delete mode 100644 ddd-lsp/src/adapter/csharp.rs delete mode 100644 ddd-lsp/src/adapter/csharp_facts.rs delete mode 100644 ddd-lsp/src/adapter/htmlcss.rs delete mode 100644 ddd-lsp/src/adapter/htmlcss_facts.rs delete mode 100644 ddd-lsp/src/adapter/htmlcss_facts_tests.rs delete mode 100644 ddd-lsp/src/adapter/htmlcss_pair.rs delete mode 100644 ddd-lsp/src/adapter/htmlcss_pair_tests.rs delete mode 100644 ddd-lsp/src/adapter/htmlcss_policy_tests.rs delete mode 100644 ddd-lsp/src/adapter/mod.rs delete mode 100644 ddd-lsp/src/adapter/rust.rs delete mode 100644 ddd-lsp/src/adapter/rust_facts.rs delete mode 100644 ddd-lsp/src/adapter/rust_policy_tests.rs delete mode 100644 ddd-lsp/src/adapter/rust_tests.rs delete mode 100644 ddd-lsp/src/bin/ddd-mock-lsp.rs delete mode 100644 ddd-lsp/src/capability.rs delete mode 100644 ddd-lsp/src/classify.rs delete mode 100644 ddd-lsp/src/classify_tests.rs delete mode 100644 ddd-lsp/src/client.rs delete mode 100644 ddd-lsp/src/declaration.rs delete mode 100644 ddd-lsp/src/error.rs delete mode 100644 ddd-lsp/src/host.rs delete mode 100644 ddd-lsp/src/jsonrpc.rs delete mode 100644 ddd-lsp/src/lib.rs delete mode 100644 ddd-lsp/src/manager.rs delete mode 100644 ddd-lsp/src/mock.rs delete mode 100644 ddd-lsp/src/mock/hierarchy.rs delete mode 100644 ddd-lsp/src/mock/parse.rs delete mode 100644 ddd-lsp/src/protocol.rs delete mode 100644 ddd-lsp/src/revdiff.rs delete mode 100644 ddd-lsp/src/revdiff_tests.rs delete mode 100644 ddd-lsp/tests/declaration_subset.rs delete mode 100644 ddd-lsp/tests/host.rs delete mode 100644 ddd-lsp/tests/real_hosts.rs delete mode 100644 ddd-lsp/tests/rust_host.rs delete mode 100644 ddd-mcp/Cargo.toml delete mode 100644 ddd-mcp/src/bin/ddd-mcp-mock-lsp.rs delete mode 100644 ddd-mcp/src/bind.rs delete mode 100644 ddd-mcp/src/dispatch.rs delete mode 100644 ddd-mcp/src/edits.rs delete mode 100644 ddd-mcp/src/govern_tools.rs delete mode 100644 ddd-mcp/src/intercept.rs delete mode 100644 ddd-mcp/src/lang_tools.rs delete mode 100644 ddd-mcp/src/lib.rs delete mode 100644 ddd-mcp/src/serve.rs delete mode 100644 ddd-mcp/src/state.rs delete mode 100644 ddd-mcp/src/tools.rs delete mode 100644 ddd-mcp/tests/rust_governed.rs delete mode 100644 ddd-mcp/tests/serve.rs delete mode 100644 ddd-mcp/tests/webpair_governed.rs delete mode 100644 deny.toml delete mode 100644 dist-workspace.toml delete mode 100644 docs/ai-foundations-conformance.md delete mode 100644 docs/audits/basis-quality-2026-08.md delete mode 100644 docs/audits/classifier-corpus-2026-08.md delete mode 100644 docs/audits/provenance-2026-08.md delete mode 100644 docs/ddd-adrs.md delete mode 100644 docs/ddd-cli-prd.md delete mode 100644 docs/ddd-format-migrations.md delete mode 100644 docs/ddd-m5-report.md delete mode 100644 docs/ddd-m6-experiment.md delete mode 100644 docs/ddd-m7-experiment.md delete mode 100644 docs/ddd-m8-report.md delete mode 100644 docs/ddd-research-protocol.md delete mode 100644 docs/ddd-roadmap.md delete mode 100644 docs/ddd-roslyn-host-plan.md delete mode 100644 docs/ddd-v1-spec.md delete mode 100644 docs/decisions/hafeok.ddd.nt delete mode 100644 docs/decisions/hafeok.ledger.nt delete mode 100644 docs/eval-format-v1.md delete mode 100644 docs/eval-format-v1/context-digest.json delete mode 100644 docs/examples/ddd-render-sample.html delete mode 100644 docs/examples/product-cli-blueprint.md delete mode 100644 docs/g-track/decisions/g-dec-01-reading-vocabulary.yaml delete mode 100644 docs/g-track/decisions/g-dec-02-projection-reasoner.yaml delete mode 100644 docs/g-track/g0-entry-probes-v3-v5-2026-08-17.md delete mode 100644 docs/g-track/g0-session-report-2026-08-19.md delete mode 100644 docs/g-track/g1-session-report-2026-08-17.md delete mode 100644 docs/g-track/kotlin-lsp-workspace-import-2026-08-17.md delete mode 100644 docs/g-track/prd-ground-as-ontology.md delete mode 100644 docs/g-track/registry-generator-design-2026-08-18.md delete mode 100644 docs/g-track/registry-generator-session-2026-08-18.md delete mode 100644 docs/g-track/registry-template/.github/workflows/validate.yml delete mode 100644 docs/g-track/registry-template/GENERATION.ttl delete mode 100644 docs/g-track/registry-template/README.md delete mode 100644 docs/g-track/registry-template/TEMPLATE.md delete mode 100644 docs/g-track/registry-template/graphs/canonical/_exemplar-decision.ttl delete mode 100644 docs/g-track/registry-template/graphs/canonical/_exemplar.ttl delete mode 100755 docs/g-track/registry-template/scripts/build-projection.sh delete mode 100644 docs/g-track/registry-template/shapes/decision.ttl delete mode 100644 docs/g-track/registry-template/shapes/derivation.ttl delete mode 100644 docs/g-track/registry-template/shapes/reading.ttl delete mode 100644 docs/g-track/registry-template/shapes/structural.ttl delete mode 100644 docs/g-track/registry-template/vocabulary/reg.ttl delete mode 100644 docs/g-track/two-axis-session-2026-08-20.md delete mode 100644 docs/guide.md delete mode 100644 docs/live-wire-plan.md delete mode 100644 docs/predicate-format.md delete mode 100644 docs/product-framework-open.md delete mode 100644 docs/reviews/ddd-cli-prd-review-2026-08.md delete mode 100644 docs/skills/product-v1.md delete mode 100644 docs/spark-dispatch.md delete mode 100644 docs/spec-flow-store-v1.md delete mode 100644 docs/two-pillars-conformance.md delete mode 100644 docs/way-of-working-decision-allocated-delivery.md delete mode 100644 docs/workshop-runbook.md delete mode 100644 docs/workshop/README.md delete mode 100644 docs/workshop/exercise-1-handout-da.md delete mode 100644 docs/workshop/exercise-2-handout-da.md delete mode 100644 docs/workshop/facilitator-spec-da.md delete mode 100644 eval-core/Cargo.toml delete mode 100644 eval-core/src/backend.rs delete mode 100644 eval-core/src/backend_tests.rs delete mode 100644 eval-core/src/behaviour.rs delete mode 100644 eval-core/src/behaviour_tests.rs delete mode 100644 eval-core/src/blobs.rs delete mode 100644 eval-core/src/blobs_tests.rs delete mode 100644 eval-core/src/declaration.rs delete mode 100644 eval-core/src/declaration_tests.rs delete mode 100644 eval-core/src/digest.rs delete mode 100644 eval-core/src/digest_tests.rs delete mode 100644 eval-core/src/escape.rs delete mode 100644 eval-core/src/escape_tests.rs delete mode 100644 eval-core/src/judgement.rs delete mode 100644 eval-core/src/judgement_tests.rs delete mode 100644 eval-core/src/lib.rs delete mode 100644 eval-core/src/pinned.rs delete mode 100644 eval-core/src/pinned_tests.rs delete mode 100644 eval-core/src/run.rs delete mode 100644 eval-core/src/run_tests.rs delete mode 100644 eval-core/src/store.rs delete mode 100644 eval-core/src/store_tests.rs delete mode 100644 eval-core/tests/fixture.rs delete mode 100644 eval-core/tests/fixtures/dotnet-written-run.json delete mode 100644 eval-core/tests/interop.rs delete mode 100644 eval-dotnet/.gitignore delete mode 100644 eval-dotnet/Directory.Build.props delete mode 100644 eval-dotnet/Directory.Packages.props delete mode 100644 eval-dotnet/Eval.slnx delete mode 100644 eval-dotnet/README.md delete mode 100644 eval-dotnet/src/Eval/Backend.cs delete mode 100644 eval-dotnet/src/Eval/Behaviour.cs delete mode 100644 eval-dotnet/src/Eval/Blobs.cs delete mode 100644 eval-dotnet/src/Eval/Declaration.cs delete mode 100644 eval-dotnet/src/Eval/Escape.cs delete mode 100644 eval-dotnet/src/Eval/Eval.csproj delete mode 100644 eval-dotnet/src/Eval/EvalStore.cs delete mode 100644 eval-dotnet/src/Eval/Evaluators.cs delete mode 100644 eval-dotnet/src/Eval/Judgement.cs delete mode 100644 eval-dotnet/src/Eval/Judging.cs delete mode 100644 eval-dotnet/src/Eval/RunContext.cs delete mode 100644 eval-dotnet/src/Eval/RunObservation.cs delete mode 100644 eval-dotnet/src/Eval/RunRecord.cs delete mode 100644 eval-dotnet/tests/Eval.Tests/BehaviourTests.cs delete mode 100644 eval-dotnet/tests/Eval.Tests/ContextDigestFixtureTests.cs delete mode 100644 eval-dotnet/tests/Eval.Tests/Eval.Tests.csproj delete mode 100644 eval-dotnet/tests/Eval.Tests/EvaluationTests.cs delete mode 100644 eval-dotnet/tests/Eval.Tests/GlobalUsings.cs delete mode 100644 eval-dotnet/tests/Eval.Tests/TempStore.cs delete mode 100644 examples/checkout/content-store.manifest.yaml delete mode 100644 examples/checkout/design-system.manifest.yaml delete mode 100644 ground-cli/Cargo.toml delete mode 100644 ground-cli/src/extract.rs delete mode 100644 ground-cli/src/extract_tests.rs delete mode 100644 ground-cli/src/main.rs delete mode 100644 ground-cli/src/read.rs delete mode 100644 ground-cli/src/read_tests.rs delete mode 100644 ground-cli/src/symbols.rs delete mode 100644 ground-cli/src/symbols_tests.rs delete mode 100644 ledger-cli/Cargo.toml delete mode 100644 ledger-cli/src/commands/add.rs delete mode 100644 ledger-cli/src/commands/authority.rs delete mode 100644 ledger-cli/src/commands/authority_enum.rs delete mode 100644 ledger-cli/src/commands/basis_text.rs delete mode 100644 ledger-cli/src/commands/cli_enum.rs delete mode 100644 ledger-cli/src/commands/common.rs delete mode 100644 ledger-cli/src/commands/declare.rs delete mode 100644 ledger-cli/src/commands/diff_cmd.rs delete mode 100644 ledger-cli/src/commands/evolve.rs delete mode 100644 ledger-cli/src/commands/export.rs delete mode 100644 ledger-cli/src/commands/graph_cmds.rs delete mode 100644 ledger-cli/src/commands/inbox.rs delete mode 100644 ledger-cli/src/commands/inbox_accept.rs delete mode 100644 ledger-cli/src/commands/inbox_findings.rs delete mode 100644 ledger-cli/src/commands/inbox_render.rs delete mode 100644 ledger-cli/src/commands/init.rs delete mode 100644 ledger-cli/src/commands/inspect.rs delete mode 100644 ledger-cli/src/commands/merge_cmd.rs delete mode 100644 ledger-cli/src/commands/mod.rs delete mode 100644 ledger-cli/src/commands/resolve_cmd.rs delete mode 100644 ledger-cli/src/commands/sign.rs delete mode 100644 ledger-cli/src/commands/terminal.rs delete mode 100644 ledger-cli/src/commands/terminal_callers_tests.rs delete mode 100644 ledger-cli/src/commands/terminal_tests.rs delete mode 100644 ledger-cli/src/commands/verify.rs delete mode 100644 ledger-cli/src/main.rs delete mode 100644 ledger-cli/tests/accept_group.rs delete mode 100644 ledger-cli/tests/authority.rs delete mode 100644 ledger-cli/tests/batch.rs delete mode 100644 ledger-cli/tests/binding_accounting.rs delete mode 100644 ledger-cli/tests/cli.rs delete mode 100644 ledger-cli/tests/closed_key.rs delete mode 100644 ledger-cli/tests/common/export_only.rs delete mode 100644 ledger-cli/tests/common/hand.rs delete mode 100644 ledger-cli/tests/common/mod.rs delete mode 100644 ledger-cli/tests/diff.rs delete mode 100644 ledger-cli/tests/digests.rs delete mode 100644 ledger-cli/tests/export.rs delete mode 100644 ledger-cli/tests/export_verifier.rs delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8HF297MMN8232TM4FCE.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8J24KD92JZQTDC6Y6B4.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8JPQSTRM3X8E7R0S1KA.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8KCK8XJWJTB98QBFZ27.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8M3D597TGJJ8W2NA04B.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8MXG2CAYGBS3X827NAY.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8NTBFYDABGM1ZHFC4SG.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8PSTE42V7GE8H747PG7.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8QWE8B74CGR8M5RC1MX.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8S0K9SNDXXK4NS2ZWFA.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8T68ZYVCJZXG9Z07FYW.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8VERG1CGNA3JWWADRF1.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8WVA4P7GEDV915Y5GG8.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8YA7J3H1HQT79599BDJ.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/log/01KZPMA8ZWBD1ADF7GP27HGH6S.yml delete mode 100644 ledger-cli/tests/fixtures/coverage/.decisions/sets/coverage-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/forked/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/forked/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDY0.yml delete mode 100644 ledger-cli/tests/fixtures/forked/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDY1.yml delete mode 100644 ledger-cli/tests/fixtures/forked/.decisions/sets/forked.yml delete mode 100644 ledger-cli/tests/fixtures/l001/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l001/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l002/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l002/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l003/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l003/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l004/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l004/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l005/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l005/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l006/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l006/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l007/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l007/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l008/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l008/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l010/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l010/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l013/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l013/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/l014/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/l014/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/pass/.decisions/log/01K2C4YQJ3F8M0PT5W7NZ9RDXW.yml delete mode 100644 ledger-cli/tests/fixtures/pass/.decisions/sets/ledger-fixture.yml delete mode 100644 ledger-cli/tests/fixtures/two-clocks/.decisions/log/01K2000000000000000000000A.yml delete mode 100644 ledger-cli/tests/fixtures/two-clocks/.decisions/log/01K2ZZZZZZZZZZZZZZZZZZZZZZ.yml delete mode 100644 ledger-cli/tests/fixtures/two-clocks/.decisions/sets/two-clocks.yml delete mode 100644 ledger-cli/tests/format_history.rs delete mode 100644 ledger-cli/tests/gate.rs delete mode 100644 ledger-cli/tests/genesis_key.rs delete mode 100644 ledger-cli/tests/git_identity.rs delete mode 100644 ledger-cli/tests/graph.rs delete mode 100644 ledger-cli/tests/immutability.rs delete mode 100644 ledger-cli/tests/inbox.rs delete mode 100644 ledger-cli/tests/inbox_failures.rs delete mode 100644 ledger-cli/tests/inbox_fixture/mod.rs delete mode 100644 ledger-cli/tests/interactive.rs delete mode 100644 ledger-cli/tests/key_across_namespaces.rs delete mode 100644 ledger-cli/tests/key_ownership.rs delete mode 100644 ledger-cli/tests/keys.rs delete mode 100644 ledger-cli/tests/landed_header.rs delete mode 100644 ledger-cli/tests/legacy_revocation.rs delete mode 100644 ledger-cli/tests/merge.rs delete mode 100644 ledger-cli/tests/policy_authors.rs delete mode 100644 ledger-cli/tests/pre_policy_binding.rs delete mode 100644 ledger-cli/tests/revisit_format.rs delete mode 100644 ledger-cli/tests/role_position.rs delete mode 100644 ledger-cli/tests/rotate_key.rs delete mode 100644 ledger-cli/tests/scalars.rs delete mode 100644 ledger-cli/tests/show.rs delete mode 100644 ledger-cli/tests/signing.rs delete mode 100644 ledger-cli/tests/trust.rs delete mode 100644 ledger-cli/tests/ungoverned_sidecar.rs delete mode 100644 ledger-cli/tests/verbs.rs delete mode 100644 ledger-core/Cargo.toml delete mode 100644 ledger-core/src/acceptance.rs delete mode 100644 ledger-core/src/acceptance_tests.rs delete mode 100644 ledger-core/src/allocation.rs delete mode 100644 ledger-core/src/allocation_tests.rs delete mode 100644 ledger-core/src/author/acceptance_ops.rs delete mode 100644 ledger-core/src/author/authority_ops.rs delete mode 100644 ledger-core/src/author/availability_ops.rs delete mode 100644 ledger-core/src/author/batch_accept.rs delete mode 100644 ledger-core/src/author/decision.rs delete mode 100644 ledger-core/src/author/declare.rs delete mode 100644 ledger-core/src/author/genesis_key.rs delete mode 100644 ledger-core/src/author/group_accept.rs delete mode 100644 ledger-core/src/author/identity_ops.rs delete mode 100644 ledger-core/src/author/mod.rs delete mode 100644 ledger-core/src/author/policy_ops.rs delete mode 100644 ledger-core/src/author/report.rs delete mode 100644 ledger-core/src/author/sign_ops.rs delete mode 100644 ledger-core/src/author/sign_tests.rs delete mode 100644 ledger-core/src/author/tests.rs delete mode 100644 ledger-core/src/author/version_ops.rs delete mode 100644 ledger-core/src/authority/availability.rs delete mode 100644 ledger-core/src/authority/binding.rs delete mode 100644 ledger-core/src/authority/check.rs delete mode 100644 ledger-core/src/authority/check_tests.rs delete mode 100644 ledger-core/src/authority/choice.rs delete mode 100644 ledger-core/src/authority/choice_tests.rs delete mode 100644 ledger-core/src/authority/filing.rs delete mode 100644 ledger-core/src/authority/filing_tests.rs delete mode 100644 ledger-core/src/authority/fixture.rs delete mode 100644 ledger-core/src/authority/grant.rs delete mode 100644 ledger-core/src/authority/key_close.rs delete mode 100644 ledger-core/src/authority/key_close_tests.rs delete mode 100644 ledger-core/src/authority/mod.rs delete mode 100644 ledger-core/src/authority/payload.rs delete mode 100644 ledger-core/src/authority/payload_tests.rs delete mode 100644 ledger-core/src/authority/policy.rs delete mode 100644 ledger-core/src/authority/references.rs delete mode 100644 ledger-core/src/authority/revocation.rs delete mode 100644 ledger-core/src/authority/role.rs delete mode 100644 ledger-core/src/authority/signers.rs delete mode 100644 ledger-core/src/authority/structure.rs delete mode 100644 ledger-core/src/authority/view.rs delete mode 100644 ledger-core/src/batch.rs delete mode 100644 ledger-core/src/batch_drift.rs delete mode 100644 ledger-core/src/batch_file.rs delete mode 100644 ledger-core/src/batch_file_tests.rs delete mode 100644 ledger-core/src/batch_render.rs delete mode 100644 ledger-core/src/batch_tests.rs delete mode 100644 ledger-core/src/blame.rs delete mode 100644 ledger-core/src/blame_tests.rs delete mode 100644 ledger-core/src/canon.rs delete mode 100644 ledger-core/src/canon_tests.rs delete mode 100644 ledger-core/src/changeset.rs delete mode 100644 ledger-core/src/coverage.rs delete mode 100644 ledger-core/src/diff.rs delete mode 100644 ledger-core/src/diff_tests.rs delete mode 100644 ledger-core/src/discharge.rs delete mode 100644 ledger-core/src/discharge_tests.rs delete mode 100644 ledger-core/src/finding.rs delete mode 100644 ledger-core/src/format.rs delete mode 100644 ledger-core/src/graph/authority.rs delete mode 100644 ledger-core/src/graph/export.rs delete mode 100644 ledger-core/src/graph/export_tests.rs delete mode 100644 ledger-core/src/graph/index.rs delete mode 100644 ledger-core/src/graph/mod.rs delete mode 100644 ledger-core/src/graph/ntriples.rs delete mode 100644 ledger-core/src/graph/revocation_tests.rs delete mode 100644 ledger-core/src/graph/shapes.rs delete mode 100644 ledger-core/src/graph/tests.rs delete mode 100644 ledger-core/src/graph/turtle.rs delete mode 100644 ledger-core/src/hash.rs delete mode 100644 ledger-core/src/hold.rs delete mode 100644 ledger-core/src/id.rs delete mode 100644 ledger-core/src/id_tests.rs delete mode 100644 ledger-core/src/identity.rs delete mode 100644 ledger-core/src/identity_tests.rs delete mode 100644 ledger-core/src/inbox/config.rs delete mode 100644 ledger-core/src/inbox/git.rs delete mode 100644 ledger-core/src/inbox/index.rs delete mode 100644 ledger-core/src/inbox/index_tests.rs delete mode 100644 ledger-core/src/inbox/list.rs delete mode 100644 ledger-core/src/inbox/mod.rs delete mode 100644 ledger-core/src/inbox/terms.rs delete mode 100644 ledger-core/src/init.rs delete mode 100644 ledger-core/src/key.rs delete mode 100644 ledger-core/src/landed.rs delete mode 100644 ledger-core/src/landed_tests.rs delete mode 100644 ledger-core/src/landing.rs delete mode 100644 ledger-core/src/landing_tests.rs delete mode 100644 ledger-core/src/lib.rs delete mode 100644 ledger-core/src/merge/conflicts.rs delete mode 100644 ledger-core/src/merge/driver.rs delete mode 100644 ledger-core/src/merge/mod.rs delete mode 100644 ledger-core/src/merge/plan.rs delete mode 100644 ledger-core/src/merge/resolve.rs delete mode 100644 ledger-core/src/merge/tests.rs delete mode 100644 ledger-core/src/mint.rs delete mode 100644 ledger-core/src/render.rs delete mode 100644 ledger-core/src/revision.rs delete mode 100644 ledger-core/src/revisit.rs delete mode 100644 ledger-core/src/scalars.rs delete mode 100644 ledger-core/src/scalars_tests.rs delete mode 100644 ledger-core/src/set.rs delete mode 100644 ledger-core/src/show.rs delete mode 100644 ledger-core/src/show_render.rs delete mode 100644 ledger-core/src/show_tests.rs delete mode 100644 ledger-core/src/signing/check.rs delete mode 100644 ledger-core/src/signing/check_tests.rs delete mode 100644 ledger-core/src/signing/dsse.rs delete mode 100644 ledger-core/src/signing/dsse_tests.rs delete mode 100644 ledger-core/src/signing/mod.rs delete mode 100644 ledger-core/src/signing/review.rs delete mode 100644 ledger-core/src/signing/ssh.rs delete mode 100644 ledger-core/src/signing/subject.rs delete mode 100644 ledger-core/src/store.rs delete mode 100644 ledger-core/src/store_tests.rs delete mode 100644 ledger-core/src/testkit.rs delete mode 100644 ledger-core/src/tier.rs delete mode 100644 ledger-core/src/tier_tests.rs delete mode 100644 ledger-core/src/verify/acts.rs delete mode 100644 ledger-core/src/verify/authority.rs delete mode 100644 ledger-core/src/verify/authority_tests.rs delete mode 100644 ledger-core/src/verify/disposition.rs delete mode 100644 ledger-core/src/verify/duplicate_tests.rs delete mode 100644 ledger-core/src/verify/escape_acceptor_tests.rs delete mode 100644 ledger-core/src/verify/genesis_role.rs delete mode 100644 ledger-core/src/verify/genesis_role_tests.rs delete mode 100644 ledger-core/src/verify/history.rs delete mode 100644 ledger-core/src/verify/integrity.rs delete mode 100644 ledger-core/src/verify/keys.rs delete mode 100644 ledger-core/src/verify/keys_tests.rs delete mode 100644 ledger-core/src/verify/mod.rs delete mode 100644 ledger-core/src/verify/mod_tests.rs delete mode 100644 ledger-core/src/verify/order_grid.rs delete mode 100644 ledger-core/src/verify/order_regressions_tests.rs delete mode 100644 ledger-core/src/verify/order_tests.rs delete mode 100644 ledger-core/src/verify/state.rs delete mode 100644 ledger-core/src/verify/state_tests.rs delete mode 100644 ledger-core/src/verify/view.rs delete mode 100644 ledger-core/src/version.rs delete mode 100644 ledger-core/src/version_tests.rs delete mode 100644 ledger-core/src/whoami.rs delete mode 100644 ledger/README.md delete mode 100644 ledger/audits/audit-2026-10.md delete mode 100644 ledger/prd/decision-ledger-prd.md delete mode 100644 ledger/prd/decision-registry-prd.md delete mode 100644 ledger/prd/ledger-cli-prd.md delete mode 100644 ledger/prd/namespace-independence-prd.md delete mode 100644 ledger/rulings/absorption-replies-rulings-2026-10-07.md delete mode 100644 ledger/rulings/basis-and-absorption-rulings-2026-10-06.md delete mode 100644 ledger/rulings/ground-and-protocol-rulings-2026-10.md delete mode 100644 ledger/rulings/namespace-design-rulings-2026-10-07.md delete mode 100644 ledger/rulings/order-from-the-acts-rulings-2026-10-09.md delete mode 100644 ledger/rulings/signing-rulings-2026-10-d5-d9.md delete mode 100644 ledger/rulings/signing-rulings-2026-10.md delete mode 100644 ledger/rulings/verification-rulings-2026-10-07.md delete mode 100644 ledger/sessions/2026-10-namespace-design.md delete mode 100644 ledger/sessions/2026-10-protocol-absorption.md delete mode 100644 ledger/sessions/2026-10-repo-setup.md delete mode 100644 ledger/sessions/2026-10-session-a.md delete mode 100644 ledger/sessions/2026-10-session-b.md delete mode 100644 ledger/sessions/2026-10-verification-fixes.md delete mode 100644 ledger/sessions/2026-10-verification.md delete mode 100644 ledger/sessions/acceptance-worksheet-2026-08.md delete mode 100644 ledger/sessions/product-cli-v2-sessions.md delete mode 100644 ledger/spec/authority/ledger-authority-shapes.ttl delete mode 100644 ledger/spec/authority/ledger-authority.ttl delete mode 100644 ledger/spec/authority/sample-invalid.ttl delete mode 100644 ledger/spec/authority/sample-valid.ttl delete mode 100644 ledger/spec/ledger-protocol.md delete mode 100644 ledger/spec/server-client-protocol.md delete mode 100644 ledger/spec/tests/README.md delete mode 100644 product-cli/Cargo.toml delete mode 100644 product-cli/src/commands/author.rs delete mode 100644 product-cli/src/commands/blueprint.rs delete mode 100644 product-cli/src/commands/build.rs delete mode 100644 product-cli/src/commands/build_guard.rs delete mode 100644 product-cli/src/commands/build_lsp.rs delete mode 100644 product-cli/src/commands/build_seam_emit.rs delete mode 100644 product-cli/src/commands/build_session.rs delete mode 100644 product-cli/src/commands/build_skip.rs delete mode 100644 product-cli/src/commands/build_verify.rs delete mode 100644 product-cli/src/commands/cell.rs delete mode 100644 product-cli/src/commands/codegen.rs delete mode 100644 product-cli/src/commands/codegen_how.rs delete mode 100644 product-cli/src/commands/completions.rs delete mode 100644 product-cli/src/commands/decider.rs delete mode 100644 product-cli/src/commands/deliverable.rs delete mode 100644 product-cli/src/commands/deployable_unit.rs delete mode 100644 product-cli/src/commands/design_system.rs delete mode 100644 product-cli/src/commands/dispatch.rs delete mode 100644 product-cli/src/commands/domain.rs delete mode 100644 product-cli/src/commands/domain_data.rs delete mode 100644 product-cli/src/commands/domain_fields.rs delete mode 100644 product-cli/src/commands/domain_fields_v16.rs delete mode 100644 product-cli/src/commands/domain_rows.rs delete mode 100644 product-cli/src/commands/feature.rs delete mode 100644 product-cli/src/commands/guide.rs delete mode 100644 product-cli/src/commands/hooks.rs delete mode 100644 product-cli/src/commands/how.rs delete mode 100644 product-cli/src/commands/how_fields.rs delete mode 100644 product-cli/src/commands/init.rs delete mode 100644 product-cli/src/commands/init_helpers.rs delete mode 100644 product-cli/src/commands/lsp.rs delete mode 100644 product-cli/src/commands/mcp_cmd.rs delete mode 100644 product-cli/src/commands/mod.rs delete mode 100644 product-cli/src/commands/output.rs delete mode 100644 product-cli/src/commands/preview.rs delete mode 100644 product-cli/src/commands/primitive.rs delete mode 100644 product-cli/src/commands/product.rs delete mode 100644 product-cli/src/commands/projector.rs delete mode 100644 product-cli/src/commands/question.rs delete mode 100644 product-cli/src/commands/registry.rs delete mode 100644 product-cli/src/commands/release.rs delete mode 100644 product-cli/src/commands/root_enum.rs delete mode 100644 product-cli/src/commands/scope.rs delete mode 100644 product-cli/src/commands/seam.rs delete mode 100644 product-cli/src/commands/session.rs delete mode 100644 product-cli/src/commands/shared.rs delete mode 100644 product-cli/src/commands/skills.rs delete mode 100644 product-cli/src/commands/skills_tests.rs delete mode 100644 product-cli/src/commands/target.rs delete mode 100644 product-cli/src/commands/verdict.rs delete mode 100644 product-cli/src/commands/work_unit.rs delete mode 100644 product-cli/src/commands/worker.rs delete mode 100644 product-cli/src/main.rs delete mode 100644 product-cli/tests/agent_context.rs delete mode 100644 product-cli/tests/code_quality_tests.rs delete mode 100644 product-cli/tests/fixtures/external-core-consumer/Cargo.toml delete mode 100644 product-cli/tests/fixtures/external-core-consumer/src/main.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/Cargo.toml delete mode 100644 product-cli/tests/fixtures/onboard-large/src/api/health.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/api/routes.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/api/versioning.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/auth/jwt.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/auth/middleware.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/config/settings.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/handlers/admin_handler.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/handlers/order_handler.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/handlers/user_handler.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/middleware/logging.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/models/order.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/models/user.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/repo/order_repo.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/repo/user_repo.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/services/notification.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/services/payment.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/storage/migrations.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/storage/postgres.rs delete mode 100644 product-cli/tests/fixtures/onboard-large/src/utils/errors.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/Cargo.toml delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/api/handler.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/api/middleware.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/api/routes.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/error.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/main.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/repo/mod.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/repo/orders.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/repo/users.rs delete mode 100644 product-cli/tests/fixtures/onboard-sample/src/storage/cache.rs delete mode 100644 product-cli/tests/fixtures/server.schema.json delete mode 100644 product-cli/tests/framework.rs delete mode 100644 product-cli/tests/registry_generator.rs delete mode 100644 product-cli/tests/registry_shacl_divergence.rs delete mode 100644 product-cli/tests/registry_support/mod.rs delete mode 100644 product-core/Cargo.toml delete mode 100644 product-core/benches/graph_bench.rs delete mode 100644 product-core/src/author/domain.rs delete mode 100644 product-core/src/author/domain_prompt.md delete mode 100644 product-core/src/author/mod.rs delete mode 100644 product-core/src/author/workflow.rs delete mode 100644 product-core/src/author/workflow_prompt.md delete mode 100644 product-core/src/config.rs delete mode 100644 product-core/src/config_sections.rs delete mode 100644 product-core/src/demo.rs delete mode 100644 product-core/src/error.rs delete mode 100644 product-core/src/fileops.rs delete mode 100644 product-core/src/ground/apply.rs delete mode 100644 product-core/src/ground/axes.rs delete mode 100644 product-core/src/ground/batch.rs delete mode 100644 product-core/src/ground/batch_tests.rs delete mode 100644 product-core/src/ground/declared.rs delete mode 100644 product-core/src/ground/declared_tests.rs delete mode 100644 product-core/src/ground/derivation.rs delete mode 100644 product-core/src/ground/entail.rs delete mode 100644 product-core/src/ground/entail_tests.rs delete mode 100644 product-core/src/ground/facts.rs delete mode 100644 product-core/src/ground/facts_tests.rs delete mode 100644 product-core/src/ground/mint.rs delete mode 100644 product-core/src/ground/mod.rs delete mode 100644 product-core/src/ground/plan.rs delete mode 100644 product-core/src/ground/plan_tests.rs delete mode 100644 product-core/src/ground/projection.rs delete mode 100644 product-core/src/ground/projection_tests.rs delete mode 100644 product-core/src/ground/propose.rs delete mode 100644 product-core/src/ground/report.rs delete mode 100644 product-core/src/ground/rows.rs delete mode 100644 product-core/src/ground/sidecar.rs delete mode 100644 product-core/src/ground/sidecar_tests.rs delete mode 100644 product-core/src/ground/structural.rs delete mode 100644 product-core/src/ground/triple.rs delete mode 100644 product-core/src/ground/vocab.rs delete mode 100644 product-core/src/ground/write.rs delete mode 100644 product-core/src/ground/write_tests.rs delete mode 100644 product-core/src/guide/mod.rs delete mode 100644 product-core/src/guide/plan.rs delete mode 100644 product-core/src/guide/render.rs delete mode 100644 product-core/src/guide/tests.rs delete mode 100644 product-core/src/io/mod.rs delete mode 100644 product-core/src/lib.rs delete mode 100644 product-core/src/parse/mod.rs delete mode 100644 product-core/src/pf/authoring_scope.rs delete mode 100644 product-core/src/pf/authoring_scope_enforce.rs delete mode 100644 product-core/src/pf/authoring_scope_join.rs delete mode 100644 product-core/src/pf/authoring_scope_tests.rs delete mode 100644 product-core/src/pf/blueprint.rs delete mode 100644 product-core/src/pf/blueprint_tests.rs delete mode 100644 product-core/src/pf/blueprint_turtle.rs delete mode 100644 product-core/src/pf/build.rs delete mode 100644 product-core/src/pf/build_metrics.rs delete mode 100644 product-core/src/pf/build_metrics_tests.rs delete mode 100644 product-core/src/pf/build_seam.rs delete mode 100644 product-core/src/pf/build_seam_convert.rs delete mode 100644 product-core/src/pf/build_spmc.rs delete mode 100644 product-core/src/pf/build_spmc_tests.rs delete mode 100644 product-core/src/pf/build_tests.rs delete mode 100644 product-core/src/pf/bundle.rs delete mode 100644 product-core/src/pf/bundle_tests.rs delete mode 100644 product-core/src/pf/capability.rs delete mode 100644 product-core/src/pf/capability_tests.rs delete mode 100644 product-core/src/pf/cell.rs delete mode 100644 product-core/src/pf/cell_validate.rs delete mode 100644 product-core/src/pf/cell_validate_tests.rs delete mode 100644 product-core/src/pf/codegen.rs delete mode 100644 product-core/src/pf/codegen_backend.rs delete mode 100644 product-core/src/pf/codegen_cell.rs delete mode 100644 product-core/src/pf/codegen_decider.rs delete mode 100644 product-core/src/pf/codegen_fixtures.rs delete mode 100644 product-core/src/pf/codegen_flow.rs delete mode 100644 product-core/src/pf/codegen_ident.rs delete mode 100644 product-core/src/pf/codegen_infer.rs delete mode 100644 product-core/src/pf/codegen_kotlin.rs delete mode 100644 product-core/src/pf/codegen_kotlin_facts.rs delete mode 100644 product-core/src/pf/codegen_manifest.rs delete mode 100644 product-core/src/pf/codegen_openapi.rs delete mode 100644 product-core/src/pf/codegen_oracle.rs delete mode 100644 product-core/src/pf/codegen_program.rs delete mode 100644 product-core/src/pf/codegen_projector.rs delete mode 100644 product-core/src/pf/codegen_prov.rs delete mode 100644 product-core/src/pf/codegen_runtime.rs delete mode 100644 product-core/src/pf/codegen_scenarios.rs delete mode 100644 product-core/src/pf/codegen_screen.rs delete mode 100644 product-core/src/pf/codegen_tests.rs delete mode 100644 product-core/src/pf/codegen_types.rs delete mode 100644 product-core/src/pf/codegen_web.rs delete mode 100644 product-core/src/pf/codegen_web_tests.rs delete mode 100644 product-core/src/pf/data_check.rs delete mode 100644 product-core/src/pf/data_check_tests.rs delete mode 100644 product-core/src/pf/decider.rs delete mode 100644 product-core/src/pf/decider_cel.rs delete mode 100644 product-core/src/pf/decider_conform.rs delete mode 100644 product-core/src/pf/decider_conform_tests.rs delete mode 100644 product-core/src/pf/decider_logic.rs delete mode 100644 product-core/src/pf/decider_sim.rs delete mode 100644 product-core/src/pf/decider_sim_tests.rs delete mode 100644 product-core/src/pf/decider_tests.rs delete mode 100644 product-core/src/pf/decider_turtle.rs delete mode 100644 product-core/src/pf/deliverable.rs delete mode 100644 product-core/src/pf/deliverable_tests.rs delete mode 100644 product-core/src/pf/deployable_unit.rs delete mode 100644 product-core/src/pf/deployable_unit_tests.rs delete mode 100644 product-core/src/pf/dispatch.rs delete mode 100644 product-core/src/pf/dispatch_tests.rs delete mode 100644 product-core/src/pf/done.rs delete mode 100644 product-core/src/pf/done_tests.rs delete mode 100644 product-core/src/pf/ds_store.rs delete mode 100644 product-core/src/pf/edit.rs delete mode 100644 product-core/src/pf/edit_tests.rs delete mode 100644 product-core/src/pf/feature.rs delete mode 100644 product-core/src/pf/feature_tests.rs delete mode 100644 product-core/src/pf/how.rs delete mode 100644 product-core/src/pf/how_edit.rs delete mode 100644 product-core/src/pf/how_edit_tests.rs delete mode 100644 product-core/src/pf/how_turtle.rs delete mode 100644 product-core/src/pf/how_validate.rs delete mode 100644 product-core/src/pf/how_validate_tests.rs delete mode 100644 product-core/src/pf/ids.rs delete mode 100644 product-core/src/pf/kind_alias.rs delete mode 100644 product-core/src/pf/layout.rs delete mode 100644 product-core/src/pf/layout_check.rs delete mode 100644 product-core/src/pf/layout_check_tests.rs delete mode 100644 product-core/src/pf/layout_tests.rs delete mode 100644 product-core/src/pf/lsp.rs delete mode 100644 product-core/src/pf/lsp_tests.rs delete mode 100644 product-core/src/pf/manifest.rs delete mode 100644 product-core/src/pf/manifest_bundle.rs delete mode 100644 product-core/src/pf/manifest_content.rs delete mode 100644 product-core/src/pf/manifest_content_tests.rs delete mode 100644 product-core/src/pf/manifest_tests.rs delete mode 100644 product-core/src/pf/mod.rs delete mode 100644 product-core/src/pf/model.rs delete mode 100644 product-core/src/pf/model_data.rs delete mode 100644 product-core/src/pf/model_product.rs delete mode 100644 product-core/src/pf/model_question.rs delete mode 100644 product-core/src/pf/model_tests.rs delete mode 100644 product-core/src/pf/model_ui.rs delete mode 100644 product-core/src/pf/ops.rs delete mode 100644 product-core/src/pf/ops_tests.rs delete mode 100644 product-core/src/pf/paths.rs delete mode 100644 product-core/src/pf/primitive.rs delete mode 100644 product-core/src/pf/primitive_tests.rs delete mode 100644 product-core/src/pf/projector.rs delete mode 100644 product-core/src/pf/projector_conform.rs delete mode 100644 product-core/src/pf/projector_conform_tests.rs delete mode 100644 product-core/src/pf/projector_logic.rs delete mode 100644 product-core/src/pf/projector_sim.rs delete mode 100644 product-core/src/pf/projector_sim_tests.rs delete mode 100644 product-core/src/pf/projector_tests.rs delete mode 100644 product-core/src/pf/provenance.rs delete mode 100644 product-core/src/pf/query.rs delete mode 100644 product-core/src/pf/question.rs delete mode 100644 product-core/src/pf/question_gate.rs delete mode 100644 product-core/src/pf/question_tests.rs delete mode 100644 product-core/src/pf/question_turtle.rs delete mode 100644 product-core/src/pf/questions.rs delete mode 100644 product-core/src/pf/reify_ds.rs delete mode 100644 product-core/src/pf/reify_ds_tests.rs delete mode 100644 product-core/src/pf/release.rs delete mode 100644 product-core/src/pf/release_tests.rs delete mode 100644 product-core/src/pf/render_contract.rs delete mode 100644 product-core/src/pf/render_contract_tests.rs delete mode 100644 product-core/src/pf/rules_data.rs delete mode 100644 product-core/src/pf/rules_data_tests.rs delete mode 100644 product-core/src/pf/rules_decider.rs delete mode 100644 product-core/src/pf/rules_how.rs delete mode 100644 product-core/src/pf/rules_pattern.rs delete mode 100644 product-core/src/pf/rules_pattern_tests.rs delete mode 100644 product-core/src/pf/rules_reify.rs delete mode 100644 product-core/src/pf/rules_reify_tests.rs delete mode 100644 product-core/src/pf/rules_ui.rs delete mode 100644 product-core/src/pf/rules_what.rs delete mode 100644 product-core/src/pf/run.rs delete mode 100644 product-core/src/pf/schedule.rs delete mode 100644 product-core/src/pf/schedule_tests.rs delete mode 100644 product-core/src/pf/seam.rs delete mode 100644 product-core/src/pf/seam_tests.rs delete mode 100644 product-core/src/pf/seed.rs delete mode 100644 product-core/src/pf/seed_canon.rs delete mode 100644 product-core/src/pf/seed_data.rs delete mode 100644 product-core/src/pf/seed_tests.rs delete mode 100644 product-core/src/pf/seed_ui.rs delete mode 100644 product-core/src/pf/session.rs delete mode 100644 product-core/src/pf/sparql_dataset.rs delete mode 100644 product-core/src/pf/sparql_rules.rs delete mode 100644 product-core/src/pf/target.rs delete mode 100644 product-core/src/pf/turtle.rs delete mode 100644 product-core/src/pf/turtle_data.rs delete mode 100644 product-core/src/pf/turtle_product.rs delete mode 100644 product-core/src/pf/turtle_ui.rs delete mode 100644 product-core/src/pf/validate.rs delete mode 100644 product-core/src/pf/validate_product.rs delete mode 100644 product-core/src/pf/validate_tests.rs delete mode 100644 product-core/src/pf/verify.rs delete mode 100644 product-core/src/pf/verify_tests.rs delete mode 100644 product-core/src/pf/viz.rs delete mode 100644 product-core/src/pf/viz_tests.rs delete mode 100644 product-core/src/pf/wcag22.rs delete mode 100644 product-core/src/pf/work_unit.rs delete mode 100644 product-core/src/pf/work_unit_validate.rs delete mode 100644 product-core/src/pf/work_unit_validate_tests.rs delete mode 100644 product-core/src/pf/worker.rs delete mode 100644 product-core/src/pf/worker_tests.rs delete mode 100644 product-core/src/pf/workflow.rs delete mode 100644 product-core/src/registry/apply.rs delete mode 100644 product-core/src/registry/check/check_tests.rs delete mode 100644 product-core/src/registry/check/eval.rs delete mode 100644 product-core/src/registry/check/file_rule.rs delete mode 100644 product-core/src/registry/check/mod.rs delete mode 100644 product-core/src/registry/check/shapes.rs delete mode 100644 product-core/src/registry/mod.rs delete mode 100644 product-core/src/registry/params.rs delete mode 100644 product-core/src/registry/params_tests.rs delete mode 100644 product-core/src/registry/plan.rs delete mode 100644 product-core/src/registry/plan_tests.rs delete mode 100644 product-core/src/registry/substitute.rs delete mode 100644 product-core/src/registry/substitute_tests.rs delete mode 100644 product-core/src/registry/template.rs delete mode 100644 product-core/src/registry/template_tests.rs delete mode 100644 product-core/src/registry/verify.rs delete mode 100644 product-core/src/registry/verify_tests.rs delete mode 100644 product-core/src/root.rs delete mode 100644 product-core/tests/graph_capability_tests.rs delete mode 100644 product-core/tests/property/fileops_properties.rs delete mode 100644 product-core/tests/property/init_properties.proptest-regressions delete mode 100644 product-core/tests/property/init_properties.rs delete mode 100644 product-core/tests/property/mod.rs delete mode 100644 product-core/tests/property_tests.rs delete mode 100644 product-mcp/Cargo.toml delete mode 100644 product-mcp/src/assets/ui/AIOCatalogView.jsx delete mode 100644 product-mcp/src/assets/ui/BuildViews.jsx delete mode 100644 product-mcp/src/assets/ui/ContentView.jsx delete mode 100644 product-mcp/src/assets/ui/DataView.jsx delete mode 100644 product-mcp/src/assets/ui/DecidersView.jsx delete mode 100644 product-mcp/src/assets/ui/DomainGraph.jsx delete mode 100644 product-mcp/src/assets/ui/FeaturesView.jsx delete mode 100644 product-mcp/src/assets/ui/FlowsTimeline.jsx delete mode 100644 product-mcp/src/assets/ui/GraphView.jsx delete mode 100644 product-mcp/src/assets/ui/HowProcessView.jsx delete mode 100644 product-mcp/src/assets/ui/HowViews.jsx delete mode 100644 product-mcp/src/assets/ui/PageGraphView.jsx delete mode 100644 product-mcp/src/assets/ui/ScenariosView.jsx delete mode 100644 product-mcp/src/assets/ui/ScreenPreview.jsx delete mode 100644 product-mcp/src/assets/ui/SystemsMap.jsx delete mode 100644 product-mcp/src/assets/ui/UIStepsView.jsx delete mode 100644 product-mcp/src/assets/ui/VersionsView.jsx delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/_adherence.oxlintrc.json delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/_ds_bundle.js delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/_ds_manifest.json delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/readme.md delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/styles.css delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/tokens/base.css delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/tokens/colors.css delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/tokens/effects.css delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/tokens/fonts.css delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/tokens/spacing.css delete mode 100644 product-mcp/src/assets/ui/_ds/product-framework-design-system-52ecf1de-fd33-455e-92d3-6b70fcfd5a68/tokens/typography.css delete mode 100644 product-mcp/src/assets/ui/app.jsx delete mode 100644 product-mcp/src/assets/ui/assets/logo-mark.svg delete mode 100644 product-mcp/src/assets/ui/assets/logo-wordmark-light.svg delete mode 100644 product-mcp/src/assets/ui/data-graph.js delete mode 100644 product-mcp/src/assets/ui/data-how.js delete mode 100644 product-mcp/src/assets/ui/data-ui.js delete mode 100644 product-mcp/src/assets/ui/data.js delete mode 100644 product-mcp/src/assets/ui/index.html delete mode 100644 product-mcp/src/assets/ui/shared.jsx delete mode 100644 product-mcp/src/assets/ui/tweaks-panel.jsx delete mode 100644 product-mcp/src/assets/ui/vendor/babel.js delete mode 100644 product-mcp/src/assets/ui/vendor/react-dom.js delete mode 100644 product-mcp/src/assets/ui/vendor/react.js delete mode 100644 product-mcp/src/assets/view.html delete mode 100644 product-mcp/src/build_handler.rs delete mode 100644 product-mcp/src/codegen_handlers.rs delete mode 100644 product-mcp/src/decider_handlers.rs delete mode 100644 product-mcp/src/decider_handlers_tests.rs delete mode 100644 product-mcp/src/delivery_handlers.rs delete mode 100644 product-mcp/src/delivery_handlers_tests.rs delete mode 100644 product-mcp/src/deployable_unit_handlers.rs delete mode 100644 product-mcp/src/deployable_unit_handlers_tests.rs delete mode 100644 product-mcp/src/design_system_handlers.rs delete mode 100644 product-mcp/src/domain/args.rs delete mode 100644 product-mcp/src/domain/handlers.rs delete mode 100644 product-mcp/src/domain/mod.rs delete mode 100644 product-mcp/src/domain/registry.rs delete mode 100644 product-mcp/src/domain/registry_tests.rs delete mode 100644 product-mcp/src/domain/session_handlers.rs delete mode 100644 product-mcp/src/domain/stdio.rs delete mode 100644 product-mcp/src/domain/tools.json delete mode 100644 product-mcp/src/domain/tools.rs delete mode 100644 product-mcp/src/domain_handlers.rs delete mode 100644 product-mcp/src/domain_handlers_tests.rs delete mode 100644 product-mcp/src/framework_read_handlers.rs delete mode 100644 product-mcp/src/framework_read_handlers_tests.rs delete mode 100644 product-mcp/src/framework_scaffold_handlers.rs delete mode 100644 product-mcp/src/framework_scaffold_handlers_tests.rs delete mode 100644 product-mcp/src/framework_write_handlers.rs delete mode 100644 product-mcp/src/framework_write_handlers_tests.rs delete mode 100644 product-mcp/src/http.rs delete mode 100644 product-mcp/src/http_ui.rs delete mode 100644 product-mcp/src/http_view.rs delete mode 100644 product-mcp/src/instructions.rs delete mode 100644 product-mcp/src/lib.rs delete mode 100644 product-mcp/src/pf_mcp.rs delete mode 100644 product-mcp/src/pf_view.rs delete mode 100644 product-mcp/src/pf_view/conformance.rs delete mode 100644 product-mcp/src/pf_view/pf_build.rs delete mode 100644 product-mcp/src/pf_view/pf_flows.rs delete mode 100644 product-mcp/src/pf_view/pf_how.rs delete mode 100644 product-mcp/src/pf_view/pf_repo.rs delete mode 100644 product-mcp/src/pf_view/pf_ui.rs delete mode 100644 product-mcp/src/primitive_handlers.rs delete mode 100644 product-mcp/src/primitive_handlers_tests.rs delete mode 100644 product-mcp/src/product_handlers.rs delete mode 100644 product-mcp/src/projector_handlers.rs delete mode 100644 product-mcp/src/projector_handlers_tests.rs delete mode 100644 product-mcp/src/question_handlers.rs delete mode 100644 product-mcp/src/registry.rs delete mode 100644 product-mcp/src/scaffold.rs delete mode 100644 product-mcp/src/scope_handlers.rs delete mode 100644 product-mcp/src/stdio.rs delete mode 100644 product-mcp/src/tests.rs delete mode 100644 product-mcp/src/tools/build.rs delete mode 100644 product-mcp/src/tools/codegen.rs delete mode 100644 product-mcp/src/tools/decider.rs delete mode 100644 product-mcp/src/tools/delivery.rs delete mode 100644 product-mcp/src/tools/deployable_unit.rs delete mode 100644 product-mcp/src/tools/design_system.rs delete mode 100644 product-mcp/src/tools/domain.rs delete mode 100644 product-mcp/src/tools/how_author.rs delete mode 100644 product-mcp/src/tools/legacy_pf.rs delete mode 100644 product-mcp/src/tools/mod.rs delete mode 100644 product-mcp/src/tools/primitive.rs delete mode 100644 product-mcp/src/tools/product.rs delete mode 100644 product-mcp/src/tools/projector.rs delete mode 100644 product-mcp/src/tools/question.rs delete mode 100644 product-mcp/src/tools/scope.rs delete mode 100644 product-mcp/src/watch.rs delete mode 100644 product-mcp/src/what_declare.rs delete mode 100644 product-mcp/src/what_intercept.rs delete mode 100644 product-mcp/src/what_intercept_tests.rs delete mode 100644 product-mcp/src/workflow.rs delete mode 100644 product-mcp/src/workflow_questions.rs delete mode 100644 product-mcp/src/workflow_tests.rs delete mode 100644 rust-toolchain.toml delete mode 100644 schema/README.md delete mode 100644 schema/examples/delivery.example.yaml delete mode 100644 schema/examples/how-contract.example.yaml delete mode 100644 schema/examples/layout-model.example.yaml delete mode 100644 schema/examples/task-type-definition.example.yaml delete mode 100644 schema/examples/todo-product.ttl delete mode 100644 schema/examples/work-unit.example.yaml delete mode 100644 schema/json/authoring-scope/authoring-scope.schema.json delete mode 100644 schema/json/build-seam/example.md delete mode 100644 schema/json/build-seam/verdictevent.schema.json delete mode 100644 schema/json/build-seam/workunit.schema.json delete mode 100644 schema/json/codegen/example.md delete mode 100644 schema/json/codegen/file-plan.schema.json delete mode 100644 schema/json/codegen/manifest.schema.json delete mode 100644 schema/json/conformance/decision-request.schema.json delete mode 100644 schema/json/conformance/decision-response.schema.json delete mode 100644 schema/json/conformance/example.md delete mode 100644 schema/json/conformance/projection-request.schema.json delete mode 100644 schema/json/conformance/projection-response.schema.json delete mode 100644 schema/json/delivery.schema.json delete mode 100644 schema/json/how-contract.schema.json delete mode 100644 schema/json/layout-model.schema.json delete mode 100644 schema/json/task-type-definition.schema.json delete mode 100644 schema/json/work-unit.schema.json delete mode 100644 schema/ontology/product-framework.ttl delete mode 100644 schema/shapes/how.shacl.ttl delete mode 100644 schema/shapes/shapes.shacl.ttl delete mode 100644 schema/validate.py delete mode 100755 scripts/checks/file-length.sh delete mode 100755 scripts/checks/function-length.sh delete mode 100755 scripts/checks/module-structure.sh delete mode 100755 scripts/checks/session-e2e.sh delete mode 100755 scripts/checks/single-responsibility.sh delete mode 100755 scripts/checks/spec-flow-e2e.sh delete mode 100755 scripts/dispatch-to-spark.sh delete mode 100755 scripts/generate-docs.sh delete mode 100755 scripts/harness/author.sh delete mode 100755 scripts/harness/implement.sh delete mode 100755 scripts/implement-all.sh delete mode 100644 scripts/showcase-acme-how.sh delete mode 100644 scripts/showcase-acme.sh delete mode 100644 spec-cli/Cargo.toml delete mode 100644 spec-cli/src/commands.rs delete mode 100644 spec-cli/src/exit.rs delete mode 100644 spec-cli/src/main.rs delete mode 100644 spec-cli/src/render.rs delete mode 100644 spec-cli/src/verbs.rs delete mode 100644 spec-cli/src/verbs/acts.rs delete mode 100644 spec-cli/src/verbs/gate.rs delete mode 100644 spec-cli/src/verbs/git.rs delete mode 100644 spec-cli/src/verbs/host.rs delete mode 100644 spec-cli/src/verbs/host_tests.rs delete mode 100644 spec-cli/src/verbs/policy.rs delete mode 100644 spec-cli/src/verbs/ratify.rs delete mode 100644 spec-cli/src/verbs/record.rs delete mode 100644 spec-cli/src/verbs/runs.rs delete mode 100644 spec-cli/src/verbs/trust.rs delete mode 100644 spec-cli/src/verbs_tests.rs delete mode 100644 spec-cli/tests/boundaries.rs delete mode 100644 spec-cli/tests/flow.rs delete mode 100644 spec-cli/tests/launcher.rs delete mode 100644 spec-cli/tests/policy.rs delete mode 100644 spec-cli/tests/ratify.rs delete mode 100644 spec-cli/tests/runs.rs delete mode 100644 spec-cli/tests/signing.rs delete mode 100644 spec-core/Cargo.toml delete mode 100644 spec-core/src/act.rs delete mode 100644 spec-core/src/act_tests.rs delete mode 100644 spec-core/src/check.rs delete mode 100644 spec-core/src/check_tests.rs delete mode 100644 spec-core/src/closure.rs delete mode 100644 spec-core/src/closure_tests.rs delete mode 100644 spec-core/src/digest.rs delete mode 100644 spec-core/src/digest_tests.rs delete mode 100644 spec-core/src/gate.rs delete mode 100644 spec-core/src/gate_signing_tests.rs delete mode 100644 spec-core/src/gate_tests.rs delete mode 100644 spec-core/src/inventory.rs delete mode 100644 spec-core/src/inventory_tests.rs delete mode 100644 spec-core/src/lib.rs delete mode 100644 spec-core/src/map.rs delete mode 100644 spec-core/src/map_tests.rs delete mode 100644 spec-core/src/metrics.rs delete mode 100644 spec-core/src/metrics_tests.rs delete mode 100644 spec-core/src/policy.rs delete mode 100644 spec-core/src/policy_check.rs delete mode 100644 spec-core/src/policy_check_tests.rs delete mode 100644 spec-core/src/policy_tests.rs delete mode 100644 spec-core/src/ratify.rs delete mode 100644 spec-core/src/ratify_tests.rs delete mode 100644 spec-core/src/record.rs delete mode 100644 spec-core/src/record_tests.rs delete mode 100644 spec-core/src/signing.rs delete mode 100644 spec-core/src/signing_tests.rs delete mode 100644 spec-core/src/store.rs delete mode 100644 spec-core/src/store_tests.rs delete mode 100644 spec-flow/.gitignore delete mode 100644 spec-flow/Directory.Build.props delete mode 100644 spec-flow/Directory.Packages.props delete mode 100644 spec-flow/README.md delete mode 100644 spec-flow/SpecFlow.slnx delete mode 100644 spec-flow/src/SpecFlow.Cli/ActGround.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/ExitCodes.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/ImplementCommand.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/ImportCommand.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/JudgeCommand.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/McpCommand.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/Options.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/Program.cs delete mode 100644 spec-flow/src/SpecFlow.Cli/SpecFlow.Cli.csproj delete mode 100644 spec-flow/src/SpecFlow.Flow/AgentSliceBuilder.cs delete mode 100644 spec-flow/src/SpecFlow.Flow/Executors.cs delete mode 100644 spec-flow/src/SpecFlow.Flow/ImplementDriver.cs delete mode 100644 spec-flow/src/SpecFlow.Flow/ImplementWorkflow.cs delete mode 100644 spec-flow/src/SpecFlow.Flow/Messages.cs delete mode 100644 spec-flow/src/SpecFlow.Flow/SpecCli.cs delete mode 100644 spec-flow/src/SpecFlow.Flow/SpecFlow.Flow.csproj delete mode 100644 spec-flow/src/SpecFlow.Import/AttributeScan.cs delete mode 100644 spec-flow/src/SpecFlow.Import/CandidateBuild.cs delete mode 100644 spec-flow/src/SpecFlow.Import/EntryPointScan.cs delete mode 100644 spec-flow/src/SpecFlow.Import/Importer.cs delete mode 100644 spec-flow/src/SpecFlow.Import/Inventory.cs delete mode 100644 spec-flow/src/SpecFlow.Import/SpecFlow.Import.csproj delete mode 100644 spec-flow/src/SpecFlow.Import/SymbolScan.cs delete mode 100644 spec-flow/src/SpecFlow.Mcp/GovernedTools.cs delete mode 100644 spec-flow/src/SpecFlow.Mcp/Server.cs delete mode 100644 spec-flow/src/SpecFlow.Mcp/SpecFlow.Mcp.csproj delete mode 100644 spec-flow/src/SpecFlow.Mcp/SpecFlowOptions.cs delete mode 100644 spec-flow/src/SpecFlow.Mcp/SpecFlowTools.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/BoundaryTests.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/DraftParsingTests.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/GlobalUsings.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/ImplementWorkflowTests.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/ImportFixture.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/ImporterTests.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/McpHarness.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/McpSurfaceTests.cs delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/SpecFlow.Flow.Tests.csproj delete mode 100644 spec-flow/tests/SpecFlow.Flow.Tests/SpecRepo.cs delete mode 100644 spec-mcp/Cargo.toml delete mode 100644 spec-mcp/src/bin/spec-mcp.rs delete mode 100644 spec-mcp/src/dispatch.rs delete mode 100644 spec-mcp/src/dispatch_tests.rs delete mode 100644 spec-mcp/src/lib.rs delete mode 100644 spec-mcp/src/serve.rs delete mode 100644 spec-mcp/src/tools.rs delete mode 100644 spec-mcp/src/tools_tests.rs delete mode 100644 spec/how-contract.example.yaml delete mode 100644 spec/how-contract.schema.json delete mode 100644 spec/how-schema/README.md delete mode 100644 spec/how-schema/examples/delivery.example.yaml delete mode 100644 spec/how-schema/examples/how-contract.example.yaml delete mode 100644 spec/how-schema/examples/layout-model.example.yaml delete mode 100644 spec/how-schema/examples/task-type-definition.example.yaml delete mode 100644 spec/how-schema/examples/todo-product.ttl delete mode 100644 spec/how-schema/examples/work-unit.example.yaml delete mode 100644 spec/how-schema/json/delivery.schema.json delete mode 100644 spec/how-schema/json/how-contract.schema.json delete mode 100644 spec/how-schema/json/layout-model.schema.json delete mode 100644 spec/how-schema/json/task-type-definition.schema.json delete mode 100644 spec/how-schema/json/work-unit.schema.json delete mode 100644 spec/how-schema/ontology/product-framework.ttl delete mode 100644 spec/how-schema/shapes/how.shacl.ttl delete mode 100644 spec/how-schema/shapes/shapes.shacl.ttl delete mode 100644 spec/how-schema/tooling/product-author-domain.md delete mode 100644 spec/how-schema/tooling/product-author-domain.tools.json delete mode 100644 spec/how-schema/validate.py delete mode 100644 spec/how.shacl.ttl delete mode 100644 spec/product-author-domain.md delete mode 100644 spec/product-author-domain.tools.json delete mode 100644 spec/schema/README.md delete mode 100644 spec/schema/examples/delivery.example.yaml delete mode 100644 spec/schema/examples/layout-model.example.yaml delete mode 100644 spec/schema/examples/task-type-definition.example.yaml delete mode 100644 spec/schema/examples/todo-product.ttl delete mode 100644 spec/schema/examples/work-unit.example.yaml delete mode 100644 spec/schema/json/delivery.schema.json delete mode 100644 spec/schema/json/layout-model.schema.json delete mode 100644 spec/schema/json/task-type-definition.schema.json delete mode 100644 spec/schema/json/work-unit.schema.json delete mode 100644 spec/schema/ontology/product-framework.ttl delete mode 100644 spec/schema/shapes/shapes.shacl.ttl delete mode 100644 spec/schema/tooling/product-author-domain.md delete mode 100644 spec/schema/tooling/product-author-domain.tools.json delete mode 100644 spec/schema/validate.py delete mode 100644 xtask/Cargo.toml delete mode 100644 xtask/src/check_id.rs delete mode 100644 xtask/src/checks/ctx001_file_length.rs delete mode 100644 xtask/src/checks/ctx004_single_responsibility.rs delete mode 100644 xtask/src/checks/ctx005_function_length.rs delete mode 100644 xtask/src/checks/mod.rs delete mode 100644 xtask/src/conventions.rs delete mode 100644 xtask/src/diagnostic.rs delete mode 100644 xtask/src/drift.rs delete mode 100644 xtask/src/lib.rs delete mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 38415065..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[alias] -# Run every test binary in one pass — continues past failures and reports all -# results at the end. Use this instead of `cargo test`, which bails on the -# first failing binary and skips subsequent suites. -t = "test --no-fail-fast" - -# Workspace convention enforcement — see conventions/README.md. -xtask = "run --package xtask --release --" - -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" - -[target.aarch64-unknown-linux-musl] -rustflags = ["-C", "linker=rust-lld", "-C", "linker-flavor=ld.lld"] diff --git a/.claude/skills/feature-e2e/SKILL.md b/.claude/skills/feature-e2e/SKILL.md deleted file mode 100644 index dd831d95..00000000 --- a/.claude/skills/feature-e2e/SKILL.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -name: feature-e2e -description: Run an end-to-end test of a Product CLI feature in a fresh tmp directory. Build the release binary, init a clean .product/ project, then exercise every CLI command the feature adds — including the existing commands the feature touches — against the new repo. Use when the user asks to "do an end-to-end test of FT-XXX", "verify FT-XXX works in a fresh repo", "test FT-XXX end to end", or "shake out FT-XXX from scratch". ---- - -# Feature End-to-End Test - -This skill runs a real-world end-to-end shake-out of a Product CLI feature against a fresh, isolated repo, **driven entirely through the CLI**. The goal is to confirm that a feature works for a brand-new user — not just that the unit tests pass. - -The CLI is the canonical user-facing surface. The MCP server is a transport over the same logic. If the feature has an MCP-only surface (e.g. it adds an MCP tool with no CLI equivalent), test that too — but only after the CLI surface is fully exercised. - -## When to use - -- The user asks to do a "thorough end-to-end test" of a specific feature ID -- A feature has just been implemented and needs human-style validation -- You suspect drift between PRD/ADR text and the actual binary behaviour - -## What to do - -### 1. Read the feature spec first - -Open `docs/features/FT-XXX-*.md`. Extract: - -- The **acceptance criteria** list — these become your test plan -- The **CLI surface** — every subcommand and flag the feature adds, plus existing commands whose behaviour the feature changes -- The **error codes** — each `EXX` has a path that should fire -- Any **config keys** the feature reads (`product.toml` sections) -- Any **resolution paths** the feature uses (e.g. repo → user → built-in) - -### 2. Use the existing release binary - -```bash -PRODUCT=/abs/path/to/repo/target/release/product -[ -x "$PRODUCT" ] || cargo build --release -``` - -Don't rebuild if a release binary already exists — the bottleneck is the test work, not compilation. - -### 3. Create a clean tmp scratch directory - -```bash -rm -rf /tmp/-e2e-test -mkdir -p /tmp/-e2e-test -cd /tmp/-e2e-test -``` - -Always start clean — the resolution order for templates / config / `.product/` directory walks up the filesystem, so a stale parent state can leak into the test. - -### 4. Initialise a fresh project via the CLI - -```bash -$PRODUCT init -y --name ft-xxx-e2e-test -``` - -Defaults to the canonical `.product/` layout (ADR-048). Pass `--legacy-layout` only if the feature specifically needs the pre-FT-057 root-based layout. - -Verify init landed correctly: - -```bash -ls -la .product/ # config.toml + products// (the product home) -$PRODUCT --help | head -40 # confirms binary loads against the fresh repo -``` - -### 5. Seed the graph via the CLI - -A typical e2e run needs at least one feature, one ADR, and one TC, linked together: - -```bash -$PRODUCT feature new "Hello World" -$PRODUCT adr new "Use TOML for config" -$PRODUCT test new "feature renders" -$PRODUCT feature link FT-001 --adr ADR-001 --yes -$PRODUCT feature link FT-001 --test TC-001 --yes -``` - -After seeding, sanity-check via the CLI's read commands: - -```bash -$PRODUCT feature list -$PRODUCT feature show FT-001 -$PRODUCT adr list -$PRODUCT test show TC-001 -$PRODUCT graph check -``` - -### 6. Exercise every CLI command the feature touches - -This is the core of the skill. Walk every acceptance criterion **and** every CLI command listed in the feature spec. Capture stdout, stderr, and exit code separately for each invocation: - -```bash -$PRODUCT [args] > /tmp/out.txt 2> /tmp/err.txt -echo "exit=$? stdout=$(wc -c ; echo "exit=$?"; cat /tmp/err.txt -``` - -Confirm exit codes follow ADR-013 (1 = error, 2 = warning). - -### 7. Test repo-local / user-level resolution (when applicable) - -Many features resolve files in repo → user → built-in order (templates, prompts, config overlays). Test all three by: - -- Drop a file under `.product//` and confirm it shadows the built-in -- Drop the same name under `~/.product//` and confirm the repo file wins -- Remove the repo override and confirm the user file then wins - -### 8. Test the MCP surface only when the feature is MCP-specific - -The MCP server is a transport over the same logic — it's exercised by every CLI handler under the hood. Only test it directly when: - -- The feature **adds** an MCP tool (e.g. FT-021, FT-046, FT-050, FT-059, FT-062) -- The feature **changes** an MCP tool's input schema or output envelope -- The feature is about MCP parity, write-tool gating, or transport behaviour - -When you do, the JSON-RPC sequence is `initialize` → `notifications/initialized` → `tools/call`. The response is double-wrapped — parse twice: - -```bash -{ - printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"e2e","version":"0"}}}' - printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}' - printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"","arguments":{...}}}' - sleep 0.3 -} | $PRODUCT mcp 2>/tmp/mcp.err -``` - -Init with `--write-tools` if you need to call a write tool, and use `python3 -c "import json,sys; ..."` to walk both wrapper layers. - -### 9. Run the health gates against the seeded repo - -After exercising the feature's commands, run the standard health gates one more time to confirm no command corrupted the graph: - -```bash -$PRODUCT graph check -$PRODUCT gap check -$PRODUCT drift check -$PRODUCT preflight # if defined for this repo -``` - -### 10. Report back - -For every acceptance criterion in the spec, mark pass/fail. Call out any drift between spec and behaviour — these are the highest-value findings of an e2e run. - -## Gotchas captured from prior runs - -- **Subcommand naming.** The CLI is `product test new`, not `product tc new`. The internal IDs use `TC-XXX` but the subcommand family is `test`. -- **`feature link` in non-TTY needs `--yes`.** Without it, the inferred-transitive-TC prompt blocks waiting on stdin. -- **`product init` is not idempotent without `--force`.** Re-running over an existing config errors out — clean the tmp dir first. -- **Stderr deprecation notes are easy to miss.** Several flags (`--for-llm`) emit deprecation notes to stderr while still producing valid stdout. Always redirect stderr to a separate file. -- **Exit code semantics.** Per ADR-013: 0 = success, 1 = hard error, 2 = warning-only state. Some commands (`gap check`, `dep check`, `preflight`) deliberately exit 2 — that is not a test failure. -- **`--format json` is supported widely but not universally.** Check the help output before parsing JSON; some commands only emit text. -- **No-flag default vs `--target human`.** Per FT-063 these should produce byte-identical Markdown. If they don't, that is a drift to flag. -- **Built-in templates ship embedded.** They live at `src/context/template/builtin/*.toml` but are pulled in via `include_str!` — there is no `templates/` directory at the repo root or anywhere on disk in a fresh install. -- **Resolution paths in `templates --where`.** Built-ins show as `(built-in)` (no path); repo/user templates show their absolute path. -- **MCP tool input arg name is `id`** (not `feature_id`). PRD examples may show `feature_id` — that is doc drift; the binary reads `id`. -- **MCP envelope is double-wrapped.** Tool results come as `{"result":{"content":[{"text":"","type":"text"}]}}`. You must `json.loads()` twice when validating tool output. - -## Skill output - -End each run with: - -1. A pass/fail table covering every acceptance criterion in the spec -2. A per-command results table — every CLI command exercised, exit code, brief notes -3. A drift list — every place spec says X but the binary does Y (cite file paths, exact strings) -4. The path to the scratch directory so the user can poke around (e.g. `/tmp/ft063-e2e-test/`) diff --git a/.claude/skills/product-build/SKILL.md b/.claude/skills/product-build/SKILL.md deleted file mode 100644 index 5358045e..00000000 --- a/.claude/skills/product-build/SKILL.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -name: product-build -description: > - Guide the Build phase of a product session — partition the What into shippable - slices, define deliverables with checkable acceptance (§7), dispatch cells into - work units, run the autonomous build, and finalize. Use when the session is in - the Build phase or the user says "create a slice", "add acceptance", "build the - deliverable", "dispatch", "run the build", or "finalize the session". ---- - -# Product Session — the Build phase - -Delivery is **partitioning the What graph into shippable slices**; "done" is a -**computed predicate**, not a judgement (§7). Realise each slice through its work -units, gate it with verifications, and finalize. - -**Precondition:** `product_workflow_status` → `phase` must be `build`. If not, use -**product-session**. - -## The question script - -1. **Slice** — which section of the event model ships? Anchor it on the relevant - nodes. → `product_slice_new id= anchors=[…] depth=N`; inspect with - `product_slice_show` / `product_slice_context`. -2. **Deliverable** — `product_deliverable_new id= slice= - acceptance=[": ", …]`. - **§7.2 (the gotcha):** each criterion is the literal form `id: statement`, and - the statement must be a **checkable predicate, not a judgement** — e.g. - `"journal-created: .product/sessions//workflow.json exists after start"`, not - "the start works well". Non-predicate criteria are rejected. -3. **Work units must already exist.** `product_build_run` consumes the §5 work - units for the slice; if none target it, it falls back to *unrelated* units. - Work units are produced in the **How phase** by `product_cell_dispatch` (bind - the cell's slots to the slice's entities) — and that tool **freezes once you're - in Build** (phases are forward-only). So dispatch every unit your deliverables - will need *before* advancing from How. If you reach Build and one is missing, - you cannot dispatch here — finalize and fix in a fresh pass. -4. **Dry-run the build first** — `product_build_run deliverable= dry_run=true`. - It returns the assembled SPMC context, the worker + parallel run plan, the - verify plan, and the gate status — **with no worker dispatched**. Review it - before spending a real run. -5. **Build it** — two ways: - - **In-process worker:** `product_build_run deliverable=` dispatches the - worker per the role bindings, writes artifacts, runs the gates. - - **Hand it to a Claude Code session:** `product_build_emit deliverable=` - returns a self-contained SPMC prompt (frozen What/How/Behaviour/Acceptance + - the work-unit build plan in order + the verify commands). Save it and run - `claude -p "$(cat )"` from the repo root, or `product build - --emit-spmc` to write `.product/build/.spmc.md` directly. The agent builds - every artifact at its declared path and makes the verify commands pass. -6. **Acceptance verdicts** — bind a runner so the build auto-verifies a criterion: - `product_deliverable_runner id= criterion= runner=cargo-test args=""` (or `runner=shell args=""`). Then the §6 verify step runs it - and records the verdict. Without a runner, record manually with - `product_deliverable_accept id= criterion= status=passing|failing`. - `product_deliverable_done` computes whether the deliverable is done (§7.2). -7. **Release (optional)** — `product_release_new` groups deliverables; - `product_release_done` checks the cut is closed (no dangling dependency). -8. **Finalize** — `product_session_finalize` validates the What, stamps - provenance, and closes the session. Everything authored in the session (the - What graph, How, and delivery artifacts alike) is already in canonical - `.product/` — finalize is the conformance gate, not a promotion. - -## The gate - -The deliverable's **done predicate** (§7.2): every concept in the slice's footprint -is conformant, every cited verification is green, and the agreed acceptance passes. -The dry-run's gate status shows domain conformance + each acceptance criterion's -state (pending / passing). - -## Worked micro-example - -`slice_new id=session-start anchors=[cmd-start-session, ev-session-started, -e-session] depth=1` → `deliverable_new id=session-start slice=session-start -acceptance=["starts-in-what: a started session is in phase what immediately after -start"]` → `build_run deliverable=session-start dry_run=true` → review → real run → -`session_finalize`. - -Guardrails: **always dry-run before a real build** (it dispatches a worker and -spends tokens). Plus the usual: locked session, `.product/products/` is user data — see -**product-session**. diff --git a/.claude/skills/product-how/SKILL.md b/.claude/skills/product-how/SKILL.md deleted file mode 100644 index 649440c4..00000000 --- a/.claude/skills/product-how/SKILL.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: product-how -description: > - Guide authoring the How — the §4 architecture contract — inside a product - session's How phase: the Why cascade (decisions → principles → patterns), the - application/infrastructure contracts, interfaces, plus archetypes, cells - (task-types), and work units. Use when the session is in the How phase or the - user says "specify the how", "add a principle/pattern/decision", "set the app - contract", "do we need a new archetype", "scaffold a cell/work unit", or - "dispatch a cell". ---- - -# Product Session — the How phase - -The How is where the engineer takes the frozen What and decides the **realisation**: -the Why cascade, the two contracts, the layout, interfaces — realised through -**archetypes** (a reusable pre-filled How), **cells** (task-types), and **work -units** (§4–§5). This is authoring, not inspection — drive the `*_add` / `*_init` -tools, not just `*_show`. - -**Precondition:** `product_workflow_status` → `phase` must be `how`. If not, use -**product-session**. - -## The question script - -1. **Scaffold** (if no contract yet) — `product_how_init` (keyed to an archetype). -2. **Foundational decisions (§4.1)** — what choices shape everything? Each carries - rationale and *licenses* principles. → `product_how_add element=decision` - (`decision`, `rationale`, `licenses[]`). -3. **Principles** — what checkable rules do those decisions license? - → `product_how_add element=principle` (`statement`, `licensed_by[]`). -4. **Patterns** — what concrete code shapes realise the principles? (a work unit - emits a pattern) → `product_how_add element=pattern` (`shape`, `realizes[]`). -5. **Application contract (§4.2)** — language, layering, cross-cutting; plus - *checkable* statements. → `product_how_set target=app-contract`, then - `product_how_add element=app-statement`. -6. **Infrastructure contract** — concrete frozen resources that satisfy the app - contract. → `product_how_set target=infra-contract`, `element=resource`. -7. **Interfaces (§4.4)** — published surfaces derived from the domain. - → `element=interface`. -8. **Refine** — `product_how_edit element= id= …` patches a Why-cascade - element (keeps unmentioned fields); `product_how_rm id=` removes one. - -### Archetype, cells, work units (§4.3 / §5) - -9. **Archetype decision** — does an existing one fit? `product_archetype_list`, - `product_archetype_show `, `product_archetype_validate`, - `product_archetype_check` (layout vs the actual tree). If none fits: - `product_archetype_init ` (scaffolds How + layout + an example cell). -10. **Cells** — `product_cell_init [archetype]`; inspect with - `product_cell_show` / `product_cell_validate`. -11. **Work units** — `product_work_unit_init` then `product_work_unit_edit` (patch - prompt/model/applies/…); **or generate real ones** with - `product_cell_dispatch` binding slots to entities (e.g. `binds={entity: Order}`) - — this is how delivery work units are produced from a cell + the What graph. -12. **Workers** — `product_worker_list` / `product_worker_resolve` confirm the - capability + role bindings a dispatch will target (read-only). - -## The gate - -`product_how_validate` → structure + conformance + trace-truth `{ ok, violations }`. -For an archetype, `product_archetype_check` confirms the layout holds against the -repo tree. Every `*_add` / `*_set` / `*_edit` / `*_rm` also re-validates the whole -contract in-loop and returns its `violations`. - -When set, advance: `product_workflow_advance` → **Build** (use **product-build**). -**Note:** How *writes freeze* once you enter Build — finish the architecture here. -In particular, **`product_cell_dispatch` is a How tool**: dispatch the work units -your Build deliverables will consume *before* advancing — you cannot dispatch in -Build, and `product_build_run` falls back to unrelated units if a slice has none. - -## Worked micro-example - -`how_init archetype=demo` → `add decision (id=d-lang, decision="Use Rust", -rationale="safety", licenses=[zero-unwrap])` → `add principle (id=zero-unwrap, -statement="no unwrap in non-test code", licensed_by=[d-lang])` → `add pattern -(id=slice-adapter, shape="pure slice + thin adapter", realizes=[zero-unwrap])` → -`set app-contract (id=demo-app, language=Rust)` → `how_validate`. - -Guardrails: see **product-session**. diff --git a/.claude/skills/product-session-e2e/SKILL.md b/.claude/skills/product-session-e2e/SKILL.md deleted file mode 100644 index dbde1354..00000000 --- a/.claude/skills/product-session-e2e/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: product-session-e2e -description: > - Shake out a full What→How→Build session end-to-end in a fresh `init --demo` - repo, driving each phase skill's worked example through the CLI and asserting - every phase gate. Use when the user asks to "run the session e2e", "verify the - phase skills", "shake out a session end to end", or after changing the - domain/how/build tool surface to confirm the skills still hold. ---- - -# Product Session — end-to-end shake-out - -Confirms the **product-what / product-how / product-build** skills still work as a -set against a brand-new repo, driven entirely through the CLI (the canonical -surface; the MCP tools are a transport over the same logic). Run it whenever the -tool surface changes. - -## Run it - -```bash -bash scripts/checks/session-e2e.sh # uses target/debug/product -PRODUCT=/abs/path/to/product bash scripts/checks/session-e2e.sh # pin a binary -``` - -The script is self-contained: it makes a fresh tmp repo, runs the sequence below, -prints `PASS`/`FAIL` per step, and exits non-zero if any step or assertion fails. - -## What it drives (each phase skill's worked example) - -1. **init** — `product init --demo --yes --name bookstore` (seeds the What). -2. **product-what** — `domain validate` → `domain new value-object` → re-validate. -3. **product-how** — `how init` → `how add decision|principle|pattern` - (the principle is `--enforced-by` so it earns its place, §4.1) → - `how set app-contract` → `how validate` → `archetype init` → `cell init` → - `cell dispatch --bind entity=Order` (produces a real work unit). -4. **product-build** — `slice new` → `deliverable new` (acceptance as - `id: statement`, §7.2) → `build --dry-run`. -5. **assertions** — the build run-plan uses the *dispatched* work unit - (`handler-order`, not a fallback) and the gate reports domain conformance. - -## When it fails - -Each `FAIL` prints the offending command's last lines. Common causes: -- A renamed/removed tool or flag → update the matching phase skill **and** this - script together (they must stay in lockstep). -- A new conformance rule (e.g. §4.1 earn-their-place) the worked example trips → - fix the example to satisfy it, then mirror the fix in the phase skill. - -## Note on session gating - -This runs the CLI, which has **no phase lock**, so it exercises the underlying -capability rather than the gated ordering. Inside a real `product mcp --workflow` -session the phases gate the tools — in particular **cell dispatch / work-unit -authoring are How-phase** and freeze in Build, so a session must dispatch work -units before advancing (see **product-how** / **product-build**). diff --git a/.claude/skills/product-session/SKILL.md b/.claude/skills/product-session/SKILL.md deleted file mode 100644 index 1de7d48c..00000000 --- a/.claude/skills/product-session/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: product-session -description: > - Launch, resume, and drive a phase-gated What→How→Build workshop session over the - Product framework graph, and route to the right phase skill. Use when the user - says "start a product session", "run a workshop session", "product session - start", "product mcp --workflow", "resume the session", "advance the phase", or - "finalize the session". Owns start / status / advance / finalize and the - locked-session guardrails; hands the actual authoring to product-what, - product-how, and product-build. ---- - -# Product Session — the launcher - -A **session** is a phase-gated facilitation run that writes the **canonical -`.product/` graph directly** (the session record is just a `workflow.json` -journal). It starts in **What**, advances strictly forward **What → How → -Build** (optionally capped), and on **finalize** validates the graph and closes -the session. Every write lands in canonical as it happens — there is no draft -or rollback. The phase **gates which tools exist** — an out-of-phase tool call -errors. - -## Starting a session - -```bash -product session start [] [--until what|how|build] [--cli claude|copilot] -product session start --no-launch # scaffold only; prints the run command -``` - -`--no-launch` prints: `product mcp --workflow --session --repo ` — the -phase-gated MCP server the agent drives. Also: `product session list`, -`product session show `, `product session resume ` (relaunch + reprint the -live-view URL). - -## Always orient first - -Before any authoring, call **`product_workflow_status`**. It returns `phase`, -`until`, `availableTools`, `finalized`, and a `hint`. Route on `phase`: - -| phase | hand off to | authoring families | -|---|---|---| -| `what` | **product-what** | `product_domain_*`, `product_decider_*`, `product_projector_*`, `product_primitive_*` | -| `how` | **product-how** | `product_how_*`, `product_archetype_*`, `product_cell_*`, `product_work_unit_*`, `product_worker_*` | -| `build` | **product-build** | `product_slice_*`, `product_deliverable_*`, `product_release_*`, `product_build_run` | - -## Advancing - -`product_workflow_advance` moves to the next phase (or jump with `to`). **Only -advance when the current phase's gate is green** — the phase skill says how to -check. Advancing freezes earlier-phase *writes* (e.g. you cannot edit the How once -in Build), so finish a phase before leaving it. - -## Finalizing - -`product_session_finalize` validates the What and, if conformant, stamps -provenance and marks the session complete. Writes have already landed in the -canonical `.product/` throughout the session — finalize is the conformance gate -and completion record, not a promotion step. - -## Guardrails (apply in every phase) - -- **Locked session.** Do **not** make source-code edits or hand-edit files while a - session is running. Finish the graph through the MCP/CLI tools; do code changes - after you exit/finalize. -- **User data.** Never `rm` or bulk-`git add` `.product/products/` — it's the - captured What and is the user's. -- **Phase gating is real.** If a tool "belongs to another phase", advance first; - don't fight the gate. - -## Reference - -The standard is `docs/product-framework-open.md` (§3 What, §4 How, §5–7 Build). -The live Event-Modeling view is served by the workflow server (URL printed on -start / `session show`). diff --git a/.claude/skills/product-what/SKILL.md b/.claude/skills/product-what/SKILL.md deleted file mode 100644 index 36bbe39e..00000000 --- a/.claude/skills/product-what/SKILL.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -name: product-what -description: > - Guide authoring the What — the domain model (§3.1) and event model (§3.2) — - inside a product session's What phase: bounded contexts, entities, value - objects, commands, events, read-models, triggers, systems, flows, plus Deciders - (§3.3) and Projectors (§3.4). Use when the session is in the What phase or the - user says "model the domain", "add an entity/command/event", "add a system", - "derive the decider", or "validate the what". ---- - -# Product Session — the What phase - -Author the **What**: one graph with two lanes — **domain** (structure, §3.1) and -**event** (behaviour, §3.2) — with bridge edges crossing between them. - -**Author in dependency order: domains → systems → flows.** The domain is the -hardest part and everything references it, so get it right first; then name the -**systems** that reference those domains; only then the **flows** — because a -flow belongs to exactly one system (§3.2.5) and cannot exist without it. - -**Precondition:** call `product_workflow_status`; `phase` must be `what`. If not, -use **product-session** to advance/route. - -## The question script (author in this order) - -1. **Bounded contexts** — what areas of meaning are there? - → `product_domain_new kind=context` -2. **Aggregates / entities** — what are the aggregate roots? their identity? - → `kind=entity` (`is_aggregate_root`, `identity`, `context`); describe them with - `kind=value-object`. -3. **Commands** — per aggregate, what commands target it, and what does each emit? - → `kind=command` (`targets`, `emits`). -4. **Events** — what does each command emit, and what entity does each event - *change*? Events are thin (just `changes` + `context`). - → `kind=event` (`changes`). -5. **Read-models** — what views project which entities/events? - → `kind=read-model` (`projects`). -6. **Triggers (§3.2.0)** — what's the *source* (user | external | automated) - issuing each command? → `kind=trigger` (`source`, `issues`). -7. **Systems (§3.2.5)** — name the surfaces that reference the domains (an app, - website, service, or CLI), *before* any flow — a flow belongs to exactly one - system. → `kind=system` (`system_kind`, `purpose`, `references_domain`). -8. **Flows (§3.2.4)** — chain trigger → command → event → read-model into named - flows and assign **system ownership** to a system from step 7. → `kind=flow` - (`steps`, `system`). -9. **Deciders (§3.3)** — make behaviour executable: - `product_decider_derive ` → `product_decider_validate ` → - `product_decider_simulate`. -10. **Projectors (§3.4)** — the read-model peer: - `product_projector_derive` → `product_projector_validate`. - -Inspect anytime: `product_domain_list`, `product_domain_show `, -`product_domain_context ` (assembles a focused bundle). Fix with -`product_domain_edit` / `product_domain_rm`. Relations (`kind=relation`) need a -`rationale`. - -## Boundary governance (only when the repo has a `.ddd/` store) - -Boundary-forming writes are **rejected until declared**, and the rejected edit -is rolled back — nothing lands. The table below is generated from the policy -rows the interceptor actually applies: - - -| rule | kind | surface when | the claim the row states | -|---|---|---|---| -| `pf-add-system` | system | always | a system (§3.2.5) is a deployment, team and contract boundary | -| `pf-add-context-mapping` | context-mapping | always | a context mapping (§3.1) declares how two models translate — it is the seam itself | -| `pf-journey-crossing` | journey-crossing | always | a journey crossing a system boundary (§3.0.1) must be absorbed by a Translation | -| `pf-translation-view` | read-model | a Translation publishes it | the View a Translation watches is the contract the other side reads | -| `pf-add-event` | event | a Translation publishes it | an event a Translation carries across a boundary is a published contract | -| `pf-event-signature` | event, command | a Translation publishes it | a payload field added or removed changes a wire contract consumers depend on | -| `pf-add-command` | command | a Translation publishes it | a command a Translation issues is an entry point exposed across a boundary | -| `pf-quality-demand` | quality-demand | always | a quality demand (§3.6) binds runtime behaviour or a How element | - - -Everything else in the question script above is internal elaboration and is -never gated. In particular an **event or command is internal until a -Translation carries it**, so steps 3–4 flow freely; step 7 (systems) and any -Translation trigger in step 6 are where you will meet the gate. - -On `"status": "rejected"`: - -1. Read `demands[].surface` — it names the element, the rule, and the claim - that rule states. -2. Call `product_what_declare` with the supplied `template`, adding - `verdict_knowledge`: one sentence on **what this boundary lets the other - side learn**. It is deliberately blank in the template — that judgement is - the thing being asked for. Empty files with a warning. -3. Retry the **identical** call. It applies. - -Do not rename, split, or re-route the node to get around a rejection, and do -not skip ahead — the demand is the point, not an obstacle. `ddd what` lists -undeclared boundaries anytime; `ddd why ` resolves one. - -## The gate - -`product_domain_validate` runs the per-node §3.1/§3.2 shapes → `{ ok, violations }`. -The **strict** graph-level checks (flow ownership §3.2.5, the Command pattern -§3.2.0, view consumption §3.4, the unreifiable seam §4.5) run on the CLI as -`product domain validate --strict` — run that before finalize for full coverage. - -When `validate` is green, advance: `product_workflow_advance` → **How** (use -**product-how**). - -## Worked micro-example - -A one-flow slice, in order — domain first, then the system, then the flow: -`entity e-thing (is_aggregate_root)` → `command cmd-x (targets=e-thing, -emits=ev-x)` → `event ev-x (changes=e-thing)` → `trigger t-x (source=user, -issues=cmd-x)` → `system sys-a (system_kind=service)` → `flow f-x (steps=[cmd-x, -ev-x], system=sys-a)`. Six `product_domain_new` calls, then -`product_domain_validate`. - -Guardrails: see **product-session** (locked session; `.product/products/` is user data). diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md deleted file mode 100644 index 31b9af62..00000000 --- a/.claude/skills/release/SKILL.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: release -description: Cut a new release of the Product CLI by bumping the workspace version in lockstep, committing, tagging `vX.Y.Z`, and pushing the tag to trigger the cargo-dist release workflow. Use when the user asks to "cut a release", "release version X", "ship 0.X.Y", "tag a release", "publish a release", or "bump and tag for release". ---- - -# Release the Product CLI - -The release pipeline (`.github/workflows/release.yml`, autogenerated by `dist -generate` — don't hand-edit it) is driven by `cargo-dist`. It fires when a git -tag matching `**[0-9]+.[0-9]+.[0-9]+*` is pushed and builds the GitHub Release -artifacts from the tagged commit. This is a **Cargo workspace**, and cargo-dist -runs in lockstep mode: pushing `vX.Y.Z` releases every dist-able package in the -workspace at version `X.Y.Z` (it errors if a dist-able package's version doesn't -match the tag). The binary is `product`, in the `product-cli` package. - -## The version sources — keep them in lockstep at `X.Y.Z` - -There is **no root version** — root `Cargo.toml` is `[workspace]`-only. The -version is spread across these files, and they must all agree: - -| File | Field | Why | -|---|---|---| -| `product-core/Cargo.toml` | line 3 `version` | published lib version | -| `product-mcp/Cargo.toml` | line 3 `version` | published lib version | -| `product-cli/Cargo.toml` | line 3 `version` | the dist-able binary package — cargo-dist matches the tag against this | -| `.product/config.toml` | line 2 `version` | Product config (mirrors the release) | -| `product-cli/Cargo.toml` | lines 20–21 internal `version` on the `product-core`/`product-mcp` path deps | **lockstep target** — see below | -| `product-mcp/Cargo.toml` | line 20 internal `version` on the `product-core` path dep | **lockstep target** — see below | - -**`xtask/Cargo.toml` stays at `0.1.0`** — it's an internal build-helper, not -dist-able, not released. Do **not** bump it. - -**The internal path-dep versions matter.** Since v0.3.0, the intra-workspace -`path` deps carry an explicit `version` (e.g. -`product-core = { path = "../product-core", version = "0.3.0" }`). This is what -lets `cargo deny check`'s `wildcards = "deny"` ban pass on the publishable -crates. If you bump the package versions but forget these, **Product CI's -cargo-deny step goes red** (and a real `cargo publish` would mismatch). They are -the easiest thing to miss — there's no test that pins them, only the CI gate. - -> **`server.json` / TC-776 are gone.** Older versions of this skill bumped a -> `server.json` MCP-registry manifest enforced by a `tc_776_*` test. Both were -> removed. The `.github/workflows/publish-mcp.yml` workflow still exists but is -> currently **inert** — it expects the deleted `server.json`. Ignore it for a -> normal release (cargo-dist ships the binaries regardless). If the user wants -> MCP-registry publishing back, that's a separate task (restore `server.json` + -> its schema check), not part of cutting a release. - -## When to use - -- The user asks to release a specific version (e.g. "release 0.3.1") -- The user asks to "tag the release" after a version-bump commit has already landed -- A release pipeline didn't run because the tag was forgotten - -## What to do - -### 1. Determine the target version - -Ask the user if not specified. Otherwise infer from the current version: - -- **Patch** (default) — `0.3.0` → `0.3.1` for bugfixes / small additions -- **Minor** — `0.3.x` → `0.4.0` for new features / non-breaking surface changes -- **Major** — reserve for breaking changes; confirm with the user first - -Read the current version: - -```bash -grep -E '^version = ' product-cli/Cargo.toml | head -1 -``` - -### 2. Verify preconditions - -```bash -git status -sb # working tree clean, on main -git fetch origin && git status -sb # up to date with origin/main -git tag -l "v" # must NOT already exist -``` - -If any check fails, surface it rather than papering over it. A dirty tree needs -a decision; an existing tag means the release was already attempted — find out -why before re-running. - -### 3. Decide: bump-and-tag, or tag-only? - -```bash -git log -1 --format='%h %s' -grep -E '^version = ' product-cli/Cargo.toml -``` - -- **If the versions already equal the target** and there's an untagged "Bump - version" commit at HEAD — skip to step 5 (tag-only). -- **Otherwise** — do step 4. - -### 4. Bump and commit - -Set `NEW` and update every lockstep source at once: - -```bash -NEW=X.Y.Z -# package versions (NOT xtask) + Product config (line 3 / line 2 each) -sed -i "s/^version = .*/version = \"$NEW\"/" \ - product-core/Cargo.toml product-mcp/Cargo.toml product-cli/Cargo.toml .product/config.toml -# internal path-dep versions — keep lockstep so cargo-deny's wildcard ban passes -# (use # as the sed delimiter; the regex itself contains a | alternation) -sed -i -E "s#(path = \"\.\./product-(core|mcp)\", version = \")[0-9.]+#\1$NEW#" \ - product-cli/Cargo.toml product-mcp/Cargo.toml -``` - -Verify all sources agree, the build is clean, and the supply-chain gate passes -(this is the check that catches a missed path-dep version): - -```bash -grep -nE 'version = "' \ - product-core/Cargo.toml product-mcp/Cargo.toml product-cli/Cargo.toml .product/config.toml -cargo build --workspace # refreshes Cargo.lock -cargo deny check # must report: bans ok (wildcard/path-dep lockstep) -cargo t # full suite green (the `t` alias = test --no-fail-fast) -``` - -Commit (push the commit **before** the tag — Actions checks out the tagged ref -from the remote, so an unpushed commit isn't fetchable): - -```bash -git add product-core/Cargo.toml product-mcp/Cargo.toml product-cli/Cargo.toml .product/config.toml Cargo.lock -git commit -m "$(cat <<'EOF' -Bump version to X.Y.Z - -Co-Authored-By: Claude Opus 4.8 (1M context) -EOF -)" -git push origin main -``` - -### 5. Tag and push - -```bash -git tag vX.Y.Z # omit the SHA to tag HEAD -git push origin vX.Y.Z -``` - -The tag must be `v`-prefixed to match the workflow trigger and the existing tag -convention (`v0.1.0` … `v0.3.0`). `git push` does **not** push tags — push it -explicitly (avoid `--tags`, which can shove stale local tags). - -### 6. Verify the pipeline started - -```bash -gh run list --workflow=release.yml --limit 1 # should show your tag, queued/in_progress -gh run watch # follow to completion (~9 min for v0.2.0/v0.3.0) -``` - -Confirm the GitHub Release object was created: - -```bash -gh release view vX.Y.Z --json tagName,isDraft,assets \ - -q '.tagName + " draft=" + (.isDraft|tostring) + " assets=" + (.assets|length|tostring)' -``` - -If `release.yml` fails immediately in `dist plan` with a version mismatch, a -dist-able package's `Cargo.toml` on the tagged commit doesn't equal the tag. -Fix: bump it, commit, **delete the tag** (`git tag -d vX.Y.Z && git push origin -:refs/tags/vX.Y.Z`), and re-tag on the new commit — but only if the workflow -hasn't already published the Release (once it has, deleting the tag won't delete -the Release; use `gh release delete`). - -## Gotchas - -- **Dist-able `Cargo.toml` version must equal the tag.** cargo-dist reads it - from the tagged commit and refuses to plan otherwise — the most common failure - mode (the 0.1.1 redo, commit `9c9f828`). -- **Six-place lockstep.** Three package `version`s + `.product/config.toml` + - the three internal path-dep `version`s. Nothing tests the path-dep versions - except Product CI's cargo-deny step — bump them or CI goes red. -- **Don't bump `xtask`.** It stays `0.1.0`; it's not released. -- **Push the commit before the tag.** An unpushed commit isn't fetchable by the - tag checkout. -- **`v0.1.0+` tags are nightlies**, not releases — don't reuse that format - for an intentional release. Plain `vX.Y.Z` only. -- **No CHANGELOG.md.** cargo-dist generates the release body from commit - messages; there's no manual changelog step. -- **`publish-mcp.yml` is currently inert** (the `server.json` it needs was - removed). It won't block a release; don't chase its failures as part of - cutting one. - -## Skill output - -After the tag is pushed, report: - -1. The version released (`vX.Y.Z`) -2. The commit SHA the tag points at -3. The status/URL of the running workflow (`gh run list --workflow=release.yml --limit 1`) -4. Anything unusual (version mismatch, dirty tree, a red gate) — don't hide - problems in the success message diff --git a/.ddd/claims/.gitkeep b/.ddd/claims/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/.ddd/claims/DDD-adapter-01.yaml b/.ddd/claims/DDD-adapter-01.yaml deleted file mode 100644 index beb63023..00000000 --- a/.ddd/claims/DDD-adapter-01.yaml +++ /dev/null @@ -1,61 +0,0 @@ -format: 2 -id: DDD-adapter-01 -statement: > - Per-language contract-surface definitions expressed as adapter policy - tables are falsifiable against where boundary defects actually occur; - wrong rows get fixed in the adapter, never in the core. -status: established -evidence: > - M7 (2026-08-10) was the scheduled falsification attempt at this claim's - own falsifier: language four, the HTML+CSS pair — an artifact class - whose contract surface (design tokens, class vocabulary, @layer order, - stylesheet links) is not symbol events, whose producer is not an LSP - host, and whose governed unit is two files. Every piece of pair - knowledge localised to the adapter module (ddd-lsp/src/adapter/ - htmlcss*): token typing, class membership, layer grading as visibility, - link rewiring, ownership and decorative thresholds, and the cross-file - contract checks. surface.rs, classify.rs and the seam-event row schema - are byte-identical to their pre-M7 state, and the PolicyRow - expressiveness gap (no matcher over extra) recurred exactly as at M6 - and was absorbed adapter-locally again. The two forced core changes - were event-source plumbing, outside this claim's scope and filed as - DDD-adapter-05. Promoted reported -> established on this result — four - languages, three fact-producer shapes, one scheduled falsification - attempt survived (classification: docs/ddd-m7-experiment.md; ruled by - Emil, 2026-08-10). - - - The M6 evidence, retained: - - M6 (2026-08-10) added a third language and looked for the falsifier without - finding it. Every Rust contract-surface assumption localised to the adapter - module: graded visibility (pub / pub(crate) / pub(super) / pub(in path)), - container capping, trait definitions, trait members, trait impls as boundary - participation with no keyword to key on, and derive changes as trait-impl - authorship. ddd-core/src/surface.rs — the contract-surface vocabulary — is - byte-identical to its pre-M6 state, as are classify.rs and protocol.rs. - - - The orphan-impl case tested the second clause directly. PolicyRow has no - matcher over SymbolFacts.extra, so it could not be expressed as a row; the - adapter absorbed it into its own kind normalisation (trait-impl-unresolved) - and the core did not move. A shape the table could not name was fixed in the - adapter, which is what the clause claims. - - - Corroboration from the other direction: DDD-adapter-03 reports that the C# - table drops enum-member before any row is consulted. The Rust table lists - enum-member in DECL_KINDS and demands a variant addition on a pub enum. The - gap was adapter-local — a second adapter did not inherit it. - - - Scope note: this evidence is contract-surface only. The host-lifecycle cost - M6 measured (2 leakage instances, 114 lines) is outside this statement and - is filed as DDD-adapter-04. -falsifier: > - A boundary defect class that no policy-table row could have named — i.e. - contract-surface knowledge that cannot be localised to a language adapter. -owner: none -changed: 2026-08-10 -revalidate_by: 2027-02-07 -depends_on: [] diff --git a/.ddd/claims/DDD-adapter-02.yaml b/.ddd/claims/DDD-adapter-02.yaml deleted file mode 100644 index 519028cc..00000000 --- a/.ddd/claims/DDD-adapter-02.yaml +++ /dev/null @@ -1,14 +0,0 @@ -format: 2 -id: DDD-adapter-02 -statement: > - In application repos, internal C# members are not consumed across the - repo boundary, so treating internal as non-surface loses no governed - contract events; the library case is detectable by InternalsVisibleTo - presence and is served by a per-repo config flip. -status: projected -falsifier: > - A boundary defect in an app repo traced to an internal member consumed - across a boundary the interceptor should have demanded a seam for. -owner: none -changed: 2026-08-06 -revalidate_by: 2027-08-06 diff --git a/.ddd/claims/DDD-adapter-03.yaml b/.ddd/claims/DDD-adapter-03.yaml deleted file mode 100644 index aafaf9ff..00000000 --- a/.ddd/claims/DDD-adapter-03.yaml +++ /dev/null @@ -1,32 +0,0 @@ -format: 2 -id: DDD-adapter-03 -statement: > - Adding a member to a public C# enum is contract-forming — a consumer's - exhaustive switch expression over that enum stops being exhaustive — and - the §9.1 policy table has no row for it, so the change class passes the - interceptor unclassified. LSP kind 22 normalizes to `enum-member`, which is - absent from DECL_KINDS, so the symbol is dropped in the adapter's facts() - before any policy row is consulted. -status: reported -evidence: > - Compiler behaviour reproduced on .NET SDK 8.0.413 (C# 12), net8.0: an enum - of three members with a consumer switch expression covering two yields - `warning CS8509: The switch expression does not handle all possible values - of its input type (it is not exhaustive). For example, the pattern - 'Status.Escalated' is not covered.` Default severity is warning, so the - break is silent unless the mechanical store is on; the DAD §4.1 - .editorconfig sets CS8509 to error, which is the arrangement this claim - assumes. Table gap verified by reading ddd-lsp/src/adapter/csharp.rs: - DECL_KINDS lists class, interface, struct, enum, method, property, field, - constructor, event, interface-member — the enum type, never its members — - and protocol.rs kind_name maps LSP kind 22 to `enum-member`. -falsifier: > - An enum-member addition that no consumer can break on — for instance if - every governed consumer's switch carries a discard arm, making exhaustive - matching moot — or a C# version in which the compiler stops treating enum - switch expressions as exhaustiveness-checked. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -depends_on: - - DDD-adapter-01 diff --git a/.ddd/claims/DDD-adapter-04.yaml b/.ddd/claims/DDD-adapter-04.yaml deleted file mode 100644 index a21a1feb..00000000 --- a/.ddd/claims/DDD-adapter-04.yaml +++ /dev/null @@ -1,71 +0,0 @@ -format: 2 -id: DDD-adapter-04 -statement: > - Adding a language costs more than an adapter plus a policy table whenever - the new host's *lifecycle* protocol differs in shape from the incumbents'. - The contract-surface half of PRD §11's mitigation held for Rust — every - Rust semantic assumption landed inside the adapter module — but the host - half did not: two assumptions about how a language server announces itself - were baked into the language-neutral LSP layer and had to be generalised - before the third adapter could be written at all. DDD-adapter-01 does not - cover this, because its falsifier is scoped to contract-surface knowledge - and says nothing about host wiring. -status: reported -evidence: > - Measured at M6 against rust-analyzer 1.95.0, with a pre-registered - classification filed before any code was written - (docs/ddd-m6-experiment.md, commit a8871da). - - - Assumption 1 — one fixed client-capability set serves every host. - `ddd-lsp/src/host.rs::initialize` sent a capability literal with no adapter - hook. rust-analyzer emits `experimental/serverStatus` only when the client - declares `experimental.serverStatusNotification: true`; probed both ways - against the same crate, the capability yields the notification at t+0.15s - and its absence yields zero such notifications for the life of the process. - Fix: `Adapter::extra_capabilities`, deep-merged over the common set. - - - Assumption 2 — readiness is the arrival of a distinctly-named notification. - That is Roslyn's shape: `workspace/projectInitializationComplete` is sent - once and means ready, so `ServerState::flags` recorded method names only. - rust-analyzer repeats one method and carries the answer in the payload — - `quiescent: false` at t+0.15s, `quiescent: true` at t+3.92s — so a - name-only flag reports Ready while the index is still building, which is - the exact dishonesty `Readiness::Loading` exists to prevent. Fix: a - `ReadySignal` variant taking a payload predicate, plus - `ServerState::last_params`. - - - Cost: 66 added / 6 deleted in adapter/mod.rs, 33/19 in host.rs, 9/0 in - client.rs, 3/2 each in the C# and Bicep adapters to restate one field — - about 114 added lines outside the new adapter, against 840 inside it. - Nothing in the interceptor, the classifier, the surface vocabulary, the MCP - layer, or the config schema had to move. -falsifier: > - (a) The false half dies if a future language server integrates with - near-zero host wiring: a fourth host needing neither a capability - announcement of its own nor a readiness shape outside the two now - expressible, and no change to the LSP layer to admit it. That would show - the 114 lines were rust-analyzer's alone rather than a per-server budget - line, and PRD §11's re-priced row would be overstating the cost. - - - (b) The true half dies if contract-surface leakage appears at language four - with an LSP host: a contract-surface assumption in the fourth LSP-hosted - adapter that cannot be localised to its adapter module and forces a change - to the classifier, the surface vocabulary, or the interceptor. That would - show the contract-surface half was not confirmed at M6, only untested at - n=3 — and would take DDD-adapter-01 down with it. A non-LSP artifact class - does not test this; M7's HTML+CSS pair is therefore outside it, because its - contract surface is stylesheets and class vocabulary rather than symbols. - - - (c) Both halves die if the M6 changes are shown to have been avoidable: a - readiness signal for rust-analyzer that is both honest about indexing and - expressible as a bare notification method name. -owner: none -changed: 2026-08-10 -revalidate_by: 2027-02-10 -depends_on: - - DDD-adapter-01 diff --git a/.ddd/claims/DDD-adapter-05.yaml b/.ddd/claims/DDD-adapter-05.yaml deleted file mode 100644 index db02db9a..00000000 --- a/.ddd/claims/DDD-adapter-05.yaml +++ /dev/null @@ -1,46 +0,0 @@ -format: 2 -id: DDD-adapter-05 -statement: > - The seam-event abstraction's contract-surface half is - producer-independent, but its event-source half assumed one producer - shape: a language server per language, one document per edit. Admitting - a producer outside that shape costs bounded, named changes in the - serve/host layer — never in the surface vocabulary. This generalises - DDD-adapter-04 (whose scope is LSP host lifecycle) to the producer - boundary itself: each new producer *shape* — not each new language — is - a budgeted core cost. -status: reported -evidence: > - Measured at M7 against a pre-registered prediction filed before any - adapter code (docs/ddd-m7-experiment.md, commit e402d55). The hostless - two-file producer (the HTML+CSS pair) tripped exactly the two predicted - assumptions. E1 — every adapter has a spawnable host: apply_edit routed - unconditionally through with_ready_host, warm_all started every - registered adapter's child, and the language tools presumed one; fix is - Adapter::hosted plus a hostless classification path (the interceptor - takes an optional host — empty symbol slices in, no reference counts - out). E2 — a symbol's facts derive from one document's text plus its - LSP symbols: pair facts (counterpart files, direction) need the - document's identity and the repo pair map; fix is the one optional - Adapter::enrich hook, called post-classification. Cost: 2 assumptions, - ~58 lines — adapter/mod.rs ~17, ddd-mcp/src/intercept.rs ~28, - lang_tools.rs 6, manager.rs 1, and a ±2-line restatement in each of the - three hosted adapters. Nothing in the surface vocabulary, the - classifier, the seam-event row schema, or the governance layer moved. - The adapter-local content-sniff dispatch (looks_like_html) is E2's - fingerprint: the facts signature carries no document identity at all. - rust-analyzer's 114 lines (DDD-adapter-04) and this 58 are the first - two entries in the producer-budget ledger. -falsifier: > - (a) A fifth producer shape — a build-system-backed producer, a - genuinely multi-document N>2 unit — forcing changes outside the two - seams now expressible (hosted, enrich): that would show the - generalisation is still enumerating cases rather than done. (b) - Contract-surface leakage at any future producer, which takes - DDD-adapter-01 down first and this claim with it. -owner: none -changed: 2026-08-10 -revalidate_by: 2027-02-10 -depends_on: - - DDD-adapter-01 - - DDD-adapter-04 diff --git a/.ddd/claims/DDD-arch-01.yaml b/.ddd/claims/DDD-arch-01.yaml deleted file mode 100644 index c8dae82c..00000000 --- a/.ddd/claims/DDD-arch-01.yaml +++ /dev/null @@ -1,18 +0,0 @@ -format: 2 -id: DDD-arch-01 -statement: > - product-cli's store conventions, SPARQL validation engine, and MCP plumbing - cover the ddd tool's storage and validation needs without forking any of them. -status: reported -evidence: > - M1 survey plus M1 implementation: ddd-core reuses pf::sparql_rules, - pf::validate::Violation, fileops atomic writes, and the ProductError model - unmodified, through public surfaces only. -falsifier: > - An M2-M4 need (LSP hosting, interception, MCP tool registration) that - product-core/product-mcp public surfaces cannot express, forcing a fork or - an internals dependency. -owner: none -changed: 2026-08-02 -revalidate_by: 2027-02-07 -depends_on: [] diff --git a/.ddd/claims/DDD-arch-02.yaml b/.ddd/claims/DDD-arch-02.yaml deleted file mode 100644 index 085477ff..00000000 --- a/.ddd/claims/DDD-arch-02.yaml +++ /dev/null @@ -1,14 +0,0 @@ -format: 2 -id: DDD-arch-02 -statement: > - For Bicep, the LSP protocol carries enough information (symbols, - visibility, signatures, diagnostics) to classify contract-surface edits - without direct Bicep compiler API access. -status: projected -falsifier: > - A Bicep contract-surface edit class that is invisible in LSP - symbol/diagnostic events and requires compiler-API access to detect. -owner: none -changed: 2026-09-29 -revalidate_by: 2027-02-07 -depends_on: [] diff --git a/.ddd/claims/DDD-arch-03.yaml b/.ddd/claims/DDD-arch-03.yaml deleted file mode 100644 index 505e6a51..00000000 --- a/.ddd/claims/DDD-arch-03.yaml +++ /dev/null @@ -1,14 +0,0 @@ -format: 2 -id: DDD-arch-03 -statement: > - An edit-flow interceptor consuming LSP events can demand seam declarations - at apply_edit time without extending either language server. -status: projected -falsifier: > - An interception point that cannot be observed client-side and would require - a server-side LSP extension to see. -owner: none -changed: 2026-08-02 -revalidate_by: 2027-02-07 -depends_on: - - DDD-arch-02 diff --git a/.ddd/claims/DDD-arch-04.yaml b/.ddd/claims/DDD-arch-04.yaml deleted file mode 100644 index e138edc5..00000000 --- a/.ddd/claims/DDD-arch-04.yaml +++ /dev/null @@ -1,14 +0,0 @@ -format: 2 -id: DDD-arch-04 -statement: > - The correspondence rows are usable as evidence about contract-surface - change only while their structural fields stay machine-authored; any - authoring path that lets a declarant rewrite symbol, kind, signature, or - reference count makes the dataset unfalsifiable. -status: projected -falsifier: > - A study over the rows in which declarant-supplied structural fields - proved as reliable as the LSP-derived ones. -owner: none -changed: 2026-08-07 -revalidate_by: 2027-08-07 diff --git a/.ddd/claims/DDD-arch-05.yaml b/.ddd/claims/DDD-arch-05.yaml deleted file mode 100644 index 3ba7c372..00000000 --- a/.ddd/claims/DDD-arch-05.yaml +++ /dev/null @@ -1,56 +0,0 @@ -format: 2 -id: DDD-arch-05 -statement: > - Same-session declaration matching admits a surface event when the - declaration names the symbol OR when its contract_location contains the - file path (ddd-mcp/src/intercept.rs, match_declarations). The file arm - makes the interceptor's enforce-mode guarantee degrade with file size: one - authored declaration admits every later surface event in that file for the - life of the serve process, including symbols whose demand was never - examined. What enforce mode then guarantees is not that every contract - change was declared, but that every file carrying a contract change had - some declaration in it. - - - The consequence reaches further than admission. link_seam_metadata writes - the matched event's LSP-derived facts onto the matched declaration, so a - file-arm match does not merely admit an unexamined symbol — it overwrites - the machine-authored structural fields of the declaration it matched with - another symbol's facts. The degraded guarantee is therefore not only weaker - than enforce mode claims; it silently corrupts the correspondence record it - is supposed to produce. -status: reported -evidence: > - First observed 2026-08-10, on the first enforce-mode edit ever run against - this repository (M6 governed tail). The edit added two pub items — - DEFAULT_COMMAND and host_command — to ddd-lsp/src/adapter/rust.rs. Two - declarations were authored, one per symbol. Both surface events linked to - the first, because match_declarations returns the first hit and the file arm - matched: - - - seam-event/3 symbol: DEFAULT_COMMAND linked: seam/rust/default-command - seam-event/4 symbol: host_command linked: seam/rust/default-command - - - seam-event/4 is verbatim the instance this claim's own falsifier names: a - linked_declaration whose seam names a symbol other than the row's own. - - - The corruption followed. .ddd/seams/seam-rust-default-command.yaml — a - declaration about a const — now carries symbol: host_command, kind: fn, - signature: fn host_command() -> Vec. seam/rust/host-command was - never matched and carries no LSP facts at all. The corrupted entry is left - in place with a notes field recording why; correcting it earlier would have - deleted the observation. -falsifier: > - Enforce-mode sessions in which file-arm matches are rare or always cover - symbols the declarant did examine — i.e. the broad arm admits nothing that - a symbol-granular rule would have demanded separately. The correspondence - rows can answer this directly: a linked_declaration whose seam names a - symbol other than the row's own is an instance of the gap. -owner: Emil -changed: 2026-08-10 -revalidate_by: 2027-02-07 -depends_on: - - DDD-arch-03 diff --git a/.ddd/claims/DDD-arch-06.yaml b/.ddd/claims/DDD-arch-06.yaml deleted file mode 100644 index 32ee43a2..00000000 --- a/.ddd/claims/DDD-arch-06.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 2 -id: DDD-arch-06 -statement: > - The correspondence dataset is conditioned on classified-surface outcomes. - A non-surface apply returns before any row is written; an off-mode apply - skips classification entirely; and on a rejected enforce-mode edit only the - unmatched events are logged, so the matched subset of that same edit leaves - no row either. Quantities computed over the rows are therefore conditional - on the surface stratum, and the base rate — what fraction of edits touch - contract surface — is not recoverable from the dataset. -status: reported -evidence: > - ddd-mcp/src/intercept.rs as implemented: the `mode == "off"` early return; - the `surface.is_empty()` early return that writes disk without calling - log_events; and the reject path, which builds `only_unmatched` and passes - that subset to log_events. Stated as a known limit in the M3+M4 report - ("every interception outcome is logged" = every classified surface - outcome). -falsifier: > - A logging path that writes a row for a non-surface or off-mode apply, or - for the matched events of a rejected edit — which would make the rows a - sample of edits rather than of surface events, and the base rate - recoverable after all. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -depends_on: - - DDD-arch-04 diff --git a/.ddd/claims/DDD-arch-07.yaml b/.ddd/claims/DDD-arch-07.yaml deleted file mode 100644 index 104c408f..00000000 --- a/.ddd/claims/DDD-arch-07.yaml +++ /dev/null @@ -1,39 +0,0 @@ -format: 2 -id: DDD-arch-07 -statement: > - An argument accessor that normalises — trims whitespace, treats blank as - absent — is correct for names, ids, and prose and silently destructive for - any argument whose value is content. Sharing one accessor across both - makes the damage invisible at the call site, because the handler that - reads a whole file looks identical to the handler that reads a symbol - name. -status: reported -evidence: > - Found at M6 by inspecting files the interceptor had written during this - repo's own governed session. `ddd-mcp/src/state.rs::opt_str` applies - `str::trim` and filters blank to `None`; `intercept.rs::resolve_new_text` - read `new_text` — a whole file — through it. Every file written through - `ddd_apply_edit` therefore lost its trailing newline, and would have lost - any leading blank line too; `new_text: ""` was additionally read as absent - rather than as an instruction to empty the file. Six files in this - workspace were observed mangled before the fix, against a control file - edited outside the interceptor which kept its newline. - - - Language-neutral: nothing about the defect is Rust-shaped, and the C# and - Bicep paths had the same behaviour since M4. It is classified as a core - bug rather than as leakage in docs/ddd-m6-experiment.md for that reason — - it was wrong for every language, and Rust only made it visible. - - - Fixed at M6 by `state.rs::raw_str`, a non-normalising accessor documented - against `opt_str`, declared as a seam (`seam/mcp/raw-str`). -falsifier: > - A content-carrying argument read through `opt_str` whose value is provably - unharmed by trimming — or a demonstration that the trailing-newline loss - was introduced elsewhere in the write path rather than by the accessor. -owner: none -changed: 2026-08-10 -revalidate_by: 2027-02-10 -depends_on: - - DDD-arch-04 diff --git a/.ddd/claims/DDD-arch-08.yaml b/.ddd/claims/DDD-arch-08.yaml deleted file mode 100644 index ace20a96..00000000 --- a/.ddd/claims/DDD-arch-08.yaml +++ /dev/null @@ -1,25 +0,0 @@ -format: 2 -id: DDD-arch-08 -statement: > - MCP-path interception alone is not a repository enforcement boundary: it - governs only edits volunteered through ddd_apply_edit, so a - contract-surface change made outside the MCP path (editor, CLI, git) is - never classified, never demanded, and produces no finding later. -status: reported -evidence: > - The architecture itself. Interception binds edits arriving through - ddd_apply_edit (ddd-mcp/src/intercept.rs); nothing binds an edit to - arrive that way, and neither `ddd diff` nor `ddd report escapes` - computes contract-surface coverage after the fact. The M6 report §5.3 - ("routing to the interceptor is voluntary") states the same limit for - the tool's own repo. Reported by the 2026-08 PRD review - (docs/reviews/ddd-cli-prd-review-2026-08.md). -falsifier: > - A demonstration that every contract-surface change in a governed repo - necessarily routes through ddd_apply_edit — an edit-path inventory - showing no write path (editor, CLI subcommand, direct git) reaches - contract surface ungoverned. -owner: none -changed: 2026-08-11 -revalidate_by: 2027-02-11 -depends_on: [] diff --git a/.ddd/claims/DDD-arch-09.yaml b/.ddd/claims/DDD-arch-09.yaml deleted file mode 100644 index 38ff17f8..00000000 --- a/.ddd/claims/DDD-arch-09.yaml +++ /dev/null @@ -1,25 +0,0 @@ -format: 2 -id: DDD-arch-09 -statement: > - Session-scoped declaration matching admits discharge of unidentified - changes: a seam declaration matches by symbol within the serving - session, so it discharges whatever change next touches that symbol — - the declaration never identifies, by content, the specific change it - was written for. -status: reported -evidence: > - The matching implementation: intercept.rs links declarations to surface - events by symbol (the file arm dropped at enforce per - dec/ddd/enforce-matching-tightens-to-symbol), with no subject content - hash and no base revision on the declaration — any same-session edit - touching the declared symbol is applied-linked. Reported by the 2026-08 PRD review - (docs/reviews/ddd-cli-prd-review-2026-08.md). -falsifier: > - A demonstration that symbol-scoped session matching cannot link a - declaration to a change other than the one it was written for — e.g. - matching shown to already compare change content, or a proof that at - most one change per symbol can occur within a session. -owner: none -changed: 2026-08-11 -revalidate_by: 2027-02-11 -depends_on: [] diff --git a/.ddd/claims/DDD-bicep-def-01.yaml b/.ddd/claims/DDD-bicep-def-01.yaml deleted file mode 100644 index f0977561..00000000 --- a/.ddd/claims/DDD-bicep-def-01.yaml +++ /dev/null @@ -1,30 +0,0 @@ -format: 4 -id: DDD-bicep-def-01 -statement: > - pred/configuration/parameter-default-governed is review-closed over a - mechanically complete worklist. No linter or PSRule rule reports a - parameter default as a governing decision, so the judgment stays with a - human; but the set that judgment must cover is extractable from the - compiled template, so the review can be checked for completeness even - though it cannot be checked for correctness. -status: reported -evidence: > - Reproduced on the indexed toolchain. Compiling the probe template and - reading `parameters[*].defaultValue` from the ARM JSON enumerated 3 of 3 - parameters carrying defaults — a literal, a function call and a - resourceGroup() expression — with no ambiguity about the set. Neither the - DAD §5.2 linter configuration nor PSRule reported any of them; the linter's - only default-related rule is secure-parameter-default, which concerns - secure parameters rather than defaults as such. -falsifier: > - A parameter default that does not appear in the compiled template's - parameter block — which would break the completeness of the worklist and - reduce this to ordinary review. Or a rule that judges defaults, which would - move it off review entirely. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/parameter-default-governed diff --git a/.ddd/claims/DDD-bicep-diag-01.yaml b/.ddd/claims/DDD-bicep-diag-01.yaml deleted file mode 100644 index 5745e5cf..00000000 --- a/.ddd/claims/DDD-bicep-diag-01.yaml +++ /dev/null @@ -1,30 +0,0 @@ -format: 4 -id: DDD-bicep-diag-01 -statement: > - PSRule closes the presence half of - pred/configuration/diagnostic-retention-declared and the retention half was - not observed closed. A resource with no diagnostic settings is reported; - whether a stated retention is checked for a value was not exercised here, - so the claim covers presence only and the retention half stands open until - someone runs it. -status: reported -evidence: > - Reproduced on the indexed toolchain: a Key Vault declared without - diagnostic settings failed Azure.KeyVault.Logs under PSRule, while the DAD - §5.2 linter configuration reported nothing about diagnostics for any - resource in the template. No rule concerning a retention *value* appeared - in the failure set, but the probe also never declared a diagnostic setting - with a retention to test — so this is absence of an observation, not - observation of an absence, and is written that way deliberately. -falsifier: > - A run in which a declared diagnostic setting with a missing or zero - retention is reported — which would extend this closure to the second half - — or a resource with no settings that PSRule does not report, which would - narrow the first. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/diagnostic-retention-declared diff --git a/.ddd/claims/DDD-bicep-exist-01.yaml b/.ddd/claims/DDD-bicep-exist-01.yaml deleted file mode 100644 index a515b52d..00000000 --- a/.ddd/claims/DDD-bicep-exist-01.yaml +++ /dev/null @@ -1,26 +0,0 @@ -format: 4 -id: DDD-bicep-exist-01 -statement: > - The linter catches only the degenerate case. `no-unused-existing-resources` - reports an existing reference nobody consumes, which is the one shape that - carries no assumption at all; a reference that is actually used — and so - actually assumes the resource exists and stays — draws nothing from either - tool. The rule that fires is the one that matters least. -status: reported -evidence: > - Reproduced on the indexed toolchain, both directions. An unused - `existing` Key Vault drew `Warning no-unused-existing-resources`. The same - reference consumed by an output — `output kvUri string = - existingKv.properties.vaultUri` — drew nothing from the linter and no - PSRule finding. Neither run reported the ordering or ownership assumption. -falsifier: > - A rule from either tool that reports a consumed existing reference as - carrying an undeclared dependency, or a means of expressing the ownership - assumption in the template such that its absence becomes detectable. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/external-dependency-declared diff --git a/.ddd/claims/DDD-bicep-idem-01.yaml b/.ddd/claims/DDD-bicep-idem-01.yaml deleted file mode 100644 index 5bf5c1a6..00000000 --- a/.ddd/claims/DDD-bicep-idem-01.yaml +++ /dev/null @@ -1,33 +0,0 @@ -format: 4 -id: DDD-bicep-idem-01 -statement: > - pred/execution/deployment-idempotence is not closable at build time, and - that is a property of its signature rather than a gap in the tooling. No - static check reports it, and none could: the ground is the target's current - state. The criterion form available is a what-if run asserted against a - real target, which relocates the check to the stage where the ground exists - rather than strengthening any build-time rule. -status: reported -evidence: > - Demonstrated negatively on the indexed toolchain: across every probe run in - this seed, neither the DAD §5.2 linter configuration nor PSRule produced - any finding concerning re-run behaviour, replacement semantics, or - deletion — the categories are absent from both rule sets rather than - present and passing. - - - Cited, not exercised: that a structural what-if assertion supplies the - criterion is DAD §5.3's proposal, and it was not run here because no Azure - subscription was available in this environment. The claim reports the - absence of a build-time check and does not report that what-if works. -falsifier: > - A build-time check that reports non-idempotent deployment shapes without - consulting a target — which would contradict the predicate's observed - ground item and mean the signature was written wrong. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/execution/deployment-idempotence diff --git a/.ddd/claims/DDD-bicep-net-01.yaml b/.ddd/claims/DDD-bicep-net-01.yaml deleted file mode 100644 index 4cfb2698..00000000 --- a/.ddd/claims/DDD-bicep-net-01.yaml +++ /dev/null @@ -1,27 +0,0 @@ -format: 4 -id: DDD-bicep-net-01 -statement: > - pred/configuration/network-exposure-declared is closed by PSRule for Azure - and not by the Bicep linter. The linter has no rule for network surface at - all; PSRule reports an unset firewall per resource type, so this predicate - is closable only in an arrangement that runs the second tool. -status: reported -evidence: > - Reproduced on the indexed toolchain. A storage account and a Key Vault - declared with no network properties drew nothing from the DAD §5.2 linter - configuration, and failed Azure.Storage.Firewall and Azure.KeyVault.Firewall - under PSRule in the same template. Related exposure rules failed in the - same run — Azure.Storage.BlobPublicAccess, Azure.Storage.LocalAuth, - Azure.Storage.MinTLS — so the coverage is per-property rather than a single - network verdict. -falsifier: > - A resource type whose network surface neither tool reports, or a linter - rule set that covers network exposure — the second would make this closable - without the PSRule dependency. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/network-exposure-declared diff --git a/.ddd/claims/DDD-bicep-rbac-01.yaml b/.ddd/claims/DDD-bicep-rbac-01.yaml deleted file mode 100644 index 9f269e5e..00000000 --- a/.ddd/claims/DDD-bicep-rbac-01.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-bicep-rbac-01 -statement: > - pred/configuration/authorization-scope-declared is open, and open in the - strong sense: neither tool evaluated the role assignment at all. The - assignment is not merely unjudged, it is absent from both tools' findings, - so an ungoverned scope leaves no trace anywhere in the pipeline. -status: reported -evidence: > - Reproduced on the indexed toolchain. A - Microsoft.Authorization/roleAssignments resource granting Contributor at - resource-group scope drew nothing from the DAD §5.2 linter configuration - and produced no PSRule finding of any kind — the only failures in that run - were Azure.AppService.AvailabilityZone and Azure.Resource.UseTags, both - about other resources in the same template. - - - Routing correction: an earlier probe's Azure.KeyVault.RBAC failure was read - as covering this item. It does not — that rule concerns a vault's - authorization model, not the scope an assignment is made at — and it is not - cited here. -falsifier: > - A rule from either tool that reports a role assignment's scope, or that - reports role assignments as a class requiring justification. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/authorization-scope-declared diff --git a/.ddd/claims/DDD-bicep-sec-01.yaml b/.ddd/claims/DDD-bicep-sec-01.yaml deleted file mode 100644 index db3f993f..00000000 --- a/.ddd/claims/DDD-bicep-sec-01.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-bicep-sec-01 -statement: > - Once a parameter carries `@secure()`, containment is checked twice over by - independent tools: the Bicep linter's output and secure-value rules, and - PSRule's Azure.Deployment.OutputSecretValue. Where the annotation is - missing, both tools switch to reporting the missing annotation rather than - falling silent — the linter as secure-secrets-in-params, PSRule as - Azure.Deployment.SecureParameter. -status: reported -evidence: > - Reproduced on the indexed toolchain, by differencing two runs of the same - template that differ only in the annotation. With `@secure()` present, - Azure.Deployment.OutputSecretValue fails on `output pw string = - adminPassword`. With it removed, that rule stops firing and - Azure.Deployment.SecureParameter fails in its place — a clean swap, one - rule each way, with the total failure count unchanged at 14. The linter - reported secure-secrets-in-params on the same unannotated parameter. -falsifier: > - A template in which removing `@secure()` silences the output check without - any rule reporting the missing annotation — which would make the two checks - jointly evadable rather than jointly covering. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/deployment-secret-containment diff --git a/.ddd/claims/DDD-bicep-sec-02.yaml b/.ddd/claims/DDD-bicep-sec-02.yaml deleted file mode 100644 index 142ea102..00000000 --- a/.ddd/claims/DDD-bicep-sec-02.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-bicep-sec-02 -statement: > - The whole chain rests on a name heuristic, and renaming the parameter - defeats every layer at once. Both tools decide that a value may be a secret - by looking at its identifier; a secret parameter whose name does not read - like one is neither required to be annotated nor prevented from leaving - through an output, and nothing reports anything. -status: reported -evidence: > - Reproduced on the indexed toolchain. The unannotated parameter that drew - both secure-secrets-in-params and Azure.Deployment.SecureParameter as - `adminPassword` drew nothing from either tool once renamed to `seedValue`, - with the template otherwise unchanged and still emitting it as a deployment - output. The linter's own message states the mechanism — "may represent a - secret (according to its name)". -falsifier: > - A check under these settings that reaches an unannotated secret whose - identifier gives no clue — by dataflow from a Key Vault reference, by type, - or by any means that does not read the name. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/deployment-secret-containment -refines: - - DDD-bicep-sec-01 diff --git a/.ddd/claims/DDD-bicep-sku-01.yaml b/.ddd/claims/DDD-bicep-sku-01.yaml deleted file mode 100644 index 3a531b5e..00000000 --- a/.ddd/claims/DDD-bicep-sku-01.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-bicep-sku-01 -statement: > - No tool judges a sizing value; PSRule reaches only consequences of one. - pred/configuration/resource-capacity-governed is therefore review-closed, - and as with parameter defaults the worklist is mechanically complete — sku - blocks are readable from the compiled template. -status: reported -evidence: > - Reproduced on the indexed toolchain. An App Service plan fixing - `{name: P1v3, tier: PremiumV3, capacity: 3}` drew nothing from the DAD §5.2 - linter configuration. PSRule failed Azure.AppService.AvailabilityZone — - which is a consequence of the sizing choice, zone redundancy, rather than a - finding about the choice — and Azure.Resource.UseTags, which is unrelated. - Reading `resources[*].sku` off the compiled ARM JSON enumerated the block - exactly, so the set requiring judgment is extractable even though the - judgment is not. -falsifier: > - A rule from either tool that reports a sizing value as ungoverned rather - than reporting a downstream property it implies — or a sizing value that - does not surface in the compiled template, which would break the - completeness of the worklist. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4; PSRule.Rules.Azure 1.47.0" -predicate: pred/configuration/resource-capacity-governed diff --git a/.ddd/claims/DDD-bicep-ver-01.yaml b/.ddd/claims/DDD-bicep-ver-01.yaml deleted file mode 100644 index 8ea8c03b..00000000 --- a/.ddd/claims/DDD-bicep-ver-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-bicep-ver-01 -statement: > - pred/configuration/api-version-currency is closed by the Bicep linter at a - bound the repo declares. `use-recent-api-versions` reports every resource - whose API version exceeds maxAgeInDays, names the age and lists the - acceptable versions, and runs at whatever severity bicepconfig.json sets. -status: reported -evidence: > - Reproduced on the indexed toolchain with the DAD §5.2 configuration - (maxAgeInDays 730, level error). A storage account declared at - 2019-06-01 produced - `Error use-recent-api-versions: '2019-06-01' is 2624 days old, should be no - more than 730 days old, or the most recent. Acceptable versions: - 2025-01-01`, and a Key Vault at 2023-07-01 produced the same at 1133 days. - The rejection witness the predicate asks for is exactly what the diagnostic - carries. -falsifier: > - A resource declaration whose API version exceeds the configured bound and - that the linter does not report — or a bound the linter cannot express, so - that currency must be judged by hand. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - target: "Azure Resource Manager" - tool: "Bicep CLI 0.37.4" -predicate: pred/configuration/api-version-currency diff --git a/.ddd/claims/DDD-cs-authz-01.yaml b/.ddd/claims/DDD-cs-authz-01.yaml deleted file mode 100644 index 7cc99608..00000000 --- a/.ddd/claims/DDD-cs-authz-01.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-authz-01 -statement: > - An endpoint carrying no authorization attribute is anonymous, and the - arrangement that changes this is a declared fallback policy rather than any - analyzer. So pred/interface/endpoint-authorization-explicit is closable — - but by composition, not by a rule: setting AuthorizationOptions.FallbackPolicy - makes silence mean deny, which converts every unattributed endpoint from an - unstated default into a stated one. -status: reported -evidence: > - Reproduced on the indexed toolchain against a running host, both cases - identical but for the fallback. `app.MapGet("/no-attribute", ...)` with - authentication and authorization registered and no fallback policy answers - 200 OK to an unauthenticated request. The same endpoint with - `FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser() - .Build()` answers 302 Found — the challenge — without the endpoint's own - code changing at all. -falsifier: > - A host configuration in which an unattributed endpoint is denied by default - without a fallback policy, or one in which a declared fallback fails to - cover endpoints that state nothing. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "ASP.NET Core 8 (Microsoft.AspNetCore.App shared framework)" -predicate: pred/interface/endpoint-authorization-explicit diff --git a/.ddd/claims/DDD-cs-conc-01.yaml b/.ddd/claims/DDD-cs-conc-01.yaml deleted file mode 100644 index d35f9a56..00000000 --- a/.ddd/claims/DDD-cs-conc-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-cs-conc-01 -statement: > - With no concurrency token configured, concurrent modification resolves to - last-write-wins with no exception, no diagnostic and no record that an - update was lost. The DAD checklist's reading is exact: last-write-wins is - not a policy anyone selected, it is what happens when nobody selects one. -status: reported -evidence: > - Reproduced on the indexed toolchain. Two contexts read the same row; the - first wrote "written by A" and committed, the second then wrote "written by - B" and committed without raising. The surviving value is "written by B" and - A's update is gone. The entity carried no concurrency token — confirmed by - walking the model, which reported none — because none was configured and - the conventions add none. -falsifier: > - An unconfigured entity under these settings whose concurrent modification - raises, or a convention that adds a concurrency token where the model - states none. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.EntityFrameworkCore.Sqlite 8.0.8" -predicate: pred/data/persistence-defaults-declared diff --git a/.ddd/claims/DDD-cs-conc-02.yaml b/.ddd/claims/DDD-cs-conc-02.yaml deleted file mode 100644 index 87b55dcf..00000000 --- a/.ddd/claims/DDD-cs-conc-02.yaml +++ /dev/null @@ -1,33 +0,0 @@ -format: 4 -id: DDD-cs-conc-02 -statement: > - Concurrency control does not resist the P(c, G) form, contrary to the - routing this checklist item was expected to take. Declaring a property - IsConcurrencyToken makes the lost update raise, and the presence of a token - per entity is readable from the built model — so the predicate is checkable - by a model assertion, and only the choice of which entities need one stays - with judgment. -status: reported -evidence: > - Reproduced on the indexed toolchain, against the DDD-cs-conc-01 run as its - control. The same two-context sequence over a model configuring - `Property(d => d.Version).IsConcurrencyToken()` throws - DbUpdateConcurrencyException on the second commit, where the unconfigured - model committed silently. Token presence was read off - `IEntityType.GetProperties().Where(p => p.IsConcurrencyToken)`, which is - what an assertion would walk. -falsifier: > - A model whose concurrency tokens cannot be enumerated at startup, or an - entity for which the token is present and the lost update still commits - silently — either would remove the mechanical half of this closure. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.EntityFrameworkCore.Sqlite 8.0.8" -predicate: pred/data/persistence-defaults-declared -refines: - - DDD-cs-conc-01 diff --git a/.ddd/claims/DDD-cs-disp-01.yaml b/.ddd/claims/DDD-cs-disp-01.yaml deleted file mode 100644 index 03f815f4..00000000 --- a/.ddd/claims/DDD-cs-disp-01.yaml +++ /dev/null @@ -1,26 +0,0 @@ -format: 4 -id: DDD-cs-disp-01 -statement: > - A `using` statement closes pred/code/deterministic-disposal for exactly the - ownership it can express: an instance whose owner is the enclosing lexical - scope. The compiler emits the try/finally, so disposal happens once on every - path including exceptional ones. It closes nothing about ownership the - syntax cannot name, which is every case where the owner is not a scope. -status: reported -evidence: > - Reproduced on the indexed toolchain: `using var r = new Res("lexical");` - followed by a thrown InvalidOperationException leaves the instance disposed - when the exception is caught outside. The refining claims are the cases - where ownership is held by something other than a scope, and each is - observed undisposed in the same run. -falsifier: > - A lexically scoped `using` whose instance is not disposed on some path — - which would break the construct itself rather than the inference around it. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/deterministic-disposal diff --git a/.ddd/claims/DDD-cs-disp-02.yaml b/.ddd/claims/DDD-cs-disp-02.yaml deleted file mode 100644 index e2d7e79a..00000000 --- a/.ddd/claims/DDD-cs-disp-02.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-disp-02 -statement: > - Field-held ownership is outside `using` but inside the analyzer set: the - language closes nothing here, while CA1001 reports a type that owns a - disposable field without being disposable, and CA2000 reports a caller that - drops a correctly-owned instance. This case is analyzer-closable rather - than open, which is a different allocation from the rest of the family. -status: reported -evidence: > - Reproduced on the indexed toolchain with AnalysisMode=All. A sealed Holder - with a `private readonly Res _r` and no IDisposable yields - `CA1001: Type 'Holder' owns disposable field(s) '_r' but is not disposable`, - and the instance is observed undisposed at exit. Separately, a GoodHolder - that does implement IDisposable, constructed by a caller that never - disposes it, yields CA2000 at the construction site. -falsifier: > - A field-held disposable that neither CA1001 nor CA2000 reports under these - settings — for instance ownership reaching a field through a path the - declaration-shape rule does not model. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/deterministic-disposal -refines: - - DDD-cs-disp-01 diff --git a/.ddd/claims/DDD-cs-disp-03.yaml b/.ddd/claims/DDD-cs-disp-03.yaml deleted file mode 100644 index b7ccc762..00000000 --- a/.ddd/claims/DDD-cs-disp-03.yaml +++ /dev/null @@ -1,33 +0,0 @@ -format: 4 -id: DDD-cs-disp-03 -statement: > - Async disposal is open in exactly one shape: a type implementing both - IDisposable and IAsyncDisposable, used with a sync `using`. The statement - binds to Dispose, so cleanup written in DisposeAsync never runs, and nothing - reports it. Where the type implements only IAsyncDisposable the compiler - refuses the statement outright, so the gap is not async disposal in general - — it is the ambiguity created by implementing both. -status: reported -evidence: > - Reproduced on the indexed toolchain. A type with a no-op Dispose and the - real work in DisposeAsync, used as `using (var a = new AsyncRes()) { }`, - builds with no diagnostic from the full analyzer set and is observed - unflushed at exit. The contrasting case compiles to - `error CS8418: type used in a using statement must implement - 'System.IDisposable'. Did you mean 'await using' rather than 'using'?` — - the compiler names both the fault and the fix when it can see one. -falsifier: > - A diagnostic from the language or the analyzer set that reports a sync - `using` over a type implementing both interfaces, which would move this - case from open to analyzer-closable. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/deterministic-disposal -refines: - - DDD-cs-disp-01 diff --git a/.ddd/claims/DDD-cs-disp-04.yaml b/.ddd/claims/DDD-cs-disp-04.yaml deleted file mode 100644 index f1ddd09c..00000000 --- a/.ddd/claims/DDD-cs-disp-04.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-disp-04 -statement: > - Ownership transferred into a container is open: neither the language nor - the analyzer set sees it. Handing a freshly constructed disposable to a - method that stores it in a collection produces no diagnostic, because - CA2000's escape analysis stops at the call and CA1001 models a field whose - type is disposable, not a field holding a collection of disposables. -status: reported -evidence: > - Reproduced on the indexed toolchain with AnalysisMode=All. A Registry - holding `private readonly List _items` with an `Add(Res)` that stores - and never disposes, called as `Reg.Add(new Res())`, draws no CA2000 at the - construction site and no CA1001 on Registry. The same build reports CA2000 - elsewhere in the file, so the analyzer was running and simply does not - reach this shape. -falsifier: > - A diagnostic under these settings on a disposable handed to a - collection-holding container with no disposal path — from CA1001, CA2000, - CA2213 or any successor rule. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/deterministic-disposal -refines: - - DDD-cs-disp-01 diff --git a/.ddd/claims/DDD-cs-disp-05.yaml b/.ddd/claims/DDD-cs-disp-05.yaml deleted file mode 100644 index ec1eb73a..00000000 --- a/.ddd/claims/DDD-cs-disp-05.yaml +++ /dev/null @@ -1,34 +0,0 @@ -format: 4 -id: DDD-cs-disp-05 -statement: > - CA2000's ownership inference is neither sound nor complete, and the - direction of its error is not predictable from the shape of the code. It - reports a site where disposal is written and stays silent on a site where - disposal appears nowhere. This is the predicate's `inferred` ground item - observed directly: C# cannot express ownership, so the analyzer must guess, - and a guess fails in both directions rather than erring consistently one - way. -status: reported -evidence: > - Reproduced on the indexed toolchain, both observations from a single build - of a single file. `static Action Capture() { var r = new Res(); return () - => r.Dispose(); }` draws CA2000 at the construction, although the returned - delegate does dispose the instance when invoked. In the same file - `Reg.Add(new Res())`, handing ownership to a container with no disposal - path at all, draws nothing. Conservative at the site that is handled, - silent at the site that is not. -falsifier: > - A characterisation of CA2000's analysis under which its false positives and - false negatives are predictable from the code shape — which would make the - residual allocable to a rule rather than to judgment. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/deterministic-disposal -refines: - - DDD-cs-disp-01 diff --git a/.ddd/claims/DDD-cs-disp-06.yaml b/.ddd/claims/DDD-cs-disp-06.yaml deleted file mode 100644 index 073b8780..00000000 --- a/.ddd/claims/DDD-cs-disp-06.yaml +++ /dev/null @@ -1,37 +0,0 @@ -format: 4 -id: DDD-cs-disp-06 -statement: > - HttpClient lifetime instantiates pred/code/deterministic-disposal's proxy - gap rather than violating the predicate. A per-call `using var http = new - HttpClient()` satisfies the predicate completely — lexical ownership, - disposal once on every path, no diagnostic from any rule — and is - nonetheless the wrong lifetime. No strengthening of the disposal check can - reach it, because with respect to disposal the code is correct. -status: reported -evidence: > - Demonstrated on the indexed toolchain: a method containing `using var http - = new HttpClient();` drew no CA2000, no CA1001 and no lifetime diagnostic - of any kind at AnalysisMode=All; the only rule that fired on the method was - CA2234, about passing a Uri rather than a string. So both halves hold — - the predicate is satisfied and nothing objects. - - - Cited, not exercised: that the per-call lifetime is wrong rests on - Microsoft's documented guidance to reuse a client or resolve one from - IHttpClientFactory, because disposal leaves the underlying connection in - TIME_WAIT. No load test was run here, so the harm is attributed rather than - observed, and the claim should not be read as reporting it. -falsifier: > - A rule reachable under these settings that reports a per-call HttpClient - construction — which would mean the case is checkable after all, just not - by the disposal predicate. Or guidance revising the lifetime advice, which - would remove the gap by making the satisfied check also the correct one. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/deterministic-disposal diff --git a/.ddd/claims/DDD-cs-ef-01.yaml b/.ddd/claims/DDD-cs-ef-01.yaml deleted file mode 100644 index 5ea03264..00000000 --- a/.ddd/claims/DDD-cs-ef-01.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-ef-01 -statement: > - EF Core supplies tracking and cascade behaviour by convention when the - model states neither, and both defaults are consequential: queries track - every entity they return, and a required relationship deletes its - dependents. Neither draws a diagnostic, and neither appears anywhere in the - source — they exist only in the built model. -status: reported -evidence: > - Read off the built model on the indexed toolchain: - `ChangeTracker.QueryTrackingBehavior` is TrackAll, and the foreign key of a - required reference navigation reports `DeleteBehavior.Cascade`. Neither was - configured; both came from the modelling conventions. Tracking additionally - has a call-site override in AsNoTracking, so it engages - pred/code/explicit-policy-argument as well — cascade has no call-site form - at all and can only be stated in the model. -falsifier: > - A modelling convention set under which an unconfigured required - relationship does not cascade, or an unconfigured context does not track — - which would mean the defaults are not the ones a governed repo must - displace. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.EntityFrameworkCore.Sqlite 8.0.8" -predicate: pred/data/persistence-defaults-declared diff --git a/.ddd/claims/DDD-cs-fail-01.yaml b/.ddd/claims/DDD-cs-fail-01.yaml deleted file mode 100644 index ef87b624..00000000 --- a/.ddd/claims/DDD-cs-fail-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-cs-fail-01 -statement: > - The SDK analyzer set closes the broadest case of - pred/code/failure-behaviour-declared: CA1031 reports a clause catching - System.Exception, which is the shape the DAD checklist calls an escape - essentially always. It closes only the breadth of the caught type, not the - continuation — a clause that narrows its type and then absorbs the failure - silently satisfies CA1031. -status: reported -evidence: > - Reproduced on the indexed toolchain with AnalysisMode=All: `try { Work(); } - catch (Exception) { }` drew CA1031 twice in the probe build. The residual - is visible in the rule's own scope — CA1031 is a check on the caught type, - so it has nothing to say about an empty body under a narrow type. -falsifier: > - A `catch (Exception)` under these settings that CA1031 does not report, or - an SDK rule that additionally reports an absorbed failure under a narrowed - type — the second would extend this closure past breadth to continuation. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/failure-behaviour-declared diff --git a/.ddd/claims/DDD-cs-map-01.yaml b/.ddd/claims/DDD-cs-map-01.yaml deleted file mode 100644 index baac3a13..00000000 --- a/.ddd/claims/DDD-cs-map-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-cs-map-01 -statement: > - The member-coverage obligation of pred/composition/convention-mapping is - mechanically closable at startup: AutoMapper's AssertConfigurationIsValid - throws when a destination member has no source and no explicit ignore, so - coverage need not be reviewed by hand provided the assertion is actually - run. -status: reported -evidence: > - Reproduced on the indexed toolchain. A destination carrying a ShippingCode - member the source cannot supply yields AutoMapperConfigurationException - from AssertConfigurationIsValid. The closure is conditional on the - assertion executing — it is a startup or test-time check, not a build-time - one, so a repo that configures the mapper without asserting has this - obligation open rather than closed. -falsifier: > - An uncovered destination member that AssertConfigurationIsValid accepts, - which would leave coverage to review alongside meaning. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "AutoMapper 13.0.1" -predicate: pred/composition/convention-mapping diff --git a/.ddd/claims/DDD-cs-map-02.yaml b/.ddd/claims/DDD-cs-map-02.yaml deleted file mode 100644 index 201f3c81..00000000 --- a/.ddd/claims/DDD-cs-map-02.yaml +++ /dev/null @@ -1,32 +0,0 @@ -format: 4 -id: DDD-cs-map-02 -statement: > - The meaning-conformance obligation is not closable by the mapper's own - validation, and the failure is silent in exactly the case the convention - handles well. A destination member matched by name to a source member of a - different meaning passes AssertConfigurationIsValid and maps a wrong value - with no diagnostic anywhere. -status: reported -evidence: > - Reproduced on the indexed toolchain, alongside DDD-cs-map-01 in one run. A - fully covered destination whose Total is populated by convention from the - source's Total — where the domain meant total including tax — passes the - assertion and yields Total=100. The uncovered case in the same run threw. - So the validator reports absence and is silent on wrongness, which is the - opposite of the ranking the risk deserves. -falsifier: > - A configuration-time or analysis-time check that reports a - convention-matched pair whose meanings differ — which would require the - meanings to be expressed somewhere the checker can read, and would move - this obligation off judgment. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "AutoMapper 13.0.1" -predicate: pred/composition/convention-mapping -refines: - - DDD-cs-map-01 diff --git a/.ddd/claims/DDD-cs-nrt-01.yaml b/.ddd/claims/DDD-cs-nrt-01.yaml deleted file mode 100644 index 139a938e..00000000 --- a/.ddd/claims/DDD-cs-nrt-01.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-nrt-01 -statement: > - With `enable` and `true`, - the C# compiler's null-state analysis closes pred/code/null-dereference-safety - at build time for values whose provenance lies entirely within the - compilation. It closes nothing for values entering from outside it — the - predicate's observed ground item — and the refining claims enumerate the - channels through which they enter. -status: reported -evidence: > - Reproduced on the indexed toolchain. A dereference of a `string?` - parameter yields CS8602; assigning it to a `string` local yields CS8600; a - non-nullable auto-property left unassigned by the constructor yields - CS8618. Under TreatWarningsAsErrors each is a build failure, so within the - compilation the tolerance holds. The refining claims each build clean under - the same settings and then fault at runtime, which is the same observation - from the other side. -falsifier: > - A value whose provenance is entirely inside the compilation, dereferenced - while possibly-null, that the analysis does not report — a flow-analysis - blind spot in ordinary intra-assembly code rather than at one of the - enumerated boundaries. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/null-dereference-safety diff --git a/.ddd/claims/DDD-cs-nrt-02.yaml b/.ddd/claims/DDD-cs-nrt-02.yaml deleted file mode 100644 index dd3b4436..00000000 --- a/.ddd/claims/DDD-cs-nrt-02.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-nrt-02 -statement: > - The closure does not extend across deserialisation. System.Text.Json - assigns a JSON null to a non-nullable reference property without the - compiler seeing it, and the idiomatic remedy for CS8618 — initialising the - property so the constructor satisfies the analysis — does not prevent the - deserialiser from overwriting it with null. -status: reported -evidence: > - Reproduced on the indexed toolchain. `sealed class Dto { public string - Name { get; set; } = ""; }` deserialised from `{"Name":null}` builds clean - under Nullable enable plus TreatWarningsAsErrors, and `d.Name.Length` - throws NullReferenceException at runtime. The `= ""` initialiser is what - silences CS8618, so the standard fix for the compiler's own complaint is - what leaves the hole unmarked. -falsifier: > - A serializer configuration under these settings that rejects or coerces the - null before it reaches the property — making the closure hold across the - boundary rather than stopping at it. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "System.Text.Json (net8.0 shared framework)" -predicate: pred/code/null-dereference-safety -refines: - - DDD-cs-nrt-01 diff --git a/.ddd/claims/DDD-cs-nrt-03.yaml b/.ddd/claims/DDD-cs-nrt-03.yaml deleted file mode 100644 index 430f2450..00000000 --- a/.ddd/claims/DDD-cs-nrt-03.yaml +++ /dev/null @@ -1,26 +0,0 @@ -format: 4 -id: DDD-cs-nrt-03 -statement: > - The closure does not extend across reflection. A PropertyInfo.SetValue call - writes null into a non-nullable reference property with no diagnostic at - the call site and none at the later dereference, because the write is - invisible to flow analysis. -status: reported -evidence: > - Reproduced on the indexed toolchain. - `typeof(Dto).GetProperty("Name")!.SetValue(o, null)` followed by - `o.Name.Length` builds clean under Nullable enable plus - TreatWarningsAsErrors and throws NullReferenceException at runtime. -falsifier: > - A reflection write of null to a non-nullable property that the runtime or - an analyzer rejects under these settings, rather than performing silently. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/null-dereference-safety -refines: - - DDD-cs-nrt-01 diff --git a/.ddd/claims/DDD-cs-nrt-04.yaml b/.ddd/claims/DDD-cs-nrt-04.yaml deleted file mode 100644 index 564dbb5b..00000000 --- a/.ddd/claims/DDD-cs-nrt-04.yaml +++ /dev/null @@ -1,35 +0,0 @@ -format: 4 -id: DDD-cs-nrt-04 -statement: > - The null-forgiving operator `!` exits the closure by assertion, and unlike - every other escape in this family it is invisible to the manifest - machinery. A `#pragma warning disable` or an .editorconfig severity is a - configured rule that `ddd diff` sees and ontology rule 4 makes cite a - risk-acceptance record; `!` suppresses no diagnostic, produces no rule id, - and appears in no SARIF, so nothing in the governance surface can count the - escapes it makes. -status: reported -evidence: > - Reproduced on the indexed toolchain: `string? maybe = null; - maybe!.Length` builds clean under Nullable enable plus - TreatWarningsAsErrors and throws NullReferenceException. The invisibility - is structural rather than observed — `!` is a syntax node, not a - diagnostic, so the configured-rule parsers (editorconfig, bicepconfig, - cargo lints) and the SARIF ingestion path have no key to join on. This is - why the DAD §4.4 escape-site analyzer registers on - SuppressNullableWarningExpression directly: a syntax-level check is the - only thing that can see it. -falsifier: > - A path by which the existing manifest or SARIF machinery enumerates `!` - sites without a purpose-written syntax analyzer — which would make the - operator governable by the same mechanism as every other escape. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/null-dereference-safety -refines: - - DDD-cs-nrt-01 diff --git a/.ddd/claims/DDD-cs-nrt-05.yaml b/.ddd/claims/DDD-cs-nrt-05.yaml deleted file mode 100644 index 65112bfa..00000000 --- a/.ddd/claims/DDD-cs-nrt-05.yaml +++ /dev/null @@ -1,34 +0,0 @@ -format: 4 -id: DDD-cs-nrt-05 -statement: > - The closure does not extend across EF Core's required reference - navigations. A navigation declared non-nullable and initialised with the - `= null!` idiom that EF's own documentation prescribes is null on any query - that does not Include it, so the dereference the compiler proved safe - faults at runtime whenever the load path differs from the one the author - had in mind. -status: reported -evidence: > - Reproduced on the indexed toolchain against SQLite in-memory: - `public Customer Customer { get; set; } = null!;` on an Order, then - `db.Orders.First().Customer.Name.Length` without Include. Builds clean - under Nullable enable plus TreatWarningsAsErrors; throws - NullReferenceException at runtime. The escape is prescribed rather than - accidental — `= null!` is the documented way to satisfy CS8618 for a - required navigation, so following the guidance is what creates the site. -falsifier: > - An EF configuration under these settings in which an unloaded required - navigation faults at the query rather than at the dereference — the - provider refusing to hand back a half-materialised graph, as it does for a - NULL scalar column (DDD-cs-nrt-06). -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.EntityFrameworkCore.Sqlite 8.0.8" -predicate: pred/code/null-dereference-safety -refines: - - DDD-cs-nrt-01 diff --git a/.ddd/claims/DDD-cs-nrt-06.yaml b/.ddd/claims/DDD-cs-nrt-06.yaml deleted file mode 100644 index 8bf8e272..00000000 --- a/.ddd/claims/DDD-cs-nrt-06.yaml +++ /dev/null @@ -1,36 +0,0 @@ -format: 4 -id: DDD-cs-nrt-06 -statement: > - EF Core scalar materialisation is not a null-dereference hole, contrary to - the suspicion this seed was written to check. A NULL column read into a - non-nullable reference property does not silently produce a null-typed - value: the provider's reader throws during materialisation. The static gap - is real — nothing at compile time checks the property's nullability against - the column's — but the arrangement closes the predicate at runtime, at the - data boundary, rather than leaking a null onward. -status: reported -evidence: > - Reproduced on the indexed toolchain: a legacy-shaped table - `Persons (Id INTEGER PRIMARY KEY, Name TEXT NULL)` holding a NULL, mapped - to `public string Name { get; set; } = "";`. `db.Persons.First()` throws - `System.InvalidOperationException: The data is NULL at ordinal 1. This - method can't be called on NULL values. Check using IsDBNull before calling.` - from Microsoft.Data.Sqlite.SqliteValueReader.GetString — before the entity - is handed back, so no dereference of a null ever occurs. -falsifier: > - A provider or a mapping configuration under these settings that materialises - a NULL column into a non-nullable reference property and returns the entity, - making the failure a NullReferenceException at some later dereference. Only - the Sqlite provider was exercised; another provider behaving differently - falsifies this as stated and narrows it to Sqlite. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.EntityFrameworkCore.Sqlite 8.0.8" -predicate: pred/code/null-dereference-safety -refines: - - DDD-cs-nrt-01 diff --git a/.ddd/claims/DDD-cs-nrt-07.yaml b/.ddd/claims/DDD-cs-nrt-07.yaml deleted file mode 100644 index a2a7a1a2..00000000 --- a/.ddd/claims/DDD-cs-nrt-07.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-nrt-07 -statement: > - The closure does not extend across unmanaged interop. A DllImport - declaration names its own return nullability, the compiler takes the - declaration on trust, and the marshaller will hand back null for a NULL - pointer regardless of what was declared. -status: reported -evidence: > - Reproduced on the indexed toolchain: - `[DllImport("libc", CharSet = CharSet.Ansi)] static extern string - getenv(string name);` — a non-nullable declared return — called with an - unset variable name. Builds clean under Nullable enable plus - TreatWarningsAsErrors; the returned value is null and dereferencing it - throws NullReferenceException. Nothing in the compilation could have known - better: the declaration is the only ground the analysis has, and it is an - assertion by the author rather than a fact about the callee. -falsifier: > - A marshalling configuration under these settings that faults at the interop - boundary when a NULL pointer meets a non-nullable declared return, rather - than passing null through to the caller. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/null-dereference-safety -refines: - - DDD-cs-nrt-01 diff --git a/.ddd/claims/DDD-cs-obs-01.yaml b/.ddd/claims/DDD-cs-obs-01.yaml deleted file mode 100644 index 648a324f..00000000 --- a/.ddd/claims/DDD-cs-obs-01.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-cs-obs-01 -statement: > - pred/code/observable-completion is open in the SDK analyzer set. `async - void` and its invocation draw no diagnostic at AnalysisMode=All, so the - shape the DAD checklist lists first under async escapes is invisible to the - arrangement that closes ConfigureAwait in the same file. Closing it needs - an added analyzer package, which is a dependency decision rather than a - severity one. -status: reported -evidence: > - Reproduced on the indexed toolchain: `internal static async void - FireAndForget() => await Task.Delay(1).ConfigureAwait(false);` and a - caller invoking it produced no diagnostic, in a build where CA2007 fired on - unqualified awaits elsewhere. DAD §4.1 names - Microsoft.VisualStudio.Threading.Analyzers as the package carrying the - rule; it was not exercised here, so no rule id is cited for it. -falsifier: > - An SDK-bundled rule at some AnalysisMode that reports `async void` or a - discarded Task, which would make this closable by severity alone. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/observable-completion diff --git a/.ddd/claims/DDD-cs-ord-01.yaml b/.ddd/claims/DDD-cs-ord-01.yaml deleted file mode 100644 index b997f9f8..00000000 --- a/.ddd/claims/DDD-cs-ord-01.yaml +++ /dev/null @@ -1,26 +0,0 @@ -format: 4 -id: DDD-cs-ord-01 -statement: > - pred/code/deterministic-sequence-order is open in the SDK analyzer set. - Positional selection over an unordered sequence draws no diagnostic at any - AnalysisMode, so nothing mechanical distinguishes a First() over an ordered - source from a First() over an arbitrary one, and the residual falls - entirely to review. -status: reported -evidence: > - Reproduced on the indexed toolchain at AnalysisMode=All: `xs.First()`, - `xs.Take(3)` and an ordered control `xs.OrderBy(o => o.Id).First()` in one - build produced no ordering diagnostic of any kind. Other rules fired in the - same build (CA1812, CA2234), so the analyzers were running. -falsifier: > - A rule in the SDK set, or reachable by severity configuration alone, that - reports a positional selection over a sequence with no established order. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/deterministic-sequence-order diff --git a/.ddd/claims/DDD-cs-pol-01.yaml b/.ddd/claims/DDD-cs-pol-01.yaml deleted file mode 100644 index 36052b08..00000000 --- a/.ddd/claims/DDD-cs-pol-01.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-cs-pol-01 -statement: > - For string comparison and for format providers, the SDK's own analyzer set - closes pred/code/explicit-policy-argument at build time: no extra package - is needed, only severity. CA1305 reports a missing IFormatProvider, CA1307 - and CA1309 report a missing or non-ordinal StringComparison, and CA1310 - reports the correctness case. -status: reported -evidence: > - Reproduced on the indexed toolchain with EnableNETAnalyzers, - AnalysisLevel=latest, AnalysisMode=All and no package references beyond - the SDK. A probe containing `a.Equals(b)`, `n.ToString()` and - `int.Parse(s)` drew CA1305 four times, CA1307 twice and CA1309 twice. The - DAD §4.1 .editorconfig raises exactly these ids to error, so the - arrangement it prescribes is available from the SDK alone. -falsifier: > - A policy-bearing comparison or formatting call under these settings that - none of CA1305/CA1307/CA1309/CA1310 reports — leaving a class of omission - that severity configuration cannot reach. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/explicit-policy-argument diff --git a/.ddd/claims/DDD-cs-pol-02.yaml b/.ddd/claims/DDD-cs-pol-02.yaml deleted file mode 100644 index 83c73ff0..00000000 --- a/.ddd/claims/DDD-cs-pol-02.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 4 -id: DDD-cs-pol-02 -statement: > - The clock is not covered. `DateTime.Now` draws no diagnostic from the SDK - analyzer set at AnalysisMode=All, so the ambient-time-zone case of - pred/code/explicit-policy-argument is open in the arrangement that closes - the comparison and culture cases. Closing it requires adding an analyzer - package, which is a dependency decision rather than a severity setting. -status: reported -evidence: > - Reproduced on the indexed toolchain in the same probe build that produced - CA1305, CA1307 and CA1309: `internal static DateTime Stamp() => - DateTime.Now;` drew nothing. The analyzers were demonstrably running, so - this is absence of a rule rather than absence of analysis. DAD §4.1 names - Meziantou.Analyzer as the package that carries the rule; that package was - not exercised here and its rule id is therefore not cited. -falsifier: > - An SDK-bundled rule at some AnalysisMode that reports ambient-clock or - ambient-time-zone use, which would make this closable by severity alone - like its siblings. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/explicit-policy-argument -refines: - - DDD-cs-pol-01 diff --git a/.ddd/claims/DDD-cs-pol-03.yaml b/.ddd/claims/DDD-cs-pol-03.yaml deleted file mode 100644 index 9b99c6a5..00000000 --- a/.ddd/claims/DDD-cs-pol-03.yaml +++ /dev/null @@ -1,27 +0,0 @@ -format: 4 -id: DDD-cs-pol-03 -statement: > - The continuation context is a policy argument, and CA2007 closes it. An - await that does not name a ConfigureAwait policy takes - SynchronizationContext.Current — an ambient value — so it falls under - pred/code/explicit-policy-argument on the same terms as culture and - comparison, and the SDK rule reports every such site. -status: reported -evidence: > - Reproduced on the indexed toolchain: every unqualified await in the probe - drew CA2007, six occurrences across the file, with no package beyond the - SDK. Routing note — the DAD checklist files this under "async shape", but - the relation is the policy-argument one: an ambient default taken silently - at a call site that has an overload for naming it. -falsifier: > - An await under these settings whose continuation context is left to the - ambient value and that CA2007 does not report. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/explicit-policy-argument diff --git a/.ddd/claims/DDD-cs-pol-04.yaml b/.ddd/claims/DDD-cs-pol-04.yaml deleted file mode 100644 index 7e1a6407..00000000 --- a/.ddd/claims/DDD-cs-pol-04.yaml +++ /dev/null @@ -1,32 +0,0 @@ -format: 4 -id: DDD-cs-pol-04 -statement: > - Cancellation is a policy argument too, and CA2016 closes only half of it. - The rule reports a method that holds a CancellationToken and calls an - overload accepting one without forwarding it — omission where the policy - was available. It reports nothing about a method that exposes no token at - all, so "this operation cannot be cancelled" is never surfaced as a choice. -status: reported -evidence: > - Reproduced on the indexed toolchain, three cases. A method holding `ct` and - calling `Inner()` on an overload with an optional token drew - `CA2016: Forward the 'ct' parameter to the 'Inner' method or pass in - 'CancellationToken.None' explicitly to indicate intentionally not - propagating the token`. Passing CancellationToken.None explicitly silenced - it, which is the rule's stated design. An async method exposing no token - parameter at all drew nothing. -falsifier: > - A rule reachable by severity configuration alone that reports an async - operation exposing no cancellation, which would extend this closure from - forwarding to presence. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "SDK-bundled .NET analyzers at AnalysisLevel=latest, AnalysisMode=All" -predicate: pred/code/explicit-policy-argument -refines: - - DDD-cs-pol-03 diff --git a/.ddd/claims/DDD-cs-proc-01.yaml b/.ddd/claims/DDD-cs-proc-01.yaml deleted file mode 100644 index 590925fd..00000000 --- a/.ddd/claims/DDD-cs-proc-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-cs-proc-01 -statement: > - pred/execution/failure-policy-declared is open, and worse than open for - most of its elements: the platform client supplies a silent default for - timeout and has no concept at all of retry, backoff or idempotency, so - those elements are not merely unset but unrepresentable on the type that - makes the call. -status: reported -evidence: > - Reproduced on the indexed toolchain: `new HttpClient().Timeout` is - 00:01:40 — a hundred-second default no caller chose — and the type exposes - no retry, backoff or idempotency surface whatever, so there is nothing for - a checker to find unset. Nothing in the SDK analyzer set reports a - boundary call lacking a failure policy. -falsifier: > - A rule reachable by severity configuration that reports an unpoliced - process-boundary call, or a platform client that represents the missing - elements so their absence becomes detectable. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "System.Net.Http (net8.0 shared framework)" -predicate: pred/execution/failure-policy-declared diff --git a/.ddd/claims/DDD-cs-ser-01.yaml b/.ddd/claims/DDD-cs-ser-01.yaml deleted file mode 100644 index a4b016a8..00000000 --- a/.ddd/claims/DDD-cs-ser-01.yaml +++ /dev/null @@ -1,33 +0,0 @@ -format: 4 -id: DDD-cs-ser-01 -statement: > - Every element of pred/data/serialisation-contract-declared defaults - silently under System.Text.Json, and nothing reports the omission. Member - naming, enum representation, null handling, temporal format and - unknown-member handling are all supplied by the serialiser when unset, with - no diagnostic at build time and no failure at run time — so the contract - exists and is authored by the library rather than by the repo. -status: reported -evidence: > - Reproduced on the indexed toolchain with default JsonSerializerOptions. - Serialising a DTO produced - `{"Name":"ada","State":1,"When":"2026-08-07T13:00:00Z","Discount":null}` — - member names carried through unchanged rather than camel-cased, the enum - emitted as the ordinal 1 rather than "Closed", a null property included - rather than omitted, and an ISO-8601 instant. Deserialising - `{"Name":"bob","Unknown":"ignored silently"}` succeeded, the unknown member - discarded without error. Five contract elements, five defaults, no - diagnostic. -falsifier: > - A default configuration under these settings that reports or rejects any - unset contract element, or a serialiser whose defaults are versioned as - part of the consuming repo rather than the library. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "System.Text.Json (net8.0 shared framework)" -predicate: pred/data/serialisation-contract-declared diff --git a/.ddd/claims/DDD-cs-tc-01.yaml b/.ddd/claims/DDD-cs-tc-01.yaml deleted file mode 100644 index 0092b89a..00000000 --- a/.ddd/claims/DDD-cs-tc-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-cs-tc-01 -statement: > - The C# compiler's static type checking closes - pred/code/static-type-conformance for values flowing between statically - typed sites, with no opt-in required — unlike the nullable analysis, this - arrangement is on by default. It closes nothing for values crossing a site - that exits the type system, and the refining claims separate the two ways - of exiting: deferring the check to runtime, and removing it. -status: reported -evidence: > - Reproduced on the indexed toolchain. An incompatible assignment yields - CS0029, a cast between unrelated sealed types yields CS0030, and a call to - an undeclared member yields CS1061 — all compile errors without any - project setting. The refining claims each build clean under the same - project, which is the same boundary observed from outside. -falsifier: > - A conversion between statically typed sites that the compiler accepts and - the runtime then rejects, with neither `dynamic`, an `object`-typed - intermediate, nor an unsafe reinterpretation on the path. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/static-type-conformance diff --git a/.ddd/claims/DDD-cs-tc-02.yaml b/.ddd/claims/DDD-cs-tc-02.yaml deleted file mode 100644 index 1ce71648..00000000 --- a/.ddd/claims/DDD-cs-tc-02.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 4 -id: DDD-cs-tc-02 -statement: > - `dynamic` defers the check rather than removing it. Member binding on a - dynamic expression is not performed at compile time, so the predicate's - tolerance — no conversion deferred to runtime — is not met; but the runtime - binder does perform the check and fails loudly at the offending site. -status: reported -evidence: > - Reproduced on the indexed toolchain: `dynamic d = 42; d.Frobnicate();` - builds clean under TreatWarningsAsErrors and throws - RuntimeBinderException. The failure names the missing member and occurs at - the call, so the rejection witness the predicate asks for exists — it is - produced at runtime instead of at build time. -falsifier: > - A dynamic member access that binds to something the static type does not - have and returns a value rather than throwing — which would move this from - a deferred check to a removed one. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/static-type-conformance -refines: - - DDD-cs-tc-01 diff --git a/.ddd/claims/DDD-cs-tc-03.yaml b/.ddd/claims/DDD-cs-tc-03.yaml deleted file mode 100644 index a5a62ae3..00000000 --- a/.ddd/claims/DDD-cs-tc-03.yaml +++ /dev/null @@ -1,27 +0,0 @@ -format: 4 -id: DDD-cs-tc-03 -statement: > - A downcast from `object` defers the check rather than removing it. Routing - a value through an `object`-typed intermediate erases what the compiler - knew, so the conversion is unverified at build time; the CLR's cast check - still runs and rejects the mismatch. -status: reported -evidence: > - Reproduced on the indexed toolchain: `object o = "text"; var n = (int)o;` - builds clean under TreatWarningsAsErrors and throws InvalidCastException. - Same shape as DDD-cs-tc-02 and filed separately because the mechanism - differs — the binder versus the cast check — and either could change - without the other. -falsifier: > - An `object` downcast to an incompatible type that succeeds and yields a - usable value of the target type, rather than throwing at the cast. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/static-type-conformance -refines: - - DDD-cs-tc-01 diff --git a/.ddd/claims/DDD-cs-tc-04.yaml b/.ddd/claims/DDD-cs-tc-04.yaml deleted file mode 100644 index 788d9e81..00000000 --- a/.ddd/claims/DDD-cs-tc-04.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-cs-tc-04 -statement: > - `Unsafe.As` removes the check rather than deferring it. It reinterprets a - reference as an unrelated type with no verification at build time and none - at runtime, so a mistyped reference is used as though it conformed and the - program continues with a plausible wrong answer instead of a rejection. -status: reported -evidence: > - Reproduced on the indexed toolchain: `object o = new int[]{1,2,3};` then - `Unsafe.As(ref o).Length` builds clean under - TreatWarningsAsErrors, throws nothing, and returns 3 — the array's length - field read at the offset where a string keeps its own. The predicate's - rejection witness is not merely late here, it does not exist. -falsifier: > - A runtime configuration under these settings in which a reinterpreted - reference is type-checked at first use, producing an exception rather than - a value read at the wrong offset. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "System.Runtime.CompilerServices.Unsafe (net8.0 shared framework)" -predicate: pred/code/static-type-conformance -refines: - - DDD-cs-tc-01 diff --git a/.ddd/claims/DDD-cs-tc-05.yaml b/.ddd/claims/DDD-cs-tc-05.yaml deleted file mode 100644 index d8141bb5..00000000 --- a/.ddd/claims/DDD-cs-tc-05.yaml +++ /dev/null @@ -1,34 +0,0 @@ -format: 4 -id: DDD-cs-tc-05 -statement: > - Unmanaged marshalling removes the check the same way, and neither of the - two removal sites is gated by `AllowUnsafeBlocks`. Marshal.PtrToStructure - reads whatever the declared layout says, past the end of the allocation if - the layout is wider, so the project-level switch teams treat as "unsafe - code is off" does not cover the unsafe surface reachable through ordinary - BCL calls. -status: reported -evidence: > - Reproduced on the indexed toolchain, in a project that does not set - AllowUnsafeBlocks (so it defaults to false): 4 bytes allocated with - AllocHGlobal and written 0x41414141, then read as an 8-byte - `[StructLayout(LayoutKind.Sequential)] struct Wide { int A; int B; }`. - Builds clean under TreatWarningsAsErrors, throws nothing, and returns - A=41414141 with B read from beyond the allocation. The same project - compiles the DDD-cs-tc-04 Unsafe.As case, which is the gating observation: - both are method calls, not `unsafe` blocks. -falsifier: > - A default project configuration under these settings in which either - Unsafe.As or Marshal.PtrToStructure fails to compile without - AllowUnsafeBlocks, or in which the marshaller bounds-checks the source - allocation against the declared layout. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 -predicate: pred/code/static-type-conformance -refines: - - DDD-cs-tc-01 diff --git a/.ddd/claims/DDD-cs-tx-01.yaml b/.ddd/claims/DDD-cs-tx-01.yaml deleted file mode 100644 index 62ecebbe..00000000 --- a/.ddd/claims/DDD-cs-tx-01.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 4 -id: DDD-cs-tx-01 -statement: > - pred/execution/transaction-boundary-declared is open, and structurally so: - the boundary is wherever SaveChanges was called, no ambient transaction - exists unless one is opened explicitly, and nothing in the code records - which writes were meant to commit together — so there is no stated intent - for any checker to compare the actual extent against. -status: reported -evidence: > - Reproduced on the indexed toolchain. `db.Database.CurrentTransaction` is - null before any explicit call, and a two-step unit of work whose second - step throws leaves the first step committed: one row survives. The commit - extent was decided by the placement of a call, silently and without a - diagnostic. Nothing in the SDK analyzer set reports it, and nothing could - — the intended extent is not expressed anywhere to be violated. -falsifier: > - A means of stating intended atomicity in the code that a checker can read — - an attribute, a scope type, a convention a rule could enforce — which would - turn the inferred ground item into a controlled one and make this checkable. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-08-07 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.EntityFrameworkCore.Sqlite 8.0.8" -predicate: pred/execution/transaction-boundary-declared diff --git a/.ddd/claims/DDD-detect-01.yaml b/.ddd/claims/DDD-detect-01.yaml deleted file mode 100644 index 47ad7df9..00000000 --- a/.ddd/claims/DDD-detect-01.yaml +++ /dev/null @@ -1,19 +0,0 @@ -format: 2 -id: DDD-detect-01 -statement: > - Both governed toolchains emit SARIF 2.1 through documented, supported - switches (the C# compiler ErrorLog property with version=2.1; bicep lint - --diagnostics-format sarif), so one structured ingestion path covers all - emitted diagnostics without console parsing. -status: reported -evidence: > - Verified against current Microsoft docs at M2 implementation (C# compiler - options errors-warnings page; Bicep CLI lint command reference) and - exercised by the committed fixtures plus the ignored regeneration tests. -falsifier: > - A governed diagnostic source in either toolchain that cannot be routed - into SARIF 2.1, forcing a per-tool console parser back into detection. -owner: none -changed: 2026-08-06 -revalidate_by: 2027-02-06 -depends_on: [] diff --git a/.ddd/claims/DDD-detect-02.yaml b/.ddd/claims/DDD-detect-02.yaml deleted file mode 100644 index 8d3e436e..00000000 --- a/.ddd/claims/DDD-detect-02.yaml +++ /dev/null @@ -1,19 +0,0 @@ -format: 2 -id: DDD-detect-02 -statement: > - Committed SARIF fixtures keep the M2 test suite hermetic: CI needs no - .NET SDK or Bicep CLI, while ignored regeneration tests keep the fixtures - falsifiable against the real toolchains locally. -status: reported -evidence: > - The ddd-cli governance suite runs green on committed fixtures alone; the - two #[ignore]d tests regenerate SARIF via the documented invocations and - feed it back through the same ingestion path. -falsifier: > - A toolchain SARIF change that the committed fixtures mask until a user - hits it — i.e. a regeneration test that would have failed but was never - run before release. -owner: none -changed: 2026-08-06 -revalidate_by: 2027-02-06 -depends_on: [] diff --git a/.ddd/claims/DDD-friction-01.yaml b/.ddd/claims/DDD-friction-01.yaml deleted file mode 100644 index 602f9b38..00000000 --- a/.ddd/claims/DDD-friction-01.yaml +++ /dev/null @@ -1,16 +0,0 @@ -format: 2 -id: DDD-friction-01 -statement: > - Pre-filling the LSP-derived observables (symbol, kind, signature, - visibility, current reference count) in an apply_edit rejection removes - the agent's re-derivation cost, while keeping verdict_knowledge plus - obligation answers blank keeps the declaration an authored judgment - rather than a rubber stamp. -status: projected -falsifier: > - Dogfood sessions showing agents pasting the pre-filled facts into - verdict_knowledge unchanged (rubber-stamping), or agents measurably - re-deriving the facts despite the pre-fill (wasted tool calls). -owner: none -changed: 2026-08-06 -revalidate_by: 2027-02-06 diff --git a/.ddd/claims/DDD-friction-02.yaml b/.ddd/claims/DDD-friction-02.yaml deleted file mode 100644 index c3d050a3..00000000 --- a/.ddd/claims/DDD-friction-02.yaml +++ /dev/null @@ -1,15 +0,0 @@ -format: 2 -id: DDD-friction-02 -statement: > - A declaration surface with no in-surface amend path pushes the agent to - either leave a warned entry uncorrected or hand-edit the YAML outside the - interceptor; the second defeats the interceptor's guarantee that - declarations and the edits they cover move together. -status: projected -falsifier: > - Dogfood sessions in which agents neither hand-edit declaration YAML nor - leave warned entries uncorrected — i.e. the missing amend path costs - nothing in practice. -owner: none -changed: 2026-08-07 -revalidate_by: 2027-02-07 diff --git a/.ddd/claims/DDD-gates-01.yaml b/.ddd/claims/DDD-gates-01.yaml deleted file mode 100644 index 7effd1a0..00000000 --- a/.ddd/claims/DDD-gates-01.yaml +++ /dev/null @@ -1,18 +0,0 @@ -format: 2 -id: DDD-gates-01 -statement: > - Denying clippy::unwrap_used workspace-wide converts every potential panic - site into an explicit Result path or a reviewed expect, at negligible - authoring cost in this codebase. -status: reported -evidence: > - The zero-unwrap policy has held across the whole workspace (enforced via - [workspace.lints.clippy] and the CI clippy gate) without a single - waiver request; error paths route through ProductError. -falsifier: > - A recurring class of code where satisfying the lint produces worse error - handling than the unwrap it replaced, or waiver pressure in review. -owner: none -changed: 2026-08-06 -revalidate_by: 2027-08-06 -depends_on: [] diff --git a/.ddd/claims/DDD-gates-02.yaml b/.ddd/claims/DDD-gates-02.yaml deleted file mode 100644 index eb93abab..00000000 --- a/.ddd/claims/DDD-gates-02.yaml +++ /dev/null @@ -1,42 +0,0 @@ -format: 2 -id: DDD-gates-02 -statement: > - A pipeline of sequentially-gated CI steps reports what broke first, not - what it did not check: when a step fails, every later independent gate is - skipped, its state for that revision is unknown, and the run still reads - as one red build. The masking is not hypothetical — main at 75b5472 - carried two defects and showed one. `9df74c1` added `note` to `AddArgs` - and updated one of four call sites, so the test targets did not compile; - CI stopped there. Behind the compile error, - `ledger-core/src/author/tests.rs` sat at exactly the 400-line fitness - ceiling, and fixing the three call sites put it at 402 — a second, - independent gate failure that the first one hid, found only when someone - repaired the build by hand (7258af1). The repo already recognises this - failure mode one level down and fixed it there: `cargo t` is - `test --no-fail-fast` precisely so one failing test binary cannot hide - the rest (.cargo/config.toml, and CLAUDE.md says so in bold). The same - reasoning is not applied one level up, across the nine steps of - .github/workflows/product-ci.yml, each of which runs under the default - `if: success()`. Family: this is the shape of the M6 leakage prediction - and of L3's cross-file merge finding — a mechanism guarding the path it - can see, blind to what sits behind it. -status: projected -falsifier: > - Either of two negative results, both about whether the hazard is real - rather than about how the pipeline is currently wired. (a) A review of - this repository's red CI runs — or of local full-sequence runs - reproducing them — finds that in every case the steps skipped by an - earlier failure would themselves have passed, so the masking never - concealed a second defect and 75b5472 was a coincidence rather than an - instance. (b) The 75b5472 case is re-graded: the code-quality gate did - report on that run, and the second defect was visible after all, which - would show the observation was misread. Deliberately drafted so the - remedy cannot fire it: making independent gates run anyway changes what - *future* runs report, and says nothing about whether past masking hid - anything — a claim whose falsifier its own remedy satisfies is the - self-sealing defect DDD-method-07 names, and this is drafted to not - carry it. -owner: none -changed: 2026-08-13 -revalidate_by: 2027-02-13 -depends_on: [] diff --git a/.ddd/claims/DDD-method-01.yaml b/.ddd/claims/DDD-method-01.yaml deleted file mode 100644 index 7816a500..00000000 --- a/.ddd/claims/DDD-method-01.yaml +++ /dev/null @@ -1,17 +0,0 @@ -format: 2 -id: DDD-method-01 -statement: > - Keeping closure status out of definitional entries prevents stale-confidence - drift: a predicate stays valid while findings about it change status. -status: reported -evidence: > - The framework corpus's predicate/claim split (predicate-definition.md, - predicate-format.md): only claims carry status, boundary clauses, and - falsifiers; the split is enforced structurally in ddd-core's schema. -falsifier: > - A predicate whose embedded status would have stayed accurate while the - separate claim graph drifted stale. -owner: none -changed: 2026-08-02 -revalidate_by: 2028-08-07 -depends_on: [] diff --git a/.ddd/claims/DDD-method-02.yaml b/.ddd/claims/DDD-method-02.yaml deleted file mode 100644 index 81f2da02..00000000 --- a/.ddd/claims/DDD-method-02.yaml +++ /dev/null @@ -1,16 +0,0 @@ -format: 2 -id: DDD-method-02 -statement: > - Mined pattern instances lack a principal and become escaped decisions; the - analyzer warning swamp is the canonical instance. -status: reported -evidence: > - The warning swamp (PRD §2): analyzer rule sets nobody can explain, - accumulated without governing commitment, check, or principal. -falsifier: > - A mined catalog whose entries remain explainable to a named principal over - time without curation. -owner: none -changed: 2026-08-02 -revalidate_by: 2028-08-07 -depends_on: [] diff --git a/.ddd/claims/DDD-method-03.yaml b/.ddd/claims/DDD-method-03.yaml deleted file mode 100644 index 847038fe..00000000 --- a/.ddd/claims/DDD-method-03.yaml +++ /dev/null @@ -1,13 +0,0 @@ -format: 2 -id: DDD-method-03 -statement: > - A governance report that renders "not checkable" identically to "checked - and clean" manufactures false assurance: the reader cannot separate - coverage from conformance, and acts on the stronger reading. -status: projected -falsifier: > - A repo where surfacing the uncheckable set changed no reader's action — - i.e. the silent form was never in fact read as an all-clear. -owner: none -changed: 2026-08-07 -revalidate_by: 2027-08-07 diff --git a/.ddd/claims/DDD-method-04.yaml b/.ddd/claims/DDD-method-04.yaml deleted file mode 100644 index 8e117507..00000000 --- a/.ddd/claims/DDD-method-04.yaml +++ /dev/null @@ -1,14 +0,0 @@ -format: 2 -id: DDD-method-04 -statement: > - For a graph committed to version control, the VCS history already answers - who amended what and when, so an in-file mutation counter duplicates the - record while adding a field that can drift from it. -status: projected -falsifier: > - An amendment whose authorship or motive could not be recovered from git - history in practice — for instance entries rewritten by tooling inside - bulk commits. -owner: none -changed: 2026-08-07 -revalidate_by: 2027-08-07 diff --git a/.ddd/claims/DDD-method-05.yaml b/.ddd/claims/DDD-method-05.yaml deleted file mode 100644 index b3cd5651..00000000 --- a/.ddd/claims/DDD-method-05.yaml +++ /dev/null @@ -1,19 +0,0 @@ -format: 2 -id: DDD-method-05 -statement: > - Whether a closure claim transfers to another repo is not knowable when it - is authored; it is established only by a second repo inheriting it without - re-litigation. Filing an entry into shared/ at authoring time therefore - records a prediction about transferability as though it were a property of - the entry, and the prediction is not marked as one anywhere in the store. -status: projected -falsifier: > - A body of entries filed into shared/ at authoring time whose observed - transfer rate matched that of entries promoted on evidence — i.e. - authoring-time judgment of transferability proving as reliable as observed - inheritance. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-02-07 -depends_on: - - DDD-method-01 diff --git a/.ddd/claims/DDD-method-06.yaml b/.ddd/claims/DDD-method-06.yaml deleted file mode 100644 index 16579630..00000000 --- a/.ddd/claims/DDD-method-06.yaml +++ /dev/null @@ -1,19 +0,0 @@ -format: 2 -id: DDD-method-06 -statement: > - Mandatory claim-basis manufactures weak claims: when ontology rule 3 - requires every decision to rest on a claim, decisions whose real basis - is a constraint, mandate, preference, experiment, or priced risk get - backfilled with claims invented to pass validation — the exact - corruption of the claim graph the rule exists to prevent. -status: projected -falsifier: > - An audit of basis quality after typed basis (format 5) ships: if the - pre-format-5 decisions' claim bases audit as genuine — each basedOn - claim would have been filed on its own merits, none exists only to - satisfy rule 3 — the manufactured-claims mechanism did not operate - here and the claim dies. -owner: none -changed: 2026-08-11 -revalidate_by: 2027-02-11 -depends_on: [] diff --git a/.ddd/claims/DDD-method-07.yaml b/.ddd/claims/DDD-method-07.yaml deleted file mode 100644 index d57ee953..00000000 --- a/.ddd/claims/DDD-method-07.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 2 -id: DDD-method-07 -statement: > - Self-sealed falsifiers are a failure mode distinct from manufactured - claims: a decision can remove the arrangement under which its own - grounding claim's falsifier could ever be observed, leaving a claim that - cannot be killed by the store it governs. Three standing instances - (basis-quality audit 2026-08, docs/audits/basis-quality-2026-08.md §3.1): - DDD-arch-04's study needs the declarant-supplied structural fields that - dec/ddd/amend-explicit-evidence-frozen abolished; DDD-method-01's - accurate-embedded-status predicate is a schema rejection under - dec/ddd/predicates-carry-no-status; DDD-method-05's authoring-time - shared/ body is forbidden by dec/ddd/seed-lands-in-claims-not-shared. A - manufactured claim is weak and visible; a self-sealed falsifier looks - strong and is untestable. -status: projected -falsifier: > - One of the three named falsifier conditions is actually observed while - its sealing decision stands — in this store or in any store governed by - the same decisions — or a re-audit of the three cases identifies a - concrete observation route the 2026-08 grading missed and re-grades the - case present under the audit's pre-registered §1.3 criteria. Either is a - negative result a future basis-quality audit can genuinely produce: it - shows the seal was never a seal. An F-batch falsifier repair retires an - instance by ruling, not by observation, and does not fire this; the - claim dies when no standing instance survives scrutiny, not when the - instances are repaired away. -owner: none -changed: 2026-08-12 -revalidate_by: 2027-02-12 -depends_on: [] diff --git a/.ddd/claims/DDD-scope-01.yaml b/.ddd/claims/DDD-scope-01.yaml deleted file mode 100644 index ec0e0e95..00000000 --- a/.ddd/claims/DDD-scope-01.yaml +++ /dev/null @@ -1,16 +0,0 @@ -format: 2 -id: DDD-scope-01 -statement: > - Context& engagement demand concentrates in C# + Bicep repositories, and - Bicep's adapter cost is low because provenance is first-class in its syntax. -status: reported -evidence: > - Engagement history to date; the Bicep adapter policy table (PRD §9.2) maps - ground provenance directly onto literal/param/runtime-resolved syntax. -falsifier: > - An engagement mix where most governed repos are neither C# nor Bicep, or a - Bicep adapter whose policy table cannot express provenance mapping. -owner: none -changed: 2026-08-02 -revalidate_by: 2027-02-07 -depends_on: [] diff --git a/.ddd/claims/DDD-web-01.yaml b/.ddd/claims/DDD-web-01.yaml deleted file mode 100644 index 009d45c1..00000000 --- a/.ddd/claims/DDD-web-01.yaml +++ /dev/null @@ -1,28 +0,0 @@ -format: 2 -id: DDD-web-01 -statement: > - The HTML↔CSS seam is where agent-authored visual work fails when it is - ungoverned: agents drift past the pair boundary — classes consumed with no - definition, selectors styling structure that no longer exists, token - values restated raw — because no predicate closes over the pair and the - only check is a human eyeball. The demand concentrates in Context&'s - visual deliverables, which are single-file HTML+CSS. -status: reported -evidence: > - Repeated observed agent failures on Context& visual deliverables, compiled - as the M7 basis (PRD §10, §13): zero closed predicates over the artifact - class, an undeclared class contract between markup and stylesheet, and - eyeball-only checking as the entire verification arrangement. The PRD - records this as compiled demand evidence — strong enough that a flip - condition against M6 was pre-registered on it — and the M6/M7 ordering - ruling (dec/ddd/m6-proceeds-no-flip) re-examined rather than assumed it. -falsifier: > - Governed pair work in which the seam failures do not occur without - enforcement — e.g. a sustained run of agent-authored HTML+CSS where orphan - classes, dead selectors, or raw restatements of token values stay near - zero with the pair class at `warn` or `off`. The pair contract check's - finding classes measure this directly. -owner: Emil -changed: 2026-08-10 -revalidate_by: 2027-08-10 -depends_on: [] diff --git a/.ddd/claims/DDD-web-02.yaml b/.ddd/claims/DDD-web-02.yaml deleted file mode 100644 index 45eec41c..00000000 --- a/.ddd/claims/DDD-web-02.yaml +++ /dev/null @@ -1,22 +0,0 @@ -format: 2 -id: DDD-web-02 -statement: > - The render page's status colours are contract, not decoration: green, - red, and amber consistently mean passing, failing, and degraded across - every section of the projection, so a reader's verdict-at-a-glance - depends on the palette staying stable and being restated nowhere. Naming - them as design tokens (custom properties) is the arrangement that closes - this — a raw hex restatement is exactly the drift the token exists to - prevent. -status: projected -falsifier: > - A reader-facing regression traceable to a token change that the token - discipline did not catch — e.g. a status hue drifting between sections - despite the palette being tokenised — or, in the other direction, a - sustained absence of raw-value restatements in a sheet with no token - discipline at all. -owner: Emil -changed: 2026-08-10 -revalidate_by: 2027-08-10 -depends_on: - - DDD-web-01 diff --git a/.ddd/claims/DDD-web-visual-01.yaml b/.ddd/claims/DDD-web-visual-01.yaml deleted file mode 100644 index 6df4f1ce..00000000 --- a/.ddd/claims/DDD-web-visual-01.yaml +++ /dev/null @@ -1,38 +0,0 @@ -format: 4 -id: DDD-web-visual-01 -statement: > - "Looks right" is an open predicate no static check closes — but a - deterministic screenshot-diff converts a visual claim from unchecked to - reported-with-evidence: given a fixed browser build, a font-free - fixture, a fixed window and scale factor, the same pair renders to - byte-identical pixels, so any pixel difference is a real visual change, - including the class the interceptor is deliberately silent on (a - same-type token value drift). -status: reported -version_index: - tool: "Chromium 141.0.7390.37 (Playwright headless_shell build 1194)" -evidence: > - Measured 2026-08-10 on this workspace, harness ddd-cli/tests/visual.rs - over the pure-geometry panel fixture - (ddd-cli/tests/fixtures/visual/panel.html). Determinism: two renders of - the unchanged panel produced byte-identical PNGs - (sha256 e3389b15fdcdc7c5be010199b0adc8c68674fef24f99356a237c567a6a24b3fc, - 440x140, --headless --disable-gpu --hide-scrollbars - --force-device-scale-factor=1). Regression: drifting --status-ok from - #1a7f37 to #2ea043 — a same-type token value change, which - web-token-membership/-retype correctly produce no event for — changed - the render to sha256 - 9ad4ed9bdb4e0b5c4673d47876a8621003e0259e492ab038e34c3792cc0ff8ca. The - harness asserts both directions and is gated on a local Chromium, per - the fixtures-not-sdk posture; CI machines without a browser skip it. -falsifier: > - Nondeterminism inside the modelled envelope — the same fixture, browser - build, window and scale factor rendering to different bytes across runs - — or a visual regression class expressible in the fixture vocabulary - (token drift, class loss, layer reorder) that renders byte-identically - and so cannot be caught as pixels. -owner: none -changed: 2026-08-10 -revalidate_by: 2027-08-10 -depends_on: - - DDD-web-02 diff --git a/.ddd/claims/DDD-what-01.yaml b/.ddd/claims/DDD-what-01.yaml deleted file mode 100644 index 2fcf7774..00000000 --- a/.ddd/claims/DDD-what-01.yaml +++ /dev/null @@ -1,15 +0,0 @@ -format: 2 -id: DDD-what-01 -statement: > - The framework's own §-numbered boundary vocabulary — system (§3.2.5), - context mapping (§3.1), journey crossing (§3.0.1), quality demand (§3.6) — - marks where irreversible commitment concentrates in a What graph, so a - policy table over those kinds selects the elements worth a declaration. -status: projected -falsifier: > - A graph where the boundary defects that actually bit originated in kinds - the table calls internal elaboration — an entity, a read-model, a UI step — - rather than in the kinds it calls surface. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-02-07 diff --git a/.ddd/claims/DDD-what-02.yaml b/.ddd/claims/DDD-what-02.yaml deleted file mode 100644 index 46fe16ea..00000000 --- a/.ddd/claims/DDD-what-02.yaml +++ /dev/null @@ -1,21 +0,0 @@ -format: 2 -id: DDD-what-02 -statement: > - Every domain event and command in a What graph is a published contract — - some consumer outside its own bounded context reads it — so each one is - boundary-forming and worth a declaration. -status: retired -evidence: > - Killed 2026-08-07 by measurement over the two reference graphs. Of 20 - events and 19 commands, 0 were referenced from any context other than - their own, and 0 read-models projected events from more than one context. - The framework does not carry events across boundaries by direct reference: - crossings run through §3.2.0 Translation triggers, of which the two graphs - hold three in total. Under the framework's own crossing structure the - published set is 4 elements in product-cli and 3 in acme, against the 39 - the unqualified rows called surface. Replaced by DDD-what-03. -falsifier: > - A conformant What graph in which most events and commands are consumed - only inside their own bounded context. -owner: Emil -changed: 2026-08-07 diff --git a/.ddd/claims/DDD-what-03.yaml b/.ddd/claims/DDD-what-03.yaml deleted file mode 100644 index 844adbbb..00000000 --- a/.ddd/claims/DDD-what-03.yaml +++ /dev/null @@ -1,22 +0,0 @@ -format: 2 -id: DDD-what-03 -statement: > - A What element is published exactly when it participates in a §3.2.0 - Translation crossing: the View a Translation watches, the Command it - issues, and the Events that View projects. Everything else is internal to - its own system whatever its kind, because the framework carries nothing - across a boundary except through a Translation. -status: reported -evidence: > - Measured over both reference graphs 2026-08-07. Direct cross-context - reference of events and commands is 0 of 39; the three Translations the - graphs declare carry 4 published elements in product-cli and 3 in acme. - The crossing structure, not the reference graph, is where boundary - traffic actually shows up. -falsifier: > - A boundary defect originating in an element this rule calls internal — - an event or command consumed across a system boundary by a path that does - not run through a Translation trigger. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-02-07 diff --git a/.ddd/claims/DDD-what-04.yaml b/.ddd/claims/DDD-what-04.yaml deleted file mode 100644 index 556ab781..00000000 --- a/.ddd/claims/DDD-what-04.yaml +++ /dev/null @@ -1,23 +0,0 @@ -format: 2 -id: DDD-what-04 -statement: > - Both What graphs in this repo are demonstration content — the acme - showcase and the self-hosted product-cli reference — and neither has a - second party that reads its elements across a boundary. A seam declared - over one of their 24 boundaries could therefore carry no verdict knowledge - that anything downstream depends on: the empty-verdict_knowledge case the - declaration surface already warns about as seam cost with no demand - absorbed. -status: projected -falsifier: > - A party that breaks on a change to a boundary element of either graph — a - downstream repo, generated code, or an engagement repo that inherits - either graph — or a boundary defect in this repo traced to one of the 24 - undeclared elements. Test suites asserting on graph validity do not - count: they consume the graph's well-formedness, not a boundary contract. -owner: Emil -changed: 2026-08-07 -revalidate_by: 2027-02-07 -depends_on: - - DDD-what-01 - - DDD-what-03 diff --git a/.ddd/concordance.yaml b/.ddd/concordance.yaml deleted file mode 100644 index 7759f464..00000000 --- a/.ddd/concordance.yaml +++ /dev/null @@ -1,295 +0,0 @@ -format: 1 -note: 'Permanent id concordance for the M8 ledger migration (ruling 6: the aliases are permanent, not transitional). Historical ids — decisions, the risk record, seams, manifest entries — remain valid forever; `ddd why` resolves either spelling. Claim ids are not re-keyed: claims remain the ddd ontology''s own.' -rows: -- historical: dec/ddd/ci-gates-report-skipped - ledger: dec:hafeok.ddd/01KZX7HW7J4FET6H3JW8EQTK0R - kind: decision - disposition: 'filed 2026-08-13, after the M8 migration: the .ddd entry carries the rationale and the typed bases, the ledger entry is the record of the decision. Same pairing the migration produced, authored rather than migrated.' -- historical: dec/bicep/linter-at-error - ledger: dec:hafeok.ddd/01KZTGGGS14S17F2P5K6EZEZXH - kind: decision -- historical: dec/bicep/psrule-in-pr-stage - ledger: dec:hafeok.ddd/01KZTGGGXG10JDSZ71EVZMSHMF - kind: decision -- historical: dec/cs/async-policy-rules-at-error - ledger: dec:hafeok.ddd/01KZTGGH252JQYS5W79V8E1NXW - kind: decision -- historical: dec/cs/authorization-fallback-deny - ledger: dec:hafeok.ddd/01KZTGGH76M8P8WE7BBP7958G8 - kind: decision -- historical: dec/cs/disposal-rules-at-error - ledger: dec:hafeok.ddd/01KZTGGHCBFNJW3X4R8FVF6TQG - kind: decision -- historical: dec/cs/general-catch-at-error - ledger: dec:hafeok.ddd/01KZTGGHHKV99BGJV5EV7V460R - kind: decision -- historical: dec/cs/policy-rules-at-error - ledger: dec:hafeok.ddd/01KZTGGHQ536SNDFP5H3Q22D0K - kind: decision -- historical: dec/cs/startup-assertions-required - ledger: dec:hafeok.ddd/01KZTGGHXBQGSCZBFKXKRN7A13 - kind: decision -- historical: dec/cs/unsafe-reinterpretation-banned - ledger: dec:hafeok.ddd/01KZTGGJ33J40CYGTVH0H20SK0 - kind: decision -- historical: dec/ddd/adapter-policy-tables - ledger: dec:hafeok.ddd/01KZTGGJ91BAQ4CDT0KATFTY0B - kind: decision -- historical: dec/ddd/amend-explicit-evidence-frozen - ledger: dec:hafeok.ddd/01KZTGGJFEP2A4MK7W2B1572D2 - kind: decision -- historical: dec/ddd/correspondence-rows-are-stratified - ledger: dec:hafeok.ddd/01KZTGGJNQ8YPMA9JTAJ660N3M - kind: decision -- historical: dec/ddd/curation-over-mining - ledger: dec:hafeok.ddd/01KZTGGJW50DDJHMF4CERVQRJ3 - kind: decision -- historical: dec/ddd/enforce-matching-tightens-to-symbol - ledger: dec:hafeok.ddd/01KZTGGK2Q197NGD28GKQAFQKZ - kind: decision -- historical: dec/ddd/enum-member-gap-priced - ledger: dec:hafeok.ddd/01KZTGGK9FT56BFDWQ67J6XA54 - kind: decision -- historical: dec/ddd/fixtures-not-sdk - ledger: dec:hafeok.ddd/01KZTGGKGB3QCVQTH1H1E1BC5H - kind: decision -- historical: dec/ddd/git-is-the-amend-trail - ledger: dec:hafeok.ddd/01KZTGGKQDQFWMACNN8QNE9Z65 - kind: decision -- historical: dec/ddd/interceptor-not-extension - ledger: dec:hafeok.ddd/01KZTGGKYN3VT9XYPATK0S0SAA - kind: decision -- historical: dec/ddd/internal-not-surface - ledger: dec:hafeok.ddd/01KZTGGM6BRXDVPSDRNKW1B5YJ - kind: decision -- historical: dec/ddd/lsp-as-seam - ledger: dec:hafeok.ddd/01KZTGGMEACBFMTC1RJJ8T90GS - kind: decision -- historical: dec/ddd/m6-proceeds-no-flip - ledger: dec:hafeok.ddd/01KZTGGMPHCW14AH6AJW4YSZ5Z - kind: decision -- historical: dec/ddd/m8-enforcement-closure - ledger: dec:hafeok.ddd/01KZTGGMYTQ5ZV5JV8TK804S48 - kind: decision -- historical: dec/ddd/pins-at-m2 - ledger: dec:hafeok.ddd/01KZTGGN7CTBMP82NEQWYGNP11 - kind: decision -- historical: dec/ddd/prd-split - ledger: dec:hafeok.ddd/01KZTGGNGC09PHHCRB0S0971YH - kind: decision -- historical: dec/ddd/predicates-carry-no-status - ledger: dec:hafeok.ddd/01KZTGGNSG6N3G6FPMTTYSHNCQ - kind: decision -- historical: dec/ddd/rejection-facts-prefilled - ledger: dec:hafeok.ddd/01KZTGGP8R9PHR599ZE9RV3BDC - kind: decision -- historical: dec/ddd/report-coverage-explicit - ledger: dec:hafeok.ddd/01KZTGGPHW32QV7DN8RF4Q243Q - kind: decision -- historical: dec/ddd/rust-class-enforced-here - ledger: dec:hafeok.ddd/01KZTGGPV4CM246FBEPP9V7RN5 - kind: decision -- historical: dec/ddd/rust-host-is-real - ledger: dec:hafeok.ddd/01KZTGGQ4HNQEEB3P4SQSZ84CX - kind: decision -- historical: dec/ddd/sarif-unification - ledger: dec:hafeok.ddd/01KZTGGQFE7AMJ86TRBBS24NJE - kind: decision -- historical: dec/ddd/seed-lands-in-claims-not-shared - ledger: dec:hafeok.ddd/01KZTGGQS9XVSMD0PEEQPE9W7V - kind: decision -- historical: dec/ddd/typed-basis - ledger: dec:hafeok.ddd/01KZTGGR3D8F6CQKJTY0Q611HX - kind: decision -- historical: dec/ddd/v1-scope - ledger: dec:hafeok.ddd/01KZTGGRE1ZQPF470GF85TH57F - kind: decision -- historical: dec/ddd/what-boundaries-priced-not-paid - ledger: dec:hafeok.ddd/01KZTGGRSFCGMZFY9ECCGWTCA7 - kind: decision -- historical: dec/ddd/what-policy-table - ledger: dec:hafeok.ddd/01KZTGGS4A3EA9K6PDRB42JPES - kind: decision -- historical: dec/ddd/what-published-qualifier - ledger: dec:hafeok.ddd/01KZTGGSFECY2REBK9HESM5Y25 - kind: decision -- historical: dec/ddd/why-resolves-three-ways - ledger: dec:hafeok.ddd/01KZTGGSV79Y1XVDV1T3WZTVJP - kind: decision -- historical: dec/ddd/workspace-member-delivery - ledger: dec:hafeok.ddd/01KZTGGT76Q28R47FAN4Y7G0MP - kind: decision -- historical: dec/rust/no-unwrap - ledger: dec:hafeok.ddd/01KZTGGTKM7T8Y6GY78TGJRNB8 - kind: decision -- historical: dec/web/htmlcss-enforce - ledger: dec:hafeok.ddd/01KZTGGV06DE3NXC5D8EQ2C17Z - kind: decision -- historical: dec/web/markup-validity - ledger: dec:hafeok.ddd/01KZTGGVHB66P88QKRC6P91G1J - kind: decision -- historical: dec/web/plain-pair-scope - ledger: dec:hafeok.ddd/01KZTGGVXY1K4WNBMVYWQADFK1 - kind: decision -- historical: dec/web/render-status-palette - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: decision -- historical: dec/web/token-discipline - ledger: dec:hafeok.ddd/01KZTGGWQGN5BKXV9MV384XZYR - kind: decision -- historical: risk/ddd/undeclared-what-boundaries - ledger: dec:hafeok.ddd/01KZTGGX5ABSQ2PVTQ32NPKVNE - kind: risk-acceptance -- historical: seam/ledger/acceptor-identity - ledger: dec:hafeok.ddd/01KZTGGXJB5EYGJBCXTYPA52D2 - kind: seam -- historical: seam/ledger/blame-attribution - ledger: dec:hafeok.ddd/01KZTGGY0PQX1B7BB199FNTWAJ - kind: seam -- historical: seam/ledger/canonical-form - ledger: dec:hafeok.ddd/01KZTGGYE2K8B3Y2591J381J18 - kind: seam -- historical: seam/ledger/disposition-states - ledger: dec:hafeok.ddd/01KZTGGYVS9BH1TAJ6F7RDVC0B - kind: seam -- historical: seam/ledger/graph-stage - ledger: dec:hafeok.ddd/01KZTGGZ9PA74EY10PD45S2AD3 - kind: seam -- historical: seam/ledger/tolerance-floor - ledger: dec:hafeok.ddd/01KZTGGZQPCF64P812DWPW00ZM - kind: seam -- historical: seam/ledger/ulid-mint - ledger: dec:hafeok.ddd/01KZTGH06V3WDM9DKKY4XNR308 - kind: seam -- historical: seam/ledger/verify-classes - ledger: dec:hafeok.ddd/01KZTGH0TTYTBXQC4H17FKKQ16 - kind: seam -- historical: seam/ledger/version-hash - ledger: dec:hafeok.ddd/01KZTGH19T9A0F4X2N3M2S4N6F - kind: seam -- historical: seam/ledger/write-refusal - ledger: dec:hafeok.ddd/01KZTGH1RKZKHPZ0CKMTS5PD4C - kind: seam -- historical: seam/mcp/raw-str - ledger: dec:hafeok.ddd/01KZTGH27P2ZA15QPD7DA4XGQ3 - kind: seam -- historical: seam/rust/default-command - ledger: dec:hafeok.ddd/01KZTGH2Q154ZNDAP2ANR37GBR - kind: seam -- historical: seam/rust/host-command - ledger: dec:hafeok.ddd/01KZTGH375TYC2TTVXZWA2EGWZ - kind: seam -- historical: seam/rust/readysignal-debug - ledger: dec:hafeok.ddd/01KZTGH3PYAF786QBW6696YNAE - kind: seam -- historical: seam/web/render-bad - ledger: dec:hafeok.ddd/01KZTGH46RGZHM294TE5V4P9TY - kind: seam -- historical: seam/web/render-card - ledger: dec:hafeok.ddd/01KZTGH4QJSZ7JG83BY2X4X3FD - kind: seam -- historical: seam/web/render-chip - ledger: dec:hafeok.ddd/01KZTGH58176ZFN8JGZA3RV2ZP - kind: seam -- historical: seam/web/render-established - ledger: dec:hafeok.ddd/01KZTGH5RPND4W7GF7KNGWTRAD - kind: seam -- historical: seam/web/render-muted - ledger: dec:hafeok.ddd/01KZTGH69V9E6KQAJ3R1V8JA46 - kind: seam -- historical: seam/web/render-ok - ledger: dec:hafeok.ddd/01KZTGH6TKXR2716T1JNVA3RD0 - kind: seam -- historical: seam/web/render-projected - ledger: dec:hafeok.ddd/01KZTGH7BTS5NFBJWSYMATJEFG - kind: seam -- historical: seam/web/render-reported - ledger: dec:hafeok.ddd/01KZTGH7XB28RYD76VB67R70HJ - kind: seam -- historical: seam/web/render-retired - ledger: dec:hafeok.ddd/01KZTGH8FFSY9ESJA7PWK8MX1N - kind: seam -- historical: seam/web/render-warn - ledger: dec:hafeok.ddd/01KZTGH919SYFNSGA1BYPXXSG7 - kind: seam -- historical: seam/web/token-ink - ledger: dec:hafeok.ddd/01KZTGH9KHPW0JJ09BERGK6MXX - kind: seam -- historical: seam/web/token-ink-faint - ledger: dec:hafeok.ddd/01KZTGHA61E2YTA0T75RDR7NE6 - kind: seam -- historical: seam/web/token-ink-muted - ledger: dec:hafeok.ddd/01KZTGHARQSN5QW37AR08GRYZA - kind: seam -- historical: seam/web/token-on-chip - ledger: dec:hafeok.ddd/01KZTGHBB8DCV5RBYJW4H56Q5X - kind: seam -- historical: seam/web/token-status-bad - ledger: dec:hafeok.ddd/01KZTGHBYNKPRB10V6EEJQXX9B - kind: seam -- historical: seam/web/token-status-ok - ledger: dec:hafeok.ddd/01KZTGHCHT7EKK6YA9TZ9BDZA6 - kind: seam -- historical: seam/web/token-status-warn - ledger: dec:hafeok.ddd/01KZTGHD5BJR20S0JDX0NM0838 - kind: seam -- historical: seam/web/token-surface - ledger: dec:hafeok.ddd/01KZTGHDS9ZYW4ZT944RV1D4N7 - kind: seam -- historical: seam/web/token-surface-raised - ledger: dec:hafeok.ddd/01KZTGHEE7SH5CZJS4B8AZN0WC - kind: seam -- historical: question/ddd/watched-edge-kind - ledger: dec:hafeok.ddd/01KZTGHF4B718YCRCKCGVM7XWA - kind: question -- historical: clippy/clippy::unwrap_used - ledger: dec:hafeok.ddd/01KZTGGTKM7T8Y6GY78TGJRNB8 - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/rust/no-unwrap)' -- historical: htmlvalidate/no-dup-class - ledger: dec:hafeok.ddd/01KZTGGVHB66P88QKRC6P91G1J - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/markup-validity)' -- historical: stylelint/declaration-property-value-disallowed-list - ledger: dec:hafeok.ddd/01KZTGGWQGN5BKXV9MV384XZYR - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/token-discipline)' -- historical: tokens/--ink - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--ink-muted - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--ink-faint - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--status-ok - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--status-bad - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--status-warn - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--on-chip - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--surface - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: tokens/--surface-raised - ledger: dec:hafeok.ddd/01KZTGGWARF9G6KQGNNZ2239QF - kind: manifest-entry - disposition: 'absorbed as an analyzer: discharge on its governing decision (dec/web/render-status-palette)' -- historical: DDD-method-07 - kind: claim - disposition: remains a .ddd claim — claims are the ddd ontology's own and do not migrate; listed explicitly because it was created after the concordance was scoped diff --git a/.ddd/config.yaml b/.ddd/config.yaml deleted file mode 100644 index f35bf035..00000000 --- a/.ddd/config.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# .ddd/config.yaml — repo-level DDD governance configuration (format v4). -format: 4 - -# Interception mode for contract-surface edits: enforce | warn | off (PRD §8). -# The global default stays `warn` for any artifact class not named below. -intercept: warn - -# Per-artifact-class modes. `code` is this workspace's own Rust sources -# (dec/ddd/rust-class-enforced-here): from M6 on, a contract-surface edit is -# rejected until a seam declaration covers it. `html-css` is the M7 pair — -# the tool's own projection surface — enforced per dec/web/htmlcss-enforce: -# warn is exactly the exhortation the observed failure mode ignores. -intercept_by_class: - code: enforce - html-css: enforce - -# Per-language adapter switches. `pub(crate)` is Rust's `internal`, so it -# rides the same switch; left at the default (off) per the app-repo posture -# in dec/ddd/internal-not-surface. -adapter: - rust: - internal_is_surface: false - -# The governed HTML+CSS pair (M7): the render stylesheet plus the committed -# representative projection page, checked bidirectionally by -# `ddd report escapes` (orphan classes / dead selectors). -pair: - units: - - html: ["docs/examples/ddd-render-sample.html"] - css: ["ddd-core/assets/render.css"] - ignore_classes: [] - -# Detection sources beyond the config walk (format 4): the render -# stylesheet is a registered token file, so each custom property resolves -# through `ddd why` to its decision and `ddd diff` flags UNGOVERNED or -# STALE tokens. -detect: - tokens: ["ddd-core/assets/render.css"] - -# Globs the interceptor plus detection skip. The M2 fixtures carry config -# files for the tests, not for this repo's governance; the M6 Rust fixture -# crate is fixture source, not governed source, and the M8 classifier -# corpus (labelled before/after case files) is measurement data under the -# same rule. -ignore: - - "ddd-cli/tests/fixtures/**" - - "ddd-cli/tests/corpus/**" diff --git a/.ddd/decisions/.gitkeep b/.ddd/decisions/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/.ddd/decisions/adapter-policy-tables.yaml b/.ddd/decisions/adapter-policy-tables.yaml deleted file mode 100644 index b5dea51f..00000000 --- a/.ddd/decisions/adapter-policy-tables.yaml +++ /dev/null @@ -1,37 +0,0 @@ -format: 2 -id: dec/ddd/adapter-policy-tables -title: Contract surface is defined per-language in adapter policy tables -rationale: > - An adapter answers exactly three questions (what is contract-surface, what - visibility means, what a signature change is); the tables are themselves - claims, falsifiable against where boundary defects occur. Wrong rows get - fixed in the adapter, never in the core. -principal: Emil (Context&) -based_on: - - claim: DDD-adapter-01 - status: projected - changed: 2026-08-02 -date: 2026-08-02 -notes: > - Pin backfilled at M5 from git, not from the present: the decision was filed - in 136aa55 (2026-08-02) and DDD-adapter-01 has not been touched since, so - the status and changed values above are the claim's state at decision time, - recovered rather than assumed. Not a re-decision. - - - Basis re-affirmed 2026-08-10 (Emil). DDD-adapter-01 moved projected -> - reported on M6's contract-surface evidence, so this decision's basis - strengthened rather than weakened and nothing in the rationale is - re-litigated. The pin above is deliberately left at its decision-time value, - per the sentence before this one and PRD §6 rule 6: it records what was - known on 2026-08-02, not what is known now. `ddd report escapes` will - therefore keep listing this edge as basis loss, correctly — the basis did - move, and this note is the record that it was looked at. - - Basis re-affirmed again 2026-08-10 (Emil), at M7's acceptance: - DDD-adapter-01 moved reported -> established after surviving its - scheduled falsification attempt at language four (the non-LSP HTML+CSS - pair; zero contract-surface leakage). The basis strengthened a second - time; nothing is re-litigated, the pin stays at its decision-time value - per PRD §6 rule 6, and this note is the record that the moved edge was - looked at. diff --git a/.ddd/decisions/amend-explicit-evidence-frozen.yaml b/.ddd/decisions/amend-explicit-evidence-frozen.yaml deleted file mode 100644 index c5cce3ee..00000000 --- a/.ddd/decisions/amend-explicit-evidence-frozen.yaml +++ /dev/null @@ -1,21 +0,0 @@ -format: 2 -id: dec/ddd/amend-explicit-evidence-frozen -title: Declarations amend through an explicit flag; LSP-derived evidence is frozen -rationale: > - declare_seam, declare_pattern and accept_risk take `amend: true`, which - files only over an existing id and errors on a missing one, so create and - amend cannot be confused in either direction and no silent upsert exists. - Judgement fields amend and omitted fields are preserved: verdict_knowledge, - obligations, obligation answers, boundary, rationale, notes. Identity and - LSP-derived evidence — id, contract_location, metadata — carry forward from - the filed entry and are ignored if supplied, because the correspondence - dataset is only evidence while those fields stay machine-authored. -principal: Emil (Context&) -based_on: - - claim: DDD-friction-02 - status: projected - changed: 2026-08-07 - - claim: DDD-arch-04 - status: projected - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/bicep-linter-at-error.yaml b/.ddd/decisions/bicep-linter-at-error.yaml deleted file mode 100644 index c2e776ac..00000000 --- a/.ddd/decisions/bicep-linter-at-error.yaml +++ /dev/null @@ -1,41 +0,0 @@ -format: 2 -id: dec/bicep/linter-at-error -title: The Bicep core linter runs at error severity, with the name heuristic named -rationale: > - The DAD §5.2 rule set at error is cheap and demonstrated: DDD-bicep-ver-01 - shows use-recent-api-versions carrying a full rejection witness — the - version, its age in days, and the acceptable alternatives — and - DDD-bicep-sec-01 shows the secret rules covering both the leak and the - missing annotation, in cooperation with PSRule. - - - Adopted with its limit stated, because DDD-bicep-sec-02 measured it: the - entire secret chain keys off the parameter's identifier. The same - unannotated secret that drew findings from both tools as `adminPassword` - drew nothing from either once renamed to `seedValue`. So this decision - buys enforcement for secrets that look like secrets, and buys nothing at - all for the ones that do not. Naming conventions for secret-bearing - parameters are therefore load-bearing governance here rather than style, - which is an unusual thing to have to write down and is exactly why it is - written down. - - - Correction carried from the seed: the DAD §5.2 configuration lists - secure-parameter-default, while the rule that actually fires on an - unannotated secret parameter is secure-secrets-in-params. A repo adopting - the configuration verbatim should expect that difference. - - - Scope: governed Bicep repositories under Context& engagement. -principal: Emil (Context&) -based_on: - - claim: DDD-bicep-ver-01 - status: reported - changed: 2026-08-07 - - claim: DDD-bicep-sec-01 - status: reported - changed: 2026-08-07 - - claim: DDD-bicep-sec-02 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/bicep-psrule-in-pr-stage.yaml b/.ddd/decisions/bicep-psrule-in-pr-stage.yaml deleted file mode 100644 index 5982e9df..00000000 --- a/.ddd/decisions/bicep-psrule-in-pr-stage.yaml +++ /dev/null @@ -1,38 +0,0 @@ -format: 2 -id: dec/bicep/psrule-in-pr-stage -title: PSRule for Azure runs in the PR stage, and its expansion is asserted -rationale: > - Network exposure and diagnostic settings are unreachable by the Bicep - linter and reachable by PSRule — DDD-bicep-net-01 and DDD-bicep-diag-01 - measured both halves. Neither predicate is closable in an arrangement that - runs only the linter, so the second tool earns its place in the PR stage - where the DAD §5.1 ground table puts static checks. - - - The load-bearing part of this decision is the second clause. PSRule's Bicep - expansion requires the standalone Bicep CLI on PATH and every parameter - resolvable; when either is missing it emits "no matching rules were found" - and reports zero failures — indistinguishable, in an exit code, from a - clean run. A gate that passes vacuously is worse than no gate, because it - reports assurance it did not obtain. So adopting PSRule means asserting - that expansion happened: a minimum evaluated-resource count, or a fixture - template with a known failure that must fail. Absent that assertion this - decision buys nothing and hides that fact. - - - Not covered by either tool, and staying with judgment: role assignment - scope (DDD-bicep-rbac-01), which neither evaluates at all; the ownership - assumption in a consumed `existing` reference (DDD-bicep-exist-01); and - sizing values (DDD-bicep-sku-01), where PSRule reaches only consequences. - - - Scope: governed Bicep repositories under Context& engagement. -principal: Emil (Context&) -based_on: - - claim: DDD-bicep-net-01 - status: reported - changed: 2026-08-07 - - claim: DDD-bicep-diag-01 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/ci-gates-report-skipped.yaml b/.ddd/decisions/ci-gates-report-skipped.yaml deleted file mode 100644 index 8bb88340..00000000 --- a/.ddd/decisions/ci-gates-report-skipped.yaml +++ /dev/null @@ -1,65 +0,0 @@ -format: 7 -id: dec/ddd/ci-gates-report-skipped -title: A CI run reports which gates did not execute; independent gates run anyway -rationale: > - A red build must say what it did not check, not only what broke first. - Two forms, and the weaker one is the floor rather than the fallback. - - - The full form: a step that does not depend on a failed step runs anyway. - Clippy, the ledger gate, the ddd gate, the convention check and the - supply-chain check are independent of each other; today a failure in any - of them silently cancels the rest. They run under `if: ${{ !cancelled() }}` - (or equivalent) so one revision produces one complete verdict. - - - The minimum viable form, which holds even where the full form cannot: the - run reports which gates did not execute. Some steps genuinely depend on - an earlier one — a gate that needs a compiled workspace cannot run - without one — so "run everything" is not universally available, and a - build that cannot run a check must say so rather than leave the reader to - infer a pass from a silence. Unknown and passing are different states and - the report must distinguish them. - - - Scope note, stated so it is not mistaken for done: this entry rules the - arrangement; it does not implement it. The follow-up is a workflow edit - plus a summary step. Its cost is that a red build stops failing fast — - every independent gate's own runtime is then paid on every red run, the - benchmark step most of all — and that cost is the point rather than a - regression: the run buys a complete verdict with it. The minimum-viable - form costs a summary step and nothing else, which is why it is the floor. -principal: Emil (Context&) -based_on: - - type: preference - statement: >- - A gate's report should distinguish "checked and passed" from "not - checked", because a reader who cannot tell them apart will read - silence as a pass — and the arrangement that produces the silence is - an accident of step ordering, not a judgment about what mattered. - - type: claim - claim: DDD-gates-02 - status: projected - changed: 2026-08-13 - content: sha256:65bfeff0770e7de976127d54d87c3428a6e4458d0f6b6088fc0b452a4c6cb89e -date: 2026-08-13 -notes: > - Filed 2026-08-13 for the principal's acceptance; not implemented in the - session that filed it, by that session's own scope. - - - Allocated as a judgment rather than a criterion on purpose. Nothing - currently checks this arrangement, so holding to it rests on a person, - and a criterion would name a discharge pointer that resolves to no - checker — the manufactured-basis shape the 2026-08 basis-quality audit - found and this store now refuses. When the workflow edit lands, the - honest move is to re-allocate this decision to a criterion whose - discharge names the check that then exists; that is a new version, not an - edit. - - - The claim edge is genuine ground here, not a citation: DDD-gates-02 is an - observation of an instance that actually occurred in this repository, and - if that instance is re-graded away this decision loses its reason. It is - therefore filed in `based_on`, not `revisit_if` — the distinction the - 2026-08-13 ruling draws, applied on its first new use. diff --git a/.ddd/decisions/correspondence-rows-are-stratified.yaml b/.ddd/decisions/correspondence-rows-are-stratified.yaml deleted file mode 100644 index 7ab3a137..00000000 --- a/.ddd/decisions/correspondence-rows-are-stratified.yaml +++ /dev/null @@ -1,32 +0,0 @@ -format: 2 -id: dec/ddd/correspondence-rows-are-stratified -title: The correspondence dataset is a surface-stratum sample; the limit is recorded, not removed -rationale: > - Takes the second choice M3/M4 flagged. Logging only classified-surface - outcomes is the right default — a row per keystroke-scale non-surface apply - would swamp the store, and off-mode exists precisely so a repo can opt out - of the machinery. The selectivity stays. - - - What must not stay implicit is what the selectivity costs. PRD §8 frames - these rows as the correspondence dataset — I(V;S) against real interface - cost — and the tempting first question of such a dataset is a base rate: - what share of edits touch contract surface. DDD-arch-06 says that question - is unanswerable here, and a reader who does not know the rows are - stratified will answer it anyway and be wrong in a direction that flatters - the tool. Filing the limit as a claim is what makes it visible at the point - of analysis rather than discoverable only by reading the interceptor. - - - Consequence accepted: the rows support within-stratum questions — which - kinds of surface change, at what reference counts, get declared versus - refused — and no question whose denominator is all edits. If a base rate is - ever wanted, it needs its own instrument, counting apply calls rather than - surface events, and that is a new decision rather than a reinterpretation - of these rows. -principal: Emil (Context&) -based_on: - - claim: DDD-arch-06 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-async-policy-rules-at-error.yaml b/.ddd/decisions/cs-async-policy-rules-at-error.yaml deleted file mode 100644 index 37502fc7..00000000 --- a/.ddd/decisions/cs-async-policy-rules-at-error.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 2 -id: dec/cs/async-policy-rules-at-error -title: CA2007 and CA2016 run at error severity in governed C# repos -rationale: > - The continuation context and the cancellation token are policy arguments - like culture and comparison, so they belong under the same treatment as - dec/cs/policy-rules-at-error: SDK rules, no package to add, cost is - severity only. - - - What each buys is not the same and the difference is worth carrying. - CA2007 reports every await that takes SynchronizationContext.Current, which - is the whole of its predicate case. CA2016 reports only a method that holds - a token and drops it; a method exposing no token at all is not reported and - cannot be, so adopting this decision closes forwarding and leaves presence - open. A repo that reads a green CA2016 as "cancellation is handled" has - misread it, and DDD-cs-pol-04 is the entry that says so. - - - Scope: governed C# engagement repos, operative where a manifest cites it. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-pol-03 - status: reported - changed: 2026-08-07 - - claim: DDD-cs-pol-04 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-authorization-fallback-deny.yaml b/.ddd/decisions/cs-authorization-fallback-deny.yaml deleted file mode 100644 index 9313c597..00000000 --- a/.ddd/decisions/cs-authorization-fallback-deny.yaml +++ /dev/null @@ -1,30 +0,0 @@ -format: 2 -id: dec/cs/authorization-fallback-deny -title: Hosts declare a deny-by-default fallback authorization policy -rationale: > - DDD-cs-authz-01 measured the default: an endpoint with no attribute answers - 200 OK, and the same endpoint answers a challenge once a fallback policy is - set, with its own code unchanged. So the choice is not whether to govern - each endpoint but which way silence points. - - - Deny-by-default is adopted because the failure modes are asymmetric. A - wrongly protected endpoint is discovered by the first person who needs it; - a wrongly open one is discovered by whoever finds it first, and nothing in - the build reports either. This closes the predicate by composition rather - than by a rule, which means it is one line in the composition root and - therefore also one line to delete — worth an arch test rather than trust. - - - Not implied: a fallback requiring only an authenticated user admits every - authenticated caller. That is a stated default rather than a correct one, - and per-endpoint authorization remains a decision this does not make. - - - Scope: governed ASP.NET Core hosts under Context& engagement. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-authz-01 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-disposal-rules-at-error.yaml b/.ddd/decisions/cs-disposal-rules-at-error.yaml deleted file mode 100644 index d3a86467..00000000 --- a/.ddd/decisions/cs-disposal-rules-at-error.yaml +++ /dev/null @@ -1,37 +0,0 @@ -format: 2 -id: dec/cs/disposal-rules-at-error -title: CA1001 and CA2000 run at error severity, with their unreliability recorded -rationale: > - Field-held disposal is the one part of the disposal family the SDK - analyzers reach, and DDD-cs-disp-02 shows they reach it from both ends — - CA1001 on a type owning a disposable field without being disposable, - CA2000 on a caller that drops a correct owner. At error, the common leak - shape stops compiling. - - - Adopted with an unusual caveat, because DDD-cs-disp-05 measured it: CA2000 - is neither sound nor complete, and it errs in both directions in the same - file — reporting a site where disposal is written, staying silent on a - container hand-off where disposal appears nowhere. Running it at error - therefore buys real coverage and also guarantees some false rejections. The - suppression path exists for exactly that, and ontology rule 4 makes each - suppression cite a risk-acceptance record, which is the right cost: an - argued exception rather than a downgraded severity. - - - What it does not buy: async disposal where a type implements both - interfaces (DDD-cs-disp-03), container hand-off (DDD-cs-disp-04), and - HttpClient lifetime, which satisfies the predicate outright - (DDD-cs-disp-06). Those stay with judgment. - - - Scope: governed C# engagement repos, operative where a manifest cites it. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-disp-02 - status: reported - changed: 2026-08-07 - - claim: DDD-cs-disp-05 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-general-catch-at-error.yaml b/.ddd/decisions/cs-general-catch-at-error.yaml deleted file mode 100644 index 56a90899..00000000 --- a/.ddd/decisions/cs-general-catch-at-error.yaml +++ /dev/null @@ -1,26 +0,0 @@ -format: 2 -id: dec/cs/general-catch-at-error -title: CA1031 runs at error severity; a general catch cites a decision or does not compile -rationale: > - The second manifest candidate. DDD-cs-fail-01 establishes that CA1031 - reports the general-catch shape from the SDK analyzers alone. Raising it to - error makes the DAD §6.2 reading operational: `catch (Exception)` stops - being something a reviewer might notice and becomes something a build - rejects, so the escape has to be argued for rather than defaulted into. - - - What this does not buy is stated in the claim: CA1031 checks the breadth of - the caught type and nothing about the continuation. A narrowed catch with - an empty body still compiles. That residual is real and stays allocated to - review until the DAD §4.4 escape-site analyzer exists, which is a separate - entry because it is a build-a-thing decision rather than a severity one. - - - Scope as with dec/cs/policy-rules-at-error: governed C# engagement repos, - operative where a manifest cites it. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-fail-01 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-policy-rules-at-error.yaml b/.ddd/decisions/cs-policy-rules-at-error.yaml deleted file mode 100644 index a7e4ae82..00000000 --- a/.ddd/decisions/cs-policy-rules-at-error.yaml +++ /dev/null @@ -1,31 +0,0 @@ -format: 2 -id: dec/cs/policy-rules-at-error -title: Comparison and format-provider rules run at error severity in governed C# repos -rationale: > - The first manifest candidate this seed produces. DDD-cs-pol-01 establishes - that CA1305, CA1307, CA1309 and CA1310 close the comparison and culture - cases of pred/code/explicit-policy-argument using the SDK's own analyzers — - no package to add, no cost beyond severity. At their default severity they - are suggestions, which constrains nothing; at error they convert a standing - review question into a build failure, which is the whole of the DAD §4.1 - argument. - - - Scope: governed C# repositories under Context& engagement, not this - workspace, which is Rust and has no C# to govern. The entry is filed here - because the catalog is where determinations live; it becomes operative in - a repo whose manifest cites it. - - - Not covered, and deliberately not implied: the ambient clock. DDD-cs-pol-02 - records that DateTime.Now draws nothing from this rule set, so a repo - adopting this decision has closed three of the four cases in the predicate - and should not read it as having closed the fourth. Closing that one needs - an added analyzer package, which is a dependency decision with its own - cost and belongs in its own entry. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-pol-01 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-startup-assertions-required.yaml b/.ddd/decisions/cs-startup-assertions-required.yaml deleted file mode 100644 index c377f4ec..00000000 --- a/.ddd/decisions/cs-startup-assertions-required.yaml +++ /dev/null @@ -1,34 +0,0 @@ -format: 2 -id: dec/cs/startup-assertions-required -title: Assertion-closed obligations must be asserted in a test, not merely available -rationale: > - Two obligations in this catalog are closable by walking a built object - rather than by a rule: AutoMapper's member coverage - (AssertConfigurationIsValid, DDD-cs-map-01) and EF concurrency-token - presence per entity (DDD-cs-conc-02). Both close nothing unless the - assertion actually runs, which makes them the most likely entries in the - catalog to be recorded as closed while being inert. - - - So the decision is not "these are closable" — the claims already say that — - but that a governed repo runs them as a test that fails the build, not as a - startup nicety that a misconfigured environment can skip. A mapper - configuration validated only in Development, or a model assertion nobody - invokes, leaves the obligation open while the catalog says otherwise. - - - Which entities need a concurrency token stays with judgment; the assertion - checks the ones the repo declares should have one, and its value is that - the declared set cannot silently empty. - - - Scope: governed C# engagement repos that use either library. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-map-01 - status: reported - changed: 2026-08-07 - - claim: DDD-cs-conc-02 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/cs-unsafe-reinterpretation-banned.yaml b/.ddd/decisions/cs-unsafe-reinterpretation-banned.yaml deleted file mode 100644 index bd66261f..00000000 --- a/.ddd/decisions/cs-unsafe-reinterpretation-banned.yaml +++ /dev/null @@ -1,38 +0,0 @@ -format: 2 -id: dec/cs/unsafe-reinterpretation-banned -title: Unsafe.As and Marshal.PtrToStructure are banned symbols, not reviewed ones -rationale: > - DDD-cs-tc-04 and DDD-cs-tc-05 separate the type-conformance holes into two - kinds, and only one of them is survivable by review. `dynamic` and object - downcasts defer the check: they fail loudly at the site, so a mistake - surfaces. Unsafe.As and Marshal.PtrToStructure remove the check: the - reinterpretation returns a plausible wrong answer — Length 3 read off an - int[] header, a struct field read past the end of its allocation — and - nothing anywhere reports it. - - - A residual with no rejection witness cannot be allocated to review, because - review has nothing to find. So these are banned by symbol rather than - watched, which needs a banned-symbols analyzer and a documented exception - path, and is a dependency decision rather than a severity one. - - - The reason this is not obvious, and the reason it is filed: neither call - site requires AllowUnsafeBlocks. The demonstration project never set it, so - it defaulted to false, and both compiled. The project switch that teams - read as "unsafe code is off" does not cover the unsafe surface reachable - through ordinary BCL method calls, and a repo relying on that switch is - relying on something it does not do. - - - Scope: governed C# engagement repos. Interop-heavy components will need the - exception path; that it is an exception with a name is the point. -principal: Emil (Context&) -based_on: - - claim: DDD-cs-tc-04 - status: reported - changed: 2026-08-07 - - claim: DDD-cs-tc-05 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/curation-over-mining.yaml b/.ddd/decisions/curation-over-mining.yaml deleted file mode 100644 index 56a4d01b..00000000 --- a/.ddd/decisions/curation-over-mining.yaml +++ /dev/null @@ -1,18 +0,0 @@ -format: 2 -id: dec/ddd/curation-over-mining -title: Curation over mining; detection is conformance checking -rationale: > - The graph is the source of truth and the code must conform to it. Detection - exists only to check declared instances against reality (ddd diff); nothing - is mined into the catalog, so every entry has a principal who can explain it. -principal: Emil (Context&) -based_on: - - claim: DDD-method-02 - status: reported - changed: 2026-08-02 -date: 2026-08-02 -notes: > - Pin backfilled at M5 from git, not from the present: the decision was filed - in 136aa55 (2026-08-02) and DDD-method-02 has not been touched since, so - the status and changed values above are the claim's state at decision time, - recovered rather than assumed. Not a re-decision. diff --git a/.ddd/decisions/enforce-matching-tightens-to-symbol.yaml b/.ddd/decisions/enforce-matching-tightens-to-symbol.yaml deleted file mode 100644 index fa6391c6..00000000 --- a/.ddd/decisions/enforce-matching-tightens-to-symbol.yaml +++ /dev/null @@ -1,54 +0,0 @@ -format: 2 -id: dec/ddd/enforce-matching-tightens-to-symbol -title: Enforce-mode declaration matching tightens to symbol granularity; warn mode keeps the file arm -rationale: > - Takes the choice M3/M4 flagged for this milestone. In enforce mode the file - arm of match_declarations is too broad to sustain what enforce mode claims: - a declaration authored about one symbol should not admit an unexamined - change to a different symbol that happens to share a file. Enforce mode - should match on symbol identity alone, and demand a declaration per - undeclared symbol. - - - Warn mode keeps the file arm. There the declaration is advisory, the edit - applies either way, and the broad arm costs only a slightly generous link - on a row that was going to be written regardless. Paying symbol-granular - friction for an advisory mode buys nothing. - - - Priced, not paid, in M5. No code changed here: this milestone is curation, - and the change is adapter-adjacent work with its own tests. It lands in M6 - alongside the Rust adapter, when this workspace first has an enforce-mode - surface of its own to run it against. Until then the exposure stands as - DDD-arch-05 describes it, and this repo's global posture is `intercept: - warn`, where the arm is the intended behaviour rather than a gap. - - - The correspondence rows are the check: once M6 lands, a row whose - linked_declaration names a symbol other than the row's own should not - occur in enforce mode. That is both the acceptance test for the change and - the falsifier for the claim. -principal: Emil (Context&) -based_on: - - claim: DDD-arch-05 - status: projected - changed: 2026-08-07 -date: 2026-08-07 -notes: > - Did not land in M6. The decision's own acceptance test is a property of the - correspondence rows, so M6 ran its governed tail with the file arm still in - place in order to observe the falsifier rather than pre-empt it. It fired on - the first enforce-mode edit — seam-event/4, symbol host_command, linked to - seam/rust/default-command — and the metadata corruption that followed is now - filed as evidence on DDD-arch-05 (reported, 2026-08-10). Rescheduled by - ruling to the M7 preamble, task 2. The corrupted seam declaration stays in - place, notes intact, until that task lands. - - - Basis re-affirmed 2026-08-10 (Emil). DDD-arch-05 moved projected -> reported - and gained a paragraph naming the metadata-corruption consequence, so this - decision's basis strengthened in both status and content — the extension - describes exactly the damage the tightening prevents. Nothing is - re-litigated. The pin stays at its decision-time value per PRD §6 rule 6, - and `ddd report escapes` will keep listing the edge as basis loss; this note - is the record that it was looked at. diff --git a/.ddd/decisions/enum-member-gap-priced.yaml b/.ddd/decisions/enum-member-gap-priced.yaml deleted file mode 100644 index b566dc33..00000000 --- a/.ddd/decisions/enum-member-gap-priced.yaml +++ /dev/null @@ -1,36 +0,0 @@ -format: 2 -id: dec/ddd/enum-member-gap-priced -title: The C# enum-member gap is a named policy-table hole, priced now and closed in M6 -rationale: > - Takes the third choice M3/M4 flagged. DDD-adapter-03 establishes that the - §9.1 table is wrong by omission rather than by a wrong row: enum-member - addition is contract-forming and the table cannot see it. - - - Exposure, stated. In a governed C# repo with the DAD §4.1 store on, adding - a public enum member breaks every consumer whose switch expression was - exhaustive, at error severity — and the interceptor applies the edit with - no demand and writes no correspondence row, because the symbol is dropped - before classification. So the gap is silent twice: no seam demanded, and no - trace in the dataset that would later reveal the miss. The blast radius is - bounded by the fact that the consumer break is a compile error rather than - a runtime fault, so it surfaces at the next build of the consumer — late, - but not in production. - - - Not paid in M5. Adding `enum-member` to DECL_KINDS plus a policy row is - adapter work with its own classifier tests, and this milestone is curation. - It lands in M6. The change is small, and if the exposure is judged to - outweigh the scope line it can be pulled forward — that is a call for the - principal, not a reason to widen M5. - - - This is what dec/ddd/adapter-policy-tables predicted: a wrong row gets - fixed in the adapter, never in the core. It is the first time the - prediction has been tested, and the fix is indeed adapter-local. -principal: Emil (Context&) -based_on: - - claim: DDD-adapter-03 - status: reported - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/fixtures-not-sdk.yaml b/.ddd/decisions/fixtures-not-sdk.yaml deleted file mode 100644 index b52ca458..00000000 --- a/.ddd/decisions/fixtures-not-sdk.yaml +++ /dev/null @@ -1,13 +0,0 @@ -format: 2 -id: dec/ddd/fixtures-not-sdk -title: Test against committed SARIF; no .NET SDK in workspace CI -rationale: > - The suite runs on committed fixtures so workspace CI stays hermetic and - fast; two #[ignore]d integration tests regenerate the SARIF from real - builds with the documented invocations for local verification (PRD §13). -principal: Emil (Context&) -based_on: - - claim: DDD-detect-02 - status: reported - changed: 2026-08-06 -date: 2026-08-06 diff --git a/.ddd/decisions/git-is-the-amend-trail.yaml b/.ddd/decisions/git-is-the-amend-trail.yaml deleted file mode 100644 index d66b8262..00000000 --- a/.ddd/decisions/git-is-the-amend-trail.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 5 -id: dec/ddd/git-is-the-amend-trail -title: Amendments leave no in-file trail; the committed graph's history is the record -rationale: > - `.ddd/` is committed, so git already carries who amended what and when. An - in-file `amended:` counter or history block duplicates that record and adds - a field that can drift from it. Revisit if the graph ever gains a backing - store that is not under version control. -principal: Emil (Context&) -based_on: - - type: constraint - statement: >- - The store is committed files under version control, so the substrate - already records every amendment — author, date, diff — and an in-file - trail can only be a second copy that drifts from the authoritative - one. -date: 2026-08-07 -notes: > - Basis re-typed 2026-08-12, following the 2026-08 basis-quality audit: a - re-decision of the basis per format 5, not an edit. The decision is - structurally forced — the trail exists because the store's substrate is - git, whether or not any in-file field repeats it — so its honest basis is - the constraint stated above; no measured finding is involved. The prior - citation of DDD-method-04 is dropped: that claim restates this same - constraint in claim form, filed the same day as the decision with nothing - behind it beyond the restatement — the rule-3 filler the audit classes as - manufactured. The claim itself is not retired here; claim fates are the - F-batch's. Format 2 -> 5 per docs/ddd-format-migrations.md. - Ratified 2026-08-12 (Emil): accepted as filed (8b7a410). diff --git a/.ddd/decisions/interceptor-not-extension.yaml b/.ddd/decisions/interceptor-not-extension.yaml deleted file mode 100644 index dd48099c..00000000 --- a/.ddd/decisions/interceptor-not-extension.yaml +++ /dev/null @@ -1,19 +0,0 @@ -format: 2 -id: dec/ddd/interceptor-not-extension -title: Seam detection is an edit-flow interceptor, not an LSP extension -rationale: > - Interception happens in the tool's apply_edit path, consuming LSP events - client-side. Neither language server is extended or forked; the check is - part of the arrangement the agent edits through, not the agent's residual - discretion. -principal: Emil (Context&) -based_on: - - claim: DDD-arch-03 - status: projected - changed: 2026-08-02 -date: 2026-08-02 -notes: > - Pin backfilled at M5 from git, not from the present: the decision was filed - in 136aa55 (2026-08-02) and DDD-arch-03 has not been touched since, so - the status and changed values above are the claim's state at decision time, - recovered rather than assumed. Not a re-decision. diff --git a/.ddd/decisions/internal-not-surface.yaml b/.ddd/decisions/internal-not-surface.yaml deleted file mode 100644 index 98cf42b8..00000000 --- a/.ddd/decisions/internal-not-surface.yaml +++ /dev/null @@ -1,52 +0,0 @@ -format: 7 -id: dec/ddd/internal-not-surface -title: internal is not C# contract surface by default; library repos flip per-repo -rationale: > - App-repo posture is the default. The config key - `adapter.csharp.internal_is_surface` (present, documented, default - false) makes the posture explicit and cheap to flip per repo; - InternalsVisibleTo presence near an edit emits a warning suggesting the - flip, so the library case cannot pass silently. Settles PRD §14 - question 2 with the stated default. -principal: Emil (Context&) -based_on: - - type: mandate - statement: >- - The principal's ruling settling PRD §14 question 2: Context& - repositories are application repositories, so the app-repo posture is - the required default and the library posture is an explicit per-repo - config flip. -revisit_if: - - claim: DDD-adapter-02 - status: projected - changed: 2026-08-06 - content: sha256:b333063d5058614855329b2dd8a1d1c5f8732ab243a2d2f9ebfa3bf95f2f5fda -date: 2026-08-06 -notes: > - Basis re-typed 2026-08-12, following the 2026-08 basis-quality audit: a - re-decision of the basis per format 5, not an edit. The default posture is - not a measured finding — it is required by who this tool serves. The - principal ruled PRD §14 question 2 for Context&'s repo population, which - is application repositories; the mandate above names that requirement. - DDD-adapter-02 is retained as a secondary basis, pin unchanged: as the - ground of the decision it is the manufactured-grade citation the audit - found (projected, filed with the decision, no evidence), but it does - state the exposure the default carries, and its falsifier — a boundary - defect traced to an internal member consumed across a boundary — is the - live tripwire for revisiting the mandate. Retained as the check on the - ruling, not its ground; its fate as a claim is the F-batch's. Format - 2 -> 5 per docs/ddd-format-migrations.md. - Ratified 2026-08-12 (Emil): accepted as filed (8b7a410). - - Edge re-typed 2026-08-13, following the principal's ruling that a - watched-not-grounding edge is a distinct edge type: a re-decision of - this edge per format 7, not an edit. DDD-adapter-02 moves out of - `based_on` and into `revisit_if` — the pin is carried across unchanged - (projected@2026-08-06, content recovered at that state by the M8 - migration), so nothing about what is recorded changes; what changes is - what the record *means* and how it reports. It never grounded this - decision, and the note above already said so in prose; it now says so in - a field. Its falsifier — a boundary defect traced to an internal member - consumed across a boundary — is the tripwire, and firing it reopens the - mandate rather than undermining it. Format 5 -> 7 per - docs/ddd-format-migrations.md. Awaiting the principal's acceptance. diff --git a/.ddd/decisions/lsp-as-seam.yaml b/.ddd/decisions/lsp-as-seam.yaml deleted file mode 100644 index 96e51297..00000000 --- a/.ddd/decisions/lsp-as-seam.yaml +++ /dev/null @@ -1,59 +0,0 @@ -format: 7 -id: dec/ddd/lsp-as-seam -title: Each language is reached through its native semantic graph, behind an LSP-shaped host -rationale: > - ddd reads and writes code through the language's own semantic graph — - Roslyn's workspace and semantic model for C#, rust-analyzer's for Rust, - the Bicep compiler's for Bicep — never through a reconstruction of it. - Each graph lives in a per-language host process that the core reaches - over LSP: the standard methods, plus `ddd/*` extension methods where the - standard vocabulary cannot carry a fact the policy table reads or a - symbol-level write. The core stays language-free — one host and one - policy table per language, and nothing in the core links a compiler API. - - - An off-the-shelf language server is an acceptable host only while the - standard protocol carries every fact its policy table classifies on. - Where it does not, the language gets its own host over the native API, - not a text-slicing workaround in the adapter. The host reports facts and - performs edits; it never decides what is contract surface — that stays - the core's, per `dec/ddd/interceptor-not-extension`. -principal: Emil (Context&) -based_on: - - type: mandate - statement: >- - The principal's ruling of 2026-09-29 on what this decision was filed - to mean: work through the native language graph of each language. - The 2026-08-02 wording forbade direct Roslyn access, which read the - protocol seam as the goal rather than the means to it. - - type: constraint - statement: >- - Standard LSP carries a symbol's name, kind and range but not its - visibility, attributes or structured signature, and it edits only by - text range. A C# adapter confined to it must reconstruct those facts - by slicing declaration text (ddd-lsp/src/adapter/csharp_facts.rs) and - cannot insert a member into a type by symbol, so both the facts the - policy table classifies on and the symbol-level write path need the - semantic model the protocol does not expose. -date: 2026-09-29 -notes: > - Re-decision of wording and basis, 2026-09-29, not an edit. As filed - 2026-08-02 the entry read "The LSP protocol is the seam to language - intelligence" with the rationale that the core "never touches Roslyn or - Bicep APIs directly", on the basis DDD-arch-02 (projected@2026-08-02). - The protocol seam survives, now between the core and a per-language host; - what changes is that a host may, and for C# will, work through the - compiler's own graph. - - - DDD-arch-02 is dropped as ground and not kept as a tripwire: whether LSP - suffices without compiler access for a given language now decides only - which host that language gets, and either answer is inside this decision. - The principal narrowed it to Bicep the same day (2026-09-29), which moves - the pin dec/ddd/rust-host-is-real holds on it; that basis loss is left - reporting for the principal to settle. dec/ddd/interceptor-not-extension is unaffected: no - third-party server is extended or forked, and interception stays in the - core's apply path. Format 2 -> 7 per docs/ddd-format-migrations.md. The - ledger record dec:hafeok.ddd/01KZTGGMEACBFMTC1RJJ8T90GS takes a new - version re-pinning ddd-content at this entry. Wording accepted by the - principal 2026-09-29; the ledger acceptance is theirs to sign. diff --git a/.ddd/decisions/m6-proceeds-no-flip.yaml b/.ddd/decisions/m6-proceeds-no-flip.yaml deleted file mode 100644 index 0bad9c9f..00000000 --- a/.ddd/decisions/m6-proceeds-no-flip.yaml +++ /dev/null @@ -1,98 +0,0 @@ -format: 7 -id: dec/ddd/m6-proceeds-no-flip -title: M6 proceeds as the Rust adapter; the M6/M7 flip condition does not fire -rationale: > - The flip condition was to be evaluated at the top of this session: swap M6 - for M7 if HTML/CSS failures had kept costing since the condition was filed. - Evaluated and not fired — M6 proceeds. - - - The evaluation has a caveat worth recording rather than hiding. The - condition is not filed anywhere in this repository. The committed PRD's §10 - milestone table ends at M5; there is no M6 row, no M7 row, and no flip - condition in `docs/ddd-cli-prd.md`, in `.ddd/decisions/`, or in - `docs/ddd-m5-report.md`. A search of the store for HTML/CSS content returns - nothing. The condition therefore could not be evaluated against filed - evidence; it was resolved by the stated default. - - - What the repo does say about M6 is consistent with proceeding: the M5 - closing report §5 is written as a list of what M6 owes, and it names the - Rust adapter as M6's content. That is corroboration of the default, not - evidence about the flip condition, and it is not treated as such here. - - - Consequence for the reader: if the flip condition matters beyond this - session, it needs filing. An unfiled condition evaluated by default is an - escaped decision in the making — the exact shape this tool exists to catch. -principal: Emil (Context&) -based_on: - - type: mandate - statement: >- - The stated default the principal set for the M6/M7 sequencing: with - the flip condition unfiled and therefore unevaluable against filed - evidence, the evaluation resolves by that default and M6 proceeds. -revisit_if: - - claim: DDD-adapter-01 - status: projected - changed: 2026-08-02 - content: sha256:922a0a70f69509bfcd73325fbdc1cd5ac281b1b52281b14f56a8f519afd99f52 -date: 2026-08-10 -notes: > - M6's content is the third language adapter, which is the experiment that - moves DDD-adapter-01 off `projected`. That is why the basis edge points at - the adapter-cost claim rather than at a scope claim. - - - Basis re-affirmed 2026-08-10 (Emil). DDD-adapter-01 did move off projected, - to reported, which is the outcome the paragraph above anticipated — so this - decision's basis strengthened and nothing is re-litigated. The pin stays at - its decision-time value per PRD §6 rule 6, and `ddd report escapes` will keep - listing the edge as basis loss; this note is the record that it was looked - at. Also noted at ratification: the entry's assertion that the flip condition - is filed nowhere in this repository was true against the tree at a8871da and - is stale now — canon was synced afterwards (e90e6ea) and the condition is - filed in the PRD M7 row. The entry stands as the record of what was known at - ruling time, which the principal ruled was its whole value. - - Basis re-affirmed again 2026-08-10 (Emil), at M7's acceptance: - DDD-adapter-01 moved reported -> established after surviving its - scheduled falsification attempt at language four (the non-LSP HTML+CSS - pair; zero contract-surface leakage). The basis strengthened a second - time; nothing is re-litigated, the pin stays at its decision-time value - per PRD §6 rule 6, and this note is the record that the moved edge was - looked at. - - - Basis re-typed 2026-08-12, following the 2026-08 basis-quality audit - (F-7e): a re-decision of the basis per format 5, not an edit. The - rationale's own deciding phrase — "the condition therefore could not be - evaluated against filed evidence; it was resolved by the stated - default" — argues from the principal's stated default, which is a - mandate, now stated above. The audit also records the content mismatch: - DDD-adapter-01 is about policy-table localisation and says nothing about - the flip condition — genuine, but about something else. It is retained - as a secondary basis, pin unchanged, with that made explicit: it never - grounded the flip evaluation, but it is the tracked stake of proceeding — - the first paragraph above records why the edge points at the - adapter-cost claim, and its movement (projected -> reported -> - established) was raised by basis loss and looked at twice, record-keeping - the edge is still doing. Format 2 -> 5 per - docs/ddd-format-migrations.md. - Ratified 2026-08-12 (Emil): accepted as filed (8531938) — the retained - edge is a claim being watched, with the content mismatch explicit so it - is not later read as ground. - - Edge re-typed 2026-08-13, following the principal's ruling that a - watched-not-grounding edge is a distinct edge type: a re-decision of - this edge per format 7, not an edit. DDD-adapter-01 moves out of - `based_on` and into `revisit_if`, pin carried across unchanged - (projected@2026-08-02, content recovered at that state by the M8 - migration). This is the one of the three whose pin has drifted, and the - drift is deliberate: the claim moved projected -> reported -> - established, and the two paragraphs above are the record that the - movement was looked at each time. That drift now reports as a **reopen** - finding rather than a basis-loss one, which is the correction the ruling - makes — the ground of this decision is the principal's stated default and - has never moved; what moved is the stake being tracked. Format 5 -> 7 per - docs/ddd-format-migrations.md. Awaiting the principal's acceptance. diff --git a/.ddd/decisions/m8-enforcement-closure.yaml b/.ddd/decisions/m8-enforcement-closure.yaml deleted file mode 100644 index 8fe6c7ad..00000000 --- a/.ddd/decisions/m8-enforcement-closure.yaml +++ /dev/null @@ -1,29 +0,0 @@ -format: 5 -id: dec/ddd/m8-enforcement-closure -title: M8 expands to enforcement closure, not ledger integration alone -rationale: > - M8 is the Decision Ledger integration plus the chain the review - demands: repository-diff contract-surface validation in CI sharing the - interception classifier, and declarations that sign the change they - discharge (subject symbol, before/after content hash, base revision — - the ledger's acceptance-signs-hash law applied to seams). Content-hash - basis pins pull forward into M8 with it. Interception governs the - governed path; CI governs the repository — the boundary claim the - review broke is replaced, not patched. Ruled at the 2026-08 PRD review. -principal: Emil (Context&) -based_on: - - type: claim - claim: DDD-arch-08 - status: reported - changed: 2026-08-11 - - type: claim - claim: DDD-arch-09 - status: reported - changed: 2026-08-11 -date: 2026-08-11 -notes: > - Ratified 2026-08-11 (Emil): accepted as filed. - The roadmap statement of this scope, with the chain as M8's acceptance - (change -> detected contract event -> justified obligation -> - declaration signing the change -> durable discharge -> authoritative - CI result), lands in docs/ddd-roadmap.md at the document split. diff --git a/.ddd/decisions/no-unwrap-gate.yaml b/.ddd/decisions/no-unwrap-gate.yaml deleted file mode 100644 index fa8c6902..00000000 --- a/.ddd/decisions/no-unwrap-gate.yaml +++ /dev/null @@ -1,51 +0,0 @@ -format: 7 -id: dec/rust/no-unwrap -title: Deny clippy::unwrap_used workspace-wide -rationale: > - Every unwrap is an undeclared panic path; the workspace routes fallibility - through ProductError instead. Enforced twice by arrangement, not - exhortation: [workspace.lints.clippy] in the root manifest and the CI - clippy gate (cargo clippy -- -D warnings -D clippy::unwrap_used). This - entry makes the flag resolvable through `ddd why clippy::unwrap_used` — - the workspace config is what `ddd diff` verifies the manifest against. -principal: Emil (Context&) -based_on: - - type: constraint - statement: >- - The workspace error model routes every failure through ProductError - to a defined exit code; an unwrap is a panic path that structurally - bypasses that contract, so denying the lint is what keeps the error - model total. -revisit_if: - - claim: DDD-gates-01 - status: reported - changed: 2026-08-06 - content: sha256:6f500ee8ef3bcbef7ce1a1d6ce2c093d61858f7a01bb3685b1c0f56aa32b4395 -date: 2026-08-06 -notes: > - Basis re-typed 2026-08-12, following the 2026-08 basis-quality audit: a - re-decision of the basis per format 5, not an edit. The gate is forced by - the workspace's error contract — every failure maps to a ProductError - exit code, and a panic path bypasses that contract structurally — stated - as the constraint above; no measured finding makes it so. DDD-gates-01 is - retained as a secondary basis, pin unchanged: as the sole ground it was - the manufactured-grade citation the audit found, but it genuinely reports - the cost side (the lint has held workspace-wide without a waiver - request), and its falsifier — waiver pressure, or a class of code where - the lint produces worse handling than the unwrap it replaced — remains - independently observable. Retained as the cost check, not the ground; its - fate as a claim is the F-batch's. Format 2 -> 5 per - docs/ddd-format-migrations.md. - Ratified 2026-08-12 (Emil): accepted as filed (8b7a410). - - Edge re-typed 2026-08-13, following the principal's ruling that a - watched-not-grounding edge is a distinct edge type: a re-decision of - this edge per format 7, not an edit. DDD-gates-01 moves out of - `based_on` and into `revisit_if`, pin carried across unchanged - (reported@2026-08-06, content recovered at that state by the M8 - migration). The constraint is the ground and stays the ground; the claim - is the cost check, and the note above already called it that. Its - falsifier — waiver pressure, or a class of code where the lint produces - worse handling than the unwrap it replaced — is the tripwire, and firing - it reopens the gate rather than removing its ground. Format 5 -> 7 per - docs/ddd-format-migrations.md. Awaiting the principal's acceptance. diff --git a/.ddd/decisions/pins-at-m2.yaml b/.ddd/decisions/pins-at-m2.yaml deleted file mode 100644 index 7fe61560..00000000 --- a/.ddd/decisions/pins-at-m2.yaml +++ /dev/null @@ -1,15 +0,0 @@ -format: 2 -id: dec/ddd/pins-at-m2 -title: Add basedOn status pins as an M2 format bump, not deferred -rationale: > - Basis-loss detection requires knowing what the decision stood on when it - was made; without the pin, `report escapes` cannot say whether the ground - moved (PRD §6 rule 6, §13). Escaped-decision reporting is load-bearing - for the tool's thesis — the warning swamp is the canonical escape — so - the format bump lands with M2 rather than waiting for a later milestone. -principal: Emil (Context&) -based_on: - - claim: DDD-method-02 - status: reported - changed: 2026-08-02 -date: 2026-08-06 diff --git a/.ddd/decisions/prd-split.yaml b/.ddd/decisions/prd-split.yaml deleted file mode 100644 index 3e487877..00000000 --- a/.ddd/decisions/prd-split.yaml +++ /dev/null @@ -1,27 +0,0 @@ -format: 5 -id: dec/ddd/prd-split -title: The PRD splits; it stops being the sole implementable contract -rationale: > - docs/ddd-cli-prd.md becomes a short umbrella (thesis, product identity, - index, history note) over four documents with one job each: - ddd-v1-spec.md — the implementable contract for what exists (M1-M7 as - built); ddd-adrs.md — the settled architecture decisions with their - bases; ddd-research-protocol.md — the correspondence dataset design and - experiment protocols with predicted and observed strictly separated; - ddd-roadmap.md — M8 at its enforcement-closure scope plus the filed - follow-ups. Ruled at the 2026-08 PRD review. -principal: Emil (Context&) -based_on: - - type: preference - statement: > - One document serving as spec, research protocol, decision record, - and roadmap at once serves none of them as a contract an - implementer can pick up cold; four documents with one job each - beat one that owns everything. -date: 2026-08-11 -notes: > - Ratified 2026-08-11 (Emil): accepted as filed. - The split itself executes once the review document is committed at - docs/reviews/ddd-cli-prd-review-2026-08.md — the spec's normative - invariants section quotes the review near-verbatim, so the split - cannot honestly land before the review text does. diff --git a/.ddd/decisions/predicates-carry-no-status.yaml b/.ddd/decisions/predicates-carry-no-status.yaml deleted file mode 100644 index 26b1325e..00000000 --- a/.ddd/decisions/predicates-carry-no-status.yaml +++ /dev/null @@ -1,38 +0,0 @@ -format: 5 -id: dec/ddd/predicates-carry-no-status -title: Predicates carry no status; enforcement is structural -rationale: > - Closure lives only in claims, with status and falsifiers. The split is - enforced by schema (deny_unknown_fields rejects a status key on a predicate - entry, naming ontology rule 1), not by convention or review. -principal: Emil (Context&) -based_on: - - type: mandate - statement: >- - The framework corpus's predicate format (predicate-format.md, - predicate-definition.md) requires the split — status, boundary - clauses and falsifiers live on claims only — and ontology rule 1 - enforces it by schema. -date: 2026-08-02 -notes: > - Pin backfilled at M5 from git, not from the present: the decision was filed - in 136aa55 (2026-08-02) and DDD-method-01 has not been touched since, so - the status and changed values above are the claim's state at decision time, - recovered rather than assumed. Not a re-decision. - - - Basis re-typed 2026-08-12, following the 2026-08 basis-quality audit: a - re-decision of the basis per format 5, not an edit (the backfilled pin - the paragraph above describes is superseded by the drop below; it stays - on record here). The split is not adopted because a claim measured it - working — it is required by the framework corpus this tool implements, - and the mandate above names that requirement and its schema-level - enforcement. The prior citation of DDD-method-01 is dropped: this very - decision seals that claim's falsifier — once predicates carry no status, - no predicate with an embedded status can exist to be observed staying - accurate, so the claim cannot be killed by the store it governs. A claim - this decision makes unfalsifiable cannot serve as this decision's basis; - that defect is the audit's F-8 finding, to be filed as a claim in its - own right. The claim itself is not retired here; claim fates are the - F-batch's. Format 2 -> 5 per docs/ddd-format-migrations.md. - Ratified 2026-08-12 (Emil): accepted as filed (8b7a410). diff --git a/.ddd/decisions/rejection-facts-prefilled.yaml b/.ddd/decisions/rejection-facts-prefilled.yaml deleted file mode 100644 index d3923bd7..00000000 --- a/.ddd/decisions/rejection-facts-prefilled.yaml +++ /dev/null @@ -1,16 +0,0 @@ -format: 2 -id: dec/ddd/rejection-facts-prefilled -title: apply_edit rejections pre-fill the facts, never the judgment -rationale: > - The structured demand carries the LSP-derived observables (symbol, - kind, signature, visibility, current reference count) as read-only - fields; verdict_knowledge plus obligation answers are always empty and - must be authored. Pre-filling the judgment invites rubber-stamping; - omitting the facts wastes the agent's calls re-deriving them. Settles - PRD §14 question 3 with the stated default. -principal: Emil (Context&) -based_on: - - claim: DDD-friction-01 - status: projected - changed: 2026-08-06 -date: 2026-08-06 diff --git a/.ddd/decisions/rejection-payload-basis.yaml b/.ddd/decisions/rejection-payload-basis.yaml deleted file mode 100644 index 08656d04..00000000 --- a/.ddd/decisions/rejection-payload-basis.yaml +++ /dev/null @@ -1,78 +0,0 @@ -format: 5 -id: dec/ddd/rejection-payload-basis -title: The rejection-payload lever's basis is cited upstream, not re-derived here -rationale: > - The M3/M4 principal decisions are open and Emil's. This entry does not make - them. It files the cross-reference their basis rests on, so that when they - are made the ground is a citation rather than a re-derivation — and so that - a reader of this register can reach it. - - - The lever. A checker's rejection payload sets the per-try acceptance - probability, and therefore the retry economics of a small actor operating at - its capacity margin. A pass/fail verdict alone gives p(accept)/try 0.35, - E[tries] 2.9, expected cost 11.4 per act; a rich rejection payload gives - 0.55, 1.8, and 7.3. The mechanism is that a check which explains its - rejection raises the weak actor's effective capacity — each retry becomes a - guided step rather than a fresh attempt. - - - The consequence for this repository. The shape of a rejection payload is a - lever on achievable actor class, not an API detail. Wherever this workspace - emits a rejection — a validation violation, a gate finding, a verdict event - on the build seam, an MCP write-tool refusal — what the payload carries - decides whether the next attempt is guided or blind, and therefore which - actor class can do the work at all. - - - Basis, upstream and unmodified: DDD-cost-11 (projected) in - actor-indexed-determination, the sign-flip claim, whose second region bound - is retry economics — the weak-actor-plus-retries crossover computable and - favourable. Its notes carry the lever directly: "rich rejection payloads - shift the retry crossover — a check that explains its rejection raises the - weak actor's effective capacity". The worked figures above are its - projection's example, decision-driven-design core/15 §4. - - - Cited, not pinned. DDD-cost-11 is upstream canon and does not exist in this - repository's claim store, so it is not filed as a `type: claim` basis: a pin - this store cannot resolve would report as basis loss forever and would be - the wrong kind of edge. The basis stated below is what this repository - actually decided; the upstream claim is named in it as the ground that was - read. -principal: Emil (Context&) -based_on: - - type: mandate - statement: >- - The principal's ruling of 2026-08-10, at the act-cost holding note's - pre-Gate-H hold: queue item 2.13 ratified as pending transfer — the - rejection-payload cross-reference files into product-cli's decision - register at the next session touching that repo, citing DDD-cost-11, and - no third pull request is opened for it. - - type: constraint - statement: >- - A basis that lives in another repository is cited by identifier and read - at its source, never copied into this store. Copying it would create a - second authority that drifts silently from the first; citing it leaves - one authority and makes the read auditable. -date: 2026-08-17 -notes: > - Filed by the freight session, 2026-08-17 (item G-7), which is the next - session to touch this repository after the ruling. Deferred since Wave 2 on - the ruling's own terms — "next session that touches that repo" — and this is - it. Nothing else in this repository was changed by that session. - - - What this entry deliberately does not do. It does not decide M3 or M4, does - not state a payload schema, and does not oblige any existing emitter to - change. It records where the ground is. The M3/M4 decisions remain the - principal's and unmade, and when they are made this entry is what they cite - rather than re-deriving the retry arithmetic inside this workspace. - - - Read the source before acting on the figures. The numbers are a worked - example over a stipulated arrangement, not a measurement of this workspace's - own checkers, and DDD-cost-11 is `projected` — it carries no evidence - entries and its falsifier is unrun. Treating the 0.35/0.55 split as a target - for this repository's rejection messages would be reading a projection as a - measurement. diff --git a/.ddd/decisions/report-coverage-explicit.yaml b/.ddd/decisions/report-coverage-explicit.yaml deleted file mode 100644 index de8ed93f..00000000 --- a/.ddd/decisions/report-coverage-explicit.yaml +++ /dev/null @@ -1,17 +0,0 @@ -format: 2 -id: dec/ddd/report-coverage-explicit -title: Escape-report sections state their coverage; uncheckable is a note, not an escape -rationale: > - Every section of `ddd report escapes` separates checked-and-clean from - not-checkable and names the uncheckable set — unpinned basedOn edges in - basis loss, claims carrying no revalidate_by in cadence. Coverage is - reported as a note and never fails the gate: formats 1-3 are all valid to - `validate`, so counting unpinned edges as escapes would break CI for every - format-1 repo and punish a supported format. `ddd render` already annotates - per edge and needs no change. -principal: Emil (Context&) -based_on: - - claim: DDD-method-03 - status: projected - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/rust-class-enforced-here.yaml b/.ddd/decisions/rust-class-enforced-here.yaml deleted file mode 100644 index c013b370..00000000 --- a/.ddd/decisions/rust-class-enforced-here.yaml +++ /dev/null @@ -1,65 +0,0 @@ -format: 5 -id: dec/ddd/rust-class-enforced-here -title: This workspace runs the code artifact class under enforce once the Rust adapter ships -rationale: > - This is a Context& repository and the tool's own first user, so the default - posture for its own artifact class is enforce rather than warn. From the - point the Rust adapter passes its fixture acceptance, contract-surface edits - to this workspace's Rust sources are rejected until a seam declaration - covers them. - - - The point is not compliance. Both friction claims have no ground in this - repo at all — `.ddd/seams/events/` is empty, so DDD-friction-01 and - DDD-friction-02 have been carrying six-month revalidation cadences against - zero observations. Running the tail of this session governed is the first - live reading of the friction falsifier on a real repo, and the declarations - it produces are the first correspondence rows. - - - One granularity limit is recorded rather than worked around. Interception - mode is keyed by *artifact class*, not by language, and both the C# and the - Rust adapters declare the class `code`. `intercept_by_class.code: enforce` - therefore enforces C# here too. That is harmless in this workspace — it has - no governed C# outside `ddd-cli/tests/fixtures/**`, which the ignore globs - already exclude — but a repo holding both languages could not enforce one - and warn the other. Priced, not paid; it needs a decision of its own if a - mixed repo ever wants the split. - - - The global `intercept: warn` stays as the default for classes that are not - named, which keeps PRD §11's adoption mitigation intact for anything the - per-class entry does not cover. -principal: Emil (Context&) -based_on: - - type: experiment - statement: >- - Adopted to find out: running this workspace's own artifact class - under enforce is the first live reading of the friction falsifier - (DDD-friction-01, DDD-friction-02) on a real repo, and the - declarations it produces are the first correspondence rows. - ref: docs/ddd-m6-experiment.md -date: 2026-08-10 -notes: > - The friction reading this produces is optimistic by a known amount: - `dec/ddd/enforce-matching-tightens-to-symbol` is deliberately not - implemented in this session, so enforce-mode matching still admits the file - arm. See docs/ddd-m6-experiment.md §1.1. - - - Basis re-typed 2026-08-12, following the 2026-08 basis-quality audit - (F-7f): a re-decision of the basis per format 5, not an edit. The - rationale is honestly self-described — "running the tail of this session - governed is the first live reading of the friction falsifier on a real - repo" — which is the experiment type: adopted to find out. The prior - citations of DDD-friction-01 and DDD-friction-02 are dropped as bases - and named in the experiment statement instead: they are the claims this - decision exists to test, not claims it rests on — its own rationale says - both "have no ground in this repo at all", so a decision resting on them - would invert the direction of evidence. Neither claim is retired here; - claim fates are the F-batch's. Format 2 -> 5 per - docs/ddd-format-migrations.md. - Ratified 2026-08-12 (Emil): accepted as filed (8531938) — citing the - claims under test as basis was backwards, not merely thin; moving them - into the experiment statement as the objects of the test is the ruled - reading. diff --git a/.ddd/decisions/rust-host-is-real.yaml b/.ddd/decisions/rust-host-is-real.yaml deleted file mode 100644 index dfab4eff..00000000 --- a/.ddd/decisions/rust-host-is-real.yaml +++ /dev/null @@ -1,38 +0,0 @@ -format: 2 -id: dec/ddd/rust-host-is-real -title: The Rust adapter's acceptance runs against real rust-analyzer, not a mock dialect -rationale: > - `dec/ddd/fixtures-not-sdk` keeps workspace CI hermetic by testing the C# and - Bicep paths against committed fixtures plus a mock LSP host, because the - .NET SDK is a heavyweight foreign prerequisite CI has no other reason to - carry. That rationale does not extend to Rust: rust-analyzer is a component - of the toolchain this repository already pins, so the real host costs one - entry in `rust-toolchain.toml` and one `components:` line in the workflow. - - - Two things follow, and both are why this is worth a decision rather than a - preference. The adapter's acceptance is measured against the server whose - output it actually parses, so a rust-analyzer change that moves symbol - kinds, impl-block naming, or the readiness protocol fails the suite instead - of passing a mock that was written to agree with the adapter. And the - session's own constraint holds — all Rust knowledge stays in the adapter - module, because no Rust dialect is added to `ddd-lsp/src/mock/parse.rs`. - - - The tests do not skip when the component is absent. A silent skip is the - adoption trap M5 recorded twice — a green exit code standing in for a check - that never ran — so a missing rust-analyzer fails with the `rustup component - add` line in the message. -principal: Emil (Context&) -based_on: - - claim: DDD-detect-02 - status: reported - changed: 2026-08-06 - - claim: DDD-arch-02 - status: projected - changed: 2026-08-02 -date: 2026-08-10 -notes: > - DDD-arch-02 is pinned as a basis because running against the real host is - what makes its LSP-sufficiency question answerable for a third language - rather than answerable only against a mock built to agree. diff --git a/.ddd/decisions/sarif-unification.yaml b/.ddd/decisions/sarif-unification.yaml deleted file mode 100644 index ab020ec6..00000000 --- a/.ddd/decisions/sarif-unification.yaml +++ /dev/null @@ -1,14 +0,0 @@ -format: 2 -id: dec/ddd/sarif-unification -title: Unify detection on SARIF ingestion with thin invocation adapters -rationale: > - One ingestion module reads every emitted diagnostic; per-language adapters - know only how to produce a SARIF file and which rule-id namespace it - carries (PRD §7, §13). No console parsing — both toolchains emit SARIF - through supported switches, verified at M2 and recorded in the README. -principal: Emil (Context&) -based_on: - - claim: DDD-detect-01 - status: reported - changed: 2026-08-06 -date: 2026-08-06 diff --git a/.ddd/decisions/seed-lands-in-claims-not-shared.yaml b/.ddd/decisions/seed-lands-in-claims-not-shared.yaml deleted file mode 100644 index 4bf07c07..00000000 --- a/.ddd/decisions/seed-lands-in-claims-not-shared.yaml +++ /dev/null @@ -1,34 +0,0 @@ -format: 2 -id: dec/ddd/seed-lands-in-claims-not-shared -title: The M5 closure-claim seed is filed in claims/; shared/ is earned by observed inheritance -rationale: > - Settles PRD §14 question 5. `shared/` is described in PRD §6 as entries - intended for promotion to a future central catalog. Intent is cheap to - declare and, once declared, indistinguishable in the store from a - transferability that was actually observed — which is what DDD-method-05 - says the field cannot honestly carry at authoring time. - - - The asymmetry decides it. Promoting later is a file move plus a note, and - the promoting act can cite the repo that inherited the entry — PRD §12's - fourth success criterion is exactly that observation. Filing early is not - recoverable: nothing downstream can tell a predicted-transferable entry - from a proven one, so the error is silent, which is the failure mode this - milestone exists to avoid. - - - The C# seed sharpens it further. Those claims are version-indexed to a - language and SDK version, and an entry inherited into a repo on a different - SDK is worse than no entry — it is a closure finding that does not hold - there, carrying the authority of the catalog. Version-indexed claims should - cross a repo boundary only when someone has checked the index against the - destination. - - - No schema change: promotion is a path move, and the entry keeps its id. -principal: Emil (Context&) -based_on: - - claim: DDD-method-05 - status: projected - changed: 2026-08-07 -date: 2026-08-07 diff --git a/.ddd/decisions/typed-basis.yaml b/.ddd/decisions/typed-basis.yaml deleted file mode 100644 index 2fe3c1e3..00000000 --- a/.ddd/decisions/typed-basis.yaml +++ /dev/null @@ -1,25 +0,0 @@ -format: 5 -id: dec/ddd/typed-basis -title: A decision's basis is typed; non-claim bases are first-class -rationale: > - The basis vocabulary is claim | constraint | mandate | preference | - experiment | risk-acceptance. The basedOn -> claim edge remains - load-bearing where a claim is the basis; the other types become honest - first-class bases instead of manufactured claims. Forcing claim-basis - where none exists manufactures weak claims to pass validation — the - exact corruption the graph exists to prevent. Ruled at the 2026-08 PRD - review; shipped as decision format 5 with ontology rule 3 restated to - "every decision has >=1 typed basis". -principal: Emil (Context&) -based_on: - - type: claim - claim: DDD-method-06 - status: projected - changed: 2026-08-11 -date: 2026-08-11 -notes: > - The pleasing recursion, on record: this is the first decision entitled - to a non-claim basis type, and its honest basis is a claim — the - manufactured-claims finding above — so a claim basis is correct for it. - Filed at format 5, the format this ruling produced. - Ratified 2026-08-11 (Emil): accepted as filed (f4ef70c). diff --git a/.ddd/decisions/undeclared-what-boundaries.yaml b/.ddd/decisions/undeclared-what-boundaries.yaml deleted file mode 100644 index 24fe83f4..00000000 --- a/.ddd/decisions/undeclared-what-boundaries.yaml +++ /dev/null @@ -1,32 +0,0 @@ -format: 1 -id: risk/ddd/undeclared-what-boundaries -kind: risk-acceptance -title: The 24 undeclared What boundaries stay undeclared; exposure accepted until review -rationale: > - Exposure accepted: `ddd what --strict` reports 24 undeclared boundaries - over 121 classified elements — 8 in acme, 16 in product-cli. While they - stay undeclared, a change to any of them goes unrecorded, and the What - surface has no green baseline against which a future regression could be - measured. The exposure is bounded two ways: both graphs are demonstration - content with no second party that reads their elements across a boundary - (DDD-what-04), and nothing enforces these boundaries in either direction — - `ddd what --strict` is not wired into CI, and What write interception - inherits the global `intercept: warn`. - - - Review by 2027-02-07, carried mechanically by DDD-what-04's - `revalidate_by` (`report escapes` raises it then; basis loss raises it - sooner if the claim moves). The trigger to pay rather than re-defer is - whichever arrives first: this workspace gaining a genuinely governed - surface of its own, or a first engagement repo bringing a What graph with - a real second party. -principal: Emil (Context&) -date: 2026-08-07 -notes: > - Filed 2026-08-12 in the basis-quality re-typing session. This record - makes explicit the risk acceptance that - dec/ddd/what-boundaries-priced-not-paid ruled on 2026-08-07 — that - decision's format-5 `risk-acceptance` basis must ref a record of this - kind, and none existed. It restates the already-ruled acceptance with its - exposure and review date; it does not re-decide it. - Ratified 2026-08-12 (Emil): accepted as filed (8b7a410). diff --git a/.ddd/decisions/v1-scope.yaml b/.ddd/decisions/v1-scope.yaml deleted file mode 100644 index c526d636..00000000 --- a/.ddd/decisions/v1-scope.yaml +++ /dev/null @@ -1,20 +0,0 @@ -format: 2 -id: dec/ddd/v1-scope -title: "v1 scope: local repo, C# + Bicep, build-output detection first" -rationale: > - The graph is repo-local (shared/ reserves the central-catalog split without - redesign); languages are C# and Bicep only; detection starts from dotnet - build and Bicep CLI output, with LSP diagnostics supplementing at edit time. - Scope follows where Context& demand concentrates and where adapter cost is - lowest. -principal: Emil (Context&) -based_on: - - claim: DDD-scope-01 - status: reported - changed: 2026-08-02 -date: 2026-08-02 -notes: > - Pin backfilled at M5 from git, not from the present: the decision was filed - in 136aa55 (2026-08-02) and DDD-scope-01 has not been touched since, so - the status and changed values above are the claim's state at decision time, - recovered rather than assumed. Not a re-decision. diff --git a/.ddd/decisions/web-htmlcss-enforce.yaml b/.ddd/decisions/web-htmlcss-enforce.yaml deleted file mode 100644 index e5d3ef4f..00000000 --- a/.ddd/decisions/web-htmlcss-enforce.yaml +++ /dev/null @@ -1,18 +0,0 @@ -format: 2 -id: dec/web/htmlcss-enforce -title: Interception for the html-css artifact class is enforce on this repo -rationale: > - Anything less than enforce is not doing the value-add this class exists - for — the observed failure mode is agents drifting past ungoverned - HTML↔CSS seams, and `warn` is exactly the exhortation that failure mode - ignores. The dogfood target is the tool's own projection surface: the - `ddd render` stylesheet and its representative output page, governed as a - pair by the tool that renders them. The class key is the artifact class, - not a language, per the granularity limit recorded on - dec/ddd/rust-class-enforced-here. -principal: Emil (Context&) -based_on: - - claim: DDD-web-01 - status: reported - changed: 2026-08-10 -date: 2026-08-10 diff --git a/.ddd/decisions/web-markup-validity.yaml b/.ddd/decisions/web-markup-validity.yaml deleted file mode 100644 index 49f52bbc..00000000 --- a/.ddd/decisions/web-markup-validity.yaml +++ /dev/null @@ -1,15 +0,0 @@ -format: 2 -id: dec/web/markup-validity -title: Duplicated classes in markup are errors (html-validate, error) -rationale: > - `no-dup-class` rejects an element carrying the same class twice — always - a copy-paste artifact, and one that silently masks a missing *second* - class the author meant to add, which is how orphan references are born. - Seeded as the one html-validate rule M7 wires; curation of a fuller rule - set is a later session's content by design. -principal: Emil (Context&) -based_on: - - claim: DDD-web-01 - status: reported - changed: 2026-08-10 -date: 2026-08-10 diff --git a/.ddd/decisions/web-plain-pair-scope.yaml b/.ddd/decisions/web-plain-pair-scope.yaml deleted file mode 100644 index 51dbe059..00000000 --- a/.ddd/decisions/web-plain-pair-scope.yaml +++ /dev/null @@ -1,17 +0,0 @@ -format: 2 -id: dec/web/plain-pair-scope -title: The pair adapter governs plain HTML+CSS only; Razor/Blazor deferred, JS/TS out -rationale: > - The M7 governed unit is the plain HTML+CSS pair. Razor/Blazor routes - partly through the C# adapter and is deferred until demand evidence - exists; JS/TS is out of scope entirely — a `\n\n\n", - &[], - &AdapterFlags::default(), - ); - let links: Vec<&str> = - list.iter().filter(|f| f.kind == "stylesheet-link").map(|f| f.name.as_str()).collect(); - assert_eq!(links, vec!["real.css"], "{list:?}"); -} - -#[test] -fn tag_parsing_reads_class_and_id_attributes() { - let parsed = tags("
\n"); - assert_eq!(parsed.len(), 2); - assert_eq!(parsed[0].1.attr("class").as_deref(), Some("a b")); - assert_eq!(parsed[0].1.attr("id").as_deref(), Some("main")); - assert_eq!(parsed[1].1.attr("class").as_deref(), Some("c")); -} diff --git a/ddd-lsp/src/adapter/htmlcss_pair.rs b/ddd-lsp/src/adapter/htmlcss_pair.rs deleted file mode 100644 index 533267a1..00000000 --- a/ddd-lsp/src/adapter/htmlcss_pair.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! The pair contract check — orphan classes, dead selectors, pair facts. -//! -//! The interceptor sees one file per edit; the pair's failures live -//! *between* the files. This module owns the cross-file knowledge: the -//! pair map from `.ddd/config.yaml` (`pair.units`), the enrichment that -//! stamps pair-level facts onto surface events (both files, direction, -//! token/class id), and the bidirectional class-contract check whose two -//! finding classes — `orphan-class` and `dead-selector` — join -//! `ddd report escapes`. Matching is necessary-condition decomposition; -//! what the checker cannot model it reports as skipped rather than silently -//! passing (`dec/ddd/report-coverage-explicit`). - -use std::path::{Path, PathBuf}; - -use ddd_core::config::{DddConfig, PairConfig}; -use ddd_core::detect::glob_match; -use serde::Serialize; - -use crate::surface::SurfaceEvent; - -/// Stamp pair-level facts onto classified events, and drop decorative -/// classes (config globs — thresholds are data, not code). Called by the -/// interceptor after classification, before demands and rows. -pub fn enrich(root: &Path, file: &Path, config: &DddConfig, events: &mut Vec) { - let pair = config.pair.clone().unwrap_or_default(); - let rel = rel_of(root, file); - let direction = if rel.ends_with(".css") { "definition" } else { "consumption" }; - events.retain(|e| { - !(e.facts.kind == "class" - && pair.ignore_classes.iter().any(|g| glob_match(g, &e.facts.name))) - }); - let counterpart = counterpart_of(root, &rel, &pair).join(";"); - for e in events.iter_mut() { - e.facts.extra.insert("pair_direction".into(), direction.to_string()); - e.facts.extra.insert("pair_counterpart".into(), counterpart.clone()); - } -} - -/// The other side of every pair unit this file belongs to. -fn counterpart_of(root: &Path, rel: &str, pair: &PairConfig) -> Vec { - let mut out: Vec = Vec::new(); - for unit in &pair.units { - let (own, other) = if rel.ends_with(".css") { - (&unit.css, &unit.html) - } else { - (&unit.html, &unit.css) - }; - if own.iter().any(|g| glob_match(g, rel)) { - for path in resolve(root, other) { - if !out.contains(&path) { - out.push(path); - } - } - } - } - out -} - -/// Repo-relative paths matching a glob list, in walk order. -fn resolve(root: &Path, globs: &[String]) -> Vec { - let mut files = Vec::new(); - walk(root, root, &mut files); - files - .into_iter() - .filter(|rel| globs.iter().any(|g| glob_match(g, rel))) - .collect() -} - -const SKIP_DIRS: &[&str] = &[".git", ".ddd", "target", "node_modules", "bin", "obj"]; - -fn walk(root: &Path, dir: &Path, out: &mut Vec) { - let Ok(rd) = std::fs::read_dir(dir) else { return }; - let mut entries: Vec<_> = rd.flatten().collect(); - entries.sort_by_key(|e| e.path()); - for entry in entries { - let path = entry.path(); - let Ok(ft) = entry.file_type() else { continue }; - if ft.is_symlink() { - continue; - } - if ft.is_dir() { - let name = entry.file_name().to_string_lossy().into_owned(); - if !SKIP_DIRS.contains(&name.as_str()) { - walk(root, &path, out); - } - } else if ft.is_file() { - out.push(rel_of(root, &path)); - } - } -} - -fn rel_of(root: &Path, path: &Path) -> String { - path.strip_prefix(root).unwrap_or(path).to_string_lossy().replace('\\', "/") -} - -/// One pair-contract violation. -#[derive(Debug, Clone, Serialize)] -pub struct PairFinding { - /// `orphan-class` (HTML consumes it, no owned CSS defines it) or - /// `dead-selector` (owned CSS targets structure no paired HTML has). - pub kind: String, - /// The class or selector at fault. - pub name: String, - /// The file the fault was observed in. - pub file: String, - pub detail: String, -} - -/// The check's own coverage, stated with its findings. -#[derive(Debug, Default, Serialize)] -pub struct PairCoverage { - pub units: usize, - pub html_files: usize, - pub css_files: usize, - pub selectors_checked: usize, - /// Selectors containing only constructs the checker does not model. - pub selectors_skipped: Vec, -} - -#[derive(Debug, Default, Serialize)] -pub struct PairReport { - pub findings: Vec, - pub coverage: PairCoverage, -} - -impl PairReport { - pub fn is_clean(&self) -> bool { - self.findings.is_empty() - } -} - -/// Run both directions of the class contract over every configured pair. -pub fn check_pairs(root: &Path, config: &DddConfig) -> PairReport { - let mut report = PairReport::default(); - let Some(pair) = &config.pair else { return report }; - report.coverage.units = pair.units.len(); - for unit in &pair.units { - let html_files = resolve(root, &unit.html); - let css_files = resolve(root, &unit.css); - report.coverage.html_files += html_files.len(); - report.coverage.css_files += css_files.len(); - check_unit(root, pair, &html_files, &css_files, &mut report); - } - report.findings.sort_by(|a, b| (&a.kind, &a.name).cmp(&(&b.kind, &b.name))); - report -} - -fn check_unit( - root: &Path, - pair: &PairConfig, - html_files: &[String], - css_files: &[String], - report: &mut PairReport, -) { - let mut defined: Vec = Vec::new(); - let mut selectors: Vec<(String, String)> = Vec::new(); - for css in css_files { - let text = super::htmlcss_facts::strip_css_comments(&read(root, css)); - for (_, name) in super::htmlcss_facts::selector_class_names(&text) { - if !defined.contains(&name) { - defined.push(name); - } - } - for sel in top_level_selectors(&text) { - selectors.push((css.clone(), sel)); - } - } - let mut consumed: Vec<(String, String)> = Vec::new(); - let mut structure = Structure::default(); - for html in html_files { - let text = read(root, html); - collect_structure(&text, html, &mut consumed, &mut structure); - } - // Direction 1: classes consumed by HTML ⊆ classes defined by owned CSS. - for (file, class) in &consumed { - let decorative = pair.ignore_classes.iter().any(|g| glob_match(g, class)); - if !decorative && !defined.contains(class) { - report.findings.push(PairFinding { - kind: "orphan-class".into(), - name: class.clone(), - file: file.clone(), - detail: format!("consumed in {file}, defined in no owned stylesheet"), - }); - } - } - // Direction 2: selectors in owned CSS ⊆ structures present in the HTML. - let classes_consumed: Vec<&str> = consumed.iter().map(|(_, c)| c.as_str()).collect(); - for (file, sel) in &selectors { - match selector_alive(sel, &classes_consumed, &structure) { - Verdict::Alive => report.coverage.selectors_checked += 1, - Verdict::Unmodelled => report.coverage.selectors_skipped.push(sel.clone()), - Verdict::Dead(part) => { - report.coverage.selectors_checked += 1; - report.findings.push(PairFinding { - kind: "dead-selector".into(), - name: sel.clone(), - file: file.clone(), - detail: format!("`{part}` matches nothing in the paired HTML"), - }); - } - } - } -} - -fn read(root: &Path, rel: &str) -> String { - std::fs::read_to_string(PathBuf::from(root).join(rel)).unwrap_or_default() -} - -/// Everything the modelled structure of the paired HTML contains. -#[derive(Debug, Default)] -struct Structure { - tags: Vec, - ids: Vec, -} - -fn collect_structure( - text: &str, - file: &str, - consumed: &mut Vec<(String, String)>, - structure: &mut Structure, -) { - for (_, tag) in super::htmlcss_facts::tags(text) { - let name = tag.name.to_ascii_lowercase(); - if !structure.tags.contains(&name) { - structure.tags.push(name); - } - if let Some(id) = tag.attr("id") { - if !structure.ids.contains(&id) { - structure.ids.push(id); - } - } - if let Some(classes) = tag.attr("class") { - for class in classes.split_whitespace() { - if !consumed.iter().any(|(_, c)| c == class) { - consumed.push((file.to_string(), class.to_string())); - } - } - } - } -} - -/// Selector texts heading top-level (or @-group-nested) rule bodies. -fn top_level_selectors(text: &str) -> Vec { - let mut out = Vec::new(); - let mut head_start = 0usize; - let mut depth = 0i32; - for (pos, c) in text.char_indices() { - match c { - '{' => { - if depth >= 0 { - let head = text[head_start..pos].trim(); - if !head.starts_with('@') && !head.is_empty() { - for sel in head.split(',') { - let sel = sel.trim(); - if !sel.is_empty() { - out.push(sel.to_string()); - } - } - } - } - depth += 1; - head_start = pos + 1; - } - '}' | ';' => { - depth -= i32::from(c == '}'); - head_start = pos + 1; - } - _ => {} - } - } - out -} - -enum Verdict { - Alive, - Unmodelled, - Dead(String), -} - -/// Necessary-condition check: split on combinators, decompose each -/// compound into simple parts, strip pseudo-classes/-elements; a modelled -/// part absent from the HTML kills the selector, a selector with no -/// modelled parts at all is reported as skipped. -fn selector_alive(sel: &str, classes: &[&str], structure: &Structure) -> Verdict { - let mut any_modelled = false; - for compound in sel.split([' ', '>', '+', '~']).filter(|s| !s.is_empty()) { - let base = compound.split(':').next().unwrap_or(""); - for part in simple_parts(base) { - match part { - Part::Class(c) => { - any_modelled = true; - if !classes.contains(&c.as_str()) { - return Verdict::Dead(format!(".{c}")); - } - } - Part::Id(id) => { - any_modelled = true; - if !structure.ids.contains(&id) { - return Verdict::Dead(format!("#{id}")); - } - } - Part::Tag(t) => { - any_modelled = true; - if !structure.tags.contains(&t) { - return Verdict::Dead(t); - } - } - Part::Unmodelled => {} - } - } - } - if any_modelled { - Verdict::Alive - } else { - Verdict::Unmodelled - } -} - -enum Part { - Class(String), - Id(String), - Tag(String), - Unmodelled, -} - -/// `div.card#main[data-x]` → Tag(div), Class(card), Id(main), Unmodelled. -fn simple_parts(compound: &str) -> Vec { - let mut out = Vec::new(); - let mut rest = compound; - while !rest.is_empty() { - let (head, tail) = split_simple(rest); - rest = tail; - let part = match head.chars().next() { - Some('.') => Part::Class(head[1..].to_string()), - Some('#') => Part::Id(head[1..].to_string()), - Some('[') | Some('*') => Part::Unmodelled, - Some(c) if c.is_ascii_alphabetic() => Part::Tag(head.to_ascii_lowercase()), - _ => Part::Unmodelled, - }; - if !matches!(&part, Part::Class(x) | Part::Id(x) | Part::Tag(x) if x.is_empty()) { - out.push(part); - } - } - out -} - -/// One simple selector off the front: its text plus the remainder. -fn split_simple(s: &str) -> (&str, &str) { - if let Some(rest) = s.strip_prefix('[') { - let end = rest.find(']').map(|p| p + 2).unwrap_or(s.len()); - return (&s[..end], &s[end..]); - } - let start = usize::from(s.starts_with(['.', '#'])); - let len = s[start..] - .find(['.', '#', '[']) - .map(|p| p + start) - .unwrap_or(s.len()); - let len = len.max(1); - (&s[..len], &s[len..]) -} - -#[cfg(test)] -#[path = "htmlcss_pair_tests.rs"] -mod tests; diff --git a/ddd-lsp/src/adapter/htmlcss_pair_tests.rs b/ddd-lsp/src/adapter/htmlcss_pair_tests.rs deleted file mode 100644 index b3137cb6..00000000 --- a/ddd-lsp/src/adapter/htmlcss_pair_tests.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! Pair contract check coverage: both directions, thresholds, enrichment. - -use super::*; -use ddd_core::config::{DddConfig, PairUnit}; - -fn write(root: &Path, rel: &str, content: &str) { - let path = root.join(rel); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("mkdir"); - } - std::fs::write(path, content).expect("write"); -} - -fn config(ignore_classes: &[&str]) -> DddConfig { - DddConfig { - format: 4, - pair: Some(PairConfig { - units: vec![PairUnit { - html: vec!["site/*.html".into()], - css: vec!["site/style.css".into()], - }], - ignore_classes: ignore_classes.iter().map(|s| s.to_string()).collect(), - }), - ..DddConfig::default() - } -} - -const PAGE: &str = "\n\n
\n\n"; -const SHEET: &str = ".page { margin: 0; }\n.card { padding: 1rem; }\n.btn { color: red; }\n"; - -#[test] -fn a_conformant_pair_is_clean_and_states_its_coverage() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write(tmp.path(), "site/style.css", SHEET); - let report = check_pairs(tmp.path(), &config(&["js-*"])); - assert!(report.is_clean(), "{report:?}"); - assert_eq!(report.coverage.units, 1); - assert_eq!(report.coverage.html_files, 1); - assert_eq!(report.coverage.css_files, 1); - assert!(report.coverage.selectors_checked >= 3, "{report:?}"); -} - -#[test] -fn an_orphan_class_is_found_unless_a_decorative_glob_exempts_it() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write(tmp.path(), "site/style.css", ".page { margin: 0; }\n.card { padding: 1rem; }\n"); - let report = check_pairs(tmp.path(), &config(&["js-*"])); - let orphans: Vec<&str> = report - .findings - .iter() - .filter(|f| f.kind == "orphan-class") - .map(|f| f.name.as_str()) - .collect(); - assert_eq!(orphans, vec!["btn"], "{report:?}"); - // Without the decorative threshold the js-hook joins them. - let report = check_pairs(tmp.path(), &config(&[])); - let orphans: Vec<&str> = report - .findings - .iter() - .filter(|f| f.kind == "orphan-class") - .map(|f| f.name.as_str()) - .collect(); - assert_eq!(orphans, vec!["btn", "js-toggle"], "{report:?}"); -} - -#[test] -fn a_dead_selector_is_found_in_class_element_and_id_form() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write( - tmp.path(), - "site/style.css", - format!("{SHEET}.ghost {{ color: blue; }}\ntable {{ margin: 0; }}\n#nowhere {{ color: red; }}\ndiv.card {{ border: 0; }}\n").as_str(), - ); - let report = check_pairs(tmp.path(), &config(&["js-*"])); - let dead: Vec<&str> = report - .findings - .iter() - .filter(|f| f.kind == "dead-selector") - .map(|f| f.name.as_str()) - .collect(); - assert_eq!(dead, vec!["#nowhere", ".ghost", "table"], "{report:?}"); -} - -#[test] -fn unmodelled_selectors_are_reported_as_skipped_not_passed() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write(tmp.path(), "site/style.css", format!("{SHEET}[data-x] {{ color: red; }}\n*:hover {{ color: blue; }}\n").as_str()); - let report = check_pairs(tmp.path(), &config(&["js-*"])); - assert!(report.is_clean(), "{report:?}"); - assert_eq!(report.coverage.selectors_skipped.len(), 2, "{report:?}"); -} - -#[test] -fn pseudo_classes_are_stripped_and_compounds_check_each_part() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write(tmp.path(), "site/style.css", format!("{SHEET}.btn:hover {{ color: blue; }}\n.card > .btn {{ margin: 0; }}\n").as_str()); - let report = check_pairs(tmp.path(), &config(&["js-*"])); - assert!(report.is_clean(), "{report:?}"); -} - -#[test] -fn enrichment_stamps_direction_and_counterpart_and_drops_decorative_classes() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write(tmp.path(), "site/style.css", SHEET); - let cfg = config(&["js-*"]); - let before = (super::super::htmlcss::ADAPTER.facts)(SHEET, &[], &Default::default()); - let after_text = format!("{SHEET}.js-only {{ color: red; }}\n.real {{ color: red; }}\n"); - let after = (super::super::htmlcss::ADAPTER.facts)(&after_text, &[], &Default::default()); - let mut events = crate::classify::diff_facts( - &(super::super::htmlcss::ADAPTER.policy)(&Default::default()), - super::super::htmlcss::ADAPTER.visibility_rank, - before, - after, - ); - enrich(tmp.path(), &tmp.path().join("site/style.css"), &cfg, &mut events); - let names: Vec<&str> = events.iter().map(|e| e.facts.name.as_str()).collect(); - assert_eq!(names, vec!["real"], "decorative dropped: {events:?}"); - let extra = &events[0].facts.extra; - assert_eq!(extra.get("pair_direction").map(String::as_str), Some("definition")); - assert_eq!(extra.get("pair_counterpart").map(String::as_str), Some("site/page.html")); -} - -/// A commented-out rule is neither vocabulary nor a live selector — the -/// dogfood run caught comment prose being parsed as dead selectors. -#[test] -fn css_comments_are_not_selectors_in_the_pair_check() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "site/page.html", PAGE); - write( - tmp.path(), - "site/style.css", - &format!("/* prose that mentions .nothing and reads like a selector */\n{SHEET}/* .ghost {{ color: red; }} */\n"), - ); - let report = check_pairs(tmp.path(), &config(&["js-*"])); - assert!(report.is_clean(), "{report:?}"); - assert!(report.coverage.selectors_skipped.is_empty(), "{report:?}"); -} diff --git a/ddd-lsp/src/adapter/htmlcss_policy_tests.rs b/ddd-lsp/src/adapter/htmlcss_policy_tests.rs deleted file mode 100644 index f9481646..00000000 --- a/ddd-lsp/src/adapter/htmlcss_policy_tests.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! One triggering plus one non-triggering case per M7 policy row. - -use crate::adapter::{htmlcss, AdapterFlags}; -use crate::classify::classify_edit; -use crate::surface::SurfaceEvent; - -fn events(before: &str, after: &str) -> Vec { - classify_edit(&htmlcss::ADAPTER, &AdapterFlags::default(), before, &[], after, &[]) -} - -fn surface_rules(evts: &[SurfaceEvent]) -> Vec<&'static str> { - evts.iter().filter(|e| e.surface).filter_map(|e| e.rule).collect() -} - -const SHEET: &str = ":root {\n --color-ok: #1a7f37;\n --gap: 1.5rem;\n}\n.btn { color: var(--color-ok); }\n.card { margin: var(--gap); }\n"; - -#[test] -fn token_added_or_removed_is_surface_but_value_change_is_no_event() { - let grown = format!("{}\n:root {{ --radius: 3px; }}\n", SHEET); - assert_eq!(surface_rules(&events(SHEET, &grown)), vec!["web-token-membership"]); - let shrunk = SHEET.replace(" --gap: 1.5rem;\n", ""); - assert_eq!(surface_rules(&events(SHEET, &shrunk)), vec!["web-token-membership"]); - // The non-trigger is the tokens' whole point: a same-type value change - // produces no event at all. - let recolored = SHEET.replace("#1a7f37", "#2ea043"); - assert!(events(SHEET, &recolored).is_empty()); -} - -#[test] -fn token_retype_is_surface_but_same_type_is_not() { - let retyped = SHEET.replace("--gap: 1.5rem;", "--gap: #fff;"); - assert_eq!(surface_rules(&events(SHEET, &retyped)), vec!["web-token-retype"]); - let same_type = SHEET.replace("--gap: 1.5rem;", "--gap: 2rem;"); - assert!(events(SHEET, &same_type).is_empty()); -} - -#[test] -fn class_membership_is_surface_but_a_styling_tweak_is_not() { - let grown = format!("{}.badge {{ color: red; }}\n", SHEET); - assert_eq!(surface_rules(&events(SHEET, &grown)), vec!["web-class-membership"]); - let dropped = SHEET.replace(".card { margin: var(--gap); }\n", ""); - assert_eq!(surface_rules(&events(SHEET, &dropped)), vec!["web-class-membership"]); - // Styling inside an existing class is the vocabulary's body, not its - // membership. - let tweaked = SHEET.replace(".btn { color: var(--color-ok); }", ".btn { color: var(--color-ok); padding: 2px; }"); - assert!(events(SHEET, &tweaked).is_empty()); -} - -#[test] -fn a_class_defined_in_a_second_rule_is_no_membership_event() { - let restyled = format!("{}.btn:focus {{ outline: none; }}\n", SHEET); - assert!(events(SHEET, &restyled).is_empty()); -} - -#[test] -fn layer_order_change_is_surface_but_a_rule_moving_between_layers_is_not_an_order_event() { - let layered = "@layer base, components;\n@layer base { .a { color: red; } }\n"; - let reordered = "@layer components, base;\n@layer base { .a { color: red; } }\n"; - assert_eq!(surface_rules(&events(layered, reordered)), vec!["web-layer-order"]); - let introduced = events(SHEET, &format!("@layer util;\n{SHEET}")); - assert_eq!(surface_rules(&introduced), vec!["web-layer-order"]); - // Moving one rule into an existing layer re-grades that rule's - // visibility, not the declared order. - let moved = "@layer base, components;\n@layer components { .a { color: red; } }\n"; - assert!(surface_rules(&events(layered, moved)).is_empty()); -} - -#[test] -fn stylesheet_link_rewiring_is_surface_but_markup_is_not() { - let page = "\n\n
x
\n"; - let rewired = page.replace("style.css", "other.css"); - let rules = surface_rules(&events(page, &rewired)); - assert_eq!(rules, vec!["web-stylesheet-link", "web-stylesheet-link"], "add + remove"); - // Markup edits — structure, class consumption — are not edit-time - // surface; the pair contract check owns them. - let restructured = page.replace("
x
", "

x

"); - assert!(events(page, &restructured).is_empty()); -} diff --git a/ddd-lsp/src/adapter/mod.rs b/ddd-lsp/src/adapter/mod.rs deleted file mode 100644 index e64f29dc..00000000 --- a/ddd-lsp/src/adapter/mod.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Language adapters — the only place language knowledge lives. -//! -//! An adapter answers exactly three questions (PRD §9): which symbol -//! events are contract-surface, what visibility means, what constitutes a -//! signature change. It also carries the mechanics those answers need: -//! the host command line, handshake quirks, facts extraction from LSP -//! symbols plus source text. The core routes by file extension; nothing -//! outside this directory names a language. - -pub mod bicep; -pub mod csharp; -mod csharp_facts; -pub mod htmlcss; -mod htmlcss_facts; -pub mod htmlcss_pair; -pub mod rust; -mod rust_facts; - -use std::path::Path; - -use serde_json::{json, Value}; - -use crate::client::ServerState; -use crate::protocol::RawSymbol; -use crate::surface::{PolicyRow, SurfaceEvent, SymbolFacts}; - -/// How a host announces that it can answer semantic requests. -/// -/// Not every server carries readiness in a method name. Roslyn sends one -/// `workspace/projectInitializationComplete` and means it; rust-analyzer -/// sends `experimental/serverStatus` repeatedly and puts the answer in the -/// payload. Both shapes are declared here so [`crate::host::Host`] stays -/// out of the business of knowing which host is which. -#[derive(Clone, Copy, Debug)] -pub enum ReadySignal { - /// Ready once the `initialized` handshake completes. - AfterInitialized, - /// Ready once this notification method has been seen at least once. - Notification(&'static str), - /// Ready once this notification's most recent params satisfy the - /// predicate. - NotificationWhere(&'static str, fn(&Value) -> bool), -} - -impl ReadySignal { - /// Has the host signalled readiness, given what the reader thread has - /// recorded so far? - pub fn satisfied(&self, state: &ServerState) -> bool { - match self { - Self::AfterInitialized => true, - Self::Notification(method) => { - state.flags.lock().map(|f| f.contains(*method)).unwrap_or(false) - } - Self::NotificationWhere(method, predicate) => state - .last_params - .lock() - .map(|p| p.get(*method).map(predicate).unwrap_or(false)) - .unwrap_or(false), - } - } - - /// What a still-loading host is being waited on for. - pub fn describe(&self) -> &'static str { - match self { - Self::AfterInitialized => "initialization", - Self::Notification(method) | Self::NotificationWhere(method, _) => method, - } - } -} - -/// The client capabilities every host is announced with; adapters whose -/// host needs nothing beyond them use this. -pub fn no_extra_capabilities() -> Value { - json!({}) -} - -/// Per-language behavior switches, resolved from `.ddd/config.yaml` -/// (`adapter..*`, config format 3). -#[derive(Debug, Clone)] -pub struct AdapterFlags { - /// Treat `internal` visibility as contract surface (library-repo - /// posture; `dec/ddd/internal-not-surface` sets the default off). - pub internal_is_surface: bool, - /// Attribute markers that make a symbol an exported endpoint. - pub exported_attributes: Vec, -} - -impl Default for AdapterFlags { - fn default() -> Self { - Self { - internal_is_surface: false, - exported_attributes: vec!["McpServerTool".to_string()], - } - } -} - -/// One language adapter: host wiring plus the three §9 answers. -pub struct Adapter { - pub language: &'static str, - /// The predicate-vocabulary artifact class this language's edits govern - /// under (drives the per-class `intercept` mode). - pub artifact_class: &'static str, - pub extensions: &'static [&'static str], - /// LSP `languageId` for didOpen. - pub language_id: &'static str, - /// Default host command; `adapter..command` overrides it. - pub default_command: &'static [&'static str], - /// How this host says it is ready. - pub ready: ReadySignal, - /// Client capabilities merged into the `initialize` payload. Some hosts - /// stay silent about their own readiness unless asked — rust-analyzer - /// sends no `experimental/serverStatus` at all without the matching - /// capability — so the announcement has to be the adapter's to make. - pub extra_capabilities: fn() -> Value, - /// Host wants the workspace's build inputs announced after initialize. - pub needs_open_handshake: bool, - /// Whether a language server exists behind this adapter at all. The - /// HTML+CSS pair has none (M7): its facts are parsed from source text, - /// so the serve layer must classify without opening a host — the host - /// fields above are inert when this is false. - pub hosted: bool, - /// Derive symbol facts from source text plus LSP symbols. - pub facts: fn(&str, &[RawSymbol], &AdapterFlags) -> Vec, - /// The contract-surface policy table (data, not logic — PRD §9). - pub policy: fn(&AdapterFlags) -> Vec, - /// Order visibilities by exposure (higher = more exposed). - pub visibility_rank: fn(&str) -> u8, - /// A repo-posture warning worth raising for edits near this file - /// (e.g. C#'s InternalsVisibleTo → suggest the library posture). - pub posture_warning: fn(&Path, &AdapterFlags) -> Option, - /// Post-classification enrichment: facts a single document cannot - /// carry (the pair's counterpart files, direction) stamped onto the - /// events before demands plus rows. `None` for single-document - /// adapters; called as (repo root, edited file, config, events). - pub enrich: Option, -} - -/// The enrichment hook's shape: (repo root, edited file, config, events). -pub type EnrichFn = fn(&Path, &Path, &ddd_core::config::DddConfig, &mut Vec); - -/// Every adapter the build ships. -pub fn all() -> &'static [&'static Adapter] { - &[&csharp::ADAPTER, &bicep::ADAPTER, &rust::ADAPTER, &htmlcss::ADAPTER] -} - -/// The adapter registered for a language name. -pub fn for_language(language: &str) -> Option<&'static Adapter> { - all().iter().copied().find(|a| a.language == language) -} - -/// The adapter claiming a file's extension. -pub fn for_path(path: &Path) -> Option<&'static Adapter> { - let ext = path.extension()?.to_str()?.to_ascii_lowercase(); - all().iter().copied().find(|a| a.extensions.contains(&ext.as_str())) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn routes_by_extension() { - assert_eq!(for_path(Path::new("/x/A.cs")).map(|a| a.language), Some("csharp")); - assert_eq!(for_path(Path::new("/x/m.bicep")).map(|a| a.language), Some("bicep")); - assert_eq!(for_path(Path::new("/x/lib.rs")).map(|a| a.language), Some("rust")); - assert_eq!(for_path(Path::new("/x/p.html")).map(|a| a.language), Some("htmlcss")); - assert_eq!(for_path(Path::new("/x/s.css")).map(|a| a.language), Some("htmlcss")); - assert!(for_path(Path::new("/x/notes.md")).is_none()); - } - - #[test] - fn artifact_classes_stay_in_the_predicate_vocabulary() { - for a in all() { - assert!( - ["code", "configuration", "html-css"].contains(&a.artifact_class), - "{}", - a.language - ); - } - } - - #[test] - fn hostless_adapters_carry_no_host_command() { - for a in all() { - assert_eq!(a.hosted, !a.default_command.is_empty(), "{}", a.language); - } - } -} diff --git a/ddd-lsp/src/adapter/rust.rs b/ddd-lsp/src/adapter/rust.rs deleted file mode 100644 index d563fc62..00000000 --- a/ddd-lsp/src/adapter/rust.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Rust adapter — rust-analyzer host wiring plus the M6 policy table. -//! -//! Host: `rust-analyzer`, the rustup component the pinned toolchain already -//! carries. It discovers the workspace itself from `rootUri` (`cargo -//! metadata`), so there is no open handshake; readiness is -//! `experimental/serverStatus` with `quiescent: true`, which the host only -//! sends when the matching client capability is declared. Facts come from -//! LSP symbols plus declaration-text slicing — no rustc, no syn, no -//! `cargo` invocation anywhere in this crate. -//! -//! Visibility is the reason the slicing exists: LSP symbol kinds carry no -//! `pub(crate)` granularity, so the grade is read off the declaration text. -//! That is adapter knowledge by construction — the same technique the C# -//! adapter uses for its own modifiers. - -use serde_json::{json, Value}; - -use crate::adapter::{rust_facts, Adapter, AdapterFlags, ReadySignal}; -use crate::surface::{ChangeKind, PolicyRow}; - -/// The host command as the adapter names it. Exposed so callers that spawn -/// the host themselves — the acceptance tests, and any consumer bypassing -/// `HostManager` — do not restate the binary name. -pub const DEFAULT_COMMAND: &[&str] = &["rust-analyzer"]; - -/// The host command with the toolchain's own rust-analyzer resolved, when -/// rustup can name it. -/// -/// `rust-analyzer` on PATH is usually the rustup shim, and the shim resolves -/// against the *current directory's* toolchain. A directory carrying no -/// `rust-toolchain.toml` therefore gets the default toolchain, which may not -/// have the component installed at all — the shim then exits with `Unknown -/// binary 'rust-analyzer'` rather than starting a server. Asking rustup for -/// the path sidesteps that; with no rustup present the bare name is returned -/// and PATH decides, which is the behaviour a packaged install wants. -pub fn host_command() -> Vec { - let resolved = std::process::Command::new("rustup") - .args(["which", "rust-analyzer"]) - .output() - .ok() - .filter(|out| out.status.success()) - .and_then(|out| String::from_utf8(out.stdout).ok()) - .map(|path| path.trim().to_string()) - .filter(|path| !path.is_empty()); - match resolved { - Some(path) => vec![path], - None => DEFAULT_COMMAND.iter().map(|s| s.to_string()).collect(), - } -} - -pub const ADAPTER: Adapter = Adapter { - language: "rust", - artifact_class: "code", - extensions: &["rs"], - language_id: "rust", - default_command: DEFAULT_COMMAND, - ready: ReadySignal::NotificationWhere("experimental/serverStatus", quiescent), - extra_capabilities: capabilities, - needs_open_handshake: false, - hosted: true, - facts: rust_facts::facts, - policy, - visibility_rank, - posture_warning, - enrich: None, -}; - -/// rust-analyzer repeats `experimental/serverStatus` and carries the answer -/// in the payload: the first one says `quiescent: false` while indexing runs. -/// Readiness is the payload, not the arrival. -fn quiescent(params: &Value) -> bool { - params.get("quiescent").and_then(Value::as_bool).unwrap_or(false) -} - -/// Without `experimental.serverStatusNotification`, rust-analyzer sends no -/// server-status notification at all — measured, not assumed — and the host -/// would have to guess when semantic requests became trustworthy. -fn capabilities() -> Value { - json!({"experimental": {"serverStatusNotification": true}}) -} - -/// Kinds the table rules on. `enum-member` is present deliberately: adding -/// a variant to a `pub` enum breaks a downstream exhaustive `match` exactly -/// as `DDD-adapter-03` describes for C#, and the gap that claim reports is -/// adapter-local, so this adapter does not inherit it. -pub const DECL_KINDS: &[&str] = &[ - "trait", "struct", "enum", "enum-member", "union", "field", "fn", "module", "const", - "static", "type-alias", "trait-member", "inherent-impl", "trait-impl", - "trait-impl-unresolved", -]; - -/// The visibilities a change is contract surface at under the app-repo -/// default. -const EXPOSED: &[&str] = &["public"]; - -/// The graded visibilities `pub(crate)` collapses into. -const GRADED: &[&str] = &["pub(crate)", "pub(super)", "pub(restricted)"]; - -/// The M6 table as data. Every row is a falsifiable claim about Rust; -/// wrong rows get amended here, never in the classifier -/// (`dec/ddd/adapter-policy-tables`). -const BASE_ROWS: &[PolicyRow] = &[ - PolicyRow { - id: "rs-trait-impl", - changes: &[ChangeKind::Added, ChangeKind::Removed], - kinds: &["trait-impl"], - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "a new trait impl on a reachable type is implicit contract participation — the type now satisfies an interface callers can dispatch on, and no `pub` keyword marks it", - }, - PolicyRow { - id: "rs-trait-impl-unresolved", - changes: &[ChangeKind::Added, ChangeKind::Removed], - kinds: &["trait-impl-unresolved"], - visibilities: &[], - exported_only: false, - surface: true, - claim: "a trait impl whose trait and self type are both declared outside this file: coherence (the orphan-impl question) is not answerable from one document, so the known escape shape is demanded rather than dropped", - }, - PolicyRow { - id: "rs-trait-definition", - changes: &[ChangeKind::Added, ChangeKind::SignatureChanged, ChangeKind::Removed], - kinds: &["trait"], - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "a public trait definition is contract surface twice over: it binds every implementor and every caller that dispatches on it", - }, - PolicyRow { - id: "rs-trait-member", - changes: &[ChangeKind::Added, ChangeKind::SignatureChanged, ChangeKind::Removed], - kinds: &["trait-member"], - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "a new or changed member on a public trait forces every implementor — a method without a default body breaks them at compile time", - }, - PolicyRow { - id: "rs-derive-change", - changes: &[ChangeKind::DecoratorChanged], - kinds: DECL_KINDS, - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "a derive or attribute change on a public item adds or removes trait impls callers may depend on; `#[derive(..)]` is trait-impl authorship in attribute syntax", - }, - PolicyRow { - id: "rs-add-exposed", - changes: &[ChangeKind::Added], - kinds: DECL_KINDS, - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "a new `pub` item is contract surface", - }, - PolicyRow { - id: "rs-signature-exposed", - changes: &[ChangeKind::SignatureChanged], - kinds: DECL_KINDS, - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "a parameter, return-type, or generic-bound change on a `pub` item is a contract change", - }, - PolicyRow { - id: "rs-remove-exposed", - changes: &[ChangeKind::Removed], - kinds: DECL_KINDS, - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "removing `pub` surface is a contract change", - }, - PolicyRow { - id: "rs-visibility-boundary", - changes: &[ChangeKind::VisibilityChanged], - kinds: DECL_KINDS, - visibilities: EXPOSED, - exported_only: false, - surface: true, - claim: "promoting to or demoting from `pub` moves the contract boundary", - }, -]; - -/// The graded-visibility pair. `pub(crate)` is Rust's `internal`, so it -/// rides the same config switch and the same default-off posture -/// (`dec/ddd/internal-not-surface`); one asymmetry is recorded on the -/// row itself. -const GRADED_NOT_SURFACE: PolicyRow = PolicyRow { - id: "rs-crate-visible-nonsurface", - changes: &[], - kinds: &[], - visibilities: GRADED, - exported_only: false, - surface: false, - claim: "`pub(crate)`/`pub(super)` items are not contract surface in an app repo, and unlike C# `internal` there is no InternalsVisibleTo to open them — the compiler enforces the bound", -}; -const GRADED_IS_SURFACE: PolicyRow = PolicyRow { - id: "rs-crate-visible-surface", - changes: &[ChangeKind::Added, ChangeKind::SignatureChanged, ChangeKind::Removed, - ChangeKind::VisibilityChanged], - kinds: &[], - visibilities: GRADED, - exported_only: false, - surface: true, - claim: "in a multi-crate workspace governed as one unit, `pub(crate)` is the seam between modules that review treats as an interface", -}; -const PRIVATE_NOT_SURFACE: PolicyRow = PolicyRow { - id: "rs-private-nonsurface", - changes: &[], - kinds: &[], - visibilities: &["private"], - exported_only: false, - surface: false, - claim: "private items are never contract surface", -}; - -fn policy(flags: &AdapterFlags) -> Vec { - let graded = if flags.internal_is_surface { GRADED_IS_SURFACE } else { GRADED_NOT_SURFACE }; - let mut rows: Vec = BASE_ROWS.to_vec(); - rows.push(graded); - rows.push(PRIVATE_NOT_SURFACE); - rows -} - -/// Order by exposure. `pub(crate)` reaches further than `pub(super)`; -/// `pub(in path)` sits between them because the path is not resolved here. -fn visibility_rank(v: &str) -> u8 { - match v { - "public" => 4, - "pub(crate)" => 3, - "pub(restricted)" => 2, - "pub(super)" => 1, - _ => 0, - } -} - -/// Rust's escape from container capping is the re-export: `pub use` of a -/// path rooted in a private module publishes an item this adapter judges -/// at its declaration site, where it looks capped. Flagged, because the -/// understatement is silent otherwise. -fn posture_warning(file: &std::path::Path, _flags: &AdapterFlags) -> Option { - let text = std::fs::read_to_string(file).ok()?; - let reexports = text - .lines() - .map(str::trim) - .filter(|l| l.starts_with("pub use ") || l.starts_with("pub(crate) use ")) - .count(); - if reexports == 0 { - return None; - } - Some(format!( - "{reexports} re-export(s) in this file — `pub use` can publish an item whose declaration site is container-capped, so effective visibility here may understate the surface" - )) -} - -#[cfg(test)] -#[path = "rust_tests.rs"] -mod tests; - -#[cfg(test)] -#[path = "rust_policy_tests.rs"] -mod policy_tests; diff --git a/ddd-lsp/src/adapter/rust_facts.rs b/ddd-lsp/src/adapter/rust_facts.rs deleted file mode 100644 index 1b434930..00000000 --- a/ddd-lsp/src/adapter/rust_facts.rs +++ /dev/null @@ -1,357 +0,0 @@ -//! Rust symbol facts — visibility grading, signature slicing, impl shapes. -//! -//! rust-analyzer's `documentSymbol` carries no visibility at all, so the -//! grade (`pub` / `pub(crate)` / `pub(super)` / `pub(in path)` / private) is -//! read off the declaration text here. Impl blocks arrive as one symbol -//! named `impl Trait for Type`, which is where trait participation is read -//! from; they carry no visibility of their own, so they are judged at the -//! visibility of the type they are written for. - -use std::collections::BTreeMap; - -use crate::adapter::rust::DECL_KINDS; -use crate::adapter::AdapterFlags; -use crate::protocol::{kind_name, RawSymbol}; -use crate::surface::SymbolFacts; - -/// Derive per-symbol facts. Two passes: type visibilities first, because an -/// impl block can be written above the type it is for. -pub fn facts(text: &str, symbols: &[RawSymbol], flags: &AdapterFlags) -> Vec { - let lines: Vec<&str> = text.lines().collect(); - let types = type_visibilities(&lines, symbols); - let mut effective: BTreeMap = BTreeMap::new(); - let mut out = Vec::new(); - for sym in symbols { - let kind = normalize_kind(sym, &types); - if !DECL_KINDS.contains(&kind.as_str()) { - continue; - } - let head = decl_start(&lines, sym.start_line as usize); - let decl = declaration_slice(&lines, head, sym.end_line as usize); - let container_vis = - effective.get(&sym.container).cloned().unwrap_or_else(|| "public".to_string()); - let vis = visibility_of(sym, &kind, &decl, &container_vis, &types); - let path = if sym.container.is_empty() { - sym.name.clone() - } else { - format!("{}/{}", sym.container, sym.name) - }; - effective.insert(path, vis.clone()); - let decorators = attribute_lines(&lines, sym.start_line as usize, head); - let exported = decorators - .iter() - .any(|d| flags.exported_attributes.iter().any(|a| d.contains(a.as_str()))); - out.push(SymbolFacts { - name: sym.name.clone(), - container: sym.container.clone(), - extra: extra_for(&kind, &sym.name), - kind, - visibility: vis, - signature: strip_visibility(&decl), - decorators, - exported, - sel_line: sym.sel_line, - sel_char: sym.sel_char, - }); - } - out -} - -/// Declaration-site visibility of every named type in the file, so an impl -/// block can be judged at the reachability of what it is written for. -fn type_visibilities(lines: &[&str], symbols: &[RawSymbol]) -> BTreeMap { - let mut map = BTreeMap::new(); - for sym in symbols { - if is_impl_symbol(&sym.name) { - continue; - } - let kind = kind_name(sym.kind); - if !matches!(kind, "struct" | "enum" | "interface" | "class" | "type-parameter" | "module") - { - continue; - } - let decl = - declaration_slice(lines, decl_start(lines, sym.start_line as usize), sym.end_line as usize); - map.insert( - base_name(&sym.name).to_string(), - declared_visibility(&decl).unwrap_or_else(|| "private".to_string()), - ); - } - map -} - -/// The visibility a symbol is judged at, capped by what encloses it. -fn visibility_of( - sym: &RawSymbol, - kind: &str, - decl: &str, - container_vis: &str, - types: &BTreeMap, -) -> String { - if kind.ends_with("impl") || kind == "trait-impl-unresolved" { - // An impl block has no visibility keyword; it is exactly as - // reachable as the type it is written for. An unknown type is - // assumed reachable, so the demand is raised rather than dropped. - let (_, self_type) = parse_impl(&sym.name); - return types.get(&self_type).cloned().unwrap_or_else(|| "public".to_string()); - } - let own = declared_visibility(decl).unwrap_or_else(|| { - if inherits_container(sym) { - container_vis.to_string() - } else { - "private".to_string() - } - }); - cap_visibility(&own, container_vis) -} - -/// Trait members and enum variants take no visibility keyword — they are as -/// visible as what declares them. -fn inherits_container(sym: &RawSymbol) -> bool { - matches!(sym.container_kind, 10 | 11) -} - -/// Effective visibility is capped by the container's: a `pub fn` in a -/// private module is not reachable. -fn cap_visibility(own: &str, container: &str) -> String { - let rank = crate::adapter::rust::ADAPTER.visibility_rank; - if rank(own) <= rank(container) { - own.to_string() - } else { - container.to_string() - } -} - -/// LSP kind plus the impl-name shape → the table's vocabulary. -fn normalize_kind(sym: &RawSymbol, types: &BTreeMap) -> String { - if is_impl_symbol(&sym.name) { - return impl_kind(&sym.name, types); - } - if sym.container_kind == 11 { - return "trait-member".to_string(); - } - match kind_name(sym.kind) { - "interface" => "trait".to_string(), - "method" | "function" => "fn".to_string(), - "class" => "struct".to_string(), - "namespace" | "package" => "module".to_string(), - "constant" => "const".to_string(), - "variable" => "static".to_string(), - "type-parameter" => "type-alias".to_string(), - other => other.to_string(), - } -} - -/// `impl Type` is inherent; `impl Trait for Type` is participation. When -/// neither side is declared in this file, coherence is unanswerable here — -/// the orphan-impl shape — so it gets its own kind rather than being -/// silently treated as an ordinary trait impl. -fn impl_kind(name: &str, types: &BTreeMap) -> String { - let (trait_name, self_type) = parse_impl(name); - let Some(trait_name) = trait_name else { - return "inherent-impl".to_string(); - }; - if types.contains_key(&trait_name) || types.contains_key(&self_type) { - "trait-impl".to_string() - } else { - "trait-impl-unresolved".to_string() - } -} - -/// An impl block's symbol name: `impl Type`, or `impl Type` with the -/// generic list written tight against the keyword. -fn is_impl_symbol(name: &str) -> bool { - name.strip_prefix("impl").map(|rest| rest.starts_with(['<', ' '])).unwrap_or(false) -} - -/// Split `impl Trait for Type` into its trait plus self type, each -/// reduced to the bare path segment. -fn parse_impl(name: &str) -> (Option, String) { - let rest = name.strip_prefix("impl").unwrap_or(name).trim_start(); - let rest = skip_generics(rest); - match rest.split_once(" for ") { - Some((t, s)) => (Some(base_name(t).to_string()), base_name(s).to_string()), - None => (None, base_name(rest).to_string()), - } -} - -/// Drop a leading `<...>` generic parameter list. -fn skip_generics(s: &str) -> &str { - if !s.starts_with('<') { - return s; - } - let mut depth = 0i32; - for (i, ch) in s.char_indices() { - match ch { - '<' => depth += 1, - '>' => { - depth -= 1; - if depth == 0 { - return s[i + 1..].trim_start(); - } - } - _ => {} - } - } - s -} - -/// The bare path segment of a type expression: `foo::Bar` → `Bar`. -fn base_name(s: &str) -> &str { - let s = s.trim().trim_start_matches('&').trim(); - let s = s.split(['<', ' ', '(']).next().unwrap_or(s); - s.rsplit("::").next().unwrap_or(s) -} - -/// Impl blocks carry their two halves onto the seam row, so a declaration -/// can name what participates in what. -fn extra_for(kind: &str, name: &str) -> BTreeMap { - let mut extra = BTreeMap::new(); - if !kind.ends_with("impl") && kind != "trait-impl-unresolved" { - return extra; - } - let (trait_name, self_type) = parse_impl(name); - if let Some(t) = trait_name { - extra.insert("trait".to_string(), t); - } - extra.insert("self_type".to_string(), self_type); - if kind == "trait-impl-unresolved" { - extra.insert( - "coherence".to_string(), - "unresolved-in-file: neither the trait nor the self type is declared here".to_string(), - ); - } - extra -} - -/// The first line of the declaration proper, skipping attribute lines that -/// rust-analyzer includes in an item's range. -fn decl_start(lines: &[&str], start: usize) -> usize { - let mut i = start; - while i < lines.len() { - let t = lines[i].trim(); - if t.starts_with("#[") || t.starts_with("#![") || t.starts_with("///") || t.starts_with("//!") - { - i += 1; - } else { - return i; - } - } - start -} - -/// The declaration text from its first line to the body or terminator, -/// whitespace-collapsed. Bodies (`{`), terminators (`;`), and initialisers -/// (`=`) are cut at depth zero only, so generic bounds, default type -/// parameters, and `where` clauses survive intact. The slice never runs -/// past `last` — the symbol's own range end — so a terminator-less -/// declaration (an enum member, a struct field) cannot absorb unrelated -/// text below it and report a phantom signature change (a false-demand -/// source the classifier corpus caught). -fn declaration_slice(lines: &[&str], start: usize, last: usize) -> String { - let span = last.saturating_sub(start).saturating_add(1).min(8); - let mut decl = String::new(); - let (mut paren, mut angle, mut bracket) = (0i32, 0i32, 0i32); - for line in lines.iter().skip(start).take(span) { - let mut piece: &str = line; - let mut done = false; - let mut prev = '\0'; - for (i, ch) in line.char_indices() { - let flat = paren == 0 && angle <= 0 && bracket == 0; - match ch { - '(' => paren += 1, - ')' => paren -= 1, - '[' => bracket += 1, - ']' => bracket -= 1, - '<' => angle += 1, - // `->` is not a closing bracket. - '>' if prev != '-' => angle -= 1, - '{' | ';' | '=' if flat => { - piece = &line[..i]; - done = true; - } - _ => {} - } - prev = ch; - if done { - break; - } - } - decl.push_str(piece); - decl.push(' '); - if done { - break; - } - } - decl.split_whitespace().collect::>().join(" ") -} - -/// The declared visibility grade, or `None` when no keyword is written. -fn declared_visibility(decl: &str) -> Option { - let rest = decl.trim_start().strip_prefix("pub")?; - match rest.chars().next() { - None => Some("public".to_string()), - Some(c) if c.is_whitespace() => Some("public".to_string()), - Some('(') => Some(restricted_grade(rest)), - // `pub` was the start of some other identifier. - _ => None, - } -} - -/// `pub(crate)` / `pub(super)` / `pub(self)` / `pub(in path)`. -fn restricted_grade(rest: &str) -> String { - let inner = rest - .trim_start_matches('(') - .split(')') - .next() - .unwrap_or("") - .trim(); - match inner { - "crate" => "pub(crate)".to_string(), - "super" => "pub(super)".to_string(), - // `pub(self)` is private by another name. - "self" => "private".to_string(), - _ => "pub(restricted)".to_string(), - } -} - -/// The signature must not carry the visibility grade — a grade change is -/// its own change kind, judged at the more exposed side. -fn strip_visibility(decl: &str) -> String { - let d = decl.trim_start(); - let rest = match d.strip_prefix("pub") { - Some(r) => { - let r = r.trim_start(); - match r.strip_prefix('(') { - Some(after) => after.find(')').map(|i| &after[i + 1..]).unwrap_or(r), - None => r, - } - } - None => d, - }; - rest.split_whitespace().collect::>().join(" ") -} - -/// Attributes attached to the declaration, whether the host's range starts -/// above them or at them. -fn attribute_lines(lines: &[&str], range_start: usize, head: usize) -> Vec { - let mut out: Vec = Vec::new(); - let mut i = range_start; - while i > 0 { - let prev = lines[i - 1].trim(); - if prev.starts_with("#[") && prev.ends_with(']') { - out.push(prev.to_string()); - i -= 1; - } else { - break; - } - } - out.reverse(); - for line in lines.iter().take(head).skip(range_start) { - let t = line.trim(); - if t.starts_with("#[") { - out.push(t.to_string()); - } - } - out -} diff --git a/ddd-lsp/src/adapter/rust_policy_tests.rs b/ddd-lsp/src/adapter/rust_policy_tests.rs deleted file mode 100644 index fe85c0a1..00000000 --- a/ddd-lsp/src/adapter/rust_policy_tests.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! Rust policy-table tests — one trigger plus one non-trigger per row. -//! -//! The table is the falsifiable part of the adapter, so every row is -//! exercised from both sides: the change it claims is boundary-forming, and -//! the neighbouring change it claims is not. - -use super::*; -use crate::adapter::rust_facts::facts; -use crate::classify::diff_facts; -use crate::protocol::RawSymbol; -use crate::surface::SurfaceEvent; - -fn sym(name: &str, kind: u64, container: &str, container_kind: u64, line: u64) -> RawSymbol { - RawSymbol { - name: name.to_string(), - kind, - container: container.to_string(), - container_kind, - start_line: line, - end_line: line, - sel_line: line, - sel_char: 0, - detail: None, - } -} - -fn flags() -> AdapterFlags { - AdapterFlags::default() -} - -/// Classify a before/after pair the way `classify_edit` does, but with -/// hand-built symbol lists on each side. -fn events( - before: (&str, Vec), - after: (&str, Vec), -) -> Vec { - let f = flags(); - diff_facts( - &policy(&f), - visibility_rank, - facts(before.0, &before.1, &f), - facts(after.0, &after.1, &f), - ) -} - -fn rule_for(events: &[SurfaceEvent], name: &str) -> (bool, Option<&'static str>) { - let e = events - .iter() - .find(|e| e.facts.name == name) - .unwrap_or_else(|| panic!("no event for {name}")); - (e.surface, e.rule) -} - -#[test] -fn adding_a_pub_fn_is_surface_and_adding_a_private_one_is_not() { - let ev = events( - ("pub fn a() {}\n", vec![sym("a", 12, "", 0, 0)]), - ( - "pub fn a() {}\npub fn b() {}\nfn c() {}\n", - vec![sym("a", 12, "", 0, 0), sym("b", 12, "", 0, 1), sym("c", 12, "", 0, 2)], - ), - ); - assert_eq!(rule_for(&ev, "b"), (true, Some("rs-add-exposed"))); - assert_eq!(rule_for(&ev, "c"), (false, Some("rs-private-nonsurface"))); -} - -#[test] -fn a_pub_crate_change_is_not_surface_by_default_but_flips_with_the_config() { - let before = ("pub(crate) fn a() {}\n", vec![sym("a", 12, "", 0, 0)]); - let after = - ("pub(crate) fn a(x: u32) {}\n", vec![sym("a", 12, "", 0, 0)]); - assert_eq!(rule_for(&events(before, after), "a"), (false, Some("rs-crate-visible-nonsurface"))); - - let mut flipped = flags(); - flipped.internal_is_surface = true; - let ev = diff_facts( - &policy(&flipped), - visibility_rank, - facts("pub(crate) fn a() {}\n", &[sym("a", 12, "", 0, 0)], &flipped), - facts("pub(crate) fn a(x: u32) {}\n", &[sym("a", 12, "", 0, 0)], &flipped), - ); - assert_eq!(rule_for(&ev, "a"), (true, Some("rs-crate-visible-surface"))); -} - -#[test] -fn a_signature_change_on_a_pub_fn_is_surface() { - let ev = events( - ("pub fn a(x: u32) {}\n", vec![sym("a", 12, "", 0, 0)]), - ("pub fn a(x: u64) {}\n", vec![sym("a", 12, "", 0, 0)]), - ); - assert_eq!(rule_for(&ev, "a"), (true, Some("rs-signature-exposed"))); -} - -#[test] -fn a_generic_bound_change_is_a_signature_change() { - let ev = events( - ("pub fn a(x: T) {}\n", vec![sym("a", 12, "", 0, 0)]), - ("pub fn a(x: T) {}\n", vec![sym("a", 12, "", 0, 0)]), - ); - assert_eq!(rule_for(&ev, "a"), (true, Some("rs-signature-exposed"))); -} - -#[test] -fn a_body_change_alone_produces_no_event() { - let ev = events( - ("pub fn a() -> u32 { 1 }\n", vec![sym("a", 12, "", 0, 0)]), - ("pub fn a() -> u32 { 2 }\n", vec![sym("a", 12, "", 0, 0)]), - ); - assert!(ev.is_empty(), "{ev:?}"); -} - -#[test] -fn adding_a_trait_member_forces_implementors() { - let before = ( - "pub trait T {\n fn a(&self);\n}\n", - vec![sym("T", 11, "", 0, 0), sym("a", 6, "T", 11, 1)], - ); - let after = ( - "pub trait T {\n fn a(&self);\n fn b(&self);\n}\n", - vec![sym("T", 11, "", 0, 0), sym("a", 6, "T", 11, 1), sym("b", 6, "T", 11, 2)], - ); - assert_eq!(rule_for(&events(before, after), "b"), (true, Some("rs-trait-member"))); -} - -#[test] -fn a_member_on_a_private_trait_is_not_surface() { - let before = - ("trait T {\n fn a(&self);\n}\n", vec![sym("T", 11, "", 0, 0), sym("a", 6, "T", 11, 1)]); - let after = ( - "trait T {\n fn a(&self);\n fn b(&self);\n}\n", - vec![sym("T", 11, "", 0, 0), sym("a", 6, "T", 11, 1), sym("b", 6, "T", 11, 2)], - ); - assert_eq!(rule_for(&events(before, after), "b"), (false, Some("rs-private-nonsurface"))); -} - -#[test] -fn adding_a_trait_impl_is_boundary_forming_without_any_pub_keyword() { - let before = ("pub struct P;\n", vec![sym("P", 23, "", 0, 0)]); - let after = ( - "pub struct P;\npub trait G {}\nimpl G for P {}\n", - vec![ - sym("P", 23, "", 0, 0), - sym("G", 11, "", 0, 1), - sym("impl G for P", 19, "", 0, 2), - ], - ); - let ev = events(before, after); - assert_eq!(rule_for(&ev, "impl G for P"), (true, Some("rs-trait-impl"))); -} - -#[test] -fn adding_an_impl_on_a_private_type_is_not_surface() { - let before = ("struct P;\n", vec![sym("P", 23, "", 0, 0)]); - let after = ( - "struct P;\npub trait G {}\nimpl G for P {}\n", - vec![sym("P", 23, "", 0, 0), sym("G", 11, "", 0, 1), sym("impl G for P", 19, "", 0, 2)], - ); - assert_eq!( - rule_for(&events(before, after), "impl G for P"), - (false, Some("rs-private-nonsurface")) - ); -} - -#[test] -fn an_unresolved_impl_is_demanded_regardless_of_visibility() { - let before = ("// nothing\n", vec![]); - let after = ( - "impl Display for Elsewhere {}\n", - vec![sym("impl Display for Elsewhere", 19, "", 0, 0)], - ); - assert_eq!( - rule_for(&events(before, after), "impl Display for Elsewhere"), - (true, Some("rs-trait-impl-unresolved")) - ); -} - -#[test] -fn adding_a_variant_to_a_pub_enum_is_surface() { - let before = ( - "pub enum E {\n A,\n}\n", - vec![sym("E", 10, "", 0, 0), sym("A", 22, "E", 10, 1)], - ); - let after = ( - "pub enum E {\n A,\n B,\n}\n", - vec![sym("E", 10, "", 0, 0), sym("A", 22, "E", 10, 1), sym("B", 22, "E", 10, 2)], - ); - // The gap DDD-adapter-03 reports for C# is adapter-local; this table - // does not inherit it. - assert_eq!(rule_for(&events(before, after), "B"), (true, Some("rs-add-exposed"))); -} - -#[test] -fn a_derive_change_on_a_pub_type_is_surface() { - let before = ( - "#[derive(Clone)]\npub struct P;\n", - vec![sym("P", 23, "", 0, 1)], - ); - let after = ( - "#[derive(Clone, Debug)]\npub struct P;\n", - vec![sym("P", 23, "", 0, 1)], - ); - assert_eq!(rule_for(&events(before, after), "P"), (true, Some("rs-derive-change"))); -} - -#[test] -fn a_derive_change_on_a_private_type_is_not_surface() { - let before = ("#[derive(Clone)]\nstruct P;\n", vec![sym("P", 23, "", 0, 1)]); - let after = ("#[derive(Clone, Debug)]\nstruct P;\n", vec![sym("P", 23, "", 0, 1)]); - assert_eq!(rule_for(&events(before, after), "P"), (false, Some("rs-private-nonsurface"))); -} - -#[test] -fn removing_pub_surface_is_a_contract_change() { - let before = ( - "pub fn a() {}\npub fn b() {}\n", - vec![sym("a", 12, "", 0, 0), sym("b", 12, "", 0, 1)], - ); - let after = ("pub fn a() {}\n", vec![sym("a", 12, "", 0, 0)]); - assert_eq!(rule_for(&events(before, after), "b"), (true, Some("rs-remove-exposed"))); -} - -#[test] -fn demoting_a_pub_fn_is_judged_at_the_more_exposed_side() { - let ev = events( - ("pub fn a() {}\n", vec![sym("a", 12, "", 0, 0)]), - ("fn a() {}\n", vec![sym("a", 12, "", 0, 0)]), - ); - assert_eq!(rule_for(&ev, "a"), (true, Some("rs-visibility-boundary"))); -} - -#[test] -fn a_trait_definition_change_names_the_trait_row() { - let ev = events( - ("pub trait G {}\n", vec![sym("G", 11, "", 0, 0)]), - ("pub trait G: Clone {}\n", vec![sym("G", 11, "", 0, 0)]), - ); - assert_eq!(rule_for(&ev, "G"), (true, Some("rs-trait-definition"))); -} - -#[test] -fn every_row_carries_a_claim() { - for row in policy(&flags()) { - assert!(!row.claim.trim().is_empty(), "{} has no claim", row.id); - assert!(row.id.starts_with("rs-"), "{}", row.id); - } -} diff --git a/ddd-lsp/src/adapter/rust_tests.rs b/ddd-lsp/src/adapter/rust_tests.rs deleted file mode 100644 index 1001ee55..00000000 --- a/ddd-lsp/src/adapter/rust_tests.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Rust facts tests — visibility grading, signature slicing, impl shapes. -//! -//! Symbols are hand-built in the shapes rust-analyzer 1.95.0 actually emits -//! (kind numbers, `impl Trait for Type` names, `container_kind`). The -//! real-host test in `tests/rust_host.rs` asserts the live server still -//! emits those shapes, so these fixtures cannot drift silently. - -use super::*; -use crate::adapter::rust_facts::facts; -use crate::protocol::RawSymbol; - -fn sym(name: &str, kind: u64, container: &str, container_kind: u64, line: u64) -> RawSymbol { - sym_span(name, kind, container, container_kind, line, line) -} - -/// A symbol whose range spans several lines, the way rust-analyzer reports -/// any declaration with a body: `end` is the item's last line, which caps -/// how far the signature slice may read (the corpus-caught bleed fix). -fn sym_span( - name: &str, - kind: u64, - container: &str, - container_kind: u64, - line: u64, - end: u64, -) -> RawSymbol { - RawSymbol { - name: name.to_string(), - kind, - container: container.to_string(), - container_kind, - start_line: line, - end_line: end, - sel_line: line, - sel_char: 0, - detail: None, - } -} - -fn flags() -> AdapterFlags { - AdapterFlags::default() -} - -fn facts_of(text: &str, symbols: &[RawSymbol]) -> Vec { - facts(text, symbols, &flags()) -} - -fn find<'a>( - list: &'a [crate::surface::SymbolFacts], - name: &str, -) -> &'a crate::surface::SymbolFacts { - list.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name} not in facts")) -} - -// ---------------------------------------------------------------- visibility - -#[test] -fn grades_every_visibility_the_language_writes() { - let text = "\ -pub fn open() {} -pub(crate) fn crate_wide() {} -pub(super) fn parental() {} -pub(in crate::a) fn restricted() {} -pub(self) fn selfish() {} -fn hidden() {} -"; - let symbols: Vec = ["open", "crate_wide", "parental", "restricted", "selfish", "hidden"] - .iter() - .enumerate() - .map(|(i, n)| sym(n, 12, "", 0, i as u64)) - .collect(); - let f = facts_of(text, &symbols); - assert_eq!(find(&f, "open").visibility, "public"); - assert_eq!(find(&f, "crate_wide").visibility, "pub(crate)"); - assert_eq!(find(&f, "parental").visibility, "pub(super)"); - assert_eq!(find(&f, "restricted").visibility, "pub(restricted)"); - // `pub(self)` is private written the long way. - assert_eq!(find(&f, "selfish").visibility, "private"); - assert_eq!(find(&f, "hidden").visibility, "private"); -} - -#[test] -fn a_pub_item_in_a_private_module_is_capped() { - let text = "\ -mod inner { - pub fn reachable_only_here() {} -} -"; - let symbols = - vec![sym("inner", 2, "", 0, 0), sym("reachable_only_here", 12, "inner", 2, 1)]; - let f = facts_of(text, &symbols); - assert_eq!(find(&f, "reachable_only_here").visibility, "private"); -} - -#[test] -fn trait_members_and_enum_variants_inherit_their_container() { - let text = "\ -pub trait Greeter { - fn greet(&self) -> String; -} -pub enum Colour { - Red, -} -"; - let symbols = vec![ - sym("Greeter", 11, "", 0, 0), - sym("greet", 6, "Greeter", 11, 1), - sym("Colour", 10, "", 0, 3), - sym("Red", 22, "Colour", 10, 4), - ]; - let f = facts_of(text, &symbols); - assert_eq!(find(&f, "greet").visibility, "public"); - assert_eq!(find(&f, "greet").kind, "trait-member"); - assert_eq!(find(&f, "Red").visibility, "public"); - assert_eq!(find(&f, "Red").kind, "enum-member"); -} - -// --------------------------------------------------------------- signatures - -#[test] -fn a_signature_keeps_bounds_and_where_clauses_but_drops_the_body() { - let text = "\ -pub fn convert>(t: T) -> Result -where - T: Clone, -{ - unimplemented!() -} -"; - let f = facts_of(text, &[sym_span("convert", 12, "", 0, 0, 5)]); - assert_eq!( - find(&f, "convert").signature, - "fn convert>(t: T) -> Result where T: Clone," - ); -} - -/// The corpus-caught false-demand source: an enum member has no `{`/`;`/`=` -/// terminator, so an unbounded slice would absorb the lines below the enum — -/// then any edit down there reported a phantom signature change on the -/// member. The symbol's own range end caps the slice instead. -#[test] -fn a_terminator_less_declaration_never_reads_past_its_own_range() { - let before = "pub enum Colour {\n Red,\n}\n\npub fn a() -> u32 { 1 }\n"; - let after = "pub enum Colour {\n Red,\n}\n\npub fn b(x: u32) -> u32 { x }\n"; - let symbols = [ - sym_span("Colour", 10, "", 0, 0, 2), - sym("Red", 22, "Colour", 10, 1), - ]; - let sig_before = find(&facts_of(before, &symbols), "Red").signature.clone(); - let sig_after = find(&facts_of(after, &symbols), "Red").signature.clone(); - assert_eq!(sig_before, "Red,"); - assert_eq!(sig_before, sig_after, "an unchanged member must keep its signature"); -} - -#[test] -fn the_arrow_in_a_return_type_is_not_a_closing_angle_bracket() { - let text = "pub fn ids() -> Vec { vec![] }\n"; - let f = facts_of(text, &[sym("ids", 12, "", 0, 0)]); - assert_eq!(find(&f, "ids").signature, "fn ids() -> Vec"); -} - -#[test] -fn a_default_type_parameter_is_not_an_initialiser() { - let text = "pub struct Holder { inner: T }\n"; - let f = facts_of(text, &[sym("Holder", 23, "", 0, 0)]); - assert_eq!(find(&f, "Holder").signature, "struct Holder"); -} - -// -------------------------------------------------------------------- impls - -#[test] -fn a_trait_impl_is_read_off_the_symbol_name() { - let text = "\ -pub struct Person; -impl Greeter for Person { - fn greet(&self) -> String { String::new() } -} -"; - let symbols = vec![ - sym("Person", 23, "", 0, 0), - sym("impl Greeter for Person", 19, "", 0, 1), - sym("greet", 6, "impl Greeter for Person", 19, 2), - ]; - let f = facts_of(text, &symbols); - let imp = find(&f, "impl Greeter for Person"); - assert_eq!(imp.kind, "trait-impl"); - // No `pub` on an impl block: judged at the type it is written for. - assert_eq!(imp.visibility, "public"); - assert_eq!(imp.extra.get("trait").map(String::as_str), Some("Greeter")); - assert_eq!(imp.extra.get("self_type").map(String::as_str), Some("Person")); -} - -#[test] -fn an_impl_on_a_private_type_is_judged_private() { - let text = "\ -struct Hidden; -impl Greeter for Hidden {} -"; - let symbols = - vec![sym("Hidden", 23, "", 0, 0), sym("impl Greeter for Hidden", 19, "", 0, 1)]; - let f = facts_of(text, &symbols); - assert_eq!(find(&f, "impl Greeter for Hidden").visibility, "private"); -} - -#[test] -fn an_impl_with_neither_side_local_is_flagged_unresolved() { - let text = "impl std::fmt::Display for OtherFile {}\n"; - let symbols = vec![sym("impl Display for OtherFile", 19, "", 0, 0)]; - let f = facts_of(text, &symbols); - let imp = find(&f, "impl Display for OtherFile"); - assert_eq!(imp.kind, "trait-impl-unresolved"); - assert!(imp.extra.contains_key("coherence"), "{:?}", imp.extra); -} - -#[test] -fn an_inherent_impl_is_not_trait_participation() { - let text = "\ -pub struct Person; -impl Person {} -"; - let symbols = vec![sym("Person", 23, "", 0, 0), sym("impl Person", 19, "", 0, 1)]; - let f = facts_of(text, &symbols); - assert_eq!(find(&f, "impl Person").kind, "inherent-impl"); -} - -// ------------------------------------------------------------------ posture - -#[test] -fn a_re_export_raises_the_capping_caveat() { - let dir = tempfile::tempdir().expect("tempdir"); - let file = dir.path().join("lib.rs"); - std::fs::write(&file, "mod inner { pub struct T; }\npub use inner::T;\n").expect("write"); - let warning = posture_warning(&file, &flags()).expect("warning"); - assert!(warning.contains("re-export"), "{warning}"); - - std::fs::write(&file, "pub struct T;\n").expect("write"); - assert!(posture_warning(&file, &flags()).is_none()); -} - -// ------------------------------------------------------------- ready signal - -#[test] -fn readiness_is_the_payload_not_the_arrival() { - let ReadySignal::NotificationWhere(method, predicate) = ADAPTER.ready else { - panic!("rust readiness should be payload-discriminated"); - }; - assert_eq!(method, "experimental/serverStatus"); - // rust-analyzer sends this first, while indexing. - assert!(!predicate(&json!({"health": "ok", "quiescent": false}))); - assert!(predicate(&json!({"health": "ok", "quiescent": true}))); - // A host that says nothing about quiescence is not ready. - assert!(!predicate(&json!({}))); -} - -#[test] -fn the_server_status_capability_is_announced() { - let caps = (ADAPTER.extra_capabilities)(); - assert_eq!(caps["experimental"]["serverStatusNotification"], json!(true)); -} diff --git a/ddd-lsp/src/bin/ddd-mock-lsp.rs b/ddd-lsp/src/bin/ddd-mock-lsp.rs deleted file mode 100644 index ac7994a9..00000000 --- a/ddd-lsp/src/bin/ddd-mock-lsp.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Mock LSP host binary — fixture-grade stdio server for hermetic tests. -//! -//! Flags: `--handshake roslyn` demands `solution/open`/`project/open` -//! before readiness; `--over-advertise type-hierarchy` advertises the -//! provider then answers `null`; `DDD_MOCK_READY_DELAY_MS` delays the -//! readiness signal to make the loading window observable. - -fn main() { - let args: Vec = std::env::args().collect(); - let roslyn_handshake = args - .windows(2) - .any(|w| w[0] == "--handshake" && w[1] == "roslyn"); - let delay_ms = std::env::var("DDD_MOCK_READY_DELAY_MS") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(0); - let over_advertise_type_hierarchy = args - .windows(2) - .any(|w| w[0] == "--over-advertise" && w[1] == "type-hierarchy"); - ddd_lsp::mock::serve_stdio(ddd_lsp::mock::MockConfig { - roslyn_handshake, - ready_delay: std::time::Duration::from_millis(delay_ms), - over_advertise_type_hierarchy, - }); -} diff --git a/ddd-lsp/src/capability.rs b/ddd-lsp/src/capability.rs deleted file mode 100644 index 6d9a84f8..00000000 --- a/ddd-lsp/src/capability.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! Capability probing — an operation's advertisement is a claim, never a verdict. -//! -//! An LSP server's `InitializeResult` capability set is a statement of -//! intent by its author. The only evidence that an operation works is a -//! live call returning a usable result, so every operation of the -//! declaration-level subset is probed at start-up against a known -//! declaration (G-track PRD §5.2, binding extractor-wide). -//! -//! Both failure directions have been observed on real servers: -//! -//! - **Over-advertising** — the flag is `true`, the call returns `null`. -//! This is the dangerous one. It is the shape of presumed discharge -//! (`DDD-delivery-02`) inside our own instrument: a gate that reads as -//! passed, zero edges extracted, a run that reports success. Left -//! unguarded, missing edges reach cross-codebase comparison, which reads -//! absence as divergence — ground fabricated by an instrument fault. -//! - **Under-advertising** — no flag, the call answers anyway. -//! -//! Therefore the probe result, not the flag, gates the mapping rows. An -//! operation that did not answer makes its row's absence *reported*, so a -//! reviewer can tell "this codebase has no hierarchy" apart from "the -//! instrument did not answer". - -use std::path::{Path, PathBuf}; - -use serde_json::Value; - -use crate::declaration::{self, Operation}; -use crate::error::Result; -use crate::host::Host; -use crate::protocol::RawSymbol; - -/// LSP `SymbolKind`s that name a type declaration — the anchor the probe -/// wants, because all six operations have a non-empty expectation there. -const TYPE_KINDS: [u64; 5] = [5, 10, 11, 23, 26]; - -/// Where the probe called: a real type declaration in the corpus. -#[derive(Debug, Clone, serde::Serialize)] -pub struct Anchor { - pub file: PathBuf, - pub name: String, - pub line: u64, - pub character: u64, -} - -/// The two ways an advertisement disagrees with behaviour. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum Divergence { - /// Advertised, did not answer. The dangerous direction. - OverAdvertised, - /// Not advertised, answered anyway. - UnderAdvertised, -} - -/// One operation's verdict. -#[derive(Debug, Clone, serde::Serialize)] -pub struct OperationProbe { - pub operation: Operation, - /// The `ServerCapabilities` flag as read; `None` when the server names - /// no such key at all. - pub advertised: Option, - /// Did a live call return a usable result? This is the verdict. - pub answered: bool, - pub divergence: Option, - /// What the call actually produced, in one line. - pub detail: String, -} - -/// The whole probe for one host. -#[derive(Debug, Clone, serde::Serialize)] -pub struct Probe { - pub language: String, - /// `None` when `documentSymbol` returned no type declaration to anchor - /// on — in which case every later operation is unprobed, not absent. - pub anchor: Option, - pub operations: Vec, -} - -impl Probe { - /// Is this operation usable? The probe answers, never the flag. - pub fn available(&self, operation: Operation) -> bool { - self.operations.iter().any(|o| o.operation == operation && o.answered) - } - - /// The operations that did not answer, whatever they advertise. - pub fn unavailable(&self) -> Vec { - self.operations.iter().filter(|o| !o.answered).map(|o| o.operation).collect() - } - - /// Every advertisement that disagreed with behaviour. - pub fn divergences(&self) -> Vec<&OperationProbe> { - self.operations.iter().filter(|o| o.divergence.is_some()).collect() - } - - /// A one-operation-per-line report. - pub fn report(&self) -> String { - let mut out = format!("capability probe — {}\n", self.language); - match &self.anchor { - Some(a) => out.push_str(&format!( - " anchor: {} at {}:{}:{}\n", - a.name, - a.file.display(), - a.line + 1, - a.character + 1 - )), - None => out.push_str(" anchor: none — documentSymbol found no type declaration\n"), - } - for probe in &self.operations { - let flag = match probe.advertised { - Some(true) => "advertised", - Some(false) => "declined", - None => "unadvertised", - }; - let verdict = if probe.answered { "AVAILABLE" } else { "UNAVAILABLE" }; - let note = match probe.divergence { - Some(Divergence::OverAdvertised) => " <- OVER-ADVERTISED", - Some(Divergence::UnderAdvertised) => " <- under-advertised", - None => "", - }; - out.push_str(&format!( - " {:<16} {:<13} {:<12} {}{}\n", - probe.operation.as_str(), - flag, - verdict, - probe.detail, - note - )); - } - out - } -} - -/// Probe every operation of the subset against a known declaration in -/// `file`. -/// -/// `documentSymbol` runs first: the anchor for the other five is derived -/// from its result, so a host that cannot list symbols yields a probe with -/// no anchor rather than five misleading absences. -pub fn probe(host: &mut Host, file: &Path) -> Result { - let language = host.language().to_string(); - let capabilities = host.server_capabilities()?; - let symbols = declaration::document_symbols(host, file).unwrap_or_default(); - let anchor = anchor_from(file, &symbols); - let mut operations = vec![row( - Operation::DocumentSymbol, - &capabilities, - !symbols.is_empty(), - format!("{} symbol(s)", symbols.len()), - )]; - for operation in Operation::ALL.iter().skip(1) { - let (answered, detail) = match &anchor { - Some(a) => call(host, *operation, a), - None => (false, "not probed — no anchor".to_string()), - }; - operations.push(row(*operation, &capabilities, answered, detail)); - } - Ok(Probe { language, anchor, operations }) -} - -/// Run one operation at the anchor, reducing it to answered-plus-detail. -/// -/// An error is not an escape: it is one way of not answering, and it is -/// recorded as the detail so a reviewer sees why. -fn call(host: &mut Host, operation: Operation, at: &Anchor) -> (bool, String) { - let (file, line, ch) = (at.file.as_path(), at.line, at.character); - match operation { - Operation::DocumentSymbol => (false, "probed first".to_string()), - Operation::WorkspaceSymbol => { - count(declaration::workspace_symbols(host, &at.name).map(|s| s.len()), "symbol") - } - Operation::Definition => { - count(declaration::definition(host, file, line, ch).map(|l| l.len()), "location") - } - Operation::References => count( - declaration::references(host, file, line, ch, true).map(|l| l.len()), - "reference", - ), - Operation::Hover => match declaration::hover(host, file, line, ch) { - Ok(Some(text)) => (true, format!("{} char(s)", text.chars().count())), - Ok(None) => (false, "empty".to_string()), - Err(e) => (false, format!("error: {e}")), - }, - Operation::TypeHierarchy => count( - declaration::prepare_type_hierarchy(host, file, line, ch).map(|i| i.len()), - "item", - ), - } -} - -fn count(result: Result, noun: &str) -> (bool, String) { - match result { - Ok(0) => (false, format!("0 {noun}s")), - Ok(n) => (true, format!("{n} {noun}(s)")), - Err(e) => (false, format!("error: {e}")), - } -} - -/// Read the flag, compare it to behaviour, name the disagreement. -fn row( - operation: Operation, - capabilities: &Value, - answered: bool, - detail: String, -) -> OperationProbe { - let advertised = advertises(capabilities, operation); - let divergence = match (advertised, answered) { - (Some(true), false) => Some(Divergence::OverAdvertised), - (Some(false) | None, true) => Some(Divergence::UnderAdvertised), - _ => None, - }; - OperationProbe { operation, advertised, answered, divergence, detail } -} - -/// The capability flag as the server states it. -/// -/// A provider key may be a bare boolean or an options object; an object is -/// an advertisement just as much as `true` is. -fn advertises(capabilities: &Value, operation: Operation) -> Option { - match capabilities.get(operation.capability_key()) { - None | Some(Value::Null) => None, - Some(Value::Bool(b)) => Some(*b), - Some(_) => Some(true), - } -} - -/// The first type declaration in the file — the probe's known declaration. -fn anchor_from(file: &Path, symbols: &[RawSymbol]) -> Option { - let symbol = symbols - .iter() - .find(|s| TYPE_KINDS.contains(&s.kind)) - .or_else(|| symbols.first())?; - Some(Anchor { - file: file.to_path_buf(), - name: symbol.name.clone(), - line: symbol.sel_line, - character: symbol.sel_char, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - fn sym(name: &str, kind: u64, line: u64, ch: u64) -> RawSymbol { - RawSymbol { - name: name.into(), - kind, - container: String::new(), - container_kind: 0, - start_line: line, - end_line: line, - sel_line: line, - sel_char: ch, - detail: None, - } - } - - #[test] - fn the_anchor_prefers_a_type_declaration_over_a_namespace() { - let symbols = vec![sym("Acme.Sql", 3, 0, 10), sym("ProfileSettings", 5, 4, 21)]; - let anchor = anchor_from(Path::new("/x/A.cs"), &symbols).expect("anchor"); - assert_eq!(anchor.name, "ProfileSettings"); - assert_eq!((anchor.line, anchor.character), (4, 21)); - } - - #[test] - fn no_symbols_means_no_anchor() { - assert!(anchor_from(Path::new("/x/A.cs"), &[]).is_none()); - } - - /// The kotlin-lsp shape: `typeHierarchyProvider: true`, result `null`. - /// This must read as UNAVAILABLE, flagged, never as an empty hierarchy. - #[test] - fn an_advertised_operation_that_does_not_answer_is_over_advertised() { - let caps = json!({"typeHierarchyProvider": true}); - let probe = row(Operation::TypeHierarchy, &caps, false, "0 items".into()); - assert!(!probe.answered); - assert_eq!(probe.divergence, Some(Divergence::OverAdvertised)); - } - - /// The Roslyn shape: no `typeHierarchy` client capability declared, - /// the server answers regardless. - #[test] - fn an_unadvertised_operation_that_answers_is_under_advertised() { - let probe = row(Operation::TypeHierarchy, &json!({}), true, "1 item".into()); - assert!(probe.answered); - assert_eq!(probe.divergence, Some(Divergence::UnderAdvertised)); - } - - #[test] - fn agreement_in_either_direction_is_not_a_divergence() { - let yes = json!({"hoverProvider": true}); - assert_eq!(row(Operation::Hover, &yes, true, String::new()).divergence, None); - let no = json!({"hoverProvider": false}); - assert_eq!(row(Operation::Hover, &no, false, String::new()).divergence, None); - } - - #[test] - fn an_options_object_is_an_advertisement_just_as_true_is() { - let caps = json!({"referencesProvider": {"workDoneProgress": true}}); - assert_eq!(advertises(&caps, Operation::References), Some(true)); - assert_eq!(advertises(&json!({}), Operation::References), None); - assert_eq!(advertises(&json!({"referencesProvider": null}), Operation::References), None); - } - - #[test] - fn an_error_is_a_way_of_not_answering_not_an_escape() { - let (answered, detail) = count(Err(crate::LspError::Io("boom".into())), "location"); - assert!(!answered); - assert!(detail.starts_with("error:"), "{detail}"); - } - - #[test] - fn availability_reads_the_probe_never_the_flag() { - let probe = Probe { - language: "csharp".into(), - anchor: None, - operations: vec![ - row(Operation::TypeHierarchy, &json!({"typeHierarchyProvider": true}), false, "0 items".into()), - row(Operation::Hover, &json!({}), true, "12 chars".into()), - ], - }; - assert!(!probe.available(Operation::TypeHierarchy)); - assert!(probe.available(Operation::Hover)); - assert_eq!(probe.unavailable(), vec![Operation::TypeHierarchy]); - assert_eq!(probe.divergences().len(), 2); - } -} diff --git a/ddd-lsp/src/classify.rs b/ddd-lsp/src/classify.rs deleted file mode 100644 index 145e2263..00000000 --- a/ddd-lsp/src/classify.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Snapshot diffing — before/after symbol facts into surface events. -//! -//! Language-neutral: facts extraction plus the policy table come in from -//! the adapter; this module only pairs symbols across the edit, names the -//! change, then lets the table decide. Symbols pair by -//! (container, kind, name); same-key overloads pair positionally after -//! sorting by signature, so an unchanged overload set yields no events. - -use std::collections::BTreeMap; - -use crate::adapter::{Adapter, AdapterFlags}; -use crate::protocol::RawSymbol; -use crate::surface::{decide, ChangeKind, PolicyRow, SurfaceEvent, SymbolFacts}; - -/// Classify one edit: the adapter derives facts on both sides, the diff -/// names each change, the policy table rules on it. -pub fn classify_edit( - adapter: &Adapter, - flags: &AdapterFlags, - before_text: &str, - before_symbols: &[RawSymbol], - after_text: &str, - after_symbols: &[RawSymbol], -) -> Vec { - let before = (adapter.facts)(before_text, before_symbols, flags); - let after = (adapter.facts)(after_text, after_symbols, flags); - let rows = (adapter.policy)(flags); - diff_facts(&rows, adapter.visibility_rank, before, after) -} - -/// Pair the two fact sets, emit one event per changed symbol. -pub fn diff_facts( - rows: &[PolicyRow], - visibility_rank: fn(&str) -> u8, - before: Vec, - after: Vec, -) -> Vec { - let mut events = Vec::new(); - let mut before_by_key = group_by_key(before); - for (key, mut afters) in group_by_key(after) { - let mut befores = before_by_key.remove(&key).unwrap_or_default(); - afters.sort_by(|a, b| a.signature.cmp(&b.signature)); - befores.sort_by(|a, b| a.signature.cmp(&b.signature)); - pair_off(rows, visibility_rank, befores, afters, &mut events); - } - for (_, befores) in before_by_key { - for facts in befores { - push_event(rows, ChangeKind::Removed, facts.visibility.clone(), facts, None, &mut events); - } - } - events -} - -fn group_by_key( - list: Vec, -) -> BTreeMap<(String, String, String), Vec> { - let mut map: BTreeMap<_, Vec> = BTreeMap::new(); - for facts in list { - map.entry(facts.key()).or_default().push(facts); - } - map -} - -/// Same-key symbols: pair positionally, surplus is added/removed. -fn pair_off( - rows: &[PolicyRow], - visibility_rank: fn(&str) -> u8, - befores: Vec, - afters: Vec, - events: &mut Vec, -) { - let paired = befores.len().min(afters.len()); - let mut afters = afters; - let surplus_after = afters.split_off(paired); - for (b, a) in befores.iter().take(paired).zip(afters) { - if let Some(change) = change_of(b, &a) { - let vis = exposed_visibility(visibility_rank, b, &a); - push_event(rows, change, vis, a, Some(b.clone()), events); - } - } - for facts in surplus_after { - push_event(rows, ChangeKind::Added, facts.visibility.clone(), facts, None, events); - } - for facts in befores.into_iter().skip(paired) { - push_event(rows, ChangeKind::Removed, facts.visibility.clone(), facts, None, events); - } -} - -/// Name the change between two paired states; `None` means untouched. -fn change_of(before: &SymbolFacts, after: &SymbolFacts) -> Option { - if before.signature != after.signature { - Some(ChangeKind::SignatureChanged) - } else if before.decorators != after.decorators { - Some(ChangeKind::DecoratorChanged) - } else if before.visibility != after.visibility { - Some(ChangeKind::VisibilityChanged) - } else { - None - } -} - -/// The visibility a paired change is judged at: the more exposed of the -/// two sides — promoting to public, demoting from public, or changing a -/// signature while demoting are all boundary events. -fn exposed_visibility(rank: fn(&str) -> u8, before: &SymbolFacts, after: &SymbolFacts) -> String { - if rank(&before.visibility) > rank(&after.visibility) { - before.visibility.clone() - } else { - after.visibility.clone() - } -} - -fn push_event( - rows: &[PolicyRow], - change: ChangeKind, - judged_visibility: String, - facts: SymbolFacts, - before: Option, - events: &mut Vec, -) { - let (surface, rule, rule_claim) = decide(rows, change, &facts, &judged_visibility); - events.push(SurfaceEvent { change, facts, before, surface, rule, rule_claim }); -} - -#[cfg(test)] -#[path = "classify_tests.rs"] -mod tests; diff --git a/ddd-lsp/src/classify_tests.rs b/ddd-lsp/src/classify_tests.rs deleted file mode 100644 index 6c09e97b..00000000 --- a/ddd-lsp/src/classify_tests.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! Classifier coverage: every policy row triggered plus not triggered, -//! with the committed M3/M4 fixture files as the edit substrate. - -use super::*; -use crate::adapter::{bicep, csharp, AdapterFlags}; -use crate::mock::parse::document_symbols; -use crate::protocol::flatten_symbols; -use crate::surface::SurfaceEvent; - -/// The committed fixture substrates (extending the M2 fixture set). -const CS_BASE: &str = include_str!("../../ddd-cli/tests/fixtures/csharp/Library.cs"); -const BI_BASE: &str = include_str!("../../ddd-cli/tests/fixtures/bicep/storage-module.bicep"); - -fn events( - adapter: &Adapter, - uri: &str, - flags: &AdapterFlags, - before: &str, - after: &str, -) -> Vec { - let bs = flatten_symbols(&document_symbols(uri, before)); - let as_ = flatten_symbols(&document_symbols(uri, after)); - classify_edit(adapter, flags, before, &bs, after, &as_) -} - -fn cs(before: &str, after: &str) -> Vec { - events(&csharp::ADAPTER, "file:///t.cs", &AdapterFlags::default(), before, after) -} - -fn bi(before: &str, after: &str) -> Vec { - events(&bicep::ADAPTER, "file:///m.bicep", &AdapterFlags::default(), before, after) -} - -fn surface_rules(evts: &[SurfaceEvent]) -> Vec<&'static str> { - evts.iter().filter(|e| e.surface).filter_map(|e| e.rule).collect() -} - -/// Assert the fixture actually contains the anchor an edit rewrites. -fn edit(base: &str, from: &str, to: &str) -> String { - assert!(base.contains(from), "fixture lost the anchor `{from}` — update the tests"); - base.replace(from, to) -} - -#[test] -fn cs_add_exposed_triggers_on_public_member_not_private() { - let with_public = - edit(CS_BASE, " public void Ping() { }\n", " public void Ping() { }\n public void Pong() { }\n"); - assert_eq!(surface_rules(&cs(CS_BASE, &with_public)), vec!["cs-add-exposed"]); - let with_private = - edit(CS_BASE, " private void Helper() { }\n", " private void Helper() { }\n private void Quiet() { }\n"); - let evts = cs(CS_BASE, &with_private); - assert!(surface_rules(&evts).is_empty(), "{evts:?}"); - assert_eq!(evts[0].rule, Some("cs-private-nonsurface")); -} - -#[test] -fn cs_signature_change_triggers_on_public_not_on_body() { - let changed = edit(CS_BASE, "public void Ping() { }", "public void Ping(int count) { }"); - assert_eq!(surface_rules(&cs(CS_BASE, &changed)), vec!["cs-signature-exposed"]); - let body = edit(CS_BASE, "private void Helper() { }", "private void Helper() { Ping(); }"); - assert!(cs(CS_BASE, &body).is_empty()); -} - -#[test] -fn cs_remove_exposed_triggers_on_public_not_private() { - let no_public = edit(CS_BASE, " protected void Guarded() { }\n", ""); - assert_eq!(surface_rules(&cs(CS_BASE, &no_public)), vec!["cs-remove-exposed"]); - let no_private = edit(CS_BASE, " private void Helper() { }\n", ""); - assert!(surface_rules(&cs(CS_BASE, &no_private)).is_empty()); -} - -#[test] -fn cs_new_ctor_param_on_public_type_is_surface() { - let after = - edit(CS_BASE, "public PublicApi(int retries) { }", "public PublicApi(int retries, string name) { }"); - let evts = cs(CS_BASE, &after); - assert_eq!(surface_rules(&evts), vec!["cs-signature-exposed"]); - let surfaced: Vec<_> = evts.iter().filter(|e| e.surface).collect(); - assert_eq!(surfaced[0].facts.kind, "constructor"); -} - -#[test] -fn cs_interface_member_forces_implementors_when_exposed() { - let after = edit(CS_BASE, " void Handle();\n", " void Handle();\n void Retry();\n"); - assert_eq!(surface_rules(&cs(CS_BASE, after.as_str())), vec!["cs-interface-member"]); - let internal_before = edit(CS_BASE, "public interface IContract", "internal interface IContract"); - let internal_after = - edit(&after, "public interface IContract", "internal interface IContract"); - assert!(surface_rules(&cs(&internal_before, &internal_after)).is_empty()); -} - -#[test] -fn cs_exported_attribute_marks_any_visibility_as_surface() { - let changed = edit(CS_BASE, "private string Fetch() { return \"x\"; }", "private string Fetch(int id) { return \"x\"; }"); - assert_eq!(surface_rules(&cs(CS_BASE, &changed)), vec!["cs-exported-endpoint"]); - let plain_attr = edit( - CS_BASE, - " private void Helper() { }\n", - " private void Helper() { }\n [Obsolete]\n private void Old() { }\n", - ); - assert!(surface_rules(&cs(CS_BASE, &plain_attr)).is_empty()); -} - -#[test] -fn cs_internal_flips_with_the_library_posture_flag() { - let after = edit( - CS_BASE, - " internal void Reach() { }\n", - " internal void Reach() { }\n internal void Farther() { }\n", - ); - assert!(surface_rules(&cs(CS_BASE, &after)).is_empty()); - let lib = AdapterFlags { internal_is_surface: true, ..AdapterFlags::default() }; - let evts = events(&csharp::ADAPTER, "file:///t.cs", &lib, CS_BASE, &after); - assert_eq!(surface_rules(&evts), vec!["cs-internal-surface"]); -} - -#[test] -fn cs_visibility_flip_is_judged_at_the_more_exposed_side() { - let promoted = edit(CS_BASE, "private int hidden;", "public int hidden;"); - assert_eq!(surface_rules(&cs(CS_BASE, &promoted)), vec!["cs-visibility-boundary"]); - let demoted = edit(CS_BASE, "public void Ping() { }", "private void Ping() { }"); - assert_eq!(surface_rules(&cs(CS_BASE, &demoted)), vec!["cs-visibility-boundary"]); - let sideways = edit(CS_BASE, "private int hidden;", "internal int hidden;"); - assert!(surface_rules(&cs(CS_BASE, &sideways)).is_empty()); -} - -/// `dec/ddd/enum-member-gap-priced`: the DDD-adapter-03 hole, closed. A -/// member added to a public enum is demanded; the same edit on an internal -/// enum is not (the app-repo posture caps it below EXPOSED). -#[test] -fn cs_enum_member_addition_triggers_on_public_enum_not_internal() { - let grown = edit(CS_BASE, " Active,\n", " Active,\n Escalated,\n"); - let evts = cs(CS_BASE, &grown); - assert_eq!(surface_rules(&evts), vec!["cs-enum-member"], "{evts:?}"); - let added: Vec<_> = evts.iter().filter(|e| e.facts.name == "Escalated").collect(); - assert_eq!(added[0].facts.kind, "enum-member"); - assert_eq!(added[0].facts.visibility, "public"); - let internal_grown = edit(CS_BASE, " On,\n", " On,\n Auto,\n"); - let evts = cs(CS_BASE, &internal_grown); - assert!(surface_rules(&evts).is_empty(), "{evts:?}"); - assert_eq!(evts[0].rule, Some("cs-internal-nonsurface")); -} - -/// Removal breaks consumers the same way (their arms stop compiling), so the -/// row demands it too. -#[test] -fn cs_enum_member_removal_is_surface() { - let shrunk = edit(CS_BASE, " Active,\n Suspended,\n", " Active,\n"); - assert_eq!(surface_rules(&cs(CS_BASE, &shrunk)), vec!["cs-enum-member"]); -} - -#[test] -fn cs_container_visibility_caps_members() { - let after = edit( - CS_BASE, - " public void LoudButCapped() { }\n", - " public void LoudButCapped() { }\n public void AlsoCapped() { }\n", - ); - let evts = cs(CS_BASE, &after); - assert!(surface_rules(&evts).is_empty(), "{evts:?}"); - let added: Vec<_> = evts.iter().filter(|e| e.facts.name == "AlsoCapped").collect(); - assert_eq!(added[0].facts.visibility, "internal"); -} - -#[test] -fn bi_param_membership_triggers_on_param_not_var() { - let with_param = edit(BI_BASE, "param accountName string\n", "param accountName string\nparam replicas int = 2\n"); - assert_eq!(surface_rules(&bi(BI_BASE, &with_param)), vec!["bi-param-membership"]); - let removed = edit(BI_BASE, "param skuName string = 'Standard_LRS'\n", ""); - // Removing the param also changes the resource body reference? No — - // sku lines are body, not name/scope, so only the param event fires. - assert_eq!(surface_rules(&bi(BI_BASE, &removed)), vec!["bi-param-membership"]); - let with_var = edit(BI_BASE, "var namePrefix = 'ddd'\n", "var namePrefix = 'ddd'\nvar suffix = 'x'\n"); - let evts = bi(BI_BASE, &with_var); - assert!(surface_rules(&evts).is_empty()); - assert_eq!(evts[0].rule, Some("bi-internal")); -} - -#[test] -fn bi_param_retype_triggers_but_default_change_does_not() { - let retyped = edit(BI_BASE, "param skuName string =", "param skuName int ="); - assert_eq!(surface_rules(&bi(BI_BASE, &retyped)), vec!["bi-param-retype"]); - let new_default = edit(BI_BASE, "param skuName string = 'Standard_LRS'", "param skuName string = 'Premium_LRS'"); - assert!(bi(BI_BASE, &new_default).is_empty()); -} - -#[test] -fn bi_decorator_change_is_a_tolerance_change() { - let widened = edit( - BI_BASE, - "@allowed(['westeurope', 'northeurope'])", - "@allowed(['westeurope', 'northeurope', 'swedencentral'])", - ); - assert_eq!(surface_rules(&bi(BI_BASE, &widened)), vec!["bi-param-tolerance"]); - let comment = edit(BI_BASE, "// every policy row", "// each policy row"); - assert!(bi(BI_BASE, &comment).is_empty()); -} - -#[test] -fn bi_output_change_is_surface() { - let changed = edit(BI_BASE, "output storageId string = stg.id", "output storageId string = stg.properties.primaryEndpoints.blob"); - assert_eq!(surface_rules(&bi(BI_BASE, &changed)), vec!["bi-output-contract"]); - let removed = edit(BI_BASE, "output storageName string = stg.name\n", ""); - assert_eq!(surface_rules(&bi(BI_BASE, &removed)), vec!["bi-output-contract"]); -} - -#[test] -fn bi_resource_name_scope_is_contract_but_body_is_not() { - let renamed = edit(BI_BASE, " name: '${namePrefix}${accountName}'", " name: accountName"); - assert_eq!(surface_rules(&bi(BI_BASE, &renamed)), vec!["bi-resource-contract"]); - let body = edit(BI_BASE, " kind: 'StorageV2'", " kind: 'BlobStorage'"); - assert!(bi(BI_BASE, &body).is_empty()); -} - -#[test] -fn bi_param_ground_provenance_maps_per_the_table() { - let syms = flatten_symbols(&document_symbols("file:///m.bicep", BI_BASE)); - let facts = (bicep::ADAPTER.facts)(BI_BASE, &syms, &AdapterFlags::default()); - let prov: Vec<(&str, &str)> = facts - .iter() - .filter(|f| f.kind == "param") - .map(|f| (f.name.as_str(), f.extra["ground_provenance"].as_str())) - .collect(); - assert_eq!( - prov, - vec![ - ("location", "controlled"), - ("accountName", "observed"), - ("skuName", "controlled"), - ("deployedAt", "inferred"), - ] - ); -} diff --git a/ddd-lsp/src/client.rs b/ddd-lsp/src/client.rs deleted file mode 100644 index aeea31f9..00000000 --- a/ddd-lsp/src/client.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! LSP client core — a stdio child process with request correlation. -//! -//! One reader thread routes responses to waiting callers by id, answers -//! server-to-client requests with safe defaults, records notifications -//! into [`ServerState`] (readiness flags, restore notes, published -//! diagnostics). Callers block on a channel with a deadline, so a hung -//! host surfaces as a `Timeout`, never as a silent hang (PRD §11). - -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::io::BufReader; -use std::path::Path; -use std::process::{Child, ChildStdin, Command, Stdio}; -use std::sync::atomic::{AtomicI64, Ordering}; -use std::sync::mpsc; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use serde_json::{json, Value}; - -use crate::error::{LspError, Result}; -use crate::jsonrpc; - -/// What the reader thread has learned from the host so far. -#[derive(Default)] -pub struct ServerState { - /// Notification methods seen at least once (readiness signals live here). - pub flags: Mutex>, - /// The most recent params per notification method. Hosts that repeat one - /// notification and carry its meaning in the payload — rust-analyzer's - /// `experimental/serverStatus` arrives first with `quiescent: false` — - /// cannot be read off `flags` alone. Bounded by the number of distinct - /// methods a host sends, not by how often it sends them. - pub last_params: Mutex>, - /// Human-readable observations (restore requests, log lines worth keeping). - pub notes: Mutex>, - /// Latest `textDocument/publishDiagnostics` params per uri. - pub diagnostics: Mutex>, -} - -type Pending = Arc>>>; - -/// A live language-server child spoken to over stdio. -pub struct LspClient { - child: Mutex, - writer: Arc>, - pending: Pending, - pub state: Arc, - next_id: AtomicI64, -} - -impl LspClient { - /// Spawn `command` in `cwd` with piped stdio; stderr is discarded. - pub fn spawn(command: &[String], cwd: &Path) -> Result { - let (program, args) = command.split_first().ok_or_else(|| { - LspError::Spawn("empty host command".to_string()) - })?; - let mut child = Command::new(program) - .args(args) - .current_dir(cwd) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .map_err(|e| LspError::Spawn(format!("{program}: {e}")))?; - let stdin = child.stdin.take().ok_or_else(|| LspError::Spawn("no stdin".into()))?; - let stdout = child.stdout.take().ok_or_else(|| LspError::Spawn("no stdout".into()))?; - let writer = Arc::new(Mutex::new(stdin)); - let pending: Pending = Arc::new(Mutex::new(HashMap::new())); - let state = Arc::new(ServerState::default()); - let (w, p, s) = (writer.clone(), pending.clone(), state.clone()); - std::thread::spawn(move || reader_loop(BufReader::new(stdout), w, p, s)); - Ok(Self { child: Mutex::new(child), writer, pending, state, next_id: AtomicI64::new(1) }) - } - - /// Send a request; block for the result up to `timeout`. - pub fn request(&self, method: &str, params: Value, timeout: Duration) -> Result { - let id = self.next_id.fetch_add(1, Ordering::SeqCst); - let (tx, rx) = mpsc::channel(); - if let Ok(mut pending) = self.pending.lock() { - pending.insert(id, tx); - } - let msg = json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}); - self.write(&msg)?; - let response = rx.recv_timeout(timeout).map_err(|_| { - if let Ok(mut pending) = self.pending.lock() { - pending.remove(&id); - } - LspError::Timeout(format!("{method} unanswered after {timeout:?}")) - })?; - if let Some(err) = response.get("error") { - return Err(LspError::Rpc { - code: err.get("code").and_then(Value::as_i64).unwrap_or(0), - message: err.get("message").and_then(Value::as_str).unwrap_or("?").to_string(), - }); - } - Ok(response.get("result").cloned().unwrap_or(Value::Null)) - } - - /// Send a notification (no answer expected). - pub fn notify(&self, method: &str, params: Value) -> Result<()> { - self.write(&json!({"jsonrpc": "2.0", "method": method, "params": params})) - } - - fn write(&self, msg: &Value) -> Result<()> { - let mut w = self.writer.lock().map_err(|_| LspError::Io("writer poisoned".into()))?; - jsonrpc::write_message(&mut *w, msg).map_err(LspError::from) - } - - /// Is the child process still running? - pub fn is_alive(&self) -> bool { - match self.child.lock() { - Ok(mut child) => matches!(child.try_wait(), Ok(None)), - Err(_) => false, - } - } - - /// The child's OS pid (for external health tooling plus tests). - pub fn pid(&self) -> Option { - self.child.lock().ok().map(|c| c.id()) - } - - /// Best-effort teardown: kill the child; the reader thread ends on EOF. - pub fn shutdown(&self) { - if let Ok(mut child) = self.child.lock() { - let _ = child.kill(); - let _ = child.wait(); - } - } -} - -impl Drop for LspClient { - fn drop(&mut self) { - self.shutdown(); - } -} - -/// Route every inbound frame: responses to callers, server requests to -/// [`answer_server_request`], notifications into the shared state. -fn reader_loop( - mut r: BufReader, - writer: Arc>, - pending: Pending, - state: Arc, -) { - while let Ok(Some(msg)) = jsonrpc::read_message(&mut r) { - let has_id = msg.get("id").map(|v| !v.is_null()).unwrap_or(false); - let method = msg.get("method").and_then(Value::as_str).map(str::to_string); - match (has_id, method) { - (true, Some(m)) => answer_server_request(&writer, &msg, &m), - (true, None) => { - let id = msg.get("id").and_then(Value::as_i64).unwrap_or(-1); - let tx = pending.lock().ok().and_then(|mut p| p.remove(&id)); - if let Some(tx) = tx { - let _ = tx.send(msg); - } - } - (false, Some(m)) => record_notification(&state, &m, &msg), - (false, None) => {} - } - } -} - -/// Answer a server-to-client request with the safest possible default, so -/// hosts that expect an editor keep running under a headless client. -fn answer_server_request(writer: &Arc>, msg: &Value, method: &str) { - let id = msg.get("id").cloned().unwrap_or(Value::Null); - let response = match method { - "workspace/configuration" => { - let n = msg - .pointer("/params/items") - .and_then(Value::as_array) - .map(Vec::len) - .unwrap_or(1); - json!({"jsonrpc": "2.0", "id": id, "result": vec![Value::Null; n]}) - } - "window/workDoneProgress/create" | "client/registerCapability" - | "client/unregisterCapability" => { - json!({"jsonrpc": "2.0", "id": id, "result": Value::Null}) - } - "workspace/applyEdit" => { - json!({"jsonrpc": "2.0", "id": id, "result": {"applied": false}}) - } - _ => json!({"jsonrpc": "2.0", "id": id, - "error": {"code": -32601, "message": format!("unhandled: {method}")}}), - }; - if let Ok(mut w) = writer.lock() { - let _ = jsonrpc::write_message(&mut *w, &response); - } -} - -/// Keep what matters from a notification: publish-diagnostics per uri, a -/// note for restore-style signals, the method name as a readiness flag. -fn record_notification(state: &ServerState, method: &str, msg: &Value) { - if method == "textDocument/publishDiagnostics" { - if let Some(uri) = msg.pointer("/params/uri").and_then(Value::as_str) { - if let Ok(mut d) = state.diagnostics.lock() { - d.insert(uri.to_string(), msg.get("params").cloned().unwrap_or(Value::Null)); - } - } - return; - } - if method.to_ascii_lowercase().contains("restore") { - if let Ok(mut notes) = state.notes.lock() { - notes.push(format!("host signalled restore: {method}")); - } - } - if let Ok(mut flags) = state.flags.lock() { - flags.insert(method.to_string()); - } - if let Ok(mut last) = state.last_params.lock() { - last.insert(method.to_string(), msg.get("params").cloned().unwrap_or(Value::Null)); - } -} diff --git a/ddd-lsp/src/declaration.rs b/ddd-lsp/src/declaration.rs deleted file mode 100644 index 6564d537..00000000 --- a/ddd-lsp/src/declaration.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! The declaration-level LSP subset the code extractor reads. -//! -//! Six operations, named by the G-track PRD §5.2: `documentSymbol`, -//! `workspaceSymbol`, `typeHierarchy`, `references`, `definition`, `hover`. -//! Not completion, not refactoring, not diagnostics — holding the extractor -//! to the declared surface is what keeps adapter maturity risk (§5.4) off -//! it. -//! -//! Each function here is a thin call over [`Host::request`], normalized -//! through `protocol`. The host layer is method-generic, so wiring an -//! operation is additive: nothing below this module changed to add -//! `definition` or `typeHierarchy`. - -use std::path::Path; - -use serde_json::{json, Value}; - -use crate::error::Result; -use crate::host::Host; -use crate::protocol::{ - self, hierarchy_params, normalize_hierarchy_items, normalize_locations, position_params, - to_uri, FileRange, HierarchyItem, RawSymbol, -}; - -/// One operation of the declaration-level subset. -/// -/// The extractor names rows by this, the capability probe reports by this, -/// and a mapping row that cannot fire says which one did not answer. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub enum Operation { - DocumentSymbol, - WorkspaceSymbol, - Definition, - References, - Hover, - TypeHierarchy, -} - -impl Operation { - /// Every operation of the subset, in probe order — `DocumentSymbol` - /// first because the probe's anchor is derived from its result. - pub const ALL: [Operation; 6] = [ - Operation::DocumentSymbol, - Operation::WorkspaceSymbol, - Operation::Definition, - Operation::References, - Operation::Hover, - Operation::TypeHierarchy, - ]; - - /// The name used in reports. - pub fn as_str(&self) -> &'static str { - match self { - Operation::DocumentSymbol => "documentSymbol", - Operation::WorkspaceSymbol => "workspaceSymbol", - Operation::Definition => "definition", - Operation::References => "references", - Operation::Hover => "hover", - Operation::TypeHierarchy => "typeHierarchy", - } - } - - /// The `ServerCapabilities` key a host advertises this under, if it - /// advertises it at all. Read as a claim, never as a verdict — see - /// [`crate::capability`]. - pub fn capability_key(&self) -> &'static str { - match self { - Operation::DocumentSymbol => "documentSymbolProvider", - Operation::WorkspaceSymbol => "workspaceSymbolProvider", - Operation::Definition => "definitionProvider", - Operation::References => "referencesProvider", - Operation::Hover => "hoverProvider", - Operation::TypeHierarchy => "typeHierarchyProvider", - } - } -} - -impl std::fmt::Display for Operation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -/// `textDocument/documentSymbol` — the declarations in one file. -pub fn document_symbols(host: &mut Host, file: &Path) -> Result> { - host.ensure_open(file)?; - let result = - host.request("textDocument/documentSymbol", json!({"textDocument": {"uri": to_uri(file)}}))?; - Ok(protocol::flatten_symbols(&result)) -} - -/// `workspace/symbol` — declarations matching a query across the workspace. -pub fn workspace_symbols(host: &mut Host, query: &str) -> Result> { - let result = host.request("workspace/symbol", json!({"query": query}))?; - Ok(protocol::flatten_symbols(&result)) -} - -/// `textDocument/definition` — where the symbol under the position is -/// declared. -/// -/// The result is `Location | Location[] | LocationLink[]`, every shape -/// `normalize_locations` already accepted before this operation was wired: -/// no new normaliser, as the entry probe found. -pub fn definition(host: &mut Host, file: &Path, line: u64, character: u64) -> Result> { - host.ensure_open(file)?; - let result = host.request("textDocument/definition", position_params(file, line, character))?; - Ok(normalize_locations(&result)) -} - -/// `textDocument/references` — the use sites of the symbol under the -/// position; the raw reference edges the usage-inferred mapping row reads. -pub fn references( - host: &mut Host, - file: &Path, - line: u64, - character: u64, - include_declaration: bool, -) -> Result> { - host.ensure_open(file)?; - let mut params = position_params(file, line, character); - params["context"] = json!({"includeDeclaration": include_declaration}); - let result = host.request("textDocument/references", params)?; - Ok(normalize_locations(&result)) -} - -/// `textDocument/hover` — the server-rendered declaration header, doc -/// comment included, flattened to plain text. -pub fn hover(host: &mut Host, file: &Path, line: u64, character: u64) -> Result> { - host.ensure_open(file)?; - let result = host.request("textDocument/hover", position_params(file, line, character))?; - Ok(hover_text(&result)) -} - -/// `textDocument/prepareTypeHierarchy` — the anchor item the two -/// directional calls take. -pub fn prepare_type_hierarchy( - host: &mut Host, - file: &Path, - line: u64, - character: u64, -) -> Result> { - host.ensure_open(file)?; - let result = - host.request("textDocument/prepareTypeHierarchy", position_params(file, line, character))?; - Ok(normalize_hierarchy_items(&result)) -} - -/// `typeHierarchy/supertypes` — what the item derives from. -pub fn supertypes(host: &mut Host, item: &HierarchyItem) -> Result> { - let result = host.request("typeHierarchy/supertypes", hierarchy_params(item))?; - Ok(normalize_hierarchy_items(&result)) -} - -/// `typeHierarchy/subtypes` — what derives from the item. -pub fn subtypes(host: &mut Host, item: &HierarchyItem) -> Result> { - let result = host.request("typeHierarchy/subtypes", hierarchy_params(item))?; - Ok(normalize_hierarchy_items(&result)) -} - -/// Flatten a `Hover` result's `contents` to plain text. -/// -/// The field is a union three versions of the protocol deep: a -/// `MarkupContent`, a marked string, or an array of either. -fn hover_text(result: &Value) -> Option { - let contents = result.get("contents")?; - let text = match contents { - Value::String(s) => s.clone(), - Value::Array(items) => items - .iter() - .filter_map(marked_string) - .collect::>() - .join("\n"), - Value::Object(_) => marked_string(contents)?, - _ => return None, - }; - let text = text.trim(); - (!text.is_empty()).then(|| text.to_string()) -} - -fn marked_string(v: &Value) -> Option { - match v { - Value::String(s) => Some(s.clone()), - Value::Object(_) => v.get("value").and_then(Value::as_str).map(str::to_string), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn every_operation_has_a_distinct_capability_key() { - let mut keys: Vec<&str> = Operation::ALL.iter().map(|o| o.capability_key()).collect(); - keys.sort_unstable(); - let before = keys.len(); - keys.dedup(); - assert_eq!(keys.len(), before); - } - - #[test] - fn document_symbol_probes_first_because_the_anchor_comes_from_it() { - assert_eq!(Operation::ALL[0], Operation::DocumentSymbol); - } - - #[test] - fn hover_flattens_every_contents_shape() { - let markup = json!({"contents": {"kind": "markdown", "value": "readonly record struct LocationId"}}); - assert_eq!(hover_text(&markup).as_deref(), Some("readonly record struct LocationId")); - let plain = json!({"contents": "a plain marked string"}); - assert_eq!(hover_text(&plain).as_deref(), Some("a plain marked string")); - let list = json!({"contents": [{"language": "csharp", "value": "class Foo"}, "Identifies a location."]}); - assert_eq!(hover_text(&list).as_deref(), Some("class Foo\nIdentifies a location.")); - } - - #[test] - fn a_silent_hover_is_none_not_an_empty_string() { - assert_eq!(hover_text(&Value::Null), None); - assert_eq!(hover_text(&json!({"contents": ""})), None); - assert_eq!(hover_text(&json!({"contents": {"kind": "markdown", "value": " "}})), None); - } -} diff --git a/ddd-lsp/src/error.rs b/ddd-lsp/src/error.rs deleted file mode 100644 index f3671062..00000000 --- a/ddd-lsp/src/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Error vocabulary for the LSP client stack. - -use std::fmt; - -/// Everything that can go wrong between a tool call and a host answer. -#[derive(Debug)] -pub enum LspError { - /// The host process could not be spawned (missing tool, bad command). - Spawn(String), - /// A transport-level read/write failure (host died mid-conversation). - Io(String), - /// The host did not answer within the request deadline. - Timeout(String), - /// The host answered with a JSON-RPC error. - Rpc { code: i64, message: String }, - /// The answer did not have the shape the protocol promises. - Protocol(String), - /// No adapter claims the file (unknown extension, no override). - NoAdapter(String), -} - -impl fmt::Display for LspError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Spawn(m) => write!(f, "language server spawn failed: {m}"), - Self::Io(m) => write!(f, "language server transport: {m}"), - Self::Timeout(m) => write!(f, "language server timeout: {m}"), - Self::Rpc { code, message } => write!(f, "language server error {code}: {message}"), - Self::Protocol(m) => write!(f, "language server protocol: {m}"), - Self::NoAdapter(m) => write!(f, "no language adapter: {m}"), - } - } -} - -impl std::error::Error for LspError {} - -impl From for LspError { - fn from(e: std::io::Error) -> Self { - Self::Io(e.to_string()) - } -} - -pub type Result = std::result::Result; diff --git a/ddd-lsp/src/host.rs b/ddd-lsp/src/host.rs deleted file mode 100644 index de65c6a7..00000000 --- a/ddd-lsp/src/host.rs +++ /dev/null @@ -1,358 +0,0 @@ -//! Language-host lifecycle — lazy start, readiness, crash recovery. -//! -//! A [`Host`] wraps one adapter's child process: spawned on first use, -//! health-checked at every entry (a kill -9'd child is respawned on the -//! next call), never blocking a caller on solution load — while the host -//! is indexing, [`Host::readiness`] says [`Readiness::Loading`] and tools -//! surface that as an explicit loading result (PRD §11). - -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; - -use serde_json::{json, Value}; - -use crate::adapter::Adapter; -use crate::client::LspClient; -use crate::error::{LspError, Result}; -use crate::protocol::to_uri; - -/// Default per-request deadline; solution loads take longer, so tools ask -/// readiness first rather than waiting on requests. -pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(15); - -/// Where the host is in its life. -#[derive(Debug, Clone, serde::Serialize)] -#[serde(rename_all = "kebab-case", tag = "state")] -pub enum Readiness { - /// Not spawned yet (lazy start pending). - NotStarted, - /// Alive but still indexing; callers get this instead of a hang. - Loading { elapsed_ms: u128, detail: String }, - Ready { elapsed_ms: u128 }, -} - -/// One live (or lazily pending) language server. -pub struct Host { - adapter: &'static Adapter, - command: Vec, - root: PathBuf, - client: Option, - started_at: Option, - ready_at: Option, - /// Open documents: sync version plus the text last sent to the host. - open_docs: HashMap, - /// Restarts performed after crashes (observability for tests + status). - pub restarts: u32, - /// `ServerCapabilities` from the last `initialize`, kept because a - /// capability flag is evidence about the server's *claims* — the - /// capability probe compares it to what live calls actually do. - capabilities: Option, -} - -impl Host { - pub fn new(adapter: &'static Adapter, command: Vec, root: PathBuf) -> Self { - Self { - adapter, - command, - root, - client: None, - started_at: None, - ready_at: None, - open_docs: HashMap::new(), - restarts: 0, - capabilities: None, - } - } - - pub fn language(&self) -> &'static str { - self.adapter.language - } - - /// The live client, spawning or respawning as needed. This is the - /// single entry every call path goes through — crash recovery lives - /// here and nowhere else. - fn client(&mut self) -> Result<&LspClient> { - let dead = self.client.as_ref().map(|c| !c.is_alive()).unwrap_or(false); - if dead { - self.client = None; - self.open_docs.clear(); - self.ready_at = None; - self.capabilities = None; - self.restarts += 1; - } - if self.client.is_none() { - let client = LspClient::spawn(&self.command, &self.root)?; - self.capabilities = Some(initialize(&client, &self.root, self.adapter)?); - if self.adapter.needs_open_handshake { - announce_workspace(&client, &self.root)?; - } - self.started_at = Some(Instant::now()); - self.client = Some(client); - } - self.client.as_ref().ok_or_else(|| LspError::Spawn("client vanished".into())) - } - - /// Non-blocking readiness probe; spawns lazily on first ask. - pub fn readiness(&mut self) -> Result { - let signal = self.adapter.ready; - let started = { - let client = self.client()?; - signal.satisfied(&client.state) - }; - let since = self.started_at.map(|t| t.elapsed().as_millis()).unwrap_or(0); - if started { - if self.ready_at.is_none() { - self.ready_at = Some(Instant::now()); - } - Ok(Readiness::Ready { elapsed_ms: since }) - } else { - Ok(Readiness::Loading { - elapsed_ms: since, - detail: format!( - "{} is loading the workspace (waiting for {}); retry shortly", - self.adapter.language, - signal.describe() - ), - }) - } - } - - /// Block until ready or the deadline passes; used by warmup, never by - /// tool calls. - pub fn wait_ready(&mut self, deadline: Duration) -> Result { - let start = Instant::now(); - loop { - let r = self.readiness()?; - if matches!(r, Readiness::Ready { .. }) || start.elapsed() > deadline { - return Ok(r); - } - std::thread::sleep(Duration::from_millis(100)); - } - } - - /// Ensure the file is open on the host (didOpen with disk text). The - /// health probe runs first: after a crash the tracking map is stale - /// until `client()` clears it, so the check order is load-bearing. - pub fn ensure_open(&mut self, path: &Path) -> Result<()> { - self.client()?; - if self.open_docs.contains_key(path) { - return Ok(()); - } - let text = std::fs::read_to_string(path) - .map_err(|e| LspError::Io(format!("{}: {e}", path.display())))?; - let language_id = self.adapter.language_id; - let client = self.client()?; - client.notify( - "textDocument/didOpen", - json!({"textDocument": { - "uri": to_uri(path), "languageId": language_id, "version": 1, "text": text, - }}), - )?; - self.open_docs.insert(path.to_path_buf(), (1, text)); - Ok(()) - } - - /// Ensure the document is open on the host with the supplied text — - /// the diff path's entry for files that do not exist on disk (deleted - /// at head, or a committed state the working tree has moved past). - /// Already-open documents are overlaid instead. - pub fn open_with_text(&mut self, path: &Path, text: &str) -> Result<()> { - self.client()?; - if self.open_docs.contains_key(path) { - return self.overlay(path, text); - } - let language_id = self.adapter.language_id; - let client = self.client()?; - client.notify( - "textDocument/didOpen", - json!({"textDocument": { - "uri": to_uri(path), "languageId": language_id, "version": 1, "text": text, - }}), - )?; - self.open_docs.insert(path.to_path_buf(), (1, text.to_string())); - Ok(()) - } - - /// Replace the file's content on the host (overlay for classification; - /// the disk is untouched). Sent as one whole-document *range* change: - /// Roslyn negotiates incremental sync and drops documents on - /// range-less full-content changes. - pub fn overlay(&mut self, path: &Path, text: &str) -> Result<()> { - self.ensure_open(path)?; - let (version, previous) = - self.open_docs.get(path).cloned().unwrap_or((1, String::new())); - let end = end_position(&previous); - let version = version + 1; - let client = self.client()?; - client.notify( - "textDocument/didChange", - json!({ - "textDocument": {"uri": to_uri(path), "version": version}, - "contentChanges": [{ - "range": {"start": {"line": 0, "character": 0}, "end": end}, - "text": text, - }], - }), - )?; - self.open_docs.insert(path.to_path_buf(), (version, text.to_string())); - Ok(()) - } - - /// A request against this host with the standard deadline. The two - /// transient LSP errors — RequestCancelled (-32800) and - /// ContentModified (-32801), which busy hosts raise mid-index — are - /// retried briefly instead of surfacing to the tool caller. - pub fn request(&mut self, method: &str, params: Value) -> Result { - let deadline = Instant::now() + Duration::from_secs(30); - let mut last = None; - loop { - match self.client()?.request(method, params.clone(), REQUEST_TIMEOUT) { - Err(LspError::Rpc { code, message }) - if (code == -32800 - || code == -32801 - // Roslyn raises this while a freshly opened document - // is still being adopted into its project (the - // design-time build can take seconds on first open). - || message.contains("Document is null")) - && Instant::now() < deadline => - { - last = Some(LspError::Rpc { code, message }); - std::thread::sleep(Duration::from_millis(500)); - } - Err(e) if Instant::now() >= deadline => { - return Err(last.unwrap_or(e)); - } - other => return other, - } - } - } - - /// The server's advertised capabilities, spawning lazily if needed. - /// - /// Read as a claim, never as a verdict: `capability::probe` calls the - /// operation before believing the flag. - pub fn server_capabilities(&mut self) -> Result { - self.client()?; - Ok(self.capabilities.clone().unwrap_or_else(|| json!({}))) - } - - /// Latest published diagnostics for a uri, when the host pushes them. - pub fn published_diagnostics(&mut self, path: &Path) -> Result> { - let uri = to_uri(path); - let client = self.client()?; - let map = client.state.diagnostics.lock().map_err(|_| LspError::Io("state".into()))?; - Ok(map.get(&uri).cloned()) - } - - /// Notes the host has raised (restore signals etc.). - pub fn notes(&mut self) -> Vec { - self.client - .as_ref() - .and_then(|c| c.state.notes.lock().ok().map(|n| n.clone())) - .unwrap_or_default() - } - - /// The child pid, when running (health tooling plus tests). - pub fn pid(&self) -> Option { - self.client.as_ref().and_then(|c| c.pid()) - } -} - -/// The position one past the last character of `text` (for the -/// whole-document replacement range). -fn end_position(text: &str) -> Value { - let lines: Vec<&str> = text.split('\n').collect(); - let last = lines.len().saturating_sub(1); - json!({"line": last, "character": lines.last().map(|l| l.chars().count()).unwrap_or(0)}) -} - -/// The `initialize`/`initialized` handshake every LSP host expects, with -/// the adapter's extra capabilities merged over the common set. -fn initialize(client: &LspClient, root: &Path, adapter: &Adapter) -> Result { - let root_uri = to_uri(root); - let mut capabilities = json!({ - "textDocument": { - "synchronization": {"didSave": true}, - "publishDiagnostics": {}, - "documentSymbol": {"hierarchicalDocumentSymbolSupport": true}, - "diagnostic": {}, - // Declared because it is correct practice, not because it - // unblocks anything: Roslyn answers `typeHierarchy` without - // being asked (the under-advertising direction), so its - // absence was never what stopped the operation. - "definition": {}, - "typeHierarchy": {"dynamicRegistration": false}, - }, - "workspace": {"workspaceFolders": true, "configuration": true}, - "window": {"workDoneProgress": true}, - }); - merge(&mut capabilities, (adapter.extra_capabilities)()); - let result = client.request( - "initialize", - json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "workspaceFolders": [{"uri": root_uri, "name": "workspace"}], - "capabilities": capabilities, - }), - Duration::from_secs(30), - )?; - client.notify("initialized", json!({}))?; - Ok(result.get("capabilities").cloned().unwrap_or_else(|| json!({}))) -} - -/// Deep-merge `extra` into `base`: objects recurse, everything else -/// overwrites. Keeps an adapter's capability additions from clobbering a -/// sibling key of the common set. -fn merge(base: &mut Value, extra: Value) { - match (base, extra) { - (Value::Object(b), Value::Object(e)) => { - for (k, v) in e { - merge(b.entry(k).or_insert(Value::Null), v); - } - } - (b, e) => *b = e, - } -} - -/// The Roslyn-style workspace announcement: find the solution (or the -/// projects) and send the community-documented open notifications; the -/// host answers with `workspace/projectInitializationComplete` when the -/// workspace is usable. -fn announce_workspace(client: &LspClient, root: &Path) -> Result<()> { - let (solutions, projects) = build_inputs(root); - if let Some(sln) = solutions.first() { - client.notify("solution/open", json!({"solution": to_uri(sln)}))?; - } else if !projects.is_empty() { - let uris: Vec = projects.iter().map(|p| to_uri(p)).collect(); - client.notify("project/open", json!({"projects": uris}))?; - } - Ok(()) -} - -/// Solutions plus projects under the root (shallow-first, pruned walk). -fn build_inputs(root: &Path) -> (Vec, Vec) { - let mut solutions = Vec::new(); - let mut projects = Vec::new(); - let mut stack = vec![root.to_path_buf()]; - while let Some(dir) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&dir) else { continue }; - for entry in rd.flatten() { - let p = entry.path(); - let name = entry.file_name().to_string_lossy().into_owned(); - if p.is_dir() { - if !matches!(name.as_str(), ".git" | ".ddd" | "target" | "node_modules" | "bin" | "obj") { - stack.push(p); - } - } else if name.ends_with(".sln") || name.ends_with(".slnx") { - solutions.push(p); - } else if name.ends_with(".csproj") { - projects.push(p); - } - } - } - solutions.sort(); - projects.sort(); - (solutions, projects) -} diff --git a/ddd-lsp/src/jsonrpc.rs b/ddd-lsp/src/jsonrpc.rs deleted file mode 100644 index 23505680..00000000 --- a/ddd-lsp/src/jsonrpc.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Content-Length framing for LSP JSON-RPC byte streams. -//! -//! LSP messages travel as `Content-Length: N\r\n\r\n`. -//! Both the client and the mock host speak through these two functions, so -//! a framing bug cannot hide on one side of the tests. - -use std::io::{BufRead, Write}; - -use serde_json::Value; - -/// Write one framed message. -pub fn write_message(w: &mut W, v: &Value) -> std::io::Result<()> { - let body = serde_json::to_vec(v)?; - write!(w, "Content-Length: {}\r\n\r\n", body.len())?; - w.write_all(&body)?; - w.flush() -} - -/// Read one framed message; `None` on a clean EOF at a frame boundary. -pub fn read_message(r: &mut R) -> std::io::Result> { - let mut content_length: Option = None; - loop { - let mut line = String::new(); - let n = r.read_line(&mut line)?; - if n == 0 { - return Ok(None); - } - let line = line.trim_end(); - if line.is_empty() { - break; - } - if let Some(rest) = header_value(line, "Content-Length") { - content_length = rest.trim().parse::().ok(); - } - } - let len = content_length.ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::InvalidData, "missing Content-Length header") - })?; - let mut body = vec![0u8; len]; - r.read_exact(&mut body)?; - let v = serde_json::from_slice(&body) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; - Ok(Some(v)) -} - -/// Case-insensitive `Header: value` match. -fn header_value<'a>(line: &'a str, name: &str) -> Option<&'a str> { - let (key, value) = line.split_once(':')?; - if key.trim().eq_ignore_ascii_case(name) { - Some(value) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn round_trips_a_message() { - let msg = json!({"jsonrpc": "2.0", "id": 1, "method": "x", "params": {"a": "ü"}}); - let mut buf = Vec::new(); - write_message(&mut buf, &msg).expect("write"); - let mut r = std::io::BufReader::new(&buf[..]); - let back = read_message(&mut r).expect("read").expect("some"); - assert_eq!(back, msg); - assert!(read_message(&mut r).expect("eof").is_none()); - } - - #[test] - fn reads_two_consecutive_frames() { - let (a, b) = (json!({"id": 1}), json!({"id": 2})); - let mut buf = Vec::new(); - write_message(&mut buf, &a).expect("write a"); - write_message(&mut buf, &b).expect("write b"); - let mut r = std::io::BufReader::new(&buf[..]); - assert_eq!(read_message(&mut r).expect("a").expect("some"), a); - assert_eq!(read_message(&mut r).expect("b").expect("some"), b); - } - - #[test] - fn missing_length_is_an_error() { - let raw = b"X-Other: 1\r\n\r\n{}"; - let mut r = std::io::BufReader::new(&raw[..]); - assert!(read_message(&mut r).is_err()); - } -} diff --git a/ddd-lsp/src/lib.rs b/ddd-lsp/src/lib.rs deleted file mode 100644 index c7a933bb..00000000 --- a/ddd-lsp/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! LSP-backed language intelligence for the ddd governance surface. -//! -//! Implements PRD M3 (`docs/ddd-v1-spec.md` §3): the tool is an LSP -//! *client* hosting per-language servers as stdio children — lazy start, -//! health check, restart on crash. The LSP protocol is the only seam to -//! language intelligence; nothing here links Roslyn or Bicep APIs. All -//! language knowledge (host command lines, handshake quirks, contract- -//! surface policy tables §9) lives under `adapter/`; every other module is -//! language-neutral. `mock` is the fixture-grade test host CI runs against -//! (the workspace CI has no .NET SDK — `dec/ddd/fixtures-not-sdk`). - -pub mod adapter; -pub mod capability; -pub mod classify; -pub mod client; -pub mod declaration; -pub mod error; -pub mod host; -pub mod jsonrpc; -pub mod manager; -pub mod mock; -pub mod protocol; -pub mod revdiff; -/// The contract-surface vocabulary, owned by `ddd-core` because the policy -/// table mechanism is language-neutral; re-exported here so adapters keep -/// reaching it as `crate::surface`. -pub use ddd_core::surface; - -pub use error::{LspError, Result}; -pub use manager::HostManager; diff --git a/ddd-lsp/src/manager.rs b/ddd-lsp/src/manager.rs deleted file mode 100644 index 65af61db..00000000 --- a/ddd-lsp/src/manager.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Host routing — one lazily started language server per adapter. -//! -//! Resolves each adapter's host command plus behavior flags from -//! `.ddd/config.yaml` (`adapter..*`, format 3), spawns hosts on -//! first use, and hands out the [`Host`] for a language or file. This is -//! the entire language-awareness of the layers above: they route here by -//! name or extension, nothing more. - -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use ddd_core::config::DddConfig; - -use crate::adapter::{self, Adapter, AdapterFlags}; -use crate::error::{LspError, Result}; -use crate::host::{Host, Readiness}; - -pub struct HostManager { - root: PathBuf, - config: Option, - hosts: BTreeMap<&'static str, Host>, -} - -impl HostManager { - /// A manager for `root`, reading adapter overrides from its config. - pub fn new(root: PathBuf) -> Self { - let config = ddd_core::config::load(&root); - Self { root, config, hosts: BTreeMap::new() } - } - - /// Override the config (tests; callers that already loaded it). - pub fn with_config(root: PathBuf, config: Option) -> Self { - Self { root, config, hosts: BTreeMap::new() } - } - - pub fn root(&self) -> &Path { - &self.root - } - - pub fn config(&self) -> Option<&DddConfig> { - self.config.as_ref() - } - - /// The adapter for an explicit language override or a file path. - pub fn resolve_adapter( - &self, - language: Option<&str>, - file: Option<&Path>, - ) -> Result<&'static Adapter> { - if let Some(lang) = language { - return adapter::for_language(lang) - .ok_or_else(|| LspError::NoAdapter(format!("unknown language `{lang}`"))); - } - let file = file.ok_or_else(|| LspError::NoAdapter("no file, no language".into()))?; - adapter::for_path(file) - .ok_or_else(|| LspError::NoAdapter(format!("no adapter claims {}", file.display()))) - } - - /// The behavior flags for a language (config over adapter defaults). - pub fn flags(&self, language: &str) -> AdapterFlags { - let mut flags = AdapterFlags::default(); - if let Some(entry) = self.config.as_ref().and_then(|c| c.adapter_entry(language)) { - if let Some(v) = entry.internal_is_surface { - flags.internal_is_surface = v; - } - if let Some(attrs) = &entry.exported_attributes { - flags.exported_attributes = attrs.clone(); - } - } - flags - } - - /// The (possibly newly created) host for a language. - pub fn host(&mut self, language: &str) -> Result<&mut Host> { - let adapter = adapter::for_language(language) - .ok_or_else(|| LspError::NoAdapter(format!("unknown language `{language}`")))?; - if !self.hosts.contains_key(adapter.language) { - let command = self.command_for(adapter); - let host = Host::new(adapter, command, self.root.clone()); - self.hosts.insert(adapter.language, host); - } - self.hosts - .get_mut(adapter.language) - .ok_or_else(|| LspError::NoAdapter("host vanished".into())) - } - - /// The host for a language only when one is already running — never - /// starts one (the `off`-mode disk/overlay sync path). - pub fn existing_host(&mut self, language: &str) -> Option<&mut Host> { - self.hosts.get_mut(language) - } - - fn command_for(&self, adapter: &'static Adapter) -> Vec { - self.config - .as_ref() - .and_then(|c| c.adapter_entry(adapter.language)) - .and_then(|e| e.command.clone()) - .unwrap_or_else(|| adapter.default_command.iter().map(|s| s.to_string()).collect()) - } - - /// Readiness of every registered adapter's host, starting each lazily - /// (the warmup path). - pub fn warm_all(&mut self, languages: Option<&[String]>) -> Vec<(String, Result)> { - let selected: Vec<&'static str> = adapter::all() - .iter() - .filter(|a| a.hosted) - .map(|a| a.language) - .filter(|l| languages.map(|ls| ls.iter().any(|x| x == l)).unwrap_or(true)) - .collect(); - let mut out = Vec::new(); - for language in selected { - let r = self.host(language).and_then(Host::readiness); - out.push((language.to_string(), r)); - } - out - } -} diff --git a/ddd-lsp/src/mock.rs b/ddd-lsp/src/mock.rs deleted file mode 100644 index a669436d..00000000 --- a/ddd-lsp/src/mock.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! Fixture-grade mock LSP host — the hermetic stand-in CI runs against. -//! -//! Speaks real Content-Length framing over stdio, tracks open documents, -//! answers the M3 request set from naive fixture-syntax parsers -//! (`mock_parse`), and can imitate the Roslyn solution-open handshake -//! (readiness only after `solution/open`/`project/open` plus a delay). -//! This is test scaffolding for the workspace's hermetic CI -//! (`dec/ddd/fixtures-not-sdk`), not a language adapter: real-host runs -//! are the `DDD_LSP_E2E=1` gated tests. - -/// Fixture-grade type-hierarchy answers. Crate-internal: it is scaffolding -/// for the mock's own responses, not surface a caller depends on. -pub(crate) mod hierarchy; -pub mod parse; - -use std::collections::HashMap; -use std::io::{BufReader, Stdin, Stdout, Write}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use serde_json::{json, Value}; - -use crate::jsonrpc; -use crate::protocol::from_uri; - -/// How the mock behaves at startup. -pub struct MockConfig { - /// Demand `solution/open`/`project/open` before readiness, then delay. - pub roslyn_handshake: bool, - pub ready_delay: Duration, - /// Advertise `typeHierarchyProvider: true`, then answer `null` — the - /// over-advertising shape observed live on kotlin-lsp, kept as a - /// fixture because it is the direction that fabricates ground. - pub over_advertise_type_hierarchy: bool, -} - -/// Serve the mock over this process's stdio until EOF or `exit`. -pub fn serve_stdio(cfg: MockConfig) { - let stdin: Stdin = std::io::stdin(); - let out: Arc> = Arc::new(Mutex::new(std::io::stdout())); - let ready = Arc::new(AtomicBool::new(false)); - let mut docs: HashMap = HashMap::new(); - let mut reader = BufReader::new(stdin.lock()); - while let Ok(Some(msg)) = jsonrpc::read_message(&mut reader) { - let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string(); - if method == "exit" { - break; - } - if let Some(id) = msg.get("id").filter(|v| !v.is_null()).cloned() { - let result = respond(&cfg, &ready, &docs, &method, &msg); - send(&out, &json!({"jsonrpc": "2.0", "id": id, "result": result})); - } else { - notify(&cfg, &ready, &out, &mut docs, &method, &msg); - } - } -} - -fn send(out: &Arc>, msg: &Value) { - if let Ok(mut o) = out.lock() { - let _ = jsonrpc::write_message(&mut *o, msg); - let _ = o.flush(); - } -} - -/// Handle a notification: document sync, handshake, readiness. -fn notify( - cfg: &MockConfig, - ready: &Arc, - out: &Arc>, - docs: &mut HashMap, - method: &str, - msg: &Value, -) { - match method { - "initialized" if !cfg.roslyn_handshake => ready.store(true, Ordering::SeqCst), - "solution/open" | "project/open" => { - let (ready, out) = (ready.clone(), out.clone()); - let delay = cfg.ready_delay; - std::thread::spawn(move || { - std::thread::sleep(delay); - send(&out, &json!({"jsonrpc": "2.0", "method": "workspace/_test_restoreNeeded", - "params": {}})); - send(&out, &json!({"jsonrpc": "2.0", - "method": "workspace/projectInitializationComplete", - "params": {}})); - ready.store(true, Ordering::SeqCst); - }); - } - "textDocument/didOpen" => { - let uri = msg.pointer("/params/textDocument/uri").and_then(Value::as_str); - let text = msg.pointer("/params/textDocument/text").and_then(Value::as_str); - if let (Some(uri), Some(text)) = (uri, text) { - docs.insert(uri.to_string(), text.to_string()); - publish_sidecar_diagnostics(out, uri); - } - } - "textDocument/didChange" => { - let uri = msg.pointer("/params/textDocument/uri").and_then(Value::as_str); - let text = msg.pointer("/params/contentChanges/0/text").and_then(Value::as_str); - if let (Some(uri), Some(text)) = (uri, text) { - docs.insert(uri.to_string(), text.to_string()); - } - } - _ => {} - } -} - -/// Push-model diagnostics: a `.diag.json` sidecar publishes on open. -fn publish_sidecar_diagnostics(out: &Arc>, uri: &str) { - if let Some(items) = sidecar_items(uri) { - send(out, &json!({"jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", - "params": {"uri": uri, "diagnostics": items}})); - } -} - -fn sidecar_items(uri: &str) -> Option { - let path = from_uri(uri); - let sidecar = path.with_file_name(format!( - "{}.diag.json", - path.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default() - )); - let text = std::fs::read_to_string(sidecar).ok()?; - serde_json::from_str(&text).ok() -} - -/// Answer a request; requests never hang, mirroring a live host. -fn respond( - cfg: &MockConfig, - ready: &Arc, - docs: &HashMap, - method: &str, - msg: &Value, -) -> Value { - let uri = msg - .pointer("/params/textDocument/uri") - .and_then(Value::as_str) - .unwrap_or_default() - .to_string(); - let text = docs.get(&uri).cloned().unwrap_or_default(); - match method { - "initialize" => json!({"capabilities": capabilities()}), - "shutdown" => Value::Null, - "textDocument/documentSymbol" => { - if cfg.roslyn_handshake && !ready.load(Ordering::SeqCst) { - json!([]) - } else { - parse::document_symbols(&uri, &text) - } - } - "workspace/symbol" => workspace_symbols(docs, msg), - "textDocument/definition" => definition(docs, &uri, &text, msg), - "textDocument/prepareTypeHierarchy" => { - if cfg.over_advertise_type_hierarchy { - Value::Null - } else { - hierarchy::prepare(docs, &uri, &text, msg) - } - } - "typeHierarchy/supertypes" => hierarchy::supertypes(docs, msg), - "typeHierarchy/subtypes" => hierarchy::subtypes(docs, msg), - "textDocument/references" => references(docs, &uri, &text, msg), - "textDocument/hover" => { - let word = word_at(msg, &text); - json!({"contents": {"kind": "markdown", "value": format!("mock: `{word}`")}}) - } - "textDocument/signatureHelp" => { - let line = line_at(msg, &text); - json!({"signatures": [{"label": line.trim()}], - "activeSignature": 0, "activeParameter": 0}) - } - "textDocument/diagnostic" => { - json!({"kind": "full", "items": sidecar_items(&uri).unwrap_or(json!([]))}) - } - "textDocument/rename" => rename(&uri, &text, msg), - _ => Value::Null, - } -} - -fn position_of(msg: &Value) -> (usize, usize) { - let line = msg.pointer("/params/position/line").and_then(Value::as_u64).unwrap_or(0); - let ch = msg.pointer("/params/position/character").and_then(Value::as_u64).unwrap_or(0); - (line as usize, ch as usize) -} - -fn line_at(msg: &Value, text: &str) -> String { - let (line, _) = position_of(msg); - text.lines().nth(line).unwrap_or("").to_string() -} - -/// The identifier under the request position. -fn word_at(msg: &Value, text: &str) -> String { - let (line_no, ch) = position_of(msg); - let line = text.lines().nth(line_no).unwrap_or(""); - let is_word = |c: char| c.is_alphanumeric() || c == '_'; - let bytes: Vec = line.chars().collect(); - if bytes.is_empty() { - return String::new(); - } - let mut start = ch.min(bytes.len().saturating_sub(1)); - while start > 0 && is_word(bytes[start - 1]) { - start -= 1; - } - let mut end = start; - while end < bytes.len() && is_word(bytes[end]) { - end += 1; - } - bytes[start..end].iter().collect() -} - -/// Whole-word occurrences of the target across every open document. -fn references(docs: &HashMap, uri: &str, text: &str, msg: &Value) -> Value { - let word = word_at(msg, text); - if word.is_empty() { - return json!([]); - } - let mut locations = Vec::new(); - let mut scan = |doc_uri: &str, doc: &str| { - for (ln, line) in doc.lines().enumerate() { - for (col, _) in whole_word_hits(line, &word) { - locations.push(json!({"uri": doc_uri, "range": { - "start": {"line": ln, "character": col}, - "end": {"line": ln, "character": col + word.len()}, - }})); - } - } - }; - scan(uri, text); - for (u, doc) in docs { - if u != uri { - scan(u, doc); - } - } - json!(locations) -} - -fn whole_word_hits(line: &str, word: &str) -> Vec<(usize, usize)> { - let is_word = |c: char| c.is_alphanumeric() || c == '_'; - let mut hits = Vec::new(); - let mut from = 0; - while let Some(idx) = line[from..].find(word) { - let at = from + idx; - let before_ok = at == 0 || !line[..at].chars().next_back().map(is_word).unwrap_or(false); - let after = &line[at + word.len()..]; - let after_ok = !after.chars().next().map(is_word).unwrap_or(false); - if before_ok && after_ok { - hits.push((at, at + word.len())); - } - from = at + word.len(); - } - hits -} - -/// What this mock says it can do. `typeHierarchyProvider` is advertised -/// unconditionally — in the over-advertising mode that is the whole point: -/// the claim stands, the answer does not come. -fn capabilities() -> Value { - json!({ - "documentSymbolProvider": true, - "workspaceSymbolProvider": true, - "definitionProvider": true, - "referencesProvider": true, - "hoverProvider": true, - "typeHierarchyProvider": true, - }) -} - -/// `workspace/symbol`: whole-word declaration matches across open -/// documents, as flat `SymbolInformation`. -fn workspace_symbols(docs: &HashMap, msg: &Value) -> Value { - let query = msg.pointer("/params/query").and_then(Value::as_str).unwrap_or_default(); - if query.is_empty() { - return json!([]); - } - let mut out = Vec::new(); - for (uri, text) in docs { - for symbol in parse::document_symbols(uri, text).as_array().into_iter().flatten() { - collect_matching(symbol, query, uri, &mut out); - } - } - json!(out) -} - -fn collect_matching(symbol: &Value, query: &str, uri: &str, out: &mut Vec) { - let name = symbol.get("name").and_then(Value::as_str).unwrap_or_default(); - if name.contains(query) { - out.push(json!({ - "name": name, - "kind": symbol.get("kind").cloned().unwrap_or(json!(0)), - "location": {"uri": uri, "range": symbol.get("range").cloned().unwrap_or(json!({}))}, - "containerName": "mock", - })); - } - for child in symbol.get("children").and_then(Value::as_array).into_iter().flatten() { - collect_matching(child, query, uri, out); - } -} - -/// `textDocument/definition`: the first declaration of the word under the -/// position, across open documents. -fn definition(docs: &HashMap, uri: &str, text: &str, msg: &Value) -> Value { - let word = word_at(msg, text); - if word.is_empty() { - return Value::Null; - } - let mut search: Vec<(&String, &String)> = docs.iter().collect(); - search.sort_by_key(|(u, _)| (*u != uri, (*u).clone())); - for (doc_uri, doc) in search { - for (ln, line) in doc.lines().enumerate() { - let declares = line.contains(&format!("class {word}")) - || line.contains(&format!("interface {word}")) - || line.contains(&format!("record {word}")) - || line.contains(&format!("struct {word}")) - || line.contains(&format!("enum {word}")); - if !declares { - continue; - } - let col = line.find(&word).unwrap_or(0); - return json!([{"uri": doc_uri, "range": { - "start": {"line": ln, "character": col}, - "end": {"line": ln, "character": col + word.len()}, - }}]); - } - } - Value::Null -} - -/// A WorkspaceEdit renaming every whole-word occurrence in the document. -fn rename(uri: &str, text: &str, msg: &Value) -> Value { - let word = word_at(msg, text); - let new_name = msg.pointer("/params/newName").and_then(Value::as_str).unwrap_or("renamed"); - let mut edits = Vec::new(); - for (ln, line) in text.lines().enumerate() { - for (col, end) in whole_word_hits(line, &word) { - edits.push(json!({"range": { - "start": {"line": ln, "character": col}, - "end": {"line": ln, "character": end}, - }, "newText": new_name})); - } - } - json!({"changes": {uri: edits}}) -} diff --git a/ddd-lsp/src/mock/hierarchy.rs b/ddd-lsp/src/mock/hierarchy.rs deleted file mode 100644 index 72459c7f..00000000 --- a/ddd-lsp/src/mock/hierarchy.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Fixture-grade type-hierarchy answers for the mock host. -//! -//! Reads the base list off a fixture declaration — `class Foo : Bar, IBaz`, -//! `record X(…) : ICommand` — across every open document, so the wiring of -//! `prepareTypeHierarchy` / `supertypes` / `subtypes` is exercised in the -//! workspace's hermetic CI, which has no .NET SDK -//! (`dec/ddd/fixtures-not-sdk`). -//! -//! The items carry an opaque `data` blob on purpose. Roslyn resolves the -//! directional calls only when its own blob returns unchanged, so the mock -//! **refuses an item whose `data` was rebuilt rather than carried** — the -//! failure a live host would show as an empty result, made loud here. - -use std::collections::HashMap; - -use serde_json::{json, Value}; - -/// One fixture declaration: where it is, what it derives from. -struct Decl { - name: String, - kind: u64, - uri: String, - line: usize, - character: usize, - bases: Vec, -} - -/// The `TypeHierarchyItem` for a declaration, `data` included. -fn item(decl: &Decl) -> Value { - json!({ - "name": decl.name, - "kind": decl.kind, - "uri": decl.uri, - "range": {"start": {"line": decl.line, "character": 0}, - "end": {"line": decl.line, "character": 200}}, - "selectionRange": {"start": {"line": decl.line, "character": decl.character}, - "end": {"line": decl.line, "character": decl.character + decl.name.len()}}, - // Stands in for Roslyn's {ProjectGuid, SymbolKeyData, TextDocument}: - // opaque to the client, load-bearing on the way back. Keyed by - // position as well as name, so two declarations sharing a name are - // two symbols here as they are on a real host. - "data": {"MockSymbolKey": format!("{}#{}#{}", decl.uri, decl.line, decl.name)}, - }) -} - -/// `textDocument/prepareTypeHierarchy` — the declaration at the position. -pub(crate) fn prepare(docs: &HashMap, uri: &str, text: &str, msg: &Value) -> Value { - let line = msg.pointer("/params/position/line").and_then(Value::as_u64).unwrap_or(0) as usize; - let mut here = declarations(uri, text); - here.extend(others(docs, uri)); - match here.iter().find(|d| d.line == line && d.uri == uri) { - Some(decl) => json!([item(decl)]), - None => Value::Null, - } -} - -/// `typeHierarchy/supertypes` — the bases the item's declaration names. -pub(crate) fn supertypes(docs: &HashMap, msg: &Value) -> Value { - directional(docs, msg) - .map(|(all, found)| { - let bases = &found.bases; - let items: Vec = - all.iter().filter(|d| bases.contains(&d.name)).map(item).collect(); - json!(items) - }) - .unwrap_or(Value::Null) -} - -/// `typeHierarchy/subtypes` — every declaration naming the item as a base. -pub(crate) fn subtypes(docs: &HashMap, msg: &Value) -> Value { - directional(docs, msg) - .map(|(all, found)| { - let items: Vec = - all.iter().filter(|d| d.bases.contains(&found.name)).map(item).collect(); - json!(items) - }) - .unwrap_or(Value::Null) -} - -/// Resolve the inbound item back to a declaration — **through its `data` -/// blob**, so an item that was rebuilt instead of carried resolves to -/// nothing, exactly as a live host would answer it. -fn directional( - docs: &HashMap, - msg: &Value, -) -> Option<(Vec, Decl)> { - let key = msg.pointer("/params/item/data/MockSymbolKey").and_then(Value::as_str)?; - let (head, name) = key.rsplit_once('#')?; - let (uri, line) = head.rsplit_once('#')?; - let line: usize = line.parse().ok()?; - let all: Vec = docs.iter().flat_map(|(u, t)| declarations(u, t)).collect(); - let found = all - .iter() - .find(|d| d.uri == uri && d.line == line && d.name == name) - .map(|d| Decl { - name: d.name.clone(), - kind: d.kind, - uri: d.uri.clone(), - line: d.line, - character: d.character, - bases: d.bases.clone(), - })?; - Some((all, found)) -} - -fn others(docs: &HashMap, skip: &str) -> Vec { - docs.iter().filter(|(u, _)| *u != skip).flat_map(|(u, t)| declarations(u, t)).collect() -} - -const TYPE_KEYWORDS: &[(&str, u64)] = - &[("class", 5), ("interface", 11), ("struct", 23), ("enum", 10), ("record", 5)]; - -/// Every fixture type declaration in one document, with its base list. -fn declarations(uri: &str, text: &str) -> Vec { - let mut out = Vec::new(); - for (i, line) in text.lines().enumerate() { - let trimmed = line.trim(); - if trimmed.starts_with("//") || trimmed.is_empty() { - continue; - } - let Some((kind, name)) = type_head(trimmed) else { continue }; - out.push(Decl { - character: line.find(&name).unwrap_or(0), - bases: base_list(trimmed, &name), - name, - kind, - uri: uri.to_string(), - line: i, - }); - } - out -} - -/// `[modifiers] class|interface|struct|enum|record Name …` -/// -/// The **last** keyword of a run wins, so `record struct LocationId` is a -/// struct named `LocationId` rather than a record named `struct` — the -/// value-object identity idiom this corpus is full of. -fn type_head(trimmed: &str) -> Option<(u64, String)> { - let tokens: Vec<&str> = trimmed.split_whitespace().collect(); - let keyword = |t: &&str| TYPE_KEYWORDS.iter().find(|(k, _)| k == t).map(|(_, kind)| *kind); - let last = tokens.iter().enumerate().rfind(|(_, t)| keyword(t).is_some())?; - let (i, kind) = (last.0, keyword(last.1)?); - let raw = tokens.get(i + 1)?; - let name: String = raw.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); - (!name.is_empty()).then_some((kind, name)) -} - -/// The names after the `:` of a declaration head, positional record -/// parameter list skipped. -fn base_list(trimmed: &str, name: &str) -> Vec { - let after_name = match trimmed.split_once(name) { - Some((_, rest)) => rest, - None => return Vec::new(), - }; - let rest = match after_name.split_once(')') { - Some((head, tail)) if head.contains('(') => tail, - _ => after_name, - }; - let Some((_, bases)) = rest.split_once(':') else { return Vec::new() }; - bases - .split('{') - .next() - .unwrap_or("") - .split(',') - .map(|b| b.trim().chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect()) - .filter(|b: &String| !b.is_empty()) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reads_a_base_list_off_a_declaration_head() { - assert_eq!(base_list("public sealed class Foo : Bar, IBaz {", "Foo"), ["Bar", "IBaz"]); - assert_eq!(base_list("public record Upsert(string A, int B) : ICommand;", "Upsert"), ["ICommand"]); - assert_eq!(base_list("public readonly record struct LocationId(string Value);", "LocationId"), Vec::::new()); - assert_eq!(base_list("internal class Handler : IHandler", "Handler"), ["IHandler"]); - } - - #[test] - fn a_record_struct_is_named_by_its_last_keyword() { - assert_eq!( - type_head("public readonly record struct LocationId(string Value);"), - Some((23, "LocationId".to_string())) - ); - assert_eq!(type_head("public record Upsert(int A) : ICommand;"), Some((5, "Upsert".to_string()))); - } - - #[test] - fn an_item_whose_data_was_rebuilt_resolves_to_nothing() { - let mut docs = HashMap::new(); - docs.insert("file:///a.cs".to_string(), "public class Foo : Bar {}\npublic class Bar {}\n".to_string()); - let carried = json!({"params": {"item": {"name": "Foo", "data": {"MockSymbolKey": "file:///a.cs#0#Foo"}}}}); - assert_eq!(supertypes(&docs, &carried).as_array().map(Vec::len), Some(1)); - let rebuilt = json!({"params": {"item": {"name": "Foo", "uri": "file:///a.cs"}}}); - assert_eq!(supertypes(&docs, &rebuilt), Value::Null); - } -} diff --git a/ddd-lsp/src/mock/parse.rs b/ddd-lsp/src/mock/parse.rs deleted file mode 100644 index db80b0f6..00000000 --- a/ddd-lsp/src/mock/parse.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Naive fixture-syntax symbol extraction for the mock host. -//! -//! Parses the *fixture dialects* only: one declaration per line, C# types -//! with braced bodies, Bicep declarations headed by their keyword. Real -//! hosts do this with compilers; the mock only needs symbol names, kinds, -//! ranges — the adapters slice the source text themselves. - -use serde_json::{json, Value}; - -/// DocumentSymbol tree for a uri, dispatched on the file extension. -pub fn document_symbols(uri: &str, text: &str) -> Value { - if uri.ends_with(".bicep") || uri.ends_with(".bicepparam") { - bicep_symbols(text) - } else { - csharp_symbols(text) - } -} - -fn symbol(name: &str, kind: u64, start: usize, end: usize, sel: (usize, usize)) -> Value { - json!({ - "name": name, "kind": kind, - "range": {"start": {"line": start, "character": 0}, - "end": {"line": end, "character": 200}}, - "selectionRange": {"start": {"line": sel.0, "character": sel.1}, - "end": {"line": sel.0, "character": sel.1 + name.len()}}, - "children": [], - }) -} - -fn brace_delta(line: &str) -> i32 { - line.chars().map(|c| match c { - '{' => 1, - '}' => -1, - _ => 0, - }).sum() -} - -const TYPE_KEYWORDS: &[(&str, u64)] = - &[("class", 5), ("interface", 11), ("struct", 23), ("enum", 10), ("record", 5)]; - -struct TypeFrame { - node: Value, - name: String, - entry_depth: i32, - opened: bool, - children: Vec, -} - -/// Fixture-grade C#: types at any depth, one-line member declarations -/// directly inside a type body. -fn csharp_symbols(text: &str) -> Value { - let mut depth = 0i32; - let mut stack: Vec = Vec::new(); - let mut top: Vec = Vec::new(); - for (i, line) in text.lines().enumerate() { - let trimmed = line.trim(); - if !trimmed.starts_with("//") && !trimmed.is_empty() && !trimmed.starts_with('[') { - if let Some((kind, name)) = type_decl(trimmed) { - let sel = (i, line.find(&name).unwrap_or(0)); - stack.push(TypeFrame { - node: symbol(&name, kind, i, i, sel), - name, - entry_depth: depth, - opened: false, - children: Vec::new(), - }); - } else if let Some(frame) = stack.last_mut() { - if depth == frame.entry_depth + 1 { - let kind = frame.node["kind"].as_u64().unwrap_or(0); - if let Some(child) = member_decl(trimmed, &frame.name, kind, i, line) { - frame.children.push(child); - } - } - } - } - depth += brace_delta(line); - for frame in stack.iter_mut() { - if depth > frame.entry_depth { - frame.opened = true; - } - } - while stack.last().map(|f| f.opened && depth <= f.entry_depth).unwrap_or(false) { - let Some(mut frame) = stack.pop() else { break }; - frame.node["range"]["end"]["line"] = json!(i); - frame.node["children"] = Value::Array(std::mem::take(&mut frame.children)); - match stack.last_mut() { - Some(parent) => parent.children.push(frame.node), - None => top.push(frame.node), - } - } - } - while let Some(mut frame) = stack.pop() { - frame.node["children"] = Value::Array(std::mem::take(&mut frame.children)); - top.push(frame.node); - } - Value::Array(top) -} - -/// `[modifiers] class|interface|struct|enum|record Name ...` -fn type_decl(trimmed: &str) -> Option<(u64, String)> { - let tokens: Vec<&str> = trimmed.split_whitespace().collect(); - for (i, t) in tokens.iter().enumerate() { - if let Some((_, kind)) = TYPE_KEYWORDS.iter().find(|(k, _)| k == t) { - let name = tokens.get(i + 1)?; - let name: String = - name.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); - if name.is_empty() { - return None; - } - return Some((*kind, name)); - } - } - None -} - -const CONTROL: &[&str] = &["if", "for", "foreach", "while", "switch", "return", "using", "throw"]; - -/// One-line member declarations: methods/ctors (have parens), properties -/// (braced accessors), fields (semicolon-terminated), enum members (bare -/// identifiers inside an enum body). -fn member_decl(trimmed: &str, type_name: &str, type_kind: u64, i: usize, line: &str) -> Option { - let first = trimmed.split_whitespace().next().unwrap_or(""); - if CONTROL.contains(&first) || trimmed.starts_with('}') { - return None; - } - if type_kind == 10 { - let name: String = trimmed.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); - if name.is_empty() { - return None; - } - return Some(symbol(&name, 22, i, i, (i, line.find(&name).unwrap_or(0)))); - } - if let Some(paren) = trimmed.find('(') { - let head = &trimmed[..paren]; - let name = head.split_whitespace().next_back()?.to_string(); - if !name.chars().all(|c| c.is_alphanumeric() || c == '_') || name.is_empty() { - return None; - } - let kind = if name == type_name { 9 } else { 6 }; - return Some(symbol(&name, kind, i, i, (i, line.find(&name).unwrap_or(0)))); - } - if trimmed.contains('{') { - let head = trimmed.split('{').next().unwrap_or(""); - let name = head.split_whitespace().next_back()?.to_string(); - if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() { - return Some(symbol(&name, 7, i, i, (i, line.find(&name).unwrap_or(0)))); - } - return None; - } - if trimmed.ends_with(';') { - let head = trimmed.trim_end_matches(';').split('=').next().unwrap_or(""); - let name = head.split_whitespace().next_back()?.to_string(); - if name.chars().all(|c| c.is_alphanumeric() || c == '_') - && head.split_whitespace().count() >= 2 - { - return Some(symbol(&name, 8, i, i, (i, line.find(&name).unwrap_or(0)))); - } - } - None -} - -const BICEP_KEYWORDS: &[(&str, u64)] = - &[("param", 13), ("output", 13), ("var", 13), ("resource", 23), ("module", 2), ("type", 26)]; - -/// Fixture-grade Bicep: keyword-headed declarations; braced blocks span -/// to their closing brace. -fn bicep_symbols(text: &str) -> Value { - let lines: Vec<&str> = text.lines().collect(); - let mut out = Vec::new(); - let mut i = 0usize; - while i < lines.len() { - let trimmed = lines[i].trim(); - let mut tokens = trimmed.split_whitespace(); - let keyword = tokens.next().unwrap_or(""); - if let Some((_, kind)) = BICEP_KEYWORDS.iter().find(|(k, _)| *k == keyword) { - if let Some(name) = tokens.next() { - let name: String = - name.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); - let end = block_end(&lines, i); - let sel = (i, lines[i].find(&name).unwrap_or(0)); - out.push(symbol(&name, *kind, i, end, sel)); - i = end + 1; - continue; - } - } - i += 1; - } - Value::Array(out) -} - -/// The line where a declaration's braces close (its own line if unbraced). -fn block_end(lines: &[&str], start: usize) -> usize { - let mut depth = 0i32; - let mut any = false; - for (offset, line) in lines.iter().skip(start).enumerate() { - depth += brace_delta(line); - if brace_delta(line) != 0 { - any = true; - } - if any && depth <= 0 { - return start + offset; - } - if offset == 0 && !line.contains('{') { - return start; - } - } - lines.len().saturating_sub(1) -} diff --git a/ddd-lsp/src/protocol.rs b/ddd-lsp/src/protocol.rs deleted file mode 100644 index 3fe0ea17..00000000 --- a/ddd-lsp/src/protocol.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! LSP payload shaping — uris, positions, normalized result forms. -//! -//! Everything here is generic LSP: no language names, no host quirks. -//! Symbols arrive either as a `DocumentSymbol` tree or a flat -//! `SymbolInformation` list; both flatten to [`RawSymbol`] rows carrying -//! the container path, so adapters see one shape. - -use std::path::{Path, PathBuf}; - -use serde::Serialize; -use serde_json::{json, Value}; - -/// A file path as a `file://` uri (minimal percent-encoding). -pub fn to_uri(path: &Path) -> String { - let mut out = String::from("file://"); - for b in path.to_string_lossy().bytes() { - match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'.' | b'-' | b'_' | b'~' => { - out.push(b as char) - } - _ => out.push_str(&format!("%{b:02X}")), - } - } - out -} - -/// The path behind a `file://` uri (percent-decoding the escaped bytes). -pub fn from_uri(uri: &str) -> PathBuf { - let raw = uri.strip_prefix("file://").unwrap_or(uri); - let mut bytes = Vec::with_capacity(raw.len()); - let mut it = raw.bytes(); - while let Some(b) = it.next() { - if b == b'%' { - let hi = it.next().unwrap_or(b'0'); - let lo = it.next().unwrap_or(b'0'); - let hex = [hi, lo]; - let s = std::str::from_utf8(&hex).unwrap_or("00"); - bytes.push(u8::from_str_radix(s, 16).unwrap_or(b'?')); - } else { - bytes.push(b); - } - } - PathBuf::from(String::from_utf8_lossy(&bytes).into_owned()) -} - -/// `{textDocument, position}` params for position-taking requests. -pub fn position_params(file: &Path, line: u64, character: u64) -> Value { - json!({ - "textDocument": {"uri": to_uri(file)}, - "position": {"line": line, "character": character}, - }) -} - -/// The LSP `SymbolKind` table (1-26) as readable names. -pub fn kind_name(kind: u64) -> &'static str { - const NAMES: [&str; 26] = [ - "file", "module", "namespace", "package", "class", "method", "property", "field", - "constructor", "enum", "interface", "function", "variable", "constant", "string", - "number", "boolean", "array", "object", "key", "null", "enum-member", "struct", - "event", "operator", "type-parameter", - ]; - NAMES.get((kind as usize).wrapping_sub(1)).copied().unwrap_or("unknown") -} - -/// One symbol row: name, LSP kind, ranges, container path. -#[derive(Debug, Clone, Serialize)] -pub struct RawSymbol { - pub name: String, - pub kind: u64, - /// `Outer/Inner` path of enclosing symbol names; empty at top level. - pub container: String, - /// LSP kind (number) of the direct container; 0 at top level. - pub container_kind: u64, - pub start_line: u64, - pub end_line: u64, - pub sel_line: u64, - pub sel_char: u64, - pub detail: Option, -} - -/// Flatten a `textDocument/documentSymbol` result of either shape. -pub fn flatten_symbols(result: &Value) -> Vec { - let mut out = Vec::new(); - if let Some(items) = result.as_array() { - for item in items { - if item.get("range").is_some() { - flatten_document_symbol(item, "", 0, &mut out); - } else if let Some(sym) = symbol_information_row(item) { - out.push(sym); - } - } - } - out -} - -fn flatten_document_symbol(v: &Value, container: &str, container_kind: u64, out: &mut Vec) { - let name = v.get("name").and_then(Value::as_str).unwrap_or("?").to_string(); - let kind = v.get("kind").and_then(Value::as_u64).unwrap_or(0); - let sym = RawSymbol { - name: name.clone(), - kind, - container: container.to_string(), - container_kind, - start_line: v.pointer("/range/start/line").and_then(Value::as_u64).unwrap_or(0), - end_line: v.pointer("/range/end/line").and_then(Value::as_u64).unwrap_or(0), - sel_line: v.pointer("/selectionRange/start/line").and_then(Value::as_u64).unwrap_or(0), - sel_char: v.pointer("/selectionRange/start/character").and_then(Value::as_u64).unwrap_or(0), - detail: v.get("detail").and_then(Value::as_str).map(str::to_string), - }; - out.push(sym); - let child_container = - if container.is_empty() { name } else { format!("{container}/{name}") }; - if let Some(children) = v.get("children").and_then(Value::as_array) { - for child in children { - flatten_document_symbol(child, &child_container, kind, out); - } - } -} - -/// A flat `SymbolInformation` entry (no selectionRange, no children). -fn symbol_information_row(v: &Value) -> Option { - let name = v.get("name").and_then(Value::as_str)?.to_string(); - let start = v.pointer("/location/range/start/line").and_then(Value::as_u64).unwrap_or(0); - Some(RawSymbol { - name, - kind: v.get("kind").and_then(Value::as_u64).unwrap_or(0), - container: v.get("containerName").and_then(Value::as_str).unwrap_or("").to_string(), - container_kind: 0, - start_line: start, - end_line: v.pointer("/location/range/end/line").and_then(Value::as_u64).unwrap_or(start), - sel_line: start, - sel_char: v.pointer("/location/range/start/character").and_then(Value::as_u64).unwrap_or(0), - detail: None, - }) -} - -/// One normalized location row. -#[derive(Debug, Clone, Serialize)] -pub struct FileRange { - pub file: PathBuf, - pub line: u64, - pub character: u64, - pub end_line: u64, - pub end_character: u64, -} - -/// Normalize `Location`, `Location[]`, or `LocationLink[]` results. -pub fn normalize_locations(result: &Value) -> Vec { - let mut out = Vec::new(); - let items: Vec<&Value> = match result { - Value::Array(a) => a.iter().collect(), - Value::Object(_) => vec![result], - _ => Vec::new(), - }; - for item in items { - let (uri_key, range_key) = if item.get("targetUri").is_some() { - ("targetUri", "targetRange") - } else { - ("uri", "range") - }; - let Some(uri) = item.get(uri_key).and_then(Value::as_str) else { continue }; - let range = |leaf: &str| { - item.pointer(&format!("/{range_key}/{leaf}")).and_then(Value::as_u64).unwrap_or(0) - }; - out.push(FileRange { - file: from_uri(uri), - line: range("start/line"), - character: range("start/character"), - end_line: range("end/line"), - end_character: range("end/character"), - }); - } - out -} - -/// One normalized `TypeHierarchyItem`. -/// -/// Type-hierarchy items are not `Location`s: they carry the symbol's own -/// identity — name, kind, detail — beside the range, which is the half the -/// `rdfs:subClassOf` mapping row needs. Normalizing them through -/// [`normalize_locations`] would keep the uri and silently drop that. -/// -/// `raw` is the item exactly as the server sent it, and it is what -/// [`hierarchy_params`] hands back on the supertypes/subtypes call. Roslyn -/// answers those only when its opaque `data` blob (`ProjectGuid`, -/// `SymbolKeyData`, `TextDocument`) returns unchanged, so the round-trip is -/// verbatim by construction rather than by copying the fields we happen to -/// know about today. -#[derive(Debug, Clone, Serialize)] -pub struct HierarchyItem { - pub name: String, - pub kind: u64, - pub detail: Option, - pub file: PathBuf, - pub line: u64, - pub character: u64, - /// The server's opaque `data`, present only when it sent one. - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, - /// The unmodified item; carried back on the next call, never rebuilt. - #[serde(skip)] - pub raw: Value, -} - -/// Normalize a `TypeHierarchyItem[]` result (prepare, supertypes, subtypes). -/// -/// A server with nothing to say answers `null`, which is an empty result -/// here — the caller decides whether that means "no hierarchy" or "the -/// instrument did not answer" (see `capability`). -pub fn normalize_hierarchy_items(result: &Value) -> Vec { - let Some(items) = result.as_array() else { return Vec::new() }; - items.iter().filter_map(hierarchy_item).collect() -} - -fn hierarchy_item(v: &Value) -> Option { - let name = v.get("name").and_then(Value::as_str)?.to_string(); - let uri = v.get("uri").and_then(Value::as_str).unwrap_or_default(); - let at = |leaf: &str| v.pointer(&format!("/selectionRange/{leaf}")).and_then(Value::as_u64); - Some(HierarchyItem { - name, - kind: v.get("kind").and_then(Value::as_u64).unwrap_or(0), - detail: v.get("detail").and_then(Value::as_str).map(str::to_string), - file: from_uri(uri), - line: at("start/line") - .or_else(|| v.pointer("/range/start/line").and_then(Value::as_u64)) - .unwrap_or(0), - character: at("start/character") - .or_else(|| v.pointer("/range/start/character").and_then(Value::as_u64)) - .unwrap_or(0), - data: v.get("data").cloned(), - raw: v.clone(), - }) -} - -/// `{item}` params for `typeHierarchy/supertypes` / `subtypes`, carrying the -/// server's own item back untouched. -pub fn hierarchy_params(item: &HierarchyItem) -> Value { - json!({"item": item.raw}) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn uri_round_trips_a_spaced_path() { - let p = PathBuf::from("/tmp/a dir/File.cs"); - let uri = to_uri(&p); - assert_eq!(uri, "file:///tmp/a%20dir/File.cs"); - assert_eq!(from_uri(&uri), p); - } - - #[test] - fn flattens_a_document_symbol_tree_with_container_paths() { - let v = serde_json::json!([{ - "name": "Outer", "kind": 5, - "range": {"start": {"line": 0, "character": 0}, "end": {"line": 9, "character": 1}}, - "selectionRange": {"start": {"line": 0, "character": 13}, "end": {"line": 0, "character": 18}}, - "children": [{ - "name": "Ping", "kind": 6, - "range": {"start": {"line": 2, "character": 4}, "end": {"line": 4, "character": 5}}, - "selectionRange": {"start": {"line": 2, "character": 16}, "end": {"line": 2, "character": 20}} - }] - }]); - let flat = flatten_symbols(&v); - assert_eq!(flat.len(), 2); - assert_eq!(flat[1].container, "Outer"); - assert_eq!(flat[1].container_kind, 5); - assert_eq!(kind_name(flat[1].kind), "method"); - } - - #[test] - fn normalizes_location_and_link_shapes() { - let v = serde_json::json!([ - {"uri": "file:///a.cs", "range": {"start": {"line": 1, "character": 2}, "end": {"line": 1, "character": 5}}}, - {"targetUri": "file:///b.cs", "targetRange": {"start": {"line": 3, "character": 0}, "end": {"line": 3, "character": 4}}} - ]); - let locs = normalize_locations(&v); - assert_eq!(locs.len(), 2); - assert_eq!(locs[0].file, PathBuf::from("/a.cs")); - assert_eq!(locs[1].line, 3); - } - - #[test] - fn normalizes_a_type_hierarchy_item_keeping_its_identity() { - let v = serde_json::json!([{ - "name": "ICommand", "kind": 11, "detail": "Acme.Messaging", - "uri": "file:///src/ICommand.cs", - "range": {"start": {"line": 4, "character": 0}, "end": {"line": 6, "character": 1}}, - "selectionRange": {"start": {"line": 4, "character": 17}, "end": {"line": 4, "character": 25}}, - "data": {"ProjectGuid": "9f1c…", "SymbolKeyData": "opaque", "TextDocument": {"uri": "file:///src/ICommand.cs"}} - }]); - let items = normalize_hierarchy_items(&v); - assert_eq!(items.len(), 1); - assert_eq!(items[0].name, "ICommand"); - assert_eq!(kind_name(items[0].kind), "interface"); - assert_eq!(items[0].file, PathBuf::from("/src/ICommand.cs")); - assert_eq!((items[0].line, items[0].character), (4, 17)); - assert_eq!(items[0].detail.as_deref(), Some("Acme.Messaging")); - } - - /// The one wiring detail the entry probe named as most likely to be got - /// wrong: supertypes/subtypes resolve only when the prepare item comes - /// back byte-identical, opaque `data` included. - #[test] - fn the_opaque_data_blob_round_trips_verbatim() { - let item = serde_json::json!({ - "name": "LocationId", "kind": 23, "uri": "file:///src/LocationId.cs", - "range": {"start": {"line": 2, "character": 0}, "end": {"line": 2, "character": 60}}, - "selectionRange": {"start": {"line": 2, "character": 29}, "end": {"line": 2, "character": 39}}, - "data": {"ProjectGuid": "3f2a", "SymbolKeyData": "\\u0001Acme|Location", "TextDocument": {"uri": "file:///src/LocationId.cs"}}, - "tags": [1], - "anUnknownFieldAFutureServerAdds": {"nested": [1, 2, 3]} - }); - let items = normalize_hierarchy_items(&serde_json::json!([item.clone()])); - let params = hierarchy_params(&items[0]); - assert_eq!(params["item"], item, "the item must go back exactly as it arrived"); - assert_eq!(items[0].data, item.get("data").cloned()); - } - - #[test] - fn a_silent_hierarchy_result_normalizes_to_nothing() { - assert!(normalize_hierarchy_items(&Value::Null).is_empty()); - assert!(normalize_hierarchy_items(&serde_json::json!([])).is_empty()); - } -} diff --git a/ddd-lsp/src/revdiff.rs b/ddd-lsp/src/revdiff.rs deleted file mode 100644 index a8bef775..00000000 --- a/ddd-lsp/src/revdiff.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Diff-over-two-revisions classification — the shared classifier's -//! repository path (M8, spec invariant 4). -//! -//! Takes both sides of every changed file from git, produces symbol -//! snapshots exactly the way the interceptor does (LSP overlay for hosted -//! adapters, source text for hostless ones), and routes them through the -//! *same* [`crate::classify::classify_edit`] — one classifier, two -//! callers. A host that fails or text that will not read is reported as -//! skipped, never as "no findings" (spec invariant 9). - -use std::path::Path; -use std::time::Duration; - -use ddd_core::config::DddConfig; -use ddd_core::contracts::{ - content_hash, finding_id, ContractDiffReport, ContractEvent, FileContracts, SkippedFile, - ABSENT, -}; -use ddd_core::{detect, gitrev}; -use serde_json::json; - -use crate::adapter::{self, Adapter}; -use crate::classify::classify_edit; -use crate::host::{Host, Readiness}; -use crate::manager::HostManager; -use crate::protocol::{flatten_symbols, to_uri, RawSymbol}; - -/// Classify every adapter-claimed change between `base` and `head` -/// (`None` = the working tree). `wait` bounds how long a lazily started -/// host may take to become ready before its files are reported skipped. -pub fn diff_contracts( - manager: &mut HostManager, - base: &str, - head: Option<&str>, - wait: Duration, -) -> Result { - let root = manager.root().to_path_buf(); - let base_sha = gitrev::rev_parse(&root, base)?; - let head_sha = head.map(|h| gitrev::rev_parse(&root, h)).transpose()?; - let config = manager.config().cloned().unwrap_or_default(); - let mut report = ContractDiffReport { - base: base_sha.clone(), - head: head_sha.clone().unwrap_or_else(|| "worktree".to_string()), - ..Default::default() - }; - for changed in gitrev::changed_files(&root, &base_sha, head_sha.as_deref())? { - let rel = changed.path; - let path = root.join(&rel); - let Some(adapter) = adapter::for_path(&path) else { continue }; - if config.ignore.iter().any(|g| detect::glob_match(g, &rel)) { - continue; - } - let before = gitrev::bytes_at(&root, &base_sha, &rel)?; - let after = match &head_sha { - Some(h) => gitrev::bytes_at(&root, h, &rel)?, - None => std::fs::read(&path).ok(), - }; - match classify_sides(manager, adapter, &config, &path, &before, &after, wait) { - Ok(events) => report.files.push(file_contracts( - adapter, - &rel, - before.as_deref(), - after.as_deref(), - events, - )), - Err(reason) => report.skipped.push(SkippedFile { file: rel, reason }), - } - } - Ok(report) -} - -fn file_contracts( - adapter: &Adapter, - rel: &str, - before: Option<&[u8]>, - after: Option<&[u8]>, - events: Vec, -) -> FileContracts { - FileContracts { - file: rel.to_string(), - language: adapter.language.to_string(), - artifact_class: adapter.artifact_class.to_string(), - before: before.map(content_hash).unwrap_or_else(|| ABSENT.to_string()), - after: after.map(content_hash).unwrap_or_else(|| ABSENT.to_string()), - events: events - .into_iter() - .map(|event| ContractEvent { id: finding_id(adapter.language, rel, &event), event }) - .collect(), - } -} - -/// Both sides through the shared classifier. Hostless adapters classify -/// text directly; hosted ones snapshot `documentSymbol` per side through -/// an overlay, exactly as the interceptor does. -#[allow(clippy::too_many_arguments)] -fn classify_sides( - manager: &mut HostManager, - adapter: &'static Adapter, - config: &DddConfig, - path: &Path, - before: &Option>, - after: &Option>, - wait: Duration, -) -> Result, String> { - let before_text = side_text(before)?; - let after_text = side_text(after)?; - let flags = manager.flags(adapter.language); - let (before_syms, after_syms) = if adapter.hosted { - let host = manager.host(adapter.language).map_err(|e| e.to_string())?; - snapshot_sides(host, path, &before_text, &after_text, wait)? - } else { - (Vec::new(), Vec::new()) - }; - let mut events = - classify_edit(adapter, &flags, &before_text, &before_syms, &after_text, &after_syms); - if let Some(enrich) = adapter.enrich { - enrich(manager.root(), path, config, &mut events); - } - Ok(events) -} - -fn side_text(bytes: &Option>) -> Result { - match bytes { - None => Ok(String::new()), - Some(b) => String::from_utf8(b.clone()).map_err(|_| "not UTF-8".to_string()), - } -} - -fn snapshot_sides( - host: &mut Host, - path: &Path, - before_text: &str, - after_text: &str, - wait: Duration, -) -> Result<(Vec, Vec), String> { - match host.wait_ready(wait).map_err(|e| e.to_string())? { - Readiness::Ready { .. } => {} - loading => { - return Err(format!( - "host `{}` not ready within {}s (waiting on {:?})", - host.language(), - wait.as_secs(), - loading - )) - } - } - host.open_with_text(path, before_text).map_err(|e| e.to_string())?; - let before = symbols(host, path)?; - host.overlay(path, after_text).map_err(|e| e.to_string())?; - let after = symbols(host, path)?; - // Put the host back on the disk state so later tools see reality. - if let Ok(disk) = std::fs::read_to_string(path) { - let _ = host.overlay(path, &disk); - } - Ok((before, after)) -} - -fn symbols(host: &mut Host, path: &Path) -> Result, String> { - let result = host - .request("textDocument/documentSymbol", json!({"textDocument": {"uri": to_uri(path)}})) - .map_err(|e| e.to_string())?; - Ok(flatten_symbols(&result)) -} - -#[cfg(test)] -#[path = "revdiff_tests.rs"] -mod tests; diff --git a/ddd-lsp/src/revdiff_tests.rs b/ddd-lsp/src/revdiff_tests.rs deleted file mode 100644 index 4ecc8126..00000000 --- a/ddd-lsp/src/revdiff_tests.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! The one-classifier proof (spec invariant 4): identical classification -//! from the diff path vs. the edit path on the same change. - -use std::path::Path; -use std::process::Command; -use std::time::Duration; - -use crate::adapter; -use crate::classify::classify_edit; -use crate::manager::HostManager; -use crate::revdiff::diff_contracts; - -fn git(dir: &Path, args: &[&str]) { - let ok = Command::new("git") - .arg("-C") - .arg(dir) - .args(args) - .output() - .map(|o| o.status.success()) - .unwrap_or(false); - assert!(ok, "git {args:?}"); -} - -fn init_repo(dir: &Path) { - git(dir, &["init", "-q"]); - git(dir, &["config", "user.email", "t@example.com"]); - git(dir, &["config", "user.name", "T"]); -} - -const BEFORE: &str = ":root { --ink: #111; }\n.chip { color: var(--ink); }\n"; -const AFTER: &str = - ":root { --ink: #111; --ink-muted: #666; }\n.chip { color: var(--ink); }\n.card { margin: 0; }\n"; - -#[test] -fn diff_path_matches_edit_path_on_the_same_change() { - let tmp = tempfile::tempdir().expect("tempdir"); - let root = tmp.path(); - init_repo(root); - std::fs::write(root.join("page.css"), BEFORE).expect("write"); - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "before"]); - std::fs::write(root.join("page.css"), AFTER).expect("write"); - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "after"]); - - // The edit path: exactly the calls the interceptor makes — - // classify_edit, then the adapter's enrichment hook. - let adapter = adapter::for_language("htmlcss").expect("adapter"); - let flags = crate::adapter::AdapterFlags::default(); - let mut edit_events = classify_edit(adapter, &flags, BEFORE, &[], AFTER, &[]); - if let Some(enrich) = adapter.enrich { - let config = ddd_core::config::DddConfig::default(); - enrich(root, &root.join("page.css"), &config, &mut edit_events); - } - - // The diff path over the committed transition. - let mut manager = HostManager::with_config(root.to_path_buf(), None); - let report = diff_contracts(&mut manager, "HEAD~1", Some("HEAD"), Duration::from_secs(5)) - .expect("diff-contracts"); - assert!(report.skipped.is_empty(), "{:?}", report.skipped); - assert_eq!(report.files.len(), 1); - let diff_events = &report.files[0].events; - - assert_eq!(edit_events.len(), diff_events.len()); - for (edit, diff) in edit_events.iter().zip(diff_events.iter()) { - assert_eq!(edit.change, diff.event.change); - assert_eq!(edit.facts, diff.event.facts); - assert_eq!(edit.surface, diff.event.surface); - assert_eq!(edit.rule, diff.event.rule); - } -} - -#[test] -fn worktree_diff_classifies_uncommitted_changes() { - let tmp = tempfile::tempdir().expect("tempdir"); - let root = tmp.path(); - init_repo(root); - std::fs::write(root.join("page.css"), BEFORE).expect("write"); - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "before"]); - std::fs::write(root.join("page.css"), AFTER).expect("write"); - - let mut manager = HostManager::with_config(root.to_path_buf(), None); - let report = - diff_contracts(&mut manager, "HEAD", None, Duration::from_secs(5)).expect("diff"); - assert_eq!(report.head, "worktree"); - assert_eq!(report.files.len(), 1); - assert!(report.files[0].events.iter().any(|e| e.event.surface)); -} - -#[test] -fn deleted_file_reports_removals() { - let tmp = tempfile::tempdir().expect("tempdir"); - let root = tmp.path(); - init_repo(root); - std::fs::write(root.join("page.css"), BEFORE).expect("write"); - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "before"]); - std::fs::remove_file(root.join("page.css")).expect("rm"); - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "gone"]); - - let mut manager = HostManager::with_config(root.to_path_buf(), None); - let report = diff_contracts(&mut manager, "HEAD~1", Some("HEAD"), Duration::from_secs(5)) - .expect("diff"); - assert_eq!(report.files.len(), 1); - assert_eq!(report.files[0].after, ddd_core::contracts::ABSENT); - assert!(report.files[0] - .events - .iter() - .all(|e| e.event.change == ddd_core::surface::ChangeKind::Removed)); -} diff --git a/ddd-lsp/tests/declaration_subset.rs b/ddd-lsp/tests/declaration_subset.rs deleted file mode 100644 index 4c6636e8..00000000 --- a/ddd-lsp/tests/declaration_subset.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! The declaration-level LSP subset against the mock host: the two newly -//! wired operations, plus the capability probe that gates every row. -//! -//! Hermetic — the workspace CI has no .NET SDK (`dec/ddd/fixtures-not-sdk`). -//! Live-host confirmation is the `DDD_LSP_E2E=1` run. - -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use ddd_lsp::capability::{self, Divergence}; -use ddd_lsp::declaration::{self, Operation}; -use ddd_lsp::host::Host; - -fn mock_command(extra: &[&str]) -> Vec { - let mut cmd = vec![env!("CARGO_BIN_EXE_ddd-mock-lsp").to_string()]; - cmd.extend(extra.iter().map(|s| s.to_string())); - cmd -} - -fn write(dir: &Path, rel: &str, content: &str) -> PathBuf { - let path = dir.join(rel); - std::fs::write(&path, content).expect("write fixture"); - path -} - -/// A fixture in the corpus's own idioms: a marker interface, records -/// implementing it, and a value-object identity type — the shapes §5.2's -/// `rdfs:subClassOf` row fires on. -const MESSAGES: &str = "\ -public interface ICommand -{ -} -public record UpsertProfileSettingsCommand(string Value) : ICommand; -public record DisableProfileCommand(string Reason) : ICommand; -public class Unrelated -{ -} -"; - -const IDENTITY: &str = "\ -public interface IStringIdentity -{ -} -public readonly record struct LocationId(string Value) : IStringIdentity; -public readonly record struct CustomerId(string Value); -"; - -fn host(dir: &Path, extra: &[&str]) -> Host { - let adapter = ddd_lsp::adapter::for_language("csharp").expect("adapter"); - Host::new(adapter, mock_command(extra), dir.to_path_buf()) -} - -fn ready_host(dir: &Path, extra: &[&str]) -> Host { - let mut h = host(dir, extra); - h.wait_ready(Duration::from_secs(10)).expect("readiness"); - h -} - -#[test] -fn definition_resolves_through_the_existing_location_normaliser() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let messages = write(tmp.path(), "Messages.cs", MESSAGES); - let mut h = ready_host(tmp.path(), &[]); - h.ensure_open(&messages).expect("open"); - // Position: the `ICommand` mention on the first record's base list. - let line = MESSAGES.lines().position(|l| l.contains("UpsertProfileSettings")).expect("line"); - let col = MESSAGES.lines().nth(line).and_then(|l| l.rfind("ICommand")).expect("col"); - let found = declaration::definition(&mut h, &messages, line as u64, col as u64).expect("definition"); - assert_eq!(found.len(), 1, "{found:?}"); - assert_eq!(found[0].file, messages); - assert_eq!(found[0].line, 0, "resolves to the interface's own declaration"); -} - -#[test] -fn type_hierarchy_answers_in_both_directions() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let messages = write(tmp.path(), "Messages.cs", MESSAGES); - let mut h = ready_host(tmp.path(), &[]); - h.ensure_open(&messages).expect("open"); - - let sub_line = MESSAGES.lines().position(|l| l.contains("UpsertProfileSettings")).expect("line"); - let prepared = declaration::prepare_type_hierarchy(&mut h, &messages, sub_line as u64, 14) - .expect("prepare"); - assert_eq!(prepared.len(), 1, "{prepared:?}"); - let supers = declaration::supertypes(&mut h, &prepared[0]).expect("supertypes"); - assert_eq!(supers.iter().map(|i| i.name.as_str()).collect::>(), ["ICommand"]); - - let marker = declaration::prepare_type_hierarchy(&mut h, &messages, 0, 17).expect("prepare"); - let subs = declaration::subtypes(&mut h, &marker[0]).expect("subtypes"); - let mut names: Vec<&str> = subs.iter().map(|i| i.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!(names, ["DisableProfileCommand", "UpsertProfileSettingsCommand"]); -} - -/// The item goes back to the server exactly as it arrived. The entry probe -/// named this the wiring detail most likely to be got wrong: Roslyn -/// resolves the directional calls only from its own opaque `data`. -#[test] -fn the_prepared_item_travels_back_untouched() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let identity = write(tmp.path(), "Identity.cs", IDENTITY); - let mut h = ready_host(tmp.path(), &[]); - h.ensure_open(&identity).expect("open"); - - let line = IDENTITY.lines().position(|l| l.contains("LocationId")).expect("line") as u64; - let items = declaration::prepare_type_hierarchy(&mut h, &identity, line, 29).expect("prepare"); - assert!(items[0].data.is_some(), "the server sent an opaque blob"); - let supers = declaration::supertypes(&mut h, &items[0]).expect("supertypes"); - assert_eq!(supers.iter().map(|i| i.name.as_str()).collect::>(), ["IStringIdentity"]); - - // The sibling identity type declares no interface — a real empty - // hierarchy, which must read differently from an instrument failure. - let sibling = IDENTITY.lines().position(|l| l.contains("CustomerId")).expect("line") as u64; - let items = declaration::prepare_type_hierarchy(&mut h, &identity, sibling, 29).expect("prepare"); - assert_eq!(items.len(), 1, "the type itself prepares"); - assert!(declaration::supertypes(&mut h, &items[0]).expect("supertypes").is_empty()); -} - -#[test] -fn the_probe_finds_every_operation_available_on_a_answering_host() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let messages = write(tmp.path(), "Messages.cs", MESSAGES); - let mut h = ready_host(tmp.path(), &[]); - let probe = capability::probe(&mut h, &messages).expect("probe"); - assert_eq!(probe.unavailable(), Vec::::new(), "{}", probe.report()); - let anchor = probe.anchor.expect("anchor"); - assert_eq!(anchor.name, "ICommand", "the probe anchors on a type declaration"); -} - -/// The kotlin-lsp shape, end to end: the flag says yes, the call answers -/// `null`. The row must read UNAVAILABLE and be flagged over-advertised — -/// never as a codebase that happens to have no hierarchy. -#[test] -fn an_over_advertising_host_is_caught_by_the_probe_not_believed() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let messages = write(tmp.path(), "Messages.cs", MESSAGES); - let mut h = ready_host(tmp.path(), &["--over-advertise", "type-hierarchy"]); - let probe = capability::probe(&mut h, &messages).expect("probe"); - - assert!(!probe.available(Operation::TypeHierarchy), "{}", probe.report()); - assert_eq!(probe.unavailable(), vec![Operation::TypeHierarchy]); - let flagged = probe.divergences(); - assert_eq!(flagged.len(), 1); - assert_eq!(flagged[0].operation, Operation::TypeHierarchy); - assert_eq!(flagged[0].divergence, Some(Divergence::OverAdvertised)); - assert_eq!(flagged[0].advertised, Some(true), "the claim still stands"); - assert!(probe.report().contains("OVER-ADVERTISED"), "{}", probe.report()); - - // Everything else still answers: an over-advertised operation costs its - // own row, never the run. - for op in [Operation::DocumentSymbol, Operation::Definition, Operation::References] { - assert!(probe.available(op), "{op} should still answer\n{}", probe.report()); - } -} - -#[test] -fn a_file_with_no_declarations_yields_no_anchor_rather_than_five_absences() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let empty = write(tmp.path(), "Empty.cs", "// nothing declared here\n"); - let mut h = ready_host(tmp.path(), &[]); - let probe = capability::probe(&mut h, &empty).expect("probe"); - assert!(probe.anchor.is_none(), "{}", probe.report()); - assert!(probe.report().contains("no type declaration")); - for op in Operation::ALL.iter().skip(1) { - let row = probe.operations.iter().find(|r| r.operation == *op).expect("row"); - assert!(row.detail.contains("not probed"), "{op}: {}", row.detail); - } -} diff --git a/ddd-lsp/tests/host.rs b/ddd-lsp/tests/host.rs deleted file mode 100644 index bd9168c7..00000000 --- a/ddd-lsp/tests/host.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Host lifecycle against the mock LSP binary: readiness gating, request -//! correlation, crash recovery (kill -9), diagnostics capture. - -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use ddd_lsp::adapter; -use ddd_lsp::host::{Host, Readiness}; -use ddd_lsp::protocol::flatten_symbols; - -fn mock_command(extra: &[&str]) -> Vec { - let mut cmd = vec![env!("CARGO_BIN_EXE_ddd-mock-lsp").to_string()]; - cmd.extend(extra.iter().map(|s| s.to_string())); - cmd -} - -fn write(dir: &Path, rel: &str, content: &str) -> PathBuf { - let path = dir.join(rel); - std::fs::write(&path, content).expect("write fixture"); - path -} - -const CS: &str = "public class Api\n{\n public void Ping() { }\n}\n"; - -/// A csharp-adapter host wired to the mock; `roslyn` turns the handshake on. -fn host(dir: &Path, roslyn: bool) -> Host { - let adapter = adapter::for_language("csharp").expect("adapter"); - let extra: &[&str] = if roslyn { &["--handshake", "roslyn"] } else { &[] }; - Host::new(adapter, mock_command(extra), dir.to_path_buf()) -} - -#[test] -fn answers_document_symbols_when_ready() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let file = write(tmp.path(), "Api.cs", CS); - let mut h = host(tmp.path(), true); - let ready = h.wait_ready(Duration::from_secs(10)).expect("readiness"); - assert!(matches!(ready, Readiness::Ready { .. }), "{ready:?}"); - h.ensure_open(&file).expect("open"); - let result = h - .request( - "textDocument/documentSymbol", - serde_json::json!({"textDocument": {"uri": ddd_lsp::protocol::to_uri(&file)}}), - ) - .expect("symbols"); - let flat = flatten_symbols(&result); - assert!(flat.iter().any(|s| s.name == "Ping"), "{flat:?}"); -} - -#[test] -fn loading_window_is_reported_not_hung() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - std::env::set_var("DDD_MOCK_READY_DELAY_MS", "600"); - let mut h = host(tmp.path(), true); - let first = h.readiness().expect("probe"); - std::env::remove_var("DDD_MOCK_READY_DELAY_MS"); - match first { - Readiness::Loading { detail, .. } => { - assert!(detail.contains("loading"), "{detail}"); - } - other => panic!("expected loading, got {other:?}"), - } - let later = h.wait_ready(Duration::from_secs(10)).expect("readiness"); - assert!(matches!(later, Readiness::Ready { .. }), "{later:?}"); - // The mock raised a restore-style notification during load; the host - // keeps it as a note (PRD §5.1 restore quirk). - assert!(h.notes().iter().any(|n| n.contains("restore")), "{:?}", h.notes()); -} - -#[test] -fn kill_dash_nine_recovers_on_the_next_call() { - let tmp = tempfile::tempdir().expect("tmp"); - write(tmp.path(), "Api.csproj", ""); - let file = write(tmp.path(), "Api.cs", CS); - let mut h = host(tmp.path(), false); - h.ensure_open(&file).expect("open"); - let pid = h.pid().expect("pid"); - let status = std::process::Command::new("kill") - .args(["-9", &pid.to_string()]) - .status() - .expect("kill"); - assert!(status.success()); - std::thread::sleep(Duration::from_millis(200)); - // Next call must respawn, re-open the document, and answer. - h.ensure_open(&file).expect("re-open after crash"); - let result = h - .request( - "textDocument/documentSymbol", - serde_json::json!({"textDocument": {"uri": ddd_lsp::protocol::to_uri(&file)}}), - ) - .expect("symbols after restart"); - assert!(!flatten_symbols(&result).is_empty()); - assert_eq!(h.restarts, 1); - assert_ne!(h.pid(), Some(pid)); -} - -#[test] -fn published_diagnostics_are_captured_from_a_sidecar() { - let tmp = tempfile::tempdir().expect("tmp"); - let file = write(tmp.path(), "main.bicep", "param unusedParam string = 'x'\n"); - write( - tmp.path(), - "main.bicep.diag.json", - r#"[{"range": {"start": {"line": 0, "character": 6}, "end": {"line": 0, "character": 17}}, - "severity": 2, "code": "no-unused-params", "source": "bicep", "message": "unused"}]"#, - ); - let adapter = adapter::for_language("bicep").expect("adapter"); - let mut h = Host::new(adapter, mock_command(&[]), tmp.path().to_path_buf()); - h.ensure_open(&file).expect("open"); - let mut published = None; - for _ in 0..50 { - published = h.published_diagnostics(&file).expect("state"); - if published.is_some() { - break; - } - std::thread::sleep(Duration::from_millis(50)); - } - let params = published.expect("diagnostics published"); - assert_eq!( - params.pointer("/diagnostics/0/code").and_then(|v| v.as_str()), - Some("no-unused-params") - ); -} diff --git a/ddd-lsp/tests/real_hosts.rs b/ddd-lsp/tests/real_hosts.rs deleted file mode 100644 index bfd5cf5b..00000000 --- a/ddd-lsp/tests/real_hosts.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Real-host verification, gated behind `DDD_LSP_E2E=1` — the workspace -//! CI stays hermetic (`dec/ddd/fixtures-not-sdk`); run locally with -//! `roslyn-language-server` plus `bicep-ls` on PATH. - -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use ddd_lsp::adapter; -use ddd_lsp::host::{Host, Readiness}; -use ddd_lsp::protocol::{flatten_symbols, to_uri}; - -fn gated() -> bool { - std::env::var("DDD_LSP_E2E").ok().as_deref() != Some("1") -} - -fn fixture(rel: &str) -> String { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../ddd-cli/tests/fixtures") - .join(rel); - std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) -} - -fn write(root: &Path, rel: &str, content: &str) -> PathBuf { - let path = root.join(rel); - std::fs::write(&path, content).expect("write"); - path -} - -fn default_host(language: &str, root: &Path) -> Host { - let a = adapter::for_language(language).expect("adapter"); - let command = a.default_command.iter().map(|s| s.to_string()).collect(); - Host::new(a, command, root.to_path_buf()) -} - -fn document_symbols(host: &mut Host, file: &Path) -> Vec { - host.ensure_open(file).expect("open"); - let result = host - .request("textDocument/documentSymbol", serde_json::json!({"textDocument": {"uri": to_uri(file)}})) - .expect("symbols"); - flatten_symbols(&result).into_iter().map(|s| s.name).collect() -} - -#[test] -fn real_bicep_ls_symbols_and_diagnostics() { - if gated() { - return; - } - let tmp = tempfile::tempdir().expect("tmp"); - let file = write(tmp.path(), "storage-module.bicep", &fixture("bicep/storage-module.bicep")); - let mut host = default_host("bicep", tmp.path()); - let ready = host.wait_ready(Duration::from_secs(60)).expect("readiness"); - assert!(matches!(ready, Readiness::Ready { .. }), "{ready:?}"); - let names = document_symbols(&mut host, &file); - assert!(names.iter().any(|n| n.contains("accountName")), "{names:?}"); - // deployedAt is deliberately unused — the linter should say so. - let mut published = None; - for _ in 0..100 { - published = host.published_diagnostics(&file).expect("state"); - if published.is_some() { - break; - } - std::thread::sleep(Duration::from_millis(200)); - } - let params = published.expect("published diagnostics"); - let text = params.to_string(); - assert!(text.contains("no-unused-params"), "{text}"); -} - -#[test] -fn real_roslyn_reaches_ready_and_answers_symbols() { - if gated() { - return; - } - let tmp = tempfile::tempdir().expect("tmp"); - write( - tmp.path(), - "Api.csproj", - "net8.0", - ); - let file = write(tmp.path(), "Library.cs", &fixture("csharp/Library.cs")); - let mut host = default_host("csharp", tmp.path()); - let ready = host.wait_ready(Duration::from_secs(300)).expect("readiness"); - assert!(matches!(ready, Readiness::Ready { .. }), "{ready:?} notes={:?}", host.notes()); - // Roslyn decorates names ("Ping() : void") — assert on prefixes here; - // the adapter's facts layer strips to bare identifiers. - let names = document_symbols(&mut host, &file); - assert!(names.iter().any(|n| n.starts_with("PublicApi")), "{names:?}"); - assert!(names.iter().any(|n| n.starts_with("Ping")), "{names:?}"); -} diff --git a/ddd-lsp/tests/rust_host.rs b/ddd-lsp/tests/rust_host.rs deleted file mode 100644 index 232ef394..00000000 --- a/ddd-lsp/tests/rust_host.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Rust host acceptance against real rust-analyzer (`dec/ddd/rust-host-is-real`). -//! -//! Unlike the .NET hosts these are not gated: rust-analyzer is a component of -//! the pinned toolchain, so it is present locally and in CI. A missing -//! component fails loudly rather than skipping — a silent skip is a green -//! exit code standing in for a check that never ran. - -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use ddd_lsp::adapter; -use ddd_lsp::host::{Host, Readiness}; -use ddd_lsp::protocol::{flatten_symbols, to_uri, RawSymbol}; - -fn fixture(rel: &str) -> String { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../ddd-cli/tests/fixtures/rust") - .join(rel); - std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) -} - -/// Materialise the fixture crate into a tempdir as a real cargo package. -pub fn fixture_crate() -> tempfile::TempDir { - let tmp = tempfile::tempdir().expect("tempdir"); - std::fs::create_dir_all(tmp.path().join("src")).expect("mkdir"); - std::fs::write(tmp.path().join("Cargo.toml"), fixture("Cargo.toml.txt")).expect("manifest"); - std::fs::write(tmp.path().join("src/lib.rs"), fixture("lib.rs")).expect("lib"); - std::fs::write(tmp.path().join("src/other.rs"), fixture("other.rs")).expect("other"); - tmp -} - -/// The adapter's own resolved host command, with the loud failure a missing -/// toolchain component deserves — a silent skip would be a green exit code -/// standing in for a check that never ran. -fn host_command() -> Vec { - let command = adapter::rust::host_command(); - let program = command.first().cloned().unwrap_or_default(); - assert!( - std::process::Command::new(&program).arg("--version").output().is_ok(), - "cannot run `{program}`. rust-analyzer is a pinned component of this toolchain \ - (rust-toolchain.toml) — install it with `rustup component add rust-analyzer`." - ); - command -} - -fn rust_host(root: &Path) -> Host { - let a = adapter::for_language("rust").expect("rust adapter registered"); - Host::new(a, host_command(), root.to_path_buf()) -} - -fn symbols(host: &mut Host, file: &Path) -> Vec { - host.ensure_open(file).expect("open"); - let result = host - .request("textDocument/documentSymbol", serde_json::json!({"textDocument": {"uri": to_uri(file)}})) - .expect("documentSymbol"); - flatten_symbols(&result) -} - -fn find<'a>(list: &'a [RawSymbol], name: &str) -> &'a RawSymbol { - list.iter().find(|s| s.name == name).unwrap_or_else(|| { - panic!("`{name}` not among {:?}", list.iter().map(|s| &s.name).collect::>()) - }) -} - -#[test] -fn a_loading_host_says_so_before_it_says_ready() { - let tmp = fixture_crate(); - let mut host = rust_host(tmp.path()); - - // First probe: the server has answered `initialize` but is still - // fetching sysroot and cargo metadata. It must not claim readiness. - let first = host.readiness().expect("readiness"); - let Readiness::Loading { detail, .. } = &first else { - panic!("expected Loading immediately after spawn, got {first:?}"); - }; - assert!( - detail.contains("experimental/serverStatus"), - "the loading detail should name what is being waited on: {detail}" - ); - - let ready = host.wait_ready(Duration::from_secs(120)).expect("wait_ready"); - assert!(matches!(ready, Readiness::Ready { .. }), "{ready:?}"); -} - -#[test] -fn readiness_waits_for_quiescent_not_for_the_first_status_notification() { - let tmp = fixture_crate(); - let mut host = rust_host(tmp.path()); - let ready = host.wait_ready(Duration::from_secs(120)).expect("wait_ready"); - let Readiness::Ready { elapsed_ms } = ready else { - panic!("never became ready: {ready:?}"); - }; - // rust-analyzer sends `experimental/serverStatus` with `quiescent: false` - // within a few hundred milliseconds of `initialize`, and only reports - // quiescence once indexing has settled. A method-name-only readiness flag - // would return here almost immediately; this asserts it does not. - assert!( - elapsed_ms > 500, - "became ready in {elapsed_ms}ms — readiness is tracking the notification's arrival, not its payload" - ); -} - -#[test] -fn the_symbol_shapes_the_adapter_parses_are_the_ones_the_server_emits() { - let tmp = fixture_crate(); - let file = tmp.path().join("src/lib.rs"); - let mut host = rust_host(tmp.path()); - host.wait_ready(Duration::from_secs(120)).expect("ready"); - let syms = symbols(&mut host, &file); - - // Traits arrive as LSP `interface`; their members carry container_kind 11. - assert_eq!(find(&syms, "Greeter").kind, 11); - assert_eq!(find(&syms, "greet").container_kind, 11); - // Impl blocks arrive as one `object` symbol named for the whole header — - // this is where trait participation is read from. - let trait_impl = find(&syms, "impl Greeter for Person"); - assert_eq!(trait_impl.kind, 19); - // Structs, enums, variants, modules, consts, type aliases. - assert_eq!(find(&syms, "Person").kind, 23); - assert_eq!(find(&syms, "Colour").kind, 10); - assert_eq!(find(&syms, "Red").kind, 22); - assert_eq!(find(&syms, "Red").container_kind, 10); - assert_eq!(find(&syms, "visible").kind, 2); - assert_eq!(find(&syms, "LIMIT").kind, 14); - assert_eq!(find(&syms, "Count").kind, 26); - // And no visibility anywhere in the payload — which is why the adapter - // slices the source text. - let payload = serde_json::to_string(&syms).expect("serialise"); - assert!(!payload.contains("pub("), "symbols unexpectedly carry visibility: {payload}"); -} - -#[test] -fn facts_from_the_real_host_carry_the_graded_visibility() { - let tmp = fixture_crate(); - let file = tmp.path().join("src/lib.rs"); - let mut host = rust_host(tmp.path()); - host.wait_ready(Duration::from_secs(120)).expect("ready"); - let syms = symbols(&mut host, &file); - let a = adapter::for_language("rust").expect("adapter"); - let text = std::fs::read_to_string(&file).expect("read"); - let facts = (a.facts)(&text, &syms, &adapter::AdapterFlags::default()); - - // `age` exists twice — the private field and the `pub(crate)` accessor — - // so lookups here carry the container, exactly as `SymbolFacts::key` does. - let grade = |container: &str, name: &str| { - facts - .iter() - .find(|f| f.name == name && f.container == container) - .unwrap_or_else(|| panic!("{container}/{name} not in facts")) - .visibility - .clone() - }; - assert_eq!(grade("impl Person", "new"), "public"); - assert_eq!(grade("impl Person", "age"), "pub(crate)"); - assert_eq!(grade("impl Person", "bump"), "private"); - assert_eq!(grade("", "describe"), "public"); - assert_eq!(grade("", "internal_helper"), "private"); - assert_eq!(grade("visible", "exported"), "public"); - // `pub fn` inside a private module is not reachable. - assert_eq!(grade("capped", "looks_public_is_not"), "private"); - // Struct fields: `pub name` is surface, bare `age` is not, on the same - // public struct. - assert_eq!(grade("Person", "name"), "public"); - assert_eq!(grade("Person", "age"), "private"); -} - -#[test] -fn a_killed_host_is_respawned_on_the_next_call() { - let tmp = fixture_crate(); - let file = tmp.path().join("src/lib.rs"); - let mut host = rust_host(tmp.path()); - host.wait_ready(Duration::from_secs(120)).expect("ready"); - let before = symbols(&mut host, &file); - assert!(!before.is_empty()); - - let pid = host.pid().expect("pid"); - let status = std::process::Command::new("kill") - .args(["-9", &pid.to_string()]) - .status() - .expect("kill"); - assert!(status.success()); - std::thread::sleep(Duration::from_millis(200)); - - // The replacement starts cold, so it is honest about loading again. - let after_crash = host.readiness().expect("readiness after crash"); - assert!(matches!(after_crash, Readiness::Loading { .. }), "{after_crash:?}"); - assert_eq!(host.restarts, 1); - assert_ne!(host.pid(), Some(pid)); - - host.wait_ready(Duration::from_secs(120)).expect("ready again"); - let after = symbols(&mut host, &file); - assert_eq!( - before.iter().map(|s| &s.name).collect::>(), - after.iter().map(|s| &s.name).collect::>() - ); -} diff --git a/ddd-mcp/Cargo.toml b/ddd-mcp/Cargo.toml deleted file mode 100644 index 67a1ddeb..00000000 --- a/ddd-mcp/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "ddd-mcp" -version = "0.7.0" -edition = "2021" -authors = ["PiCloud Contributors"] -license = "Apache-2.0" -description = "The ddd_* MCP tool surface: LSP-backed language intelligence, the contract-surface interceptor, governance tools." -repository = "https://github.com/Hafeok/product-cli" -homepage = "https://github.com/Hafeok/product-cli" -readme = "../README.md" - -[lib] -name = "ddd_mcp" -path = "src/lib.rs" - -[package.metadata.dist] -dist = false - -[lints] -workspace = true - -[dependencies] -ddd-core = { path = "../ddd-core", version = "0.7.0" } -ddd-lsp = { path = "../ddd-lsp", version = "0.7.0" } -product-core = { path = "../product-core", version = "0.7.0" } -product-mcp = { path = "../product-mcp", version = "0.7.0" } -chrono = "0.4" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_yaml = "0.9" - -[dev-dependencies] -tempfile = "3" diff --git a/ddd-mcp/src/bin/ddd-mcp-mock-lsp.rs b/ddd-mcp/src/bin/ddd-mcp-mock-lsp.rs deleted file mode 100644 index fcaee215..00000000 --- a/ddd-mcp/src/bin/ddd-mcp-mock-lsp.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Mock LSP host binary for this crate's integration tests. -//! -//! Mirrors `ddd-lsp`'s `ddd-mock-lsp` (Cargo only exposes -//! `CARGO_BIN_EXE_*` for same-crate binaries, so each crate that tests -//! against the mock carries this thin wrapper). - -fn main() { - let args: Vec = std::env::args().collect(); - let roslyn_handshake = args.windows(2).any(|w| w[0] == "--handshake" && w[1] == "roslyn"); - let delay_ms = std::env::var("DDD_MOCK_READY_DELAY_MS") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(0); - let over_advertise_type_hierarchy = args - .windows(2) - .any(|w| w[0] == "--over-advertise" && w[1] == "type-hierarchy"); - ddd_lsp::mock::serve_stdio(ddd_lsp::mock::MockConfig { - roslyn_handshake, - ready_delay: std::time::Duration::from_millis(delay_ms), - over_advertise_type_hierarchy, - }); -} diff --git a/ddd-mcp/src/bind.rs b/ddd-mcp/src/bind.rs deleted file mode 100644 index 90791ee5..00000000 --- a/ddd-mcp/src/bind.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Signed-binding resolution — which stored declaration discharges an -//! edit's transition (M8, spec invariant 3). -//! -//! Same-session matching is retired (`DDD-arch-09`: it discharged -//! whatever change next touched the symbol). A declaration now signs the -//! change it discharges — subject symbol, before/after content hashes, -//! base revision — and the interceptor matches edits against those -//! signatures alone. The parent state must be committed: a dirty file -//! refuses to bind, so a binding always names a reproducible transition. - -use std::path::Path; - -use ddd_core::binding::SeamBinding; -use ddd_core::contracts::{content_hash, ABSENT}; -use ddd_core::gitrev; -use ddd_core::seam::SeamDeclaration; -use ddd_core::surface::SurfaceEvent; - -/// The transition an edit proposes, content-addressed. -#[derive(Debug, Clone)] -pub struct Transition { - /// Repo-relative file, forward slashes. - pub rel: String, - /// Hash of the parent state, or [`ABSENT`]. - pub before: String, - /// Hash of the proposed state. - pub after: String, - /// The HEAD commit, when the root is a git repository. - pub head: Option, - /// Whether the parent state is exactly what HEAD carries — the - /// precondition for binding (M8 ruling 2: pin against HEAD, never - /// against uncommitted parent state). - pub committed_parent: bool, -} - -impl Transition { - /// Content-address the edit and check its parent state against HEAD. - pub fn of(root: &Path, file: &Path, rel: &str, old_text: &str, new_text: &str) -> Self { - let on_disk = file.exists(); - let before = - if on_disk { content_hash(old_text.as_bytes()) } else { ABSENT.to_string() }; - let after = content_hash(new_text.as_bytes()); - let head = gitrev::head(root).ok(); - let committed_parent = match &head { - None => false, - Some(h) => match (gitrev::bytes_at(root, h, rel).ok().flatten(), on_disk) { - (None, false) => true, - (Some(bytes), true) => bytes.as_slice() == old_text.as_bytes(), - _ => false, - }, - }; - Self { rel: rel.to_string(), before, after, head, committed_parent } - } - - /// The refusal text when the parent state is not committed. - pub fn dirty_reason(&self) -> String { - match &self.head { - None => "the repository has no HEAD commit — a declaration binds against committed \ - parent state, and there is none to bind against (M8 ruling 2)" - .to_string(), - Some(h) => format!( - "{} is dirty against HEAD ({}) — a declaration never binds uncommitted parent \ - state; commit the current state first (M8 ruling 2, the ledger's \ - construction-limit precedent)", - self.rel, - &h[..12.min(h.len())] - ), - } - } -} - -/// The stored seam whose signed binding discharges each event, matched on -/// the exact transition: subject symbol, file, before/after hashes, and -/// the pinned base revision equal to the current HEAD. -pub fn match_bindings( - seams: &[SeamDeclaration], - events: &[SurfaceEvent], - t: &Transition, -) -> Vec> { - events - .iter() - .map(|e| { - seams - .iter() - .find(|s| { - s.bindings.iter().any(|b| { - binding_signs(b, &e.facts.name, t) - && Some(&b.base_revision) == t.head.as_ref() - }) - }) - .map(|s| s.id.clone()) - }) - .collect() -} - -/// Warn-mode advisory links: a signed binding when one matches, else the -/// generous file arm over stored declarations (the edit applies either -/// way, so a broad link can inform without discharging). -pub fn advisory_links( - seams: &[SeamDeclaration], - events: &[SurfaceEvent], - t: &Transition, -) -> Vec> { - let exact = match_bindings(seams, events, t); - events - .iter() - .zip(exact) - .map(|(e, hit)| { - hit.or_else(|| { - seams - .iter() - .find(|s| { - s.contract_location.contains(&t.rel) - || s.bindings.iter().any(|b| b.symbol == e.facts.name) - }) - .map(|s| s.id.clone()) - }) - }) - .collect() -} - -fn binding_signs(b: &SeamBinding, symbol: &str, t: &Transition) -> bool { - b.signs(symbol, &t.rel, &t.before, &t.after) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - - fn seam(id: &str, bindings: Vec) -> SeamDeclaration { - SeamDeclaration { - format: 2, - id: id.to_string(), - boundary: "b".into(), - verdict_knowledge: "v".into(), - contract_location: "src/a.rs#f".into(), - obligations: Vec::new(), - metadata: BTreeMap::new(), - bindings, - notes: None, - } - } - - fn event(name: &str) -> SurfaceEvent { - SurfaceEvent { - change: ddd_core::surface::ChangeKind::Added, - facts: ddd_core::surface::SymbolFacts { - name: name.into(), - container: String::new(), - kind: "fn".into(), - visibility: "public".into(), - signature: "fn f()".into(), - decorators: Vec::new(), - exported: false, - extra: BTreeMap::new(), - sel_line: 0, - sel_char: 0, - }, - before: None, - surface: true, - rule: None, - rule_claim: None, - } - } - - fn transition() -> Transition { - Transition { - rel: "src/a.rs".into(), - before: "sha256:aa".into(), - after: "sha256:bb".into(), - head: Some("headsha".into()), - committed_parent: true, - } - } - - #[test] - fn a_binding_discharges_only_its_exact_transition() { - let t = transition(); - let b = SeamBinding::seal("f", "src/a.rs", "sha256:aa", "sha256:bb", "headsha"); - let seams = vec![seam("seam/rust/f", vec![b])]; - let hit = match_bindings(&seams, &[event("f")], &t); - assert_eq!(hit, vec![Some("seam/rust/f".to_string())]); - - // A different edit landing on identical content: same after, - // different before — refused. - let other = Transition { before: "sha256:cc".into(), ..t.clone() }; - assert_eq!(match_bindings(&seams, &[event("f")], &other), vec![None]); - - // Same content pair pinned at a different base revision — refused. - let moved = Transition { head: Some("othersha".into()), ..t }; - assert_eq!(match_bindings(&seams, &[event("f")], &moved), vec![None]); - } - - #[test] - fn warn_mode_links_generously_but_only_signed_bindings_discharge() { - let t = transition(); - let seams = vec![seam("seam/rust/f", Vec::new())]; - assert_eq!(match_bindings(&seams, &[event("f")], &t), vec![None]); - let links = advisory_links(&seams, &[event("f")], &t); - assert_eq!(links, vec![Some("seam/rust/f".to_string())]); - } -} diff --git a/ddd-mcp/src/dispatch.rs b/ddd-mcp/src/dispatch.rs deleted file mode 100644 index e90c1b41..00000000 --- a/ddd-mcp/src/dispatch.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Tool-name routing for the `ddd_*` namespace. - -use serde_json::Value; - -use crate::state::ServeState; -use crate::{govern_tools as gov, intercept, lang_tools as lang}; - -/// Route one call; `None` means the name is not ours. -pub fn dispatch(state: &ServeState, name: &str, args: &Value) -> Option> { - Some(match name { - "ddd_find_symbol" => lang::find_symbol(state, args), - "ddd_references" => lang::references(state, args), - "ddd_hover" => lang::hover(state, args), - "ddd_diagnostics" => lang::diagnostics(state, args), - "ddd_signature" => lang::signature(state, args), - "ddd_rename" => lang::rename(state, args), - "ddd_apply_edit" => intercept::apply_edit(state, args), - "ddd_warmup" => lang::warmup(state, args), - "ddd_why" => gov::why(state, args), - "ddd_graph_query" => gov::graph_query(state, args), - "ddd_declare_seam" => gov::declare_seam(state, args), - "ddd_declare_pattern" => gov::declare_pattern(state, args), - "ddd_accept_risk" => gov::accept_risk(state, args), - _ => return None, - }) -} diff --git a/ddd-mcp/src/edits.rs b/ddd-mcp/src/edits.rs deleted file mode 100644 index 3e86a9b8..00000000 --- a/ddd-mcp/src/edits.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Edit-payload resolution — full text or LSP-style range edits. -//! -//! `ddd_apply_edit` accepts either the whole new file (`new_text`, read -//! through `raw_str` so a trailing newline survives — `DDD-arch-07`) or a -//! list of range edits applied last-to-first against the current content. - -use std::path::Path; - -use serde_json::Value; - -use crate::state::raw_str; - -pub fn read_or_empty(file: &Path) -> Result { - if file.exists() { - std::fs::read_to_string(file).map_err(|e| format!("{}: {e}", file.display())) - } else { - Ok(String::new()) - } -} - -pub fn resolve_new_text(args: &Value, old_text: &str) -> Result { - // `raw_str`: this value is a whole file, so trimming it loses a newline. - if let Some(text) = raw_str(args, "new_text") { - return Ok(text); - } - let edits = args - .get("edits") - .and_then(Value::as_array) - .ok_or("give `new_text` (full content) or `edits` (range edits)")?; - apply_range_edits(old_text, edits) -} - -/// Apply LSP-style range edits (applied last-to-first so earlier offsets -/// stay valid). -fn apply_range_edits(text: &str, edits: &[Value]) -> Result { - let mut offsets: Vec<(usize, usize, String)> = Vec::new(); - for edit in edits { - let pos = |leaf: &str| -> Result { - let line = edit.pointer(&format!("/range/{leaf}/line")).and_then(Value::as_u64); - let ch = edit.pointer(&format!("/range/{leaf}/character")).and_then(Value::as_u64); - match (line, ch) { - (Some(l), Some(c)) => byte_offset(text, l as usize, c as usize), - _ => Err("edit range missing line/character".to_string()), - } - }; - let new = edit - .get("new_text") - .or_else(|| edit.get("newText")) - .and_then(Value::as_str) - .ok_or("edit missing new_text")?; - offsets.push((pos("start")?, pos("end")?, new.to_string())); - } - offsets.sort_by_key(|e| std::cmp::Reverse(e.0)); - let mut out = text.to_string(); - for (start, end, new) in offsets { - if start > end || end > out.len() { - return Err("edit range out of bounds".to_string()); - } - out.replace_range(start..end, &new); - } - Ok(out) -} - -fn byte_offset(text: &str, line: usize, character: usize) -> Result { - let mut offset = 0usize; - for (i, l) in text.split_inclusive('\n').enumerate() { - if i == line { - return Ok(offset + character.min(l.len())); - } - offset += l.len(); - } - if line == 0 { - return Ok(character.min(text.len())); - } - Err(format!("line {line} beyond end of file")) -} diff --git a/ddd-mcp/src/govern_tools.rs b/ddd-mcp/src/govern_tools.rs deleted file mode 100644 index 37f9e88f..00000000 --- a/ddd-mcp/src/govern_tools.rs +++ /dev/null @@ -1,399 +0,0 @@ -//! M4 governance handlers — why, graph_query, declare, accept_risk. -//! -//! All of these run over the `.ddd/` store (PRD §8): `why` mirrors the -//! CLI's resolution as structured content; `declare_seam` files the seam -//! (warning when no verdict knowledge is declared); `declare_pattern` -//! fetches the catalog predicate's obligation list — unanswered -//! obligations are rejected; `accept_risk` files the record a -//! suppression must cite (the M2 `UNCITED_SUPPRESSION` check). -//! -//! Every declare tool takes `amend: true` to revise a filed entry -//! (`dec/ddd/amend-explicit-evidence-frozen`): judgement fields amend and -//! omitted ones are preserved, while identity and LSP-derived evidence -//! carry forward untouched. The flag is explicit in both directions, so a -//! create can never silently overwrite and an amend can never silently -//! create. - -use std::collections::BTreeMap; -use std::path::Path; - -use ddd_core::store::{load, STORE_DIR}; -use serde_json::{json, Value}; - -use crate::intercept::slug; -use crate::state::{opt_str, req_str, ServeState}; - -type ToolResult = Result; - -/// Resolve the create-vs-amend intent for `path`, loading the filed entry -/// when amending. `amend: true` requires an entry to exist; its absence -/// requires that none does — neither stands in for the other. -fn amend_target( - state: &ServeState, - path: &Path, - id: &str, - args: &Value, -) -> Result, String> { - let amend = args.get("amend").and_then(Value::as_bool).unwrap_or(false); - match (amend, path.exists()) { - (false, true) => Err(format!( - "{id} is already filed at {} — pass `amend: true` to revise it", - state.rel_display(path) - )), - (true, false) => Err(format!("nothing is filed at {id} — drop `amend` to file it")), - (true, true) => { - let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?; - serde_yaml::from_str::(&text) - .map(Some) - .map_err(|e| format!("{id} is filed at {} but unreadable: {e}", state.rel_display(path))) - } - (false, false) => Ok(None), - } -} - -/// `"amended"` or `"filed"`, for the tool's status field. -fn outcome(existing: bool) -> &'static str { - if existing { - "amended" - } else { - "filed" - } -} - -/// `ddd_why`: the governance chain behind an id. -pub fn why(state: &ServeState, args: &Value) -> ToolResult { - let id = req_str(args, "id")?; - let store = load(&state.root.join(STORE_DIR)); - // Detection re-reads the config sources per call; the same three-way - // resolution as the CLI, per PRD §8. - let detected = ddd_core::detect::detect(&state.root, store.config.as_ref(), &[]); - match ddd_core::why::resolve_why(&store, Some(&detected), &id) { - ddd_core::why::WhyResolution::Resolved(chain) => { - Ok(json!({"status": "ok", "id": id, "chain": chain})) - } - ddd_core::why::WhyResolution::DetectedUnfiled { namespace, rule_id, sources } => { - Ok(json!({ - "status": "detected-unfiled", "id": id, - "namespace": namespace, "rule_id": rule_id, "detected": sources, - "hint": "the rule is detected but no manifest entry maps it — file a decision plus a manifest entry, then why resolves it", - })) - } - ddd_core::why::WhyResolution::NotFound => Ok(json!({ - "status": "not-found", "id": id, - "hint": "no decision, claim, manifest rule, pattern, or seam carries this id, and detection does not know it either", - })), - } -} - -/// `ddd_graph_query`: read one entry family with simple filters. -pub fn graph_query(state: &ServeState, args: &Value) -> ToolResult { - let select = req_str(args, "select")?; - let id = opt_str(args, "id"); - let status = opt_str(args, "status"); - let predicate = opt_str(args, "predicate"); - let store = load(&state.root.join(STORE_DIR)); - let keep = |candidate: &str| id.as_deref().map(|w| w == candidate).unwrap_or(true); - let entries: Vec = match select.as_str() { - "predicates" => to_rows(store.predicates.iter().filter(|p| keep(&p.id))), - "claims" => to_rows(store.claims.iter().filter(|c| { - keep(&c.id) - && status.as_deref().map(|s| c.status.as_str() == s).unwrap_or(true) - && predicate - .as_deref() - .map(|p| c.predicate.as_deref() == Some(p)) - .unwrap_or(true) - })), - "decisions" => to_rows(store.decisions.iter().filter(|d| keep(&d.id))), - "patterns" => to_rows(store.patterns.iter().filter(|p| keep(&p.id))), - "seams" => to_rows(store.seams.iter().filter(|s| keep(&s.id))), - "seam-events" => to_rows(store.seam_events.iter().filter(|e| keep(&e.id))), - "manifests" => store - .manifests - .iter() - .map(|m| json!({"namespace": m.name, "rules": m.file.rules})) - .collect(), - other => { - return Err(format!( - "unknown selector `{other}` — one of predicates | claims | decisions | manifests | patterns | seams | seam-events" - )) - } - }; - Ok(json!({"status": "ok", "select": select, "count": entries.len(), "entries": entries})) -} - -fn to_rows<'a, T: serde::Serialize + 'a>(items: impl Iterator) -> Vec { - items.filter_map(|i| serde_json::to_value(i).ok()).collect() -} - -/// `ddd_declare_seam`: file the declaration, warn when the boundary -/// absorbs no demand. A `binding` block signs the exact transition the -/// declaration discharges (M8): its parent state must be committed — -/// a dirty file refuses to bind, naming the constraint. -pub fn declare_seam(state: &ServeState, args: &Value) -> ToolResult { - let id = req_str(args, "id")?; - if !id.starts_with("seam/") { - return Err("seam ids are `seam//`".to_string()); - } - let path = state.root.join(STORE_DIR).join("seams").join(format!("{}.yaml", slug(&id))); - let existing: Option = - amend_target(state, &path, &id, args)?; - let amending = existing.is_some(); - let mut seam = seam_entry(args, &id, existing)?; - let bound = match args.get("binding") { - Some(spec) => Some(attach_binding(state, &mut seam, spec)?), - None => None, - }; - write_yaml(&path, &seam)?; - let warning = seam.verdict_knowledge.trim().is_empty().then_some( - "verdict_knowledge is empty — this boundary declares seam cost with no demand absorbed (PRD §8); state what the boundary encodes about the verdict; pass `amend: true` to fill it in", - ); - Ok(json!({ - "status": outcome(amending), "id": id, "path": state.rel_display(&path), - "binding": bound, "warning": warning, - })) -} - -/// Verify and append a signed binding. Edit-time form (no -/// `base_revision`): the file must be clean against HEAD and `before` -/// must equal the committed parent's hash. Post-hoc form (explicit -/// `base_revision`, the CI-remedy path): `before` is verified against -/// that committed revision instead. -fn attach_binding( - state: &ServeState, - seam: &mut ddd_core::seam::SeamDeclaration, - spec: &Value, -) -> Result { - let get = |k: &str| { - spec.get(k) - .and_then(Value::as_str) - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_string) - .ok_or_else(|| format!("binding.{k} is required")) - }; - let (symbol, file, before, after) = (get("symbol")?, get("file")?, get("before")?, get("after")?); - let base_revision = match spec.get("base_revision").and_then(Value::as_str) { - Some(rev) if !rev.trim().is_empty() => { - let rev = ddd_core::gitrev::rev_parse(&state.root, rev.trim())?; - verify_before(state, &rev, &file, &before)?; - rev - } - _ => { - let head = ddd_core::gitrev::head(&state.root).map_err(|_| { - "the repository has no HEAD commit — a declaration binds against committed \ - parent state, and there is none to bind against (M8 ruling 2)" - .to_string() - })?; - let disk = std::fs::read(state.root.join(&file)).ok(); - let at_head = ddd_core::gitrev::bytes_at(&state.root, &head, &file)?; - if disk != at_head { - return Err(format!( - "{file} is dirty against HEAD ({}) — a declaration never binds uncommitted \ - parent state; commit the current state first (M8 ruling 2, the ledger's \ - construction-limit precedent)", - &head[..12.min(head.len())] - )); - } - verify_before(state, &head, &file, &before)?; - head - } - }; - let binding = - ddd_core::binding::SeamBinding::seal(&symbol, &file, &before, &after, &base_revision); - if !seam.bindings.iter().any(|b| b.hash == binding.hash) { - seam.bindings.push(binding.clone()); - } - seam.format = seam.format.max(2); - serde_json::to_value(&binding).map_err(|e| e.to_string()) -} - -/// The binding's `before` must equal the parent state actually committed -/// at `rev` — a declaration signs a reproducible transition or nothing. -fn verify_before(state: &ServeState, rev: &str, file: &str, before: &str) -> Result<(), String> { - let committed = ddd_core::gitrev::bytes_at(&state.root, rev, file)? - .map(|b| ddd_core::contracts::content_hash(&b)) - .unwrap_or_else(|| ddd_core::contracts::ABSENT.to_string()); - if committed != before { - return Err(format!( - "binding.before is {before} but {file} at {} is {committed} — the declaration \ - would not sign the transition it claims to discharge", - &rev[..12.min(rev.len())] - )); - } - Ok(()) -} - -/// Build the entry to write. On amend, judgement fields take the supplied -/// value and fall back to the filed one; `contract_location` plus -/// `metadata` carry forward untouched, because the correspondence rows are -/// only evidence while those stay machine-authored (`DDD-arch-04`). -fn seam_entry( - args: &Value, - id: &str, - existing: Option, -) -> Result { - let Some(prev) = existing else { - let mut metadata: BTreeMap = BTreeMap::new(); - if let Some(s) = opt_str(args, "symbol") { - metadata.insert("symbol".to_string(), s.into()); - } - return Ok(ddd_core::seam::SeamDeclaration { - format: 1, - id: id.to_string(), - boundary: req_str(args, "boundary")?, - verdict_knowledge: opt_str(args, "verdict_knowledge").unwrap_or_default(), - contract_location: req_str(args, "contract_location")?, - obligations: string_list(args, "obligations"), - metadata, - bindings: Vec::new(), - notes: opt_str(args, "notes"), - }); - }; - Ok(ddd_core::seam::SeamDeclaration { - format: prev.format, - id: id.to_string(), - boundary: opt_str(args, "boundary").unwrap_or(prev.boundary), - verdict_knowledge: opt_str(args, "verdict_knowledge").unwrap_or(prev.verdict_knowledge), - contract_location: prev.contract_location, - obligations: match args.get("obligations") { - Some(_) => string_list(args, "obligations"), - None => prev.obligations, - }, - metadata: prev.metadata, - bindings: prev.bindings, - notes: opt_str(args, "notes").or(prev.notes), - }) -} - -/// `ddd_declare_pattern`: obligations come from the catalog predicate; -/// unanswered ones reject the declaration. -pub fn declare_pattern(state: &ServeState, args: &Value) -> ToolResult { - let id = req_str(args, "id")?; - if !id.starts_with("pat/") { - return Err("pattern-instance ids are `pat//`".to_string()); - } - let path = state.root.join(STORE_DIR).join("patterns").join(format!("{}.yaml", slug(&id))); - let existing: Option = - amend_target(state, &path, &id, args)?; - let pattern = pattern_predicate_for(args, existing.as_ref())?; - let store = load(&state.root.join(STORE_DIR)); - let predicate = store - .predicates - .iter() - .find(|p| p.id == pattern) - .ok_or_else(|| format!("pattern predicate `{pattern}` is not in the catalog (ontology rule 5)"))?; - // Amending merges onto the filed answers, so one obligation can be - // refined without restating the rest; completeness is still checked. - let base = existing.as_ref().map(|e| e.obligations.clone()).unwrap_or_default(); - let answers = checked_answers(args, base, &predicate.obligations, &pattern)?; - let instance = opt_str(args, "instance") - .or_else(|| existing.as_ref().map(|e| e.instance.clone())) - .ok_or("instance is required when filing")?; - let entry = ddd_core::pattern::PatternInstance { - format: existing.as_ref().map(|e| e.format).unwrap_or(1), - id: id.clone(), - pattern_predicate: pattern, - instance: instance.clone(), - obligations: answers, - decision: existing.as_ref().and_then(|e| e.decision.clone()), - notes: opt_str(args, "notes").or_else(|| existing.as_ref().and_then(|e| e.notes.clone())), - }; - write_yaml(&path, &entry)?; - Ok(json!({ - "status": outcome(existing.is_some()), "id": id, "path": state.rel_display(&path), - })) -} - -/// The predicate a filed instance instantiates is identity, not judgement: -/// changing it would change which obligations apply, so it is a new id. -fn pattern_predicate_for( - args: &Value, - existing: Option<&ddd_core::pattern::PatternInstance>, -) -> Result { - let Some(prev) = existing else { - return req_str(args, "pattern"); - }; - match opt_str(args, "pattern") { - Some(p) if p != prev.pattern_predicate => Err(format!( - "a filed instance cannot change its predicate ({} -> {p}) — file it under a new id", - prev.pattern_predicate - )), - _ => Ok(prev.pattern_predicate.clone()), - } -} - -/// The obligation answers, rejected unless every catalog obligation has -/// a non-blank answer (PRD §8). -fn checked_answers( - args: &Value, - base: BTreeMap, - obligations: &[String], - pattern: &str, -) -> Result, String> { - let mut answers = base; - if let Some(supplied) = args.get("obligation_answers").and_then(Value::as_object) { - for (k, v) in supplied { - if let Some(s) = v.as_str() { - answers.insert(k.clone(), s.to_string()); - } - } - } - let unanswered: Vec<&str> = obligations - .iter() - .filter(|o| answers.get(*o).map(|a| a.trim().is_empty()).unwrap_or(true)) - .map(String::as_str) - .collect(); - if unanswered.is_empty() { - Ok(answers) - } else { - Err(format!( - "unanswered obligations for {pattern}: {} — answer every catalog obligation to declare the instance", - unanswered.join(", ") - )) - } -} - -/// `ddd_accept_risk`: the record a suppression cites (ontology rule 4). -pub fn accept_risk(state: &ServeState, args: &Value) -> ToolResult { - let diagnostic_id = req_str(args, "diagnostic_id")?; - let id = format!("risk/rule/{}", slug(&diagnostic_id)); - let path = state.root.join(STORE_DIR).join("decisions").join(format!("{}.yaml", slug(&id))); - let existing: Option = amend_target(state, &path, &id, args)?; - let amending = existing.is_some(); - let record = ddd_core::decision::Decision { - format: existing.as_ref().map(|e| e.format).unwrap_or(1), - id: id.clone(), - kind: ddd_core::decision::DecisionKind::RiskAcceptance, - title: format!("Risk accepted: {diagnostic_id} suppressed"), - rationale: field(args, "rationale", existing.as_ref().map(|e| e.rationale.clone()))?, - principal: field(args, "principal", existing.as_ref().map(|e| e.principal.clone()))?, - based_on: existing.as_ref().map(|e| e.based_on.clone()).unwrap_or_default(), - revisit_if: existing.as_ref().map(|e| e.revisit_if.clone()).unwrap_or_default(), - date: Some(chrono::Utc::now().format("%Y-%m-%d").to_string()), - notes: opt_str(args, "notes").or_else(|| existing.as_ref().and_then(|e| e.notes.clone())), - }; - write_yaml(&path, &record)?; - Ok(json!({ - "status": outcome(amending), "id": id, "path": state.rel_display(&path), - "next": format!("cite it from the manifest entry: suppression.risk_acceptance: {id}"), - })) -} - -/// A field that is required when filing and optional when amending. -fn field(args: &Value, key: &str, fallback: Option) -> Result { - opt_str(args, key) - .or(fallback) - .ok_or_else(|| format!("{key} is required when filing")) -} - -fn string_list(args: &Value, key: &str) -> Vec { - args.get(key) - .and_then(Value::as_array) - .map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()) - .unwrap_or_default() -} - -fn write_yaml(path: &std::path::Path, value: &T) -> Result<(), String> { - let yaml = serde_yaml::to_string(value).map_err(|e| e.to_string())?; - product_core::fileops::write_file_atomic(path, &yaml).map_err(|e| e.to_string()) -} diff --git a/ddd-mcp/src/intercept.rs b/ddd-mcp/src/intercept.rs deleted file mode 100644 index ea90c234..00000000 --- a/ddd-mcp/src/intercept.rs +++ /dev/null @@ -1,367 +0,0 @@ -//! The `apply_edit` interceptor — PRD §8 outcome semantics, exactly. -//! -//! Non-surface applies. Surface with a stored declaration whose signed -//! binding names this exact transition applies plus links (M8: matching -//! is by signature — subject symbol, before/after content hash, base -//! revision — never by session). Surface without one rejects with a -//! structured demand: LSP-derived facts pre-filled, the binding -//! pre-computed, judgment fields blank -//! (`dec/ddd/rejection-facts-prefilled`). A dirty parent state refuses to -//! bind at all. Mode per artifact class: `enforce` rejects, `warn` -//! applies with a warning, `off` skips classification. Every classified -//! surface outcome logs one row under `.ddd/seams/events/`. - -use std::path::Path; - -use ddd_lsp::adapter::Adapter; -use ddd_lsp::classify::classify_edit; -use ddd_lsp::host::Host; -use ddd_lsp::protocol::{flatten_symbols, position_params, to_uri}; -use ddd_lsp::surface::{ChangeKind, SurfaceEvent}; -use serde_json::{json, Value}; - -use crate::bind::{self, Transition}; -use crate::edits::{read_or_empty, resolve_new_text}; -use crate::state::{opt_str, req_str, ServeState}; - -pub fn apply_edit(state: &ServeState, args: &Value) -> Result { - let file = state.resolve_file(&req_str(args, "file")?); - let old_text = read_or_empty(&file)?; - let new_text = resolve_new_text(args, &old_text)?; - let config = ddd_core::config::load(&state.root).unwrap_or_default(); - let (adapter, flags) = { - let manager = state.manager.lock().map_err(|_| "host manager poisoned".to_string())?; - let adapter = manager - .resolve_adapter(opt_str(args, "language").as_deref(), Some(&file)) - .map_err(|e| e.to_string())?; - (adapter, manager.flags(adapter.language)) - }; - let mode = config.intercept_for(adapter.artifact_class).to_string(); - if mode == "off" { - product_core::fileops::write_file_atomic(&file, &new_text).map_err(|e| e.to_string())?; - if let Ok(mut manager) = state.manager.lock() { - if let Some(host) = manager.existing_host(adapter.language) { - let _ = host.overlay(&file, &new_text); - } - } - return Ok(json!({"status": "applied", "intercepted": false, "mode": "off"})); - } - if !adapter.hosted { - // A hostless producer (the HTML+CSS pair): classification runs on - // source text alone — no document to open, no references to count. - return intercept(state, None, adapter, &flags, &config, &mode, &file, &old_text, &new_text); - } - crate::lang_tools::with_ready_host(state, args, Some(&file), |host| { - intercept(state, Some(host), adapter, &flags, &config, &mode, &file, &old_text, &new_text) - }) -} - -/// The classified path: diff the symbol surfaces, decide per PRD §8. -/// The caller holds the manager lock through a `Some` host — nothing here -/// may lock the manager again. `None` is the hostless path: empty symbol -/// slices in (the facts fn parses text), no reference counts out. -#[allow(clippy::too_many_arguments)] -fn intercept( - state: &ServeState, - mut host: Option<&mut Host>, - adapter: &'static Adapter, - flags: &ddd_lsp::adapter::AdapterFlags, - config: &ddd_core::config::DddConfig, - mode: &str, - file: &Path, - old_text: &str, - new_text: &str, -) -> Result { - let (before, after) = match host.as_deref_mut() { - Some(h) => { - h.ensure_open(file).map_err(|e| e.to_string())?; - let before = symbols(h, file)?; - h.overlay(file, new_text).map_err(|e| e.to_string())?; - (before, symbols(h, file)?) - } - None => (Vec::new(), Vec::new()), - }; - let mut events = classify_edit(adapter, flags, old_text, &before, new_text, &after); - if let Some(enrich) = adapter.enrich { - enrich(&state.root, file, config, &mut events); - } - let surface: Vec = events.into_iter().filter(|e| e.surface).collect(); - if surface.is_empty() { - write_disk(file, new_text)?; - return Ok(json!({"status": "applied", "intercepted": true, "mode": mode, - "surface_events": 0})); - } - let counts = match host.as_deref_mut() { - Some(h) => reference_counts(h, file, old_text, new_text, &surface), - None => vec![None; surface.len()], - }; - let warning = (adapter.posture_warning)(file, flags); - let rel = state.rel_display(file); - let transition = Transition::of(&state.root, file, &rel, old_text, new_text); - let seams = load_seams(state); - if mode == "warn" { - let matches = bind::advisory_links(&seams, &surface, &transition); - let rows = - log_events(state, adapter, mode, &rel, &surface, &counts, "applied-warned", &matches)?; - write_disk(file, new_text)?; - return Ok(json!({ - "status": "applied", "intercepted": true, "mode": "warn", - "warning": "contract-surface change applied unverified — declare the seam with a signed binding (ddd_declare_seam) or switch this class to enforce", - "surface": demands(&surface, &counts, adapter, &rel, &transition), - "events_logged": rows, "posture_warning": warning, - })); - } - if !transition.committed_parent { - // Restore the host overlay; nothing may bind to a dirty parent. - if let Some(h) = host { - h.overlay(file, old_text).map_err(|e| e.to_string())?; - } - let rows = log_events(state, adapter, mode, &rel, &surface, &counts, "rejected", &[])?; - return Ok(json!({ - "status": "rejected", "mode": "enforce", - "reason": transition.dirty_reason(), - "events_logged": rows, "posture_warning": warning, - })); - } - let matches = bind::match_bindings(&seams, &surface, &transition); - if matches.iter().all(Option::is_some) { - let linked: Vec = matches.iter().flatten().cloned().collect(); - let rows = - log_events(state, adapter, mode, &rel, &surface, &counts, "applied-linked", &matches)?; - link_seam_metadata(state, &surface, &counts, &matches); - write_disk(file, new_text)?; - return Ok(json!({ - "status": "applied", "intercepted": true, "mode": "enforce", - "linked": linked, "events_logged": rows, "posture_warning": warning, - })); - } - // Reject: put the host back on the disk state, demand declarations. - if let Some(h) = host { - h.overlay(file, old_text).map_err(|e| e.to_string())?; - } - let unmatched: Vec = - matches.iter().enumerate().filter(|(_, m)| m.is_none()).map(|(i, _)| i).collect(); - let only_unmatched: Vec = - unmatched.iter().map(|&i| surface[i].clone()).collect(); - let unmatched_counts: Vec> = unmatched.iter().map(|&i| counts[i]).collect(); - let rows = - log_events(state, adapter, mode, &rel, &only_unmatched, &unmatched_counts, "rejected", &[])?; - Ok(json!({ - "status": "rejected", "mode": "enforce", - "reason": "contract-surface change without a stored declaration signing this transition (PRD §8, M8)", - "demands": demands(&only_unmatched, &unmatched_counts, adapter, &rel, &transition), - "next": "file ddd_declare_seam with the pre-computed `binding` (or ddd_declare_pattern) covering each demanded surface, then re-apply this edit", - "events_logged": rows, "posture_warning": warning, - })) -} - -fn load_seams(state: &ServeState) -> Vec { - ddd_core::store::load(&state.root.join(ddd_core::store::STORE_DIR)).seams -} - -fn write_disk(file: &Path, new_text: &str) -> Result<(), String> { - product_core::fileops::write_file_atomic(file, new_text).map_err(|e| e.to_string()) -} - -fn symbols(host: &mut Host, file: &Path) -> Result, String> { - let result = host - .request("textDocument/documentSymbol", json!({"textDocument": {"uri": to_uri(file)}})) - .map_err(|e| e.to_string())?; - Ok(flatten_symbols(&result)) -} - -/// References per event, queried on the side the symbol exists on: the -/// after-overlay for additions/changes, the before-state for removals. -fn reference_counts( - host: &mut Host, - file: &Path, - old_text: &str, - new_text: &str, - events: &[SurfaceEvent], -) -> Vec> { - let removed: Vec = events - .iter() - .enumerate() - .filter(|(_, e)| e.change == ChangeKind::Removed) - .map(|(i, _)| i) - .collect(); - let mut counts: Vec> = events - .iter() - .map(|e| { - if e.change == ChangeKind::Removed { - None - } else { - count_at(host, file, e.facts.sel_line, e.facts.sel_char) - } - }) - .collect(); - if !removed.is_empty() && host.overlay(file, old_text).is_ok() { - for i in removed { - counts[i] = count_at(host, file, events[i].facts.sel_line, events[i].facts.sel_char); - } - let _ = host.overlay(file, new_text); - } - counts -} - -fn count_at(host: &mut Host, file: &Path, line: u64, character: u64) -> Option { - let mut params = position_params(file, line, character); - params["context"] = json!({"includeDeclaration": false}); - host.request("textDocument/references", params) - .ok() - .map(|r| ddd_lsp::protocol::normalize_locations(&r).len() as u64) -} - -/// PRD §8 + settled question 3: facts pre-filled, judgment blank. The -/// `binding` block is the exact transition the declaration must sign — -/// copy it into `ddd_declare_seam` verbatim (M8). -fn demands( - events: &[SurfaceEvent], - counts: &[Option], - adapter: &Adapter, - rel_file: &str, - t: &Transition, -) -> Vec { - events - .iter() - .zip(counts) - .map(|(e, count)| { - json!({ - "surface": { - "file": rel_file, - "symbol": e.facts.name, - "container": e.facts.container, - "kind": e.facts.kind, - "change": e.change, - "signature": e.facts.signature, - "visibility": e.facts.visibility, - "reference_count": count, - "language": adapter.language, - "artifact_class": adapter.artifact_class, - "rule": e.rule, - "rule_claim": e.rule_claim, - "extra": e.facts.extra, - }, - "required": "a seam declaration (ddd_declare_seam) or pattern instance (ddd_declare_pattern) whose signed binding names this transition", - "template": { - "id": format!("seam/{}/{}", adapter.language, slug(&e.facts.name)), - "boundary": format!("{} {} in {}", e.facts.kind, e.facts.name, rel_file), - "contract_location": format!("{rel_file}#{}", e.facts.name), - "symbol": e.facts.name, - "verdict_knowledge": "", - "obligations": [], - "binding": { - "symbol": e.facts.name, - "file": rel_file, - "before": t.before, - "after": t.after, - "base_revision": t.head, - }, - }, - }) - }) - .collect() -} - -/// One correspondence row per surface event (PRD §8). -#[allow(clippy::too_many_arguments)] -fn log_events( - state: &ServeState, - adapter: &Adapter, - mode: &str, - rel_file: &str, - events: &[SurfaceEvent], - counts: &[Option], - outcome: &str, - matches: &[Option], -) -> Result, String> { - let ddd_dir = state.root.join(ddd_core::store::STORE_DIR); - let mut ids = Vec::new(); - for (i, event) in events.iter().enumerate() { - let seq = ddd_core::seam_event::next_seq(&ddd_dir); - let row = ddd_core::seam_event::SeamEvent { - format: 1, - id: format!("seam-event/{seq}"), - at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), - language: adapter.language.to_string(), - artifact_class: adapter.artifact_class.to_string(), - mode: mode.to_string(), - file: rel_file.to_string(), - symbol: event.facts.name.clone(), - container: event.facts.container.clone(), - kind: event.facts.kind.clone(), - change: change_name(event.change).to_string(), - visibility: event.facts.visibility.clone(), - signature: event.facts.signature.clone(), - reference_count: counts.get(i).copied().flatten(), - rule: event.rule.map(str::to_string), - rule_claim: event.rule_claim.map(str::to_string), - outcome: outcome.to_string(), - linked_declaration: matches.get(i).and_then(|m| m.clone()), - extra: event.facts.extra.clone(), - }; - ddd_core::seam_event::save(&ddd_dir, &row).map_err(|e| e.to_string())?; - ids.push(row.id); - } - Ok(ids) -} - -fn change_name(change: ChangeKind) -> &'static str { - match change { - ChangeKind::Added => "added", - ChangeKind::Removed => "removed", - ChangeKind::SignatureChanged => "signature-changed", - ChangeKind::DecoratorChanged => "decorator-changed", - ChangeKind::VisibilityChanged => "visibility-changed", - } -} - -/// Update matched seam declarations with the LSP-derived metadata, so the -/// declaration carries the correspondence facts (PRD §8). -fn link_seam_metadata( - state: &ServeState, - events: &[SurfaceEvent], - counts: &[Option], - matches: &[Option], -) { - for (i, m) in matches.iter().enumerate() { - let Some(decl_id) = m else { continue }; - if !decl_id.starts_with("seam/") { - continue; - } - let Some(event) = events.get(i) else { continue }; - let path = state - .root - .join(ddd_core::store::STORE_DIR) - .join("seams") - .join(format!("{}.yaml", slug(decl_id))); - let Ok(text) = std::fs::read_to_string(&path) else { continue }; - let Ok(mut seam) = serde_yaml::from_str::(&text) else { - continue; - }; - let meta = &mut seam.metadata; - let mut set = |k: &str, v: serde_yaml::Value| { - meta.insert(k.to_string(), v); - }; - set("symbol", event.facts.name.clone().into()); - set("kind", event.facts.kind.clone().into()); - set("signature", event.facts.signature.clone().into()); - set("visibility", event.facts.visibility.clone().into()); - if let Some(count) = counts.get(i).copied().flatten() { - set("reference_count", count.into()); - } - for (k, v) in &event.facts.extra { - set(k, v.clone().into()); - } - if let Ok(yaml) = serde_yaml::to_string(&seam) { - let _ = product_core::fileops::write_file_atomic(&path, &yaml); - } - } -} - -/// `seam/api/foo` → `seam-api-foo` (the on-disk filename form). -pub fn slug(id: &str) -> String { - id.chars() - .map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '-' }) - .collect() -} diff --git a/ddd-mcp/src/lang_tools.rs b/ddd-mcp/src/lang_tools.rs deleted file mode 100644 index 3a1052d4..00000000 --- a/ddd-mcp/src/lang_tools.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! M3 language-intelligence handlers — normalized LSP pass-through. -//! -//! Every handler routes by adapter, surfaces host loading as an explicit -//! `{status: "loading"}` result (never a hang, never a fake answer — -//! PRD §11), and returns plain JSON the agent can consume. Diagnostics -//! join to the `.ddd/` manifests on the same `(namespace, rule_id)` key -//! the SARIF path uses (PRD §7). - -use std::path::Path; - -use ddd_lsp::host::{Host, Readiness}; -use ddd_lsp::protocol::{ - flatten_symbols, kind_name, normalize_locations, position_params, to_uri, -}; -use serde_json::{json, Value}; - -use crate::state::{opt_str, opt_u64, req_str, ServeState}; - -type ToolResult = Result; - -/// Run `f` on the ready host for the request's language/file; a loading -/// host short-circuits into the explicit loading result. -pub fn with_ready_host( - state: &ServeState, - args: &Value, - file: Option<&Path>, - f: impl FnOnce(&mut Host) -> ToolResult, -) -> ToolResult { - let language = opt_str(args, "language"); - let mut manager = state.manager.lock().map_err(|_| "host manager poisoned".to_string())?; - let adapter = - manager.resolve_adapter(language.as_deref(), file).map_err(|e| e.to_string())?; - if !adapter.hosted { - return Err(format!( - "`{}` has no language server — the pair adapter classifies edits from source text; use ddd_apply_edit", - adapter.language - )); - } - let host = manager.host(adapter.language).map_err(|e| e.to_string())?; - match host.readiness().map_err(|e| e.to_string())? { - Readiness::Ready { .. } => f(host), - loading => Ok(json!({ - "status": "loading", - "language": adapter.language, - "readiness": serde_json::to_value(&loading).unwrap_or(Value::Null), - "notes": host.notes(), - })), - } -} - -fn position_of(args: &Value) -> Result<(u64, u64), String> { - let line = opt_u64(args, "line").ok_or("missing required argument `line`")?; - let character = opt_u64(args, "character").ok_or("missing required argument `character`")?; - Ok((line, character)) -} - -/// `ddd_find_symbol`: documentSymbol for a file, workspace/symbol for a query. -pub fn find_symbol(state: &ServeState, args: &Value) -> ToolResult { - let file = opt_str(args, "file").map(|f| state.resolve_file(&f)); - let query = opt_str(args, "query"); - if file.is_none() && query.is_none() { - return Err("give `file` (document symbols) or `query` (workspace search)".to_string()); - } - with_ready_host(state, args, file.as_deref(), |host| { - let result = match &file { - Some(path) => { - host.ensure_open(path).map_err(|e| e.to_string())?; - host.request( - "textDocument/documentSymbol", - json!({"textDocument": {"uri": to_uri(path)}}), - ) - .map_err(|e| e.to_string())? - } - None => host - .request("workspace/symbol", json!({"query": query})) - .map_err(|e| e.to_string())?, - }; - let symbols: Vec = flatten_symbols(&result) - .into_iter() - .map(|s| { - json!({ - "name": s.name, "kind": kind_name(s.kind), "container": s.container, - "line": s.sel_line, "character": s.sel_char, - }) - }) - .collect(); - Ok(json!({"status": "ok", "symbols": symbols})) - }) -} - -/// `ddd_references`: locations plus the count the seam metadata wants. -pub fn references(state: &ServeState, args: &Value) -> ToolResult { - let file = state.resolve_file(&req_str(args, "file")?); - let (line, character) = position_of(args)?; - with_ready_host(state, args, Some(&file), |host| { - host.ensure_open(&file).map_err(|e| e.to_string())?; - let mut params = position_params(&file, line, character); - params["context"] = json!({"includeDeclaration": true}); - let result = host.request("textDocument/references", params).map_err(|e| e.to_string())?; - let locations: Vec = normalize_locations(&result) - .into_iter() - .map(|l| json!({"file": l.file, "line": l.line, "character": l.character})) - .collect(); - Ok(json!({"status": "ok", "count": locations.len(), "references": locations})) - }) -} - -/// `ddd_hover`: the hover contents as one markdown string. -pub fn hover(state: &ServeState, args: &Value) -> ToolResult { - let file = state.resolve_file(&req_str(args, "file")?); - let (line, character) = position_of(args)?; - with_ready_host(state, args, Some(&file), |host| { - host.ensure_open(&file).map_err(|e| e.to_string())?; - let result = host - .request("textDocument/hover", position_params(&file, line, character)) - .map_err(|e| e.to_string())?; - Ok(json!({"status": "ok", "contents": hover_text(&result)})) - }) -} - -fn hover_text(result: &Value) -> String { - let contents = result.get("contents").unwrap_or(&Value::Null); - match contents { - Value::String(s) => s.clone(), - Value::Object(o) => o.get("value").and_then(Value::as_str).unwrap_or("").to_string(), - Value::Array(items) => items - .iter() - .map(|i| match i { - Value::String(s) => s.clone(), - other => other.get("value").and_then(Value::as_str).unwrap_or("").to_string(), - }) - .collect::>() - .join("\n"), - _ => String::new(), - } -} - -/// `ddd_signature`: normalized signature help. -pub fn signature(state: &ServeState, args: &Value) -> ToolResult { - let file = state.resolve_file(&req_str(args, "file")?); - let (line, character) = position_of(args)?; - with_ready_host(state, args, Some(&file), |host| { - host.ensure_open(&file).map_err(|e| e.to_string())?; - let result = host - .request("textDocument/signatureHelp", position_params(&file, line, character)) - .map_err(|e| e.to_string())?; - let signatures: Vec = result - .get("signatures") - .and_then(Value::as_array) - .map(|sigs| { - sigs.iter() - .map(|s| json!({"label": s.get("label").and_then(Value::as_str).unwrap_or("")})) - .collect() - }) - .unwrap_or_default(); - Ok(json!({"status": "ok", "signatures": signatures})) - }) -} - -/// `ddd_rename`: compute the workspace edit; application goes through -/// `ddd_apply_edit` so the interceptor sees every mutation. -pub fn rename(state: &ServeState, args: &Value) -> ToolResult { - let file = state.resolve_file(&req_str(args, "file")?); - let (line, character) = position_of(args)?; - let new_name = req_str(args, "new_name")?; - with_ready_host(state, args, Some(&file), |host| { - host.ensure_open(&file).map_err(|e| e.to_string())?; - let mut params = position_params(&file, line, character); - params["newName"] = json!(new_name); - let result = host.request("textDocument/rename", params).map_err(|e| e.to_string())?; - Ok(json!({ - "status": "computed", - "applied": false, - "note": "apply each file's edits through ddd_apply_edit — mutations only land through the interceptor", - "workspace_edit": workspace_edit_files(&result), - })) - }) -} - -/// Normalize a WorkspaceEdit's `changes` map into per-file edit lists. -fn workspace_edit_files(result: &Value) -> Vec { - let mut files = Vec::new(); - if let Some(changes) = result.get("changes").and_then(Value::as_object) { - for (uri, edits) in changes { - files.push(json!({ - "file": ddd_lsp::protocol::from_uri(uri), - "edits": edits, - })); - } - } - files -} - -/// `ddd_warmup`: start hosts, optionally block for readiness. -pub fn warmup(state: &ServeState, args: &Value) -> ToolResult { - let languages: Option> = args - .get("languages") - .and_then(Value::as_array) - .map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()); - let wait_ms = opt_u64(args, "wait_ms").unwrap_or(0); - let mut manager = state.manager.lock().map_err(|_| "host manager poisoned".to_string())?; - let mut rows = Vec::new(); - for (language, first) in manager.warm_all(languages.as_deref()) { - let readiness = match first { - Ok(r) if wait_ms > 0 => manager - .host(&language) - .and_then(|h| h.wait_ready(std::time::Duration::from_millis(wait_ms))) - .unwrap_or(r), - Ok(r) => r, - Err(e) => { - rows.push(json!({"language": language, "error": e.to_string()})); - continue; - } - }; - let notes = manager.host(&language).map(|h| h.notes()).unwrap_or_default(); - rows.push(json!({ - "language": language, - "readiness": serde_json::to_value(&readiness).unwrap_or(Value::Null), - "notes": notes, - })); - } - Ok(json!({"status": "ok", "hosts": rows})) -} - -/// `ddd_diagnostics`: pull (or fall back to published) diagnostics, each -/// joined to its manifest namespace by rule id — the SARIF join. -pub fn diagnostics(state: &ServeState, args: &Value) -> ToolResult { - let file = state.resolve_file(&req_str(args, "file")?); - let store = ddd_core::store::load(&state.root.join(ddd_core::store::STORE_DIR)); - with_ready_host(state, args, Some(&file), |host| { - host.ensure_open(&file).map_err(|e| e.to_string())?; - let items = pull_or_published(host, &file)?; - let joined: Vec = items.iter().map(|d| join_governance(&store, d)).collect(); - Ok(json!({"status": "ok", "diagnostics": joined})) - }) -} - -/// Pull diagnostics; hosts that only push get a short published-state poll. -fn pull_or_published(host: &mut Host, file: &Path) -> Result, String> { - let pulled = host - .request("textDocument/diagnostic", json!({"textDocument": {"uri": to_uri(file)}})) - .ok() - .and_then(|r| r.get("items").and_then(Value::as_array).cloned()); - if let Some(items) = pulled { - if !items.is_empty() { - return Ok(items); - } - } - for _ in 0..20 { - if let Some(params) = host.published_diagnostics(file).map_err(|e| e.to_string())? { - return Ok(params - .get("diagnostics") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default()); - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - Ok(Vec::new()) -} - -/// The M2 join: `(namespace_for(source, rule_id), rule_id)` → manifest -/// entry → decision | UNGOVERNED | no entry. -fn join_governance(store: &ddd_core::DddStore, d: &Value) -> Value { - let rule_id = d - .get("code") - .map(|c| match c { - Value::String(s) => s.clone(), - Value::Object(o) => { - o.get("value").and_then(Value::as_str).unwrap_or_default().to_string() - } - other => other.to_string(), - }) - .unwrap_or_default(); - let source = d.get("source").and_then(Value::as_str).unwrap_or_default(); - let namespace = ddd_core::detect::namespace_for(source, &rule_id); - let entry = store - .manifests - .iter() - .find(|s| s.name == namespace) - .and_then(|s| s.file.rules.iter().find(|r| r.rule_id == rule_id)); - let governance = match entry { - Some(rule) => match (&rule.decision, &rule.governance) { - (Some(d), _) => json!({"decision": d, "resolve": format!("ddd why {rule_id}")}), - (None, Some(g)) => json!({"marker": g}), - (None, None) => json!({"marker": "no decision (ontology rule 4)"}), - }, - None => json!({"marker": "no manifest entry — UNGOVERNED if it fires (run ddd diff)"}), - }; - json!({ - "rule_id": rule_id, - "namespace": namespace, - "message": d.get("message").cloned().unwrap_or(Value::Null), - "severity": d.get("severity").cloned().unwrap_or(Value::Null), - "range": d.get("range").cloned().unwrap_or(Value::Null), - "source": source, - "governance": governance, - }) -} diff --git a/ddd-mcp/src/lib.rs b/ddd-mcp/src/lib.rs deleted file mode 100644 index a2e7b165..00000000 --- a/ddd-mcp/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! The `ddd_*` MCP tool namespace served through product-mcp's plumbing. -//! -//! PRD §5.1/§8: language-intelligence tools (M3) plus the interceptor -//! with governance tools (M4), dispatched against the repo-local `.ddd/` -//! graph plus per-language LSP hosts. The core here never names a -//! language beyond routing into `ddd-lsp`'s adapter registry. - -pub mod bind; -pub mod dispatch; -pub mod edits; -pub mod govern_tools; -pub mod intercept; -pub mod lang_tools; -pub mod serve; -pub mod state; -pub mod tools; - -pub use serve::serve_stdio; diff --git a/ddd-mcp/src/serve.rs b/ddd-mcp/src/serve.rs deleted file mode 100644 index ef2556ed..00000000 --- a/ddd-mcp/src/serve.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! `ddd serve` — the MCP surface over product-mcp's stdio plumbing. -//! -//! Builds a registry carrying only the `ddd_*` tools (a governed C#/Bicep -//! repo's agent has no use for the `product_*` framework surface) with a -//! dispatcher bound to one [`ServeState`] — the per-language hosts live -//! for the process; declaration matching reads the stored graph (M8). - -use std::path::PathBuf; -use std::sync::Arc; - -use product_core::error::Result; -use product_mcp::registry::{CustomDispatch, ToolRegistry}; - -use crate::state::ServeState; - -/// The registry `ddd serve` runs (also the seam tests drive directly). -pub fn build_registry(root: PathBuf) -> (Arc, ToolRegistry) { - let state = Arc::new(ServeState::new(root.clone())); - let dispatch_state = state.clone(); - let dispatch: CustomDispatch = - Arc::new(move |name, args, _root| crate::dispatch::dispatch(&dispatch_state, name, args)); - let registry = - ToolRegistry::with_tools(root, true, "ddd-mcp", crate::tools::build(), dispatch); - (state, registry) -} - -/// Serve MCP over stdio until EOF. -pub fn serve_stdio(root: PathBuf) -> Result<()> { - let (_state, registry) = build_registry(root); - product_mcp::stdio::run_stdio_registry(registry, None) -} diff --git a/ddd-mcp/src/state.rs b/ddd-mcp/src/state.rs deleted file mode 100644 index b7558128..00000000 --- a/ddd-mcp/src/state.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Serve-time shared state — the per-language LSP hosts. -//! -//! One [`ServeState`] lives for the whole `ddd serve` process. The host -//! manager holds the per-language children. There is no session log: M8 -//! retired same-session declaration matching (`DDD-arch-09` — it -//! discharged whatever change next touched the symbol); the interceptor -//! matches against *stored* seam declarations whose signed bindings name -//! the exact transition. - -use std::path::{Path, PathBuf}; -use std::sync::Mutex; - -use ddd_lsp::HostManager; -use serde_json::Value; - -pub struct ServeState { - pub root: PathBuf, - pub manager: Mutex, -} - -impl ServeState { - pub fn new(root: PathBuf) -> Self { - let manager = Mutex::new(HostManager::new(root.clone())); - Self { root, manager } - } - - /// Resolve a tool `file` argument against the repo root. - pub fn resolve_file(&self, rel_or_abs: &str) -> PathBuf { - let p = Path::new(rel_or_abs); - if p.is_absolute() { - p.to_path_buf() - } else { - self.root.join(p) - } - } - - /// A path repo-relative for display plus event rows. - pub fn rel_display(&self, path: &Path) -> String { - path.strip_prefix(&self.root) - .unwrap_or(path) - .to_string_lossy() - .replace('\\', "/") - } -} - -/// Required string argument, trimmed, non-empty. -pub fn req_str(args: &Value, key: &str) -> Result { - opt_str(args, key).ok_or_else(|| format!("missing required argument `{key}`")) -} - -/// Optional string argument (absent, null, or blank → `None`). -pub fn opt_str(args: &Value, key: &str) -> Option { - args.get(key) - .and_then(Value::as_str) - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_string) -} - -/// Optional string argument with no normalisation at all. -/// -/// [`opt_str`] trims and treats blank as absent, which is right for names, -/// ids, and prose. It is wrong for anything whose value *is* the payload: -/// `apply_edit`'s `new_text` is a whole file, and trimming it silently -/// deletes the trailing newline plus any leading blank line, on every -/// language. Callers holding content reach for this instead. -pub fn raw_str(args: &Value, key: &str) -> Option { - args.get(key).and_then(Value::as_str).map(str::to_string) -} - -/// Optional integer argument. -pub fn opt_u64(args: &Value, key: &str) -> Option { - args.get(key).and_then(Value::as_u64) -} diff --git a/ddd-mcp/src/tools.rs b/ddd-mcp/src/tools.rs deleted file mode 100644 index 114be7c7..00000000 --- a/ddd-mcp/src/tools.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! The `ddd_*` tool definitions — names, schemas, write flags (PRD §8). - -use product_mcp::tools::ToolDef; -use serde_json::{json, Value}; - -fn tool(name: &str, description: &str, write: bool, props: Value, required: &[&str]) -> ToolDef { - ToolDef { - name: name.to_string(), - description: description.to_string(), - requires_write: write, - input_schema: json!({ - "type": "object", - "properties": props, - "required": required, - }), - } -} - -fn file_props(extra: Value) -> Value { - let mut props = json!({ - "file": {"type": "string", "description": "File path, repo-root-relative or absolute"}, - "language": {"type": "string", "description": "Adapter override (csharp | bicep); inferred from the file extension when omitted"}, - }); - if let (Some(base), Some(add)) = (props.as_object_mut(), extra.as_object()) { - for (k, v) in add { - base.insert(k.clone(), v.clone()); - } - } - props -} - -fn position_props() -> Value { - file_props(json!({ - "line": {"type": "integer", "description": "Zero-based line"}, - "character": {"type": "integer", "description": "Zero-based character"}, - })) -} - -/// Every tool `ddd serve` exposes, M3 language intelligence plus M4 -/// governance, in the order agents see them. -pub fn build() -> Vec { - vec![ - find_symbol(), - references(), - hover(), - diagnostics(), - signature(), - rename(), - apply_edit(), - warmup(), - why(), - graph_query(), - declare_seam(), - declare_pattern(), - accept_risk(), - ] -} - -fn find_symbol() -> ToolDef { - tool( - "ddd_find_symbol", - "Find symbols: in one file (documentSymbol) or across the workspace (query).", - false, - file_props(json!({"query": {"type": "string", "description": "Workspace symbol query (used when no file is given)"}})), - &[], - ) -} - -fn references() -> ToolDef { - tool( - "ddd_references", - "References to the symbol at a position.", - false, - position_props(), - &["file", "line", "character"], - ) -} - -fn hover() -> ToolDef { - tool( - "ddd_hover", - "Hover information at a position.", - false, - position_props(), - &["file", "line", "character"], - ) -} - -fn diagnostics() -> ToolDef { - tool( - "ddd_diagnostics", - "Diagnostics for a file, each joined to its .ddd/ manifest governance by rule id.", - false, - file_props(json!({})), - &["file"], - ) -} - -fn signature() -> ToolDef { - tool( - "ddd_signature", - "Signature help at a position.", - false, - position_props(), - &["file", "line", "character"], - ) -} - -fn rename() -> ToolDef { - tool( - "ddd_rename", - "Compute (not apply) the workspace edit renaming the symbol at a position; apply through ddd_apply_edit so the interceptor sees it.", - false, - file_props(json!({ - "line": {"type": "integer"}, "character": {"type": "integer"}, - "new_name": {"type": "string"}, - })), - &["file", "line", "character", "new_name"], - ) -} - -fn apply_edit() -> ToolDef { - tool( - "ddd_apply_edit", - "Apply an edit to a file through the contract-surface interceptor (PRD §8): non-surface applies; surface needs a same-session seam/pattern declaration or is rejected with a structured demand.", - true, - file_props(json!({ - "new_text": {"type": "string", "description": "Full replacement content (or use `edits`)"}, - "edits": {"type": "array", "description": "Range edits [{range:{start:{line,character},end:{line,character}}, new_text}]", - "items": {"type": "object"}}, - })), - &["file"], - ) -} - -fn warmup() -> ToolDef { - tool( - "ddd_warmup", - "Start the language hosts, optionally waiting for readiness (wait_ms).", - false, - json!({ - "languages": {"type": "array", "items": {"type": "string"}}, - "wait_ms": {"type": "integer", "description": "Block up to this long for readiness"}, - }), - &[], - ) -} - -fn why() -> ToolDef { - tool( - "ddd_why", - "Resolve a diagnostic, pattern, seam, claim, or decision id to its governance chain.", - false, - json!({"id": {"type": "string"}}), - &["id"], - ) -} - -fn graph_query() -> ToolDef { - tool( - "ddd_graph_query", - "Read the .ddd/ graph: select predicates | claims | decisions | manifests | patterns | seams | seam-events, filtered by id/status/predicate.", - false, - json!({ - "select": {"type": "string"}, - "id": {"type": "string", "description": "Exact id filter"}, - "status": {"type": "string", "description": "Claim status filter"}, - "predicate": {"type": "string", "description": "Predicate id filter (claims)"}, - }), - &["select"], - ) -} - -fn declare_seam() -> ToolDef { - tool( - "ddd_declare_seam", - "File a seam declaration (boundary + verdict_knowledge + contract_location). Empty verdict_knowledge files with a seam-cost-without-demand warning. Pass amend:true to revise a filed seam — boundary and contract_location are required only when filing. Pass `binding` (copy it from the rejection demand) to sign the exact transition this declaration discharges; a dirty parent state refuses to bind.", - true, - json!({ - "id": {"type": "string", "description": "seam//"}, - "boundary": {"type": "string"}, - "verdict_knowledge": {"type": "string", "description": "What the boundary encodes about the verdict — author this; it is never pre-filled"}, - "contract_location": {"type": "string", "description": "Required when filing; frozen on amend"}, - "obligations": {"type": "array", "items": {"type": "string"}}, - "symbol": {"type": "string", "description": "The symbol this seam covers (links interceptor demands)"}, - "binding": {"type": "object", "description": "The signed transition this declaration discharges: {symbol, file, before, after[, base_revision]} — pre-filled on the rejection demand; base_revision defaults to HEAD (the file must be clean against it); pass it explicitly only for the post-hoc CI-remedy path over an already-committed transition"}, - "notes": {"type": "string"}, - "amend": {"type": "boolean", "description": "Revise the filed seam; omitted fields keep their filed values, and contract_location plus LSP-derived metadata are frozen"}, - }), - &["id"], - ) -} - -fn declare_pattern() -> ToolDef { - tool( - "ddd_declare_pattern", - "File a pattern instance against a catalog pattern predicate; every catalog obligation must be answered. Pass amend:true to revise a filed instance — answers merge onto the filed ones, so a single obligation can be refined alone.", - true, - json!({ - "id": {"type": "string", "description": "pat//"}, - "pattern": {"type": "string", "description": "The catalog pattern predicate id; required when filing, and a filed instance cannot change it"}, - "instance": {"type": "string", "description": "Where the instance lives"}, - "obligation_answers": {"type": "object", "description": "Answers keyed by the catalog obligation names"}, - "notes": {"type": "string"}, - "amend": {"type": "boolean", "description": "Revise the filed instance; obligation answers merge onto the filed set and completeness is re-checked"}, - }), - &["id"], - ) -} - -fn accept_risk() -> ToolDef { - tool( - "ddd_accept_risk", - "File the risk-acceptance record a suppression must cite (M2 UNCITED_SUPPRESSION). Pass amend:true to revise a filed record.", - true, - json!({ - "diagnostic_id": {"type": "string"}, - "rationale": {"type": "string"}, - "principal": {"type": "string", "description": "A named human; never a model identity"}, - "notes": {"type": "string"}, - "amend": {"type": "boolean", "description": "Revise the filed record; omitted fields keep their filed values"}, - }), - &["diagnostic_id"], - ) -} diff --git a/ddd-mcp/tests/rust_governed.rs b/ddd-mcp/tests/rust_governed.rs deleted file mode 100644 index 347bca69..00000000 --- a/ddd-mcp/tests/rust_governed.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! M6 acceptance: the interception loop over real rust-analyzer. -//! -//! Every policy row is exercised through the interceptor on the fixture -//! crate, once from the side it claims is boundary-forming and once from the -//! neighbouring side it claims is not. Then the full enforce-mode loop — -//! reject, declare, re-apply, correspondence row — on a `pub` item and on a -//! trait-impl addition, which is the row with no `pub` keyword to key on. - -use std::path::{Path, PathBuf}; - -use serde_json::{json, Value}; - -/// The adapter's own resolved host command as a YAML flow sequence, with the -/// loud failure a missing toolchain component deserves. -fn host_command_yaml() -> String { - let command = ddd_lsp::adapter::rust::host_command(); - let program = command.first().cloned().unwrap_or_default(); - assert!( - std::process::Command::new(&program).arg("--version").output().is_ok(), - "cannot run `{program}`. rust-analyzer is a pinned component of this toolchain \ - (rust-toolchain.toml) — install it with `rustup component add rust-analyzer`." - ); - serde_json::to_string(&command).unwrap_or_else(|_| "[]".to_string()) -} - -fn fixture(rel: &str) -> String { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../ddd-cli/tests/fixtures/rust") - .join(rel); - std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) -} - -/// Run `git` in `root`, asserting success. -fn git(root: &Path, args: &[&str]) { - let out = std::process::Command::new("git") - .current_dir(root) - .args(args) - .output() - .unwrap_or_else(|e| panic!("git {args:?}: {e}")); - assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); -} - -/// Commit the whole tree, so every governed file's parent state is -/// committed — the precondition for binding (M8 ruling 2). -fn commit_all(root: &Path) { - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "init"]); -} - -/// A governed fixture crate: `.ddd/` scaffold, format-3 config binding the -/// Rust adapter to the real host, the `code` class at `mode` — all -/// committed, because enforce mode only binds against committed parent -/// state (M8). -fn repo(mode: &str) -> (tempfile::TempDir, product_mcp::ToolRegistry) { - let tmp = tempfile::tempdir().expect("tempdir"); - ddd_core::init::apply_init(&ddd_core::init::plan_init(tmp.path())).expect("init"); - std::fs::create_dir_all(tmp.path().join("src")).expect("mkdir"); - std::fs::write(tmp.path().join("Cargo.toml"), fixture("Cargo.toml.txt")).expect("manifest"); - std::fs::write(tmp.path().join("src/lib.rs"), fixture("lib.rs")).expect("lib"); - std::fs::write(tmp.path().join("src/other.rs"), fixture("other.rs")).expect("other"); - std::fs::write( - tmp.path().join(".ddd/config.yaml"), - format!( - "format: 3\nintercept: warn\nignore: []\nintercept_by_class:\n code: {mode}\nadapter:\n rust:\n command: {}\n", - host_command_yaml() - ), - ) - .expect("config"); - git(tmp.path(), &["init", "-q"]); - git(tmp.path(), &["config", "user.email", "t@example.com"]); - git(tmp.path(), &["config", "user.name", "T"]); - commit_all(tmp.path()); - let (_state, registry) = ddd_mcp::serve::build_registry(tmp.path().to_path_buf()); - (tmp, registry) -} - -fn call(registry: &product_mcp::ToolRegistry, name: &str, args: Value) -> Value { - registry.call_tool(name, &args).unwrap_or_else(|e| panic!("{name}: {e}")) -} - -fn warm(registry: &product_mcp::ToolRegistry) { - let out = call(registry, "ddd_warmup", json!({"wait_ms": 120_000, "languages": ["rust"]})); - let host = &out["hosts"][0]; - assert_eq!(host["readiness"]["state"], "ready", "{out}"); -} - -fn source(root: &Path) -> String { - std::fs::read_to_string(root.join("src/lib.rs")).expect("read lib.rs") -} - -fn apply(registry: &product_mcp::ToolRegistry, new_text: &str) -> Value { - call(registry, "ddd_apply_edit", json!({"file": "src/lib.rs", "new_text": new_text})) -} - -/// The demand raised for `symbol`, or `None` when the edit raised none. -fn demand_for<'a>(result: &'a Value, symbol: &str) -> Option<&'a Value> { - let list = result.get("demands").or_else(|| result.get("surface"))?.as_array()?; - list.iter().find(|d| d["surface"]["symbol"] == symbol) -} - -fn event_rows(root: &Path) -> Vec { - let dir = root.join(".ddd/seams/events"); - let mut paths: Vec<_> = std::fs::read_dir(&dir) - .map(|rd| rd.flatten().map(|e| e.path()).collect()) - .unwrap_or_else(|_| Vec::new()); - paths.sort(); - paths - .iter() - .map(|p| { - let text = std::fs::read_to_string(p).expect("row"); - serde_yaml::from_str::(&text).expect("yaml") - }) - .collect() -} - -// ------------------------------------------------------- the row sweep - -/// Warn mode applies every edit and reports the classification, so one host -/// can walk the whole table. Each case names the row it must select. -#[test] -fn every_policy_row_triggers_on_the_fixture_and_its_neighbour_does_not() { - let (tmp, registry) = repo("warn"); - warm(®istry); - let base = source(tmp.path()); - - // Each case: (description, the edit, symbol, expected row, expected surface). - let cases: Vec<(&str, String, &str, &str, bool)> = vec![ - ( - "a new pub fn", - base.replace("pub fn describe(", "pub fn added(v: u32) -> u32 { v }\n\npub fn describe("), - "added", - "rs-add-exposed", - true, - ), - ( - "a new private fn", - base.replace("fn internal_helper(", "fn also_private(v: u32) -> u32 { v }\n\nfn internal_helper("), - "also_private", - "rs-private-nonsurface", - false, - ), - ( - "a signature change on a pub fn", - base.replace("pub fn describe(colour: &Colour)", "pub fn describe(colour: &Colour, terse: bool)"), - "describe", - "rs-signature-exposed", - true, - ), - ( - "a signature change on a private fn", - base.replace("fn internal_helper(value: u32)", "fn internal_helper(value: u64)"), - "internal_helper", - "rs-private-nonsurface", - false, - ), - ( - "removing a pub fn", - base.replace( - "pub fn describe(colour: &Colour) -> &'static str {\n match colour {\n Colour::Red => \"red\",\n Colour::Green => \"green\",\n }\n}\n", - "", - ), - "describe", - "rs-remove-exposed", - true, - ), - ( - "demoting a pub fn", - base.replace("pub fn describe(", "fn describe("), - "describe", - "rs-visibility-boundary", - true, - ), - ( - "a new member on a public trait", - base.replace( - "pub trait Greeter {\n fn greet(&self) -> String;", - "pub trait Greeter {\n fn greet(&self) -> String;\n fn wave(&self) -> String;", - ), - "wave", - "rs-trait-member", - true, - ), - ( - "a new member on a private trait", - base.replace( - "trait Internal {\n fn tally(&self) -> u32;", - "trait Internal {\n fn tally(&self) -> u32;\n fn recount(&self) -> u32;", - ), - "recount", - "rs-private-nonsurface", - false, - ), - ( - "a supertrait bound on a public trait", - base.replace("pub trait Greeter {", "pub trait Greeter: Clone {"), - "Greeter", - "rs-trait-definition", - true, - ), - ( - "a new variant on a public enum", - base.replace("pub enum Colour {\n Red,", "pub enum Colour {\n Red,\n Blue,"), - "Blue", - "rs-add-exposed", - true, - ), - ( - "a new variant on a private enum", - base.replace("enum Mode {\n _On,", "enum Mode {\n _On,\n _Idle,"), - "_Idle", - "rs-private-nonsurface", - false, - ), - ( - "a derive change on a public struct", - base.replace("#[derive(Clone)]\npub struct Person", "#[derive(Clone, Debug)]\npub struct Person"), - "Person", - "rs-derive-change", - true, - ), - ( - "a pub(crate) signature change, default posture", - base.replace("pub(crate) fn age(&self) -> u32", "pub(crate) fn age(&self) -> u64"), - "age", - "rs-crate-visible-nonsurface", - false, - ), - ( - "a new trait impl on a public type", - base.replace( - "pub enum Colour {", - "impl std::fmt::Debug for Person {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.name) }\n}\n\npub enum Colour {", - ), - "impl std::fmt::Debug for Person", - "rs-trait-impl", - true, - ), - ( - "a new inherent impl on a public type", - base.replace( - "pub enum Colour {", - "impl Colour {\n fn shade(&self) -> u32 { 0 }\n}\n\npub enum Colour {", - ), - "impl Colour", - "rs-add-exposed", - true, - ), - ( - "an impl whose trait and self type are both declared elsewhere", - base.replace( - "pub enum Colour {", - "impl other::Remote for other::Far {\n fn ping(&self) -> u32 { 0 }\n}\n\npub enum Colour {", - ), - "impl other::Remote for other::Far", - "rs-trait-impl-unresolved", - true, - ), - ]; - - let mut covered: Vec<&str> = Vec::new(); - for (what, edited, symbol, expected_row, expected_surface) in &cases { - assert_ne!(&edited[..], &base[..], "the edit for `{what}` changed nothing"); - let result = apply(®istry, edited); - assert_eq!(result["status"], "applied", "{what}: {result}"); - match demand_for(&result, symbol) { - Some(d) => { - assert!(*expected_surface, "{what}: expected no demand, got {d}"); - assert_eq!(d["surface"]["rule"], json!(expected_row), "{what}: {d}"); - } - None => assert!( - !*expected_surface, - "{what}: expected row {expected_row} on `{symbol}`, got {result}" - ), - } - covered.push(expected_row); - // Put the fixture back for the next case. - apply(®istry, &base); - } - - for row in [ - "rs-add-exposed", - "rs-signature-exposed", - "rs-remove-exposed", - "rs-visibility-boundary", - "rs-trait-member", - "rs-trait-definition", - "rs-derive-change", - "rs-trait-impl", - "rs-trait-impl-unresolved", - "rs-crate-visible-nonsurface", - "rs-private-nonsurface", - ] { - assert!(covered.contains(&row), "no case demonstrated {row}"); - } -} - -#[test] -fn the_crate_visible_row_flips_with_the_library_posture() { - let (tmp, registry) = repo("warn"); - std::fs::write( - tmp.path().join(".ddd/config.yaml"), - format!( - "format: 3\nintercept: warn\nignore: []\nintercept_by_class:\n code: warn\nadapter:\n rust:\n command: {}\n internal_is_surface: true\n", - host_command_yaml() - ), - ) - .expect("config"); - let (_state, registry2) = ddd_mcp::serve::build_registry(tmp.path().to_path_buf()); - drop(registry); - warm(®istry2); - let base = source(tmp.path()); - let edited = base.replace("pub(crate) fn age(&self) -> u32", "pub(crate) fn age(&self) -> u64"); - let result = apply(®istry2, &edited); - let d = demand_for(&result, "age").unwrap_or_else(|| panic!("no demand: {result}")); - assert_eq!(d["surface"]["rule"], json!("rs-crate-visible-surface")); - assert_eq!(d["surface"]["visibility"], json!("pub(crate)")); -} - -// -------------------------------------------------- the interception loop - -/// Reject → declare → re-apply → correspondence row, on a `pub` item. -#[test] -fn a_pub_item_is_rejected_until_a_seam_is_declared() { - let (tmp, registry) = repo("enforce"); - warm(®istry); - let base = source(tmp.path()); - let edited = - base.replace("pub fn describe(", "pub fn added(v: u32) -> u32 { v }\n\npub fn describe("); - - let rejected = apply(®istry, &edited); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - assert_eq!(source(tmp.path()), base, "a rejected edit must not reach the disk"); - let demand = demand_for(&rejected, "added").unwrap_or_else(|| panic!("{rejected}")); - assert_eq!(demand["surface"]["kind"], json!("fn")); - assert_eq!(demand["surface"]["visibility"], json!("public")); - assert_eq!(demand["surface"]["rule"], json!("rs-add-exposed")); - assert_eq!(demand["surface"]["language"], json!("rust")); - // Facts pre-filled, judgment blank (dec/ddd/rejection-facts-prefilled). - assert_eq!(demand["template"]["verdict_knowledge"], json!("")); - assert!(demand["template"]["signature"].is_null() || demand["surface"]["signature"].is_string()); - // The template carries the pre-computed binding for this transition. - let binding = demand["template"]["binding"].clone(); - assert_eq!(binding["symbol"], json!("added"), "{demand}"); - assert_eq!(binding["file"], json!("src/lib.rs"), "{demand}"); - assert!(binding["before"].is_string() && binding["after"].is_string(), "{demand}"); - assert!(binding["base_revision"].is_string(), "{demand}"); - - call( - ®istry, - "ddd_declare_seam", - json!({ - "id": "seam/rust/added", - "boundary": "fn added in src/lib.rs", - "contract_location": "src/lib.rs#added", - "symbol": "added", - "verdict_knowledge": "callers learn that a u32 maps to a u32 with no failure mode, so no error path is owed on this side", - "binding": binding, - }), - ); - - let applied = apply(®istry, &edited); - assert_eq!(applied["status"], "applied", "{applied}"); - assert_eq!(applied["linked"], json!(["seam/rust/added"])); - assert!(source(tmp.path()).contains("pub fn added")); - - let rows = event_rows(tmp.path()); - let rejected_row = rows - .iter() - .find(|r| r["symbol"] == "added" && r["outcome"] == "rejected") - .unwrap_or_else(|| panic!("no rejected row in {rows:?}")); - assert_eq!(rejected_row["language"], json!("rust")); - assert_eq!(rejected_row["kind"], json!("fn")); - assert_eq!(rejected_row["visibility"], json!("public")); - assert_eq!(rejected_row["change"], json!("added")); - assert_eq!(rejected_row["rule"], json!("rs-add-exposed")); - assert!(rejected_row["signature"].as_str().unwrap_or_default().contains("fn added")); - assert!(rejected_row["reference_count"].is_number(), "{rejected_row}"); - - let linked_row = rows - .iter() - .find(|r| r["symbol"] == "added" && r["outcome"] == "applied-linked") - .unwrap_or_else(|| panic!("no linked row in {rows:?}")); - assert_eq!(linked_row["linked_declaration"], json!("seam/rust/added")); - - // The declaration itself carries the machine-authored structural facts. - let seam: Value = serde_yaml::from_str( - &std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-rust-added.yaml")) - .expect("declaration"), - ) - .expect("yaml"); - assert_eq!(seam["metadata"]["kind"], json!("fn")); - assert_eq!(seam["metadata"]["visibility"], json!("public")); -} - -/// The same loop for a trait impl, which carries no visibility keyword at -/// all — the row exists because nothing in the syntax marks it as surface. -#[test] -fn a_trait_impl_addition_is_rejected_until_a_seam_is_declared() { - let (tmp, registry) = repo("enforce"); - warm(®istry); - let base = source(tmp.path()); - let edited = base.replace( - "pub enum Colour {", - "impl std::fmt::Debug for Person {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.name) }\n}\n\npub enum Colour {", - ); - - let rejected = apply(®istry, &edited); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - let demand = demand_for(&rejected, "impl std::fmt::Debug for Person") - .unwrap_or_else(|| panic!("{rejected}")); - assert_eq!(demand["surface"]["kind"], json!("trait-impl")); - assert_eq!(demand["surface"]["rule"], json!("rs-trait-impl")); - // The two halves of the participation ride along on the demand. - assert_eq!(demand["surface"]["extra"]["trait"], json!("Debug")); - assert_eq!(demand["surface"]["extra"]["self_type"], json!("Person")); - - call( - ®istry, - "ddd_declare_seam", - json!({ - "id": "seam/rust/person-debug", - "boundary": "impl std::fmt::Debug for Person in src/lib.rs", - "contract_location": "src/lib.rs#impl std::fmt::Debug for Person", - "symbol": "impl std::fmt::Debug for Person", - "verdict_knowledge": "callers learn that Person may be formatted for diagnostics, which commits the name field to appearing in logs and makes it a disclosure surface rather than an internal field", - "binding": demand["template"]["binding"], - }), - ); - - let applied = apply(®istry, &edited); - assert_eq!(applied["status"], "applied", "{applied}"); - assert_eq!(applied["linked"], json!(["seam/rust/person-debug"])); - - let rows = event_rows(tmp.path()); - let row = rows - .iter() - .find(|r| r["symbol"] == "impl std::fmt::Debug for Person" && r["outcome"] == "applied-linked") - .unwrap_or_else(|| panic!("no linked row in {rows:?}")); - assert_eq!(row["kind"], json!("trait-impl")); - assert_eq!(row["extra"]["self_type"], json!("Person")); -} - -#[test] -fn a_private_edit_needs_no_declaration_even_under_enforce() { - let (tmp, registry) = repo("enforce"); - warm(®istry); - let base = source(tmp.path()); - let edited = base.replace( - "fn internal_helper(", - "fn also_private(v: u32) -> u32 { v }\n\nfn internal_helper(", - ); - let result = apply(®istry, &edited); - assert_eq!(result["status"], "applied", "{result}"); - assert_eq!(result["surface_events"], json!(0)); - assert!(source(tmp.path()).contains("fn also_private")); -} diff --git a/ddd-mcp/tests/serve.rs b/ddd-mcp/tests/serve.rs deleted file mode 100644 index 663d47cb..00000000 --- a/ddd-mcp/tests/serve.rs +++ /dev/null @@ -1,740 +0,0 @@ -//! M3+M4 acceptance over the ddd_* registry with mock LSP hosts: language -//! tools on the fixtures, the manifest join, and the full interceptor -//! loop (reject → declare → re-apply → correspondence rows). - -use std::path::{Path, PathBuf}; - -use serde_json::{json, Value}; - -fn fixture(rel: &str) -> String { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../ddd-cli/tests/fixtures") - .join(rel); - std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) -} - -fn write(root: &Path, rel: &str, content: &str) { - let path = root.join(rel); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("mkdir"); - } - std::fs::write(path, content).expect("write"); -} - -/// Run `git` in `root`, asserting success. -fn git(root: &Path, args: &[&str]) { - let out = std::process::Command::new("git") - .current_dir(root) - .args(args) - .output() - .unwrap_or_else(|e| panic!("git {args:?}: {e}")); - assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); -} - -/// Commit the whole tree, so every governed file's parent state is -/// committed — the precondition for binding (M8 ruling 2). -fn commit_all(root: &Path) { - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "init"]); -} - -/// A governed temp repo: `.ddd/` scaffold, format-3 config binding both -/// adapters to the mock host, the fixture sources on disk — all committed, -/// because enforce mode only binds against committed parent state (M8). -fn repo(config_tail: &str) -> (tempfile::TempDir, product_mcp::ToolRegistry) { - let tmp = tempfile::tempdir().expect("tempdir"); - ddd_core::init::apply_init(&ddd_core::init::plan_init(tmp.path())).expect("init"); - let mock = env!("CARGO_BIN_EXE_ddd-mcp-mock-lsp"); - write( - tmp.path(), - ".ddd/config.yaml", - &format!( - "format: 3\nignore: []\nadapter:\n csharp:\n command: [\"{mock}\", \"--handshake\", \"roslyn\"]\n bicep:\n command: [\"{mock}\"]\n{config_tail}" - ), - ); - write(tmp.path(), "Library.cs", &fixture("csharp/Library.cs")); - write(tmp.path(), "Api.csproj", ""); - write(tmp.path(), "storage-module.bicep", &fixture("bicep/storage-module.bicep")); - git(tmp.path(), &["init", "-q"]); - git(tmp.path(), &["config", "user.email", "t@example.com"]); - git(tmp.path(), &["config", "user.name", "T"]); - commit_all(tmp.path()); - let (_state, registry) = ddd_mcp::serve::build_registry(tmp.path().to_path_buf()); - (tmp, registry) -} - -fn call(registry: &product_mcp::ToolRegistry, name: &str, args: Value) -> Value { - registry.call_tool(name, &args).unwrap_or_else(|e| panic!("{name}: {e}")) -} - -fn warm(registry: &product_mcp::ToolRegistry) { - let out = call(registry, "ddd_warmup", json!({"wait_ms": 10_000})); - let hosts = out["hosts"].as_array().expect("hosts"); - for h in hosts { - assert_eq!(h["readiness"]["state"], "ready", "{h}"); - } -} - -/// Zero-based (line, character) of the first occurrence of `needle`. -fn pos_of(text: &str, needle: &str) -> (usize, usize) { - for (i, line) in text.lines().enumerate() { - if let Some(col) = line.find(needle) { - return (i, col); - } - } - panic!("`{needle}` not in text"); -} - -fn event_rows(root: &Path) -> Vec { - let dir = root.join(".ddd/seams/events"); - let mut rows = Vec::new(); - if let Ok(rd) = std::fs::read_dir(&dir) { - let mut paths: Vec<_> = rd.flatten().map(|e| e.path()).collect(); - paths.sort(); - for p in paths { - let text = std::fs::read_to_string(&p).expect("row"); - rows.push(serde_yaml::from_str::(&text).expect("yaml")); - } - } - rows -} - -const CLAIM: &str = "format: 1\nid: DDD-t-1\nstatement: the rule pays rent\nstatus: reported\nevidence: exercised\nfalsifier: a counterexample\nowner: none\nchanged: 2026-08-01\n"; -const DECISION: &str = "format: 1\nid: dec/cs/async-config\ntitle: Adopt CA2007\nrationale: sync contexts bite\nprincipal: Emil\nbased_on: [DDD-t-1]\n"; - -#[test] -fn m3_language_tools_answer_on_both_fixtures() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - let cs = fixture("csharp/Library.cs"); - - let found = call(®istry, "ddd_find_symbol", json!({"file": "Library.cs"})); - let names: Vec<&str> = - found["symbols"].as_array().expect("symbols").iter().filter_map(|s| s["name"].as_str()).collect(); - for expect in ["PublicApi", "Ping", "IContract", "Inner", "Fetch"] { - assert!(names.contains(&expect), "{expect} missing from {names:?}"); - } - - let (line, ch) = pos_of(&cs, "Ping"); - let refs = call(®istry, "ddd_references", json!({"file": "Library.cs", "line": line, "character": ch})); - assert!(refs["count"].as_u64().expect("count") >= 1, "{refs}"); - - let hover = call(®istry, "ddd_hover", json!({"file": "Library.cs", "line": line, "character": ch})); - assert!(hover["contents"].as_str().expect("contents").contains("Ping"), "{hover}"); - - let sig = call(®istry, "ddd_signature", json!({"file": "Library.cs", "line": line, "character": ch})); - assert!(!sig["signatures"].as_array().expect("sigs").is_empty(), "{sig}"); - - let ren = call(®istry, "ddd_rename", - json!({"file": "Library.cs", "line": line, "character": ch, "new_name": "Pulse"})); - assert_eq!(ren["status"], "computed"); - assert_eq!(ren["applied"], false); - assert!(!ren["workspace_edit"].as_array().expect("we").is_empty()); - - let bi = fixture("bicep/storage-module.bicep"); - let bi_found = call(®istry, "ddd_find_symbol", json!({"file": "storage-module.bicep"})); - let bi_names: Vec<&str> = - bi_found["symbols"].as_array().expect("symbols").iter().filter_map(|s| s["name"].as_str()).collect(); - for expect in ["location", "accountName", "stg", "storageId"] { - assert!(bi_names.contains(&expect), "{expect} missing from {bi_names:?}"); - } - let (bl, bc) = pos_of(&bi, "accountName"); - let bi_refs = call(®istry, "ddd_references", - json!({"file": "storage-module.bicep", "line": bl, "character": bc})); - assert!(bi_refs["count"].as_u64().expect("count") >= 2, "{bi_refs}"); - drop(tmp); -} - -#[test] -fn m3_diagnostics_join_the_manifest_by_rule_id() { - let (tmp, registry) = repo("intercept: enforce\n"); - write(tmp.path(), ".ddd/manifest/analyzers.yaml", - "format: 1\nrules:\n - rule_id: CA2007\n severity: warning\n decision: dec/cs/async-config\n"); - write(tmp.path(), ".ddd/claims/ddd-t-1.yaml", CLAIM); - write(tmp.path(), ".ddd/decisions/dec-cs-async-config.yaml", DECISION); - write(tmp.path(), "Library.cs.diag.json", - r#"[{"range": {"start": {"line": 4, "character": 0}, "end": {"line": 4, "character": 10}}, - "severity": 2, "code": "CA2007", "source": "csharp", "message": "consider ConfigureAwait"}, - {"range": {"start": {"line": 5, "character": 0}, "end": {"line": 5, "character": 10}}, - "severity": 3, "code": "CA1848", "source": "csharp", "message": "use LoggerMessage"}]"#); - warm(®istry); - let out = call(®istry, "ddd_diagnostics", json!({"file": "Library.cs"})); - let diags = out["diagnostics"].as_array().expect("diagnostics"); - assert_eq!(diags.len(), 2, "{out}"); - let ca2007 = diags.iter().find(|d| d["rule_id"] == "CA2007").expect("CA2007"); - assert_eq!(ca2007["namespace"], "analyzers"); - assert_eq!(ca2007["governance"]["decision"], "dec/cs/async-config"); - let ca1848 = diags.iter().find(|d| d["rule_id"] == "CA1848").expect("CA1848"); - assert!(ca1848["governance"]["marker"].as_str().expect("marker").contains("no manifest entry")); -} - -#[test] -fn m4_full_loop_csharp_reject_declare_reapply() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - let cs = fixture("csharp/Library.cs"); - let edited = cs.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n", - ); - - // 1. The edit touches contract surface → rejected with the demand. - let rejected = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - let demand = &rejected["demands"][0]; - assert_eq!(demand["surface"]["symbol"], "Pong"); - assert_eq!(demand["surface"]["kind"], "method"); - assert_eq!(demand["surface"]["visibility"], "public"); - assert_eq!(demand["surface"]["rule"], "cs-add-exposed"); - assert!(demand["surface"]["reference_count"].is_u64(), "{demand}"); - assert_eq!(demand["template"]["verdict_knowledge"], ""); - // The template carries the pre-computed binding for this transition. - let binding = demand["template"]["binding"].clone(); - assert_eq!(binding["symbol"], "Pong", "{demand}"); - assert_eq!(binding["file"], "Library.cs", "{demand}"); - assert!(binding["before"].is_string() && binding["after"].is_string(), "{demand}"); - assert!(binding["base_revision"].is_string(), "{demand}"); - assert!(std::fs::read_to_string(tmp.path().join("Library.cs")).expect("read").contains("Ping")); - assert!(!std::fs::read_to_string(tmp.path().join("Library.cs")).expect("read").contains("Pong")); - - // 2. Declare the seam the demand names, signing the demanded transition. - let declared = call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", - "boundary": "method Pong in Library.cs", - "contract_location": "Library.cs#Pong", - "symbol": "Pong", - "verdict_knowledge": "callers learn whether the service answers", - "binding": binding, - })); - assert_eq!(declared["status"], "filed", "{declared}"); - assert!(declared["warning"].is_null(), "{declared}"); - assert!(declared["binding"]["hash"].is_string(), "{declared}"); - - // 3. Re-apply → applied, linked to the declaration. - let applied = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - assert_eq!(applied["linked"][0], "seam/csharp/pong"); - assert!(std::fs::read_to_string(tmp.path().join("Library.cs")).expect("read").contains("Pong")); - - // 4. The correspondence rows: rejected then applied-linked, with the - // structural metadata. - let rows = event_rows(tmp.path()); - assert_eq!(rows.len(), 2, "{rows:?}"); - assert_eq!(rows[0]["outcome"], "rejected"); - assert_eq!(rows[1]["outcome"], "applied-linked"); - assert_eq!(rows[1]["symbol"], "Pong"); - assert_eq!(rows[1]["kind"], "method"); - assert_eq!(rows[1]["linked_declaration"], "seam/csharp/pong"); - assert!(rows[1]["reference_count"].is_u64(), "{:?}", rows[1]); - - // 5. The seam declaration carries the LSP-derived metadata now. - let seam = std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-csharp-pong.yaml")).expect("seam"); - assert!(seam.contains("kind: method"), "{seam}"); - assert!(seam.contains("reference_count:"), "{seam}"); - - // 6. why resolves the seam id end-to-end. - let why = call(®istry, "ddd_why", json!({"id": "seam/csharp/pong"})); - assert_eq!(why["status"], "ok"); - assert!(why["chain"].as_str().expect("chain").contains("callers learn"), "{why}"); -} - -#[test] -fn m4_full_loop_bicep_param_change() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - let bi = fixture("bicep/storage-module.bicep"); - let edited = bi.replace( - "param accountName string\n", - "param accountName string\nparam replicas int = 2\n", - ); - let rejected = call(®istry, "ddd_apply_edit", - json!({"file": "storage-module.bicep", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - let demand = &rejected["demands"][0]; - assert_eq!(demand["surface"]["symbol"], "replicas"); - assert_eq!(demand["surface"]["kind"], "param"); - assert_eq!(demand["surface"]["rule"], "bi-param-membership"); - assert_eq!(demand["surface"]["extra"]["ground_provenance"], "controlled"); - - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/bicep/storage-replicas", - "boundary": "storage module param replicas", - "contract_location": "storage-module.bicep", - "symbol": "replicas", - "verdict_knowledge": "deployers choose redundancy; the module encodes the allowed range", - "binding": demand["template"]["binding"], - })); - let applied = call(®istry, "ddd_apply_edit", - json!({"file": "storage-module.bicep", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - let rows = event_rows(tmp.path()); - assert_eq!(rows.last().expect("row")["extra"]["ground_provenance"], "controlled"); -} - -#[test] -fn m4_warn_and_off_modes_per_artifact_class() { - // code (C#) stays enforce; configuration (Bicep) warns. - let (tmp, registry) = - repo("intercept: enforce\nintercept_by_class:\n configuration: warn\n"); - warm(®istry); - let bi = fixture("bicep/storage-module.bicep"); - let edited = bi.replace("param accountName string\n", "param accountName string\nparam extra int = 1\n"); - let out = call(®istry, "ddd_apply_edit", - json!({"file": "storage-module.bicep", "new_text": edited})); - assert_eq!(out["status"], "applied", "{out}"); - assert_eq!(out["mode"], "warn"); - assert!(out["warning"].as_str().expect("warning").contains("declare the seam")); - let rows = event_rows(tmp.path()); - assert_eq!(rows.last().expect("row")["outcome"], "applied-warned"); - - // C# under the same config still enforces. - let cs = fixture("csharp/Library.cs"); - let cs_edit = cs.replace(" public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n"); - let rejected = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": cs_edit})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - - // off skips classification entirely: no host consulted, no rows. - let (tmp2, registry2) = repo("intercept: off\n"); - let out2 = call(®istry2, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": cs_edit})); - assert_eq!(out2["status"], "applied"); - assert_eq!(out2["intercepted"], false); - assert!(event_rows(tmp2.path()).is_empty()); -} - -#[test] -fn m4_declare_pattern_round_trips_the_decorator_obligations() { - let (tmp, registry) = repo("intercept: enforce\n"); - write(tmp.path(), ".ddd/predicates/composition-decorator.yaml", - &fixture("../../../.ddd/predicates/composition-decorator.yaml")); - let missing = registry.call_tool("ddd_declare_pattern", &json!({ - "id": "pat/decorator/logging", - "pattern": "pred/composition/decorator", - "instance": "Scrutor .Decorate in Program.cs", - "obligation_answers": {"ordering": "logging outermost"}, - })); - let err = missing.expect_err("must reject unanswered obligations"); - assert!(err.contains("identity-preservation"), "{err}"); - assert!(err.contains("forwarding-completeness"), "{err}"); - - let filed = call(®istry, "ddd_declare_pattern", json!({ - "id": "pat/decorator/logging", - "pattern": "pred/composition/decorator", - "instance": "Scrutor .Decorate in Program.cs", - "obligation_answers": { - "ordering": "logging outermost, retry innermost", - "identity-preservation": "decorators re-expose the decorated interface only", - "forwarding-completeness": "all members forwarded; verified by the interface test", - }, - })); - assert_eq!(filed["status"], "filed", "{filed}"); - let q = call(®istry, "ddd_graph_query", json!({"select": "patterns"})); - assert_eq!(q["count"], 1, "{q}"); - let why = call(®istry, "ddd_why", json!({"id": "pat/decorator/logging"})); - assert!(why["chain"].as_str().expect("chain").contains("forwarding"), "{why}"); -} - -/// Amending a filed declaration (dec/ddd/amend-explicit-evidence-frozen): -/// judgement fields revise, identity plus LSP-derived evidence do not. -#[test] -fn m4_amend_fills_in_the_warned_verdict_knowledge() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - let cs = fixture("csharp/Library.cs"); - let edited = cs.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n", - ); - let rejected = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - - // File it warned: the boundary declares cost with no demand absorbed. - // The binding still signs the demanded transition, so the re-apply - // below discharges. - let warned = call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", "boundary": "method Pong in Library.cs", - "contract_location": "Library.cs#Pong", "symbol": "Pong", "verdict_knowledge": "", - "binding": rejected["demands"][0]["template"]["binding"], - })); - assert_eq!(warned["status"], "filed"); - assert!(warned["warning"].is_string(), "{warned}"); - - // Re-declaring without the flag still refuses — no silent overwrite. - let clash = registry.call_tool("ddd_declare_seam", &json!({ - "id": "seam/csharp/pong", "boundary": "b", "contract_location": "c", - })); - assert!(clash.expect_err("must refuse").contains("amend: true")); - - // Re-apply so the interceptor writes its LSP-derived evidence onto the - // declaration; the amendment below must leave that evidence alone. - let applied = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - - // Amending supplies only the field being revised. - let amended = call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", "amend": true, - "verdict_knowledge": "callers learn whether the service answers", - })); - assert_eq!(amended["status"], "amended", "{amended}"); - assert!(amended["warning"].is_null(), "{amended}"); - - let seam = std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-csharp-pong.yaml")) - .expect("seam"); - assert!(seam.contains("callers learn whether the service answers"), "{seam}"); - // Omitted judgement fields are preserved, not blanked. - assert!(seam.contains("boundary: method Pong in Library.cs"), "{seam}"); - // The interceptor's evidence survives the amendment. - assert!(seam.contains("kind: method"), "{seam}"); - assert!(seam.contains("reference_count:"), "{seam}"); - - let why = call(®istry, "ddd_why", json!({"id": "seam/csharp/pong"})); - assert!(why["chain"].as_str().expect("chain").contains("callers learn"), "{why}"); -} - -#[test] -fn m4_amend_cannot_create_and_cannot_rewrite_frozen_evidence() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - // Amending something never filed is an error, not a create. - let missing = registry.call_tool("ddd_declare_seam", &json!({ - "id": "seam/csharp/ghost", "amend": true, "verdict_knowledge": "x", - })); - assert!(missing.expect_err("must refuse").contains("nothing is filed")); - assert!(!tmp.path().join(".ddd/seams/seam-csharp-ghost.yaml").exists()); - - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", "boundary": "method Pong", "symbol": "Pong", - "contract_location": "Library.cs#Pong", "verdict_knowledge": "v", - })); - // contract_location is evidence: a supplied value is ignored, not applied. - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", "amend": true, "contract_location": "Elsewhere.cs#Fake", - })); - let seam = std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-csharp-pong.yaml")) - .expect("seam"); - assert!(seam.contains("Library.cs#Pong"), "{seam}"); - assert!(!seam.contains("Elsewhere.cs#Fake"), "{seam}"); -} - -#[test] -fn m4_amend_refines_one_obligation_without_restating_the_rest() { - let (tmp, registry) = repo("intercept: enforce\n"); - write(tmp.path(), ".ddd/predicates/composition-decorator.yaml", - &fixture("../../../.ddd/predicates/composition-decorator.yaml")); - let args = json!({ - "id": "pat/decorator/logging", "pattern": "pred/composition/decorator", - "instance": "Scrutor .Decorate in Program.cs", - "obligation_answers": { - "ordering": "logging outermost", - "identity-preservation": "decorated interface only", - "forwarding-completeness": "all members forwarded", - }, - }); - call(®istry, "ddd_declare_pattern", args.clone()); - - // One obligation revised; the other two carry forward and completeness - // still holds. - let amended = call(®istry, "ddd_declare_pattern", json!({ - "id": "pat/decorator/logging", "amend": true, - "obligation_answers": {"ordering": "logging outermost, retry innermost"}, - })); - assert_eq!(amended["status"], "amended", "{amended}"); - let q = call(®istry, "ddd_graph_query", json!({"select": "patterns"})); - let obligations = &q["entries"][0]["obligations"]; - assert_eq!(obligations["ordering"], "logging outermost, retry innermost"); - assert_eq!(obligations["identity-preservation"], "decorated interface only"); - assert_eq!(obligations["forwarding-completeness"], "all members forwarded"); - - // The predicate is identity: a filed instance cannot be re-pointed. - let repointed = registry.call_tool("ddd_declare_pattern", &json!({ - "id": "pat/decorator/logging", "amend": true, "pattern": "pred/other/thing", - })); - assert!(repointed.expect_err("must refuse").contains("cannot change its predicate")); -} - -#[test] -fn m4_accept_risk_amends_its_rationale() { - let (tmp, registry) = repo("intercept: enforce\n"); - call(®istry, "ddd_accept_risk", json!({ - "diagnostic_id": "CA1848", "rationale": "first pass", "principal": "Emil", - })); - let amended = call(®istry, "ddd_accept_risk", json!({ - "diagnostic_id": "CA1848", "amend": true, - "rationale": "the service logs under 200 lines/min; the codegen is not worth it", - })); - assert_eq!(amended["status"], "amended", "{amended}"); - let record = std::fs::read_to_string(tmp.path().join(".ddd/decisions/risk-rule-ca1848.yaml")) - .expect("record"); - assert!(record.contains("200 lines/min"), "{record}"); - // principal was not re-supplied, so it is preserved. - assert!(record.contains("principal: Emil"), "{record}"); -} - -#[test] -fn m4_accept_risk_satisfies_the_uncited_suppression_check() { - let (tmp, registry) = repo("intercept: enforce\n"); - write(tmp.path(), "bicepconfig.json", &fixture("bicep/bicepconfig.json")); - - let filed = call(®istry, "ddd_accept_risk", json!({ - "diagnostic_id": "no-hardcoded-env-urls", - "rationale": "the only URLs are Azure public-cloud endpoints pinned on purpose", - "principal": "Emil (Context&)", - })); - assert_eq!(filed["status"], "filed", "{filed}"); - let risk_id = filed["id"].as_str().expect("id"); - - write(tmp.path(), ".ddd/manifest/linter.yaml", &format!( - "format: 1\nrules:\n - rule_id: no-unused-params\n severity: warning\n governance: UNGOVERNED\n - rule_id: no-hardcoded-env-urls\n severity: \"off\"\n governance: UNGOVERNED\n suppression:\n risk_acceptance: {risk_id}\n" - )); - let store = ddd_core::store::load(&tmp.path().join(".ddd")); - let detected = ddd_core::detect::detect(tmp.path(), store.config.as_ref(), &[]); - let report = ddd_core::diff::diff(&store, &detected); - let uncited: Vec<_> = report - .findings - .iter() - .filter(|f| f.kind == ddd_core::diff::FindingKind::UncitedSuppression) - .collect(); - assert!(uncited.is_empty(), "{uncited:?}"); -} - -/// The full interceptor loop against the *real* Roslyn host — gated like -/// the other real-host tests (`DDD_LSP_E2E=1`, tools on PATH). -#[test] -fn m4_real_roslyn_full_loop() { - if std::env::var("DDD_LSP_E2E").ok().as_deref() != Some("1") { - return; - } - let tmp = tempfile::tempdir().expect("tempdir"); - ddd_core::init::apply_init(&ddd_core::init::plan_init(tmp.path())).expect("init"); - write(tmp.path(), ".ddd/config.yaml", "format: 3\nintercept: enforce\nignore: []\n"); - write(tmp.path(), "Library.cs", &fixture("csharp/Library.cs")); - write(tmp.path(), "Api.csproj", - "net8.0"); - git(tmp.path(), &["init", "-q"]); - git(tmp.path(), &["config", "user.email", "t@example.com"]); - git(tmp.path(), &["config", "user.name", "T"]); - commit_all(tmp.path()); - let (_state, registry) = ddd_mcp::serve::build_registry(tmp.path().to_path_buf()); - let out = call(®istry, "ddd_warmup", json!({"wait_ms": 240_000})); - assert_eq!(out["hosts"][0]["readiness"]["state"], "ready", "{out}"); - let cs = fixture("csharp/Library.cs"); - let edited = cs.replace(" public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n"); - let rejected = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - assert_eq!(rejected["demands"][0]["surface"]["symbol"], "Pong", "{rejected}"); - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", "boundary": "method Pong in Library.cs", - "contract_location": "Library.cs#Pong", "symbol": "Pong", - "verdict_knowledge": "callers learn whether the service answers", - "binding": rejected["demands"][0]["template"]["binding"], - })); - let applied = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - assert_eq!(applied["linked"][0], "seam/csharp/pong"); - assert_eq!(event_rows(tmp.path()).len(), 2); -} - -/// `dec/ddd/enforce-matching-tightens-to-symbol`, the regression M6 observed -/// as seam-event/4: one edit adds two public symbols, two declarations are -/// authored one per symbol, and each surface event must link to the -/// declaration naming its own symbol — never to whichever declaration -/// happened to share the file. The metadata write must stay symbol-exact -/// too, or the correspondence record silently carries another symbol's -/// facts (DDD-arch-05). -#[test] -fn m7_enforce_links_each_symbol_to_its_own_declaration() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - let cs = fixture("csharp/Library.cs"); - let edited = cs.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Alpha() { }\n public void Beta(int n) { }\n", - ); - // One rejection raises one demand per symbol, each carrying its own - // pre-computed binding. - let rejected = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - let demands = rejected["demands"].as_array().expect("demands").clone(); - let binding_for = |symbol: &str| { - demands - .iter() - .find(|d| d["surface"]["symbol"] == symbol) - .unwrap_or_else(|| panic!("no demand for {symbol}: {demands:?}"))["template"]["binding"] - .clone() - }; - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/alpha", "boundary": "method Alpha in Library.cs", - "contract_location": "Library.cs#Alpha", "symbol": "Alpha", - "verdict_knowledge": "callers learn the alpha outcome", - "binding": binding_for("Alpha"), - })); - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/beta", "boundary": "method Beta in Library.cs", - "contract_location": "Library.cs#Beta", "symbol": "Beta", - "verdict_knowledge": "callers learn the beta count", - "binding": binding_for("Beta"), - })); - let applied = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - - // Every applied-linked row links the declaration naming its own symbol. - let mut linked_symbols = Vec::new(); - for row in event_rows(tmp.path()) { - if row["outcome"] != "applied-linked" { - continue; - } - let (symbol, linked) = (row["symbol"].as_str().expect("symbol"), - row["linked_declaration"].as_str().expect("linked")); - match symbol { - "Alpha" => assert_eq!(linked, "seam/csharp/alpha", "{row}"), - "Beta" => assert_eq!(linked, "seam/csharp/beta", "{row}"), - other => panic!("unexpected row symbol {other}"), - } - linked_symbols.push(symbol.to_string()); - } - assert!(linked_symbols.contains(&"Alpha".to_string()), "{linked_symbols:?}"); - assert!(linked_symbols.contains(&"Beta".to_string()), "{linked_symbols:?}"); - - // Each declaration carries its own symbol's facts — the mis-attribution - // seam-event/4 produced can no longer occur. - let alpha = std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-csharp-alpha.yaml")).expect("alpha"); - assert!(alpha.contains("symbol: Alpha"), "{alpha}"); - let beta = std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-csharp-beta.yaml")).expect("beta"); - assert!(beta.contains("symbol: Beta"), "{beta}"); - assert!(beta.contains("signature: void Beta(int n)"), "{beta}"); -} - -/// The file arm is gone from enforce mode: a stored declaration whose -/// contract_location names the file but whose symbol is a different one -/// admits nothing — and under M8 a declaration with no signed binding -/// discharges nothing at all, however its location matches. -#[test] -fn m7_enforce_rejects_a_file_arm_only_declaration() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/ping", "boundary": "method Ping in Library.cs", - "contract_location": "Library.cs", - "symbol": "Ping", - "verdict_knowledge": "an unrelated symbol in the same file", - })); - let cs = fixture("csharp/Library.cs"); - let edited = cs.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n", - ); - let out = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(out["status"], "rejected", "{out}"); - assert_eq!(out["demands"][0]["surface"]["symbol"], "Pong", "{out}"); - let rows = event_rows(tmp.path()); - assert_eq!(rows.last().expect("row")["outcome"], "rejected"); -} - -/// Warn mode keeps the broad arm: the row gets the generous link, but the -/// declaration's metadata is never overwritten by a file-arm match. -#[test] -fn m7_warn_keeps_the_file_arm_and_never_writes_metadata() { - let (tmp, registry) = repo("intercept: warn\n"); - warm(®istry); - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/bicep/storage", "boundary": "the storage module contract", - "contract_location": "storage-module.bicep", - "verdict_knowledge": "deployers learn the module's parameter shape", - })); - let bi = fixture("bicep/storage-module.bicep"); - let edited = bi.replace( - "param accountName string\n", - "param accountName string\nparam replicas int = 2\n", - ); - let out = call(®istry, "ddd_apply_edit", - json!({"file": "storage-module.bicep", "new_text": edited})); - assert_eq!(out["status"], "applied", "{out}"); - assert_eq!(out["mode"], "warn"); - let rows = event_rows(tmp.path()); - let row = rows.last().expect("row"); - assert_eq!(row["outcome"], "applied-warned"); - assert_eq!(row["linked_declaration"], "seam/bicep/storage", "{row}"); - let seam = std::fs::read_to_string(tmp.path().join(".ddd/seams/seam-bicep-storage.yaml")).expect("seam"); - assert!(!seam.contains("symbol: replicas"), "file-arm match wrote metadata: {seam}"); -} - -/// M8 ruling 2: enforce mode never binds uncommitted parent state. A -/// governed file that is dirty against HEAD rejects the surface edit -/// outright, and a binding attempt against the dirty file refuses by name. -#[test] -fn m8_dirty_parent_state_refuses_to_bind() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - // Dirty the governed file after the initial commit: disk != HEAD. - let cs = fixture("csharp/Library.cs"); - let dirty = format!("// uncommitted local drift\n{cs}"); - std::fs::write(tmp.path().join("Library.cs"), &dirty).expect("dirty write"); - - let edited = dirty.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n", - ); - let out = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": edited})); - assert_eq!(out["status"], "rejected", "{out}"); - let reason = out["reason"].as_str().expect("reason"); - assert!( - reason.contains("never binds uncommitted parent state") || reason.contains("M8 ruling 2"), - "{reason}" - ); - assert!(!std::fs::read_to_string(tmp.path().join("Library.cs")).expect("read").contains("Pong")); - - // Declaring a binding whose file is dirty refuses, naming the constraint. - let err = registry - .call_tool("ddd_declare_seam", &json!({ - "id": "seam/csharp/pong", "boundary": "method Pong in Library.cs", - "contract_location": "Library.cs#Pong", "symbol": "Pong", - "verdict_knowledge": "callers learn whether the service answers", - "binding": { - "symbol": "Pong", "file": "Library.cs", - "before": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "after": "sha256:1111111111111111111111111111111111111111111111111111111111111111", - }, - })) - .expect_err("a dirty file must refuse to bind"); - assert!(err.contains("dirty against HEAD"), "{err}"); -} - -/// A signed binding discharges exactly the transition it names (M8, spec -/// invariant 3): the same symbol arriving via a different edit — different -/// proposed content — stays rejected; only the signed edit applies. -#[test] -fn m8_a_binding_does_not_discharge_a_different_transition() { - let (tmp, registry) = repo("intercept: enforce\n"); - warm(®istry); - let cs = fixture("csharp/Library.cs"); - let e1 = cs.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { }\n", - ); - let e2 = cs.replace( - " public void Ping() { }\n", - " public void Ping() { }\n public void Pong() { System.Console.WriteLine(\"pong\"); }\n", - ); - - let rejected = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": e1})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/csharp/pong", "boundary": "method Pong in Library.cs", - "contract_location": "Library.cs#Pong", "symbol": "Pong", - "verdict_knowledge": "callers learn whether the service answers", - "binding": rejected["demands"][0]["template"]["binding"], - })); - - // E2 adds the same symbol but lands different content — not the - // transition the binding signs. - let still = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": e2})); - assert_eq!(still["status"], "rejected", "{still}"); - assert!(!std::fs::read_to_string(tmp.path().join("Library.cs")).expect("read").contains("Pong")); - - // E1 exactly is what was signed. - let applied = call(®istry, "ddd_apply_edit", json!({"file": "Library.cs", "new_text": e1})); - assert_eq!(applied["status"], "applied", "{applied}"); - assert_eq!(applied["linked"][0], "seam/csharp/pong"); - assert!(std::fs::read_to_string(tmp.path().join("Library.cs")).expect("read").contains("Pong")); -} diff --git a/ddd-mcp/tests/webpair_governed.rs b/ddd-mcp/tests/webpair_governed.rs deleted file mode 100644 index 032269f3..00000000 --- a/ddd-mcp/tests/webpair_governed.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! M7 acceptance: the full interception loop over the hostless HTML+CSS -//! pair adapter — reject → declare → re-apply → correspondence rows -//! carrying the pair-level facts (both files, direction, token/class id). -//! No language server exists behind any of this; no warmup is called. - -use std::path::{Path, PathBuf}; - -use serde_json::{json, Value}; - -fn fixture(rel: &str) -> String { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../ddd-cli/tests/fixtures/webpair") - .join(rel); - std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) -} - -fn write(root: &Path, rel: &str, content: &str) { - let path = root.join(rel); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("mkdir"); - } - std::fs::write(path, content).expect("write"); -} - -/// Run `git` in `root`, asserting success. -fn git(root: &Path, args: &[&str]) { - let out = std::process::Command::new("git") - .current_dir(root) - .args(args) - .output() - .unwrap_or_else(|e| panic!("git {args:?}: {e}")); - assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); -} - -/// Commit the whole tree, so every governed file's parent state is -/// committed — the precondition for binding (M8 ruling 2). -fn commit_all(root: &Path) { - git(root, &["add", "-A"]); - git(root, &["commit", "-qm", "init"]); -} - -/// A governed temp repo with the pair map on and the html-css class in the -/// given mode — all committed, because enforce mode only binds against -/// committed parent state (M8). -fn repo(mode: &str) -> (tempfile::TempDir, product_mcp::ToolRegistry) { - let tmp = tempfile::tempdir().expect("tempdir"); - ddd_core::init::apply_init(&ddd_core::init::plan_init(tmp.path())).expect("init"); - write( - tmp.path(), - ".ddd/config.yaml", - &format!( - "format: 4\nintercept: warn\nintercept_by_class:\n html-css: {mode}\npair:\n units:\n - html: [\"site/*.html\"]\n css: [\"site/style.css\"]\n ignore_classes: [\"js-*\"]\n" - ), - ); - write(tmp.path(), "site/page.html", &fixture("page.html")); - write(tmp.path(), "site/style.css", &fixture("style.css")); - git(tmp.path(), &["init", "-q"]); - git(tmp.path(), &["config", "user.email", "t@example.com"]); - git(tmp.path(), &["config", "user.name", "T"]); - commit_all(tmp.path()); - let (_state, registry) = ddd_mcp::serve::build_registry(tmp.path().to_path_buf()); - (tmp, registry) -} - -fn call(registry: &product_mcp::ToolRegistry, name: &str, args: Value) -> Value { - registry.call_tool(name, &args).unwrap_or_else(|e| panic!("{name}: {e}")) -} - -fn event_rows(root: &Path) -> Vec { - let dir = root.join(".ddd/seams/events"); - let mut rows = Vec::new(); - if let Ok(rd) = std::fs::read_dir(&dir) { - let mut paths: Vec<_> = rd.flatten().map(|e| e.path()).collect(); - paths.sort(); - for p in paths { - let text = std::fs::read_to_string(&p).expect("row"); - rows.push(serde_yaml::from_str::(&text).expect("yaml")); - } - } - rows -} - -#[test] -fn token_change_runs_the_full_loop_with_pair_facts_on_the_row() { - let (tmp, registry) = repo("enforce"); - let css = fixture("style.css"); - let edited = format!("{css}:root {{ --color-warn: #8a5a00; }}\n"); - - // 1. Rejected: a token joined the stylesheet's params undeclared. - let rejected = - call(®istry, "ddd_apply_edit", json!({"file": "site/style.css", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - let surface = &rejected["demands"][0]["surface"]; - assert_eq!(surface["symbol"], "--color-warn"); - assert_eq!(surface["kind"], "token"); - assert_eq!(surface["rule"], "web-token-membership"); - assert_eq!(surface["language"], "htmlcss"); - assert_eq!(surface["artifact_class"], "html-css"); - assert_eq!(surface["extra"]["pair_direction"], "definition"); - assert_eq!(surface["extra"]["pair_counterpart"], "site/page.html"); - assert!(!std::fs::read_to_string(tmp.path().join("site/style.css")) - .expect("read") - .contains("--color-warn")); - // The template carries the pre-computed binding for this transition. - let binding = rejected["demands"][0]["template"]["binding"].clone(); - assert_eq!(binding["symbol"], "--color-warn", "{rejected}"); - assert_eq!(binding["file"], "site/style.css", "{rejected}"); - assert!(binding["base_revision"].is_string(), "{rejected}"); - - // 2. Declare the seam the demand names, signing the demanded transition. - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/web/color-warn-token", - "boundary": "design token --color-warn in site/style.css", - "contract_location": "site/style.css#--color-warn", - "symbol": "--color-warn", - "verdict_knowledge": "consumers learn the degraded-state colour without knowing its value; retiring or retyping it breaks every var() site", - "binding": binding, - })); - - // 3. Re-apply → applied, linked; the row carries the pair facts. - let applied = - call(®istry, "ddd_apply_edit", json!({"file": "site/style.css", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - assert_eq!(applied["linked"][0], "seam/web/color-warn-token"); - let rows = event_rows(tmp.path()); - assert_eq!(rows.len(), 2, "{rows:?}"); - let row = &rows[1]; - assert_eq!(row["outcome"], "applied-linked"); - assert_eq!(row["symbol"], "--color-warn"); - assert_eq!(row["kind"], "token"); - assert_eq!(row["language"], "htmlcss"); - assert_eq!(row["artifact_class"], "html-css"); - assert_eq!(row["extra"]["pair_direction"], "definition"); - assert_eq!(row["extra"]["pair_counterpart"], "site/page.html"); - assert!(row.get("reference_count").is_none(), "hostless rows carry no count: {row}"); -} - -#[test] -fn class_vocabulary_change_runs_the_full_loop() { - let (tmp, registry) = repo("enforce"); - let css = fixture("style.css"); - let edited = format!("{css}.badge {{ color: var(--color-ok); }}\n"); - let rejected = - call(®istry, "ddd_apply_edit", json!({"file": "site/style.css", "new_text": edited})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - assert_eq!(rejected["demands"][0]["surface"]["symbol"], "badge"); - assert_eq!(rejected["demands"][0]["surface"]["rule"], "web-class-membership"); - call(®istry, "ddd_declare_seam", json!({ - "id": "seam/web/badge-class", - "boundary": "class badge in site/style.css", - "contract_location": "site/style.css#badge", - "symbol": "badge", - "verdict_knowledge": "markup learns a status chip exists; removing the class strands every element consuming it", - "binding": rejected["demands"][0]["template"]["binding"], - })); - let applied = - call(®istry, "ddd_apply_edit", json!({"file": "site/style.css", "new_text": edited})); - assert_eq!(applied["status"], "applied", "{applied}"); - let rows = event_rows(tmp.path()); - let last = rows.last().expect("row"); - assert_eq!(last["symbol"], "badge"); - assert_eq!(last["kind"], "class"); - assert_eq!(last["linked_declaration"], "seam/web/badge-class"); -} - -#[test] -fn token_value_change_and_markup_edits_apply_untouched() { - let (tmp, registry) = repo("enforce"); - let css = fixture("style.css"); - let recolored = css.replace("#1a7f37", "#2ea043"); - let out = call(®istry, "ddd_apply_edit", - json!({"file": "site/style.css", "new_text": recolored})); - assert_eq!(out["status"], "applied", "{out}"); - assert_eq!(out["surface_events"], 0, "{out}"); - let html = fixture("page.html"); - let restructured = html.replace("", ""); - let out = call(®istry, "ddd_apply_edit", - json!({"file": "site/page.html", "new_text": restructured})); - assert_eq!(out["status"], "applied", "{out}"); - assert_eq!(out["surface_events"], 0, "{out}"); - assert!(event_rows(tmp.path()).is_empty()); -} - -#[test] -fn stylesheet_rewiring_is_demanded_from_the_html_side() { - let (_tmp, registry) = repo("enforce"); - let html = fixture("page.html"); - let rewired = html.replace("style.css", "other.css"); - let rejected = - call(®istry, "ddd_apply_edit", json!({"file": "site/page.html", "new_text": rewired})); - assert_eq!(rejected["status"], "rejected", "{rejected}"); - let symbols: Vec<&str> = rejected["demands"] - .as_array() - .expect("demands") - .iter() - .map(|d| d["surface"]["symbol"].as_str().expect("symbol")) - .collect(); - assert!(symbols.contains(&"other.css") && symbols.contains(&"style.css"), "{symbols:?}"); - assert_eq!(rejected["demands"][0]["surface"]["extra"]["pair_direction"], "consumption"); -} - -#[test] -fn language_tools_say_plainly_there_is_no_host() { - let (_tmp, registry) = repo("enforce"); - let err = registry - .call_tool("ddd_find_symbol", &json!({"file": "site/style.css"})) - .expect_err("no host"); - assert!(err.to_string().contains("no language server"), "{err}"); -} diff --git a/deny.toml b/deny.toml deleted file mode 100644 index 6a990bf4..00000000 --- a/deny.toml +++ /dev/null @@ -1,90 +0,0 @@ -# cargo-deny configuration — supply-chain rules (Tier 3c). -# See conventions/docs/CTX-DEPS.md for the rule catalogue and rationale. -# -# Run locally with: cargo deny check -# CI runs the same command on every PR via .github/workflows/product-ci.yml. - -[graph] -all-features = true - -[output] -feature-depth = 1 - -# --------------------------------------------------------------------------- -# Advisories — RustSec security database -# --------------------------------------------------------------------------- -[advisories] -version = 2 -# Vulnerable crates fail the build. Yanked crates warn (downgrades are -# usually a maintainer mistake, not a security issue, and resolving them -# requires a `cargo update` we may not want on the same PR). -yanked = "warn" -# RUSTSEC-2024-0436: `paste` is unmaintained. It is a build-time proc-macro -# pulled in transitively by cel-interpreter (the CEL evaluator under pf/rules) -# — no runtime exposure and no upstream replacement available. Revisit if -# cel-interpreter drops it or a maintained fork lands. -# -# RUSTSEC-2026-0194 / RUSTSEC-2026-0195: DoS advisories in `quick-xml` <0.41 -# (quadratic duplicate-attribute check; unbounded namespace allocation in -# `NsReader`). Both require parsing attacker-controlled XML. quick-xml reaches -# us only transitively through oxigraph's `oxrdfxml` (RDF/XML) and `sparesults` -# (SPARQL-Results XML) — neither path is exercised: product-core loads oxigraph -# exclusively with `RdfFormat::Turtle` and consumes query results as native Rust -# `QueryResults` enums (see pf/{seed,query,sparql_rules}.rs), never XML, and the -# only bytes parsed are our own generated Turtle or the user's local `.ttl`. The -# fix requires `quick-xml >=0.41`, but every published oxigraph/sparesults/ -# oxrdfxml release still pins `quick-xml ^0.37`, so there is no upgrade path yet. -# Revisit when the oxigraph ecosystem moves to quick-xml >=0.41. -ignore = ["RUSTSEC-2024-0436", "RUSTSEC-2026-0194", "RUSTSEC-2026-0195"] - -# --------------------------------------------------------------------------- -# Licenses — explicit allow-list. New license types require a deliberate PR. -# --------------------------------------------------------------------------- -[licenses] -version = 2 -allow = [ - "Apache-2.0", - "Apache-2.0 WITH LLVM-exception", - "MIT", - "BSD-2-Clause", - "BSD-3-Clause", - "ISC", - "Unicode-DFS-2016", - "Unicode-3.0", - "Zlib", - "MPL-2.0", - "CDLA-Permissive-2.0", - "CC0-1.0", - "OpenSSL", -] -confidence-threshold = 0.8 - -# ring uses a custom license; the SPDX expression below mirrors what the -# upstream cargo-deny docs recommend. -[[licenses.exceptions]] -allow = ["OpenSSL"] -crate = "ring" - -# --------------------------------------------------------------------------- -# Bans — duplicate dependencies and explicit denies. -# --------------------------------------------------------------------------- -[bans] -multiple-versions = "warn" -wildcards = "deny" -# Intra-workspace `path` deps (product-core/product-mcp) carry no version and -# would otherwise trip the wildcard ban — they are first-party, not crates.io. -allow-wildcard-paths = true -highlight = "all" - -# Explicitly banned crates with rationale. -deny = [] - -# --------------------------------------------------------------------------- -# Sources — only crates.io and the workspace itself; no git URLs without -# explicit allow. -# --------------------------------------------------------------------------- -[sources] -unknown-registry = "deny" -unknown-git = "deny" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] diff --git a/dist-workspace.toml b/dist-workspace.toml deleted file mode 100644 index 00723e45..00000000 --- a/dist-workspace.toml +++ /dev/null @@ -1,19 +0,0 @@ -[workspace] -members = ["cargo:."] - -# Config for 'dist' -[dist] -# The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.31.0" -# CI backends to support -ci = "github" -# The installers to generate for each app -installers = ["shell", "powershell", "homebrew"] -# Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] -# Path that installers should place binaries in -install-path = "CARGO_HOME" -# Where to host releases -hosting = "github" -# Whether to install an updater program -install-updater = false diff --git a/docs/ai-foundations-conformance.md b/docs/ai-foundations-conformance.md deleted file mode 100644 index af0ba6d0..00000000 --- a/docs/ai-foundations-conformance.md +++ /dev/null @@ -1,174 +0,0 @@ -# AI Development Foundations — Conformance Statement - -> This statement asserts that **The Product Framework** conforms to the -> [AI Development Foundations](https://github.com/Hafeok/ai-development-foundations) -> **Pillar One — Specification Framework**. Conformance is self-declared: every -> claim below cites the section of [`docs/product-framework.md`](docs/product-framework.md) -> that satisfies it, and anyone may verify it by reading the framework against the -> published [specification checklist](https://github.com/Hafeok/ai-development-foundations/blob/main/conformance/specification-conformance.md). - -## Framework - -| Field | Value | -|-------|-------| -| **Name** | The Product Framework | -| **Version** | 1.6.0 | -| **Repository** | https://github.com/Hafeok/product-framework | -| **License** | Apache-2.0 (shapes, schemas, code) · CC BY 4.0 (specification text) | -| **Maintainer** | Emil ([@Hafeok](https://github.com/Hafeok)) | -| **Conforms to** | Specification Framework ☑ / Execution Contract ☐ | -| **Foundation version targeted** | `ai-development-foundations` @ `13b0cb4` (2026-06-26) | -| **Autonomy level** | n/a (specification framework; does not execute) | - -## Statement - -> We assert that any artifact produced under **The Product Framework**, following -> its defined process, satisfies every requirement checked below. Each checked -> item cites where in the framework the requirement is met. Requirements that are -> only partially met, or deliberately out of scope, are declared honestly in -> [Known gaps](#known-gaps) rather than overclaimed. - -The Product Framework is a catalog-agnostic standard for describing software as -three connected, machine-readable models — **What**, **How**, and **Delivery** — -in one typed graph. It is, by construction, a Pillar One framework: it defines the -shape of a specification, the derivation contract that keeps it traceable, and the -seam at which a frozen work unit is handed to an executor. It stops at that seam -and specifies nothing on the far side of it (§5.1), so it makes **no** Pillar Two -claim. - -## Pillar One — Specification - -**Coverage:** What layer ☑ · How layer ☑ · SPMC ◐ (partial — see §5 below and -[Known gaps](#known-gaps)). - -### Summary - -| Requirement | Met? | Where in the framework | -|-------------|------|------------------------| -| Premise (LLM input artifact, 5 properties) | ☑ | §1, §2, §9, §5, §6 | -| Layer separation (What / How / SPMC) | ☑ | §2, §8 (mapping table) | -| What completeness (9 sections) | ◐ | §3, §10 — 7 of 9 fully; 2 partial | -| How completeness (8 sections) | ◐ | §4, §9 — 7 of 8 fully; 1 partial | -| Derivation contract (5 rules) | ☑ | §9 (typed links), §4.1, §10 | -| SPMC (4 axes, frozen context) | ◐ | §5, §5.1, §6.2 — Schema & Context met; Model axis out of scope | -| Scale discipline | ☑ | §10 (rules 1, 13–15), §8.1 | -| The seam (Section 7) | ☑ | §5.1 — defined, frozen, independently versioned, gated before emit | - -### Section 0 — Premise (spec is an LLM input artifact) - -| Property | Met? | Where | -|---|---|---| -| Unambiguous — every term defined, nothing left to inference | ☑ | One typed graph with defined node/link kinds (§9); "intent" (unmodelled residue) is explicitly marked as specification debt to be promoted, not left to the model (§3.2.1, rule 4) | -| Complete at the boundary | ☑ | Work unit carries a **frozen, fully-resolved input bundle**; freeze precedes emit; a unit needing a mid-execution callback must not be emitted (§5, §5.1) | -| Internally consistent — contradictions resolved in the spec | ☑ | The derivation contract and verification kinds (state/Decider justification, coherence bar) surface contradictions as findings (§6.2, §6.3, §9) | -| Machine-readable — structured, not prose | ☑ | A product is one machine-readable graph; reference encoding RDF + SHACL, any typed-node/typed-link/validatable-shape encoding conformant (§9, §10 rule 1) | -| Explicitly bounded — out-of-scope declared | ◐ | Framework-level open/closed line is explicit (§1); bounded contexts carry explicit cross-context mappings (§3.1). A required *per-What* non-empty out-of-scope list is not enforced — see Known gaps | - -### Section 1 — Layer separation - -| Requirement | Met? | Where | -|---|---|---| -| What / How / SPMC kept distinct, not collapsed | ☑ | §2 "the three models and the split"; §8 mapping table | -| What ownable/validatable by a non-technical product role | ☑ | What (systems, domain model, event model) owned by product and design; authored and agreed before the How (§2.1, §3, §10 rule 2) | -| How authored by the team and **derived** from the What, not restating it | ☑ | §4; "references, never re-derives" (§5); `derived_from` link (§9) | -| Each unit of work decomposes into an SPMC bundle before a model consumes it | ☑ | Work unit = one bounded transformation with frozen input (§5); §8 maps it to SPMC | - -### Section 2 — What completeness - -| Required element | Met? | Where | -|---|---|---| -| Identity — single unique name | ☑ | Each **system** is a first-class node with a name, used consistently downstream (§3.2.5, §10 rule 2) | -| Purpose — problem / who / why | ☑ | §1; each system carries a purpose (§3.2.5) | -| Actors — every person/role/system, what each may and may not do | ☑ | Triggers typed by source (user / external / automated) (§3.2.0); what an actor *may not* do is the Decider's reachable rejections (§3.3, Decider justification §6.3) | -| Behaviors — initiator, precondition, postcondition, **and unhappy path** | ☑ | Event model patterns (§3.2.0); the Decider derives commands/events/evolution and **must reject** ≥1 invariant — the unhappy path (§3.3) | -| Boundaries — in/out scope; out-of-scope **non-empty** | ◐ | In-scope is derived (feature footprints, allowlist); framework-level scope explicit (§1). A mandatory non-empty out-of-scope list per What is not enforced — see Known gaps | -| Data — every entity CRUD, with owner and lifecycle | ☑ | Domain model structure + data (§3.1); events `change` entities (§3.2, §9); reference vs production data distinguished (§10 rule 3) | -| Constraints — every non-derivable rule, with source and a way to verify | ☑ | Invariants in a constraint language (§3.1); constraints map to NFRs and are checked (§6, §9) | -| Acceptance Criteria — ≥1 testable criterion per behavior | ☑ | Versioned criteria per artifact kind; "done" is a predicate over behavioural conformance and acceptance criteria (§6.2, §7.2) | -| Open Questions — every known unknown, with owner and what it blocks | ◐ | Under-specification is a **measured signal** (intent-reliance rate, §3.2.1; data-divergence rate, §3.1) and undeclared decisions are surfaced (derivation Rule 5). A dedicated owner-tagged Open-Questions artifact is not a required construct — see Known gaps | - -### Section 3 — How completeness - -| Required element | Met? | Where | -|---|---|---| -| What Reference — identity and version derived from | ☑ | Work units reference the What/How by pointer; `derived_from` carries the upstream identity (§5, §9); Level 2 (§8.1) | -| Components — single-responsibility, dependencies directed and acyclic | ☑ | Contracts fix layering and isolation; decision logic kept in a pure isolable core (§4.2); structure enforced by the repository layout model (§4.3) | -| Data Model — a technical rep for every What entity, no orphans | ☑ | Derivation Rule "every data model entry maps to a What data entity"; domain conformance forbids structural drift (§6.3, §9) | -| Integrations — external system, direction, protocol, contract, **and failure** | ☑ | Interface contracts use industry standards generated from the domain model (§4.4); cross-system Translation pattern (§3.2.0); failure handled by error categories and `failed` read-model state (§3.2, §6.3) | -| Decisions — question, decision, rationale, rejected alternatives, driving What | ◐ | Decisions carry what/why/when-it-applies/when-it-does-not, by pointer (§4.1); driving What via `derived_from` (§9). "Rejected alternatives" is implied by "when it does not apply", not a mandated field — see Known gaps | -| Error Handling — a declared behavior per error category | ☑ | Decider rejections (§3.3); `failed` projection state (§3.2); error/seam verification kinds (§6.3) | -| Non-Functional Requirements — measurable threshold per What constraint | ☑ | Derivation Rule "every NFR maps to a What constraint"; verifications are deterministic gates (§6, §9) | -| Open Questions — propagated and structured as in the What | ◐ | Same mechanism and same gap as the What layer — see Known gaps | - -### Section 4 — The derivation contract (5 rules) - -| Rule | Met? | Where | -|---|---|---| -| Every component traces to ≥1 What behavior or data entity | ☑ | `derived_from` typed links (§9); rationale trace per work unit (§5) | -| Every data model entry maps to a What data entity | ☑ | §9; domain conformance verification (§6.3) | -| Every NFR maps to a What constraint | ☑ | §9; NFRs derive from constraints (§4, §6) | -| Every decision traces to a What element that required it | ☑ | Decisions linked to their driving What element (§4.1, §9) | -| Every How element with no What anchor is flagged as an undeclared product decision and escalated | ☑ | Surfaced, not silently blocked: the `escalate` verdict (§6.2) and the **earn-their-place rule** (§4.1) flag elements with no anchor; the intent-reliance rate measures the residue (§3.2.1) | - -### Section 5 — SPMC - -The work unit (§5) is the framework's SPMC bundle (§8 mapping). Coverage is partial -by design — the framework owns the **specification** axes and treats the -**Model** axis as the executor's concern beyond the Build seam (§1 open/closed, -§5.1). - -| Axis / property | Met? | Where | -|---|---|---| -| **Schema** — output shape; non-conforming output rejected | ☑ | One unit produces one artifact against versioned acceptance criteria; a failed verdict rejects it (§5, §6.2); the acceptance schema is dual-read like the layout model (§4.3) | -| Schema pins the shape language, recorded alongside | ☑ | Encoding fixes the shape language (SHACL or any validatable-shape language), recorded in the graph (§9) | -| **Prompt** — derived from What behaviors/criteria, filtered through How responsibility | ◐ | A unit reads its What concepts and How principles/patterns by pointer and re-derives nothing (§5); the framework does not name "prompt" as a separate pinned axis — see Known gaps | -| **Model** — selected by complexity remaining after the How | ☐ | **Out of scope by design.** Choosing/running the model is the executor's concern across the seam (§1, §5.1) — see Known gaps | -| Model pinned as a binding (precision/quantization/params) | ☐ | Out of scope — same reason | -| **Context** — assembled from the How, **frozen and bounded** at execution; no mid-execution retrieval | ☑ | Frozen, fully-inlined input bundle travelling by value with a content-hash identity; nothing fetched mid-run; freeze precedes emit (§5, §5.1, §6.2) | -| Each axis pinned to the precision at which it affects output | ◐ | The frozen bundle's **content hash is its identity**, so any change to schema/context is a distinct unit (§5.1); Model-axis pinning is out of scope | -| Axes independently attributable (a quality failure points to which axis) | ◐ | Per-criterion findings localize failures to a model element (§6.2) and the rationale trace retracts exactly the failed claims (§5); attribution is by graph element rather than by named SPMC axis | - -### Section 6 — Scale discipline - -| Requirement | Met? | Where | -|---|---|---| -| Same floor for a one-page feature as for a large project (same structure, smaller content) | ☑ | The framework defines shapes and rules only and applies them uniformly; authoring follows the order the derivation contract forces regardless of size (§10 rules 1 & 14–15) | -| A spec missing a required section or holding placeholder content is a draft and does not proceed | ☑ | Conformance is cumulative and claimed at the highest level **satisfied**; an incomplete What does not reach the How (§8.1, §10 rule 13); verification is by construction, not assertion (§6.1) | - -### Section 7 — The seam (framework hands work to an execution pillar) - -The Product Framework emits frozen work units to an executor, so the seam section applies — and is a first-class part of the design (§5.1). - -| Requirement | Met? | Where | -|---|---|---| -| The framework **defines** the handoff artifact's shape (contents, identity, return event) | ☑ | The Build seam fixes two schemas: an emitted work unit (by value, content-hash identity) and a self-describing verdict event (§5.1) | -| The unit is **frozen and self-contained** at emit; no callback to resolve | ☑ | "The freeze is the decoupling"; a unit needing a mid-execution callback is not frozen and must not be emitted (§5.1) | -| The interface **versions independently** of the framework (executor swap needs no framework change) | ☑ | A data contract with no live surface; either side can be rebuilt against the contract alone; by-value→by-reference is additive, not breaking (§5.1) | -| Any spec-side conformance gate the seam depends on runs **before emit** | ☑ | Layout conformance runs first and behavioural simulation runs before realisation; the freeze (and its gates) precede the emit (§6.3, §5.1) | - -## Pillar Two — Execution - -**Not claimed.** The Product Framework is a specification pillar with a published -socket (§5.1): it defines the Build seam and the verdict-event vocabulary, but it -deliberately specifies nothing about how a unit is scheduled, admitted, batched, -or run — that is the executor's concern, out of scope by the open/closed line -(§1, §5.1). It declares none of the eight Execution building blocks (workers, -capabilities, environment, tools, credentials, transition contract) and asserts no -closure contract. An execution framework conforming to Pillar Two can plug in -behind this seam without either side changing. - -## Known gaps - -Honest partial conformance, with intent for each item. - -| Gap | Status / plan | -|-----|---------------| -| **What/How Open Questions** — no dedicated owner-tagged "Open Questions" artifact with an explicit "what it blocks" field. | Functionally covered by measured under-specification signals (intent-reliance rate §3.2.1, data-divergence rate §3.1) and by Rule 5 / the `escalate` verdict surfacing undeclared decisions. Considering promoting an explicit Open-Question node kind in a future cut so each known unknown carries an owner and a blocked-set as graph data. | -| **Out-of-scope list per What** — the framework does not require every What instance to carry a non-empty explicit out-of-scope list (it declares scope at the framework level and derives in-scope footprints). | Under consideration as a What-side completeness rule; today the boundary is expressed structurally (bounded-context mappings, derived feature footprints, allowlist layout) rather than as a prose exclusion list. | -| **Decision "rejected alternatives"** — §4.1 mandates what/why/when-it-applies/when-it-does-not but not a discrete rejected-alternatives field. | "When it does not apply" carries the same information for traceability; a dedicated `rejected_alternatives` attribute is a candidate addition to the decision shape. | -| **SPMC Model axis** — the framework does not select, pin, or bind the model (precision/quantization/invocation params). | **Deliberate scope boundary**, not a defect: model choice and runtime live across the Build seam and belong to a Pillar Two executor (§1, §5.1). A Pillar One + Pillar Two pairing built on these foundations closes this axis. | -| **SPMC "Prompt" as a named axis** — the unit's instruction derives from What+How by pointer but is not pinned as a distinct labelled axis. | Coverage is by derivation and reference rather than by an explicit per-axis label; revisiting whether to name the prompt axis explicitly when (or if) the framework moves closer to the execution boundary. | - ---- - -*Statement last verified: 2026-06-26 against ai-development-foundations `13b0cb4`.* diff --git a/docs/audits/basis-quality-2026-08.md b/docs/audits/basis-quality-2026-08.md deleted file mode 100644 index 271166de..00000000 --- a/docs/audits/basis-quality-2026-08.md +++ /dev/null @@ -1,585 +0,0 @@ -# Basis-quality audit — pre-format-5 decisions (2026-08) - -**What is under test.** `DDD-method-06` — *mandatory claim-basis manufactures -weak claims* — filed `projected` when typed basis (`dec/ddd/typed-basis`, -decision format 5) was ruled, with this falsifier: - -> An audit of basis quality after typed basis (format 5) ships: if the -> pre-format-5 decisions' claim bases audit as genuine — each basedOn claim -> would have been filed on its own merits, none exists only to satisfy rule 3 -> — the manufactured-claims mechanism did not operate here and the claim dies. - -This document is that audit. **Section 1 was written and committed before any -rationale text was read or any classification made.** Nothing in it was -adjusted after results were seen; if the criteria look badly chosen in -hindsight, that is a finding about the pre-registration, not a licence to -re-cut it. - ---- - -## 1. Method, as pre-registered - -*(Committed before the data was gathered. Fixed.)* - -### 1.1 Population - -Every decision file under `.ddd/decisions/` whose `format:` field is **less -than 5** — i.e. filed before decision format 5, the format that made basis -typed. In such a file every `based_on` entry is an untyped `claim: ` -reference, because rule 3 admitted nothing else. - -Format-5 decisions are **excluded**: they were filed after the affordance -existed, so they cannot exhibit the mechanism under test. - -The unit of the headline reading is the **decision**. The unit of the table is -the **citation edge** (decision → claim). - -### 1.2 Per cited claim, record — mechanically - -| Signal | How it is computed | -|---|---| -| **Co-filing** | The commit that introduced the claim file (`git log --diff-filter=A -1`) compared with the commit that introduced the citing decision file. Same commit → `co-filed`. Different → `pre-existing`, with both hashes recorded. | -| **Citation count** | Number of **distinct decisions** (any format, whole store) whose `based_on` names the claim. `1` = single-purpose. | -| **Falsifier** | `present` / `absent` / `unfalsifiable`. See 1.3. | -| **Status** | The claim's `status:` field verbatim. | -| **Evidence** | Whether an `evidence:` field is present. | -| **Basis classification** | What the basis actually was, read from the **decision's own `rationale` / `title` text** — never from the claim, never from the `type` field. One quoted phrase per row, the phrase that decided it. See 1.4. | - -### 1.3 Falsifier grading — criteria fixed in advance - -A falsifier is **`present-but-unfalsifiable`** if no observation of this repo, -its toolchain, or its use could ever produce the stated condition. Three -disqualifiers, one word recorded per claim: - -- **`restatement`** — it names no observable event, only the negation of the - statement ("if X turns out not to hold"). -- **`vapour`** — it is conditioned on an artifact or milestone that does not - exist and has no scheduled existence, so the observation can never be made. -- **`taste`** — it requires a judgement of quality or intent with no stated - threshold, so no observation settles it. - -Everything else with a `falsifier:` field is **`present`**. A falsifier that is -merely *unlikely to fire*, or that names a future-but-scheduled milestone, is -`present` — hard to falsify is not unfalsifiable. - -### 1.4 Basis vocabulary — definitions fixed in advance - -Read from the decision's rationale text. The question is *what the rationale -actually argues from*, not what it cites. - -- **claim** — the rationale argues from a **contestable proposition about the - world** that could be shown false by observation, and the decision follows - from it. Form: "X does/does not Y", "X is sufficient for Y". -- **constraint** — the rationale argues from something the project **cannot - change**: a language, tool, or format property; a host-system fact. Form: - "Rust has no X", "the format requires Y". -- **mandate** — the rationale argues from an **authority requiring it**: a - principal's ruling, a spec, an upstream or house rule. Form: "ruled at - review", "the PRD requires", "policy is". -- **preference** — the rationale argues from **taste, ergonomics, or style**, - with no falsifiable proposition. Form: "cleaner", "reads better", "we prefer". -- **experiment** — the rationale is explicitly **provisional, adopted to find - out**. Form: "try it and see", "for now". -- **risk-acceptance** — the rationale **acknowledges a known cost or gap and - prices it**. Form: "priced, not paid", "accepted cost", "we take the hit". -- **indeterminate** — the text supports two or more of the above and **no - phrase discriminates them**. Recorded as a result, not resolved by guessing. - -A row where the classification was genuinely close is additionally marked -**borderline** and listed in §5 with its quote, for the principal's ruling. It -still receives its best-reading classification in the table; borderline is an -annotation, not a third value. - -### 1.5 Weak-claim composite — fixed in advance - -A cited claim is **weak** iff it meets **two or more** of: - -- **W1** — co-filed with the citing decision in the same commit; -- **W2** — cited by exactly one decision; -- **W3** — falsifier `absent` or `unfalsifiable`; -- **W4** — status `projected` **and** no evidence attached. - -Two-of-four, not one: any single signal has an innocent reading (a claim filed -alongside its first consumer is normal practice; a claim cited once may simply -be young). Two together is the pattern the claim predicts. - -### 1.6 The reading - -A citation edge is **manufactured** iff **both**: - -- the citing decision's basis classification is **not `claim`** (it is mandate, - preference, experiment, risk-acceptance, or constraint), **and** -- the cited claim is **weak** by §1.5. - -A **decision** counts as a **manufactured-basis decision** iff **every** claim -it cites is manufactured. The `every` is deliberate and tight: if a decision -cites one genuine claim alongside one weak one, rule 3 was already satisfied by -the genuine one and nothing needed inventing. Only a decision with no genuine -claim to stand on was *forced* to manufacture. - -**The audit's result is that count, over the population.** - -### 1.7 Verdict bands — fixed in advance - -Chosen before the count was known: - -| Count of manufactured-basis decisions | Verdict | Proposed status | -|---|---|---| -| **0** | The falsifier fired exactly as written. | `retired` | -| **1–3** (≤ ~7%) | Mechanism operated, but marginally. Report it as *held weakly* and say plainly that the typed-basis ruling rests mostly on its ergonomic argument. | `reported`, weakly | -| **≥ 4** (≥ ~10%) | Prediction held. | `reported` | - -**Underpowered** is claimed only if **both**: fewer than 10 decisions in the -population classify as non-`claim` basis, **and** more than 30% of -classifications come out `indeterminate` — i.e. the rationale texts cannot -discriminate the types at all. Population size alone is not grounds; 41 is not -a small N for this question. - -### 1.8 Constraints on this audit - -Nothing in the store is changed. No re-typing of bases, no claim edits, no -retirements, no status moves, no cleanup of anything noticed. Every finding in -§6 is a **proposal for the principal's ruling**, filed unactioned. - ---- - -## 2. Population - -> *Note on §1:* the pre-registered text above is left byte-identical to what was -> committed in `9488ba6`, including two cross-references that the final section -> numbering moved — §1.4's "listed in §5" is now §6, and §1.8's "finding in §6" -> is now §8. Correcting them in place would edit a committed pre-registration; -> they are pointed out here instead. - -**41 decisions**, every one under `.ddd/decisions/` at `format: 2`. The store -holds 44 decision files; the three at `format: 5` — `dec/ddd/typed-basis`, -`dec/ddd/prd-split`, `dec/ddd/m8-enforcement-closure` — are excluded per §1.1. -No decision sits at format 1, 3 or 4. - -All 41 cite at least one claim; none cites anything else, because rule 3 -admitted nothing else. Together they make **52 citation edges** over **41 -distinct claims** (of the 77 claims in the store). - -Mechanical totals over the 52 edges: - -| Signal | Count | -|---|---| -| Co-filed with the citing decision (same commit) | 31 / 52 | -| Cited claim is single-purpose (1 citing decision) | 31 / 52 | -| Cited claim `projected` with no evidence | 17 / 52 | -| Cited claim has no `falsifier` field at all | **0** / 52 | -| Cited claim's falsifier graded unfalsifiable (§1.3) | 8 / 52 (4 distinct claims) | - ---- - -## 3. The per-claim table - -`W` lists which of the §1.5 signals fired. **Weak** = two or more. **Manuf.** -= the §1.6 conjunction: non-`claim` basis **and** weak claim. - -| Decision (pre-format-5) | Cited claim | Co-filed | Cites | Falsifier | Status / ev. | W | Weak | Basis read from rationale | Manuf. | -|---|---|---|---|---|---|---|---|---|---| -| `bicep/linter-at-error` | DDD-bicep-ver-01 | no | 1 | present | reported + ev | W2 | no | claim | | -| `bicep/linter-at-error` | DDD-bicep-sec-01 | no | 1 | present | reported + ev | W2 | no | claim | | -| `bicep/linter-at-error` | DDD-bicep-sec-02 | no | 1 | present | reported + ev | W2 | no | claim | | -| `bicep/psrule-in-pr-stage` | DDD-bicep-net-01 | no | 1 | present | reported + ev | W2 | no | claim | | -| `bicep/psrule-in-pr-stage` | DDD-bicep-diag-01 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/async-policy-rules-at-error` | DDD-cs-pol-03 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/async-policy-rules-at-error` | DDD-cs-pol-04 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/authorization-fallback-deny` | DDD-cs-authz-01 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/disposal-rules-at-error` | DDD-cs-disp-02 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/disposal-rules-at-error` | DDD-cs-disp-05 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/general-catch-at-error` | DDD-cs-fail-01 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `cs/policy-rules-at-error` | DDD-cs-pol-01 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `cs/startup-assertions-required` | DDD-cs-map-01 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/startup-assertions-required` | DDD-cs-conc-02 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/unsafe-reinterpretation-banned` | DDD-cs-tc-04 | no | 1 | present | reported + ev | W2 | no | claim | | -| `cs/unsafe-reinterpretation-banned` | DDD-cs-tc-05 | no | 1 | present | reported + ev | W2 | no | claim | | -| `ddd/adapter-policy-tables` | DDD-adapter-01 | **yes** | 2 | present | established + ev | W1 | no | claim | | -| `ddd/amend-explicit-evidence-frozen` | DDD-friction-02 | **yes** | 2 | present | projected / no ev | W1 W4 | **weak** | claim | | -| `ddd/amend-explicit-evidence-frozen` | DDD-arch-04 | **yes** | 1 | unfalsifiable (vapour) | projected / no ev | W1 W2 W3 W4 | **weak** | claim | | -| `ddd/correspondence-rows-are-stratified` | DDD-arch-06 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `ddd/curation-over-mining` | DDD-method-02 | **yes** | 2 | present | reported + ev | W1 | no | claim | | -| `ddd/enforce-matching-tightens-to-symbol` | DDD-arch-05 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `ddd/enum-member-gap-priced` | DDD-adapter-03 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `ddd/fixtures-not-sdk` | DDD-detect-02 | **yes** | 2 | present | reported + ev | W1 | no | claim | | -| `ddd/git-is-the-amend-trail` | DDD-method-04 | **yes** | 1 | present | projected / no ev | W1 W2 W4 | **weak** | constraint | **YES** | -| `ddd/interceptor-not-extension` | DDD-arch-03 | **yes** | 1 | present | projected / no ev | W1 W2 W4 | **weak** | indeterminate | | -| `ddd/internal-not-surface` | DDD-adapter-02 | **yes** | 1 | present | projected / no ev | W1 W2 W4 | **weak** | mandate | **YES** | -| `ddd/lsp-as-seam` | DDD-arch-02 | **yes** | 2 | present | projected / no ev | W1 W4 | **weak** | claim | | -| `ddd/m6-proceeds-no-flip` | DDD-adapter-01 | no | 2 | present | established + ev | — | no | mandate | | -| `ddd/pins-at-m2` | DDD-method-02 | no | 2 | present | reported + ev | — | no | claim | | -| `ddd/predicates-carry-no-status` | DDD-method-01 | **yes** | 1 | unfalsifiable (vapour) | reported + ev | W1 W2 W3 | **weak** | mandate | **YES** | -| `ddd/rejection-facts-prefilled` | DDD-friction-01 | **yes** | 2 | present | projected / no ev | W1 W4 | **weak** | claim | | -| `ddd/report-coverage-explicit` | DDD-method-03 | **yes** | 2 | unfalsifiable (taste) | projected / no ev | W1 W3 W4 | **weak** | claim | | -| `ddd/rust-class-enforced-here` | DDD-friction-01 | no | 2 | present | projected / no ev | W4 | no | experiment | | -| `ddd/rust-class-enforced-here` | DDD-friction-02 | no | 2 | present | projected / no ev | W4 | no | experiment | | -| `ddd/rust-host-is-real` | DDD-detect-02 | no | 2 | present | reported + ev | — | no | claim | | -| `ddd/rust-host-is-real` | DDD-arch-02 | no | 2 | present | projected / no ev | W4 | no | claim | | -| `ddd/sarif-unification` | DDD-detect-01 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `ddd/seed-lands-in-claims-not-shared` | DDD-method-05 | **yes** | 1 | unfalsifiable (vapour) | projected / no ev | W1 W2 W3 W4 | **weak** | claim | | -| `ddd/v1-scope` | DDD-scope-01 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `ddd/what-boundaries-priced-not-paid` | DDD-what-04 | **yes** | 1 | present | projected / no ev | W1 W2 W4 | **weak** | risk-acceptance | **YES** | -| `ddd/what-policy-table` | DDD-what-01 | **yes** | 1 | present | projected / no ev | W1 W2 W4 | **weak** | claim | | -| `ddd/what-policy-table` | DDD-what-03 | **yes** | 2 | present | reported + ev | W1 | no | claim | | -| `ddd/what-published-qualifier` | DDD-what-03 | **yes** | 2 | present | reported + ev | W1 | no | claim | | -| `ddd/why-resolves-three-ways` | DDD-method-03 | **yes** | 2 | unfalsifiable (taste) | projected / no ev | W1 W3 W4 | **weak** | claim | | -| `ddd/workspace-member-delivery` | DDD-arch-01 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | claim | | -| `rust/no-unwrap` | DDD-gates-01 | **yes** | 1 | present | reported + ev | W1 W2 | **weak** | constraint | **YES** | -| `web/htmlcss-enforce` | DDD-web-01 | **yes** | 3 | present | reported + ev | W1 | no | claim | | -| `web/markup-validity` | DDD-web-01 | no | 3 | present | reported + ev | — | no | claim | | -| `web/plain-pair-scope` | DDD-web-01 | **yes** | 3 | present | reported + ev | W1 | no | claim | | -| `web/render-status-palette` | DDD-web-02 | **yes** | 2 | present | projected / no ev | W1 W4 | **weak** | claim | | -| `web/token-discipline` | DDD-web-02 | **yes** | 2 | present | projected / no ev | W1 W4 | **weak** | claim | | - -### 3.1 Falsifier gradings that are not `present` - -Four of the 41 cited claims were graded unfalsifiable under §1.3. Every other -cited claim carries a falsifier naming an observable event, so it is graded -`present` — including ones that are merely hard to fire (`DDD-adapter-02`, -`DDD-what-01`, `DDD-what-04`), per the §1.3 sentence that hard-to-falsify is -not unfalsifiable. - -| Claim | Word | Why | -|---|---|---| -| `DDD-arch-04` | **vapour** | Falsifier asks for "a study over the rows in which declarant-supplied structural fields proved as reliable as the LSP-derived ones". `dec/ddd/amend-explicit-evidence-frozen` — the decision this claim grounds — abolished declarant-supplied structural fields, so the rows can never contain the contrast the study needs. Self-sealing. | -| `DDD-method-01` | **vapour** | Falsifier asks for "a predicate whose embedded status would have stayed accurate". `dec/ddd/predicates-carry-no-status` makes a status-bearing predicate a schema rejection (`deny_unknown_fields`), so the artifact the observation requires cannot exist in the store. Self-sealing. | -| `DDD-method-03` | **taste** | Falsifier asks for "a repo where surfacing the uncheckable set changed no reader's action". Reader action, no threshold, and no instrument named anywhere in the store that would settle it. | -| `DDD-method-05` | **vapour** | Falsifier asks for "a body of entries filed into `shared/` at authoring time whose observed transfer rate matched…". `dec/ddd/seed-lands-in-claims-not-shared` forbids filing into `shared/` at authoring time, so the body of entries cannot accrue here. Self-sealing. | - -Worth recording as a pattern in its own right: **three of the four are -self-sealing in the same way** — the decision the claim grounds removes the -arrangement under which the claim's falsifier could ever be observed. That is -not the mechanism `DDD-method-06` predicts, and it is not counted toward the -reading. It is filed as finding **F-8**. - -`DDD-method-02` was graded **present**, though close: its decisive term -("entries remain explainable to a named principal") is a judgement, but the -store names the instrument that produced its evidence — the PRD §2 warning-swamp -analysis — and the same instrument would settle the falsifier. Recorded as a -borderline grading in §5. - ---- - -## 4. Basis classification, read from each decision's own rationale - -One row per decision, with the phrase that decided it. Classified per §1.4, -from the rationale text alone. - -| Decision | Basis | The phrase that decided it | -|---|---|---| -| `ddd/adapter-policy-tables` | claim | "the tables are themselves claims, falsifiable against where boundary defects occur" | -| `ddd/amend-explicit-evidence-frozen` | claim | "because the correspondence dataset is only evidence while those fields stay machine-authored" | -| `bicep/linter-at-error` | claim | "Adopted with its limit stated, because DDD-bicep-sec-02 measured it" | -| `bicep/psrule-in-pr-stage` | claim | "DDD-bicep-net-01 and DDD-bicep-diag-01 measured both halves" | -| `ddd/correspondence-rows-are-stratified` | claim | "DDD-arch-06 says that question is unanswerable here, and a reader who does not know the rows are stratified will answer it anyway and be wrong" | -| `cs/async-policy-rules-at-error` | claim | "A repo that reads a green CA2016 as 'cancellation is handled' has misread it, and DDD-cs-pol-04 is the entry that says so" | -| `cs/authorization-fallback-deny` | claim | "DDD-cs-authz-01 measured the default: an endpoint with no attribute answers 200 OK" | -| `cs/disposal-rules-at-error` | claim | "Adopted with an unusual caveat, because DDD-cs-disp-05 measured it" | -| `cs/general-catch-at-error` | claim | "DDD-cs-fail-01 establishes that CA1031 reports the general-catch shape from the SDK analyzers alone" | -| `cs/policy-rules-at-error` | claim | "DDD-cs-pol-01 establishes that CA1305, CA1307, CA1309 and CA1310 close the comparison and culture cases" | -| `cs/startup-assertions-required` | claim | "Both close nothing unless the assertion actually runs, which makes them the most likely entries in the catalog to be recorded as closed while being inert" | -| `cs/unsafe-reinterpretation-banned` | claim | "A residual with no rejection witness cannot be allocated to review, because review has nothing to find" | -| `ddd/curation-over-mining` | claim | "nothing is mined into the catalog, so every entry has a principal who can explain it" | -| `ddd/enforce-matching-tightens-to-symbol` | claim | "the file arm of match_declarations is too broad to sustain what enforce mode claims" | -| `ddd/enum-member-gap-priced` | claim | "DDD-adapter-03 establishes that the §9.1 table is wrong by omission rather than by a wrong row" | -| `ddd/fixtures-not-sdk` | claim | "two #[ignore]d integration tests regenerate the SARIF from real builds with the documented invocations for local verification" | -| **`ddd/git-is-the-amend-trail`** | **constraint** | "`.ddd/` is committed, so git already carries who amended what and when" | -| **`ddd/interceptor-not-extension`** | **indeterminate** | "the check is part of the arrangement the agent edits through, not the agent's residual discretion" — states the design and its properties; nothing in the text says what settled the choice between feasibility (a claim), fork-avoidance (a constraint) and architectural cleanliness (a preference) | -| **`ddd/internal-not-surface`** | **mandate** | "Settles PRD §14 question 2 with the stated default" | -| `ddd/lsp-as-seam` | claim | "This bounds the blast radius of prerelease language-server churn to version pins and keeps the core language-free" | -| **`ddd/m6-proceeds-no-flip`** | **mandate** | "The condition therefore could not be evaluated against filed evidence; it was resolved by the stated default" | -| `ddd/pins-at-m2` | claim | "Escaped-decision reporting is load-bearing for the tool's thesis — the warning swamp is the canonical escape" | -| **`ddd/predicates-carry-no-status`** | **mandate** | "The split is enforced by schema (deny_unknown_fields rejects a status key on a predicate entry, naming ontology rule 1), not by convention or review" | -| `ddd/rejection-facts-prefilled` | claim | "Pre-filling the judgment invites rubber-stamping; omitting the facts wastes the agent's calls re-deriving them" | -| `ddd/report-coverage-explicit` | claim | "Every section of `ddd report escapes` separates checked-and-clean from not-checkable and names the uncheckable set" | -| **`ddd/rust-class-enforced-here`** | **experiment** | "Running the tail of this session governed is the first live reading of the friction falsifier on a real repo" | -| `ddd/rust-host-is-real` | claim | "a rust-analyzer change that moves symbol kinds, impl-block naming, or the readiness protocol fails the suite instead of passing a mock that was written to agree with the adapter" | -| `ddd/sarif-unification` | claim | "both toolchains emit SARIF through supported switches, verified at M2 and recorded in the README" | -| `ddd/seed-lands-in-claims-not-shared` | claim | "Intent is cheap to declare and, once declared, indistinguishable in the store from a transferability that was actually observed" | -| `ddd/v1-scope` | claim | "Scope follows where Context& demand concentrates and where adapter cost is lowest" | -| **`ddd/what-boundaries-priced-not-paid`** | **risk-acceptance** | "The finding stays standing and visible rather than being suppressed or satisfied cheaply" | -| `ddd/what-policy-table` | claim | "The table names what forms a boundary … and everything else … is internal elaboration that never demands a declaration" | -| `ddd/what-published-qualifier` | claim | "The unqualified rows called every event and command boundary-forming, which the measurement killed (DDD-what-02): 39 elements selected, 0 of them actually crossing anything" | -| `ddd/why-resolves-three-ways` | claim | "an ungoverned detected rule is a real escape — but reads as a governance failure rather than a lookup failure" | -| `ddd/workspace-member-delivery` | claim | "Basis rechecked against the current repo state at M1 and confirmed by the M1 implementation itself" | -| **`rust/no-unwrap`** | **constraint** | "Every unwrap is an undeclared panic path" | -| `web/htmlcss-enforce` | claim | "the observed failure mode is agents drifting past ungoverned HTML↔CSS seams, and `warn` is exactly the exhortation that failure mode ignores" | -| `web/markup-validity` | claim | "always a copy-paste artifact, and one that silently masks a missing *second* class the author meant to add, which is how orphan references are born" | -| `web/plain-pair-scope` | claim | "Context&'s visual deliverables are single-file HTML+CSS, so the plain pair is where the demand is" | -| `web/render-status-palette` | claim | "the palette is one choice — status colours mean verdicts together or not at all" | -| `web/token-discipline` | claim | "a raw restatement is not a lesser form of the token — it is the drift the token exists to prevent" | - -### 4.1 Classification totals - -| Basis actually argued from | Decisions | Share of 41 | -|---|---|---| -| claim | 33 | 80.5% | -| mandate | 3 | 7.3% | -| constraint | 2 | 4.9% | -| experiment | 1 | 2.4% | -| risk-acceptance | 1 | 2.4% | -| preference | **0** | 0% | -| indeterminate | 1 | 2.4% | - -`preference` came out empty. Worth saying plainly, because it is the type the -typed-basis vocabulary's critics would expect to dominate a backfill: not one -pre-format-5 rationale argues from taste alone. The indeterminate count is -1/41 = 2.4%. - - ---- - -## 5. The reading - -Applying §1.6 to the table: - -> **5 of the 41 pre-format-5 decisions are manufactured-basis decisions** — -> every claim they cite is both weak by §1.5 and cited by a decision whose -> real basis, read from its own rationale, was not a claim. -> -> **5 / 41 = 12.2%.** - -The five: - -| Decision | Real basis | Sole cited claim | Why the claim is weak | -|---|---|---|---| -| `ddd/git-is-the-amend-trail` | constraint | `DDD-method-04` | co-filed · single-purpose · projected, no evidence | -| `ddd/internal-not-surface` | mandate | `DDD-adapter-02` | co-filed · single-purpose · projected, no evidence | -| `ddd/what-boundaries-priced-not-paid` | risk-acceptance | `DDD-what-04` | co-filed · single-purpose · projected, no evidence | -| `ddd/predicates-carry-no-status` | mandate | `DDD-method-01` | co-filed · single-purpose · falsifier self-sealing | -| `rust/no-unwrap` | constraint | `DDD-gates-01` | co-filed · single-purpose | - -Per §1.7 that lands in the **≥ 4** band: **the prediction held.** - -### 5.1 How strongly — the honest decomposition - -The five are not equally strong, and the count should not be read as five -instances of one thing. - -**Three are unambiguous.** `DDD-method-04`, `DDD-adapter-02` and `DDD-what-04` -each fire W1 + W2 + W4: filed in the same commit as the one decision that has -ever cited them, `projected`, no evidence attached. Each restates its citing -decision's rationale back at it — `DDD-method-04` restates "git carries -history", `DDD-adapter-02` restates the app-repo posture, `DDD-what-04` -restates "these graphs have no second party". That is the shape -`DDD-method-06` describes: a claim written so a decision could cite one. - -**One is medium.** `DDD-method-01` carries a `reported` status and real -evidence (the framework corpus's predicate/claim split), so it is not -invented — but it is single-purpose, co-filed, and its falsifier is -self-sealing (§3.1). The decision it grounds argues from ontology rule 1, not -from observed drift. - -**One is marginal, and may be a false positive of the measure.** -`DDD-gates-01` is `reported`, carries evidence that was actually read ("without -a single waiver request"), and its falsifier is concrete and firable. It is -weak only on W1 + W2 — co-filed and single-purpose — which is the composite's -weakest firing pattern. A reasonable ruling is that it would have been filed on -its own merits, in which case `rust/no-unwrap` drops out. - -**This does not change the verdict.** The kill condition in §1.7 was **0**. -Even ruling both `DDD-gates-01` and `DDD-method-01` genuine, the floor is -**3 / 41**, which is band "held weakly" — still `reported`, never `retired`. -The claim survives under every reading of its own borderline calls. - -### 5.2 The secondary reading, which is stronger than the primary - -The mechanism `DDD-method-06` names has two steps: rule 3 forces a claim slot, -and the slot gets filled with something manufactured. The audit measures the -second step. **The first step is confirmed outright:** - -> **7 of 41 (17.1%)** pre-format-5 decisions argue from a basis that is not a -> claim — 3 mandate, 2 constraint, 1 experiment, 1 risk-acceptance — and **all -> 7 cite a claim anyway**, because format 2 offered no other slot. An eighth -> (`ddd/interceptor-not-extension`) is indeterminate and also cites a claim. - -That is the precondition of the mechanism, present in a fifth of the -population, with no counter-example: not one pre-format-5 decision left -`based_on` empty or found another way to say what it rested on. Whether the -claim in the slot was *manufactured* is the 5-of-7 question above; whether the -slot was *misfilled* is 7-of-7 among non-claim bases. - -### 5.3 What the measure got wrong, reported not repaired - -The weak-claim composite (§1.5) turned out to have **little discriminating -power in this store**: it flags **24 of 52 edges** and **22 of the 41 distinct -cited claims** as weak. Co-filing (W1, 31/52) and single-purpose citation (W2, -31/52) are simply the house style here — claims are filed in the same commit as -the work that motivated them, and most are cited once — so two-of-four fires on -claims nobody would call manufactured, including the whole evidence-backed -C#/Bicep seed. - -The result is therefore carried almost entirely by the **basis classification**, -not by the weakness composite: of the 7 non-claim-basis decisions, the composite -excluded only 2 (`ddd/m6-proceeds-no-flip`, `ddd/rust-class-enforced-here`). - -This is recorded, not fixed. Re-cutting W1/W2 after seeing that they fire -everywhere is exactly the adjustment §1 forbids, and it would move the number. -The correct response is to say the composite was poorly chosen for this store -and let the principal weigh the result knowing that. - -### 5.4 The underpower test, run as pre-registered - -§1.7 required **both** arms. First arm: fewer than 10 decisions classify as -non-`claim` — **met** (7, or 8 counting indeterminate). Second arm: more than -30% indeterminate — **not met** (1 decision, 2.4%). The conjunction fails, so -**underpowered is not claimed.** - -That the first arm fired is worth the principal's attention anyway: the -discriminating subpopulation is 7 decisions, and a single reclassification moves -the count by ~2.4 points. The rationale texts are unusually explicit — they -routinely name their own limits — which is why the indeterminate rate came out -so low; a store with terser rationales would not classify this cleanly. - ---- - -## 6. Borderline set — for the principal's ruling - -Recorded per constraint 4: not consulted, not guessed away. Each carries its -best-reading classification in §4; these are the ones where the text -genuinely pulled two ways. **The three that would move the reading are marked -★.** - -| Decision | Recorded as | Pulled toward | The phrase | -|---|---|---|---| -| ★ `rust/no-unwrap` | constraint | claim | "Every unwrap is an undeclared panic path" — true by definition of `unwrap` (a language fact), but `DDD-gates-01`'s cost proposition ("at negligible authoring cost in this codebase") is contestable and does not appear in the rationale at all | -| ★ `ddd/predicates-carry-no-status` | mandate | claim | "Closure lives only in claims, with status and falsifiers" — restates the ontology; the drift argument that `DDD-method-01` makes is nowhere in the rationale | -| ★ `ddd/interceptor-not-extension` | **indeterminate** | claim / preference / constraint | "Neither language server is extended or forked" — fork-avoidance (constraint), client-side feasibility (claim, = `DDD-arch-03`) and architectural cleanliness (preference) are all present and none is marked as decisive. Its claim `DDD-arch-03` is weak (W1+W2+W4), so ruling this `mandate`/`constraint`/`preference` would make it a sixth manufactured-basis decision; ruling it `claim` leaves the count at 5 | -| `ddd/curation-over-mining` | claim | mandate | "The graph is the source of truth and the code must conform to it" — stipulated posture; the `DDD-method-02` proposition survives only in the trailing "so every entry has a principal who can explain it" | -| `ddd/internal-not-surface` | mandate | preference | "Settles PRD §14 question 2 with the stated default" — adopting a stated default could be read as deference to the PRD (mandate) or as taste (preference). Either way it is not a claim, so the count is unaffected | -| `ddd/lsp-as-seam` | claim | preference | "keeps the core language-free (one adapter + policy table per new language, by design)" — architectural taste, but "bounds the blast radius … to version pins" is contestable and could be shown false | -| `ddd/enum-member-gap-priced` | claim | risk-acceptance | "Not paid in M5. … this milestone is curation." — the gap is claim-grounded (`DDD-adapter-03`, measured), the deferral is scope. Reclassifying would not add a manufactured decision: `DDD-adapter-03` is `reported` with evidence | -| `ddd/enforce-matching-tightens-to-symbol` | claim | risk-acceptance | "Priced, not paid, in M5." — same shape; `DDD-arch-05` is `reported` with an observation that already fired | -| `ddd/correspondence-rows-are-stratified` | claim | risk-acceptance | "Consequence accepted: the rows support within-stratum questions … and no question whose denominator is all edits" | -| `cs/startup-assertions-required` | claim | mandate | "the decision is not 'these are closable' — the claims already say that" — says outright that the claims are not the decision's content; the basis is still the inertness proposition in the same paragraph | -| `ddd/report-coverage-explicit` | claim | constraint | "counting unpinned edges as escapes would break CI for every format-1 repo and punish a supported format" — the not-a-gate half rests on format compatibility, the state-your-coverage half on `DDD-method-03` | -| `ddd/pins-at-m2` | claim | preference | "so the format bump lands with M2 rather than waiting for a later milestone" — the sequencing is a priority call; the necessity ("without the pin, `report escapes` cannot say whether the ground moved") is mechanism, not observation | -| `ddd/fixtures-not-sdk` | claim | preference | "so workspace CI stays hermetic and fast" — "fast" is preference; "hermetic" is a property `DDD-detect-02` measured | -| `ddd/amend-explicit-evidence-frozen` | claim | preference | "so create and amend cannot be confused in either direction and no silent upsert exists" — API taste; the decisive clause is the evidence-freezing one | -| `web/markup-validity` | claim | — | Not a type dispute: the proposition the rationale argues from ("always a copy-paste artifact … which is how orphan references are born") is **not what `DDD-web-01` states**. The basis is a claim; the cited claim is a different one. Filed as F-9 | -| `DDD-method-02` (falsifier grading) | present | unfalsifiable (taste) | "A mined catalog whose entries remain explainable to a named principal over time" — a judgement, but the store names the instrument (PRD §2 warning-swamp analysis) that produced its evidence and would settle it | - -**Net effect if every ★ is ruled the other way:** `ddd/interceptor-not-extension` -becomes manufactured (+1) and `rust/no-unwrap` and `ddd/predicates-carry-no-status` -stay manufactured — count 6/41. If ruled toward `claim` instead: 3/41. **The -band is `reported` across the whole range.** - ---- - -## 7. Status proposal for `DDD-method-06` — for the principal's ruling, and no other authority - -**Proposed: `projected` → `reported`.** - -The argument, from the reading and nothing else: - -1. **The falsifier as written did not fire.** It required that the pre-format-5 - claim bases "audit as genuine — each `basedOn` claim would have been filed on - its own merits, none exists only to satisfy rule 3." Three claims — - `DDD-method-04`, `DDD-adapter-02`, `DDD-what-04` — are single-purpose, - co-filed with their one consumer, `projected`, unevidenced, and restate - their citing decision's own rationale. Each grounds a decision whose - rationale argues from a constraint, a mandate, and a risk-acceptance - respectively. "None exists only to satisfy rule 3" is false. - -2. **The mechanism's precondition is confirmed at 7/41.** A fifth of the - population argues from a non-claim basis and cites a claim regardless, with - no counter-example. That is rule 3 doing exactly what the claim says it - does, independent of how the weak-claim composite is graded. - -3. **The strength is moderate, not strong.** 5/41 = 12.2%, just over the - pre-registered ≥4 threshold, and only 3 of the 5 are unambiguous. The - corruption is real and bounded — it did not swamp the store. `reported` - ("exercised evidence") is the right status; `established` ("checker-engaged") - is not claimed and nothing here supports it. - -4. **The verdict is robust to every borderline call in §6.** Across the full - range of rulings the count moves between 3 and 6. Killing the claim required - 0. No reading of this population retires it. - -**Why not `retired`:** the kill condition did not come close. **Why not -underpowered:** the pre-registered conjunction failed (§5.4) — the population -discriminated, with a 2.4% indeterminate rate. - -**What the principal should discount:** §5.3. The weak-claim composite was -badly chosen for this store and carries almost none of the result; the basis -classification carries all of it. A ruling that trusts the number should be a -ruling that trusts the §4 reading of 41 rationale texts, which is a judgement -call made against fixed definitions — not a mechanical measurement. - -### 7.1 If the ruling is `reported`, the evidence text to attach - -Drafted, **not applied** — no file in `.ddd/` was touched by this audit. - -```yaml -status: reported -evidence: > - Basis-quality audit of the 41 pre-format-5 decisions, 2026-08-11 - (docs/audits/basis-quality-2026-08.md), method pre-registered before the - data was read. 7 of 41 decisions (17.1%) argue from a basis that is not a - claim — 3 mandate, 2 constraint, 1 experiment, 1 risk-acceptance — and all - 7 cite a claim anyway, because format 2 offered no other slot. In 5 of - those 7 the cited claim is itself weak: co-filed with its one consumer, - single-purpose, and mostly projected with no evidence. Three are - unambiguous — DDD-method-04, DDD-adapter-02, DDD-what-04 — each restating - its citing decision's own rationale back at it. Two (DDD-method-01, - DDD-gates-01) rest on the weakest firing pattern of the audit's composite - and may be false positives; the count survives at 3/41 if both are ruled - genuine. Recorded limit: the audit's weak-claim composite flagged 22 of 41 - cited claims, so it discriminated poorly and the result rests on the basis - classification of the 41 rationale texts. -``` - ---- - -## 8. Findings — filed unactioned, for per-entry ruling *after* the status question - -Nothing below was acted on. Per constraint 1 and the sequencing in the brief, -these are held until the status question in §7 is settled, so the audit's -result is not contaminated by cleanup. - -### 8.1 Weak claims worth considering for retirement or repair - -| # | Claim | Why | Note | -|---|---|---|---| -| F-1 | `DDD-arch-04` | All four weakness signals. Falsifier self-sealing: the study it names cannot be run because the decision it grounds abolished the data | Retire, or rewrite the falsifier against a store that still has both arms | -| F-2 | `DDD-method-05` | All four. Falsifier self-sealing in the same way | Same | -| F-3 | `DDD-method-01` | Falsifier self-sealing; single-purpose; co-filed. Carries genuine evidence, so repair may beat retirement | Rewrite the falsifier | -| F-4 | `DDD-method-03` | Falsifier ungraded-able ("changed no reader's action", no instrument); projected, unevidenced, both citations co-filed | Name an instrument or retire | -| F-5 | `DDD-method-04` | Restates a git property as a proposition; projected, unevidenced, single-purpose, co-filed | Candidate for retirement as `constraint` re-typing (see F-6) | -| F-6 | `DDD-adapter-02`, `DDD-what-04` | Projected, unevidenced, single-purpose, co-filed. Both restate their citing decision's rationale | Both have firable falsifiers if the world supplies the case; retirement is not obviously right | - -### 8.2 Decisions whose basis type is visibly wrong - -| # | Decision | Filed as | Reads as | Note | -|---|---|---|---|---| -| F-7a | `ddd/git-is-the-amend-trail` | claim | **constraint** | | -| F-7b | `rust/no-unwrap` | claim | **constraint** | see ★ in §6 | -| F-7c | `ddd/internal-not-surface` | claim | **mandate** | | -| F-7d | `ddd/predicates-carry-no-status` | claim | **mandate** | see ★ in §6 | -| F-7e | `ddd/m6-proceeds-no-flip` | claim | **mandate** | **Also a content mismatch:** it cites `DDD-adapter-01`, which is about policy-table localisation and says nothing about the M6/M7 flip condition. The rationale states outright that the condition "could not be evaluated against filed evidence". The cited claim is genuine but unrelated | -| F-7f | `ddd/rust-class-enforced-here` | claim | **experiment** | Honestly self-described in its own rationale; the claims it cites are the ones it exists to test | -| F-7g | `ddd/what-boundaries-priced-not-paid` | claim | **risk-acceptance** | | -| F-7h | `ddd/interceptor-not-extension` | claim | **indeterminate** | Needs the principal to say what settled it | - -### 8.3 Other - -| # | Finding | -|---|---| -| F-8 | **Self-sealing falsifiers are a pattern, not three coincidences.** In `DDD-arch-04`, `DDD-method-01` and `DDD-method-05`, the decision the claim grounds removes the arrangement under which the claim's falsifier could ever be observed. This is a distinct failure mode from manufactured claims and is not what `DDD-method-06` predicts. It may deserve a claim of its own — but filing one is a ruling, not an audit finding, and none was filed | -| F-9 | **`web/markup-validity` cites a claim that does not state its rationale.** The argument is that a duplicated class is always a copy-paste artifact that masks a missing second class; `DDD-web-01` says nothing about duplicate classes. The basis is a claim — an unfiled one | -| F-10 | **31 of 52 citation edges point at a single-purpose claim; 22 of 41 cited claims are weak by §1.5.** Most are the evidence-backed C#/Bicep catalog, where single-purpose is correct by design — a catalog entry records a measurement, not a decision prop. This is offered as a caution against reading §1.5's counts as a defect list, not as a cleanup queue | - ---- - -## 9. One line for M8 - -**Yes, and it lands on both halves.** The id migration will carry 41 -pre-format-5 decisions into `dec:` ids, and 7 of them (17%) have a real basis -that is not a claim — so a migration that copies `based_on: {claim: …}` edges -verbatim imports 5 manufactured claim-bases into the ledger graph as though -they were genuine, while re-typing them on the way is a principal's ruling and -not a migration step; and since retroactive acceptance means signing a version -hash, those 5 decisions would be signed with a stated basis this audit says is -wrong, which argues for settling §8.2 **before** the migration runs rather than -after. diff --git a/docs/audits/classifier-corpus-2026-08.md b/docs/audits/classifier-corpus-2026-08.md deleted file mode 100644 index 6a181677..00000000 --- a/docs/audits/classifier-corpus-2026-08.md +++ /dev/null @@ -1,99 +0,0 @@ -# Classifier-corpus reading — hand-labelled recall / false-demand (2026-08) - -**What is under test.** Spec success criteria 1–2 (classifier recall and -false-demand rate) over the M8 repository-diff path, via the instrument -named in the research protocol §3 -([`ddd-research-protocol.md`](../ddd-research-protocol.md)): a fixture set -of labelled changes per language, measured through -`ddd_lsp::revdiff::diff_contracts` — the same shared classifier the edit -interceptor runs (spec invariant 4), so a number measured here is a number -about both paths. - -The corpus doubles as the **acceptance harness for policy-table -amendments**: a row change that silently trades recall for friction fails -the harness by case name. - ---- - -## 1. The instrument - -- **Corpus:** `ddd-cli/tests/corpus//-/` — each case is - one `before.*`/`after.*` file pair plus `labels.yaml` naming, per changed - symbol, whether the change **is** contract surface (`surface:`) or **is - not** (`non_surface:`), under the app-repo default posture - (`internal_is_surface` off). A `change: any` label marks a symbol whose - correct classification is *no event at all* (e.g. a body-only edit, a - same-type token value change); any surface event on it is a false demand. -- **Harness:** `ddd-cli/tests/corpus.rs` — one temp git repo per language, - each case replayed as a commit pair and classified with `diff_contracts` - over that pair; one `HostManager` per language, so rust-analyzer starts - once. Per case, every `surface` row must appear as a surface event and no - `non_surface` row may; the test fails on any mismatch. -- **Re-run:** `cargo test -p ddd-cli --test corpus -- --nocapture` - (set `CORPUS_VERBOSE=1` for the full per-case event dump). Runs in normal - CI; rust-analyzer is a pinned component of the toolchain. - -## 2. How the labels were produced — provenance, stated honestly - -Labels were written by the M8 implementing session, by reading each diff -against the fixed policy-row semantics (`ddd-lsp/src/adapter/rust.rs`, -`ddd-lsp/src/adapter/htmlcss.rs`) **before** running the classifier over the -corpus — not by copying classifier output. That discipline is what let the -corpus catch a real defect on its first run (§4). The residual caveat: the -labeller also maintains the classifier, so the labels test the -implementation against the table's stated claims, not against an -independent reading of "contract surface". Independent labelling (a second -person, or labels derived from observed downstream breakage) is future -work, and the recall/false-demand numbers below should be read with that -limit attached. - -## 3. Measured numbers (2026-08-12, harness passing) - -| Language | Cases | Surface labels matched (recall) | Non-surface labels flagged (false-demand) | -|---|---|---|---| -| rust | 12 | 9/9 = **100%** | 0/19 = **0%** | -| htmlcss | 9 | 10/10 = **100%** | 0/3 = **0%** | - -Rust covers every policy row from both sides: pub/private fn addition, -pub/private signature change, visibility promotion, `pub(crate)` member -under the default posture, trait-impl addition, pub removal, derive change, -trait-member addition, plus two naturalistic mixed edits. The rust -non-surface count includes the §4 guard rows. HTML+CSS covers token -add/remove/retype, same-type value change (silence expected), class -add/remove, `@layer` reorder, stylesheet-link rewiring on the HTML side, -plus one mixed theme pass. - -## 4. What the corpus caught on its first run - -The initial run passed vacuously on labels that only named the *intended* -changes. Dumping full event lists showed the classifier also emitting -phantom `signature-changed` **surface** events on symbols the diffs never -touched — enum members (`Red`, `Green`) and a struct field (`id`) — in 6 of -12 rust cases. Cause: `declaration_slice` in -`ddd-lsp/src/adapter/rust_facts.rs` read an 8-line lookahead that ran past -a terminator-less declaration's own extent (an enum member or field has no -`{`/`;`/`=` to cut at), so any edit in the lines below an enum polluted its -members' signatures. The same facts fn feeds the interceptor, so this was -a live false-demand source on both paths. - -Resolution: the slice is now capped at the symbol's own range end (a fact -rust-analyzer already reports) — an adapter-facts fix, no policy-row or -classifier-logic change. The affected cases carry explicit `non_surface` -guard rows (`change: any`) for the previously-phantom symbols, so a -regression fails the harness by name; a unit test -(`a_terminator_less_declaration_never_reads_past_its_own_range`) pins it in -`ddd-lsp` as well. No hand label was changed to match classifier output. - -## 5. Not covered — scoped follow-up - -- **C# and Bicep**: no real host is available in this measurement - environment (the workspace CI has no .NET SDK — `dec/ddd/fixtures-not-sdk`), - so their corpora are the named follow-up, to be built where their hosts - run. Note that `csharp_facts.rs` carries its own unbounded - `declaration_slice`; whether C# exhibits the §4 bleed is exactly the kind - of question its corpus must answer — it should be checked first. -- **Posture variants**: all rust cases run the default app-repo posture; - `internal_is_surface: true` is exercised by the adapter's own tests but - has no corpus cases yet. -- **Label independence**: see §2 — single-labeller provenance is the - standing caveat on both headline numbers. diff --git a/docs/audits/provenance-2026-08.md b/docs/audits/provenance-2026-08.md deleted file mode 100644 index 5688b829..00000000 --- a/docs/audits/provenance-2026-08.md +++ /dev/null @@ -1,649 +0,0 @@ -# Cross-repo provenance audit — declared, implicit, absent (2026-08) - -**What is under test.** Not a claim. This audit measures a *property of the -corpus*: for each decision in scope, does it rest on something that is canon one -level up, and if so is that dependence **declared**, **implicit**, or **absent**? - -The corpus is five levels, each basing on the one above: - -``` -product-cli → product-framework → ai-development-foundations - → decision-driven-design → actor-indexed-determination -``` - -There is no upstreams manifest, no SHA pin, no lockfile. Every cross-repo basis -is convention. L4 will make *declared* edges mechanical; it cannot create edges -that were never declared. This audit counts what L4 would have to work with. - -**The prediction, stated so the result can contradict it.** The tooling repos -will show heavy *implicit* upstream dependence — decisions reasoning from seams, -closure, escapes, allocation, tolerance without an edge — because author and -canon were the same person, which is exactly the condition under which -provenance goes unrecorded. **If implicit dependence turns out to be rare, the -corpus is better connected than assumed and the gap is only mechanical (L4 alone -fixes it).** - -**Section 1 was written and committed before any decision rationale, statement, -or `notes` field was read, and before any classification was made.** Nothing in -it was adjusted after results were seen. If the criteria look badly chosen in -hindsight, that is a finding about the pre-registration, not a licence to re-cut -it. - -*Disclosed limit on that claim, stated rather than glossed:* before writing §1 I -read file **shapes** — the `Decision` / `BasedOn` / `VersionRaw` structs in -`ddd-core` and `ledger-core`, directory listings, `format:`/`kind:`/`based_on:` -field counts — and, while inspecting the ledger file layout, saw the header -comment and the first version `statement` of -`.decisions/log/01KZNF77DG2RJPY7CA63HYTRJ1.yml` on screen. No classification was -made from it and no other decision text was read. That one exposure is recorded -here rather than claimed away. - ---- - -## 1. Method, as pre-registered - -*(Committed before the data was gathered. Fixed.)* - -### 1.1 Population - -Two stores in this repo, counted and reported **separately**, never pooled into -a single headline rate: - -- **P1 — `.ddd/decisions/`**: every decision file, all formats. **45 files.** - The unit is the decision. Format is recorded per row but does **not** gate - inclusion: this audit is about cross-repo reach, and the format-5 typed-basis - vocabulary is orthogonal to it. -- **P2 — `.decisions/` (the ledger)**: every decision's **latest version** (per - spec v1.2, the unique tip of the `parent`/`merged_from` chain). **12 - decisions, 12 versions**, all in set `ledger-design`. The unit is the version, - because the version is what carries `statement` and `based_on`. - -**Total population: 57 decision units.** - -Other corpus repos are **not present on disk** and are **not in this session's -repository scope**. Per the brief's constraint, nothing is cloned or fetched. -The audit is therefore scoped to this repo and the reading is **partial by -construction**; §2.3 states exactly what could not be seen. - -### 1.2 The text that is graded - -Grading is from the decision's own text and its basis edges — nothing else. No -claim file, no commit message, no spec section is read *in order to decide a -grade* (they are read to build the §1.3 inventory, which happens first and is -independent of any decision). - -- **P1**: `title`, `rationale`, `notes`, and the `based_on` edges. -- **P2**: `statement`, `expectation`, `exposure`, the file-level `note`, the - YAML comments attached to the version block, and the `based_on` refs. - -### 1.3 Upstream construct inventory — sourcing rule fixed in advance - -Built **before** any decision text is read. A term enters an upstream level's -inventory only if one of these holds: - -- **(a)** the brief for this audit names it as belonging to that level; or -- **(b)** a document **present in this repo** explicitly attributes it to that - upstream repo (a mirror notice, a conformance statement, a "canon wins" - pointer). - -Nothing is inferred from resemblance. Where a level's canon is unavailable the -inventory is marked **incomplete** and grading is **conservative**: a term not -admitted by (a) or (b) is **not** counted as an upstream reach, even where it -plausibly is one. Per constraint 5, an unverifiable upstream is not an absent -one — undercounting is the intended direction of error. - -Per-level sourcing, fixed here: - -| Level | Repo | Canon available here? | Inventory basis | -|---|---|---|---| -| **U1** | `product-framework` | **Yes, vendored** — `docs/product-framework-open.md`, declared in `CLAUDE.md` as a mirror of the canonical repo | The §-numbered construct vocabulary of that document. **Complete enough to grade.** | -| **U2** | `ai-development-foundations` | **No** — only a self-declared conformance statement (`docs/ai-foundations-conformance.md`) referencing the upstream repo | Terms that statement attributes upstream, plus (a). **Incomplete.** | -| **U3** | `decision-driven-design` | **No** — local `docs/ddd-*.md` are *this repo's tooling spec*, not upstream canon, though they restate method constructs and one names canon as ground truth | The brief's enumerated list (seam, escape, closure, predicate, store allocation, specification demand, floor, …) plus terms local docs explicitly attribute to canon. **Incomplete.** | -| **U4** | `actor-indexed-determination` | **No**, and no local mirror | The determination-tuple constructs named in the brief only. **Maximally incomplete.** | - -*Naming note, recorded not corrected:* the brief writes -`ai-foundation-development` and "actor-general determination"; the repos that -exist under the account are `ai-development-foundations` and -`actor-indexed-determination`. Treated as the same levels. - -### 1.4 Source attribution of a construct — three values, fixed in advance - -The corpus's hard case: this repo **implements** the upstream method, so a -construct can be upstream canon *and* local vocabulary at once. Without this -split, every DDD decision reads as implicit upstream dependence trivially and -the prediction is confirmed by construction. So each construct invoked is -attributed: - -- **upstream-only** — no local filed home. Its definition lives only upstream. -- **domesticated** — an upstream origin **and** a local filed home in this repo: - a schema type, a store directory, or a normative section of a local spec. -- **local-only** — originates here; not an upstream reach at all. - -### 1.5 The grade — fixed in advance - -Per decision, per construct reached: - -- **declared** — a machine-resolvable reference to an upstream entry: an id, a - scheme-prefixed `based_on` ref, a URL, or a §-number **of an upstream - document**. An edge or an id. Nothing else. -- **implicit** — the construct is named in prose (one quoted phrase recorded), - with no id and no edge. -- **absent** — the reasoning depends on the construct and the text does not name - it. Recorded **only** when the construct is the decision's *operative - mechanism* — removing it makes the decision unintelligible — and neither its - name nor a direct synonym appears. Otherwise the decision is `n/a`. -- **n/a** — no upstream construct invoked. - -**Per constraint 4, prose mention is never sufficient for `declared`.** Declared -requires an id or an edge. That distinction is the whole point of the audit and -is not negotiable after the fact. - -### 1.6 Kind of the upstream thing — fixed in advance - -Per the brief, each reach records what is being reached for: - -- **claim** — a contestable proposition about the world, citable as ground. -- **definition** — a term or its meaning. Arguably not a basis at all; counted - separately for exactly that reason. -- **method-rule** — a rule the method imposes. Arguably a mandate rather than a - ground. - -### 1.7 Relation — grounding vs watched-not-grounding, recorded not resolved - -The re-typing session surfaced a relation with no machine-readable home. It is -**counted separately and left unresolved**, per the brief: - -- **grounding** — the decision would be different, or unsupported, were the - upstream thing false or absent. -- **watched-not-grounding** — the decision tracks or conforms to the upstream - thing without deriving from it ("we mirror the spec version", "we stay - conformant", "canon wins if we diverge"). - -Recorded for every reach, not only declared ones — the brief asks what the edge -*would be* if filed. - -### 1.8 Canon gaps — the count that may matter most - -A construct is a **canon gap** iff it is invoked by at least one decision in -scope **and** has no citable upstream entry: no id, no §-number, no filed home -at any upstream level that this audit can see. Counted as constructs (not as -decisions), with the number of invoking decisions alongside. Because U2–U4 are -unavailable, every canon gap is stated as **"not citable from here"**, never as -"does not exist upstream". - -### 1.9 The reading - -Reported at two widths, **both pre-registered, neither chosen after the fact**: - -- **Reading A (strict)** — over **upstream-only** reaches. The decisions that - depend on something with no local home at all. -- **Reading B (broad)** — over **all** reaches including domesticated. The full - set of provenance edges an upstreams manifest would have to pin. - -For each: counts and rates per grade, per level reached, per upstream-thing -kind; plus the canon-gap count from §1.8. - -### 1.10 Verdict bands — fixed in advance - -On the **implicit rate** = implicit ÷ (declared + implicit + absent), computed -under **Reading B** (the broad width — the prediction is about the corpus as a -whole, and Reading A's denominator may be small): - -| Implicit rate | Verdict on the prediction | What it means for L4 | -|---|---|---| -| **≥ 50%** | Held strongly. | Provenance is a **filing job** at scale; L4 alone is insufficient. | -| **20–49%** | Held. | Filing job, bounded. | -| **< 20%** | **Contradicted.** The corpus is better connected than assumed. | The gap is **mechanical**; L4 alone fixes it. | - -The `< 20%` band is the kill condition and is stated first so it cannot be -softened later. - -### 1.11 Borderline handling - -Per constraint 3, a reach whose grade or attribution genuinely pulls two ways is -marked **borderline** and listed with its quoted phrase. It still receives its -best-reading value in the table; borderline is an annotation, not a fourth -grade. **Borderline counts are a result, not a failure.** - -### 1.12 Underpower test — fixed in advance - -**Underpowered** is claimed only if **both**: - -- fewer than **10** decision units in the population invoke any upstream - construct at all; **and** -- more than **30%** of the reaching decisions are marked borderline. - -Population size alone is not grounds. 57 is not a small N for this question. - -### 1.13 Constraints on this audit - -Nothing in either store is changed. **No edges filed, no re-typing, no claims -filed, no id references inserted, no cleanup of anything noticed.** No repo is -cloned or fetched. The pre-existing `graph_bench.rs` lints stay untouched. Every -finding is a **proposal for the principal's ruling**, filed unactioned. - -If, after seeing results, the criteria above look wrong, the audit **stops and -says so** rather than adjusting them. - ---- - -## 2. Population, and what could not be seen - -### 2.1 The two stores - -| | Unit | Count | -|---|---|---| -| **P1** | `.ddd/decisions/*.yaml` — the decision | **45** (1 at format 1, 34 at format 2, 10 at format 5) | -| **P2** | `.decisions/` — the latest version of each decision | **12** (12 decisions, 12 versions, all in set `ledger-design`; no chain has a second tip) | -| | **Total** | **57** | - -P1 carries **59** basis edges: 51 `claim`, 3 `mandate`, 2 `constraint`, 1 `preference`, 1 `experiment`, 1 `risk-acceptance`. P2 carries **28** basis refs across its 12 versions. - -### 2.2 The upstream construct inventory, as built - -Built before any decision text was read, per §1.3. Terms admitted: - -- **U1 — `product-framework`** *(vendored canon, complete enough to grade)*. From `docs/product-framework-open.md`: What / How / Delivery; system (§3.2.5); context mapping (§3.1); journey and journey crossing (§3.0.1); Translation and the four patterns (§3.2.0); command, event, trigger, read model / View, entity, value object, UI step; Decider (§3.3); Projector (§3.4); named-algorithm primitive / Polanyi floor (§3.5); quality demand (§3.6); How contract and the Why cascade (§4.1); repository layout model (§4.3); work unit (§5); Build seam and verdict event (§5.1); codegen seam (§5.2); verification kinds (§6.3); feature ⊇ flow ⊇ slice (§7.1); done-as-predicate (§7.2); versions and direction (§7.3); the derivation contract and `derived_from` (§9); payload signature. -- **U2 — `ai-development-foundations`** *(canon absent; inventory incomplete)*. From `docs/ai-foundations-conformance.md`, which cites the upstream repo by URL: Pillar One / Pillar Two; Specification Framework; Execution Contract; SPMC and its four axes (Schema, Prompt, Model, Context); frozen context; the derivation contract's five rules; layer separation; scale discipline; the seam (its Section 7); autonomy level; closure contract; the eight Execution building blocks. -- **U3 — `decision-driven-design`** *(canon absent; inventory incomplete)*. From the brief plus the two local documents that name canon as ground truth (`docs/way-of-working-decision-allocated-delivery.md` §0–1, `ledger/prd/decision-ledger-prd.md` §2 "Compressed canon"): specification demand and its conservation; the four stores and store allocation (encoded constraint · mechanical verification/criterion · judgment · escaped); escape, priced vs silent; coverage-not-pass-rate; the governing decision set; the granularity bound; tolerance / assurance level / tiers T0–T2; the floor; ground and ground state; the closure principle; predicate; claim and falsifier; seam; discharge; enumeration; extra-actor; recon branch; blame. -- **U4 — `actor-indexed-determination`** *(absent, no local mirror; maximally incomplete)*. From the brief only: the determination-tuple constructs — **determination** itself; plus terms `ledger/prd/decision-ledger-prd.md` §9.3 uses without defining (recognition-over-recall, low-transfer-floor, pinnability, outcome-accountability). - -*Application note, not a new criterion.* §1.5 grades a construct **reached**. Read as: the decision invokes the construct's **content** — its definition, or a rule about it — as reasoning material. Merely storing data in the tool's own schema (`based_on: {claim: …}`) is plumbing, not a reach. Recorded here because it is the single judgement that most moves the counts. - -*Second application note.* §1.4's "local filed home" is read to exclude a **vendored mirror of upstream canon**: `docs/product-framework-open.md` is the U1 canon readable here, not a local spec. A U1 construct is therefore **domesticated** only where it also has a local schema type in `product-core` — which, this repo being product-framework's reference implementation, is nearly always. - -### 2.3 What this audit could not see — stated, not glossed - -**No upstream repository was cloned or fetched.** Per the brief's constraint and its anti-goals, availability was checked and nothing was pulled. - -| Repo | On disk? | In session scope? | Exists on the account? | -|---|---|---|---| -| `Hafeok/product-framework` | no (vendored mirror only) | no | yes, public | -| `Hafeok/ai-development-foundations` | no | no | yes, public | -| `Hafeok/decision-driven-design` | no | no | yes, public | -| `Hafeok/actor-indexed-determination` | no | no | yes, public | - -So all four are *cloneable in principle* and **none was cloned**. Consequences, carried into every number below: - -1. **U2, U3 and U4 inventories are incomplete.** Grading was conservative per §1.3 — a term not admitted by (a) or (b) was not counted, so the reach counts are **floors, not estimates**. -2. **No canon gap is asserted as "does not exist upstream."** Every one is stated as *not citable from here*. Constraint 5, applied literally. -3. **U2 returning zero reaches (§4.2) may be an artifact of its inventory**, not a fact about the corpus. Flagged there rather than reported as a finding. -4. The brief's names `ai-foundation-development` and "actor-general determination" resolve to the repos `ai-development-foundations` and `actor-indexed-determination`. Recorded, not corrected. - -A further limit, internal to this repo: `dec/ddd/prd-split` split the PRD into four documents, so the many `PRD §N` references in P1 rationales now point at a section numbering that no longer exists. That is a *local* citation-rot problem, not a cross-repo one, and is noted once here rather than counted. - ---- - -## 3. The per-decision table - -One row per **reach**. A decision with no reach appears in §3.3. `Attr` is §1.4; `Grade` is §1.5; `Kind` is §1.6; `Rel` is §1.7. ★ marks a row listed as borderline in §6. - -### 3.1 P1 — `.ddd/decisions/` (35 of 45 reach; 38 reaches) - -| Decision | Construct reached | Lvl | Attr | Grade | Kind | Rel | The phrase | -|---|---|---|---|---|---|---|---| -| `ddd/adapter-policy-tables` | claim / falsifiability | U3 | dom | implicit | definition | ground | "the tables are themselves claims, falsifiable against where boundary defects occur" | -| ★ `ddd/amend-explicit-evidence-frozen` | closure principle (own output is not ground) | U3 | dom | **absent** | method-rule | ground | "the correspondence dataset is only evidence while those fields stay machine-authored" | -| ★ `bicep/linter-at-error` | encoded-constraint store | U3 | dom | implicit | method-rule | ground | "The DAD §5.2 rule set at error is cheap and demonstrated" | -| `bicep/psrule-in-pr-stage` | predicate closure · judgment store | U3 | dom | implicit | method-rule | ground | "Neither predicate is closable in an arrangement that runs only the linter"; "staying with judgment" | -| `cs/async-policy-rules-at-error` | predicate case / closure | U3 | dom | implicit | definition | ground | "adopting this decision closes forwarding and leaves presence open" | -| `cs/authorization-fallback-deny` | predicate closure | U3 | dom | implicit | definition | ground | "This closes the predicate by composition rather than by a rule" | -| `cs/disposal-rules-at-error` | judgment store | U3 | dom | implicit | definition | ground | "Those stay with judgment." | -| `cs/general-catch-at-error` | escape · allocation to review | U3 | dom | implicit | method-rule | ground | "so the escape has to be argued for rather than defaulted into"; "That residual is real and stays allocated to review" | -| `cs/policy-rules-at-error` | predicate closure | U3 | dom | implicit | definition | ground | "close the comparison and culture cases of pred/code/explicit-policy-argument" | -| ★ `cs/policy-rules-at-error` | **determination** | **U4** | **upstream-only** | implicit | definition | ground | "the catalog is where determinations live" | -| `cs/startup-assertions-required` | closure (closable vs closed) | U3 | dom | implicit | definition | ground | "Both close nothing unless the assertion actually runs" | -| `cs/unsafe-reinterpretation-banned` | rejection witness · allocation to review | U3 | dom | implicit | method-rule | ground | "A residual with no rejection witness cannot be allocated to review, because review has nothing to find" | -| `ddd/curation-over-mining` | named accountable actor | U3 | dom | implicit | definition | ground | "every entry has a principal who can explain it" | -| `ddd/enforce-matching-tightens-to-symbol` | priced escape | U3 | dom | implicit | method-rule | ground | "Priced, not paid, in M5." | -| `ddd/enum-member-gap-priced` | silent vs priced escape | U3 | dom | implicit | method-rule | ground | "So the gap is silent twice: no seam demanded, and no trace in the dataset" | -| `ddd/interceptor-not-extension` | arrangement vs residual discretion | U3 | dom | implicit | method-rule | ground | "part of the arrangement the agent edits through, not the agent's residual discretion" | -| ★ `ddd/lsp-as-seam` | seam | U3 | dom | implicit | definition | ground | "The LSP protocol is the seam to language intelligence" | -| `ddd/m6-proceeds-no-flip` | escaped decision | U3 | dom | implicit | definition | ground | "An unfiled condition evaluated by default is an escaped decision in the making" | -| `ddd/m8-enforcement-closure` | closure · discharge | U3 | dom | implicit | method-rule | ground | "declarations that sign the change they discharge"; "durable discharge" | -| ★ `rust/no-unwrap` | encoded constraint vs judgment | U3 | dom | implicit | method-rule | ground | "Enforced twice by arrangement, not exhortation" | -| `ddd/pins-at-m2` | escape | U3 | dom | implicit | definition | ground | "the warning swamp is the canonical escape" | -| ★ `ddd/predicates-carry-no-status` | predicate/claim split rule | U3 | dom + **upstream doc named** | implicit | method-rule | ground | "The framework corpus's predicate format (predicate-format.md, predicate-definition.md) requires the split" | -| `ddd/rejection-facts-prefilled` | judgment store | U3 | dom | implicit | definition | ground | "Pre-filling the judgment invites rubber-stamping" | -| `ddd/report-coverage-explicit` | coverage-not-pass-rate | U3 | dom | implicit | method-rule | ground | "separates checked-and-clean from not-checkable and names the uncheckable set" | -| `ddd/rust-class-enforced-here` | falsifier | U3 | dom | implicit | definition | ground | "the first live reading of the friction falsifier on a real repo" | -| `ddd/rust-host-is-real` | coverage-not-pass-rate (vacuous gate) | U3 | dom | implicit | method-rule | ground | "a green exit code standing in for a check that never ran" | -| `ddd/seed-lands-in-claims-not-shared` | closure finding | U3 | dom | implicit | definition | ground | "it is a closure finding that does not hold there, carrying the authority of the catalog" | -| ★ `ddd/typed-basis` | claim as basis | U3 | dom | implicit | definition | ground | "The basedOn -> claim edge remains load-bearing where a claim is the basis" | -| `risk/ddd/undeclared-what-boundaries` | priced escape / exposure accepted | U3 | dom | implicit | method-rule | ground | "Exposure accepted"; "Review by 2027-02-07" | -| `risk/ddd/undeclared-what-boundaries` | What boundary kinds | **U1** | dom | implicit | definition | ground | "24 undeclared boundaries over 121 classified elements" | -| `web/htmlcss-enforce` | seam · arrangement vs exhortation | U3 | dom | implicit | method-rule | ground | "agents drifting past ungoverned HTML↔CSS seams, and `warn` is exactly the exhortation that failure mode ignores" | -| ★ `web/render-status-palette` | closure | U3 | dom | implicit | definition | ground | "The stylelint rule … is the closure" | -| `ddd/what-boundaries-priced-not-paid` | priced escape | U3 | dom | implicit | method-rule | ground | "The finding stays standing and visible rather than being suppressed or satisfied cheaply" | -| `ddd/what-boundaries-priced-not-paid` | boundary kinds (system, quality demand, context mapping, journey crossing, command, event, read model) | **U1** | dom | implicit | definition | ground | "2 system, 2 quality demand, 1 command, 1 event, 1 read model, 1 journey crossing" | -| `ddd/what-policy-table` | boundary kinds | **U1** | dom | implicit | definition | ground | "a policy table over **the framework's boundary kinds**"; "system, context mapping, journey crossing, event, command, payload signature, quality demand" | -| **`ddd/what-published-qualifier`** | **Translation (§3.2.0)** | **U1** | dom | **declared** | definition | ground | "the adapter computes a published/internal visibility from the **§3.2.0** Translations" | -| `ddd/why-resolves-three-ways` | escape | U3 | dom | implicit | definition | ground | "an ungoverned detected rule is a real escape" | -| ★ `ddd/workspace-member-delivery` | What/How vocabulary | **U1** | dom | implicit | definition | **watched** | "it does not extend the What/How vocabulary" | - -### 3.2 P2 — `.decisions/` (6 of 12 reach; 6 reaches) - -Every P2 version **declares** its bases — 28 refs across 12 versions — and **all 28 point inside this repo** (`prd:`, `format:`, `ruling:`, `constraint:`, `preference:`). Not one names an upstream repo. The `Declared-local` column records that; the `Grade` column is the *upstream* grade, per §1.5. - -| Decision (tip) | Construct reached | Lvl | Attr | Grade | Kind | Rel | Declared-local | The phrase | -|---|---|---|---|---|---|---|---|---| -| ★ `…68QNJ0H9` → `…9HV1X61F` (acceptor identity) | acceptance cannot be delegated / accountable actor | U3 | dom | **absent** | method-rule | ground | `prd:…#9.3`, `#12.OD-3`, `#4.4`, `format:…#3.2` | "refusing model and CI identities … requiring an acceptance's actor to match the author of the commit" | -| `…VYR60TD3` (tolerance floor) | tolerance · floor | U3 | dom | implicit | definition | ground | `prd:…#12.OD-4`, `#4.2.1`, `format:…#3.3` | "A set's tolerance is a floor and a version may override it only upward" | -| `…7EJWN9EK` (L010) | judgment store | U3 | dom | implicit | definition | ground | `format:…#5.2`, `…migrations#spec-v1.1`, `prd:…#9.3` | "a judgment whose actor resolves to a model or CI identity fails the gate" | -| `…Z5Y366Y7` (disposition vocabulary) | priced escape | U3 | dom | implicit | definition | ground | `format:…#8`, `prd:…#8` | "escaped-priced, escape-review-due" | -| `…5ERWSCWJ` (merge two mechanisms) | judgment store | U3 | dom | implicit | definition | ground | `prd:…#7`, `#11-L3`, `ruling:emil-2026-08-11#…` | "refuses anything requiring judgment with the conflict preserved and named" | -| `…RJ7QYFDK` (certificate signing) | tolerance · floor · tier gating | U3 | dom | implicit | definition | ground | `prd:…#4.5`, `#11-L6`, `ruling:…`, `constraint:…`, `preference:…` | "signature required above the tolerance floor … because the floor is up-only, the signature requirement cannot be shopped out" | - -### 3.3 Decisions that reach for nothing upstream (16 of 57) - -**P1 (10):** `ddd/correspondence-rows-are-stratified` · `ddd/fixtures-not-sdk` · `ddd/git-is-the-amend-trail` · `ddd/internal-not-surface` · `ddd/prd-split` · `ddd/sarif-unification` · `ddd/v1-scope` · `web/markup-validity` · `web/plain-pair-scope` · `web/token-discipline` - -**P2 (6):** `…68QNJ0H9` (workspace member) · `…KT2G4M0K` (write refusal) · `…S3ACXJDJ` (revise refusal) · `…E2RQ5W0Q` (graph stage) · `…PZDFYK5Q` (index determinism) · `…AXMFRCWV` (acceptance never transfers) - -These are tool-mechanics decisions, and `n/a` is the right reading: each is intelligible without any upstream construct. `web/token-discipline` is the closest call — its severity argument runs on encoded-constraint logic — but "ban raw hex so colours go through tokens" stands on its own, so it is `n/a` per §1.5's tight rule for `absent`. - ---- - -## 4. The reading - -### 4.1 Reading B (broad) — the pre-registered verdict width - -**41 of 57 decision units (71.9%) reach for at least one upstream construct.** Graded at the decision level, taking the best grade present: - -| Grade | Units | Rate over the 41 reaching | -|---|---|---| -| **declared** | **1** | **2.4%** | -| **implicit** | **38** | **92.7%** | -| **absent** | **2** | **4.9%** | - -> **Implicit rate = 38 / 41 = 92.7%.** Per §1.10 that is the **≥ 50%** band: **the prediction held strongly.** Cross-repo provenance in this corpus is a **filing job**, not a mechanical one. L4 alone does not fix it. - -The single declared edge is `dec/ddd/what-published-qualifier` → product-framework **§3.2.0** — and even that is a section number in prose, not a basis edge. - -### 4.2 By level reached (44 reaches; a decision may reach twice) - -| Level | Reaches | declared | implicit | absent | -|---|---|---|---|---| -| **U1** `product-framework` | 5 | **1** | 4 | 0 | -| **U2** `ai-development-foundations` | **0** | 0 | 0 | 0 | -| **U3** `decision-driven-design` | **38** | 0 | 36 | 2 | -| **U4** `actor-indexed-determination` | 1 | 0 | 1 | 0 | - -Two things to say plainly. **U3 carries 86% of the reaches and has not one declared edge** — the level the corpus depends on most is the level it cites least. And **U2 returning zero is not a finding**: its inventory is the thinnest of the four (§2.3), so zero may be the inventory's shape rather than the corpus's. - -### 4.3 By kind of the upstream thing - -| Kind | Reaches | Share | -|---|---|---| -| **definition** (a term) | 27 | 61.4% | -| **method-rule** (a rule the method imposes) | 17 | 38.6% | -| **claim** (citable as ground) | **0** | **0%** | - -**Not one decision in 57 reaches upstream for a claim.** Every cross-repo reach is for a *definition* or a *method rule* — which is to say, for vocabulary and for mandates, never for ground. - -This has a structural explanation the audit can point at, and it is not authorial neglect. In P1, a **claim** basis is `BasisPin { claim, status, changed }` — an in-repo claim id plus that claim's in-repo state. A **non-claim** basis is `TypedBasis { type, statement, ref }`, whose `ref` is a free-form pointer. So a typed non-claim basis *can* carry an upstream pointer today (unpinned, unresolved); **a claim basis structurally cannot** — the one kind of upstream thing that would be citable as ground is the one kind the format has no slot for. P2's `BasisRef` is an open string and can hold anything, but nothing resolves or pins it. - -### 4.4 By relation (§1.7) — recorded, not resolved - -| Relation | Reaches | -|---|---| -| grounding | 43 | -| watched-not-grounding | **1** | - -The one is `dec/ddd/workspace-member-delivery` — "it does not extend the What/How vocabulary" — which tracks U1's vocabulary in order to stay clear of it, deriving nothing from it. Reported as asked, and with this observation: **at the cross-repo boundary the relation barely fires.** The watched-not-grounding relation the re-typing session surfaced is an *intra-repo* phenomenon — the secondary claim edges retained on `m6-proceeds-no-flip`, `internal-not-surface`, `rust/no-unwrap` and `what-boundaries-priced-not-paid` — and the ontology question it raises is not, on this evidence, blocking for L4. - -### 4.5 Reading A (strict) — upstream-only reaches - -Constructs with **no local filed home at all**: - -| Decision | Construct | Level | Grade | -|---|---|---|---| -| `cs/policy-rules-at-error` | **determination** | U4 | implicit | -| `ddd/predicates-carry-no-status` | **`predicate-definition.md`** (named upstream document, not present here) | U3 | implicit | - -**2 reaches, 2 decisions, 0 declared, 2 implicit.** The implicit rate is 100% and the N is 2 — far too small to carry a verdict, which is why §1.10 fixed Reading B as the verdict width in advance. - -### 4.6 The tension between the two readings — flagged, not resolved - -Reading B says 92.7%; Reading A says the strictly-upstream population is two decisions. Both are true, and the gap between them is the entire content of §1.4's `domesticated` category: 42 of the 44 reaches are for constructs that have *both* an upstream origin *and* a local home. - -A critic can say Reading B measures the corpus reasoning in its own implemented vocabulary, which is not cross-repo dependence at all. A defender can say a local restatement is precisely how provenance goes unrecorded — the construct got copied instead of cited, which is the failure the audit was built to find. - -**Per constraint 2, I say this rather than act on it:** knowing the numbers, I would now argue that a third width — domesticated reaches whose *local home is itself an uncited restatement of canon* — is the sharpest measure of the three. **I did not pre-register it and I am not substituting it.** The verdict above stands on Reading B as fixed in §1.10, and the principal should rule with both numbers in view. - -### 4.7 Borderline rate - -**10 of the 41 reaching decisions (24.4%) are borderline** (§6). Reported as a result, per §1.11. - -### 4.8 The underpower test, run as pre-registered - -§1.12 required **both** arms. First: fewer than 10 units reach — **not met** (41 reach). Second: more than 30% borderline — **not met** (24.4%). The conjunction fails, so **underpowered is not claimed**, and neither arm came close. - ---- - -## 5. Canon gaps (§1.8) — constructs used, filed nowhere citable - -Split by what the audit can actually assert, per constraint 5. - -### 5.1 (i) Not citable from anywhere this audit can see — and not defined locally either - -| Construct | Invoking decisions | Why it is a gap | -|---|---|---| -| **determination** | 1 — `cs/policy-rules-at-error` ("the catalog is where determinations live") | Used as a **defined term of art** with no definition anywhere in this repo. The only other occurrence is a bare use in `docs/ddd-cli-prd.md` line 17 ("determinations"), also undefined. Its home is the actor-general level, which this audit cannot see at all. | - -**This is the one true canon gap the audit can assert** — a construct load-bearing enough to justify where an entry is filed, with no filed home on either side of the boundary that is reachable from here. - -### 5.2 (ii) Not citable *from here* — a missing pin, not necessarily a missing entry - -Each has a local restatement; none has a citable upstream entry reachable from this repo. Seven constructs, **38 invoking reaches**: - -| # | Construct | Reaches | Local restatement (its only home here) | -|---|---|---|---| -| C-2 | **the predicate/claim split rule** | 1 | `docs/predicate-format.md`. The decision also names **`predicate-definition.md`**, which **does not exist in this repo** — direct evidence the upstream entry exists and is unpinned. | -| C-3 | **specification demand · conservation · the four stores** | ~12 (the whole C#/Bicep store-allocation family) | `decision-ledger-prd.md` §2.1; `way-of-working-decision-allocated-delivery.md` §0 | -| C-4 | **escape, priced vs silent** | 8 | `decision-ledger-prd.md` §2.1; `ddd report escapes` | -| C-5 | **the closure principle** | 6 | `decision-ledger-prd.md` §2.3 — one sentence | -| C-6 | **coverage-not-pass-rate** | 2 | `decision-ledger-prd.md` §2.4 | -| C-7 | **tolerance · floor · assurance level** | 3 | `decision-ledger-prd.md` §2.2; `ledger-format-v1.md` §3.3 | -| C-8 | **seam** | 2 explicit (11 mentions corpus-wide) | `.ddd/seams/`; no definitional section anywhere local | - -### 5.3 The sharpest single number in this section - -**The corpus's foundational principle is never named by any decision in it.** The strings *"specification demand"*, *"conservation"* and *"governing decision set"* appear **zero times** across all 57 decision texts — while roughly twelve decisions reason directly from the store allocation that principle produces ("stays with judgment", "allocated to review", "arrangement, not exhortation", "encoded constraint"). - -That is the audit's clearest picture of implicit dependence: the mechanism is everywhere and the ground is nowhere. - ---- - -## 6. Borderline set — for the principal's ruling - -Per constraint 3. Each carries its best-reading value in §3; these are the ten where the text pulled two ways. **★★ marks the two that would move a headline number.** - -| # | Decision | Recorded as | Pulled toward | The phrase, and the tension | -|---|---|---|---|---| -| ★★ | `ddd/predicates-carry-no-status` | **implicit** | **declared** | "The framework corpus's predicate format (predicate-format.md, **predicate-definition.md**) requires the split" — it names the upstream corpus *and* the upstream document. It is the closest thing in the corpus to a declared cross-repo edge, and it fails §1.5 only on having no id, no URL, no §-number and no pin. Ruling it `declared` doubles the declared count from 1 to 2. | -| ★★ | `cs/policy-rules-at-error` | **U4 reach, implicit** | n/a | "the catalog is where determinations live" — a single word. If "determinations" is ordinary English here rather than the term of art, **Reading A drops from 2 to 1**, §4.2's U4 row goes to zero, and §5.1 — the audit's only asserted canon gap — empties. Everything in §5.1 rests on this one word. | -| | `ddd/amend-explicit-evidence-frozen` | **absent** | n/a | "the correspondence dataset is only evidence while those fields stay machine-authored" — the closure principle exactly (an actor's own output is not ground for that actor), or ordinary data hygiene about self-reported fields. Graded `absent` on the reading that the argument is unintelligible without it. | -| | `…9HV1X61F` (ledger, acceptor identity) | **absent** | n/a | "refusing model and CI identities" — the acceptance-cannot-be-delegated rule is the whole mechanism, and the version cites `prd:…#9.3` where that rule lives locally; but the version's own text never names it. The two `absent` grades in the whole audit are this row and the one above. | -| | `ddd/lsp-as-seam` | **U3, implicit** | U2 / local-only | "The LSP protocol is the seam to language intelligence" — *seam* is a DDD-canon construct, an AI-foundations construct (its Section 7), and a generic software term from Feathers. Level assignment is a judgement; the grade is `implicit` under all three. | -| | `bicep/linter-at-error` | **implicit** | n/a | "The DAD §5.2 rule set at error" — cites a local document by section, and that document declares canon its ground truth. The strongest form of implicit, and arguably not a reach at all. | -| | `rust/no-unwrap` | **implicit** | n/a | "Enforced twice by arrangement, not exhortation" — the encoded-constraint-vs-judgment distinction in canon's own idiom, or an ordinary phrase. | -| | `web/render-status-palette` | **implicit** | n/a | "The stylelint rule … is the closure" — the DDD closure construct, or plain English for "the closing rule". | -| | `ddd/typed-basis` | **implicit** | local-only | "The basedOn -> claim edge remains load-bearing where a claim is the basis" — the format-5 basis vocabulary (claim · constraint · mandate · preference · experiment · risk-acceptance) rhymes with canon's four stores but is not them, and was ruled here. Counted as a `claim`-construct reach only. | -| | `ddd/workspace-member-delivery` | **watched-not-grounding** | n/a | "it does not extend the What/How vocabulary" — a non-interference assertion. It is the audit's only watched-not-grounding row, so ruling it `n/a` empties §4.4's second line entirely. | - ---- - -## 7. Findings — filed unactioned, in the three buckets the brief asked for - -**Nothing below was acted on.** No edge filed, no re-typing, no claim filed, no id inserted, nothing cloned. Per constraint 1 these are proposals for the principal's ruling. - -### 7.1 Bucket one — edges that should be filed as **grounding** - -Grouped by upstream target, because the tail is one target serving many decisions. - -| # | Decisions | Upstream target | Note | -|---|---|---|---| -| **G-1** | `ddd/what-published-qualifier` | `product-framework` **§3.2.0** (Translation) | The section number is already in the prose. This is a **prose-to-edge promotion plus a pin** — the cheapest edge in the corpus and the natural first test of the manifest. | -| **G-2** | `ddd/what-policy-table` | `product-framework` §3.2.5, §3.1, §3.0.1, §3.6 (system · context mapping · journey crossing · quality demand) | The table *is* a projection of the framework's boundary kinds; "the framework's boundary kinds" is prose today. | -| **G-3** | `ddd/what-boundaries-priced-not-paid`, `risk/ddd/undeclared-what-boundaries` | same U1 boundary kinds | Both enumerate the kinds by name; both cite none. | -| **G-4** | `ddd/predicates-carry-no-status` | `decision-driven-design` — `predicate-format.md`, `predicate-definition.md` | The mandate already names both documents. Filing the edge is pinning what the mandate says. **The highest-value single edge in the audit**, because it is the only place a decision states an upstream document as its requirement. | -| **G-5** | `cs/general-catch-at-error`, `cs/unsafe-reinterpretation-banned`, `cs/disposal-rules-at-error`, `cs/startup-assertions-required`, `bicep/psrule-in-pr-stage`, `bicep/linter-at-error`, `ddd/interceptor-not-extension`, `rust/no-unwrap`, `web/htmlcss-enforce` | `decision-driven-design` — the four stores / store allocation | **9 decisions, one target.** The largest single unrecorded dependence in the corpus. | -| **G-6** | `ddd/pins-at-m2`, `ddd/why-resolves-three-ways`, `ddd/m6-proceeds-no-flip`, `ddd/enum-member-gap-priced`, `ddd/enforce-matching-tightens-to-symbol`, `ddd/what-boundaries-priced-not-paid`, `risk/ddd/undeclared-what-boundaries`, ledger `…Z5Y366Y7` | `decision-driven-design` — escape, priced vs silent | 8 decisions, one target. | -| **G-7** | `ddd/report-coverage-explicit`, `ddd/rust-host-is-real` | `decision-driven-design` — coverage-not-pass-rate | | -| **G-8** | `ddd/seed-lands-in-claims-not-shared`, `ddd/m8-enforcement-closure`, `cs/authorization-fallback-deny`, `cs/async-policy-rules-at-error`, `cs/policy-rules-at-error`, `ddd/amend-explicit-evidence-frozen` | `decision-driven-design` — closure / predicate closure | Includes the audit's one P1 `absent` grade. | -| **G-9** | ledger `…VYR60TD3`, `…RJ7QYFDK` | `decision-driven-design` — tolerance · granularity bound · floor | | -| **G-10** | ledger `…7EJWN9EK`, `…5ERWSCWJ`, `…9HV1X61F` | `decision-driven-design` — the named accountable actor / acceptance cannot be delegated | Includes the audit's one P2 `absent` grade. | -| **G-11** | `ddd/adapter-policy-tables`, `ddd/rust-class-enforced-here`, `ddd/curation-over-mining`, `ddd/typed-basis` | `decision-driven-design` — claim · falsifier | Lowest confidence in the bucket; each could be ruled ontology-plumbing rather than a reach. | - -**Blocking prerequisite, not a proposal:** none of G-1…G-11 is filable today for a **claim** target. §4.3 — a claim basis is `{claim, status, changed}` over an in-repo id. Filing an upstream *claim* edge needs a format change first. Non-claim types can carry an unpinned `ref` now. - -### 7.2 Bucket two — edges that should be filed as **watched-not-grounding** - -| # | Decision | Upstream target | Note | -|---|---|---|---| -| **W-1** | `ddd/workspace-member-delivery` | `product-framework` — the What/How vocabulary | "it does not extend the What/How vocabulary" — a *boundary-maintenance* dependence: the decision must keep tracking the vocabulary in order to keep not extending it. If U1 adds a node kind, this decision needs re-checking; nothing in it derives from U1. | -| **W-2** | `ddd/adapter-policy-tables` | `decision-driven-design` — claim / falsifiability | Candidate only. The tables are *typed as* claims — conformance to the ontology rather than derivation from it. Listed so the bucket is not artificially a single row; ruling it `grounding` (G-11) is equally defensible. | - -**The bucket is one row, possibly two, out of 44 reaches.** On this evidence the missing relation type is not a cross-repo blocker: it is intra-repo, where the re-typing session already found it. Recorded, not resolved, per §1.7. - -### 7.3 Bucket three — constructs used but not filed upstream (the canon gaps) - -| # | Construct | Reaches | Assertion the audit can make | -|---|---|---|---| -| **C-1** | **determination** | 1 | **A true gap.** No definition in this repo, no citable entry anywhere reachable. Rests on one word — see the ★★ borderline. | -| **C-2** | the predicate/claim split rule (`predicate-definition.md`) | 1 | Upstream entry evidently exists — a decision names the file. **Missing pin, not missing entry.** | -| **C-3** | specification demand · conservation · the four stores | ~12 | Not citable from here. Never named by any decision (§5.3). | -| **C-4** | escape, priced vs silent | 8 | Not citable from here. | -| **C-5** | the closure principle | 6 | Not citable from here; the local restatement is one sentence. | -| **C-6** | coverage-not-pass-rate | 2 | Not citable from here. | -| **C-7** | tolerance · floor · assurance level | 3 | Not citable from here. | -| **C-8** | seam | 2 explicit | Not citable from here, and **no definitional home locally either** — `.ddd/seams/` stores instances, nothing defines the construct. Second-closest to a true gap after C-1. | - -**Eight constructs.** Only C-1 is asserted as a genuine canon gap; C-2 is evidence of an unpinned entry; C-3…C-8 are stated as *not citable from here*, per constraint 5, and would resolve to pins the moment the upstream repos are readable. - ---- - -## 8. Scoping line for L4 - -**Declared cross-repo edges that exist today for an upstreams manifest to pin: one — and it is not machine-readable.** - -The full inventory: - -- **P1** — 59 basis edges, **0** naming an upstream repo. One prose section reference (`ddd/what-published-qualifier` → §3.2.0) and one prose document reference (`ddd/predicates-carry-no-status` → `predicate-definition.md`). -- **P2** — 28 basis refs, **0** naming an upstream repo. All 28 resolve inside this repo. -- **`.decisions/upstreams.yaml`** — does not exist. **`upstreams.lock`** — does not exist. -- Neither store can pin an upstream **claim** at all (§4.3). - -> **Federation delivers value only *after* an edge-filing pass, not before.** Building the manifest, the transitive resolver, the lockfile, the diamond-conflict finding and the basis-cone loader today would produce machinery with **one edge** to operate on — and that one edge would have to be hand-promoted from prose first. Every L4 mechanism the ledger PRD §9.4 specifies is correct and none of it has input. -> -> The ordering this implies: **file the edges first** (§7.1 is a work-list of roughly 40 reaches over ~10 upstream targets, most of them one-to-many), and let L4 pin what exists. The one caveat is the format prerequisite in §7.1 — a claim-typed upstream edge needs a slot before it can be filed — so a **minimal format change enabling a cross-repo basis ref is the true first step**, ahead of both the filing pass and L4 proper. - ---- - -## 9. One line on the bottom of the stack - -**Yes — one decision, and its construct's home is not a filed claim anywhere this audit can reach.** - -`dec/cs/policy-rules-at-error` reaches below `decision-driven-design` for **determination** — "the entry is filed here because the catalog is where determinations live" — the actor-general level's own construct. It is used as a settled term of art to justify where an entry lives, and it is defined nowhere in this repo; the only other occurrence, `docs/ddd-cli-prd.md` line 17, uses it equally undefined. Whether its upstream home is a filed claim, a definition, or a paper's related-work **cannot be determined from here**, because `actor-indexed-determination` was not cloned and has no local mirror. - -So the honest answer is two-part: **the stack's bottom is reached exactly once in 57 decisions, and the audit cannot tell you what it reached.** That is a smaller exposure than the corpus's shape would suggest — but it is also the one reach where "unverifiable, not absent" (constraint 5) does the most work, and where a single ruling on one word (§6) decides whether the finding exists at all. - ---- - -## 10. Addendum, 2026-08-12 — the store moved under the audit - -**Post-hoc. Nothing in §1–§9 is restated, re-cut, or re-run.** This section -exists because the branch carrying this audit later merged `main` at `75b5472`, -which brought the M8 phase-3 ledger migration (`9df74c1`) — and that migration -changes one of the two populations §2.1 measured. Leaving the document silent -about that would make its counts read as current when they are a snapshot. - -### 10.1 What the audit measured, and what the store looks like now - -Every number in §2–§9 is the store **at `6d3dff5`**, this branch's base. - -| | At `6d3dff5` (measured) | At `75b5472` (after the merge) | -|---|---|---| -| **P1** `.ddd/decisions/*.yaml` | 45 | **45 — unchanged** | -| **P2** ledger decisions / versions | 12 / 12 | **91 / 91** | -| **P2** sets | `ledger-design` (12) | `ledger-design` (12) + **`ddd-governance` (79)** | -| **P2** basis refs | 28 | **172** | - -The 79 new entries are the `.ddd` governance records migrated into `dec:` ids — -the M8 work the audit's own §9 anticipated. **The reading is not re-run here.** -Re-measuring a 7.5×-larger P2 is a separate session against the same fixed §1, -not an edit to this one. - -### 10.2 The headline survives the migration — and strengthens - -The §8 scoping line was: *zero declared cross-repo edges; one prose reference.* -Against the post-migration store, **172 basis refs across 91 versions carry -these schemes**: - -`ddd-content` 78 · `claim` 47 · `prd` 20 · `format` 9 · `mandate` 3 · -`watched` 3 · `ruling` 3 · `constraint` 3 · `preference` 2 · -`risk-acceptance` 1 · `indeterminate` 1 · `handoff` 1 · `experiment` 1 - -**Not one is cross-repo.** No `repo:`, no URL, no SHA pin. So the L4 scoping -conclusion holds over a store six times the size the audit graded: the migration -multiplied the *in-repo* edge count and added **zero** upstream edges, which is -what §4.1 predicted a mechanical migration would do — it carries forward what -was declared, and nothing was. - -### 10.3 One statement in §4.4 and §7.2 is now partly superseded - -Both said the watched-not-grounding relation has **no machine-readable home**. -That was true at `6d3dff5`. It is no longer exactly true: the migration -introduced an explicit **`watched:` basis-ref marker**, carried on three edges — - -- `watched:DDD-adapter-02@sha256:b333063d…` on `dec/ddd/internal-not-surface` -- `watched:DDD-gates-01@sha256:6f500ee8…` on `dec/rust/no-unwrap` -- `watched:DDD-adapter-01@sha256:922a0a70…` on `dec/ddd/m6-proceeds-no-flip` - -— alongside a ledger decision stating that the marker's standing is *"either a -distinct edge kind in the ddd ontology or is retired at the F-batch; until -ruled, migrated watched edges carry the explicit `watched:` marker and are never -read as ground."* - -Three observations, and no proposal: - -1. **The marker is provisional by its own text**, so the relation has a - *transport* now, not a ruled ontology. §7.2's bucket remains open. -2. **The three marked edges are exactly the three the audit identified as - intra-repo** in §4.4 — the secondary claim edges retained by the re-typing - session. The migration and the audit found the same set independently, which - is corroboration rather than coincidence. -3. **It does not move §4.4's cross-repo count.** All three are in-repo claim - edges. The audit's one watched-not-grounding *upstream* row — - `ddd/workspace-member-delivery` → the What/How vocabulary — is untouched and - still has no home. - -### 10.4 Addendum (2026-08-13) — the relation is ruled; the upstream row is not - -Recorded, not re-cut: the reading above stands as taken. What changed after it -is one ruling and its consequence for the one row §10.3 left open. - -The principal ruled (2026-08-13) that a watched-not-grounding edge is a -**distinct edge type**, `revisit_if` — this claim's death reopens the decision; -it is not the decision's ground — filed as its own edge with its own -vocabulary, never inside `based_on`. §7.2's bucket therefore closes, and -§10.3's observation 1 is superseded: the relation now has a ruled ontology, -not only a transport. The three intra-repo edges §10.3 names were re-decided -onto the real edge type, each filed for the principal's acceptance, with their -pins carried across unchanged. `dec/ddd/m6-proceeds-no-flip`'s drifted edge — -the only one of the three that fires — now reports as a **reopen** finding -rather than a basis-loss one, which is the correction the ruling makes: that -decision's ground is the principal's stated default and has never moved. - -§10.3's observation 3 is **unchanged and now the whole of what is open**. The -audit's one *upstream* watched-not-grounding row — -`dec/ddd/workspace-member-delivery` tracking the What/How vocabulary — is still -unfiled, and the reason is precise: `revisit_if` gives the *relation* a home; it -does not give a *cross-repo pointer* one. That needs a reference shape carrying -a repo and a revision, which is a separate amendment that has not landed and -was not in flight on 2026-08-13. The row is blocked on it, not forgotten. When -that shape lands the row becomes expressible with no further change to this -edge type — one shape, both pointer fields. - -§4.4's cross-repo count is untouched: still zero declared cross-repo edges. diff --git a/docs/ddd-adrs.md b/docs/ddd-adrs.md deleted file mode 100644 index 5406af33..00000000 --- a/docs/ddd-adrs.md +++ /dev/null @@ -1,88 +0,0 @@ -# DDD Architecture Decisions — the settled set - -Extracted from the PRD's §13 and the `.ddd/` graph per `dec/ddd/prd-split`. -**This document records; it decides nothing.** Every entry cites its graph -id — `ddd why ` resolves the full chain (rationale, principal, typed -bases with pin state). Where a decision's basis has moved, the graph entry -carries the re-affirmation note; this document does not restate it. - -## The load-bearing six - -### 1. Workspace delivery — `dec/ddd/workspace-member-delivery` - -Deliver `ddd` as workspace members of `product-cli` (`ddd-core` / -`ddd-lsp` / `ddd-mcp` / `ddd-cli`), reusing the store conventions, the -validation engine, and the MCP plumbing through public crate surfaces -only. The DDD ontology is its own namespace and store; it does not extend -What/How. Basis: `DDD-arch-01`. - -### 2. Declarative validation on the shared SPARQL engine (SHACL-style) — `dec/ddd/workspace-member-delivery`, ontology rules in `ddd-core/src/rules.rs` - -The §-6 ontology rules are declarative shapes — `sh:sparql`-style SELECTs -run by `product_core::pf::sparql_rules` over a Turtle projection — -rather than hand-rolled graph walks. A consequence of the reuse decision, -recorded separately because it is load-bearing: adding an ontology rule -is adding a query, not a traversal. - -### 3. SARIF unification — `dec/ddd/sarif-unification` - -One ingestion module; per-language invocation adapters know only how to -produce a SARIF file and which rule-id namespace it carries. Basis: -`DDD-detect-01` (both toolchains emit SARIF; one structured format beats -two console parsers). - -### 4. Native language graph, behind an LSP-shaped host — `dec/ddd/lsp-as-seam` - -Each language is reached through its own semantic graph (Roslyn for C#), -held in a per-language host the core talks to over LSP plus `ddd/*` -extensions; the core itself links no compiler API. Re-decided 2026-09-29 -(awaiting acceptance): as filed 2026-08-02 it read "the core never touches -Roslyn or Bicep APIs", on basis `DDD-arch-02`, which the re-decision drops. -Plan: `docs/ddd-roslyn-host-plan.md`. Corollary: -`dec/ddd/interceptor-not-extension` — seam detection is an edit-flow -interceptor consuming LSP events, not an LSP server extension. - -### 5. Adapter capabilities — `dec/ddd/adapter-policy-tables`, split forced by `DDD-adapter-04` - -Contract surface is defined per-language in adapter policy tables, and -tables are falsifiable claims (wrong rows die in the table, not the -core). The M6/M7 experiments split the adapter into capabilities: the -policy-table cost claim held for the classifier (`DDD-adapter-01`, -established at M7), while host lifecycle proved bespoke per server -(`DDD-adapter-04` — the evidence that forced the host / diagnostic / -config / classifier / detector decomposition in the spec §4), and -producer shapes are priced at the serve layer (`DDD-adapter-05`). - -### 6. Curation, not mining — `dec/ddd/curation-over-mining` - -Detection is conformance checking against declarations; the tool never -infers commitments from code. Basis: `DDD-method-01`/`DDD-method-02` -territory: the graph is the source of truth. - -## The rest of the settled set - -| Decision | Graph id | -|---|---| -| v1 scope: local repo, C# + Bicep, build-output detection first | `dec/ddd/v1-scope` | -| Predicates carry no status; enforcement structural | `dec/ddd/predicates-carry-no-status` | -| Basis pins land at M2, not deferred | `dec/ddd/pins-at-m2` | -| Tests run against committed SARIF fixtures; no .NET in workspace CI | `dec/ddd/fixtures-not-sdk` | -| `internal` is not contract surface by default (app-repo posture) | `dec/ddd/internal-not-surface` | -| Rejection returns facts pre-filled, judgment blank (PRD §14 q3) | `dec/ddd/rejection-facts-prefilled` | -| M5 seed lands in `claims/`, not `shared/` (PRD §14 q5) | `dec/ddd/seed-lands-in-claims-not-shared` | -| Enforce-mode matching tightens to the symbol | `dec/ddd/enforce-matching-tightens-to-symbol` | -| The tool's own Rust class is enforced here | `dec/ddd/rust-class-enforced-here` | -| Rust host is real (rust-analyzer child, not a stub) | `dec/ddd/rust-host-is-real` | -| M6 proceeds; the M6/M7 flip condition did not fire | `dec/ddd/m6-proceeds-no-flip` | -| HTML+CSS governed as a pair; plain-pair scope; enforce mode | `dec/web/plain-pair-scope`, `dec/web/htmlcss-enforce` | -| `why` resolves three ways (unfiled ≠ unknown) | `dec/ddd/why-resolves-three-ways` | -| What boundaries priced, not paid | `dec/ddd/what-boundaries-priced-not-paid` | -| The What policy table + published qualifier | `dec/ddd/what-policy-table`, `dec/ddd/what-published-qualifier` | -| A decision's basis is typed; non-claim bases first-class | `dec/ddd/typed-basis` | -| M8 expands to enforcement closure | `dec/ddd/m8-enforcement-closure` | -| The PRD splits | `dec/ddd/prd-split` | - -The ledger relationship (record substrate, `dec:` migration at M8) is -owned by [`decision-ledger-prd.md`](../ledger/prd/decision-ledger-prd.md) OD-2 and the -roadmap; the process layer (DAD) by -[`way-of-working-decision-allocated-delivery.md`](way-of-working-decision-allocated-delivery.md). diff --git a/docs/ddd-cli-prd.md b/docs/ddd-cli-prd.md deleted file mode 100644 index ebf74334..00000000 --- a/docs/ddd-cli-prd.md +++ /dev/null @@ -1,67 +0,0 @@ -# DDD Specification Platform — umbrella - -**Working name:** `ddd` (final name open) -**Author:** Emil, Context& -**Status:** Umbrella index; the implementable contract lives in the -documents below (split per `dec/ddd/prd-split`, 2026-08) - -## Thesis - -A CLI and MCP server that make a repository's governing decisions -explicit, versioned, and checkable. The graph under `.ddd/` stores -predicates, closure claims, decisions, analyzer/linter manifests, pattern -declarations, and seam declarations; the tool checks the code against -them. **Curation, not mining: the graph is the source of truth and the -code must conform to it.** - -The platform is the seam between Context& engagements: determinations -resolved once are filed as graph entries and inherited by subsequent -projects. Prompt rules are exhortation an agent can drift past; a tool in -the edit loop is a policy-level commitment — and its honest boundary is -stated in the spec: interception governs the governed path; CI governs -the repository. - -## The documents - -| Document | Owns | -|---|---| -| [`ddd-v1-spec.md`](ddd-v1-spec.md) | The implementable contract for what exists (M1–M7 as built): invariants, architecture, adapter capabilities, graph store, commands, interception semantics, operational requirements, success criteria | -| [`ddd-adrs.md`](ddd-adrs.md) | The settled architecture decisions, each citing its graph entry | -| [`ddd-research-protocol.md`](ddd-research-protocol.md) | The correspondence dataset, the adapter-cost experiment (predicted/observed separated), the classifier corpus | -| [`ddd-roadmap.md`](ddd-roadmap.md) | M8 (enforcement closure) and the filed follow-ups | -| [`reviews/ddd-cli-prd-review-2026-08.md`](reviews/ddd-cli-prd-review-2026-08.md) | The review that forced the split — the checker event, filed in the graph (`DDD-arch-08`, `DDD-arch-09`, `DDD-method-06`) | - -## Ontology and companion references - -- Predicates and claims: `predicate-format.md`, claim formats, migration - record in [`ddd-format-migrations.md`](ddd-format-migrations.md) -- Process layer: [`way-of-working-decision-allocated-delivery.md`](way-of-working-decision-allocated-delivery.md) - (DAD — what an engagement runs; this tool is its enforcement substrate) -- Record substrate: [`decision-ledger-prd.md`](../ledger/prd/decision-ledger-prd.md) - (canonical for the decision record; `ledger/spec/ledger-protocol.md` is what an - outside implementation imports). The M8 migration (2026-08) made the - ledger the record of `.ddd` governance: the `.ddd` files remain as - pinned content artifacts, and `.ddd/concordance.yaml` maps the ids - permanently. - -## History note - -M1 (graph store, `validate`, `why`), M2 (SARIF diff, `report escapes`, -basis pins), M2.5 (`render`), M3 (MCP language tools over LSP hosts), M4 -(the `apply_edit` interceptor), M5 (dogfood curation — the DAD checklists -seeded ~70 claims), M6 (Rust adapter; the adapter-cost experiment), and -M7 (the HTML+CSS pair; the experiment's falsifier) shipped 2026-08, -each with its report or graph record. The 2026-08 review found the PRD -overstating the enforcement boundary and conflating five documents; the -three resulting rulings (typed basis, M8 enforcement closure, this split) -are filed as `dec/ddd/typed-basis`, `dec/ddd/m8-enforcement-closure`, -`dec/ddd/prd-split`. This umbrella introduces nothing; every normative -statement lives in exactly one of the documents above. - -**Section concordance for historical references.** Code comments and -graph entries citing "PRD §N" cite the pre-split PRD's numbering, kept -stable here: §5 architecture → spec §3; §6 graph store and ontology -rules (rule 6 = basis pins) → spec §5; §7 command surface and finding -taxonomy → spec §6; §8 MCP surface and interception semantics → spec §7; -§9 adapters → spec §4; §13 decisions embodied → the ADRs; §14 open -questions → spec §10 and the ADRs' settled table. diff --git a/docs/ddd-format-migrations.md b/docs/ddd-format-migrations.md deleted file mode 100644 index 95219a1d..00000000 --- a/docs/ddd-format-migrations.md +++ /dev/null @@ -1,388 +0,0 @@ -# DDD Entry Format Migrations - -Per PRD §6, every schema change is a format-version bump with a migration -note, and `ddd validate` checks each entry against the version it declares — -existing entries never break silently. This file is the migration record. - -## Format 2 (M2) - -Format 1 entries remain valid unchanged; nothing forces a migration. Format 2 -exists for the two capabilities `ddd report escapes` needs. - -### Claims: optional `revalidate_by` - -A format-2 claim may declare a revalidation cadence — the date by which its -status must be rechecked: - -```yaml -format: 2 -id: DDD-cat-01 -# ... -changed: 2026-08-02 -revalidate_by: 2027-02-01 -``` - -`ddd report escapes` flags any live claim whose `revalidate_by` has passed. -Declaring the field under `format: 1` is a validation violation. - -**Migrating a v1 claim:** set `format: 2` and add `revalidate_by` if the claim -should carry a cadence. No other field changes meaning. - -### Decisions: pinned `based_on` edges (PRD §6 rule 6) - -A format-2 decision pins **every** `based_on` edge with the claim's `status` -and `changed` values as they were at decision time: - -```yaml -format: 2 -id: dec/cs/async-config -# ... -based_on: - - claim: DDD-cat-01 - status: reported - changed: 2026-08-02 -``` - -Basis-loss detection (`ddd report escapes`) compares each pin against the -claim's current `status`/`changed`; a mismatch means the ground the decision -stood on has moved. Format-1 decisions keep plain claim-id strings — valid, -but their basis loss is not detectable, and pinned edges under `format: 1` -are a validation violation. Mixing pinned and plain edges in one format-2 -decision is also a violation: pin all edges or stay on format 1. - -**Migrating a v1 decision:** set `format: 2` and replace each `- ` -with a `claim`/`status`/`changed` map, copying the status and changed values -the claim had **when the decision was made** (from git history if the claim -has since moved; if you re-affirm the decision against the claim's current -state, use today's values — that is a re-decision, note it in `notes`). - -### Config: `diff` and `detect` sections - -`.ddd/config.yaml` gains two optional sections under `format: 2`: - -```yaml -format: 2 -intercept: warn -ignore: [] - -# Per-finding treatment for `ddd diff`: error (default) | warn | off. -diff: - ungoverned: error - stale: warn - uncited_suppression: error - -# SARIF files ingested by default; `--sarif` appends. -detect: - sarif: - - artifacts/csharp.sarif - - artifacts/bicep.sarif -``` - -Using either section under `format: 1` is a validation violation. A v1 -config without them stays valid. - -## Format 3 (M3/M4) - -Formats 1 and 2 remain valid unchanged. Format 3 exists for the two -capabilities the MCP surface (M3) and the interceptor (M4) need. - -### Predicates: `obligations` (pattern predicates) - -A format-3 predicate may declare the obligation list a pattern instance -must answer at declaration time (`ddd_declare_pattern` rejects unanswered -obligations): - -```yaml -predicate: - id: pred/composition/decorator - format: 3 - # ... - obligations: - - ordering - - identity-preservation - - forwarding-completeness -``` - -Declaring `obligations` under format 1 or 2 is a validation violation. - -### Config: `intercept_by_class` and `adapter` sections - -`.ddd/config.yaml` gains two optional sections under `format: 3`: - -```yaml -format: 3 -intercept: warn # the global default, as before - -# Per-artifact-class overrides (PRD §8): enforce | warn | off. -intercept_by_class: - code: enforce # C# edits - configuration: warn # Bicep edits - -# Per-language adapter switches, keyed by adapter language name. -adapter: - csharp: - internal_is_surface: false # dec/ddd/internal-not-surface; flip in library repos - exported_attributes: [McpServerTool] - command: ["roslyn-language-server", "--stdio", "--autoLoadProjects"] # host override - bicep: - command: ["bicep-ls"] -``` - -Both intercept vocabularies are validated (`enforce | warn | off`); -unknown modes are violations. Declaring either section under a lower -format is a violation. - -### Seam events (new entry family, format 1) - - -The interceptor logs every classified surface outcome as one row under -`.ddd/seams/events/` — separate from the seam *declarations* directly in -`.ddd/seams/`. Rows are format-versioned like every other entry and load -into `validate`. This is the correspondence dataset (PRD §8); its field -set is the schema M5 files claims against. - -## Format 4 (M5) - -Formats 1–3 remain valid unchanged. Format 4 exists for one capability the -closure-claim seed needs. - -### Claims: `version_index` - -A closure claim reports what an arrangement closes, and an arrangement is a -versioned thing: `Nullable` closes what it closes *in a given C# version, on -a given SDK*. Before format 4 that anchor could only live in `evidence` -prose, where nothing can read it. A format-4 claim may index it: - -```yaml -format: 4 -id: DDD-cs-nrt-01 -version_index: - language: "C# 12" - sdk: "8.0.413" - target: net8.0 - tool: "Microsoft.CodeAnalysis.NetAnalyzers 9.0.0" -status: reported -``` - -Every sub-field is optional, because the anchor differs by claim: a language -rule pins `language`, an SDK behaviour pins `sdk`, an analyzer finding pins -`tool`. But an index must anchor **something** — an index with no version in -it is a validation violation, since it would record the appearance of a drift -anchor without the substance. Declaring `version_index` under format 1–3 is -a violation. - -The field is what a promoting repo checks before inheriting an entry -(`dec/ddd/seed-lands-in-claims-not-shared`): a closure claim indexed to an -SDK the destination does not run is worse than no entry, because it carries -the catalog's authority without holding there. - -## Format 7 (the reopen edge, 2026-08-13) - -Formats 1–6 remain valid unchanged. Format 7 exists for one ruling, and -adopts into this store the one shape the ledger format specifies at spec -v1.5 / `format: 4` (`ledger-format-v1.md` §3.7) — the same one-shape rule -the M8 `contract:` scheme followed. - -### Decisions: `revisit_if`, the reopen edge - -The principal ruled (2026-08-13) that a **watched-not-grounding edge is a -distinct edge type, not a basis**: this claim's death reopens the decision; -it is not the decision's ground. - -```yaml -format: 7 -based_on: - - type: mandate - statement: >- - The principal's ruling settling PRD §14 question 2 … -revisit_if: - - claim: DDD-adapter-02 - status: projected - changed: 2026-08-06 - content: sha256:… -``` - -Rules that arrive with it: - -- **A reopen edge is never a basis.** It lives in its own field, carries its - own type, and is scanned by its own pass. `based_on`'s basis-loss scan - cannot see it and `ddd why` renders it under its own heading, below the - ground and visibly apart. Declaring `revisit_if` under formats 1–6 is a - violation. -- **Every reopen edge pins**, `content:` included — an unpinned tripwire - cannot fire, so an edge without one is a violation. The pin is the - claim's canonical content hash (`ddd.claim-content.v1`), exactly as a - format-6 claim basis pins. -- **One claim is ground or tripwire, never both.** A claim appearing in - both lists on one decision is a violation: an edge read both ways is - precisely the conflation the distinct type exists to end. -- **It reports as `reopen`, never as basis loss.** `ddd report escapes` - gains a fourth section with its own heading and its own message: a fired - tripwire means the decision is due a fresh look, its ground untouched. - A lost basis means the ground moved. Merging the two tells the reader - neither. -- **It is hashed content.** `revisit_if` joins `decision_content_hash` - under its own key, so re-typing an edge from ground to tripwire moves the - decision's content hash — which is what makes the re-decision visible to - the ledger entry pinning it rather than a silent rewrite. - -**Migrating a format-5/6 decision:** set `format: 7`, move the claim edge -out of `based_on` into `revisit_if`, and carry its pin across unchanged -(`status`, `changed`, and the `content` hash at that pinned state — the M8 -migration's recovered hashes are the ones already in the ledger's `watched:` -tokens). Do **not** re-pin at the claim's current state: a recorded drift is -the record that the movement was looked at, and re-pinning would erase it. -Then file a new ledger version for the decision, re-taking its -`ddd-content:` pin at the entry's new content and stating the reopen edge — -a re-decision filed for the principal's acceptance, never a silent rewrite. -`ddd content-hash ` prints the pin to state. - -**Migrated by this amendment:** the three edges the M8 migration carried as -provisional `watched:` markers — `DDD-adapter-02` on -`dec/ddd/internal-not-surface`, `DDD-gates-01` on `dec/rust/no-unwrap`, -`DDD-adapter-01` on `dec/ddd/m6-proceeds-no-flip`. The third is the one -whose pin has drifted; it fires, now as a reopen finding. - -## Format 6 (M8, 2026-08) - -Formats 1–5 remain valid unchanged. Format 6 exists for the two M8 -capabilities: declarations that sign the change they discharge, and -basis loss as an exact content comparison. Both hash forms ride the -ledger's canonical-JSON law (`ledger_core::canon`'s primitives) under -their own domain prefixes — one canonicalisation scheme, per-payload -field sets. - -### Seams: signed `bindings` (seam format 2) - -A seam declaration's entry family moves to `format: 2` when it carries -`bindings` — the signed transitions the declaration discharges: - -```yaml -format: 2 -id: seam/rust/added -# ... -bindings: - - symbol: added - file: src/lib.rs - before: sha256:… # parent-state content hash, or `absent` - after: sha256:… # proposed-state content hash - base_revision: # where the parent state is committed - hash: sha256:… # ddd.seam-binding.v1 over the five fields -``` - -A binding's parent state must be committed — a dirty file refuses to -bind (M8 ruling 2) — and matching is by signature only: same-session -matching is retired (M8 ruling 4, closing `DDD-arch-09`). Declaring -`bindings` under seam format 1 is a validation violation; a stored -binding hash that does not recompute is a violation. - -### Decisions: content-hash pins - -A format-6 decision pins **every** claim edge with `content:` — the -hash of the claim's canonical content at decision time -(`ddd.claim-content.v1`). Basis loss becomes pinned-hash ≠ current-hash, -mechanically exact (spec invariant 2); status and `changed` stay on the -pin as the human-readable record, and since both are part of the hashed -content, hash equality subsumes the old heuristic: - -```yaml -format: 6 -based_on: - - type: claim - claim: DDD-arch-08 - status: reported - changed: 2026-08-11 - content: sha256:… -``` - -Declaring `content` under formats 1–5 is a violation; a format-6 -decision with a claim pin missing `content` is a violation. - -**Migrating a v2–v5 decision:** set `format: 6` and add `content:` to -every claim pin. The pinned hash is the claim's content *at decision -time*: when the claim's current status+changed still equal the pin, -hash the current content; when the pin records drift that was -deliberately kept (a re-affirmed edge), recover the claim's content at -the pinned state from git history and hash that — the recorded drift -then keeps reporting as basis loss, exactly as before, now exactly. - -## Format 5 (PRD review, 2026-08) - -Formats 1–4 remain valid unchanged. Format 5 exists for one ruling: a -decision's basis becomes **typed** — `claim | constraint | mandate | -preference | experiment | risk-acceptance`. Forcing claim-basis where none -exists manufactures weak claims to pass validation, the exact corruption -the graph exists to prevent; the non-claim types are honest first-class -bases instead. The `basedOn → claim` edge remains the load-bearing form -where a claim *is* the basis. - -### Decisions: typed `based_on` entries - -A format-5 decision types **every** basis. A claim basis is the format-2 -pinned shape plus `type: claim` — the pin discipline (rule 6) carries -forward unchanged. A non-claim basis states what it rests on; a -`risk-acceptance` basis refs the record it rests on (which must exist in -`decisions/` with `kind: risk-acceptance` — checked by the ontology rules): - -```yaml -format: 5 -id: dec/x/y -# ... -based_on: - - type: claim - claim: DDD-cat-01 - status: reported - changed: 2026-08-02 - - type: preference - statement: Four documents beat one implementable-contract PRD - - type: risk-acceptance - ref: risk/rule/ca2007 -``` - -Ontology rule 3 is restated accordingly: "every decision has ≥1 `basedOn` -claim" becomes **"every decision has ≥1 typed basis"** — satisfied by a -claim edge or by a format-5 non-claim basis. `ddd why` renders the basis -type; basis-loss detection (`ddd report escapes`) still checks claim pins -and counts non-claim bases separately (they carry no status that could -move). Declaring a type under formats 1–4, leaving a basis untyped under -format 5, a `type: claim` basis without its pin, and a non-claim basis -with no `statement:` (or, for `risk-acceptance`, no `ref:`) are each -validation violations. - -**Migrating a v1–v2 decision:** nothing forces a migration — existing -entries stay valid under their declared format, and their untyped edges -*read as* `type: claim` implicitly (that is what they always were; the -plan-gate call here is no mechanical rewrite, so decision-time records -stay byte-identical). Move to `format: 5` only when filing or re-filing a -decision whose basis is genuinely not a claim — that is a re-decision of -its basis, note it in `notes`. - -**Migrating a v1–v3 claim:** set `format: 4` and add `version_index` if the -claim is a closure finding whose truth depends on a toolchain version. Claims -about method, architecture or scope need no index and should stay at their -current format. No other field changes meaning. - -### Config: the pair map plus the web detection sources (M7) - -`.ddd/config.yaml` gains, at `format: 4`: - -- **`pair`** — the governed HTML+CSS pair map (`units`, each an - `html`/`css` glob pair) plus the class-contract thresholds - (`ignore_classes`, decorative one-off class globs). Consumed by the - `htmlcss` adapter's enrichment and by the pair contract check in - `ddd report escapes`. Thresholds are data; what a pair *means* stays in - the adapter. -- **`detect.stylelint` / `detect.htmlvalidate`** — the tools' own JSON - output files as emitted sources for the `stylelint` / `htmlvalidate` - namespaces (stylelint ≥16 writes its report to stderr: - `stylelint -f json 2> out.json`; html-validate: - `html-validate -f json > out.json`). Configured sources need no - registration — `.stylelintrc.json` and `.htmlvalidate.json` are found by - the detection walk like `bicepconfig.json` is. -- **`detect.tokens`** — design-token stylesheets whose custom properties - become the configured source for the `tokens` namespace, one rule per - token, so a token resolves through `why` to its decision and `diff` - flags UNGOVERNED or STALE tokens. - -Declaring any of these under `format: 1`–`3` is a validation violation. -Existing configs remain valid unchanged. diff --git a/docs/ddd-m5-report.md b/docs/ddd-m5-report.md deleted file mode 100644 index 3c7ba913..00000000 --- a/docs/ddd-m5-report.md +++ /dev/null @@ -1,192 +0,0 @@ -# M5 — Curation: closing report - -The milestone's product is accepted content. Every entry below was proposed -in-session, presented with its ground, and accepted per entry by the -principal before the next batch was drafted. Nothing was generated in bulk. - -**Store:** 24 predicates · 68 claims · 33 decisions (126 entries with -manifests and config). **This branch:** 85 entries added, 22 amended, across -17 content batches plus one format bump. - ---- - -## 1. Coverage by phase - -### Phase 1 — dogfood closure - -`ddd report escapes` now prints clean in all three sections **with no -not-checkable set anywhere**, which is what the M3+M4 session left standing. - -| Finding | Disposition | -|---|---| -| 7 unpinned `based_on` edges | **Resolved.** Decision-time status recovered from git (`136aa55`); none of the seven claims had moved since, so the pins are recovered rather than assumed. No late-pin marker needed. | -| 15 live claims with no `revalidate_by` | **Resolved.** Cadence set per claim from drift rate × consequence — 6 months on claims resting on a moving spec or a market read, 24 months on corpus-settled methodology. | -| 24 undeclared What boundaries | **Priced, not paid.** `dec/ddd/what-boundaries-priced-not-paid`, exposure stated and verified, review by 2027-02-07. `ddd what --strict` still exits 1, deliberately. | -| M3/M4 flagged choices (4) | **Settled.** PRD §14 q5; enforce-mode matching granularity; correspondence-row stratification; the C# enum-member policy-table gap. | - -`changed` was deliberately not touched when adding cadences: a cadence is -metadata about when to recheck a finding, not a change to the finding, and -bumping it would have fired basis loss on seven decisions — reporting that -the ground moved when it had not. - -### Phase 2 — the C# closure-claim seed - -Three predicates, 17 claims, **every status earned by compilation rather than -recall**. Each hole builds clean under `Nullable enable` + -`TreatWarningsAsErrors` and then faults at runtime. - -- `pred/code/null-dereference-safety` — 7 claims -- `pred/code/static-type-conformance` — 5 claims -- `pred/code/deterministic-disposal` — 6 claims - -Two of the kickoff's assumed boundary clauses came back **corrected**, which -is the main argument that the discipline was worth its cost: - -- **EF scalar materialisation is not a null-dereference hole.** A NULL column - against a non-nullable property throws `InvalidOperationException` from the - provider's reader before the entity is returned. The static gap is real; - the runtime closure is the provider's. (`DDD-cs-nrt-06`) -- **The real EF hole is prescribed by EF's own documentation.** `= null!` on - a required navigation is the documented remedy for CS8618; unloaded, it - faults at the dereference. (`DDD-cs-nrt-05`) -- **Async disposal is open in one shape only.** Implementing *only* - `IAsyncDisposable` under a sync `using` is `CS8418`, and the compiler names - the fix. The gap is the ambiguity of implementing both. (`DDD-cs-disp-03`) - -### Phase 3 — checklist conversion - -All 19 items routed with ground attached. §6.2's eleven and §6.3's nine, one -already covered. - -| Routing | Count | Examples | -|---|---|---| -| Analyzer-closable | 6 | CA1305/1307/1309, CA1031, CA2007, CA2016, `use-recent-api-versions` | -| Assertion-closable | 2 | AutoMapper coverage, EF concurrency tokens | -| Composition-closable | 1 | authorization fallback policy | -| Review over a complete worklist | 2 | Bicep parameter defaults, sizing values | -| Open | 7 | ordering, `async void`, transaction extent, RBAC scope, … | -| Already covered | 1 | `!` → `DDD-cs-nrt-04` | - -Item 9 (async shape) **decomposed onto predicates already filed** rather than -needing its own: ConfigureAwait and CancellationToken are policy arguments, -HttpClient lifetime is a disposal proxy-gap instance. Only `async void` -needed a new predicate. That is the catalog composing rather than growing -linearly, and it is the first evidence that the predicate set is the right -shape. - -Item 10 (concurrency) was predicted to resist the `P(c, G)` form and does -not: token presence is readable from the built model, so only *which* -entities need one stays with judgment. - ---- - -## 2. The five closure modes - -Not planned; they emerged from the routing and they are not interchangeable. -Recording them because the middle three are routinely described as "we have a -check for that" when their failure modes differ completely. - -1. **Analyzer-closed** — build time, unconditional, no way to skip. -2. **Assertion-closed** — startup or test time, **conditional on the - assertion running**. Most likely to be recorded as closed while inert. - `dec/cs/startup-assertions-required` exists for this reason. -3. **Snapshot-closed** — an approved output shape (DAD §4.2). Closes - serialisation contracts, which no analyzer reaches. -4. **Composition-closed** — an arrangement in the composition root. One line - to add, one line to delete; warrants an architecture test. -5. **Review-closed over a mechanically complete worklist** — the judgment is - human, the *enumeration* is not. DAD §11 names enumeration completeness as - the load-bearing gap with no mechanical check; for Bicep parameter - defaults and sizing values, there is one. - ---- - -## 3. Manifest candidates awaiting adoption - -Nine decisions are filed and accepted but operative only where a repo's -manifest cites them. None applies to this workspace, which is Rust. - -| Decision | Closes | Not bought | -|---|---|---| -| `dec/cs/policy-rules-at-error` | CA1305/1307/1309/1310 | the ambient clock | -| `dec/cs/general-catch-at-error` | CA1031 | the continuation under a narrowed type | -| `dec/cs/async-policy-rules-at-error` | CA2007, CA2016 | token *presence* | -| `dec/cs/disposal-rules-at-error` | CA1001, CA2000 | async, container hand-off, HttpClient | -| `dec/cs/startup-assertions-required` | mapper coverage, EF tokens | which entities need one | -| `dec/cs/authorization-fallback-deny` | unattributed endpoints | per-endpoint policy | -| `dec/cs/unsafe-reinterpretation-banned` | `Unsafe.As`, `Marshal.PtrToStructure` | interop exception path | -| `dec/bicep/linter-at-error` | API currency, secret rules | secrets not *named* like secrets | -| `dec/bicep/psrule-in-pr-stage` | network exposure, diagnostics | RBAC scope, `existing`, sizing | - -**Two adoption traps, both measured:** - -- **PSRule passes vacuously.** Without the standalone Bicep CLI on `PATH` and - every parameter resolvable, expansion fails, PSRule reports *"no matching - rules were found"* and zero failures — indistinguishable from a clean run - in an exit code. Adoption must assert that expansion happened. -- **The Bicep secret chain is a name heuristic.** `adminPassword` drew - findings from both tools; renamed to `seedValue`, still leaving through a - deployment output, it drew nothing from either. One rename defeats two - independent tools. - ---- - -## 4. Evidence to upgrade later - -Five claims are honest about resting on something weaker than a run. Each -says so in its own `evidence` field; none should be promoted without closing -the gap. - -| Claim | What is cited rather than exercised | -|---|---| -| `DDD-cs-disp-06` | socket exhaustion from per-call HttpClient — Microsoft guidance, no load test run | -| `DDD-bicep-idem-01` | that a structural `what-if` assertion supplies the criterion — no Azure subscription available | -| `DDD-cs-pol-02` | Meziantou.Analyzer as the clock rule's home — package not exercised, rule id deliberately not cited | -| `DDD-cs-obs-01` | Threading.Analyzers as `async void`'s home — same | -| `DDD-bicep-diag-01` | retention *values* — the probe never declared a setting with a retention, so this is absence of an observation, not observation of an absence | - -Thirteen claims remain `projected`. Two are worth a look: **`DDD-arch-02` and -`DDD-arch-03` were exercised by M3/M4** — the LSP surface classified real -edits on real Roslyn and the interceptor demanded declarations without -extending either server. They look like `reported` candidates. Status -promotion is the principal's; they were not promoted here. - ---- - -## 5. What M6 needs from this catalog - -M6 adds the Rust adapter and gives this workspace a governed surface of its -own. Three entries are waiting for it: - -- **`dec/ddd/enforce-matching-tightens-to-symbol`** — enforce-mode matching - drops the file arm. Its acceptance test is already written as the claim's - falsifier: a correspondence row whose `linked_declaration` names a symbol - other than its own should not occur in enforce mode. -- **`dec/ddd/enum-member-gap-priced`** — add `enum-member` to `DECL_KINDS` - plus a policy row. Small, adapter-local, and the first live test of - `dec/ddd/adapter-policy-tables`, which it passed. -- **`DDD-what-04` / the boundary deferral** — reviewed 2027-02-07, or sooner - when M6 gives the workspace a real governed surface, whichever comes first. - -Beyond that, M6 is the first opportunity to put **correspondence rows** in -the store. `.ddd/seams/events/` is still empty, so `DDD-arch-04`, -`DDD-arch-05`, `DDD-arch-06` and both friction claims have no ground in this -repo at all — their six-month cadences are a bet that M6 supplies it. If it -does not, the empty recheck is itself the finding. - ---- - -## 6. Known limits of this milestone - -- **One toolchain.** Every closure claim is indexed to C# 12 / SDK 8.0.413 / - net8.0, or to Bicep CLI 0.37.4 / PSRule 1.47.0. Nothing was checked against - .NET 9 or 10, both of which are installed here. -- **One provider.** Every EF observation is Sqlite. `DDD-cs-nrt-06` names - this explicitly and makes another provider behaving differently its - falsifier. -- **No engagement repo.** The catalog has never been inherited by a second - repo, which is PRD §12's fourth success criterion and the whole basis of - `dec/ddd/seed-lands-in-claims-not-shared`. `shared/` is still empty by - decision, not by omission. -- **The committed PRD is an older revision than the working draft.** Claims - cite §-numbers from the committed copy; a re-sync should check them. diff --git a/docs/ddd-m6-experiment.md b/docs/ddd-m6-experiment.md deleted file mode 100644 index 9d5c2acd..00000000 --- a/docs/ddd-m6-experiment.md +++ /dev/null @@ -1,472 +0,0 @@ -# M6 — the Rust adapter as an adapter-cost experiment - -**Status:** complete — prediction filed 2026-08-10 in commit `a8871da`, before any -code was written; §4 onward added after implementation. -**Date filed:** 2026-08-10 -**Claim under test:** `DDD-adapter-01` (status `projected`), and the broader -PRD §11 mitigation *"new languages require only an adapter + policy table by -design"*. - ---- - -## 1. What is being measured - -M6 adds a third language. The measurement is **how much of the core had to -move to admit it**. Every change outside `ddd-lsp/src/adapter/rust*.rs` is -classified into exactly one of: - -| Class | Meaning | Counts against the claim? | -|---|---|---| -| **registration glue** | wiring the adapter into routing/config | no — expected and bounded | -| **genuine core bug** | wrong for *all* languages, fixed here | no — filed separately | -| **leakage** | the core assumed something C#/Bicep-shaped and had to change to admit Rust | **yes** | - -Leakage count > 0 weakens the claim. Zero leakage is the evidence that would -move it to `reported`. - -### 1.1 One pre-declared exclusion - -`dec/ddd/enforce-matching-tightens-to-symbol` (filed 2026-08-07) says the -enforce-mode matching change "lands in M6 alongside the Rust adapter". It is -**not implemented in this session**, and it is not in any of the three classes -above. Reasons, in order: - -1. The session's hard constraint is no feature work beyond the adapter; a - fourth category would contaminate exactly the number M6 exists to produce. -2. The decision's own stated acceptance test is a property of the - correspondence rows — *"a row whose `linked_declaration` names a symbol - other than its own should not occur in enforce mode"*. Running M6's - governed tail **with the file arm still in place** is what produces the - first real evidence for that decision. Changing the behaviour first would - destroy the observation. - -M6 therefore leaves it standing and reports what the rows show. The friction -reading in §5 is stated with this caveat attached: it is measured under the -*looser* matching rule and is therefore optimistic. - ---- - -## 2. Prediction (filed before any code was written) - -Files suspected of carrying leaked language assumptions, with the call on -whether Rust will **force** a change. "Latent" = the assumption is real but -Rust does not trip it, so it costs nothing here. - -### 2.1 Predicted leakage — change forced - -**L1 · `ddd-lsp/src/host.rs::initialize` — client capabilities are hardcoded.** -The `initialize` payload is a fixed literal with no adapter hook. rust-analyzer -emits its readiness signal *only* if the client declares -`experimental.serverStatusNotification: true`. Measured on rust-analyzer -1.95.0: with the capability declared, `experimental/serverStatus` arrives at -t+0.15s; **without it, zero such notifications are ever sent**. The core -assumed one fixed capability set serves every host — true for Roslyn and -bicep-ls, false for rust-analyzer. - -**L2 · `Adapter::ready_flag` + `ServerState::flags` — readiness is modelled as -"a notification method name was seen at least once".** -That shape is Roslyn's (`workspace/projectInitializationComplete`, which is -sent once and means ready). rust-analyzer's signal is -**payload-discriminated**: `experimental/serverStatus` is sent first with -`quiescent: false` (t+0.15s) and again with `quiescent: true` (t+3.92s). A -method-name-only flag reports Ready at t+0.15s — during indexing — which is -precisely the dishonesty `Readiness::Loading` exists to prevent. - -L1 and L2 land as one coherent change but are two distinct assumptions and are -counted as two. - -**Note on not dodging this.** A readiness signal *could* be scavenged from -`$/progress` (token `rustAnalyzer/cachePriming`, `kind: end`), which needs no -extra capability and would avoid L1. That would be gaming the measurement: -`serverStatus`/`quiescent` is rust-analyzer's documented readiness protocol and -cachePriming is disableable and primes dependencies rather than the workspace. -The honest signal is used and the leakage is taken. - -### 2.2 Predicted latent leakage — no change forced - -- **`host.rs::build_inputs`** hardcodes `.sln` / `.slnx` / `.csproj`. Real C# - leakage sitting in a language-neutral module, but reachable only behind - `needs_open_handshake`, which Rust sets `false` (rust-analyzer discovers the - workspace itself — the probe shows `cargo metadata: started/finished` at - t+1.96s from `rootUri` alone). Costs nothing here. -- **`host.rs::request`** retries on the Roslyn-specific error string - `"Document is null"`. Harmless for Rust. -- **`ddd-core/src/config.rs::AdapterEntry::internal_is_surface`** — a - C#-flavoured *name* in the core config schema. Predicted to cost nothing, - because the concept is language-neutral (crate-internal visibility is - surface) and Rust's `pub(crate)` is the exact analogue of C# `internal`. - Reusing the key is what the session prompt's "per the internal precedent" - points at. Latent *naming* leakage, not structural. -- **`ddd-lsp/src/mock/parse.rs`** dispatches on file extension and falls - through to a C# parser — language knowledge outside `adapter/`, declared as - test scaffolding. Predicted to cost nothing: unlike the .NET SDK, - rust-analyzer is a rustup component of the toolchain this repo already pins, - so the Rust fixtures run against the **real host**. `dec/ddd/fixtures-not-sdk` - is scoped to .NET and its rationale does not extend to Rust. -- **`ddd-core/src/cargolints.rs`** already reads `[workspace.lints.clippy]` — - Rust knowledge that is *already* in the core. It belongs to the manifest/diff - path, not the adapter contract, and is out of this session's scope. Named - here so it is not mistaken for something M6 introduced. - -### 2.3 Predicted clean — no change expected - -- `ddd-core/src/surface.rs` — `SymbolFacts.visibility` and `.kind` are free - strings and `visibility_rank` is an adapter function, so Rust's graded - visibility (`pub` / `pub(crate)` / `pub(super)` / private) and its extra - kinds (`trait`, `trait-impl`) should need no vocabulary change. -- `ddd-mcp/src/intercept.rs` — the whole interception loop. -- `ddd-lsp/src/classify.rs` — the before/after diff. -- `ddd-lsp/src/protocol.rs` — generic LSP shaping. -- `ddd-mcp/src/{dispatch,lang_tools,govern_tools,state}.rs` — the MCP layer. - -### 2.4 Predicted registration glue - -- `adapter/mod.rs` — `pub mod rust;` and one entry in `all()`. -- Config/docs mentioning `adapter.rust.*`. - -### 2.5 A shape the table cannot express (predicted, adapter-local workaround) - -`PolicyRow` matches on `changes` / `kinds` / `visibilities` / `exported_only` -only — there is no matcher over `SymbolFacts.extra`. The orphan-impl case -therefore cannot be a row keyed on an `extra` marker. Predicted resolution: -encode orphan-ness in the **normalized kind** (shipped as `trait-impl-unresolved`), which is -adapter-local and forces no core change. If that works, it is evidence *for* -the claim: a language-specific escape shape absorbed by the adapter's own -normalization. - ---- - -## 3. Measured facts about rust-analyzer (survey, 1.95.0) - -From a direct LSP probe against a minimal cargo crate. - -| Observation | Value | -|---|---| -| `documentSymbol` answers | t+0.15s, syntactic, before any indexing | -| `cargo metadata` | t+1.96s → t+1.96s (workspace discovery from `rootUri`) | -| `experimental/serverStatus` `quiescent: true` | t+3.92s | -| Same, without the client capability | **never sent** | -| Visibility in symbol payloads | **absent** — no `pub` / `pub(crate)` anywhere | - -Symbol shapes (`name`, LSP `kind`, `detail`): - -``` -Greeter kind=11 (trait) - greet kind=6 detail='fn(&self) -> String' -Person kind=23 (struct) - name kind=8 detail='String' -impl Person kind=19 detail=None - new kind=12 detail='fn(name: String) -> Self' - secret kind=6 detail='fn(&self) -> u32' -impl Greeter for Person kind=19 detail=None <-- trait impl, readable from the name - greet kind=6 -Colour kind=10 (enum) - Red kind=22 (enum-member) -inner kind=2 (module) -LIMIT kind=14 (constant) -Alias kind=26 (type alias) -``` - -Two consequences for the adapter, both adapter-local: - -1. **Visibility must be sliced from source text.** LSP symbol kinds carry no - `pub(crate)` granularity, exactly as the session's survey item anticipated. - This is the same technique the C# adapter already uses - (`declaration_slice` + `declared_visibility`), so it is adapter knowledge, - not leakage. -2. **Trait impls are readable off the symbol name** — `impl Greeter for Person` - at kind 19. The boundary-forming trait-impl row needs no extra request. - Members of a trait arrive with `container_kind == 11`, the same LSP kind - the C# adapter already keys `interface-member` on. - ---- - ---- - -## 4. Classification table - -Every change outside `ddd-lsp/src/adapter/rust*.rs`, measured as -`git diff --numstat` from the pre-registration commit (`a8871da`) to the end -of the session. Adapter-internal lines are shown for scale but are not -classified — they are the thing being paid for. - -### 4.1 The new adapter module (not classified) - -| File | +/− | What | -|---|---|---| -| `adapter/rust.rs` | +262 | host wiring, the 15-row policy table, visibility ranking, posture warning | -| `adapter/rust_facts.rs` | +351 | visibility grading, signature slicing, impl-shape parsing | -| `adapter/rust_tests.rs` | +227 | facts unit tests | -| `adapter/rust_policy_tests.rs` | +246 | one trigger + one non-trigger per row | -| **total** | **+1086** | | - -### 4.2 Changes outside the adapter — classified - -| File | +/− | Class | What, and why that class | -|---|---|---|---| -| `adapter/mod.rs` | +65 / −5 | **leakage** (57) + **glue** (8) | `ReadySignal` + `no_extra_capabilities` + the two `Adapter` fields are leakage (L1, L2). `pub mod rust`, the `all()` entry, and the routing test are glue. | -| `host.rs` | +33 / −19 | **leakage** | Adapter-supplied capabilities merged into `initialize`; readiness evaluated through `ReadySignal` rather than a method-name set. | -| `client.rs` | +9 | **leakage** | `ServerState::last_params`, so a payload-discriminated readiness signal is readable at all. | -| `adapter/csharp.rs` | +3 / −2 | **leakage** | Consequential restatement of one field. No policy row touched. | -| `adapter/bicep.rs` | +3 / −2 | **leakage** | Same. | -| `state.rs` | +11 | **core bug** | `raw_str`: `opt_str` trims, and `new_text` is a whole file. | -| `intercept.rs` | +3 / −2 | **core bug** | `resolve_new_text` reads content through `raw_str`. | -| `rust-toolchain.toml` | +3 / −1 | **glue** | `rust-analyzer` as a pinned component. | -| `.github/workflows/product-ci.yml` | +5 | **glue** | The matching `components:` line. | -| `tests/fixtures/rust/*` | +126 | test fixture | The fixture crate. | -| `ddd-lsp/tests/rust_host.rs` | +196 | test | Lifecycle acceptance against the real host. | -| `ddd-mcp/tests/rust_governed.rs` | +425 | test | Row sweep + the interception loop. | - -### 4.3 The numbers - -| Class | Files | Lines added | -|---|---|---| -| **Leakage** | 5 | **114** | -| Genuine core bug | 2 | 14 | -| Registration glue | 3 | 16 | -| Test / fixture | 3 | 747 | -| *(adapter module — the cost being measured)* | *4* | *1086* | - -**Leakage count: 2 instances, 114 lines, all inside the LSP host layer.** - -### 4.4 Prediction scored - -| Predicted | Outcome | -|---|---| -| L1 — fixed client capabilities → forced | **Correct.** | -| L2 — readiness as a bare method name → forced | **Correct.** | -| `build_inputs` `.sln`/`.csproj` → latent, no change | **Correct** — Rust sets `needs_open_handshake: false`. | -| `"Document is null"` retry → latent, no change | **Correct.** | -| `AdapterEntry::internal_is_surface` → reused, no change | **Correct.** `pub(crate)` rode the C# switch unmodified. | -| `mock/parse.rs` → no Rust dialect added | **Correct**, via `dec/ddd/rust-host-is-real`. | -| `surface.rs`, `classify.rs`, `protocol.rs`, `intercept.rs`, MCP layer, `config.rs` → clean | **Correct for all but `intercept.rs`**, which changed for a bug unrelated to Rust (§4.5). | -| Orphan impls need an `extra` matcher the table lacks → resolved adapter-locally via the kind | **Correct.** Encoded as `trait-impl-unresolved`; no core change. | - -Nothing unpredicted leaked. The one file outside the prediction that moved -(`intercept.rs`) moved for a defect that predates Rust. - -### 4.5 The core bug, stated plainly - -`ddd_apply_edit` had been deleting the trailing newline of every file it -wrote, in every language, since M4. `opt_str` trims and treats blank as -absent — right for names and ids, wrong for a whole file — and -`resolve_new_text` read `new_text` through it. Six files in this workspace -were observed mangled during the governed tail before the cause was found; a -control file edited outside the interceptor kept its newline. Filed as -`DDD-arch-07`, fixed by `state.rs::raw_str`, declared as `seam/mcp/raw-str`. - -This is the category the pre-registration set aside as *not* counting against -the adapter-cost claim, and it does not: it is wrong for C# and Bicep in -exactly the same way. What Rust supplied was the first author who read the -bytes back. - ---- - -## 5. Governed-tail friction reading - -The `code` artifact class was switched to `enforce` after fixture acceptance -passed, and the rest of the session ran governed: eight `ddd_apply_edit` -calls against this repo's own Rust sources, producing **8 correspondence -rows** and **4 seam declarations** — the first entries `.ddd/seams/` has ever -held. - -| What happened | Count | -|---|---| -| Contract-surface edits rejected, then declared, then applied | 4 events / 3 edits | -| Non-surface edits applied untouched | 6 | -| Seam declarations authored | 4 | -| Declarations filed with empty `verdict_knowledge` (the rubber-stamp signal) | 0 | - -### 5.1 Where the friction actually was - -**It was not in authoring the declarations.** The rejection payload arrived -with the facts pre-filled — symbol, kind, signature, visibility grade, -reference count, the rule and its claim — and with a template whose -`verdict_knowledge` was blank. Nothing had to be re-derived; the only work -was the sentence about what the boundary lets the other side learn, which is -the work the mechanism exists to force. `DDD-friction-01` predicted exactly -this, and nothing in the session contradicts it. Rubber-stamping did not -occur, but four declarations is far too small a sample to say it will not. - -**It was in the first call of every session.** `ddd serve` starts hosts -lazily, so the first `ddd_apply_edit` of a fresh session returns -`{"status": "loading"}` rather than an outcome — honest, and correct -behaviour, but it means every governed session begins with a discarded call -and a ~14s warmup on this workspace. An agent that treated `loading` as an -error rather than as "retry" would read the surface as broken. This is the -concrete form of PRD §11's solution-load risk, and it is real for Rust too. - -**It was in the tooling around the surface, not the surface.** `ddd serve` -speaks stdio only, and same-session declaration matching requires the -declare and the re-apply to be one process. Driving governed edits from a -shell therefore needed a batch driver holding one session open. An agent -with the tools connected has no such problem; a human at a terminal has no -in-surface path at all, which is the shape `DDD-friction-02` describes. - -### 5.2 The reading is optimistic, by a known and now-measured amount - -Per §1.1, `dec/ddd/enforce-matching-tightens-to-symbol` was left -unimplemented so its own falsifier could be observed. It fired on the first -governed edit. - -That edit added two `pub` items to one file. Two declarations were authored, -one per symbol. Both surface events linked to the **first** declaration, -because `match_declarations` still admits the file arm and returns the first -hit: - -``` -seam-event/3 symbol: DEFAULT_COMMAND linked_declaration: seam/rust/default-command -seam-event/4 symbol: host_command linked_declaration: seam/rust/default-command -``` - -`seam-event/4` is verbatim the row the decision pre-registered as its -acceptance test: *"a row whose `linked_declaration` names a symbol other than -its own should not occur in enforce mode."* - -The consequence is worse than over-admission. `link_seam_metadata` writes the -matched event's LSP-derived facts onto the matched declaration, so -`seam/rust/default-command` — a declaration about a `const` — now carries -`symbol: host_command`, `kind: fn`, `signature: fn host_command() -> Vec`. -A machine-authored structural field on a correspondence entry holds another -symbol's facts, which is precisely what `DDD-arch-04` names as the thing that -makes the dataset unfalsifiable. `seam/rust/host-command`, meanwhile, was -never matched and so carries no LSP facts at all. - -The corrupted entry is **left in place** with a `notes` field recording why; -correcting it now would delete the observation. It should be corrected on the -same commit that lands the enforce-matching change. - -Read against that: had the file arm not been there, the two edits would have -demanded and linked per symbol, and the friction count would be unchanged — -the arm cost nothing in *effort* here and cost the dataset its integrity. - -### 5.3 The larger limit: routing to the interceptor is voluntary - -§5.2 is the smaller caveat. This is the bigger one, and it qualifies the -words "self-governance" everywhere they appear above. - -Interception binds edits that arrive through `ddd_apply_edit`. Nothing binds -an edit to arrive that way. A contract-surface change made with an ordinary -editor — `pub fn foo()` typed into a file — is never classified, never -demanded, and never logged. There is no after-the-fact check either: neither -`ddd diff` nor `ddd report escapes` computes contract-surface coverage, so an -undeclared `pub` item introduced outside the tool produces no finding at any -later point. - -Every one of the eight correspondence rows in §5 is therefore an edit that -was **volunteered**. The switch to `enforce` changed what happened to edits -already routed through the tool; it did not change what fraction of edits -were routed. M6 demonstrates that the interceptor works when invoked. It does -not demonstrate that the interceptor is in the loop. - -This matters against PRD §2, which is where the tool's whole justification -sits: *"prompt rules are exhortation an agent can drift past. A tool in the -edit loop is a policy-level commitment — the check is part of the -arrangement, not the agent's residual discretion."* If routing to the tool is -itself discretionary, then so is the commitment, and the difference between -this arrangement and a prompt rule is smaller than §2 claims. Nothing in M6 -tests that difference. - -`DDD-arch-06` does not cover this. It conditions the dataset on the **surface -stratum** — surface versus non-surface, and interception mode — and states -that the base rate of surface-touching edits is unrecoverable. The **routing -stratum** is a further and larger conditioning it does not mention: what -fraction of edits reached the classifier at all. On this repository that -fraction is unknown and, with the current instrumentation, unknowable. - -The What surface has already met this problem and answered it: `product -domain new` on the CLI is not intercepted, and `ddd what --strict` is the -gate that catches what the write path misses. Code has no equivalent gate. -Supplying one — a coverage check that reads the declared seams against the -`pub` surface actually present in the tree — is the obvious shape, and is -filed here as an observation rather than a claim, because it is a decision -for the principal and not a finding M6 established. - ---- - -## 6. Proposed status changes — for acceptance, not applied - -Nothing below has been applied to the graph. - -### 6.1 `DDD-adapter-01` — `projected` → **`reported`**, unchanged in wording - -> Per-language contract-surface definitions expressed as adapter policy tables -> are falsifiable against where boundary defects actually occur; wrong rows -> get fixed in the adapter, never in the core. - -Its falsifier is *"a boundary defect class that no policy-table row could have -named — i.e. contract-surface knowledge that cannot be localised to a language -adapter."* M6 looked for one and did not find it. Every Rust contract-surface -assumption localised: graded visibility (`pub(crate)`/`pub(super)`/`pub(in …)`), -container capping, trait definitions and members, trait impls as boundary -participation with no keyword to key on, derive changes as trait-impl -authorship, and the orphan-impl escape — which the table could not express as -a row matcher and which the adapter absorbed into its own kind normalisation -instead, with no core change. The surface vocabulary in `ddd-core/src/surface.rs` -was not touched. - -One corroboration worth naming: `DDD-adapter-03` reports that the C# table -drops `enum-member` before any row is consulted. The Rust table lists -`enum-member` in `DECL_KINDS` and demands a variant addition on a `pub` enum. -The gap really was adapter-local — a second adapter did not inherit it. - -**Recommendation: promote to `reported`.** Evidence text is drafted and can be -filed on acceptance. - -### 6.2 `DDD-adapter-04` — filed as **`reported`** (new) - -The host-lifecycle half that `DDD-adapter-01` does not cover. This is where -the two leakage instances land, and it is the honest answer to PRD §11's -broader phrasing: *"new languages require only an adapter + policy table by -design"* is **true of contract surface and false of host lifecycle**, and the -mitigation as written does not distinguish them. - -**Recommendation: accept as filed, and amend the PRD §11 risk row to say -"adapter + policy table + any host-lifecycle shape the LSP layer cannot yet -express."** - -### 6.3 `DDD-arch-05` — `projected` → **`reported`** - -Its exposure was hypothetical until this session. It now has a row id -(`seam-event/4`), a corrupted declaration to point at, and a mechanism — -`link_seam_metadata` propagating the mismatch into machine-authored fields — -that the claim's current wording does not mention. - -**Recommendation: promote to `reported` and extend the statement to cover the -metadata-corruption consequence.** - -### 6.4 `DDD-arch-02` — `projected`, **unchanged** - -*"The LSP protocol carries enough information … to classify contract-surface -edits for C# and Bicep without direct Roslyn or Bicep API access."* Rust now -supports the same conclusion — every fact came from `documentSymbol` plus -source slicing, no rustc and no `syn` — but the claim names two languages and -extending it is a rewording, not a status change. - -**Recommendation: leave alone, or reword to name the protocol rather than the -language list. Principal's call.** - -### 6.5 `DDD-arch-07` — filed as **`reported`** (new) - -The shared-accessor defect. Filed, fixed, and declared. No promotion needed; -listed so it is not mistaken for something the adapter caused. - ---- - -## 7. What M6 leaves standing - -- **`dec/ddd/enforce-matching-tightens-to-symbol` is not implemented.** - Deliberate (§1.1), and the session produced its evidence instead. It should - land next, together with the correction of - `.ddd/seams/seam-rust-default-command.yaml`'s metadata block. -- **`dec/ddd/enum-member-gap-priced` is not implemented.** The M5 report - assigns it to M6, but it is a C#-adapter policy change, which this session's - constraints exclude. The Rust table demonstrates the fix shape. -- **The M6 and M7 milestone rows are not in the PRD.** §10 ends at M5, and the - flip condition this session was to evaluate is filed nowhere — see - `dec/ddd/m6-proceeds-no-flip`. Both need the principal. -- **`ddd render` (M2.5) shipped** — `ddd-core/src/render.rs`, the CLI - subcommand, and `ddd-cli/tests/render.rs` are all present. Untouched here. -- **PRD §9.3 is unwritten.** The Rust policy table is documented in its own - claim strings and in this report; the committed PRD is an older revision - than the working draft (M5 report §6), so it was not edited from here. diff --git a/docs/ddd-m7-experiment.md b/docs/ddd-m7-experiment.md deleted file mode 100644 index 8216138c..00000000 --- a/docs/ddd-m7-experiment.md +++ /dev/null @@ -1,493 +0,0 @@ -# M7 — the HTML+CSS pair adapter as a seam-event-abstraction experiment - -**Status:** prediction filed before any adapter code was written; §6 onward -added after implementation. -**Date filed:** 2026-08-10 -**Claim under test:** `DDD-adapter-01` (status `reported`) — this is language -four, the falsification attempt its evidence text scheduled. `DDD-adapter-04` -is *not* under test: its own falsifier (b) scopes a non-LSP artifact class -out, and choosing adapter-local parsing (§2) means falsifier (a) — a fourth -LSP host with near-zero wiring — is not exercised either. - ---- - -## 1. What is being measured - -M7 adds a fourth language — the HTML+CSS **pair**, governed as one unit at -its seam. The measurement is the same as M6's: **how much of the core had to -move to admit it**. Every change outside `ddd-lsp/src/adapter/htmlcss*.rs` -is classified into exactly one of: - -| Class | Meaning | Counts against DDD-adapter-01? | -|---|---|---| -| **registration glue** | wiring the adapter into routing/config | no — expected and bounded | -| **genuine core bug** | wrong for *all* languages, fixed here | no — filed separately | -| **contract-surface leakage** | the core's *surface vocabulary or classification* had to change to express tokens, class vocabulary, layers, or pair-ness | **yes** — this is the falsifier firing | -| **event-source leakage** | the core assumed the before/after facts arrive from an LSP host serving one document, and had to change to admit a hostless pair producer | no — outside DDD-adapter-01 (its falsifier is scoped to contract-surface knowledge), filed as its own claim in the adapter-04 family | - -The fourth class is what M7 stresses that M6 did not. Rust changed the -answer to "how does a host announce readiness"; HTML+CSS changes the answer -to "what produces the events at all". The pair's contract surface — tokens, -class vocabulary, `@layer` structure — is not symbol events from a language -server, so this session tests whether the core's seam-event abstraction -(`surface.rs` vocabulary, `classify.rs` diffing, `seam_event.rs` rows, the -PRD §8 outcome semantics) admits a non-LSP producer without the *surface* -half moving. - -Reading the result onto the claim: zero contract-surface leakage at its own -falsifier **strengthens** `DDD-adapter-01`; any contract-surface leakage -**weakens** it. Either way §7 proposes the status consequence to the -principal and applies nothing. - -### 1.1 One scope note on the manifest path - -`ddd-core/src/detect.rs` and its per-tool siblings (`editorconfig.rs`, -`bicepconfig.rs`, `cargolints.rs`) are the M2 detection machinery, which the -M6 report explicitly placed outside the adapter contract ("the namespace -routing table below is the whole per-language adapter for M2"; -`cargolints.rs` named as Rust knowledge that belongs to the manifest/diff -path). The stylelint / html-validate / token-file ingestion M7 adds there is -the same per-tool invocation-adapter category — counted and reported, but -under **manifest wiring**, not under the adapter-cost classes above. - ---- - -## 2. The event source — priced, and adapter-local parsing chosen - -The candidate LSP hosts are the vscode-extracted pair: -`vscode-css-language-server` and `vscode-html-language-server` -(npm `vscode-langservers-extracted`). Priced under the DDD-adapter-04 -budget, rust-analyzer's 114 lines being the reference point: - -- **Two hosts for one governed unit.** The pair is one artifact class; the - interceptor would have to start, ready-check, and overlay against *two* - child processes per edit pair, and the `Adapter` abstraction binds one - host per language — the pair would leak its two-ness into the host layer - immediately. -- **Two lifecycle budgets.** Each server carries its own wiring line — - capabilities, readiness shape, workspace behaviour. Both are - ready-after-initialize (cheaper than rust-analyzer), call it 30–60 lines - each — but it is 2× per-server cost plus a **Node.js runtime as a - prerequisite of every governed repo**, for a Rust CI gate. -- **They do not produce the facts the table needs.** The CSS server's - `documentSymbol` is a selector outline; the HTML server's is an element - outline. Custom-property *typing* (dimension ↔ color ↔ keyword), class - *vocabulary* (defined vs consumed), `@layer` membership and order, and - every cross-file pair fact would still be sliced from source text in the - adapter — the same technique the C# and Rust adapters already use for - visibility. The hosts would be paid for and then bypassed. - -**Decision: adapter-local parsing, no LSP host.** The Rust precedent applies -verbatim: parsing within the adapter is adapter knowledge, not leakage. This -also keeps the experiment honest — wiring two servers that produce nothing -the table consumes would dodge the very question (does the core admit a -non-LSP producer?) that M7 exists to answer. - ---- - -## 3. Existing-tooling survey for the class-contract check (PRD §10 requires it) - -The load-bearing checker: classes referenced in HTML ⊆ classes defined in -owned CSS ("orphan classes"); selectors in owned CSS ⊆ structures present in -the paired HTML ("dead selectors"). - -- **stylelint** core rules: syntax, complexity, and value discipline over - CSS alone — no HTML awareness, so neither direction is expressible. - Its value here is the *closure stack* (§5): rule-id-keyed findings that - join the M2 manifest machinery, and token discipline - (`declaration-property-value-disallowed-list` banning raw values where a - token exists). -- **stylelint-no-unused-selectors** (plugin): unmaintained, JSX-oriented, - one direction only, Node-resident — a CI gate dependency this workspace - deliberately avoids (`dec/ddd/fixtures-not-sdk` posture). -- **PurgeCSS / UnCSS**: dead-CSS *removal* tools, not checkers; one - direction; no rule-id join, no governance join; Node-resident. -- **html-validate**: markup validity with machine-readable output — joins - the closure stack for the HTML side — but has no cross-file CSS - awareness. - -**Residue to build:** the bidirectional pair check, in the adapter, with the -two finding classes joining `ddd report escapes`. Matching is -necessary-condition decomposition: compound selectors split into simple -parts (class / element / id), pseudo-classes and pseudo-elements stripped, -and any selector containing constructs the checker does not model (attribute -selectors, combinators beyond descent) is checked on its modelled parts and -counted in an explicit coverage line (`dec/ddd/report-coverage-explicit`). -Thresholds — decorative one-off class globs, the pair map itself — are -config, not code (hard constraint 2). - ---- - -## 4. Prediction (filed before any code was written) - -### 4.1 Predicted event-source leakage — change forced - -**E1 · the serve layer assumes every adapter has a spawnable host.** -`Adapter` carries `default_command`, `ready`, `language_id`, -`extra_capabilities`, `needs_open_handshake` — all host fields — and -`apply_edit` routes unconditionally through `with_ready_host`, which spawns -the child on first use (`manager.host()`); `warm_all` starts every -registered adapter's host. A hostless adapter needs: a way to say it has no -host, a classification path in `intercept.rs` that does not open/overlay a -host document (empty `RawSymbol` slices — the facts fn parses text), no -reference-count requests (rows carry `None`), and guards in -`with_ready_host`/`warm_all`. Estimate: 30–60 lines across -`adapter/mod.rs`, `ddd-mcp/src/intercept.rs`, `ddd-mcp/src/lang_tools.rs`, -`ddd-lsp/src/manager.rs`. - -**E2 · facts are assumed derivable from one document alone.** -`facts: fn(&str, &[RawSymbol], &AdapterFlags)` has no document identity and -no repo context, so pair-level facts — counterpart file(s), direction -(definition side vs consumption side) — cannot be produced where facts are -made. Predicted change: one optional enrichment hook on `Adapter` -(`fn(root, file, config, &mut events)`), called by `intercept.rs` after -classification, populating `SymbolFacts.extra` so the seam-event rows carry -the pair facts. Estimate: 10–20 lines outside the adapter. - -E1 and E2 are the event-source class: they say nothing about *what is -contract surface* in HTML+CSS — that knowledge is predicted to localise -completely (§4.3). If they land as predicted they are filed as a new claim -(`DDD-adapter-05`): the M6 host-lifecycle finding generalised — the -language-neutral layer had one producer shape (an LSP host per language, -one document per edit), and each producer shape outside it is a budgeted -core cost. - -### 4.2 Predicted latent — no change forced - -- `facts`'s `&[RawSymbol]` parameter is dead weight for a parsing adapter — - LSP-shaped, costs nothing (empty slice in, ignored). -- `SeamEvent.reference_count` is `Option` already; hostless rows carry - `None`. -- `SeamEvent.language` will carry `htmlcss` for both files of the pair; the - pair identity rides `extra`, not a schema change. -- `mock/parse.rs` dispatches to a C# parser for unknown extensions — never - reached (the pair adapter's tests need no mock host at all). -- Config `adapter.` entries: `command` is meaningless for a - hostless adapter (ignored); `internal_is_surface` unused (the pair grades - by layer, not by a crate-internal analogue); `exported_attributes` unused. - -### 4.3 Predicted clean — the DDD-adapter-01 test proper - -- **`ddd-core/src/surface.rs`** — unchanged. `SymbolFacts` free-string - `kind`/`visibility`/`signature` plus the `extra` map are predicted to - express every pair fact: tokens as `kind: token` with `signature` = the - value's *type* (`color` | `dimension` | `keyword` | `other`, so a retype - is `SignatureChanged` and a same-type value change produces **no event** - — that silence is the point of tokens); class vocabulary as - `kind: class` with membership = `Added`/`Removed`; `@layer` order as its - own single symbol whose `signature` is the layer list; stylesheet links - (``) as `kind: stylesheet-link` — rewiring the pair - is itself boundary-forming. `@layer` membership grades visibility: - `unlayered` outranks `layer:`, via the adapter's own - `visibility_rank`, exactly the mechanism C#/Rust visibility uses. -- **`ddd-lsp/src/classify.rs`** — unchanged; pairing by - (container, kind, name) and change naming carry over. -- **`ddd-core/src/seam_event.rs`** — unchanged; pair facts ride `extra`. -- **`ddd-mcp/src/govern_tools.rs`**, `seam.rs`, `why.rs`, `store.rs`, - `render.rs` — unchanged. - -Known risk, named in advance (the M6 orphan-impl shape): `PolicyRow` has no -matcher over `extra`, so "only in owned CSS" cannot be a row predicate. The -predicted resolution is adapter-local, again: ownership and decorative -exemptions are resolved in the adapter's facts/enrichment (non-surface -symbols simply not lifted), never in `surface.rs`. If instead `surface.rs` -must gain an extra-matcher, **that is contract-surface leakage and weakens -the claim** — the pre-registered failure mode. - -### 4.4 Predicted registration glue - -- `adapter/mod.rs`: `pub mod htmlcss;`, the `all()` entry, the routing test, - and the artifact-class test gaining `html-css`. -- The artifact-class vocabulary: `html-css` joins `predicate-format.md`'s - enumeration (PRD §13 already admits HTML+CSS as a governed artifact - class) and `.ddd/config.yaml`'s `intercept_by_class`. -- Config format 4 (`ddd-core/src/config.rs` + migration note): the `pair` - section (units, decorative-class globs) and `detect` gaining - `stylelint` / `htmlvalidate` / `tokens` input lists. The field names are - html/css-flavoured naming in a core schema — the same latent *naming* - category `AdapterEntry::internal_is_surface` was for C# at M6: routing - data, no language semantics. -- Manifest wiring (§1.1): stylelint/html-validate config + output parsers, - token-file parser, `namespace_for` driver routing, CLI `report escapes` - merge of the pair-check findings. - -### 4.5 The policy table, pre-registered - -| row | change | kind | surface | claim (short form) | -|---|---|---|---|---| -| `web-token-membership` | added/removed | `token` | yes | custom properties are the stylesheet's params; membership is contract | -| `web-token-retype` | signature-changed | `token` | yes | dimension ↔ color ↔ keyword retype is a contract change; same-type value change is no event at all | -| `web-class-membership` | added/removed | `class` | yes | the class vocabulary is consumed by HTML, defined by owned CSS | -| `web-layer-order` | any | `layer-order` | yes | `@layer` order is the visibility grading of the whole sheet | -| `web-stylesheet-link` | added/removed | `stylesheet-link` | yes | which CSS an HTML document consumes is the pair boundary itself | - -Non-surface by default, resolved in the adapter (not rows): inline -` - - -

DDD governance graph

-

A static projection of .ddd/ — regenerated, never edited. Cadence judged against 2026-08-10.

-

Claims (4)

- - - - - -
idstatusstatementfalsifierchangedrevalidate by
DDD-ex-1projecteda worked-example claim at the projected runga counterexample2026-08-012027-01-01
DDD-ex-2reporteda worked-example claim at the reported runga counterexample2026-08-012027-01-01
DDD-ex-3establisheda worked-example claim at the established runga counterexample2026-08-012027-01-01
DDD-ex-4retireda worked-example claim at the retired runga counterexample2026-08-012026-01-01
-

Decisions (1)

-

dec/example/adopt — Adopt the worked example

-

decision · principal Emil

-

the sample graph shows every render state once

-
    -
  • basedOn DDD-ex-3 — pin holds (established@2026-08-01)
  • -
  • basedOn DDD-ex-2 — basis moved: pinned projected@2026-07-01, now reported@2026-08-01
  • -
-
-

Manifest coverage (2 rules)

-

analyzers

- - - -
ruleseveritygovernancesuppression
CA2007warningdec/example/adopt—
CA1848warningUNGOVERNED—
-

Escapes dashboard

-

Basis loss (1)

- - -
decisionclaimpinnedcurrent
dec/example/adoptDDD-ex-2projected@2026-07-01reported@2026-08-01
-

note: namespace analyzers: no detection source seen — staleness not evaluated (provide SARIF via detect.sarif or --sarif)

-

Seam map (1 declarations, 1 interception rows)

-

seam/example/api — method Ping in Api.cs

callers learn whether the service answers

contract: Api.cs#Ping

- - -
rowfilesymbolchangeoutcomerefslinked
seam-event/1Api.csPingaddedapplied-linked2seam/example/api
- - diff --git a/docs/examples/product-cli-blueprint.md b/docs/examples/product-cli-blueprint.md deleted file mode 100644 index bc48f80c..00000000 --- a/docs/examples/product-cli-blueprint.md +++ /dev/null @@ -1,61 +0,0 @@ -# Worked example: product-cli describes and verifies its own How - -This is a dogfood of the Product-Framework toolchain (FT-109–FT-117) on -`product-cli` itself: the repo's *What* graph, an *blueprint* (How + layout + -cells) describing how `product-cli` is actually built, and a dispatch that -produces frozen SPMC work units — every layer validated by the same tools. - -The blueprint lives at [`.product/blueprints/product-cli/`](../../.product/blueprints/product-cli/). - -## The four-layer verified chain - -```console -$ product domain validate # What — §3 (Described) -conformant — 152 node(s), 0 violations - -$ product blueprint validate product-cli # How + layout + cells — §4/§5 (Realised) -conformant — blueprint 'product-cli': how present, layout present, 1 cell(s) [domain: cross-checked] - -$ product cell dispatch \ - --file .product/blueprints/product-cli/cells/implement-slice.yaml \ - --bind concept=e-entity --bind operations=validate,show \ - --out .product/blueprints/product-cli/work-units -Dispatched core-slice-e-entity -> …/core-slice-e-entity.yaml -Dispatched cli-adapter-e-entity -> …/cli-adapter-e-entity.yaml -Dispatched integration-tests-e-entity -> …/integration-tests-e-entity.yaml - -$ product work-unit validate --file …/integration-tests-e-entity.yaml -conformant — work unit 'integration-tests-e-entity' produces … [domain: cross-checked] - -$ product conformance check # the FT/ADR/TC graph — Two Pillars -Verdict: conforms to Level 3 (spec-driven) — checkable subset -``` - -## What it captures - -The blueprint encodes `product-cli`'s real architecture as a conformant How: - -- **Top decisions** — *slice + adapter* (pure `pf::` domain slices in - product-core, thin CLI adapters in product-cli) and *native checkers* - (conformance checkers mirror the vendored SHACL/JSON schemas, cross-checked - against `pyshacl`). -- **Principles** — `slice-cohesion`, `pure-core`, `conformance-fidelity`, - `zero-unwrap`; each enforced by a named verification. -- **Patterns** — `slice-plus-adapter` (applied by the `implement-slice` cell), - `native-checker`. -- **Contracts** — an application contract (Rust, `core`→`cli` layering, - statements: no `unwrap()`, files under 400 lines) and an infrastructure - contract (cargo-dist release), with the layout model placing slices in - `product-core/src/pf/**` and adapters in `product-cli/src/commands/**`. - -## The realisation step - -`implement-slice` is a task type whose `concept` slot binds to a real entity in -the What graph. Dispatch resolves the cell's `domain:concept` input to the -bound entity (`domain:e-entity`), freezes and content-hashes the context, and -emits one SPMC work unit per cell (core slice → CLI adapter → integration -tests). Binding to a value that is **not** an entity in the What graph is -rejected — a cell can only ever be realised against concepts that exist. - -> The dispatched work units under `work-units/` are generated output -> (gitignored); regenerate them with the `cell dispatch` command above. diff --git a/docs/g-track/decisions/g-dec-01-reading-vocabulary.yaml b/docs/g-track/decisions/g-dec-01-reading-vocabulary.yaml deleted file mode 100644 index f41f9378..00000000 --- a/docs/g-track/decisions/g-dec-01-reading-vocabulary.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# G-track register — track decisions, not canon. First entry. -# -# This register holds decisions scoped to the G-track (docs/g-track/). It is not -# actor-indexed-determination canon, not decision-driven-design canon, and not the -# product-cli program ledger (.decisions/). Entries here follow the canon decision -# file's shape loosely; nothing validates them yet. - -format: g-track-decision/v1 -id: g-dec-01 -title: The Reading tuple's unfiled vocabulary is track vocabulary, pending the Q25/Q27/Q30 wave -status: ruled -made: 2026-08-17 -by: Emil (ruled at G-1 Gate 1; recorded by the session) -decision: | - The G-track owns, as track vocabulary with no canon claim behind it: - - 1. The provenance value set on Readings — controlled / observed / inferred / institutional - (PRD §4.1). The G-track SHACL shapes define and enforce it, including the rule that - `institutional` requires a `trust_decision` reference. - 2. The Reading three-tuple's other unfiled halves — the per-reading as-of and the - assurance-at-which-read field (the ground-axes note's Q11 as amended at its revision 13). - - Neither files upstream from the G-1 session. Canon changes get their own gated sessions, and - the owning session is the queued Q25/Q27/Q30 filing wave, where Q27's trust-decision mechanism - is the backing institutional provenance needs. -region: | - The G-track only: its SHACL shapes, its Reading and Act records, its registry repository. - Applicability predicate (DDD-ground-01 gate): artefact-path under docs/g-track/ or the - registry repository's shapes — axis artefact-path, mechanically-evaluable. -basis: | - - The G-1 reconciliation finding: the four-value provenance typing exists in no live canon - at v5.5.0 (upstream 110bf10, downstream 4848b9e); the ground-axes holding note presupposes - it (Q12, Q27) without canon ever having filed it. - - Emil's Gate 1 ruling of 2026-08-17, verbatim in the session transcript, summarised in - docs/g-track/g1-session-report-2026-08-17.md. -source: | - The ground-axes holding note's Q12/Q27 usage (unratified), and its Q11 amendment at - revision 13 for the three-tuple. wasAttributedTo: the G-track. -revisit_if: | - The Q25/Q27/Q30 filing wave files provenance typing (or any part of the Reading tuple) as - canon. This decision is superseded the moment that happens, and the track re-pins to the - canon claims by the G-1 session's own re-pin discipline: re-derive at a named commit, never - hand-patch. -forwarded: | - This finding is forwarded to the Q25/Q27/Q30 filing wave as evidence: the wave's Q27 filing - has a named consumer waiting on it — the G-track Reading tuple. A customer strengthens the - filing; it does not rush it. diff --git a/docs/g-track/decisions/g-dec-02-projection-reasoner.yaml b/docs/g-track/decisions/g-dec-02-projection-reasoner.yaml deleted file mode 100644 index 4f20be0e..00000000 --- a/docs/g-track/decisions/g-dec-02-projection-reasoner.yaml +++ /dev/null @@ -1,25 +0,0 @@ -format: g-track-decision/v1 -id: g-dec-02 -title: Projection-build inference is CONSTRUCT-to-fixpoint on Oxigraph, reasonable as fallback -status: ruled -made: 2026-08-17 -by: Emil (ruled at G-1 Gate 2; recorded by the session) -decision: | - Inference at projection build (PRD §3 serving row) runs as SPARQL CONSTRUCT rules iterated to - fixpoint on Oxigraph — the primary mechanism. It reuses the pf::sparql_rules shape already in - the product-cli codebase and adds no dependency. The named fallback is the Rust `reasonable` - crate (OWL 2 RL materialiser, 0.4.4 at ruling time, BSD-3), whose supported rule subset must be - checked against the §5.2 entailments before any flip. -region: | - The G-track projection-build pipeline only. Axis artefact-path (mechanically-evaluable): - the projection-build script and its rule set. -basis: | - - G-1 verification V1/V9 (session report §2.2, dated 2026-08-17): Oxigraph healthy at 0.5.9; - OWL 2 RL translates mechanically to CONSTRUCT rules (SPIN OWL-RL prior art); no published - fixpoint benchmark exists for Oxigraph — the no-benchmark hedge is discharged by measuring - at G0, not by choosing conservatively now (Emil's Gate 2 ruling). - - Known caveat carried: Oxigraph CONSTRUCT blank-node issue (#220). -revisit_if: | - Fixpoint wall-time exceeds an acceptable projection-build budget at G0 scale. On that reading, - this decision flips to the named fallback (`reasonable`), and the flip is recorded as a new - version of this decision, not an edit. diff --git a/docs/g-track/g0-entry-probes-v3-v5-2026-08-17.md b/docs/g-track/g0-entry-probes-v3-v5-2026-08-17.md deleted file mode 100644 index 7f129b54..00000000 --- a/docs/g-track/g0-entry-probes-v3-v5-2026-08-17.md +++ /dev/null @@ -1,616 +0,0 @@ -# G0 entry probes — V3(a) and V5 - -**Session type:** verification. Reports, does not rule. -**Date:** 2026-08-17. **All findings pinned to the refs below.** - -> **Status update — Emil ruled on this report the same day.** V5 accepted as wiring-only. Kotlin -> ruled **conditionally in**, pending one narrow follow-up: the kotlin-lsp workspace-import probe. -> The proposed PRD edits below were ruled in **and have since been applied** to -> `docs/g-track/prd-ground-as-ontology.md` — read them here as the rationale, not as pending -> work. Two edits were added on the ruling: the **capability-flag discipline** generalised -> extractor-wide (§5.2), and the **foreign-key row marked not-derivable** from the declaration -> subset rather than left as an empty result (§5.2). The three-project-subset caveat was carried -> into §5.2 as an explicit provisionality note, and the discharged Oxigraph sentence (§3, §11, -> §12 item 8) was updated in the same pass. - -## Findings at a glance - -| Check | Status | One-line result | -|---|---|---| -| **V3(a).1** Gradle ≥ 8.8 floor | **Discharged** | Gradle **8.14.3**, AGP 8.13.0, Kotlin 2.2.10 — clears comfortably | -| **V3(a).2** JDK compatibility | **Discharged** | Project targets JDK 17; kotlin-lsp ships its own **JBR 25.0.2** — no conflict, nothing to install | -| **V3(a).3** Project shape | **Discharged** | 25 modules + composite build, no KMP, 3 flavours × 2 build types = 6 variants | -| **V3(a).4** kotlin-lsp import run | **Partial — the one open item** | Server ran; **workspace import never started** (2 handshakes). But an independent Gradle control enumerated **all six variants cleanly** | -| **V3(a).5** Kotlin synthesis route | **Discharged, premise corrected** | typeHierarchy is **advertised but returns `null`**; `hover` returns the supertype clause, so synthesis is *cheaper* than assumed — but Kotlin's subClassOf edges are ~75% UI state | -| **V5.1** `definition` + `typeHierarchy` | **Discharged — closed** | Roslyn 5.11.0 answers **both**, both hierarchy directions, on production code. Wiring only | -| **V5.2** Six-operation coverage | **Discharged** | All six return usable results, with examples | -| **V5.3** §5.2 rows vs real C# | **Discharged** | Five of six rows fire; the **foreign-key half finds nothing** | - -**Net:** V5 is closed and closed favourably. V3(a) is three-and-a-half conditions of four; the -single open question is whether kotlin-lsp can be made to import a Gradle/AGP workspace, and -that question — not the Gradle floor, not typeHierarchy — is what now decides n = 2 versus -single-source. - -## Refs read - -| Repo | Branch | HEAD | Head commit date | Notes | -|---|---|---|---|---| -| `acme-app/android` | `develop` (default) | `67340f69b4b4fa3230a9371831c979c1c83175b5` | 2025-10-02 | Session branch is at the same commit; `git ls-remote --symref origin HEAD` confirms `develop` is the default branch and is at this SHA. The repo has genuinely not moved since Oct 2025 — not a stale clone. | -| `acme-app/backend` | `main` (default) | `83c9339a47f8f01dfc28fe138691559d40536e7f` | 2026-08-13 | Session branch at the same commit; `main` confirmed default and at this SHA. | -| `Hafeok/product-cli` | — | `d2bf37d0bdfabc3af32647cf55a1e9a9a0a40065` | 2026-08-17 | Merge of PR #44; PRD + C# adapter as merged (PR #43 + #44). | - -> **Corpus names are pseudonyms.** The two client repositories are private, and this repository -> is public, so their organisation, repository, namespace, package and domain-type names are -> replaced throughout with neutral placeholders under an `acme-app` / `Acme.App` / `com.acme` -> scheme. The substitution is **consistent and structure-preserving**: every count, ratio, -> version, path shape, declaration form and LSP result is the real measurement, and the commit -> SHAs are the real pins — only the identifying strings are stand-ins. Where a literal name was -> itself the evidence (the exact-name intersection list), the finding is characterised rather -> than enumerated, and the real names remain recoverable from the corpus at the pinned refs by -> anyone with access. - -Neither corpus repository was written to. Verified after all work: zero tracked-file -modifications, zero non-ignored untracked files, HEADs unchanged in both. Build by-products -(`obj/`, Gradle caches) were confined to already-gitignored paths or to the scratchpad. - -**Method note.** Both corpora were already present as local clones, so the cross-tier -`add_repo` refusal that blocked G-1 did not recur. Server-side facts about kotlin-lsp were -re-verified first-hand from the published distribution (checksum-verified) rather than -inherited from G-1's pin; `github.com` and `api.github.com` are denied by this session's -egress policy (403), but `raw.githubusercontent.com` and `download-cdn.jetbrains.com` are not. - ---- - -## Probe 1 — V3(a): can Kotlin be G0's second leg? - -### Results - -| # | Check | Finding | Verdict | -|---|---|---|---| -| 1 | Gradle version | **8.14.3** (`gradle/wrapper/gradle-wrapper.properties`, `distributionUrl=…gradle-8.14.3-bin.zip`) | **Clears the ≥ 8.8 floor** | -| 1 | AGP version | **8.13.0** (`gradle/libs.versions.toml`, `gradle-plugin = "8.13.0"`); Android tools 31.9.0 | Current | -| 1 | Kotlin / KSP | Kotlin **2.2.10**, KSP **2.2.10-2.0.2** | K2-era, matches kotlin-lsp's K2 base | -| 1 | SDK levels | compileSdk **36**, targetSdk **36**, minSdk **24** | — | -| 2 | Project JDK target | **17** everywhere — `JavaVersion.VERSION_17` + `JvmTarget.JVM_17` (`build-logic/src/main/java/com/acme/app/KotlinAndroid.kt:33,34,62`; `build-logic/build.gradle.kts:10,11,15`); Azure CI pins `jdkVersionOption: 1.17` | — | -| 2 | kotlin-lsp JDK 25 requirement | **Satisfied by the distribution itself** — the standalone archive bundles JetBrains Runtime **25.0.2** (`jbr/bin/java` → `JBR-25.0.2+10-432.48-nomod`). No JDK 25 needed on the host, and no conflict with the project's JDK 17 target: they are different JVMs (server runtime vs Gradle daemon/toolchain). | **No blocker** | -| 3 | Modules | **25 Gradle modules** (`settings.gradle.kts`) — `:app`, `:api`, `:designsystem`, `:util`, 10 × `:data:*`, 9 × `:core:*` — plus a **`build-logic` composite build** (`includeBuild`) supplying five convention plugins | Large but conventional | -| 3 | Variants | **Multi-variant**: 3 product flavours on one `environment` dimension (`production`, `staging`, `develop` — `AppFlavor.kt`) × 2 build types (`debug`, `release` — `AppBuildType.kt`) = **6 variants for `:app`** | Multi-variant, as expected | -| 3 | KMP | **None.** No `multiplatform` plugin, no `commonMain`; single-target Android/JVM throughout | Simplifies import | -| 3 | Generated sources | KSP, Room (`androidx-room 2.8.0`), protobuf (`:core:datastore-proto` registers per-variant source sets), Navigation safe-args | Import must tolerate generated roots | -| 3 | Other import risks | Config cache + parallel on; `RepositoriesMode.FAIL_ON_PROJECT_REPOS`; one **third-party vendor Maven repository** (unreachable from this session — connection failure, not a policy denial) supplying a single vendor SDK artifact; no `local.properties`, so an Android SDK must come from `ANDROID_HOME` | One private-repo dependency | -| 4 | Live `bin/intellij-server` run | **ATTEMPTED — see below** | — | -| 5 | Synthesis route | **Premise changed — see below** | — | - -### Item 4 — the live import run - -Unlike G-1, this session could obtain the toolchain: kotlin-lsp **v262.9593.0** standalone -Linux-x64 archive downloaded from `download-cdn.jetbrains.com`, **SHA-256 verified against the -published digest** (`2d99d8e198fbe4aa8f4481e37799724ce94803b4ea12a60b416040e3fcd7cc5e`), Android -SDK platform 36 + build-tools 36.0.0 installed, `ANDROID_HOME` set (no `local.properties` -written into the repo). `bin/intellij-server --stdio` was run against the Android repo at -`67340f69`. - -**Result: partial. The Gradle floor is cleared and the variant model demonstrably resolves — -but the kotlin-lsp workspace import did not start, so "variants import *through kotlin-lsp*" -is not demonstrated.** - -| Sub-check | Result | -|---|---| -| Server runs headless on Linux | **Yes.** `bin/intellij-server --stdio`, JBR 25.0.2 bundled; no host JDK 25 needed | -| `initialize` against the repo root | **Yes.** Server extensions loaded, incl. `WorkspaceImportLanguageServerExtension` | -| Advertised capabilities | documentSymbol, workspaceSymbol, references, definition, hover, implementation, typeDefinition, **and `typeHierarchyProvider: true`** | -| Per-file analysis | **Works.** `documentSymbol` returned the full nested tree for `BadgeIcon.kt` (class 5, `data object` 19, two `data class` 23, properties, constructors); `hover`, `definition`, `typeDefinition` all answered | -| **Gradle/AGP workspace import** | **Did not run.** `workspace/symbol("Session")` → `array[0]`; `workspace/symbol("BadgeIcon")` → `array[0]`; `references` returned only the in-file occurrence (`array[1]`); the server log contains **zero** occurrences of the string "gradle" | -| **Independent Gradle control** | **All six variants resolve.** `./gradlew :app:tasks --all` on JDK 21 with Android SDK 36 configured the whole project — `assembleDevelopDebug`, `assembleDevelopRelease`, `assembleProductionDebug`, `assembleProductionRelease`, `assembleStagingDebug`, `assembleStagingRelease` (plus AndroidTest/UnitTest variants) — **no failures**. The unreachable vendor repository does not block configuration, because variant collection does not resolve dependency graphs | - -**Two handshakes tried, neither started an import.** - -1. Plain LSP handshake (`initialize` + `initialized`, workspace folder = repo root): no import; - zero "gradle" in the server log. -2. Handshake with `initializationOptions: {importWorkspace: true, gradle: {enabled: true}}`, - `executeCommand` capability declared, followed by an explicit - `workspace/executeCommand{command: "exportWorkspace"}`. The command **exists but rejected the - call** — `-32602 "Expected 1 argument, got: 0"` — and its name indicates an *export* of the - workspace model, not an import trigger. After a further four-minute settle, - `workspace/symbol` still returned **0** for `Session`, `BadgeIcon` and - `InstallationId`, and the only occurrence of "gradle" in the server log was **my own - `initializationOptions` echoed back**. - -**What this separates.** The ≥ 8.8 floor is not the obstacle: Gradle is 8.14.3, and the -project's variant model configures cleanly and enumerates exactly the six expected variants -under Gradle itself. The obstacle is that **kotlin-lsp never began an import** in this -environment. This session did **not** establish *why*, and says so rather than guessing. -Unexplored candidates for a follow-up: the argument `exportWorkspace` expects; whatever -`initializationOptions` the "Kotlin by JetBrains" VS Code extension v0.0.6 actually sends (it -ships in the same release and would be the authoritative reference client); and whether the -headless server needs a project to be opened by an explicit non-standard notification. - -Reported as a partial discharge of §5.4 condition (a), which has two halves: - -- **"Gradle clears ≥ 8.8"** — **discharged.** 8.14.3, and variant resolution independently - confirmed under Gradle itself. -- **"one `bin/intellij-server` import run confirms variants resolve"** — **not discharged.** - Attempted, did not start. Not simulated. - -### Item 5 — the synthesis route - -**A capability trap, found live and worth naming.** kotlin-lsp v262.9593.0 **advertises -`typeHierarchyProvider: true`** in its `InitializeResult` — but `textDocument/prepareTypeHierarchy` -returned **`null`** on every anchor tried, including a nested sealed subclass -(`NoIcon : BadgeIcon()`) and a top-level data class (`SessionPrice`). -`textDocument/implementation` likewise returned `null`. - -This **confirms G-1's source reading at runtime** ("API scaffolding exists, no provider is -registered") against the pinned release, and adds a detail G-1 could not see from source: the -failure mode is a **silent `null`, not a `MethodNotFound` error**. An adapter that gates on the -advertised capability — the natural implementation — will conclude typeHierarchy is available -for Kotlin and then quietly extract **zero** `rdfs:subClassOf` edges. That is the worst -available failure mode for an extractor whose output is a proposal graph a human reviews: -absence of edges reads as "this codebase has no hierarchy", not as "the instrument did not -answer". **The Kotlin adapter must treat typeHierarchy as unavailable by probe result, never by -capability flag.** - -I also re-verified the release line independently: `Kotlin/kotlin-lsp` `RELEASES.md` fetched -2026-08-17 still has **v262.9593.0** at the top — unchanged since G-1 pinned it on 2026-07-27. -No typeHierarchy implementation has shipped in the interval. - -**The good news: the synthesis route is cheaper for Kotlin than for C#.** `hover` returns the -**server-rendered declaration header, including the supertype clause**: - -- `NoIcon` → ` ```kotlin\ndata object NoIcon : BadgeIcon()\n``` ` -- `SessionPrice` → ` ```kotlin\ndata class SessionPrice(itemId: String?, pricePerUnit: Double?, currency: String?, unit: String?)\n``` ` - -So the Kotlin `subClassOf` route need **not** slice raw file text at all — the supertype clause -arrives already normalised, per-symbol, from the server, and it works **without a workspace -import** (both results above came from the un-imported, per-file mode). The second hover also -delivers the **full property list with types and nullability**, which feeds §5.2's typed-property -row directly. This is a materially better route than the C# adapter's file-text slicing, and it -is available today. - -**First, what the C# adapter actually carries.** Read at `d2bf37d`: -`ddd-lsp/src/adapter/csharp.rs` + `csharp_facts.rs`. The pattern is LSP `documentSymbol` for -names/kinds/ranges, plus **declaration-text slicing** (`declaration_slice`, -`declared_visibility`, `cap_visibility`, `strip_visibility`, `attribute_lines`) to recover -normalised kind, container-capped effective visibility, body-free signature and attached -attributes. - -**The adapter carries no `rdfs:subClassOf` synthesis today.** A workspace-wide search for -`subClassOf` / `base_type` / `supertype` / `typeHierarchy` returns nothing relevant -(`rust_facts::inherits_container` concerns *visibility* inheritance, not type inheritance). The -adapter is a **contract-surface classifier**, not an ontology extractor. So what transfers to -Kotlin is the **technique**, not code. §5.2's phrase "the pattern the C# adapter already -carries" is accurate about the technique and should not be read as "an implementation exists to -port" — a later session sizing this work should budget for a new component in both languages. - -**Does the technique transfer to Kotlin as this codebase writes it?** Mechanically, largely -yes: Kotlin puts the supertype list on the declaration line just as C# does, and -`declaration_slice`'s cut at `{` / `;` / top-level `=` with paren-depth tracking is broadly -right for Kotlin class headers. Four cases degrade or break it: - -| Case | How this repo writes it | Transfers? | -|---|---|---| -| **Supertype-call syntax** | `data class TrailingIcon(@DrawableRes val res: Int) : BadgeIcon()` — the supertype is written as a *constructor call* | Yes, with work: the slicer must strip the supertype's own argument list, whose parens re-open depth after the `:`. New case, not a blocker | -| **Delegation** | `data class LocationId(val value: String) : Parcelable, Comparable by value, CharSequence by value` (`data/location/…/LocationId.kt`); also `class ImmutableSet(…) : Set by protectedSet` (`app/…/CollectionWrappers.kt:22`) | **Lossy, and it lands on domain types.** A slicer emits `LocationId subClassOf Comparable` and `subClassOf CharSequence` — true in Kotlin, but these are **implementation conveniences, not domain facts**. `by` forwards members rather than inheriting them. This is proposal-graph *noise* on exactly the types that matter most, and a reviewer must reject it triple by triple | -| **Nested sealed hierarchies** | `sealed class BadgeIcon { data object NoIcon : BadgeIcon(); data class TrailingIcon(…) : BadgeIcon() }` — the dominant idiom (50 sealed classes, 6 sealed interfaces) | **Easier than C#**, not harder: containment *and* the supertype clause both carry the edge. But a slicer keyed only on the clause will double-count, and `data object` is a *value* that nonetheless participates in the hierarchy | -| **`typealias`** | `typealias InstallationId = String` (`app/…/io/model/Models.kt:70`) | **Irrecoverably lossy.** No LSP operation and no text slice recovers "InstallationId is a distinct domain identity" — after resolution it *is* `String` | - -`@JvmInline value class`: **zero occurrences**, so Kotlin's identity types are ordinary data -classes rather than a zero-cost value-object construct. - -**Identity modelling — a structural agreement and a two-sided inconsistency.** Kotlin *does* -model identity as types: `data class LocationId(val value: String)`, `DeviceId`, `UnitId`, -`ConnectorId` (all in `data/location/…/model/`), each wrapping a single `String`. That is -structurally the **same idea** as C#'s `public readonly record struct LocationId(string Value)` -— and `LocationId` is one of the 23 exact-name matches. This is a genuine triangulation -candidate, not a divergence. - -The divergence is narrower and more interesting than "one side types identity and the other does -not": **both codebases apply the convention inconsistently, in different places.** On the C# -side only `LocationId` of four sibling identity types declares `: IStringIdentity`. On the -Kotlin side four identity types exist under `data/location`, while `InstallationId` is a bare -`typealias … = String` in `app/…/io/model/Models.kt`. So the extractor would surface a -**partially-applied identity convention on each side independently**, before any cross-codebase -comparison runs — two instances of the same escaped decision, in two languages. The `typealias` -case remains irrecoverable by any LSP route; the four real identity types are recoverable, but -only through `hover` (see above), since Kotlin `typeHierarchy` returns `null`. - -**Where Kotlin's subClassOf edges actually are — the finding that matters most.** Census over -965 non-test `.kt` files: - -| Measure | Count | -|---|---| -| `data class` / `data object` declarations | **606** | -| …carrying a supertype | **201** | -| …bare, no supertype | **405** | -| `class … : Supertype` declarations | 123 | -| sealed class / sealed interface / interface / enum class / abstract class | 50 / 6 / 56 / 94 / 13 | -| `typealias` | 2 | -| `@JvmInline value class` | 0 | - -Of the 51 files holding supertype-carrying data declarations: **33 under `app/`** -(UI/presentation), 5 under `designsystem/`, 6 under `core/`, **5 under `data/`**, 2 under -`api/`. So **~75% sit in the UI and design-system layers** — `ProfileAction`, -`InstallationUiStatus`, `BadgeIcon`, `ButtonType` — precisely §5.3's "client-only → UI state, -or a divergence" row, which the reviewer classifies and which does *not* enter the shared -domain. - -Sharper still: across the three `data/*/domain/model/` directories there are **10 data classes -and not one carries a supertype**. `Session` is typical — 11 properties, no base type. -The five `data/` files that *do* carry supertypes are four identity types plus one sealed -status (`CustomerProfileStatus`), and the identity types' supertypes are the `by`-delegation -noise described above. **The shared-domain layer of this client contributes essentially no -genuine `subClassOf` edges.** - -**Consequence: implementing Kotlin `subClassOf` synthesis buys comparatively little for the -shared-domain intersection**, whichever route is taken. It is worth doing for completeness of -the client's own graph; it is not what makes or breaks n=2. - -**A second shape finding — DTO/domain twinning.** -`data/sessions/domain/model/Session.kt` and -`data/sessions/remote/model/SessionResponse.kt` declare **structurally identical** -11-property data classes, differing only in the nested price type -(`SessionPrice` vs `PriceResponse`). The Backend has its own entity/DTO split. -The intersection is therefore computed over a 2×2 of (backend entity, backend DTO) × -(client domain, client DTO), and naive class-candidate extraction **inflates the candidate -count on both sides** before matching runs. - -**Cross-codebase overlap, measured.** Declared type names: **942** (C#, `src/`) vs **1090** -(Kotlin, non-test). **Exact-name intersection: 23** (~2% of either side). By character, the -matches split into three groups: **identity and location concepts** (the largest group, and the -one carrying the identity finding below), **subscription and payment concepts**, and a tail of -**generic infrastructure names** that are coincidental rather than domain agreement — a -reviewer would reject that tail on sight. *(The literal names are sector-specific and are -withheld here; they are recoverable from the corpus at the pinned refs.)* This is the concrete -size of the naive intersection, and it is why §5.2's lexical-plus-structural ontology matching — -not exact-name matching — is the load-bearing step. A reviewer told "here are 23 agreements" -would be seeing a small fraction of the real overlap, **and a fraction that is itself part -noise**. - ---- - -## Probe 2 — V5: closing the C# adapter's gap - -### How the live probe was obtained - -The PRD pins `roslyn-language-server` at 5.11.0. That tool is not on nuget.org, and the official -prerelease feed (`pkgs.dev.azure.com`) is **403 under this session's egress policy**. The server -was instead obtained by downloading the `ms-dotnettools.csharp` VS Code extension **v2.148.23 -(linux-x64)** from the marketplace `vspackage` endpoint and extracting the bundled -**`Microsoft.CodeAnalysis.LanguageServer 5.11.0-1.26380.4`** — the exact pinned 5.11.0 line, -exposing the same `--stdio --autoLoadProjects` entry point the adapter declares as its -`default_command`. .NET SDK **10.0.110** installed (the repo targets `net10.0`, `global.json` -sdk `10.0.0`). - -**Scope of the probe.** The Backend's own solution includes projects that depend on the private -`AcmePackageFeeds` Azure feed (403 here). A **restorable subgraph** was used instead: -`…Backend.Sql` → `…Options`, `…Primitives` — zero private `PackageReference`s, restored from -nuget.org, driven through a scratch `.sln` created **outside** the repo. `…Sql` is the EF-entity -and CQRS project, i.e. precisely the declaration shapes the extractor cares about. This is -**real production C#**, reduced to a subset — not a synthetic or substitute project. The -subset excludes `…Domain`, `…Dto`, `…Providers` and the API host, which is stated as a limit -below rather than papered over. - -All requests were issued **through the shipped adapter host layer** (`ddd_lsp::host::Host` with -`adapter::for_language("csharp")`), so the readiness signal, solution-open handshake and retry -path are the ones in the repo, not a bespoke client. - -### Item 1 — the two unwired operations, live - -Adapter readiness fired unchanged: `{"state":"ready","elapsed_ms":2604}` — Roslyn sent -`workspace/projectInitializationComplete`, which is exactly the `ReadySignal` the C# adapter -declares. No adapter change was needed to reach a ready host. - -| Operation | Roslyn answers? | Result shape | Evidence (anchor → result) | -|---|---|---|---| -| `textDocument/definition` | **Yes** | `Location[]` | `UpsertProfileSettingsCommand` → `array[1]`, self-location at 4:14 | -| `textDocument/prepareTypeHierarchy` | **Yes** | `TypeHierarchyItem[]` | → `array[1]`, item carries `data.ProjectGuid` + `data.SymbolKeyData` | -| `typeHierarchy/supertypes` | **Yes** | `TypeHierarchyItem[]` | `UpsertProfileSettingsCommand` → **`ICommand`**; `LocationId` → **`IStringIdentity`** | -| `typeHierarchy/subtypes` | **Yes** | `TypeHierarchyItem[]` | `ICommand` → **`array[3]`** (the three command records); leaf types → `array[0]` | - -**This closes the PRD's open question.** §5.4's C# row says "probe whether the Roslyn server -answers typeHierarchy". It does — **both directions**, with correct results on production -declarations. §5.2's `rdfs:subClassOf` row therefore needs **no synthesis for C#**; it needs -wiring only, and stays on the "high — declared" assurance line. - -**Wiring the adapter needs** (concrete, read against `d2bf37d`): - -1. **No host-layer change.** `Host::request(method, params)` is method-generic (`host.rs:200`); - both operations are additive call sites, as the PRD assumed. -2. **`definition` is nearly free.** The result is `Location[]`, already handled by - `protocol::normalize_locations`, which accepts `Location | Location[] | LocationLink[]` - (`protocol.rs:147–174`). Position params already exist (`protocol::position_params`). - **No new normaliser.** -3. **`typeHierarchy` is three call sites, not one** — `textDocument/prepareTypeHierarchy`, then - `typeHierarchy/supertypes` and `typeHierarchy/subtypes`. -4. **`typeHierarchy` does need a new normaliser.** Items are not `Location`s: they carry - `name`, `kind`, `detail`, `uri`, `range`, `selectionRange`, `data`. Reusing - `normalize_locations` would silently keep uri+range and **drop the symbol identity** — the - one thing the subClassOf row needs. -5. **The opaque `data` field must round-trip verbatim.** Roslyn returns - `data: {ProjectGuid, SymbolKeyData, TextDocument}`; supertypes/subtypes resolve only when the - prepare item is passed back unchanged. This is the single wiring detail most likely to be - got wrong. -6. **Client capability declaration is not the blocker.** `host.rs::initialize` does *not* - advertise `textDocument.typeHierarchy`, and Roslyn answered anyway. Declaring it remains - correct practice, but its absence is not what has been stopping the operation. - -### Item 2 — declaration-level coverage on real code - -All six operations return usable results on this codebase's actual declarations. - -| Operation | Usable? | Example (real declaration at `83c9339a`) | -|---|---|---| -| `documentSymbol` | **Yes** | `Entities/ProfileSettings.cs` → 15 symbols: the namespace, the class, and 13 properties **carrying their types in the symbol name** — `Id : ProfileId`, `CustomerId : CustomerId`, `DisabledReason : int?`, `DepartureTime : TimeOnly?` | -| `workspaceSymbol` | **Yes** | `"ProfileSettings"` → `array[15]`, each with `containerName: "project Acme.App.Backend.Sql (net10.0)"`; `"ICommand"` → `array[2]` | -| `typeHierarchy` | **Yes** | `ICommand` → 3 subtypes; `LocationId` → supertype `IStringIdentity` | -| `references` | **Yes** | `UpsertProfileSettingsCommand` → `array[9]`; `ICommand` → `array[5]`; `LocationId` → `array[8]` | -| `definition` | **Yes** | `LocationId` → `array[1]`, resolving to its declaration in `…Primitives/ValueObjects/LocationId.cs` | -| `hover` | **Yes** | `LocationId` → `"readonly record struct …LocationId\nIdentifies a location."` — **the XML doc summary comes through**, feeding the low-assurance `skos:altLabel` row | - -The `documentSymbol` result is stronger than §5.2 assumes: the **"typed properties, foreign -keys" row lists `documentSymbol, hover`, but on this codebase `documentSymbol` alone already -carries the property type**, including nullability. `hover` is a refinement, not a requirement, -for that row. - -### Item 3 — extraction-relevant shape, and which §5.2 rows fire - -How domain entities are actually declared here (census over `src/`, 951 `.cs` files): **662 -classes, 127 interfaces, 98 records** (74 positional), **74 enums, 4 structs**; **113 -declarations carry a base/interface list**. - -The Backend uses four distinct declaration idioms, and they do not extract equally: - -- **EF entities** — `internal sealed class ProfileSettings` with `required public` - auto-properties, and `internal class Location` with a private parameterless constructor for - EF materialisation. Note **`internal`**, not public. -- **CQRS messages** — positional records implementing marker interfaces: - `public record UpsertProfileSettingsCommand(…) : ICommand`. -- **Generic handler interfaces** — `ICommandHandler where TCommand : ICommand`. -- **Strongly-typed identities** — `public readonly record struct CustomerId(string Value);` - -| §5.2 row | Fires here? | On what | -|---|---|---| -| Classes, records, entities, aggregates → `owl:Class` | **Fires strongly** | 662 classes + 98 records + 4 record structs; `documentSymbol`/`workspaceSymbol` return them all with container paths | -| Inheritance, interface implementation → `rdfs:subClassOf` | **Fires, but thinly** | Only **113 of ~890** type declarations carry a base/interface list. Confirmed live via typeHierarchy in both directions | -| Typed properties, foreign keys → object/datatype properties with ranges | **Fires strongly** | Property types arrive inline on `documentSymbol` (`Id : ProfileId`), so ranges are recoverable without hover | -| Composition and reference → relations (mid, usage-inferred) | **Fires** | `references` returns 5–9 sites per type on this subset; this is the CONSTRUCT-to-fixpoint input | -| Namespaces, modules → domain axis registries | **Fires** | `workspaceSymbol` returns `containerName: "project …Sql (net10.0)"`; the 16-project split is a clean structural axis | -| Naming, comments → synonyms / `skos:altLabel` | **Fires** | XML `` reaches `hover` verbatim | - -**Two findings the extraction table did not anticipate.** - -**(a) Foreign keys are not declared, so the FK half of row 3 finds nothing.** The row reads -"typed properties, **foreign keys**". This codebase has no navigation properties and no -`[ForeignKey]` attributes on the entities probed: `ProfileSettings.CustomerId` is a -`CustomerId` value object, not a relationship to a `Customer` entity, and there is no -`Customer` type in the model at all. Relational structure lives in `AppDbContext` / -`Migrations`, i.e. in Fluent-API configuration and generated migration code, **not** in the -declaration surface the extractor reads. Row 3 will therefore yield **datatype properties with -value-object ranges and no object properties** from entities like these. This is a real limit -of the declaration-level subset, not a defect in the server. - -**(b) The identity marker is applied asymmetrically — an extraction-surfaced inconsistency.** -Of four sibling identity types in `…Primitives/ValueObjects/`, **only `LocationId` declares -`: IStringIdentity`**; `CustomerId`, `ProfileId` and `DocumentId` do not. The live -probe reflects this exactly: `LocationId` has a supertype edge, the other three would have -none. This is not cosmetic — `StringIdentityJsonConverterFactory.CanConvert` gates on -`typeof(IStringIdentity).IsAssignableFrom(typeToConvert)`, so only `LocationId` receives the -plain-JSON-string treatment the interface's own doc comment describes as its purpose. Whether -that asymmetry is intentional is the reviewer's call, not this session's. It is reported here -because it is a clean instance of §5.3's "escaped decision in the wild" surfacing from a single -codebase before any cross-codebase comparison has run. - -### Limits of this probe, stated - -- Ran on the `Sql`/`Options`/`Primitives` subgraph only; `Domain` (165 `.cs` files), `Dto`, - `Providers` and the API host were **not** loaded, because they need the private - `AcmePackageFeeds` feed which this session's egress policy denies. The mapping-rule verdicts - above are sound for the shapes probed and should be re-confirmed over the full solution on a - machine with feed access. -- The server is the bundled `Microsoft.CodeAnalysis.LanguageServer 5.11.0-1.26380.4` from the - C# extension, not the `roslyn-language-server` global-tool wrapper the adapter's - `default_command` names. Same server, same protocol surface, different packaging; the thin - wrapper's `--stdio --autoLoadProjects` flags are accepted by the underlying server as shown. -- `csharp-ls` 0.26.0 (the PRD's named fallback) was also installed and probed. It advertises - `definitionProvider: true` but **`typeHierarchyProvider` absent**, and it never completed an - MSBuild workspace load for this `net10.0` solution within 7 minutes, so it produced no - operation results. **The fallback is not equivalent for the subClassOf row.** - ---- - -## What each probe means for G0 - -### Probe 1 - -Kotlin is **closer to clearing than G-1 could establish, with one half of one condition still -open.** Three of the four things that were unknown are now settled, and settled favourably: -Gradle is 8.14.3, well clear of the silent 8.8 floor; the project is conventional (no KMP, -one flavour dimension, six variants) and its variant model configures and enumerates cleanly; -and the JDK 25 requirement turns out to be a non-issue because the distribution bundles its own -runtime, so it never meets the project's JDK 17 target. - -Two things changed the shape of the question rather than just answering it. First, -**typeHierarchy is advertised but dead** — which makes §5.4 condition (b) not merely -"preferable" but **mandatory**, and makes capability-flag gating an active hazard. Second, -**`hover` already returns the supertype clause**, so condition (b)'s route is cheaper than the -PRD assumed and needs no workspace import to work. Against that, the census finding cuts the -other way: ~75% of Kotlin's subClassOf edges sit in the UI and design-system layers, and across -the three `data/*/domain/model/` directories **10 data classes carry not one supertype between -them**. **The subClassOf row is worth implementing for completeness, but it is not what -determines whether Kotlin is a useful second leg** — the entity and typed-property rows are, -and those work today, per-file, without an import. - -The genuinely open item is narrow: **can kotlin-lsp be made to import this Gradle/AGP -workspace?** Everything downstream of that — `workspaceSymbol`, cross-file `references`, the -"composition and reference" row — depends on it, and all three were empty in the un-imported -mode. Per-file extraction alone would yield class candidates and typed properties but no -cross-file relations. - -### Probe 2 - -**V5 is closed, and closed better than the PRD's expectation.** The question §5.4 posed — -"probe whether the Roslyn server answers typeHierarchy" — is answered **yes**, in both -directions, on production declarations. The C# `rdfs:subClassOf` row needs no synthesis and -stays on the "high — declared" line. The remaining work is genuinely additive wiring, and one -of the two operations (`definition`) needs no new normalisation code at all. - -The more interesting result is that **the extraction table survives contact with production -C#, with one row narrower than written**: `documentSymbol` carries property types inline -(better than expected), while the "foreign keys" half of the typed-property row finds -**nothing**, because this codebase declares no navigation properties and keeps relational -structure in Fluent-API configuration and migrations — outside the declaration surface. That is -a limit of the declaration-level subset, and it should be written down before G0 rather than -discovered as a shortfall in the first proposal graph. - ---- - -## Consequence — both branches, not chosen - -Emil rules. Stated so that whichever way the ruling goes, the consequence is already written. - -### If Kotlin clears — G0 runs C# + Kotlin, n = 2 - -- **Two-root agreement is evidence, not the three-root assurance upgrade.** §5.3's first row - makes the upgrade conditional on independent roots, and §5.3's own n=2 qualifier already says - the upgrade's full form applies from the third root onward. With C# and Kotlin only, - agreement is a **review signal**; it does not carry the Q11/Q27 triangulation upgrade, and - proposal graphs should not be marked as though it does. -- **The intersection over a backend and one client is narrower than over a backend and two - clients** — and this session can now say how much narrower in concrete terms. The naive - exact-name intersection is **23 type names** against 942 C# and 1090 Kotlin declarations - (~2%). With two clients, a backend-only element can be classified by whether *both* clients - lack it; with one client, "backend-only" and "this client happens not to model it" are - indistinguishable without the reviewer supplying the judgement. §5.3's third and fourth rows - ("backend-only → server concern, or a divergence" / "client-only → UI state, or a - divergence") both collapse toward "reviewer classifies", with less structural evidence to - classify from. -- **The "should be identical" decision still has something to discharge against**, which is the - main thing n=2 buys: one backend and one client either do carry the same entities and - relations or they do not. This session already found concrete material for it to bite on — - **`LocationId` is modelled as a single-string identity type on both sides** - (`readonly record struct LocationId(string Value)` / `data class LocationId(val value: String)`), - a real agreement; while **each side applies its identity convention inconsistently** (C#: one - of four types declares `IStringIdentity`; Kotlin: four identity types under `data/location` - but `InstallationId` left as a bare `typealias … = String`). That is the decision's falsifier - finding usable evidence on the very first run. -- **Independence looks genuinely satisfiable here** (§5.3's "independence is a field, not an - assumption"): the two models are written in different idioms — `by`-delegating data classes - against `readonly record struct`s — with only ~2% exact-name overlap, which is not the - signature of one model generated into two languages. That is a reviewer's declaration to - make, not this session's, but the - structural evidence does not suggest a correlated-failure trap. - -### If Kotlin does not clear — G0 is single-source pipeline validation - -- Everything on the pipeline side **still exercises**: the §5.2 mapping rules (and this session - has shown five of six rows fire on production C#, with the FK half of one row empty), RDFS/OWL-RL - entailment, the CONSTRUCT-to-fixpoint relation rules, proposal graphs as registry branches, - per-triple review, and the founding ratification. The first registry instance is generated and - ratified exactly as planned. -- **What is lost is the whole comparison instrument**: no divergence finding, no shared-domain - intersection, no dissent rows, and — the sharpest loss — **the "they should be identical" - decision has nothing to discharge against**. §11 currently calls G0 "where the first real - finding lands — three codebases that should carry one domain, and the extractor saying - whether they do". With one codebase, that sentence is false and the phasing note should say - so rather than quietly under-deliver. -- **Proposed gate for the single-source branch** (proposal, not a ruling): replace the - cross-codebase gate with a **re-run determinism and yield gate** — (a) Emil ratifies a first - ontology from one codebase by merging, per-triple; (b) the extractor is re-run at a second, - later ref of the same repo and the diff is reported as candidates-added / symbols-disappeared, - which exercises §5.5's re-runnability and the Q21 decay mechanism without a second language; - (c) **bootstrapping yield reported per assurance row** — the proportion of proposed triples - accepted, which is the one §9 success criterion that does not need n≥2; and (d) the - "should be identical" decision is **filed but explicitly left undischarged**, with its - falsifier named and the reason recorded, so it is a known open rather than a silent gap. - This keeps G0 honest about what it did and did not demonstrate, and it leaves the comparison - finding as the first thing the second leg buys whenever it arrives. - ---- - -## Proposed PRD edits — ruled in, and applied - -*Written against `docs/g-track/prd-ground-as-ontology.md` at `d2bf37d` and proposed as text. -**Emil ruled these in on 2026-08-17 and they are applied in the same PR as this report**, with -two additions and one carry-along noted in the status block at the top. The text below is -retained as the rationale for each edit.* - -### Edit A — §5.4, the Swift row: deferred, reason typed as an arrangement limit - -Replace the Swift row's **Status** and **Extractor precondition** cells with: - -> | Swift | SourceKit-LSP, bundled with Xcode and swift.org toolchains | **deferred at G0 — arrangement limit, not a tooling limit.** All six operations implemented, typeHierarchy included; SwiftPM projects background-index by default since Swift 6.1. The server is not the constraint and nothing about it is in doubt | **Deferred: no macOS machine with the client's Xcode is in the programme's arrangement at G0 entry.** An xcodeproj app needs the `xcode-build-server` BSP shim and a completing build to materialise the index (unit/record files + IndexStoreDB); there is no Linux path for iOS-SDK code. This is a property of *who has which machine*, not of the adapter set: it falsifies nothing about SourceKit-LSP, requires no code change, and lifts the moment a macOS runner with the client's Xcode enters the arrangement. Re-enters the corpus as the third root, at which point §5.3's independence-triangulation upgrade becomes available for the first time | - -And add, after the table: - -> **On typing this as an arrangement limit.** The three adapter rows fail for different reasons -> and the distinction is load-bearing. C# is *wired or not wired* — engineering in this repo. -> Kotlin is *implemented or not implemented upstream* — a tooling limit we do not control. -> Swift is *neither*: the instrument is complete and verified, and only the arrangement is -> missing. Recording it as a tooling limit would misattribute the gap to the adapter set and -> would make the eventual third root look like new capability rather than a machine becoming -> available. - -### Edit B — §11, the G0 row: made conditional on this probe - -Replace the G0 row's **Delivers** and **Gate** cells with: - -> | G0 | First registry instance generated from the template (parameters supplied at generation; birth provenance in its first commit; founding decision filed by its ratifier); **Oxigraph 0.5 upgrade as task one**; local embedded store over a clone; Reading and Act types; PROV-O wiring; **code extractor over the corpus — C# first (probed and closed 2026-08-17), Kotlin second *conditional on the import question below*; Swift deferred as an arrangement limit (§5.4)**; proposal graphs as registry branches; per-triple review; **shared-domain intersection only if the Kotlin leg lands (n = 2 qualifier, §5.3) — otherwise G0 is single-source and the gate below changes** | **G0-entry checklist, as re-verified 2026-08-17:** generate the first instance (parameters supplied then); the Oxigraph 0.5 upgrade as task one; **Roslyn `typeHierarchy` probe — DISCHARGED** (answers both directions on `acme-app/backend` @ `83c9339a`; `definition` also confirmed; wiring only); **Android Gradle read — DISCHARGED** (`acme-app/android` @ `67340f69`: Gradle 8.14.3, AGP 8.13.0, six variants enumerate cleanly under Gradle); **kotlin-lsp workspace import — OPEN**, the single remaining entry condition, and the one that decides whether G0 is n = 2 or single-source; ~~the iOS build completing on Emil's machine~~ **struck as a gate — re-filed as a deferred leg (§5.4)**. Gate, **if the Kotlin leg lands**: Emil ratifies a first shared-domain ontology by merging; contradiction count reported; the "should be identical" decision filed with the extractor as its discharge. Gate, **if it does not**: the single-source substitute gate — first ontology ratified per-triple from one codebase; extractor re-run at a second ref with the diff reported; bootstrapping yield reported per assurance row; the "should be identical" decision filed but explicitly left undischarged, with its falsifier named | - -### Two further edits this probe implies, offered for the same ruling - -**Edit C — §5.2, the `subClassOf` row.** The row currently records kotlin-lsp's gap as -"typeHierarchy — where a server lacks it (kotlin-lsp, verified 2026-08-17)". That is now -imprecise in a way that could cost an implementer a day. Suggested replacement for the -parenthetical: - -> *(re-verified live 2026-08-17 against kotlin-lsp v262.9593.0:* the server **advertises -> `typeHierarchyProvider: true` and then returns `null`** — the gap is a silent empty result, -> not a capability denial or an error. **Adapters must probe, never trust the capability flag**, -> or Kotlin will contribute zero `subClassOf` edges while reporting success. Synthesis for Kotlin -> is cheaper than assumed: `hover` returns the server-rendered declaration header including the -> supertype clause (`data object NoIcon : BadgeIcon()`), so no raw-text slicing is required and -> the route works without a workspace import. For **C#, this row needs no synthesis at all** — -> Roslyn 5.11.0 answers `prepareTypeHierarchy` / `supertypes` / `subtypes` correctly.* - -**Edit D — §5.2, the typed-properties row.** Add the measured limit: - -> *(measured on production C# 2026-08-17:* `documentSymbol` alone returns property **types and -> nullability** inline (`Id : ProfileId`, `DisabledReason : int?`), so `hover` refines -> rather than enables this row. **The "foreign keys" half found nothing**: the probed codebase -> declares no navigation properties and no FK attributes — relational structure lives in -> EF Fluent-API configuration and generated migrations, outside the declaration surface. Expect -> datatype properties with value-object ranges and **no object properties** from entity -> declarations of this shape.* - ---- - -## Appendix — reproduction - -Everything below ran on Linux x86-64 with no macOS and no Android Studio. Nothing was written -into either corpus repository. - -| Component | Version / source | -|---|---| -| Roslyn LSP | `Microsoft.CodeAnalysis.LanguageServer` **5.11.0-1.26380.4**, extracted from `ms-dotnettools.csharp` **v2.148.23** (linux-x64), fetched from the marketplace `vspackage` endpoint | -| .NET SDK | **10.0.110** (apt `dotnet-sdk-10.0`); repo `global.json` asks for 10.0.0 | -| csharp-ls (fallback) | **0.26.0** via `dotnet tool install --global csharp-ls` | -| kotlin-lsp | **v262.9593.0** standalone Linux-x64, `download-cdn.jetbrains.com`, SHA-256 `2d99d8e1…7cc5e` verified against the published digest; bundles JBR 25.0.2 | -| Android SDK | cmdline-tools `11076708`, `platforms;android-36`, `build-tools;36.0.0`, `platform-tools` — exposed via `ANDROID_HOME`, **no `local.properties` written** | -| Gradle | **8.14.3** via the repo's own wrapper, run with `-g`/`--project-cache-dir` pointed at scratch | - -**Egress notes** (this session's policy): `github.com`, `api.github.com`, -`pkgs.dev.azure.com` (both the Microsoft prerelease feed and the private `AcmePackageFeeds`) -and `ms-dotnettools.gallerycdn.vsassets.io` all return **403** and were not routed around. -`raw.githubusercontent.com`, `download-cdn.jetbrains.com`, `marketplace.visualstudio.com`, -`api.nuget.org`, `services.gradle.org`, `dl.google.com` and `repo.maven.apache.org` are -reachable. The third-party vendor Maven repository fails to connect — a connection failure, not a policy denial. - -**Probe harnesses** (scratchpad, not committed): a Rust binary depending on `ddd-lsp` by path -that drives `ddd_lsp::host::Host` with `adapter::for_language("csharp")` — so V5 exercised the -**shipped** adapter host layer, not a bespoke client — plus two Python raw-JSON-RPC harnesses -for kotlin-lsp and for reading `InitializeResult` capabilities (which `Host::initialize` -discards). - -**Solution scoping for V5:** `dotnet new sln --format sln` in the scratchpad, adding -`…Backend.Sql`, `…Options`, `…Primitives` by absolute path, restored from nuget.org. This keeps -the private-feed projects out of the graph without editing the Backend's own solution. diff --git a/docs/g-track/g0-session-report-2026-08-19.md b/docs/g-track/g0-session-report-2026-08-19.md deleted file mode 100644 index 00bbb97d..00000000 --- a/docs/g-track/g0-session-report-2026-08-19.md +++ /dev/null @@ -1,425 +0,0 @@ -# G0 (resumed) — bootstrapping the registry from code: session report - -**Repository:** `Hafeok/product-cli`, branch `claude/registry-bootstrap-code-gl2erk`. -**Instance:** `Hafeok/ground-registry-g0` (private) — generated, published, founding content ratified. -**Corpus:** `corpus-backend`, read-only, at `3b0a56b3`; baseline ref `b8aba87` for the re-run. -**Gates:** 1 instance · 2 adapter wiring · 3 extractor · 4 per-triple review · 5 close. -All five held; gates 1–4 ratified by emil@okkels-klein.dk. - -**Status: G0 is not discharged.** The pipeline is validated end to end — extraction, proposal, -per-triple review and ratification hold. Two clauses of §11's single-source substitute gate remain -open, both named in §9 below. - -**Naming discipline** holds throughout: corpus repositories are `corpus-backend` / `corpus-android`, -and domain identifiers are abstracted structurally. Extracted triples are content and live in the -generated instance, never here. - ---- - -## 1. Manifest - -| Deliverable | Outcome | -|---|---| -| First registry instance | Generated by the tested mechanism, published, founding content ratified | -| The two C# adapter operations | Wired; `definition` and `typeHierarchy` both answering live | -| Capability probe | Implemented extractor-wide; six of six available on this corpus | -| Code extractor | Model-free, rung zero; 561 assertions proposed | -| Proposal graph + per-triple review | Ruled: 512 accepted, 49 rejected in two groups | -| Re-run at a second ref | 22 candidates added, 3 symbols disappeared | - ---- - -## 2. Gate 1 — the instance, generated by the mechanism - -Generated with `product registry generate`, **template 0.2.0, generator 0.6.0**. No hand -substitution, no script, no post-generation edit. Parameters as settled: - -| Parameter | Value | -|---|---| -| Owning organisation | `Hafeok` | -| Repository | `ground-registry-g0` | -| Ratifier | `emil@okkels-klein.dk` | -| Display name | `Ground Registry (G0 validation)` | -| Base IRI | `tag:emil@okkels-klein.dk,2026-08-17:ground/` | -| Mint date | `2026-08-17` | -| Generated by | the G0 session, named in the birth provenance | - -Gate output: **10 files, 28 parameter sites verified byte-identical**, birth commit -`7cc77b22`. Determinism re-checked — regenerating from the same parameters produced the same commit -id, and the only tree difference was the founding content added afterwards. - -`GENERATION.ttl` records **both versions**, template beside generator, as 0.2.0 requires. - -### The finding of that gate: parameter nesting makes substitution failures correlated - -Raw occurrence counts per parameter are `RATIFIER` 13, `MINT_DATE` 13, `BASE_IRI` 9 — higher than -the gate's 28 recorded spans, because **the ratifier's address and the mint date are substrings of -the base IRI itself**. That is why the first attempt's corruption was pervasive rather than local: -the sigil that broke sat in the one parameter carrying two others inside it, at nine sites across -every file class. - -Two consequences worth carrying. Substitution failures are **correlated, not independent** — a -nesting relation between parameters propagates one fault to every dependent site. And the gate's -span-counting is the right instrument precisely because it does not care how values compose: it -records what it wrote and reads it back, so nesting is invisible to it. - -A corruption that touches everything is also, perversely, the easiest kind for a human to spot. A -single-site one would not have been caught by reading output. - -### The founding texts, supplied verbatim - -Both texts were ratified **as text** at the first G0 attempt and carried forward unchanged; the -ratification was a ruling on the wording, not on the tree it was written into. - -No literal was retyped. Each was extracted programmatically from the handoff, written into Turtle, -then read back out of the **parsed graph** by an independent reader (rdflib) and compared against a -fresh extraction of the handoff. All nine multi-line fields byte-identical by sha256: - -| Field | chars | sha256 (16) | -|---|---|---| -| founding · `resolution` | 843 | `97eb04e162d277d2` | -| founding · `region` | 854 | `766c4dd9d2802a3d` | -| founding · `falsifier` | 275 | `1a917b7aed359d0b` | -| g-dec-03 · `resolution` | 321 | `388cfe0d4bc9393d` | -| g-dec-03 · `basis` | 281 | `e7c6c215b7d97e5c` | -| g-dec-03 · `region` | 128 | `982201e179993ede` | -| g-dec-03 · `acceptedCost` | 263 | `14d496850759c7c9` | -| g-dec-03 · `revisitIf` | 192 | `46925b7fd0170667` | -| g-dec-03 · `falsifier` | 184 | `af8a2cf7cc2745ba` | - -Neither of the template additions ratified alongside them was re-applied instance-locally; both come -from the template at 0.2.0, so the instance does not fork it. - ---- - -## 3. Gate 2 — the adapter operations, and the probe - -`definition` reuses `normalize_locations` unchanged, exactly as the entry probe predicted — no new -normaliser, no host-layer change. `typeHierarchy` is three call sites and needed a new item -normaliser: hierarchy items are not locations, and routing them through the location normaliser would -have kept uri and range while dropping the symbol identity, which is the half the `rdfs:subClassOf` -row needs. - -**The opaque `data` blob round-trips by construction rather than by field-copying**: the normalised -item keeps the server's own item verbatim and hands *that* back, so a field a future server adds -survives too. A fixture strikes it, and the mock **refuses an item whose `data` was rebuilt**, making -the failure a live host would show as a silent empty result loud in CI. - -### The probe, live - -Six of six operations available on `corpus-backend`'s restorable subgraph, through the shipped host -layer, Roslyn `5.11.0-1.26380.4`, ready in ~3s. Both hierarchy directions reproduce the entry probe: -a marker interface resolves to its three implementing command records; a value-object identity type -resolves to its interface supertype. - -**The probe earned its existence on its first real run.** Four sibling identity types all *answer* -`prepareTypeHierarchy` with one item; only one returns a supertype. So the asymmetry reads as three -**genuine empty hierarchies**, not three instrument failures. Without the probe those absences would -have entered a proposal graph indistinguishable from a silent tool fault, and at G0.5 the -cross-codebase comparison would have read them as divergence — a finding manufactured out of an -instrument's silence. Prevented before the extractor existed. - -### A correction to §5.2's under-advertising bullet - -§5.2 cites Roslyn as the observed under-advertising case. Measured under two client handshakes — with -and without `textDocument.typeHierarchy` declared — Roslyn advertises `typeHierarchyProvider: true` -**both times**. It never under-advertised as a *server*; the entry probe's observation was about the -*client* capability the adapter omitted, which is a different pair from the one the probe compares. - -The direction is real (`csharp-ls` does not advertise the provider at all) but it is **not evidenced -by Roslyn**, and the fixture is what carries it until a server demonstrates it. The client capability -is now declared because that is correct practice, not because it unblocked anything — measurement -shows it changes nothing about what this server answers. - ---- - -## 4. Gate 3 — the extractor - -`product-core/src/ground/` is the pure slice — no LSP, no governance stack, so the -downstream-consumer contract holds. `ground-cli` is the adapter carrying the reader and the language -host. Model-free, rung zero: deterministic reads, mapping rules, entailment, CONSTRUCT to fixpoint, -SHACL. There is no candidate-generation step, so there is nothing for a model to do. - -### Yield by assurance row - -| Row | Grade | Provenance | Outcome | -|---|---|---|---| -| classes | high | observed | fired — 58 | -| subclass | high | observed | fired — 17 | -| properties | high | observed | fired — 215 | -| **foreign-keys** | **not-derivable** | — | **absence reported, not synthesised** | -| modules | mid | observed | fired — 86 | -| usage-relations | mid | inferred | fired — 130 | -| synonyms | low | inferred | fired — 55 | - -**290 high · 216 mid · 55 low**, 561 assertions in 561 files. Six of seven rows fired; **no row found -nothing**, and **no row was gated off** — all six operations answered. The only silent row is the one -with no instrument, and it says so in those words rather than as a zero. - -Mechanical checks over the proposal: **0** assertions with `inferred` provenance at an assurance the -row does not justify, **0** with no evidence, **0** inferred edges without a citable reference site. - -### The fixpoint - -``` -3 rounds · 3 rule parses · 9 executions · 13 ms -seed 1630 triples · 130 proposed · 109 entailed (recomputed at build, never stored) - usage-direct 130 over 1 productive round - usage-transitive 109 over 2 productive rounds [entailed, not proposed] - subclass-transitive 0 over 0 productive rounds [entailed, not proposed] -``` - -Parse-once holds — 3 parses against 9 executions — and a fixture asserts `parses == rules` and -`executions == rounds × rules` so it cannot regress. - -**Do not read 13 ms as a scale datum.** At 61 types over a schema-only graph the stage is barely -working. The figure to watch against `g-dec-02`'s `revisit_if` is the **130 → 109 ratio**, not the -milliseconds: the transitive rule is the quadratic one, and the full solution is roughly fifteen times -this corpus. - -The line between proposed and entailed: *an assertion is proposed when it is not recomputable from -the proposal set.* The containment join qualifies — its inputs are reference positions that never -enter the registry. The two closures do not, so they are measured and discarded. - -### The extraction predicate's closure record - -The predicate closed on every row that has an instrument. The declared rows compute their verdict -from the language's own declarations; the structural and heuristic rows compute theirs by rule. -Nothing was judged and nothing halted. One row has no instrument on this surface and is reported as -such. - -**But the predicate that closed is narrower than the one a reviewer needs, and the corpus made that -visible.** "Is this a declared type" closed completely. "Is this a *domain* type" was never the -predicate, was never computed, and is where the reviewer's work actually falls — **including on the -declared rows**, which §5.2 does not anticipate. See §7.1. - ---- - -## 5. Gate 4 — review, and the ratification outcome - -Two pull requests on the instance, split deliberately: a decision must not ride in on a batch of 561 -extraction assertions. - -**Ruled dispositions** (the merge acts are the ratifier's and are pending at the time of writing): - -| | Count | Disposition | -|---|---|---| -| Group A — generated migration code | 18 | rejected | -| Group B — field-derived properties | 31 | rejected | -| Remainder | 512 | accepted, reviewed by grade | -| `g-dec-04` — assertion identity | 1 decision | accepted | -| **Total proposed** | **561** | **512 accepted · 49 rejected** | - -Review batched by the axis: the 255 high/high at pace, attention spent on the 206 mid, the 51 low, -and four ranked groups (synonyms, module axes, unranged properties, discarded nullability). - -`g-dec-04` files the Reading-identity ruling: content-addressed assertion identity, with the loss of -re-confirmation as its accepted cost and the format-change condition as its `revisit_if`. - ---- - -## 6. The re-run at a second ref — §5.5 and the Q21 decay mechanism - -The extractor was run at baseline `b8aba87` (2026-05-19) and at `3b0a56b3`, both writing nothing, and -the two proposal sets diffed by assertion id. - -``` -baseline b8aba87 : 542 assertions, 57 declarations -later 3b0a56b : 561 assertions, 61 declarations - -candidates added : 22 classes 4 · modules 6 · properties 3 · subclass 1 · synonyms 4 · usage 4 -symbols disappeared : 3 properties 3 -unchanged : 539 -``` - -The 22 additions are four types introduced in the primitives project — a marker interface, two -serialisation converters, and an identity type — with their module, synonym, hierarchy and usage -triples. The 3 disappearances are the three property assertions of a **boolean feature-flag property -removed from a configuration options type**, verified independently against the corpus diff: present -at the baseline, absent at the later ref. - -That is the holding note's Q21 decay-of-relevance mechanised, on real code, with no second language -and no drift-detection machinery — it falls out of content-addressed identity and the file set. - -**A first attempt at this was vacuous and is recorded as such.** Run initially against the entry -probe's ref, two commits back, the diff was empty — correctly, because the intervening commits touched -only files outside the read subset (confirmed by `git diff --stat`). An empty diff exercises the -mechanism's plumbing but not the decay detection, so the baseline was moved to a ref with real churn. - ---- - -## 7. Findings for the PRD - -Reported, not applied. Edits ride a later PR. - -### 7.1 The extraction table needs a second axis — **ranked first** - -§5.2's assurance column conflates two independent things: - -- **Derivation confidence** — how reliably the instrument read the code. -- **Domain relevance** — whether the result is the kind of thing the registry is for. - -The corpus proved they are independent. **Group B is 31 assertions at `high` assurance, every -mechanical check passing, the instrument having read the code perfectly, and every one of them wrong -to ratify.** They are private serialisation state on infrastructure types. Group A is 18 more — -generated schema-change scripts proposed as class candidates, spread across four rows and all three -grades. - -§5.2 says the declared rows are where extraction closes and review earns its keep on the inferred -ones. That is now **measured as wrong**: the declared rows are where the largest single block of -rejections sat, and it sat there at the top grade. - -Proposal: rows carry `derivation_confidence × domain_relevance`. High/high ratifies at pace; -high/low — code read perfectly, irrelevant to the registry — is exactly the 49. - -**The forcing argument is recoverability, not convenience.** Group A is recoverable from the graph -after the fact, via the `reg:evidence` paths its assertions carry. **Group B is not recoverable from -the graph at all** — nothing in a ratified assertion records that its property came from a field -rather than a property, and the count had to be taken from the extractor's own JSON output. A future -reviewer re-examining this class of error cannot find its members. That is not a reporting gap; it is -**ground that cannot be audited**, and it makes marking derivation on the Reading non-optional rather -than nice to have. - -### 7.2 Widening the declaration-level subset should be a filed decision - -Both practical faces of §7.1 have cheap remedies that are **out of the current subset**: - -- Generated code carries markers — attributes, a folder convention, a generated-file header — none of - which appear in `documentSymbol` output. -- Visibility is already derived from source text by the C# adapter's own facts layer, which the - extractor does not use because it is outside the declaration-level six. - -The subset was chosen to keep the extraction predicate closing, and it does close — on a predicate -narrower than useful. Widening it is the obvious next move and should be **a filed decision with the -closure consequence stated**, not a quiet extension: each widening trades constructive closure for -relevance, and the trade should be recorded where it can be revisited. - -### 7.3 The entailment stage is correct and idle - -`subclass-transitive` fired **zero** times across 17 hierarchy edges: every declared derivation in -this corpus is one level, type → marker interface. With a schema-only graph and no instance data, the -RDFS/OWL-RL stage's entire available yield *is* subclass transitivity, and this corpus has none. - -Recorded prominently so no later reader of the manifest concludes the reasoner is doing work. It is -wired, tested, and currently contributing nothing. - -### 7.4 Per-triple review does not scale, and the answer is the same second axis - -561 files is already at the edge of per-triple review, and the full solution is roughly fifteen times -this corpus. The answer is not wholesale merge — that is manufactured ground by §5.6. It is review -**batched by derivation confidence × domain relevance**, which is §7.1 arriving from the review side -rather than the extraction side. Two independent routes to the same change. - -### 7.5 `definition` is wired but unspent - -Two of the six operations answer and no mapping row consumes them: `workspaceSymbol` and -`definition`. The reads take the per-file route, and range resolution matches on simple name, which -is safe **on this corpus** — 0 cross-module name collisions, measured. `definition` is the -exact-resolution route if collisions appear at full-solution scale, so it is insurance rather than -waste. Worth recording that Gate 2's wiring bought a capability the extractor does not yet spend. - -### 7.6 Explanatory output is review infrastructure, not debug detail - -The extractor's notes — which properties carry no range, which came from fields, which types are -partial — were typed `#[serde(skip)]`, i.e. as debug detail. They are the reviewer's **batching -keys**. Two hand-counts were wrong before the notes were made queryable (13 fields is not 31 -assertions; a hardcoded list taken from a report that had truncated at six). General shape: an -instrument's account of its own limits belongs in its output contract, not in its logs. - -### 7.7 Three collapsed report states - -"Nothing written — planned only" covered a dry run, a run with nothing new to propose, and a run whose -files were already byte-identical. Three states rendering as one passing line — the presumed-discharge -pattern in miniature, inside our own instrument. Split. - -### 7.8 Smaller observations against the table - -- **Nullability is read and discarded.** 20 properties declare it; expressing it needs OWL cardinality - restrictions, outside the row's stated output. Nothing wrong is in the graph; something known is - missing from it. -- **Doc summaries reach the extractor unmapped.** `hover` returns the declaration's summary verbatim; - the synonyms row's output vocabulary has nowhere to put it. -- **The synonyms row may be specified wrong.** A word split over an identifier is a *spelling*, not a - synonym — and the weakest thing that satisfies a row's stated output is usually a sign the output - was mis-specified. -- **`partial` means a declaration count is not a type count**, and same-name-in-one-module (one type) - must not be reported as same-name-across-modules (two types). Reported as one number, it says the - wrong thing about both. -- **A nested type's module is its namespace, not its enclosing type.** Reading the innermost container - proposed three module axes named after classes. - ---- - -## 8. Mechanics carried from the Oxigraph manifest - -| Input | Disposition | -|---|---| -| `SparqlEvaluator::prepare` | Rules parsed once, cloned per round. 3 parses / 9 executions, fixtured. Wall-time recorded; see §4 on why the ratio matters more | -| Registry Turtle from our own writer | Fixed field order, sorted evidence, no blank nodes, one assertion per file. The oxigraph serialiser is transport and inspection only | -| Blank-node re-minting | Nothing to bite on — the template reifies onto a minted IRI. The writer emits no blank node at all, and the store→triple conversion drops any quad carrying one | -| Drain-before-insert | Every CONSTRUCT drained before writing back; the triple iterator borrows the store | - -**Reading identity, in full.** An assertion's identity is `sha256(subject, predicate, object)` — not -the run, not the `as_of`, not the assurance. Three consequences, each fixtured and each demonstrated -live: re-running at the same ref proposes nothing and leaves the tree byte-clean; re-running at a -later ref proposes only the delta; a disappeared symbol leaves a ratified file with no counterpart. - -The limit, ruled and filed as `g-dec-04`: because the Reading shares the assertion's identity, a -**re-confirmation at a later ref is not recorded**. Writing a fresh `as_of` is a rewrite, which -supersession-not-rewriting forbids; giving the Reading its own node needs a file carrying no assertion -and no decision, which the file rule rejects. Both are format changes rather than instance ones. - -At volume, content-addressed identity is what makes review possible at all: sequential numbering would -make every second run's diff total churn. - ---- - -## 9. What blocks G0 - -§11's single-source substitute gate has four clauses: - -| Clause | State | -|---|---| -| First ontology ratified per-triple from one codebase | **Ruled** — 512 accepted, 49 rejected. The merge acts are the ratifier's and are pending | -| Extractor re-run at a second, later ref; diff reported | **Discharged** — 22 added, 3 disappeared (§6) | -| Bootstrapping yield reported per assurance row | **Discharged** (§4) | -| The "should be identical" decision filed, explicitly undischarged, falsifier named | **Open** — outside this session's scope; recommended as the next act | - -**G0 is therefore not discharged.** What blocks it is the two merge acts and one unfiled decision — -not the instrument, which is validated end to end. - -### The G0.5 condition, restated - -The comparison instrument — cross-codebase agreement, dissent, shared-domain intersection — moves to -**G0.5**, gated on an environment with private-feed access. That single condition also unblocks two -other things named in the PRD: the Kotlin workspace import (which decides n = 2), and the -**re-confirmation of these mapping-rule verdicts over the full solution**, which this session's -subgraph limit makes necessary rather than optional. Every yield number here is sound for the shapes -probed and is scoped to three of the corpus's projects. - -Swift remains deferred as an **arrangement limit**, not a tooling one. - ---- - -## 10. Gates green - -At every hold: `cargo t` (1454 tests, 0 failed), `cargo clippy --workspace -- -D warnings -D -clippy::unwrap_used`, `cargo xtask check` (0 errors), `ledger verify`, `ddd validate` (272 entries, -conformant), and the **contract-surface gate at 331 events, 0 undischarged**. - -The surface was narrowed before it was declared, as the generator session established: the extraction -slice publishes its acts — plan, apply, render — with the types they carry, and nothing about how a -row is spelled or how the fixpoint is driven. The narrowing also removed a containment lookup that -the CONSTRUCT rule had made dead. - -Instance-side: `product registry check` conformant at every stage, and the instance's own CI steps run -locally — the file rule clean and pySHACL 0.40.1 `Conforms: True` over 9,074 triples. - ---- - -## 11. What this session did not touch - -The TUI, the loop, the model ladder, the Copilot arm, Feature 1, Feature 2. Kotlin and Swift legs. -The cross-codebase comparison. Any PRD edit — the findings above are reported and nothing in -`prd-ground-as-ontology.md` was changed. Canon. `corpus-backend` was read only; no commit, branch or -file was written to it, and the temporary worktree used for the baseline read was removed. diff --git a/docs/g-track/g1-session-report-2026-08-17.md b/docs/g-track/g1-session-report-2026-08-17.md deleted file mode 100644 index ea59a116..00000000 --- a/docs/g-track/g1-session-report-2026-08-17.md +++ /dev/null @@ -1,501 +0,0 @@ -# G-1 session report — reconcile, verify, initialise (2026-08-17) - -**Session:** G-1, per the session prompt of 2026-08-17. Interactive; Emil rules at every gate. -**Branch:** `claude/g1-reconcile-verify-initialise-g0uno2` (all three repositories). -**This file grows gate by gate.** Step 1 (canon reconciliation) is below; Steps 2–4 append after -their gates open. - -## Sources pinned (the staleness discipline, applied to this session's own inputs) - -Every artefact this report projects is named here with its ref. A row without a ref would fail the -projection-as-source diagnostic the session prompt applies to its own outputs. - -| Source | Ref / identity | Role | -|---|---|---| -| `actor-indexed-determination` (upstream canon) | `110bf10ff8fc0de0d71440310c869df78e34d8ef` — v5.5.0 (merge `e8663b8`, tagged 2026-08-16) plus two post-tag merges (PR #11 release CI, PR #12 session reconciliation); no canon content moved after the tag | reconciliation target | -| `decision-driven-design` (downstream canon) | `4848b9e15ea49bc923b2d23933e2c05a21202ba0` — merge of PR #21; `graph/upstream.yaml` pinned at v5.5.0 (DDD-dec-18) | reconciliation target | -| `product-cli` | `d506ac94310bb24e3c6a1b786034046ac0d024b0` (branch point) | the G-track home | -| PRD `prd-ground-as-ontology.md` | Emil's upload, 589 lines, written 2026-08-14 against holding-note revisions 13–14 | the document reconciled | -| Ground-axes holding note | Emil's upload, revision 18 (2026-08-15), 2,093 lines | **context only — canon outranks it everywhere they touch** | -| `meta/corpus-test-results-2026-08-14.md` | downstream, at `4848b9e` | evidence, fetched not uploaded | -| `meta/vocabulary-delivery-session-2026-08-15.md` | downstream, at `4848b9e` | evidence, fetched not uploaded | -| `meta/session-reconciliation-2026-08-16.md` | upstream, at `110bf10` | evidence, fetched not uploaded | - -Queue position: satisfied. The vocabulary-and-delivery scope merged as v5.5.0 (upstream PR #10, -downstream PR #21); this session reconciles against that canon, not against the holding note. - ---- - -# Step 1 — canon reconciliation - -## 1.1 What v5.5.0 filed, and the deltas against what the PRD assumed - -The PRD was pinned to holding-note revisions 13–14. The vocabulary-and-delivery session filed from -holding-note revision 8 plus the applicability note of 2026-08-12, with every filing scoped to what -the corpus test of 2026-08-14 evidenced. The deltas that matter to the PRD: - -| Canon object | What it says | Delta against the PRD's assumption | -|---|---|---| -| `DDD-ground-01` (upstream, normative, projected) | A governing decision must declare a **resolvable applicability predicate**, or explicit universal applicability; non-evaluation never silently becomes non-applicability; implemented axes are marked mechanically-evaluable or judgement-evaluable | The PRD assumed Q1's named-axis gate. Canon amended it: the gate is predicate-general, and factored axes are one implementation, not the ontology of every region. The corpus put beyond-region predicate cases at ~27–36% | -| `DDD-ground-01` evidence (the matched pair) | The axis-type mark is a **maturity state**, not a fixed type — an axis moved nameable→resolvable in ~2.5 weeks | This answers open ruling 12 in canon's favour, which the PRD's §9.1 treated as open. Erratum noted in canon: the corpus document's "five weeks" was wrong; the repo dates govern | -| `DDD-ground-02` (upstream, conceptual, projected) | Source coverage (covered · declared-empty · undeclared · unknown), resolution (resolved · deliberately-open · unknown), and assurance (adequate · inadequate · unknown) are **orthogonal**; Unknown is never a pass | The PRD's §6.2 used Q3's four-state typing as UI. Canon rules the orthogonal typing governing; the four states survive only as the corpus's recorded projection, with "inert" replaced by **declared-empty** (ruled in, zero corpus draws recorded as its evidence status; the filing of a declared-empty is a claim-layer act) | -| `DDD-ground-03` (upstream, conceptual, projected) | Timing carries a fourth value, **"—(open)"**, for decisions whose resolution is deliberately-open | New since the PRD; touches the status line and any timing display | -| `DDD-ground-04` (upstream, normative, projected) | A retro-filed decision carries **two fields** — when the gap was uncovered, and that it was retro-filed | Ruled in, as the PRD hoped. Replaces holding-note §13.4 as the authority for §5.3's retro-filed decision set. Both sub-rulings recorded: retro-filing is the ledger-side discharge mechanism for the escape generators, and the retro-filing act is a claim-layer act | -| `DDD-delivery-01…03` (upstream) + `DDD-delivery-04` (downstream) + `core/13-delivery.md` + `term:delivery` / `term:undelivered` / `term:presumed-discharge` | Filing is not encoding; undelivered governance is **escape that presents as governance** (a generator, answering Q18's open question); unretrieved decision + unretrieved check are correlated failures; maturation's paid-once property holds only where the channel delivers per act-site | The PRD's delivery references (Q15/Q16/Q18/Q19) re-pin to canon terms and claims. The delivered-vs-emitted comparison the PRD names as its primary evidence output now has its stake in canon (`DDD-delivery-02`) | -| Term-collision repair | `00-primitives`' closing aside renamed "A note on delivery" → "A note on **presentation**" — the registry owns "delivery" | The PRD's uses of "delivery" were audited: every instance is in canon's sense (how authored governance reaches an act). No edit required. Spelling: canonical texts take "judgment"; claim and projection prose keeps British "judgement" — the PRD conforms | -| `graph/axis-registry.yaml` (downstream) | axis-registry/v1; **artefact-not-canon**; 22 axes (19 resolvable, 1 resolvable-partial, 2 nameable); promotion path: a validator reads it plus a ratification act | The first concrete axis registry exists — the framework program's own instance, not the G-track registry. It is the format precedent G0's proposal graphs inherit: quality marks and extractor sketches per axis | - -One finding the PRD did not anticipate, in the other direction: - -**The provenance typing is not in canon.** The PRD's §4.1 calls the four-value set (controlled / -observed / inferred / institutional) "canon's provenance typing", and §10 listed it among ratified -canon the PRD stands on without qualification. A search of both repositories at the pinned commits -finds no such typing — not in the core documents, not in `core/graph/terms.yaml`, not in any claim. -The holding note itself presupposes it rather than defining it (Q12 "the existing provenance typing -already supplies the scaling"; Q27 says canon "has carried this slot empty since the foundation -document"). The mechanism in the PRD stands; its basis is unratified. Marked in the PRD as -UNVERIFIED — Emil review, with two resolution paths: file the typing upstream before G0 consumes -it, or let the G-track SHACL shapes own the value set as track vocabulary with no canon claim -behind it. This is a mis-pin, not a design collision. - -## 1.2 The §10 walk, row by row - -Statuses read against upstream `110bf10` and downstream `4848b9e`. The PRD's fallback column -applies only on rejection; nothing was rejected, so no fallback fires. - -| §10 construct | Status | Canon citation / absence | PRD sections touched | -|---|---|---|---| -| Position / region retrieval (Q2, Q19) | **ratified amended** (gate half); not filed (retrieval mechanism) | `DDD-ground-01` — predicate-or-explicit-universal, axis marking; region states axes are one implementation. No claim files the evaluator or subsumption retrieval itself | §7.2 (edits 2, 7), §7.3 | -| Extraction verification (Q20) | **not filed** | No claim; `DDD-ground-01`'s marking clause anchors the closure commitment; open ruling 13 open | none — dependency stands as flagged | -| Reading three-tuple (Q11 amended) | **not filed** | Assurance appears only as `DDD-ground-02`'s orthogonal property of ground; the per-reading tuple is note-only | §4.1 (provenance note, edit 11) | -| Trust decision backing (Q27) | **not filed** | Open ruling 27 open | none | -| Emitted proxy (ruling 22) | **not filed** as obligation; stake now canon | Open ruling 22 open; `DDD-delivery-02` + `term:presumed-discharge` file the comparison's stake | §4.2 (edit 8) | -| Registry = existing ontology (ruling 25, A1) | **not filed** | Open ruling 25 explicitly open — "the fork … the retrieval PRD waits behind" | none — A1 stands as assumption | -| Triangulation-with-independence (Q27 amended) | **not filed** | No filing | none | -| Decay-of-relevance under a pin (Q21) | **not filed** | Open rulings 14, 15 open | none | - -## 1.3 Named holding-note items outside §10 - -| Item | PRD section | Status | Canon citation / absence | Edit | -|---|---|---|---|---| -| Q1 gate at proposal time | §4.3 | **ratified amended** | `DDD-ground-01` — named-axis became resolvable-predicate-or-explicit-universal | edit 2 | -| Q3 four-state typing | §6.2, §7.6 | **ratified amended** | `DDD-ground-02` — orthogonal typing governs; four states demoted to recorded projection; declared-empty ruled in; `DDD-ground-03` adds "—(open)" | edits 3, 4 | -| Q6 declaration discipline | §6.1 declare row | **not filed** | Precedence discipline is note-only; `DDD-ground-04`'s before/after evidential-status reasoning is the adjacent canon | none | -| Q11 three-tuple | §4.1 | **not filed** | as §10 row above | edit 11 (provenance only) | -| Q21/Q12 remedies | §6.1 declare row, §5.5 | **not filed**; the note's Q12/Q18 tension is resolved | `DDD-delivery-02` rules undelivered a generator of escape; ground-not-as-expected stays outside escape — both rulings now compatible, delivery is what distinguishes them | none | -| Q23 contract | §4.2 | **not filed**; one field anchored | `unevaluated_axes` is `DDD-ground-01`'s non-evaluation clause mechanised; the exposure profile stays note-only | edit 7 | -| Q26 ontology reading | §3, substrate | **not filed** | The design substrate remains unratified; axis-registry/v1 is the first concrete instance (artefact) | edit 9 | -| Q27 trust decisions | §4.4 | **not filed** | as §10 row above | none | -| Q30 authority/projection, "ground registry" | §3, §5.3 | **not filed** as canon term | Open ruling 32 is a freight-list item; Emil's rulings embedded in the PRD header are settled and unaffected | none | - -## 1.4 The re-pin table — edits applied to the PRD copy - -The edited PRD is `docs/g-track/prd-ground-as-ontology.md` on this branch. Every edit carries the -marker *(re-pinned at G-1)* in place. COLLISION count: **0**. One basis error (provenance typing) -is marked UNVERIFIED — Emil review rather than COLLISION, because it is a mis-pin, not a design -conflict between canon and the PRD. - -| # | PRD section | Construct | Canon status | Edit | -|---|---|---|---|---| -| 1 | Provenance header | pinning | — | re-pin note added: commits named, holding note demoted to context | -| 2 | §4.3 proposed-decisions row | Q1 gate | ratified amended | gate restated as `DDD-ground-01`'s predicate-or-explicit-universal with axis marking | -| 3 | §6.2 status line | Q3 typing | ratified amended | orthogonal typing governing; four-state kept as canon's recorded projection; inert→declared-empty, uncovered-undeclared→undeclared; "—(open)" added (`DDD-ground-03`) | -| 4 | §7.6 halt row 2 | Q3 typing | ratified amended | trigger restated as source coverage = `undeclared`; escalation options renamed to canon values | -| 5 | §5.3 identity decision | retro-filing §13.4 | ratified as `DDD-ground-04` | authority re-pinned from the note to the claim; two fields and both Gate 4 sub-rulings cited | -| 6 | §9.1 evidence criterion | ruling 12 | answered in canon | criterion reframed from type-vs-maturity to transition rate; matched-pair evidence cited | -| 7 | §4.2 `unevaluated_axes`; §7.2 step 3 | Q23 field; Q2 regions | anchored / partially filed | `DDD-ground-01` citations added; predicate-generality of regions noted | -| 8 | §4.2 comparison; §7.2 close | delivery vocabulary | filed (draft) | stake cited to `DDD-delivery-02` / `term:presumed-discharge`; "holding note's Q19 sense" → `term:delivery`, `core/13-delivery.md` | -| 9 | §5.2 axis-registries row | axis registry format | artefact exists | axis-registry/v1 named as format precedent; quality-mark vocabulary adopted; scope distinction stated (framework's own instance, not this registry) | -| 10 | §10 table and closing line | all eight rows | as walked above | canon-status column added; provenance typing removed from the stands-without-qualification list; v5.5.0 filings added as named canon the PRD now stands on | -| 11 | §4.1 provenance row | provenance typing | **not in canon** | row re-marked as holding-note vocabulary; UNVERIFIED — Emil review note added with the two resolution paths | - -## 1.5 What was checked and not edited - -- The PRD's uses of "delivery" — all in canon's term sense; the term-collision repair requires - nothing here. -- Emil's rulings embedded in the PRD header — settled; untouched. -- The PRD's remaining "verify at G-1" annotations (ratatui, GitHub Models, LogMap-class tooling, - reasoner mechanics) — Step 2's work, not Step 1's. -- The fallback column of §10 — never applied; no construct was rejected. - -**GATE 1 — closed** (Emil, 2026-08-17). Commit `077790d` stands; both structural amendments -accepted as canon's improvement on the draft. Rulings recorded: - -1. **Provenance typing: the track owns it, pending the wave.** The value set does not file - upstream from this session; the owning session is the queued Q25/Q27/Q30 filing wave (Q27's - trust-decision mechanism is the backing institutional provenance needs). The G-track SHACL - shapes own the value set as track vocabulary; filed as track decision `g-dec-01` - (`docs/g-track/decisions/g-dec-01-reading-vocabulary.yaml`) with a `revisit_if` pinned to the - wave. The finding is forwarded to the wave as evidence — its Q27 filing now has a named - consumer waiting on it. The same disposition covers the Reading tuple's other unfiled halves - (Q11 three-tuple, assurance-on-reading); one decision file carries all of it. The PRD's §4.1 - is marked down from "canon's provenance typing" to "track vocabulary, candidate for the Q27 - wave" — the mis-pin corrected in the artifact, not just the report. -2. **Status line: projection for the human, orthogonal for the machine.** §6.2 keeps the - four-state projection (a status line is a projection for a human arrangement mid-act; compact - and absorbable beats complete); halt logic and the act log run on the orthogonal values; a - future detail view displays the orthogonal triple raw. Recorded in the PRD at §6.2. - ---- - -# Step 2 — verification worklist (V1–V9) - -**Method.** Every row verified against live primary sources on 2026-08-17 (release feeds, official -docs, source code at named commits), per the session prompt's discipline: asserted-from-knowledge -claims in the PRD treated as unverified regardless of confidence. Web verification ran through four -parallel research passes; local verification ran against the product-cli workspace at `d506ac9`. -Consequence vocabulary per the prompt: none / edit / open-item answered / new risk. **PRD edits are -marked, not applied** — Step 1 applied its edits on the prompt's instruction; Step 2's edits hold -for the Gate 2 ruling, and scope cuts are proposed, never applied. - -**Access limit, stated first.** The corpus repos are visible to the account -(`acme-app/android`, `acme-app/backend`, `acme-app/ios`) but this session cannot -attach them: cross-tier `add_repo` is refused (session sources are Hafeok-tier). Per the prompt, no -public repo was substituted. V3, V4 and V5 therefore split into a verified public/local half and a -**blocked repo-dependent half**; the blocked checks are enumerated at the end of this section with -the ask. - -## 2.1 Summary - -| # | Item | Verdict | Consequence for the PRD | -|---|---|---|---| -| V1 | Oxigraph | **verified — holds**, one new risk | open item 8 answered (embedded half); new risk: workspace pin 0.4 is a superseded line; CONSTRUCT-fixpoint performance undocumented | -| V2 | ratatui | **verified — holds** | open item 12 (first half) answered; edit: drop the "asserted from knowledge" flag | -| V3 | kotlin-lsp | **public half verified — the trouble row, as expected**; repo half blocked | new risk: typeHierarchy not implemented; AGP import experimental, silent below Gradle 8.8; fwcd fallback is end-of-life; scope cut proposed below | -| V4 | SourceKit-LSP | **capability half verified — holds**; build/index half blocked | edit: iOS extraction requires a macOS runner with the client's Xcode and a completing build; no Linux path (confirmed) | -| V5 | C# adapter | **local half verified — two wiring gaps**; live-run half blocked | edit: `definition` and `typeHierarchy` are not wired (additive work); Roslyn-server typeHierarchy support unverified | -| V6 | GitHub Models / Copilot API | **verified — closes negative** | open item 9 answered: GitHub Models retired 2026-07-30; **Scaleway-only**, per the PRD's own fallback | -| V7 | Scaleway structured outputs | **verified — holds** | open item 10 ready: three rungs with structured outputs + tool calling confirmed; Emil picks | -| V8 | Ontology matching | **verified** | open item 11 (matching half) answered with recommendation: reimplement the small lexical+structural core in Rust; LogMap as offline calibration oracle | -| V9 | OWL-RL at projection build | **verified — options established** | open item 11 (reasoner half): three credible options ranked; ruling stays Emil's; architecture indifferent, as the PRD stated | - -## 2.2 Rows in detail - -### V1 — Oxigraph (verified 2026-08-17) - -**Checked:** crates.io release history, repo activity, README conformance statement, Store API -docs, published benchmarks, issue tracker. **Found:** healthy and active — latest 0.5.9 -(2026-06-18), eight releases in twelve months, ~monthly cadence; effectively one maintainer (Tpt) -with commercial sponsors — bus-factor ~1 is the honest risk signal. SPARQL 1.1 Query, Update and -Federated Query implemented, "nearly fully conformant" per README, preliminary SPARQL/RDF 1.2 -draft support; no documented conformance gap list. Full quad/named-graph support (`insert_named_graph`, -GRAPH clauses, dataset management) — the PRD's named-graph-per-source design is covered. -Embeddability is not a hypothesis: `product-core/Cargo.toml` already carries -`oxigraph = "0.4"` driving the pf rule engine. **New risk:** the 0.4 line is superseded — last -0.4.11 (2025-05-21), 0.5.0 shipped 2025-09-13 as a breaking line; staying on 0.4 means no fixes -since May 2025. Upgrade to 0.5.x is a G0 task (API migration; 0.5.9 added thread-safe -Transaction/BulkLoader). **CONSTRUCT performance:** no CONSTRUCT-specific or rule-fixpoint -benchmark exists anywhere in Oxigraph's published material; the README's standing caveat is -"SPARQL query evaluation has not been optimized yet"; BSBM (~35M triples, SELECT-heavy, run on -0.2/0.3) is the closest proxy; known blank-node CONSTRUCT correctness issue (#220). At registry -scale (thousands to low millions of triples) this is a watch-item, not a blocker — V9's options -hedge it. **Consequence:** open item 8 answered for the embedded store; edit marked (upgrade note, -§3 serving row); new risk logged (0.4 pin; fixpoint perf undocumented). - -### V2 — ratatui (verified 2026-08-17) - -**Checked:** crates.io, repo governance, licence, widget ecosystem. **Found:** the live successor -of tui-rs (forked 2023, org-governed, ~22.3k stars); latest 0.30.2 (2026-06-19); licence **MIT**. -The §6.1 pipeline's needs map to shipped pieces: prompt window — `ratatui-textarea` v0.9.2 -(2026-06-12), adopted into the ratatui org, or `tui-input` 0.15.4 (2026-08-10) for single-line; -panels, status line — core `Layout`/`Block`/`Paragraph`. Production users include gitui, atuin, -yazi, bottom. **Consequence:** open item 12's framework half answered; edit marked — §3 TUI row -drops "asserted from knowledge; verify at G-1". - -### V3 — JetBrains kotlin-lsp (public half verified 2026-08-17; repo half blocked) - -**Checked:** the actual repo (`Kotlin/kotlin-lsp`, read-only mirror), RELEASES.md, source at HEAD -`bd8bca2` (2026-08-16) including the Gradle importer and the registered LSP providers, -kotlinlang.org docs, fwcd fallback repo and issues. **Found:** - -- Still **Alpha** (README badge and warning at HEAD); latest release v262.9593.0 (2026-07-27); - weekly pre-alpha builds; JDK 25 required (platform builds bundle a runtime); headless launch - first-class (`bin/intellij-server`; `kotlin-lsp.sh` deprecated). Apache-2.0 binary, still - partially closed-source (IntelliJ/Fleet proprietary parts). -- **AGP import is now native but explicitly experimental** (moved from community workaround to - in-tree importer ~April 2026); no compatibility matrix exists. Source-level evidence: Android - variant collection **silently returns when Gradle < 8.8** — a client repo below that imports - with no Android variants and no error; in-repo Android fixtures use AGP 9.1.0. -- **Operations: five of six.** definition, hover, references, documentSymbol, workspaceSymbol all - registered. **typeHierarchy is not implemented** — API scaffolding exists, no provider is - registered (inferred from source and feature-list absence; flagged as inference). Call - hierarchy exists; type hierarchy does not. -- **Fallback assessment: fwcd/kotlin-language-server is end-of-life.** Self-declared deprecated - in favour of kotlin-lsp; last release 1.3.13 (2025-01-18); no typeHierarchy either; known-broken - Android classpath resolution (generated R classes, AGP variant blindness). Not a viable Android - fallback; plain-JVM repos only. - -**What breaks in the PRD's phasing.** §5.2's `rdfs:subClassOf` row names typeHierarchy as its LSP -operation; for Kotlin that operation does not exist, so the highest-assurance hierarchy row cannot -be extracted as specified. §5.4's "AGP support is the flag for the Android repo specifically" -is still the right flag and still cannot be cleared from public information: whether the Android -repo's Gradle/AGP versions clear the ≥ 8.8 floor is the **blocked repo-dependent check**. - -**Narrowest scope cut, proposed not applied:** G0 runs **C# + Swift**; Kotlin joins when two -conditions clear: (a) the Android repo's Gradle version is ≥ 8.8 and kotlin-lsp demonstrably -imports its variants (one `bin/intellij-server` run against the repo answers it), and (b) the -Kotlin `subClassOf` extraction path is settled — either synthesised from -implementation/typeDefinition/references plus declaration-text (the C# adapter already slices -declaration text, so the pattern exists in-house), or deferred until kotlin-lsp ships -typeHierarchy. Cross-codebase comparison (§5.3) degrades gracefully: agreement analysis over two -codebases still surfaces contradictions, minus the third leg's triangulation. The cut is Emil's. - -### V4 — SourceKit-LSP (capability half verified 2026-08-17; build/index half blocked) - -**Checked:** repo at HEAD `7b387eb` (2026-08-13) — server capabilities in -`SourceKitLSPServer.swift`, background-indexing docs, BSP docs; swift.org and Xcode release notes; -xcode-build-server. **Found:** **all six operations implemented, typeHierarchy included** -(prepare/supertypes/subtypes). Ships with Swift toolchains (current stable Swift 6.3, Xcode -26.4). The index story splits by project type: SwiftPM projects get **background indexing on by -default since Swift 6.1** — no user build required; xcodeproj/xcworkspace apps (the realistic -shape of a client iOS repo) are **not natively supported** — they need the BSP shim -(`xcode-build-server`, macOS-only) and **a completing Xcode/xcodebuild build is a hard -precondition**: references, workspaceSymbol and typeHierarchy read IndexStoreDB over the unit and -record files the build writes, and go stale until rebuilt. "The index materialises" concretely -means: index-store unit/record files exist and IndexStoreDB serves occurrences over them. **No -Linux path for iOS-SDK code — confirmed** (by absence, flagged as such: the iOS SDK exists only -inside Xcode; xcode-build-server is macOS-only). **Consequence — edit marked:** §5.4's Swift row -precondition sharpens from "must build first" to "must build first, on a macOS runner with the -client's Xcode version, through xcode-build-server for an xcodeproj app; the extraction pipeline -cannot run iOS extraction in a Linux container". The **blocked repo-dependent check**: the actual -iOS repo's project shape (SwiftPM vs xcodeproj, Xcode version) and a real build+index run — both -need the repo and a macOS machine. - -### V5 — the M-track C# adapter (local half verified 2026-08-17 at `d506ac9`; live-run half blocked) - -**Checked:** `ddd-lsp/src/adapter/csharp.rs`, `ddd-lsp/src/host.rs`, every real (non-mock) -`request(` site in the ddd stack, `docs/ddd-v1-spec.md`. **Found:** the adapter drives -`roslyn-language-server` (official prerelease .NET global tool, pinned expectation 5.11.0, -readiness on `workspace/projectInitializationComplete`, `csharp-ls` documented as fallback; -host fragility is a *standing risk* in the spec's own words). Of the declaration-level six, the -stack **issues four**: documentSymbol (revdiff, intercept, MCP lang-tools), workspace/symbol, -references, hover (plus signatureHelp/rename/diagnostic beyond the set). **Gaps: `definition` is -never issued anywhere, and typeHierarchy is neither issued nor capability-declared.** Both ride -the generic `host.request(method, params)` layer, so the wiring is additive, not structural. -Unverified: whether `roslyn-language-server` answers typeHierarchy at all — needs a live probe. -**Consequence — edit marked:** §5.4's C# row "extractor precondition: none" becomes "wiring: -definition + typeHierarchy requests to be added; Roslyn typeHierarchy support to be probed". -The **blocked repo-dependent check**: the six operations returning real results on -`acme-app/backend` (needs the repo plus a .NET installation for the host). - -### V6 — GitHub Models / Copilot CLI API (verified 2026-08-17) - -**Checked:** docs.github.com/en/github-models, GitHub changelog, Copilot docs, plans and billing -pages, Copilot SDK repo, ToS pages. **Found:** **GitHub Models was fully retired on 2026-07-30** -— playground, catalogue, inference API and BYOK all gone, for existing customers included -(closed to new customers 2026-06-16; brownouts July 16/23). GitHub's stated alternatives: -Microsoft Foundry for API access, Copilot for GitHub-integrated workflows. Copilot itself exposes -**no supported OpenAI-compatible chat-completions endpoint**: the official surfaces are the -Copilot CLI programmatic mode and the Copilot SDK (GA 2026-06-02) — agent-session runtimes billed -in AI credits, not a `/chat/completions` with `json_schema` that the PRD's provider layer could -consume; community Copilot proxies are reverse-engineered and unsupported. **Consequence — open -item 9 answered by the PRD's own fallback clause:** "if no or unclear, Scaleway-only and the open -item closes that way". It closes that way: **Scaleway-only at G0**; a second provider, if wanted -later, is a new evaluation (Microsoft Foundry being the successor GitHub itself names). Edit -marked: §3 model-access row and §12 item 9. - -### V7 — Scaleway structured outputs (verified 2026-08-17) - -**Checked:** Scaleway Generative APIs docs — OpenAI compatibility, structured outputs, function -calling, supported models, per-model cards, pricing, rate limits, data privacy. **Found:** -OpenAI-compatible at `https://api.scaleway.ai/v1`; structured outputs in strict `json_schema` mode -and tool calling documented as supported across the chat catalogue, with per-model cards -confirming; EU (Paris) processing, zero-retention by default, no extraterritorial exposure — -the PRD's GDPR rationale holds. Rate limits at verified identity: ~1–2M tokens/min, 600 -requests/min. **Three candidate rungs, each with structured outputs + tool calling + parallel -tools confirmed on its model card:** - -| Rung | Model | Size | Price €/1M in/out | -|---|---|---|---| -| 1 — small instruct | `pixtral-12b-2409` (literal ≤12B) — or `gemma-4-26b-a4b-it` / `qwen3.6-35b-a3b` on the active-params reading (4B/3B active) | 12B · 26B(A4B) · 35B(A3B) | 0.20/0.20 · 0.25/0.50 · 0.25/1.50 | -| 2 — mid coding | `qwen3-coder-30b-a3b-instruct` | 30B (A3B), 128k ctx | 0.20/0.80 | -| 3 — best open-weight coding | `glm-5.2` (docs: best open-weight for long-horizon/coding at release, June 2026); cost runner-up `deepseek-v4-flash-0731` | frontier, 256k ctx | 1.80/5.50 · 0.40/0.80 | - -Caveat: `gpt-oss-120b` tool calling requires the Responses API — avoid for the ladder. First 1M -tokens free; no production-use restriction. **Consequence:** open item 10 is decision-ready — -verified candidates on the table, Emil picks the rungs. - -### V8 — ontology matching (verified 2026-08-17) - -**Checked:** LogMap repo and commit history, OAEI 2025 results, AML/Matcha/MELT/LLM-matcher -landscape, Rust crate coverage, the 2026 stable-matching result. **Found:** LogMap is actively -maintained (commits to 2026-08-07, OAEI-2025 participant) and invocable as `java -jar` — but it is -built for the wrong problem here: its differentiator is logic-based repair over rich OWL -axiomatisation, worthless on shallow code-extracted ontologies; its lexical layer leans -biomedical and does nothing for camelCase/snake_case identifiers; the 2021 binary is stale -(build-from-source + JVM in every deploy target). At registry scale (hundreds of classes, -10⁴–10⁵ pairs) the big-ontology machinery buys nothing — runtime is a non-issue either way. No -Rust ontology-matching crate exists; every ingredient does (strsim/rapidfuzz, petgraph, -horned-owl, oxigraph already in-workspace, thesaurus for WordNet). Evidence bound: a -lexical+structural core with stable-matching extraction reaches F1 0.832 on Anatomy -(arXiv:2605.09184) — and signal weights barely matter once extraction is stable matching. -**Consequence — open item 11's matching half answered, recommendation:** reimplement the small -lexical + structural core as rules in Rust (~1–2 weeks: identifier-aware normalisation, -Jaro-Winkler + token-Jaccard + TF-IDF, domain synonym table, one round of neighbourhood -propagation, stable-matching 1:1 extraction with per-signal score breakdown for the review UI); -run LogMap **once, offline, as a calibration oracle** for thresholds. An optional LLM-judge pass -over top-k candidates (+2–3 days) is where the field's marginal quality now comes from and fits -the per-triple review stage naturally — a later choice, not a G0 need. - -### V9 — OWL-RL at projection build (verified 2026-08-17) - -**Checked:** the sidecar landscape (owlrl, Jena, RDFox, EYE, HyLAR, Soufflé, Nemo, Rust crates) -and the CONSTRUCT-rules prior art. **Found — the options, ranked, no measurement taken:** - -1. **`reasonable`** (Rust crate 0.4.4, 2026-05-28, BSD-3) — OWL 2 RL materialiser, the only - maintained in-process no-new-runtime option; caveat: not 100% of RL rules — the supported - subset must be checked against the entailments §5.2 needs (subclass transitivity, domain/range, - inverses, property chains). Integration cost: low (Cargo dependency). -2. **CONSTRUCT-to-fixpoint on Oxigraph** — zero new dependency; reuses the exact shape of the - workspace's existing `pf::sparql_rules`; the OWL 2 RL/RDF rule table translates mechanically - (SPIN OWL-RL is the prior art). Caveats: no published fixpoint performance data (V1) and the - blank-node CONSTRUCT issue (#220). Integration cost: low, rules vendored in-repo. -3. **Nemo** (knowsys, Rust, v0.10.1 2026-08-01, MIT/Apache-2.0) — Datalog engine with native RDF - I/O, library or `nmo` subprocess; RL rules written once in its dialect. Integration cost: - low-medium. - -Sidecars dragging in runtimes (Python owlrl 7.6.2 — slow but now with Oxigraph-store -integration; Jena 6.2.0 — JVM and no stock OWL CLI, custom wrapper needed; RDFox — commercial, -Samsung-owned, licence-gated) are all viable but strictly worse fits for a Rust workspace. EYE, -HyLAR, Soufflé, whelk-rs: not credible here. **Consequence:** open item 11's reasoner half has -its options established with integration costs; the architecture is indifferent, as the PRD -stated (inference is a projection concern) — the ruling stays Emil's, and options 1+2 are the -two that add zero runtimes. - -## 2.3 Blocked checks — the ask - -Cross-tier `add_repo` refuses the corpus repos from this session. The blocked checks, exactly: - -| Check | Needs | Discharged by | -|---|---|---| -| V3: Android repo's Gradle and AGP versions against the ≥ 8.8 silent floor; one kotlin-lsp import run confirming variants resolve | `acme-app/android` | a session seeded with the repo as an initial source (version read), plus any machine with JDK/`bin/intellij-server` for the import probe | -| V4: iOS repo project shape (SwiftPM vs xcodeproj), Xcode version; a completing build + index materialisation; the six operations returning real results | `acme-app/ios` **and a macOS machine with the client's Xcode** | Emil's machine, or a macOS runner; not any Linux session | -| V5: the six operations returning real results on the backend solution via `roslyn-language-server` | `acme-app/backend` + .NET installation | a session seeded with the repo, or Emil's machine | - -Options: (a) Emil authorises fresh sessions seeded with the acme-app repos as -initial sources for the two non-macOS checks; (b) the checks run on Emil's machine at G0 entry; -(c) both — the version reads via (a) now, the build-dependent runs via (b). The session makes no -choice. - -## 2.4 PRD edits marked by Step 2 (held for the Gate 2 ruling, not applied) - -| PRD section | Edit | -|---|---| -| §3 substrate, serving row | Oxigraph verified (0.5.9, 2026-06-18); note the workspace's 0.4 pin as superseded — upgrade at G0; drop "both store candidates asserted from knowledge; verify at G0" | -| §3 substrate, TUI row | ratatui verified (MIT, 0.30.2; `ratatui-textarea` for the prompt window); drop "asserted from knowledge" | -| §3 substrate, model access row | GitHub Models retired 2026-07-30; Scaleway-only; second-provider slot closes (Microsoft Foundry named as the successor evaluation if ever wanted) | -| §5.2 subClassOf row | for Kotlin, typeHierarchy does not exist in kotlin-lsp; extraction path synthesised or deferred (per the Gate 2 ruling on the scope cut) | -| §5.4 adapter table | C# row: definition + typeHierarchy wiring to add, Roslyn typeHierarchy to probe. Swift row: precondition sharpened — macOS runner, client's Xcode, xcode-build-server for xcodeproj, no Linux path. Kotlin row: Alpha; AGP experimental; Gradle ≥ 8.8 silent floor; JDK 25; no typeHierarchy; fwcd fallback end-of-life | -| §12 open items 8, 9, 10, 11, 12 | 8: answered (Oxigraph holds; 0.5 upgrade). 9: closed Scaleway-only. 10: candidates verified, Emil picks. 11: options established (V8/V9 above), Emil rules. 12: ratatui verified (framework half) | - -**GATE 2 — closed** (Emil, 2026-08-17). Commit `19058a7` stands. Rulings recorded: - -1. **V3 scope cut accepted as proposed.** G0 runs C# + Swift; Kotlin joins on both conditions — - Gradle ≥ 8.8 with one confirmed variants import, and the `subClassOf` path settled, **preferring - the synthesis route** (implementation/typeDefinition/references + declaration-text slicing, the - C# adapter's existing pattern) over waiting for kotlin-lsp typeHierarchy: no G0 leg sits on - JetBrains' roadmap. The n=2 qualifier recorded in §5.3: two-codebase agreement is evidence, not - the three-root triangulation upgrade. -2. **Blocked checks: route (c), both.** Emil authorises fresh sessions seeded with - `acme-app/android` and `acme-app/backend` for the Gradle version-read and the Roslyn - typeHierarchy probe (discharging V3 condition (a) and V5's live probe). The iOS build (V4) runs - on Emil's machine at G0 entry — **a named G0-entry precondition**, not a session task. -3. **§2.4 PRD edits: leave granted and applied**, including the Oxigraph 0.5 upgrade filed as - **G0 task one** (dead-line pin = live stale-ground finding, treated with that priority); open - item 9 closed Scaleway-only; V8's recommendation adopted (Rust lexical/structural core, LogMap - once-offline as calibration oracle, F1 0.832 evidence bound cited). -4. **Rungs and reasoner both ruled now.** Ladder: `gemma-4-26b-a4b-it` (small-rung winner on - active-params pricing for a coding task; pixtral excluded as vision-oriented) / - `qwen3-coder-30b-a3b` / `glm-5.2`, plus `deepseek-v4-flash-0731` as cost-alternate fourth point; - prices captured in the PRD at 2026-08-17 — prices are ground with a drift rate. Reasoner: - CONSTRUCT-fixpoint on Oxigraph primary, `reasonable` named fallback, `revisit_if` on - projection-build wall-time at G0 scale — filed as track decision `g-dec-02`; the no-benchmark - hedge is discharged by measuring at G0. - -PRD open items now answered or ruled: 8, 9, 10, 11, and 12's framework half (§12 marked in place). - ---- - -# Step 3 — registry repository skeleton - -The skeleton is staged at `docs/g-track/registry-skeleton/` on this branch — reviewable in the PR -diff, pushed to **no new repository**: the session creates nothing under any org before the Gate 3 -ruling. On the ruling, the directory transplants verbatim to the new repository's root (per its -`STAGING.md`). - -## 3.1 What is staged - -| Path | Content | Prompt requirement | -|---|---|---| -| `README.md` | one page: structure, ratification path (proposal branches → per-triple PR review → Emil merges → endpoint rebuilds on merge), validation, ownership placeholder; cites the PRD rather than restating it | step 3.3 | -| `STAGING.md` | the transplant note; the two placeholders (org/repo name, base IRI) and the one gate ruling (file format) | — | -| `graphs/canonical/_exemplar.ttl` + `_exemplar.yaml` | the same single assertion in both candidate per-triple forms — **Turtle or YAML is Emil's ruling at the gate**; one assertion per file, per the per-claim file pattern | step 3.1 | -| `shapes/reading.ttl` | the §4.1 Reading tuple constraint: value/as-of/provenance/assurance required; provenance from the g-dec-01 value set; **`institutional` requires `trust_decision`** (SPARQL constraint) | step 3.2 | -| `shapes/structural.ttl` | structural well-formedness (assertion names exactly one subject/predicate/object, IRIs where required) | step 3.2 | -| `.github/workflows/validate.yml` | SHACL on every change (pySHACL) plus a one-assertion-per-file check; grows at G0 | step 3.2 | -| `scripts/build-projection.sh` | stub — ref in, store build out; inference hook carries the g-dec-02 ruling (CONSTRUCT-fixpoint primary, `reasonable` fallback, `revisit_if`) instead of the pre-ruling TODO | step 3.4 | - -`proposals/` is documented as **branch-based, not directory-based** in the README — no directory -exists, deliberately. - -## 3.2 Verified before the gate - -The staged CI logic was executed locally against the skeleton (pySHACL 2026-08-17): - -- the Turtle exemplar parses, carries exactly one assertion, and **conforms** to both shapes; -- the negative case — an `institutional` Reading with no `trust_decision` — **fails** the SHACL - gate, as §4.1 requires. - -## 3.3 Held for the Gate 3 ruling - -1. **Host org and repository name** (ownership = the accountable-principal field; answers PRD open - item 7; the base IRI derives from it). -2. **Per-triple file format** — Turtle (validates as staged) or YAML (the per-claim pattern's - literal shape; adds a YAML→RDF step to CI at G0). Both exemplars sit side by side for the - comparison. - -**GATE 3 — closed with a revised ruling** (Emil, 2026-08-17), replacing the ownership question -with a better type: - -1. **The skeleton is a template, not a repository.** No repository was created in this session. - The directory finalised as `docs/g-track/registry-template/` — a parameterised template for - *any* registry: owning organisation, repository name, base IRI (with the durability - requirement documented, no host picked), ratifier, and display name are **generation - parameters**, supplied when an instance is generated. Instance generation is a **G0-entry - step** beside the iOS build. -2. **The template is versioned (0.1.0), and instances record it.** Every generated instance's - first commit fills `GENERATION.ttl`: template version, parameters supplied, generated-by, - date — `prov:wasAttributedTo` the generation act — which is what lets instances re-pin when - the template moves, by this session's own discipline. -3. **The founding-decision slot is scaffolded** (`graphs/canonical/founding-decision.ttl`, empty - by design): the owning organisation's decision to keep a registry, filed by its ratifier as - the first ratified content. The template provides the slot; only the instance can fill it. -4. **PRD open item 7 re-typed, not answered**: ownership is a generation parameter, answered per - instance. The PRD is edited accordingly (§3 authority row, §11 G0 row, §12 item 7). -5. **Format ruled: Turtle.** The authority file is the assertion; SHACL reads it directly; zero - conversion in CI. The YAML exemplar is deleted. The per-claim pattern transfers (one assertion - per file, PR review, supersession) — the format was never the load-bearing part. - -Applied: `registry-skeleton/` → `registry-template/` with `TEMPLATE.md` (version, parameter -table, generation procedure), parameterised README (`{{OWNER_ORG}}`, `{{RATIFIER}}`, -`{{DISPLAY_NAME}}`; base-IRI placeholder kept IRI-valid so the template validates as-is), -`GENERATION.ttl` and founding-decision slots, CI updated (slot exempt from the one-assertion rule -while empty), YAML exemplar and `STAGING.md` removed. CI logic re-run locally after the rework: -exemplar conforms; the slot parses at zero triples and is exempt; template conforms. - ---- - -# Step 4 — close - -The PR against product-cli carries the manifest; this section records the close state. - -**Deliverables on the branch:** the edited PRD (`prd-ground-as-ontology.md`), this report, the -track register (`decisions/g-dec-01`, `g-dec-02`), and the registry template -(`registry-template/`, version 0.1.0). No canon repo was touched; no repository was created. - -**Status line: G-1 discharged; G0 is buildable.** Nothing is unpinned. The G0-entry checklist -(Gates 2–3 rulings): generate the first registry instance (parameters supplied then); the iOS -build on Emil's machine; the Oxigraph 0.5 upgrade as task one; the seeded-session discharges for -V3 (Android Gradle read) and V5 (Roslyn typeHierarchy probe). - -**GATE 4 — Emil merges.** diff --git a/docs/g-track/kotlin-lsp-workspace-import-2026-08-17.md b/docs/g-track/kotlin-lsp-workspace-import-2026-08-17.md deleted file mode 100644 index dca2ffaf..00000000 --- a/docs/g-track/kotlin-lsp-workspace-import-2026-08-17.md +++ /dev/null @@ -1,510 +0,0 @@ -# kotlin-lsp workspace import — V3(a).4, the final G0-entry open item - -**Session type:** verification. Reports, does not rule. **Date:** 2026-08-17. -**Ruling authority:** Emil. Nothing here changes the PRD; §11's two branches are -both stated and neither is chosen. - -> **Naming.** The client repositories are `corpus-android` and `corpus-backend` -> throughout. Domain identifiers are described structurally. Paths inside the -> corpus are written `/…`. - ---- - -## 0. The answer, up front - -**The import starts.** Under the handshake the reference client actually sends, -kotlin-lsp v262.9593.0 begins a real Gradle/AGP workspace import of -`corpus-android` within ~0.2 s of `initialized`, injects its own Gradle init -script, applies its own tooling plugin, configures the included build, resolves -the plugin classpath (AGP 8.13.0 among it) and proceeds to per-module, -per-variant task realisation. - -It did **not complete** in this environment. It ran 238.8 s and failed at -dependency resolution for **one third-party vendor SDK artifact** whose Maven -repository this sandbox cannot reach (`CONNECT tunnel failed, response 502`). -That artifact exists on no other reachable repository. This is an **environment -limit, precisely located** — not an AGP-experimental limit, not a corpus fault, -and not the handshake. - -So V3(a).4 splits cleanly into two findings that should not be merged: - -| Sub-question | Status | -|---|---| -| *Can the import be made to start at all?* — the actual open item | **Discharged. It starts.** The two earlier handshakes were wrong in transport and in option schema; the reference client's handshake works first time | -| *Does the import run to completion on `corpus-android`?* | **Not demonstrated here.** Blocked by one unreachable vendor Maven repository. Blocker named exactly; not simulated | - -A reduced-corpus control — the same corpus with the single module carrying that -vendor dependency removed — separates "the mechanism cannot work" from "this -sandbox cannot reach one host". **It imports successfully**, fetches an -`AndroidProject` model, and `workspace/symbol` goes from **empty to populated** -across the import boundary. Details in §4. - -So the mechanism is demonstrated end to end; what is undemonstrated is one -network hop. That is the shape of the remaining uncertainty, and it is -deliberately not resolved into a recommendation here. - ---- - -## 1. The reference client's handshake, as extracted - -**Source.** `kotlin-server-0.0.6-linux-amd64.vsix`, downloaded 2026-08-17 from -`download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/`, -**SHA-256 `90974cd8…687ec` verified against the published `.sha256`**. This -digest is new to the G-track record; the prior session verified only the -standalone archive (`2d99d8e1…7cc5e`, re-verified here and unchanged). - -`Kotlin/kotlin-lsp` `RELEASES.md`, fetched 2026-08-17, still lists -**v262.9593.0** at the top — the pin is unchanged since 2026-07-27. - -**Method — capture beat inference where it could.** The extension ships -`extension/out/dist/extension.js.map` **with `sourcesContent` populated**, so the -handshake was read off the extension's *original TypeScript* -(`src/lspClient.ts`, 396 lines) and its bundled **`vscode-languageclient` -9.0.1** (`lib/common/client.js`), not off minified output or documentation. -Two independent confirmations then came from the server side (§1.3, §3.1). - -### 1.1 Transport — the first thing both earlier probes had wrong - -The extension does **not** run the server over stdio. From -`src/lspClient.ts:163-210, 220-264, 266-291`: - -``` -spawn: /server/bin/intellij-server --socket 0 [--system-path ] - ↳ read stdout until a line contains "Server is listening on …:" - ↳ TCP-connect 127.0.0.1:; that socket is both reader and writer -``` - -`--socket 0` means "bind an ephemeral port and announce it". The launcher's -port announcement is a **stdout side-channel that exists only in socket mode**. -Timeouts the extension applies: 60 s for the announcement, 10 s to connect, -100 ms retry delay. - -### 1.2 `initializationOptions` — the second thing they had wrong - -From `src/lspClient.ts:319-342`: - -```jsonc -{ - "defaultSdk": , // string path | null - "buildTools": { "": } -} -``` - -`buildTools` is a **map keyed by workspace-folder URI**, one entry per folder. -Per the extension's own `package.json` contribution, `intellij.buildTool` is -*"A build tool to use for a server, workspace, folder"* with **`null` = any** -(auto-detect) and `""` = none. Default is `null`, i.e. the shipped default -**does** import. - -### 1.3 Confirmed against the server's own type - -`javap -p` on `com.jetbrains.ls.snapshot.api.impl.core.InitializeOptions` -(extracted from `server/lib/product.jar`) gives the wire contract exactly: - -```java -private final java.nio.file.Path defaultSdk; -private final java.util.Map buildTools; -private final java.nio.file.Path indexDir; -private final boolean skipWorkspaceCreationForTests; -private final LibraryRootsMode libraryRootsModeForTests; -``` - -Five fields, three usable. This settles a loose end from the prior probe: the -options it invented — `{importWorkspace: true, gradle: {enabled: true}}` — are -not merely ineffective, they are **not in the schema and are silently -discarded**. That is why the earlier log's only occurrence of "gradle" was the -probe's own options echoed back. - -The sibling class `InitializeKt` carries the diagnostics -`No build tools selected for `, `No applicable build tools found for ` and -`Unknown build tool '` — the three ways `buildTools` can decline to import. - -### 1.4 The rest of the initialize envelope - -From `vscode-languageclient` 9.0.1 `lib/common/client.js:791-830` plus -`workspaceFolder.js:32-41`: - -- `processId: null` (**not** the client's real pid) -- `clientInfo: {name, version}`, `locale`, `trace` -- `rootPath` **and** `rootUri` both set to the first workspace folder -- `workspaceFolders: [{uri, name}]` filled by the workspace-folders feature -- **`progressOnInitialization: true`** (`lspClient.ts:323`) ⇒ the client mints a - UUID, sends it as **`workDoneToken` inside `initialize`**, and attaches a - progress part to it. This is how server-side startup progress is reported - *during* `initialize`. -- `documentSelector` covers schemes `file`, `jar`, `jrt`, and adds language - `java` alongside `kotlin` (`lspClient.ts:293-317`). - -**No post-`initialize` notification triggers the import.** There is none in the -extension. The import is driven from inside the server's `initialize` handler — -confirmed by the stack trace in §3.1. - ---- - -## 2. What was run, on what - -| | | -|---|---| -| Corpus | `corpus-android` @ **`67340f69`** — unchanged, and **verified pristine (`git status --porcelain` empty) after every run** | -| Server | `intellij-server` **build 262.9593.0** (`product-info.json`), taken from the **vsix's own bundled server**, i.e. the exact binary the reference client launches; bundles JBR 25.0.2 | -| Route | **Minimal harness**, not the shipped host layer — see §5 for why the host layer cannot express this handshake today | -| Host | Linux, JDK 21 on host (irrelevant — the server uses its bundled JBR 25), Gradle 8.14.3 via the corpus's own wrapper, Android SDK platform 36 + build-tools 36.0.0 installed to a scratch dir, `ANDROID_HOME` exported, **no `local.properties` written into the corpus** | - -The harness (`probe.py`) reproduces §1 faithfully: socket transport with port -announcement parsing, the reference `initializationOptions`, `workDoneToken` in -`initialize`, a full VS Code capability block, and — importantly — it **answers -server→client requests** (`workspace/configuration`, `workspace/workspaceFolders`, -`window/workDoneProgress/create`, `client/registerCapability`) rather than -leaving them hanging. - -**Two false starts of my own, recorded so they are not mistaken for findings:** - -1. The harness first stripped `JAVA_TOOL_OPTIONS`, which in this sandbox carries - the proxy CA truststore. The import started and immediately died on - `SSLHandshakeException … PKIX path building failed` fetching the Gradle - distribution. **My fault, not kotlin-lsp's.** Restored, the distribution - downloaded cleanly (137 MB). -2. The harness first passed a **relative** `--system-path` while running with - `cwd` = corpus root, so the server created a scratch directory inside - `corpus-android`. Caught, removed, path made absolute, and the corpus - re-verified clean. No corpus file was ever modified. - ---- - -## 3. What the import actually did - -### 3.1 It starts — timeline, run 3, full corpus - -| t | event | -|---|---| -| 1.5 s | port announced, socket connected | -| 1.5 s | `initialize` sent | -| 1.5–2.3 s | `$/progress` on **my `workDoneToken`**: "Initializing server" → "Initializing IntelliJ" → "Starting indexer" → "Opening project database" → "Loading workspace model from cache" → end | -| 2.3 s | `initialize` returns | -| 2.3 s | `initialized` sent | -| **2.33 s** | **`$/progress` begin, title "Importing project"** — server-minted token | -| 2.35 s | `"Importing folder "` | -| 2.5–5.5 s | downloads `gradle-8.14.3-bin.zip` per the corpus's wrapper (137 MB) | -| ~13 s | compiles `/tmp/lsp-gradle-init*.gradle`; **applies `com.jetbrains.ls.imports.gradle.IdeaGradleLspPlugin`** | -| 25–31 s | compiles `settings.gradle.kts` (CLASSPATH, then BODY) | -| ~90 s | resolves AGP **8.13.0** and the plugin classpath | -| ~127 s | builds the corpus's included `build-logic` build | -| ~168 s | realises per-module, per-variant tasks across the module set | -| 182–240 s | resolves the projects' own dependency graphs | -| **241.1 s** | `"Run build failed"` → `window/logMessage` with the stack → `"Workspace is not imported"` | -| **241.2 s** | **`$/progress` end** on the import token — duration **238.8 s** | - -The importer is `com.jetbrains.ls.imports.gradle.GradleWorkspaceImporter.importWorkspace` -called from `com.intellij.ls.server.requests.core.InitializeKt$importWorkspaceFromFolder` -(`initialize.kt:240`) — i.e. **import is part of `initialize`**, per folder, exactly -as §1.4 predicted from the client side. - -### 3.2 Why it failed — the exact `Caused by` chain - -``` -LocationAwareException: Execution failed for task ':app:dataBindingMergeDependencyArtifactsDevelopDebug' - → TypedResolveException: Could not resolve all files for configuration ':app:developDebugCompileClasspath' - → ModuleVersionResolveException: Could not resolve - → ResourceException: Could not get resource '/…/.pom' - → HttpErrorStatusCodeException: Received status code 502 from server: Bad Gateway -``` - -Independently confirmed at the network layer: `curl` to that host returns -`CONNECT tunnel failed, response 502` — the sandbox's egress proxy will not -tunnel to it. The artifact is **absent from Maven Central, JitPack and Google's -Maven** (404 on all three), so no substitution is available. The same host was -already recorded unreachable in the prior session. - -### 3.3 A finding that matters independently of the network - -**kotlin-lsp's import executes Gradle tasks; it does not merely configure.** The -failure is in *task execution* (`:app:dataBindingMergeDependencyArtifacts…`) -resolving a **compile classpath**. - -This retires an inference in the current PRD text. The prior session's control — -`./gradlew :app:tasks --all` enumerating all six variants with no failures — was -read as "the project is not the obstacle". That control **only configures**; it -never resolves a dependency graph, a point the prior report itself made. The -kotlin-lsp importer does resolve, so **the control was not equivalent to the -import** and could not have predicted this outcome either way. The PRD's §5.4 -row should not continue to lean on it as evidence that import will succeed. - ---- - -## 4. The reduced-corpus control - -To separate *mechanism* from *reachability*, the import was re-run against a -**copy** of `corpus-android` with the single module carrying the unreachable -vendor dependency excluded from `settings.gradle.kts`. `corpus-android` itself -was untouched (copy made outside it; original verified pristine). - -This is a **reduced corpus, not the corpus** — stated plainly so the result is -not over-read. It retains the design-system and data layers, which is where the -sealed hierarchies and the value-object identity types that G0 cares about -actually live. - -### 4.1 The import completes - -| t | event | -|---|---| -| 2.14 s | `$/progress` begin "Importing project" | -| ~124.5 s | `Fetch model 'com.jetbrains.ls.imports.gradle.model.AndroidProject' for project scope succeeded` | -| 124.7 s | `Run build succeeded` | -| 125.6 s | **`Successfully imported folder `** | -| **127.0 s** | `$/progress` **end** — duration **124.8 s** | -| 126.9 → 189.6 s | a **second** phase: `Indexing`, **62.8 s**, `Processed 181 471 files` | -| **189.65 s** | notification **`intellij/ready-for-test`** | - -Two things follow. - -**The AGP path is exercised, not just plain Gradle.** The importer fetches its -own `…imports.gradle.model.AndroidProject` model. This is the sub-check the PRD -records as undemonstrated — "variants import *through kotlin-lsp*" — and on a -fully-resolvable corpus of this shape it holds. - -**Success and failure are distinguished only by the last `report` message before -`end`.** `Successfully imported folder …` versus `Workspace is not imported`. -The `end` frame itself is identical, and **no error is returned to any request**. - -### 4.2 The extraction surface, before and after - -Same server, same file, same position — the only difference is whether the -import had finished. - -| Operation | During import (t + 20 s) | After import + indexing (t + 600 s) | -|---|---|---| -| `workspace/symbol` | **0 results** | **returns the declaration** | -| `documentSymbol` | works | works | -| `hover` on a sealed subclass | `data object NoIcon : BadgeIcon()` | identical | -| `prepareTypeHierarchy` | **`null`** | **`null`** | - -Three conclusions, each load-bearing: - -1. **The import is what unlocks `workspace/symbol`.** Before: empty. After: - populated. The prior session's `workspace/symbol("…") → array[0]` was a - correct reading of a server that had never imported — not evidence about the - corpus. -2. **The empty answer is silent.** No error, no flag, well-formed empty array. - Identical in shape to the advertised-then-null `typeHierarchy` trap. This is - the concrete justification for R8. -3. **The import does not unlock `typeHierarchy`.** `typeHierarchyProvider: true` - is still advertised and `prepareTypeHierarchy` still returns `null` with a - **fully imported and indexed workspace**. PRD §5.2's Kotlin row stands exactly - as written, and the `hover`-based synthesis route remains the only route. - -### 4.3 What the control does and does not license - -It licenses: *the handshake is right; kotlin-lsp's Gradle/AGP importer works on -a corpus of this shape, at this Gradle/AGP/Kotlin version, on this host; and a -completed import demonstrably unlocks the workspace-scoped surface.* - -It does **not** license: *`corpus-android` as it stands imports.* One module was -removed to get here. Whether the full corpus imports remains **untested**, and -on this evidence the only known obstacle to testing it is network reachability -of one private repository — not the tool. - ---- - -## 5. What an adapter must do differently — requirements - -Stated as requirements, not code. Every one is a gap against `ddd-lsp` as -merged at `e830c57`. - -**R1 — Socket transport with port-announcement handshake.** -`ddd-lsp/src/client.rs` is stdio-only by construction ("A live language-server -child spoken to over stdio"; `LspClient::spawn` pipes stdin/stdout). A Kotlin -host needs: spawn with `--socket 0`, read the child's **stdout** for -`Server is listening on …:`, then connect a TCP socket and speak LSP over -it. Transport therefore has to become a per-adapter property. - -**R2 — stderr must stop being discarded.** `spawn` sets `stderr(Stdio::null())`. -The launcher reports startup faults there. Keep it, at least to a note buffer. - -**R3 — Per-adapter `initializationOptions`.** `host.rs::initialize` sends no -`initializationOptions` at all — there is no field for them on `Adapter`. Kotlin -requires `{defaultSdk, buildTools: {: null}}`. The `buildTools` key -**must be the folder URI in the same spelling as `workspaceFolders[].uri`**. - -**R4 — Send `workDoneToken` in `initialize`.** Otherwise server startup progress -is unaddressed. Cheap, and it is what the reference client does. - -**R5 — `processId: null`.** The host layer currently sends -`std::process::id()`. The reference client sends `null`. Harmless-looking, but -some servers shut down when the announced pid dies; match the reference. - -**R6 — Answer `workspace/configuration` with real values, and implement -`workspace/workspaceFolders`.** Today `workspace/configuration` is answered with -an array of `null`s and `workspace/workspaceFolders` is **not handled at all** — -it falls through to `-32601 unhandled`. A server that re-reads its build-tool -choice through configuration would get "none" from a null. This is a latent -correctness bug for any host that asks. - -**R7 — Readiness must be a progress-token lifecycle, not a notification name.** -This is the substantive one. kotlin-lsp signals import completion by **ending -the `$/progress` token whose `begin.title` is `"Importing project"`**, and puts -the verdict in the **last `report` message before that `end`** — `"Workspace is -not imported"` on failure. There is no `workspace/projectInitializationComplete` -equivalent. - -`ReadySignal` cannot express this today: -- `Notification("$/progress")` is satisfied by the *first* progress message, - which arrives ~0.2 s in, while the import has 4 minutes to run. -- `NotificationWhere("$/progress", …)` reads `ServerState::last_params`, which - keys **by method**, so every one of the hundreds of progress messages - overwrites the previous. The single `end` frame would have to be caught in the - exact polling window, and interleaved tokens (an "Indexing" token runs - concurrently) would collide. - -Worse, readiness here is **two-phase**. The import token ending is *not* -sufficient: a separate `Indexing` token then ran **62.8 s** over 181 471 files, -and `workspace/symbol` is only reliable after that. Immediately afterwards the -server emits **`intellij/ready-for-test`** — the one plain notification that -marks the whole sequence complete. Its name suggests a test affordance rather -than a supported readiness API, so it should be treated as corroboration, not as -the contract. Note also that after the main indexing token the server emits a -**storm of same-titled zero-duration `Indexing` tokens** (thousands), so "an -Indexing token is open" is useless as a not-ready test. - -Requirement: track progress **per token** — record each `begin` title, retain -that token's most recent `report` message, latch on its `end`. Readiness = -**the import token ended with a success message _and_ the substantial indexing -token ended**. The retained message is the outcome, and a failed import must -surface as **failed**, never as ready and never as merely slow. - -**R8 — Gate every workspace-scoped query on R7, and never treat empty as -absent.** Measured, twice, on two corpora: while the import is running, -`workspace/symbol` returns **`0` results** with no error, while `documentSymbol` -and `hover` on an open file answer correctly. An extractor that queries early -gets a **silent, well-formed empty answer** — the same failure shape as the -advertised-then-null `typeHierarchy` trap already named in PRD §5.2. Absence of -symbols must be distinguishable from absence of an import. - -**R9 — Budget a real timeout.** `REQUEST_TIMEOUT` is 15 s and `Host::request` -gives up at 30 s. Measured here: - -| Run | Import | Indexing | Ready at | -|---|---|---|---| -| Full corpus, cold | 238.8 s → **failed** | — | never | -| Reduced corpus, cold | 124.8 s | 62.8 s | **~190 s** | -| Reduced corpus, warm caches | **19.1 s** | (cached) | **~37 s** (`intellij/ready-for-test`) | - -Readiness is a **minutes-scale** wait on first run and a tens-of-seconds wait -warm. The adapter must not convert either into a per-request timeout, and should -not assume the warm number. - -**R10 — Environment preconditions the adapter cannot fix.** An Android import -needs a reachable Gradle distribution, a JDK the Tooling API accepts, an Android -SDK (`ANDROID_HOME`), and **network reachability for every repository the build -declares** — including private/vendor ones. Failure of the last is indisputably -fatal and, on this evidence, arrives only after minutes of apparently healthy -progress. The adapter should report the retained progress message verbatim -rather than a generic timeout. - -**R11 — Keep the capability discipline (PRD §5.2).** Re-confirmed live at build -262.9593.0, now also *with an import under way*: `typeHierarchyProvider: true` -is advertised and `textDocument/prepareTypeHierarchy` returns **`null`** — -before, during, and after import. The workspace import does **not** unlock it. -`hover` returns the server-rendered declaration header including the supertype -clause, so the synthesis route stands and, as the prior session established, -needs no import at all. - ---- - -## 6. Both branches, without choosing - -Emil rules. Stated as the PRD's §11 conditional requires. - -### 6.1 If Kotlin joins — G0 runs two legs, n = 2 - -- Two roots, C# + Kotlin, with the shared-domain intersection and the n = 2 - qualifier of §5.3 in play; the "should be identical" decision gets the - extractor as its discharge, and Emil ratifies a first shared-domain ontology - by merging, with contradiction count reported. -- `subClassOf` for Kotlin is **synthesised from `hover`'s declaration header**, - never from `typeHierarchy` — which stays advertised-and-null. This route is - confirmed working here both mid-import and independent of import. -- The prior session's two cautions stand unchanged and are the real risk to the - leg's *value*: ~75 % of Kotlin's `subClassOf` edges sit in UI/design-system - layers, and exact-name intersection with the C# side is ~2 %. -- **Cost this session adds:** the Kotlin leg is not "wire up an adapter". It is - R1–R9 — a second transport, per-adapter init options, and a genuinely new - readiness mechanism — plus R10, an operational precondition that no code - change can satisfy. - -### 6.2 If Kotlin does not clear — G0 is single-source - -- The §11 substitute gate applies as written: first ontology ratified per-triple - from one codebase; extractor re-run at a second, later ref with the diff - reported as candidates-added / symbols-disappeared; bootstrapping yield - reported per assurance row; and the "should be identical" decision **filed but - explicitly left undischarged**, with its falsifier named. -- §5.3 does not run at all. - -### 6.3 What this session changes about the choice - -The open item was *"can the import be made to start"*. **It can, and the recipe -is now known and cheap** (§1). What is *not* established is that it **completes -on `corpus-android`**, and the obstacle to establishing that is a private -repository this environment cannot reach — an **arrangement limit of the same -family as the deferred Swift leg (§5.4)**, not a tooling limit. - -That distinction is the session's main contribution to the ruling, and it is -deliberately left as a distinction rather than a recommendation. The one thing -the evidence does rule out is treating V3(a).4 as "kotlin-lsp cannot import -Android": that reading is now false. - ---- - -## 7. The recipe, in one place - -For whoever picks up the adapter work. This is the whole difference between an -import that starts and one that never does. - -``` -1. spawn: /bin/intellij-server --socket 0 --system-path - (absolute path — a relative one lands in the repo being imported) -2. read the child's STDOUT until: "Server is listening on …:" -3. TCP-connect 127.0.0.1:; speak LSP over that socket -4. initialize: - processId: null - rootUri / rootPath: - workspaceFolders: [{uri: , name: …}] - workDoneToken: - initializationOptions: { - defaultSdk: null, // or a JDK path - buildTools: { "": null } // null = auto-detect; "" = none - } -5. initialized ← the import begins here, unprompted -6. answer server→client requests: workspace/configuration, - workspace/workspaceFolders, window/workDoneProgress/create, - client/registerCapability -7. wait for: $/progress token titled "Importing project" to END, - whose last `report` reads "Successfully imported folder …" - (failure reads "Workspace is not imported" — same `end` frame, no error) -8. then wait for the substantial "Indexing" token to END - (corroborated by the `intellij/ready-for-test` notification) -9. only now are workspace-scoped queries meaningful -``` - -Environment, non-negotiable: reachable Gradle distribution, `ANDROID_HOME`, a -JVM trust store that works for every host the build fetches from, **and network -reachability for every declared repository**. - ---- - -## 8. Provenance - -| Artefact | Pin | -|---|---| -| `corpus-android` | `67340f69` — read-only; verified pristine after every run | -| kotlin-lsp / VS Code extension | v262.9593.0 / v0.0.6, `RELEASES.md` fetched 2026-08-17, top entry unchanged | -| `kotlin-server-0.0.6-linux-amd64.vsix` | SHA-256 `90974cd8…687ec`, verified against published digest | -| `kotlin-server-262.9593.0.tar.gz` | SHA-256 `2d99d8e1…7cc5e`, verified; matches the prior session | -| server build | `262.9593.0` per bundled `product-info.json` | -| `vscode-languageclient` | 9.0.1 (extension `package.json`) | -| `product-cli` | `e830c57` (`main`, PRs #43/#44/#46/#47 merged) | -| Gradle / AGP / Kotlin in corpus | 8.14.3 / 8.13.0 / 2.2.10 | - -**Not done, and out of scope:** adapter implementation; any change to -`corpus-android`; the C# full-solution re-confirmation; PRD edits; registry -generation; the extractor. diff --git a/docs/g-track/prd-ground-as-ontology.md b/docs/g-track/prd-ground-as-ontology.md deleted file mode 100644 index f58d3f51..00000000 --- a/docs/g-track/prd-ground-as-ontology.md +++ /dev/null @@ -1,1094 +0,0 @@ -# PRD — Ground as Ontology: Bootstrapping, Entity Surfacing, and the Governed Loop - -**Product:** `product-cli` (Hafeok/product-cli), new track. Working name **G-track**. -**Status:** Draft for Emil's ratification. Nothing here is built. -**Provenance:** Written 2026-08-14 against the ground-axes holding note at revision 13 and canon as -last verified by the related-work gate-pass session (a312a55). Constructs from the holding note are -**unratified** unless the corpus test's canon session has since filed them; the PRD names which it -depends on (§10). Refresh rule: re-derive at a named commit, never hand-patch. -**Re-pinned at G-1 (2026-08-17):** reconciled against live canon — `actor-indexed-determination` -at `110bf10` (v5.5.0, tagged 2026-08-16, plus two post-tag merges), `decision-driven-design` at -`4848b9e` (pin advanced to v5.5.0, DDD-dec-18). The corpus test's canon session has filed: -`DDD-ground-01…04`, `DDD-delivery-01…04`, `core/13-delivery.md` with `term:delivery` / -`term:undelivered` / `term:presumed-discharge`, and `graph/axis-registry.yaml` (axis-registry/v1, -artefact-not-canon). Edits below carry the marker *(re-pinned at G-1)*; the full re-pin table is -in the G-1 session report (`docs/g-track/g1-session-report-2026-08-17.md`). The ground-axes -holding note is context only from this revision on; canon outranks it everywhere they touch. -**Rulings already taken (Emil, 2026-08-14):** existing entities only — the loop never adds to the -ontology; the whole session's output is a review bundle of new ground, new decisions, and code, for a -reviewer; entity extraction runs on prompt submission, not on user request; **the ontology is -bootstrapped by a code extractor run over multiple codebases sharing a domain**, with the reviewer -ratifying per triple; where codebases share a domain the overlap is a shared ontology they import; -**a codebase is a projection of the domain and cannot be its authority — the domain is the decisions -that came before, and the shared-domain ontology is the retro-filed decision set the extractions are -evidence for; "domain" is a lacking term — the ontology reaches for the organisation's ground -registry across all axis layers, of which software's domain is one projection; the registry is not -part of the what-specification — the what starts at the Event Model, and the registry is the space -the what is expressed over; **extraction is model-free** — RDF inference and rules cover what a model -was penciled in for, and Feature 3 is the ladder's rung zero.** - ---- - -## 1. Purpose - -Three features, one substrate. The substrate is the RDF/OWL ontology already validated by product-cli's -SHACL/SPARQL stack, read as the **ground axis registry**: entities are classes, axes are properties -with ranges, regions are class expressions, and subsumption carries specificity. - -**Feature 1 — entity surfacing, as stage one of the TUI.** The product surface is a **TUI with a -prompt window** (Emil's ruling). Composing is stage one: as the user types, recognise entities that -already exist in the registry — model-free, no tokens — show what they entail, and let the user -attach ground and see the governing decisions. When the prompt is ready and its metadata attached, it -is a **declared act**; execution (Feature 2) consumes declarations, never raw text. - -**Feature 2 — the governed loop.** A controlled LLM loop whose only tools are ground reads, decision -reads and proposals, and LSP access to a code graph. Every tool call is an act with a position, so the -governing set is delivered mechanically per call. The session's output is one review bundle — proposed -ground, proposed decisions, code — for a human reviewer to accept, amend, or reject. - -**Feature 3 — bootstrapping by code extraction.** A code extractor reads existing codebases through -their LSP adapters and proposes ontology objects — entities, hierarchies, relations, axis registries — -into a proposal graph the reviewer ratifies per triple. This populates the software layer of the -ground registry (§3, terminology); other layers are out of scope. Run over multiple codebases sharing one -domain, agreement across independent codebases yields higher-assurance ground and disagreement -surfaces the escaped domain decisions that no ratifier ever took. **Feature 3 is G0**: it produces the -code-to-ontology mapping Feature 2's extractors need before their first act. - -**Why build it now.** The loop is the first act-site where the holding note's constructs run live and -generate their own corpus. Every session emits delivered-set-versus-emitted-proxy comparisons, -extraction closure records, and proposed-ground provenance — the evidence that ruling 22 (surfacing -obligation), Q23 (remedy routing), and the hostile corpus test all want and do not have. **The loop -pays for itself in evidence before it pays for itself in code.** That is the primary success criterion -(§9), and the code output is second. - -## 2. Non-goals - -- Ontology growth by the model. Candidate entities the model believes are new are **not** filed - anywhere by the loop — not as intents, not as proposals. They appear in the session's report as - observations for the reviewer, untyped. (Emil's ruling; keeps the registry a controlled vocabulary.) - Feature 3 is not an exception: the code extractor emits a *proposal graph*, never a merge, and the - reviewer's per-triple acceptance is what populates the ontology. -- Resolving cross-codebase disagreement. Where extractions disagree, the extractor reports the - dissent; deciding which model is authoritative is a domain decision filed by the reviewer with a - region and a falsifier. The tooling never picks. -- Populating the ground registry beyond the software layer. Organisational, regulatory, and technical - registries are named in §3 and not extracted here; G0's output is one layer and is labelled as such. -- **What-extraction.** Recovering commands, events, and workflows from code — bootstrapping the Event - Model layer the way G0 bootstraps the registry — is a real future instrument with a different - target and different LSP surface (call graphs, control flow, handlers). It does not ride along. - Behavioural divergence across the corpus codebases is expected (§5.3) and is not a G0 finding. -- The coverage sweep, the demand map, the projection matrix, arrangement profiles, the escalation - queue. The loop is the act-site that generates their input; building them here bundles. -- Any canon filing. Decisions the loop proposes are `[PROPOSED]` in the ledger's existing sense. -- Autonomy. The loop halts on the conditions in §7.6 and never self-approves. - -## 3. Substrate - -**Terminology (Emil's ruling).** "Domain" is used in this PRD in Evans's sense — the business-problem -vocabulary a piece of software addresses — and it is a **lacking term** for what the ontology is. The -ontology reaches for the organisation's **ground registry**: the full set of declared axes across all -layers named in the holding note's §13.9 — domain, technical, organisational, regulatory — plus the -decisions that region them and the trust decisions (Q27) that let ground be supplied by citation. -Software's domain is one ground projection of that registry, and it is the one this PRD bootstraps -first because it is the one extractable from code. Where "domain" appears below, read *the software -layer of the ground registry*. The registry is regioned by decisions; that — not entity coverage — is -what distinguishes it from enterprise-knowledge-graph efforts, which modelled entities without -modelling who decided anything about them. - -**The registry is not the what (Emil's ruling).** The registry declares the space; it holds entities, -relations, and axes, and no acceptance predicates about behaviour. **The what starts at the Event -Model** — the first verdict-bearing layer, where behaviour-decisions are filed in store-pair form -(blueprint as constraint, Given/When/Then as criterion). Code is the how: a projection of the what. -The chain, with the projection-as-source diagnostic applying at every step: - -| Layer | Holds | Decisions at this layer are about | Falsifier subject | -|---|---|---|---| -| **Ground registry** | entities, relations, axes; trust decisions; naming rulings | the space — regioning other decisions | representational fidelity (does `Order` mean one thing) | -| **Event Model (the what)** | commands, events, read models, GWTs | behaviour — verdicts over the space | outcomes (does ordering behave correctly) | -| **Code (the how)** | implementations | realisation of the what | conformance to the what | - -The registry is not decision-free — it holds the trust decisions and the retro-filed shared-domain -set. The split is by subject: **registry decisions region other decisions; what-decisions resolve -behaviour.** - -| Component | Source | Note | -|---|---|---| -| Ontology vocabulary | product-cli's existing RDF vocabulary | **Assumption A1:** the axis registry uses this vocabulary, extended with act-relevant properties, not a separate one. Open ruling 25; the PRD takes the extend position. If ruled otherwise, §7.2 changes and nothing else does. The registry vocabulary gains tick rate as a property of entities and axes (§5.6), carried as a Reading like any other ground. It is not a caching parameter and is not editable outside the proposal path. | -| **Registry authority** | a dedicated **registry repository** per instance, generated from the versioned **registry template** *(re-typed at G-1 Gate 3: `docs/g-track/registry-template/`; the first instance generates at G0 entry with its parameters — owner, name, base IRI, ratifier — supplied then)* | canonical store of ratified triples; per-triple files in Turtle (the per-claim file pattern — the pattern transfers, the format was never the load-bearing part; Gate 3 ruling); ratification by PR merge; supersession, never rewriting; CI runs SHACL on every change; the founding-decision slot is the instance's first ratified content. **Not** inside any codebase and **not** inside the framework canon repos — it is the owning organisation's ground, not the framework's. | -| **Registry serving** | SPARQL endpoint rebuilt from the registry repo at each merge | read-only; **pinned to a ref** — every Reading served carries the ref, giving Q11 as-of semantics with no extra machinery. Local mode: product-cli embeds a triple store (Oxigraph — Rust, SPARQL 1.1, fits the workspace natively) over a pinned clone; works offline. Central mode: the same store or Fuseki/GraphDB in Azure Container Apps behind Entra ID — the O-track stack arriving early. Store choice is swappable because **the endpoint is a projection of the repo, not the source** (Q30's diagnostic, applied to our own database). **Inference runs at projection build**: entailments are recomputable, so they never enter the authority repo — each endpoint rebuild derives the inferred graph for that ref, every entailed triple `prov:wasDerivedFrom` the asserted triples plus the rule id, assurance = the rule. The repo stays pure assertion; inference inherits as-of discipline for free. *(Verified at G-1, 2026-08-17:)* Oxigraph holds — 0.5.9 (2026-06-18), active ~monthly cadence, SPARQL 1.1 Query/Update/Federated, full named-graph support; honest risk signal is a bus-factor of ~1. ~~**The workspace's `oxigraph = "0.4"` pin is a dead line**~~ (last 0.4.11, 2025-05-21; 0.5.0 shipped 2025-09-13 as a breaking line) — **discharged 2026-08-17, before G0 entry**: the upgrade landed as PR #46 (`product-core` now pins `oxigraph = "0.5"`, with `pf::query`, `pf::seed` and `pf::sparql_rules` migrated to the 0.5 API). The stale-ground finding in our own ground is closed; what remains is the standing obligation not to let the pin drift again. *(Ruled at G-1 Gate 2:)* inference mechanism is **CONSTRUCT-to-fixpoint on Oxigraph as primary** — it reuses the `pf::sparql_rules` shape already in the codebase and adds no dependency — with the Rust `reasonable` crate (0.4.4, BSD-3) as the named fallback; recorded as track decision `g-dec-02` with a `revisit_if`: fixpoint wall-time exceeding an acceptable projection-build budget at G0 scale flips to the fallback. The no-benchmark hedge is discharged by measuring at G0, not by choosing conservatively now. | -| Validation | SHACL shapes, SPARQL | as-is; shapes gain the act properties in §4.2 | -| Provenance | W3C PROV-O | `prov:wasDerivedFrom`, `prov:wasAttributedTo`, `prov:generatedAtTime`; named graph per source | -| Decisions | the ledger (L-track) | read for governing sets; write only as proposals | -| Code | LSP-backed language adapters (M-track) | C#, Bicep, Rust, HTML/CSS as they exist; **Swift and Kotlin added for Feature 3** (§5.4) | -| **TUI** | Rust terminal UI in the product-cli workspace — **ratatui, verified at G-1** (2026-08-17: MIT, 0.30.2, live successor of tui-rs; `ratatui-textarea` v0.9.2 in the ratatui org covers the prompt window) | prompt window, enhance panel, ground-state status line, rung and α pickers, bundle summary | -| **Model access** | provider-agnostic layer over OpenAI-compatible chat completions + tool calling + structured outputs | The loop must run on cheap and small models; Claude is not required (Emil's ruling — cost, and the experiment in §7.7). **Scaleway Generative APIs verified 2026-08-14**: OpenAI-compatible with tool calling and structured outputs, EU-hosted (GDPR; data does not leave Europe — relevant for client corpora), catalogue spans small instruct models through current open-weight coding models on one endpoint. *(Verified and ruled at G-1, 2026-08-17:)* **GitHub Models was fully retired on 2026-07-30**; Copilot's official surfaces (CLI programmatic mode, SDK) are agent runtimes, not OpenAI-compatible chat-completions endpoints — the second-provider slot **closes Scaleway-only** per this PRD's own fallback (open item 9). A second provider, if ever wanted, is a fresh evaluation (Microsoft Foundry is the successor GitHub itself names). Scaleway re-verified at G-1: `json_schema` strict mode plus tool calling confirmed per-model across the ladder rungs (§7.7). Structured-output support is load-bearing: §7.5's required emitted-proxy field survives on small models only if the provider constrains output shape. The Copilot SDK is deliberately **not** listed here — it is an agent runtime, not a completions endpoint, and enters as an executor alternative (§7.8). | - -## 4. Data model additions - -### 4.1 Reading - -Every ground reading the loop or the surfacing pass makes is recorded as a three-tuple, per the -holding note's Q11 as amended: - -| Field | Type | Source | -|---|---|---| -| `value` | per axis range | the read | -| `as_of` | timestamp | when read — clock, not context | -| `provenance` | controlled / observed / inferred / institutional | **track vocabulary, candidate for the Q25/Q27/Q30 filing wave** (G-track decision `g-dec-01`; ruled at G-1 Gate 1) *(re-pinned at G-1)* | -| `assurance` | instrument / method / trust-decision ref | at what assurance the reading was made | -| `derivation_confidence` | high / mid / low / not-derivable | *(added 2026-08-20)* how reliably the instrument read — §5.2's first axis. Ordinal | -| `domain_relevance` | high / low / unknown | *(added 2026-08-20)* whether the result is the kind of thing the registry is for — §5.2's second axis. **Not ordinal**: `unknown` is the absence of a verdict | -| `relevance_basis` | computed / defaulted / not-computed / not-applicable | *(added 2026-08-20)* where the relevance mark came from, so an uncomputed mark never reads as a verdict | - -`institutional` provenance requires a `trust_decision` reference (Q27). A reading with `institutional` -and no reference fails SHACL. - -*(Re-pinned at G-1; ruled at Gate 1.)* The PRD as drafted called the four-value set "canon's -provenance typing". The G-1 reconciliation searched both canon repositories at the pinned commits -and found no such typing filed anywhere — the holding note presupposes it (Q12, Q27) but nothing in -live canon carries it. **Emil's Gate 1 ruling:** the value set does not file upstream from this -session; the session that owns it is the queued Q25/Q27/Q30 filing wave, where Q27's trust-decision -mechanism is the backing institutional provenance needs. Until that wave files, the G-track SHACL -shapes own the value set as **track vocabulary**, filed as track decision `g-dec-01` -(`docs/g-track/decisions/`) with a `revisit_if` pinned to the wave — superseded the moment canon -files provenance typing, the track re-pinning by this session's own discipline. The same -disposition covers the Reading tuple's other unfiled halves (the Q11 three-tuple, assurance-on- -reading): track vocabulary, candidate input to the wave, `wasAttributedTo` the G-track. The -finding is forwarded to the wave as evidence — its Q27 filing now has a named consumer waiting. - -Readings served from the registry carry: `as_of` = the endpoint's ref, `provenance = institutional`, -`assurance` = the trust decision by which this arrangement connected (§4.4). Local-clone readings are -identical in form — the ref is the clone's, which makes staleness visible rather than silent. - -### 4.2 Act - -Every tool call in the loop is an act: - -| Field | Content | -|---|---| -| `act_id`, `session_id`, `sequence` | identity and order | -| `tool`, `arguments` | the call | -| `position` | asserted properties over registry axes, each a Reading (§4.1) | -| `unevaluated_axes` | axes the extractor could not evaluate for this act — legible partial delivery (`DDD-ground-01`: non-evaluation must never silently become non-applicability — the field is that clause mechanised) *(re-pinned at G-1)* | -| `governing_set` | decision ids retrieved by position (§7.2) | -| `delivered_form` | how the governing set was rendered into the model's context | -| `emitted_proxy` | what the model states it checked against, captured per §7.5 | -| `outcome` | tool result summary | - -This is the Q23 output contract, logged. The `governing_set`/`emitted_proxy` pair is the mechanisable -comparison — the primary evidence output. *(Re-pinned at G-1:)* the comparison's stake is now canon: -`DDD-delivery-02` files undelivered governance as escape that presents as governance, and -`term:presumed-discharge` names the record state — a pass indistinguishable from a skip — that the -delivered-vs-emitted check exists to break. - -### 4.3 Session bundle - -| Section | Content | Reviewer action | -|---|---|---| -| Proposed ground | new Readings the loop asserts, each with full tuple and `prov:wasAttributedTo` the session | accept into named graph / reject | -| Proposed decisions | ledger entries, `[PROPOSED]`, each carrying the `DDD-ground-01` gate enforced at proposal time — a resolvable applicability predicate or explicit universal applicability, with each implemented axis marked mechanically-evaluable or judgement-evaluable *(re-pinned at G-1: canon amended Q1's named-axis gate to predicate-or-explicit-universal)* — and the acts that motivated them | accept / amend / reject through the ledger's existing path | -| Code changes | diff, with each hunk linked to the act(s) that produced it and their governing sets | ordinary review | -| Observations | candidate entities and anything the loop believed was missing from the registry — untyped, for the reviewer only | read; file by hand if warranted | -| Halts | every halt (§7.6) with its reason and what was escalated | resolve or dismiss | -| Evidence | act log (§4.2) in full; summary table of delivered-vs-proxy | none required; it is the corpus | - -### 4.4 Connecting is a trust decision - -"Opt to connect" is Q27's filing, literally, not by analogy. Configuring an arrangement to use the -central registry files a trust decision — *rely on this registry for ground of these layers* — with -the organisation as the accountable principal behind it. The config entry is typed as that decision -and referenced by every institutional Reading the arrangement makes (§4.1). An arrangement that has -not opted in reads only its local ground. Revoking the trust decision is supersession, and every -inference standing on registry readings inherits the revocation through the assurance field — the -propagation problem (holding note ruling 27) gets its mechanism here: the reference is explicit, so -the affected set is a query. - -Multi-organisation registries (Context& serving several clients, each with their own registry) are -out of scope; one registry, one organisation, one ratifier. - -## 5. Feature 3 — bootstrapping by code extraction (G0) - -### 5.1 Behaviour - -For each configured codebase, run the code extractor through the language's LSP adapter and emit a -**proposal graph** — a named graph, `prov:wasAttributedTo` the extraction run, `prov:generatedAtTime` -the run, and pinned to the codebase's commit ref. Proposal graphs land as **branches of the registry -repository**: per-triple files on a branch, so per-triple acceptance is PR review line by line and -ratification is the merge — the mechanism the registry already has, not a second one. Nothing merges -without the reviewer. Accepted triples enter the canonical graph or a shared-domain ontology (§5.3); -the endpoint rebuilds on merge. - -### 5.2 What is extracted, at what confidence, at what relevance - -*(Revised by the two-axis session, 2026-08-20, implementing the G0 session report's top-ranked -finding §7.1 — with §7.4 arriving at the same change from the review side. G0's mapping-rule -verdicts were **re-confirmed over the full solution** in that session; the provisional qualifier -below is discharged.)* - -**The assurance column was two properties wearing one name, and the corpus proved them -independent.** 31 assertions sat at the top grade with every mechanical check passing, the -instrument having read the code perfectly, and every one of them wrong to ratify — private -serialisation state on infrastructure types. 18 more, generated schema-change code, failed the same -way across four rows and all three grades. So the mark splits in two: - -- **Derivation confidence** — how reliably the instrument read the code. Ordinal: - `high` (declared — a probed operation returned the thing itself) · `mid` (structural or inferred — - a rule took a step the language does not) · `low` (heuristic — a lexical rule, no server semantics - behind it) · `not-derivable` (no instrument on this surface). **The value set is unchanged from - the old column, deliberately:** that column was already measuring derivation and nothing else, so - this is a rename rather than a re-marking, and no ratified assertion changes. -- **Domain relevance** — whether the result is the kind of thing the registry is for: - `high` · `low` · `unknown`. **Not ordinal.** `unknown` is the absence of a verdict, not a value - between the other two, which is why it carries a **basis**: `computed` · `defaulted` · - `not-computed` · `not-applicable`. - -**Relevance is `unknown`, basis `not-computed`, on every row at G0, and the table says so.** The -remedies §7.2 names — generated-code attributes, a generated-file header, visibility — are all -outside the declaration-level six, and widening that subset is a separate filed decision. This -column is therefore **declared-empty, not undeclared**: it exists, its value set is stated, and every -row honestly reports that nothing computed it. That is a different and better state than the single -column it replaces, where relevance was undeclared and silently read as high — the distinction -`DDD-ground-02` draws, arriving in our own instrument. An axis defaulting to a plausible `high` -would read as *computed*, which is `term:presumed-discharge` a second time inside the one instrument -built to prevent it. - -**What separates the two rejected groups is therefore not the pair but the derivation record**, and -that record is the half that discharges the forcing argument — see *Derivation is recorded, in the -evidence layer* below. - -| Code structure | Proposed ontology object | Derivation confidence | Domain relevance (basis) | Derivation | LSP operations | -|---|---|---|---|---|---| -| Classes, records, entities, aggregates | `owl:Class` candidates | high — declared | `unknown` (not-computed) — the language says it is a type; nothing in the subset says it is a *domain* type, and both rejected groups are perfectly ordinary declared types | mapping rule | documentSymbol, workspaceSymbol | -| Inheritance, interface implementation | `rdfs:subClassOf` | high — declared | `unknown` (not-computed) — whether a taxonomy edge is a domain taxonomy or a framework one is not in `typeHierarchy`'s answer | mapping rule | typeHierarchy — *(ruled at G-1 Gate 2; re-verified live at G0-entry, 2026-08-17:)* **C# needs no synthesis** — Roslyn 5.11.0 answers `prepareTypeHierarchy` / `supertypes` / `subtypes` correctly on production declarations, so this row stays fully declared for C#. **kotlin-lsp v262.9593.0 advertises `typeHierarchyProvider: true` and then returns `null`** — the gap is a silent empty result, not a capability denial or an error (see *Capability flags are claims* below). Where a server lacks it, the hierarchy is **synthesised**; for Kotlin the route is cheaper than assumed — `hover` returns the server-rendered declaration header including the supertype clause (`data object NoIcon : BadgeIcon()`), so no raw declaration-text slicing is needed and the route works without a workspace import. Note the C# adapter carries the *technique* (LSP symbols + declaration-text slicing), **not** an existing subClassOf implementation to port; budget a new component. No G0 leg sits on JetBrains' roadmap | -| Typed properties | datatype and object properties with ranges | high — declared | `unknown` (not-computed) — **the measured case**: 31 assertions here at top confidence, every check passing, all wrong to ratify. *(Full solution, 2026-08-20: the same class is 195 assertions, and its 31 ratified members are now recoverable by one query over `reg:memberKind`.)* **Open defect:** range resolution matches on **simple name**, which G0 measured safe at 0 cross-module collisions on the three-project subset. At full solution the corpus carries **21** colliding simple names, so the row now conflates types where they collide. `definition` is wired, answers, and is consumed by no row — it is the exact-resolution route, and spending it is the fix | mapping rule | documentSymbol (hover refines) — *(measured on production C#, 2026-08-17:)* `documentSymbol` alone returns property **types and nullability** inline (`Id : ProfileId`, `DisabledReason : int?`), so `hover` refines rather than enables this row | -| **Foreign keys, relational structure** | object properties between entities | **not-derivable** — no instrument on this surface | `unknown` (**not-applicable**) — the row proposes nothing, so no assertion carries a mark; the pair is stated so the row reads as *no instrument* rather than *no relevance* | **not extracted at G0** | **none.** *(Measured 2026-08-17:)* the probed C# codebase declares no navigation properties and no FK attributes — relational structure lives in EF Fluent-API configuration and generated migrations, outside the declaration surface the extractor reads. Entity declarations of this shape therefore yield datatype properties with value-object ranges and **no object properties**. Migrations and Fluent configuration are named here as a **separate potential source at lower assurance — structural, not declared — and are out of G0 scope**. The row is kept rather than dropped so the gap is a stated limit, not a silent one | -| Composition and reference | relations | mid — inferred from usage | `unknown` (not-computed) — relevance rides on the two endpoints, and both are themselves `unknown` | SPARQL CONSTRUCT rules to fixpoint over raw reference edges | references, definition | -| Namespaces, modules, bounded contexts | domain axis registries (holding note §13.9's layers) — proposed axes carry the axis-registry/v1 quality mark, resolvable / nameable, with an extractor sketch per resolvable axis *(re-pinned at G-1: the format precedent is `decision-driven-design/graph/axis-registry.yaml`, seeded at v5.5.0, artefact-not-canon, 22 axes — the framework program's own instance, not this registry; its promotion path — a validator reads it, plus a ratification act — is the discipline G0's proposal graphs inherit)* | mid — structural | `unknown` (not-computed) — a project name is a fact about code arrangement; whether it names a bounded context is a domain reading nobody has taken | mapping rule + RDFS entailment | documentSymbol — *(corrected 2026-08-20:)* this cell read `workspaceSymbol`, and a full-solution run measured otherwise: on the run where `workspace/symbol` did not answer, the row still fired **1 514** assertions, because module containment is read off `documentSymbol`. On the three-project subset both operations answered, so the divergence between table and implementation could not surface | -| Naming, comments, string usage | synonyms, `skos:altLabel` | low — heuristic | `unknown` (not-computed) — a spelling of an identifier; §7.8 already suspects this row's stated output is mis-specified, and a relevance verdict would be a second guess on a first | lexical rules; model optional, default off | hover, textual | - -**Capability flags are claims, not verdicts — probe the operation, never the advertisement -(Emil's ruling, G0 entry).** This is an **extractor-wide discipline**, not a Kotlin footnote. -An LSP server's `InitializeResult` capability set is a statement of intent by its author; the -only evidence that an operation works is a live call returning a usable result. Both failure -directions were observed on the same day, 2026-08-17: - -- **Over-advertising.** kotlin-lsp declares `typeHierarchyProvider: true` and returns `null`. - *(Second case, measured 2026-08-20 on two live full-solution runs:)* **Roslyn advertises - `workspaceSymbolProvider: true` and answered `workspace/symbol` on one 16-project run and timed - out on another, minutes apart, against the same corpus at the same ref.** So the operation is not - missing — it is *intermittently unavailable under load*, and the nuance the Kotlin case does not - carry is this: **a probe verdict is itself a reading, scoped to its run.** An operation can be - recorded unavailable on one run and available on the next, so a row gated on it would yield - different results on two runs that differ in nothing a reviewer can see. The discipline is - unchanged — probe, never trust the advertisement — but the probe's own output must be read as - run-scoped rather than as a standing fact about the server, and it must be taken against the - workspace actually being read. Neither the three-project subset nor a toy workspace loads the - server enough to show this. -- **Under-advertising.** `csharp-ls` does not advertise `typeHierarchyProvider` at all. - *(Corrected 2026-08-20, owed from the G0 session report §3.)* This bullet previously cited - **Roslyn**, which never under-advertised as a server: measured under two client handshakes — with - and without `textDocument.typeHierarchy` declared — it advertises `typeHierarchyProvider: true` - **both times**. The entry probe's observation was about the *client* capability our adapter - omitted, which is a different pair from the one the probe compares. The direction is real and - `csharp-ls` is the case that carries it; until a server demonstrates it live, the fixture is what - holds the row. - -Over-advertising is the dangerous one, and it is **exactly the shape of presumed discharge** -(`DDD-delivery-02`, `term:presumed-discharge`) reappearing inside our own instrument: a -capability advertised, a gate that reads as passed, and zero edges extracted while the run -reports success. Left unguarded it does more than lose data — the missing subclass edges would -reach §5.3's cross-codebase comparison, which would read the **absence as divergence** and -manufacture a finding out of a silent tool failure. Ground would be fabricated by an -instrument fault, which is the one outcome this track exists to prevent. - -Therefore, binding on every adapter: **each of the six declaration-level operations is -capability-probed at adapter start-up against a known declaration, and an operation that -advertises support but returns null or empty is recorded as unavailable for that language.** -The probe result, not the flag, gates the mapping rules; an unavailable operation makes its -row's absence *reported* rather than silently empty, so a reviewer can tell "this codebase has -no hierarchy" apart from "the instrument did not answer". Whatever the fourth language turns -out to be, it enters under this same discipline. - -**A probe verdict is itself a Reading, and the capability discipline and the Reading discipline -are the same mechanism.** *(Ruled 2026-08-20, on the measurement above.)* An operation that answers -on one run and times out on the next, over the same corpus at the same ref, is not a server whose -capabilities changed — it is a reading taken at an instant. So a verdict carries the §4.1 tuple like -any other: a **value** (what the call returned), an **as-of** (when), a **provenance** (`observed` — -a live call, never a flag), and an **assurance** (the probe, named). Two consequences, binding: - -- **Every probe verdict is recorded in the run evidence**, beside the derivation facts, with its - as-of and the advertisement it agreed or disagreed with. Two runs that disagree then differ - *visibly* rather than mysteriously, and a reviewer can tell **"this row found nothing"** from - **"this row was gated off that day"** — a distinction that is otherwise unrecoverable, and the - same class of distinction the four-outcome row report exists to preserve. -- **A single unavailable verdict is not final.** An operation recorded unavailable after one timeout - is a reading at very low assurance being treated as a verdict, and a row gated on it becomes - non-reproducible across runs differing in nothing a reviewer can see — which defeats the point of - gating. The honest instrument is **retry-then-record**: retry on a non-answer, and record the - attempts with the verdict so its assurance is visible. *Open: the shape of the retry — how many, - how spaced, and how the attempts are written — is proposed at the next gate, not built here.* - -**Mapping-rule verdicts above were provisional; they are no longer.** G0 measured them against a -**three-project subset** of the C# corpus — the projects that restore without the private package -feed — leaving four projects and the API host unloaded. The two-axis session (2026-08-20) re-ran -the extractor at the same corpus ref against the **full solution** on a machine with feed access, -16 of 16 projects restoring, ≈16.7× the source files. **Every row's behaviour was confirmed; no row -behaved differently at full scale.** Counts are larger, as they must be; the mapping verdicts stand -as written. The per-row confirm-or-correct table is in that session's report. - -**Extraction is model-free (Emil's ruling).** The pipeline is: deterministic LSP reads, per-language -mapping rules, RDFS/OWL-RL entailment (subclass transitivity, domain/range propagation, inverses, -property chains), CONSTRUCT rules iterated to fixpoint for usage-inferred relations, and SHACL plus -disjointness axioms for the cross-codebase contradiction surfacing of §5.3. Cross-codebase entity -alignment (Swift `Client` vs C# `Customer`) uses **ontology matching** — lexical plus structural -similarity — producing *candidates* for per-triple review, which is where judgement was always going -to sit. *(Verified and ruled at G-1, 2026-08-17:)* the lexical/structural core is **reimplemented as -rules in Rust** (identifier-aware normalisation, Jaro-Winkler + token-Jaccard + TF-IDF, a domain -synonym table, one round of neighbourhood propagation, stable-matching 1:1 extraction with -per-signal score breakdown for the review UI; every ingredient exists as maintained crates, and the -evidence bound is F1 0.832 on OAEI Anatomy for exactly this recipe, arXiv:2605.09184). LogMap is -alive (OAEI-2025 participant) but built for rich axiomatised ontologies and biomedical lexicons — -wrong problem at this scale; it runs **once, offline, as the calibration oracle** for the Rust -core's thresholds, and never ships. The model's residual role is naming suggestions on the lowest-assurance row, and it defaults off. - -**Why this does not trip canon's retirement of "closed predicates make intelligence unnecessary":** -extraction is **constructively closed** — the verdict is *computed* by rule; there is no candidate -generation step to make cheap or expensive. The loop's code edits are the opposite case: acceptance -is checked mechanically but candidates are searched, which is exactly why Feature 2 needs a model and -Feature 3 does not. Constructive versus verification closure is the crisp answer to "when is a model -needed at all". - -The declared rows are why extraction closes as a predicate for the top half: the language semantics -say what a class is and the LSP returns it. **What that closure does *not* buy is relevance**, and -G0 measured the difference: the declared rows are where the largest single block of rejections sat, -and it sat there at the top confidence. Review does not earn its keep only on the inferred rows. -Every proposed triple carries **both** marks as a Reading (§4.1), so acceptance can be graded on the -pair rather than wholesale — and batched by it, which is §7.4's volume answer as well as §7.1's -correctness one. - -**Derivation is recorded, in the evidence layer.** *(Added 2026-08-20.)* Two marks are not enough on -their own: they say what a class of assertion *is*, not how a reviewer *finds* it again. Group A — -generated code — was recoverable after the fact through `reg:evidence` paths; **Group B was not -recoverable at all**, because nothing in a ratified assertion recorded that its property came from a -member the server reported as a *field*. So every assertion now carries a derivation record: - -| Fact | What it makes recoverable | -|---|---| -| `reg:derivedByRule` | the named rule inside the row — a row is too coarse to batch on | -| `reg:standsOn` | the probed operations the assertion's facts came from | -| `reg:memberKind` | **Group B, as one query** — property, field, or enum member, as reported, never as judged | -| `reg:containerKind` | the kind of declaration the assertion is about | -| `reg:sourcePath` | **Group A, as one query** — the typed form of what `reg:evidence` carries mixed with type names | - -The principle: **record the facts, and let the reviewer's query — not the extractor's rule — do the -classification.** Nothing here filters either group, and nothing widens the LSP subset; the -predicate over paths that names generated code is the reviewer's to write. - -**And the predicate is a real choice, so both readings are recorded here rather than rediscovered.** -Measured on the ratified cohort, against the generated-migration group G0 rejected: - -| Predicate over `reg:sourcePath` | Recovers | -|---|---| -| **every** path under the generated folder | 14 of 18 — the four it misses are `usage-construct` edges whose second endpoint is an ordinary type | -| **any** path under the generated folder | 18 of 18 — G0's group exactly | - -Neither is wrong. `every` asks *is this assertion wholly about generated code*; `any` asks *does this -assertion touch generated code*. A relation between a generated type and a domain type answers no to -the first and yes to the second, and which a reviewer wants depends on what they are about to do -with the batch. The extractor records the paths; it does not choose. - -These facts live in the **per-source evidence layer** — one graph per extraction run, pinned to the -corpus ref, under `runs/` in the instance — keyed by the assertion's own content-addressed id, -leaving the canonical graph pure ratified assertion. Three consequences, each mechanised in template -0.3.0: - -- **Identity is untouched.** `g-dec-04` hashes the triple and nothing else, so the marks and the - derivation cannot make a ratified assertion into a fresh proposal. Verified by fixture, not assumed. -- **The cohort ratified before the split becomes auditable.** The extractor is deterministic and the - ref is pinned, so re-running at the ref an assertion was extracted from regenerates its derivation - exactly. Not one byte of ratified content changes. Axis 1 backfills by entailment - (`reg:assuranceGrade owl:equivalentProperty reg:derivationConfidence`); axis 2 is `unknown` for - everything at G0, so its absence on the older generation reads correctly. -- **The join is checked, and validated where it is read.** An entry either names an assertion the - graph holds or it is an orphan, reported per run. The shapes run over `graphs/` ∪ `runs/` ∪ - `vocabulary/`, because that is what a consumer reads — and because a shape keyed on a *predicate* - would pass pre-entailment and fail post-entailment. The 0.3.0 shapes therefore target the class - `reg:GradedAssertion`, which the writer stamps and nothing entails. - -**What this does not do.** It does not filter Group A. It does not filter Group B. It does not widen -the declaration subset. It makes both groups **visible and batchable**, and it stops the table -claiming a property nobody computed. Filtering needs the widened subset, which is the separate filed -decision §7.2 calls for. - -**The extractor uses the declaration-level LSP subset only** — documentSymbol, workspaceSymbol, -typeHierarchy, references, definition, hover. Not completion, not refactoring, not diagnostics. This -is what keeps adapter maturity risk (§5.4) off the extractor. - -**The extractor's own limit, stated so G0 is not misread.** A codebase projects only the decisions -that reached software. Pricing rules never implemented, organisational structure, procurement -constraints, the reason a field exists — none of it is in LSP output. The code extractor therefore -bootstraps **one layer** of the ground registry, and §5.3's "unfiled decisions" are the ones that -reached code but were never filed. Decisions that never reached code are not unfiled — they are -invisible to this instrument entirely. This is the holding note's §1.2 (the declared-space limit) -applied to the extractor: complete relative to the codebases, silent about every layer that has no -projection in them. Other layers are populated by other extractors or by filing, out of scope here. - -### 5.3 Multiple codebases, one domain - -Emil's case: one app, backend in C#, iOS in Swift, Android in Kotlin, one domain. Run the extractor -over all three and compare the proposal graphs on **graph structure in ontology terms**, never on -source text — which is why the languages need not match and a fourth costs an adapter and nothing -else. - -| Agreement | Reading | Output | -|---|---|---| -| Present in all codebases, same structure | triangulated ground; assurance upgraded per the Q11/Q27 amendments **if roots are independent** | candidate for the shared-domain ontology | -| Present in a majority, absent or different in one | candidate with a named dissent | reviewer decides; the dissent is a finding, not a merge conflict | -| Backend-only | server concern, or a divergence | reviewer classifies | -| Client-only | UI state, or a divergence | reviewer classifies | -| Contradictory relations across codebases | **an escaped domain decision in the wild** — no ratifier said which model is authoritative and each team resolved locally | reviewer files the decision; region and falsifier | - -**A codebase is a projection of the domain, not an authority for it (Emil's correction).** The -domain is the decisions that came before — most unfiled, some escaped, made when the domain was -designed and every time a team resolved a divergence locally. The extractor recovers *evidence* of -those decisions from three projections; it does not recover the decisions themselves. Where the -projections agree, they are jointly evidence of an unfiled decision. Where they diverge, they are -evidence that no decision was made — or that one was made and never delivered to two of the three -teams. Asking which repo is authoritative is asking which shadow is the object. - -**Shared domain as an ontology object — correctly typed.** The intersection of the proposal graphs is -*not* the shared domain; it is evidence for it. The shared-domain ontology is the **retro-filed -decision set** the intersection supports: the reviewer names and ratifies it as an ontology in its own -right, each triple `prov:wasDerivedFrom` the code extractions and `prov:wasAttributedTo` the -ratification — and **the ratification is the decision, not the extraction**. Each codebase's domain -`owl:imports` it and adds local extensions. That makes "the client and backend models are one model" -a **testable statement**: the shared domain is a SHACL shape, and each codebase conforms or does not. -Consequence: when a fourth codebase appears with a different `Order`, the question is not "which of -four wins" but "does the filed decision govern this projection, and did it arrive there" — a delivery -question, not an authority question. - -**"They should be identical" is a decision, and Feature 3 discharges it — scoped to the registry -layer.** Without the registry/what split (§3), any legitimate behavioural difference between clients -would falsify the identity decision: the iOS app and the backend *do* different things, and should. -With it, the decision's region is precisely the registry layer — entities and relations identical; -behaviour expected to diverge per client. File it with that region and a falsifier (any client-only or -backend-only *entity or relation* not declared a local extension). -It is a **retro-filed** decision in `DDD-ground-04`'s sense *(re-pinned at G-1: ratified at v5.5.0, -replacing the holding note's §13.4 as the authority)* and carries its two fields: when the gap was -uncovered, distinct from when the act occurred; and that it was retro-filed. Without the fields, -retro-filing launders escape into coverage; with them it is the ledger-side discharge mechanism for -the escape generators (Emil's v5.5.0 Gate 4 ruling), and the retro-filing act is a claim-layer act — -attributable, falsifiable by one act in the ungoverned interval, subject to claimant calibration. -Re-running the extractor at each ref is -the mechanical check that discharges it — registry divergence becomes a shape violation with a -decision behind it, not a bug report. **Behavioural divergence is not a violation of this decision.** -The declaration-level LSP subset (§5.2) extracts almost only registry-layer structure, so the -extractor and the decision's region are aligned — now on purpose rather than by accident. - -*(Ruled at G-1 Gate 2 — the n=2 qualifier; **corpus composition re-ruled at G0 entry, -2026-08-17**: the pairing is **C# + Kotlin**, with Swift deferred as an arrangement limit and -re-entering as the third root — §5.4.)* Where G0 runs two codebases, two-codebase comparison -still surfaces contradictions, but the independence-triangulation assurance upgrade **weakens at -n=2**: agreement between two independent roots is evidence, not the three-root upgrade. The -upgrade's full form applies from the third root onward. - -**Two further consequences of n=2 with a backend and one client**, measured on the actual corpus -at G0 entry. First, **the intersection is narrower than over a backend and two clients**: the -naive exact-name intersection across the two repos is **23 type names** against 942 declared C# -types and 1090 Kotlin ones (~2% of either side), which is the concrete reason the lexical-plus- -structural matching of §5.2 — not exact-name matching — is the load-bearing step. Second, the -third and fourth rows of the table above **lose their discriminating power**: with two clients, a -backend-only element can be classified by whether *both* clients lack it; with one, "backend-only" -and "this client happens not to model it" are indistinguishable without the reviewer supplying the -judgement. Both rows collapse toward "reviewer classifies", with less structural evidence to -classify from. **If the Kotlin leg does not land, G0 is single-source and this entire section does -not run** — see §11's substitute gate. - -**Independence is a field, not an assumption.** Three repos by three teams in three type systems are -genuinely independent roots. Client models *generated* from a shared schema, or a Kotlin model written -by copying the C#, are one source presenting as three — the correlated-failure trap. `prov: -wasDerivedFrom` on the proposal graph is populated by the reviewer where generation or copying is -known. Independence declared, not inferred; the assurance upgrade in the first row applies only where -it is. - -### 5.4 Adapters for the G0 corpus - -*(Table re-verified at G-1, 2026-08-17; the scope cut below is Emil's Gate 2 ruling.)* - -| Language | Server | Status (verified 2026-08-17) | Extractor precondition | -|---|---|---|---| -| C# | existing M-track adapter (`roslyn-language-server`, pinned expectation 5.11.0; `csharp-ls` fallback) | in use; of the declaration-level six, four are wired (documentSymbol, workspace/symbol, references, hover). **Live-run check DISCHARGED 2026-08-17** against `acme-app/backend` @ `83c9339a` with `Microsoft.CodeAnalysis.LanguageServer 5.11.0-1.26380.4`, driven through the shipped `ddd-lsp` host layer | **Wiring only — no synthesis.** Roslyn answers **both** unwired operations: `definition`, and `typeHierarchy` in **both directions** (`UpsertProfileSettingsCommand` → `ICommand`; `ICommand` → 3 subtypes; `LocationId` → `IStringIdentity`). Four wiring notes: (i) `Host::request` is method-generic — no host-layer change; (ii) `definition` returns `Location[]`, already handled by `protocol::normalize_locations` — **no new normaliser**; (iii) `typeHierarchy` is *three* call sites (`prepareTypeHierarchy`, then `supertypes`/`subtypes`) and **does** need a new item normaliser — items are not `Location`s and reusing the location normaliser would silently drop the symbol identity; (iv) **the opaque `data` field (`ProjectGuid`, `SymbolKeyData`) must round-trip verbatim** or supertypes/subtypes will not resolve. Note the `csharp-ls` fallback is **not equivalent**: it does not advertise `typeHierarchyProvider` at all | -| Swift | SourceKit-LSP, bundled with Xcode and swift.org toolchains | **deferred at G0 — arrangement limit, not a tooling limit.** All six operations implemented, typeHierarchy included; SwiftPM projects background-index by default since Swift 6.1. The server is not the constraint and nothing about it is in doubt | **Deferred: no macOS machine with the client's Xcode is in the programme's arrangement at G0 entry.** An xcodeproj app needs the `xcode-build-server` BSP shim and a completing build to materialise the index (unit/record files + IndexStoreDB); there is no Linux path for iOS-SDK code. This is a property of *which machines are in the arrangement*, not of the adapter set: it falsifies nothing about SourceKit-LSP, requires no code change, and lifts the moment a macOS runner with the client's Xcode enters the arrangement. Re-enters the corpus as the third root, at which point §5.3's independence-triangulation upgrade becomes available for the first time | -| Kotlin | JetBrains kotlin-lsp (official; K2; IntelliJ platform) | **alpha** (v262.9593.0, release line re-verified unchanged 2026-08-17); partially closed-source; AGP import native but **experimental**, with a **silent no-Android-import floor at Gradle < 8.8**; fwcd fallback is end-of-life (self-deprecated, Android-broken) — not a fallback for this corpus. **Two status corrections from the live G0-entry probe (2026-08-17):** the **JDK 25 requirement is self-satisfying** — the standalone distribution bundles JBR 25.0.2, so nothing is installed on the host and it never meets the project's own JDK target; and **typeHierarchy is advertised-then-null**, not merely absent — the server declares `typeHierarchyProvider: true` and returns `null` (five of six operations genuinely present) | **Conditionally in — one probe outstanding** (G0-entry ruling, superseding the Gate 2 scope cut). Condition (a) is **half discharged** against `acme-app/android` @ `67340f69`: Gradle is **8.14.3**, clearing the 8.8 floor, AGP 8.13.0, Kotlin 2.2.10, no KMP, 25 modules, 3 flavours × 2 build types; and an independent control (`./gradlew :app:tasks --all`) enumerated **all six variants with no failures**, so the project is not the obstacle. **Outstanding:** kotlin-lsp did not start a Gradle/AGP workspace import under either handshake tried, so "variants import *through kotlin-lsp*" is undemonstrated — one narrow follow-up probe, not a guess. Condition (b) is **settled in route**: synthesis rides `hover`, which returns the declaration header with the supertype clause, needing no text slicing and no workspace import. **If the import starts, Kotlin is G0's second leg (n = 2); if it cannot, G0 is single-source** (§11) | - -**The three rows fail for three different reasons, and the distinction is load-bearing.** C# is -*wired or not wired* — engineering in this repo, and as of 2026-08-17 the remaining work is -known and small. Kotlin is *implemented or not implemented upstream* — a tooling limit we do -not control, plus one open question about our own client handshake. Swift is **neither**: the -instrument is complete and verified, and only the arrangement is missing. Recording Swift as a -tooling limit would misattribute the gap to the adapter set and would make the eventual third -root look like new capability rather than a machine becoming available. Typing it as an -**arrangement limit** keeps the ledger honest about what was built versus what was borrowed. - -Adapter maturity risk lands on the *loop's* future code-edit surface for Android, not on the -extractor, because the extractor uses only the declaration-level subset (§5.2). Precedent for the -adapter set exists: LSP plugins for Claude Code already cover C#, Kotlin, Rust, and Swift. - -### 5.5 Re-runnable by design - -Design the extractor to run at each ref and diff against the ratified ontology. New candidates -surface; **disappearing symbols flag decisions whose entity is gone** — the holding note's Q21 -decay-of-relevance under a pin, mechanised. Drift detection is out of scope until G4, but the -extractor is built re-runnable from day one because it costs nothing then and everything later. - -### 5.5.1 Review is batched by the pair, with the derivation as the sub-key - -*(Added 2026-08-20.)* Per-triple review does not scale: 561 files was already at the edge of it and -the full solution is ≈16.7× that corpus. Wholesale merge is manufactured ground by §5.7, so the -answer is neither of those — it is **review by kind**. - -A **batch** is the set of assertions sharing a derivation signature: - -``` -(derivation confidence, domain relevance, §5.2 row, rule, member kind, container kind) -``` - -Everything in one batch was read the same way about the same kind of thing, so a reviewer's verdict -on a sample carries to the batch. Each batch carries the pair, the row and rule, the probed -operations it stands on, its count, a bounded sample rendered as a reviewer reads a triple, and the -full member id list so the batch is actionable rather than merely descriptive. - -Ordering is **total and deterministic** — confidence descending (the axis is ordinal), then relevance, -row, rule, member kind, container kind — so two runs over the same facts emit the same batches in -the same order and a batch listing diffs cleanly across refs. - -The batching lives in the extractor's **output contract**, not in its logs. G0 found the reviewer's -batching keys typed as debug detail, and two hand-counts were wrong before they were made queryable. -An instrument's account of its own limits belongs where a query can reach it. - -### 5.5.2 A ratification act must be checkable against the ruling it discharges - -*(Added 2026-08-20, from a defect in G0's own Gate 4.)* - -G0 ruled per-triple: 49 assertions rejected in two named groups, the remaining 512 accepted. The -ratification act was a merge of a pull request containing **all 561 files**. The ruling was prose in -a chat message; the act was a button; **nothing checked that the second carried the first**. What -merged reads as acceptance of a reviewed set, and what was actually ratified was the unreviewed -superset. - -This is `term:presumed-discharge` at the ratifier's seat rather than in an instrument: a record -state where a pass is indistinguishable from a skip. It is also the rubber-stamp failure §5.7 exists -to forbid, arriving through the one path §5.7 did not describe — not wholesale merge *instead of* -review, but wholesale merge *after* review, which looks like the discharged form and is not. - -Binding from here: - -- **A merge is preceded by a stated accept/reject set**, by assertion id, recorded where the merge - can be checked against it. -- **The merged tree is diffed against that set**, and a divergence blocks the act. Per-triple review - that ends in one undifferentiated merge is per-triple review in name only. - -**Removal is by retraction, never by deletion.** Assertions ratified in error are removed by a -*retraction decision* filed in the instance: the members named by id, the basis naming the ruling -they should have discharged, and the assertion files removed **in that same act** — so history -retains them and the decision records why. That is supersession at the file level rather than a -rewrite, and it is a ratifier's act with a ruling behind it, never an instrument's. - -**Recoverability, paid out.** The 49 were unfindable in the ratified graph until the derivation -record landed; they are now two queries (§5.2). The argument for recording derivation was made in -the abstract; this is the first real defect in the ratified set it has repaid. - -### 5.6 Tick rate and the validity horizon - -**The claim.** An entity's expected rate of change is ground about that entity. Held in the registry -rather than in a caching layer, it makes read-versus-cache a computed decision rather than a -judgement — and it makes the same question answerable at design time, before any system runs. - -**The computation, and where each input lives.** - -| Input | Held by | Note | -|---|---|---| -| `as_of` | the Reading (§4.1) | already carried | -| tick rate | the **entity or axis** in the registry | this addendum's addition | -| tolerance τ | the **act** | not the entity — see below | -| **validity horizon** | computed, per act | the age at which expected drift exceeds what this act tolerates | - -**Tolerance sits on the act, and that is the load-bearing part.** The same entity read for two -different acts may have two different acceptable stalenesses: a display may tolerate an hour where a -settlement tolerates none. So the horizon is a function of the *pair*, never of the entity alone. -This is v5.7.0's act-indexed discharge (`DDD-frame-16`) applied to freshness — a reading is fresh -*for an act*, not fresh in general. - -**The tick rate is itself a Reading, and this is what lets the system be designed before it runs.** -The same four-tuple as any other ground: - -| Stage | value | provenance | assurance | -|---|---|---|---| -| Design time | the expected rate | `declared` *(not in §4.1's value set — open item 21)* | low — an assumption by whoever declared it | -| Running system | the observed rate | `observed` | higher — measured over retained acts | - -So design-time assumptions are not a separate mode of the system. They are the same field at lower -assurance, superseded when observation arrives. **The gap between declared and observed is -measurable** — the proxy-fidelity check applied to the drift model itself: a rate was declared, a -rate was observed, and the divergence is a finding rather than an embarrassment. - -**Observation is nearly free where the arrangement is event-sourced.** The event stream is the tick -record; rate per entity is a query over retained acts. This is the same argument retention always -makes — retained events answer questions nobody planned for, and this is one of them. - -**It makes co-drift computable.** Clustering by co-drift and ordering by rate has been a heuristic -for domain modelling. With rates on entities it is a measurement, and cache boundaries — what -invalidates together — fall out of the registry rather than out of intuition. - -### 5.6.1 Three constraints on the model - -**Rate is a distribution, not a scalar.** Many entities change in bursts, and a mean rate silently -under-serves exactly the moments that matter. The cheap form is a scalar; the honest form carries -burstiness. Whichever is taken, the vocabulary must record **which form a given rate is**, so a -consumer never reads a mean as though it were a bound. - -**It is ground, not a tuning knob.** The value of a tick rate is that it carries provenance and -assurance and is consumed by caching decisions. The moment it becomes a parameter someone edits for -performance, it stops being ground and every horizon computed from it stands on a number nobody -filed. Rate changes are proposals, reviewed like any other assertion. - -**Bootstrap honestly.** Declared rates are proxies over open predicates — the same structure as the -proxy-predicate gap. That is legitimate and better than nothing, on two conditions: they are marked -`declared`, and they are **superseded** rather than left in place once observation is available. A -declared rate still governing after a year of observation is the proxy-predicate gap in miniature. - -### 5.6.2 What this replaces - -Data as a separate specification domain. There is no data layer holding freshness policy: there are -entities carrying rates, acts carrying tolerances, and horizons computed from the pair. Caching -configuration becomes a *projection* of that, regenerated when either side moves — not a source. - -### 5.7 Guard - -The reviewer's acceptance path is **per triple, never per run**. A single review of a large codebase -that accepts wholesale is a rubber stamp, and rubber-stamped ground is manufactured ground. - -## 6. Feature 1 — the TUI: compose, declare, execute - -### 6.1 The pipeline - -The TUI is the delivery mechanism made visible and human-gated before any model runs. - -| Stage | What happens | Model | -|---|---|---| -| **Compose** | the user types in the prompt window; the entity extractor matches continuously against registry labels, `skos:altLabel`, and the code identifiers Feature 3 mapped. **Match only** — no open-vocabulary NER, no model-proposed entities | none | -| **Enhance** | per match: entailments (already materialised at projection build — lookups, not inference), related entities via declared properties, governing decisions by region, each with as-of and provenance. The user tabs through candidates, accepts or removes. Accepting is **ratifying the act's position by hand** — a wrong match caught here costs a keystroke; caught after execution it costs a review cycle | none | -| **Declare** | the prepared artifact: prompt text + position + attached Readings (full §4.1 tuple) + governing set + **working set** (per attached entity: symbols, definitions, and reference sites — the Feature 3 mapping as candidates, resolved live through LSP at declare time, each site a fresh Reading) + chosen model rung (§7.7) + α for the act. This is a **declared act** — declaration precedes act, the Q6 discipline as product spine. **Dead symbols surface here**: a mapped entity with no current references is the §5.5 decay detector firing at the useful moment, shown in the TUI before a token is spent | none | -| **Execute** | the loop (§7) consumes the declaration against the selected provider, **context seeded with the working set — the model starts at the code, not searching for it**. "Exploring the codebase" is judgement-mediated retrieval at act time, occasioned supply for what extraction already paid for as standing; the declaration delivers it instead. The cheap model never sees an unprepared prompt: **delivery is complete before the generator enters** | the rung chosen at declare | -| **Review** | the session bundle (§4.3) lands as a registry-repo branch; the TUI shows the summary; ratification stays in PR review | none | - -### 6.2 Ground state in the status line - -Stage two shows the **ground state at the declared position** before execution. *(Re-pinned at -G-1:)* the typing is canon's orthogonal one (`DDD-ground-02`), not the holding note's four states — -source coverage (covered · declared-empty · undeclared · unknown), resolution (resolved · -deliberately-open · unknown), and assurance (adequate · inadequate · unknown), with **Unknown never -rendered as a pass**. The status line shows the four-state projection canon recorded for exactly -this use — governed (covered ∧ resolved) / deliberately-open / declared-empty / **undeclared** — -with the note's "inert" renamed to canon's declared-empty and "uncovered-undeclared" to undeclared. -Where a governing decision's resolution is deliberately-open, any timing display uses the -"—(open)" value (`DDD-ground-03`): a deferred verdict is not a position in time. *(Ruled at G-1 -Gate 1: projection for the human, orthogonal for the machine — the status line keeps the four-state -projection because a status line is a projection for a human arrangement mid-act, compact and -absorbable over complete; the halt logic and the act log run on the orthogonal values; a future -detail view is where the orthogonal triple displays raw.)* Acting into -ungoverned ground gets its warning at compose time, before the loop's empty-governing-set halt would -catch it. Pre-act escape detection in a status line, and the warning is dismissable: declaring an act -into uncovered ground knowingly is legitimate (exploration), and the dismissal is recorded on the -declaration. - -### 6.3 What it is not - -Not a suggestion engine and not a chat feature. The enhance panel is an **act projection** in the -holding note's Q28 sense — position-scoped, per prompt, disposable, precision over recall — rendered -before the act. If it carries noise the user stops reading it, which is the §7.1 failure; matches are -ranked by subsumption distance and decisions by exposure, and the panel is capped, not paged. The TUI -is not a chat client: there is no conversation, there are declared acts and their bundles. - -### 6.4 Extractor status - -The prompt-entity extractor is a position extractor (§7.3) whose act-site is `prompt.submit`. Its -predicate closes over the ontology's label set. Where the prompt contains a term that resembles an -entity but does not match, the panel shows nothing — silence, not a guess. That term appears in -nothing; there is no observations channel on Feature 1 because there is no reviewer in the loop. - -## 7. Feature 2 — the governed loop - -### 7.1 Tool surfaces, and only these - -| Surface | Reads | Writes | -|---|---|---| -| Ground | SPARQL over the registry endpoint (central or local clone, per §4.4) + named graphs; entailment (subclass, related) | propose a Reading into the session's proposal graph — a registry-repo branch, never the canonical graph | -| Decisions | governing set by position; decision by id; predicate text | propose a `[PROPOSED]` entry | -| Code (LSP) | symbols, references, definitions, diagnostics, hover | edits, staged to the session's working tree | - -No shell, no web, no file system outside the working tree, no other MCP. - -### 7.2 Mechanical delivery per act - -Before each tool call is executed: - -1. **Extract position** — the extractor for the act-site reads coordinates from the call itself - (target file/symbol → domain entity via the code-to-ontology mapping; tool name → act kind; - arguments → whatever axes they carry). Each coordinate is a Reading with as-of = now. -2. **Verify extraction** (Q20) — the extractor's own closing predicate runs: coordinate matches the - axis's declared type and range; the act sits there. Failure halts (§7.6). -3. **Retrieve** — SPARQL: which decision regions (class expressions) subsume this position. Result is - the governing set. Subsumption is what makes a decision on `Order` govern an `OrderLine` act. - *(Re-pinned at G-1: class expressions are one implementation of `DDD-ground-01`'s applicability - predicates — the gate is predicate-general; factored axes are an implementation where the ground - admits them, not the ontology of every region. Graph and temporal predicates beyond simple regions - were ~27–36% of the corpus.)* -4. **Deliver** — render the governing set into the model's context in **decision-then-predicate** - form: the resolution, then its acceptance predicate text where stated. Record `delivered_form`. - Where Event Models exist for the region, their GWTs enter the governing set as predicate artifacts - (the what-layer's criterion store); EM-to-registry conformance — every event field typed by a - registry entity — is a later feature, not built here. -5. **Execute** the call. - -The user never triggers this. It is the act triggering retrieval — mechanical delivery in canon's -sense *(re-pinned at G-1: `term:delivery`, `core/13-delivery.md` — the trigger, not the index, is -what distinguishes the values, and delivery is a property of a decision at an act-site, never of the -decision alone)*. - -**Exploration acts.** `lsp.read` beyond the declaration's working set stays available — an unnamed -helper, a config file — and every such use is typed **exploration act** in the log. Not a failure: the -completeness measure of the declaration. *Exploration acts per session* is a §9.1 metric and a ladder -metric; a declaration whose working set is complete shows zero, and the count is the per-session -empirical answer to how good the extraction mapping is. - -### 7.3 Position extractors - -One per act-site, each a small pure function with a declared closing predicate, committed alongside -the ontology mapping it depends on. Initial set: `lsp.edit`, `lsp.read`, `decisions.propose`, -`ground.propose`. Extractors are governed artefacts — each has a decision file naming its axes. - -### 7.4 Ground writes - -The loop proposes Readings; it does not assert them. Every proposed Reading carries -`provenance = inferred`, `assurance = model-session:`, `prov:wasAttributedTo` the session, and -`prov:wasDerivedFrom` the Readings it stood on. The reviewer accepting a Reading into a named graph is -the trust decision (Q27) — the acceptance is what converts model output into ground with a principal -behind it. **The loop cannot manufacture ground into the canonical graph. Structurally, not by -policy.** - -### 7.5 Emitted proxy - -Per act, the model is required to state, in a fixed field before the tool call executes: which -governing decisions it is honouring, and what additional acceptance checks it is applying that were -not in the delivered set. That statement is the `emitted_proxy`. Enforcement is by tool schema — the -call is malformed without it. This is ruling 22's surfacing obligation, implemented as a required -argument rather than a norm. - -Code-edit acts additionally emit the predicate the edit is meant to satisfy, in GWT-shaped form where -the model can state one. - -### 7.6 Halts - -The loop stops and writes a halt record when: - -| Condition | Why | Escalation | -|---|---|---| -| Extraction fails to close for a needed axis | the axis is judgement-evaluable at this act-site; delivery would be judgement-mediated | reviewer states the coordinate or marks the axis out of scope | -| Governing set is empty for a code-edit act *and* the position's source coverage is `undeclared` (`DDD-ground-02`; the four-state projection's uncovered-undeclared) *(re-pinned at G-1)* | acting in ungoverned ground | reviewer files a decision, declares the region deliberately-open or declared-empty, or authorises the act explicitly | -| A governing decision's predicate is stated and open | the loop cannot verify against it | reviewer discharges the predicate or accepts the act at judgement | -| Emitted proxy contradicts a delivered decision | improvisation against governance | reviewer rules | -| Reading assurance below the act's declared α (Q11 amendment) | acting on low-assurance ground | reviewer supplies a higher-assurance reading or lowers α for the act | - -Halts are not failures. They are the escalation channel the holding note said every detector needed — -and the observations they produce are the corpus. - -### 7.7 The model ladder — arrangement variation as the experiment - -**Emil's requirement, read through the framework:** run the loop on the smallest model that works, not -the best model available. This is not a budget compromise on the design; it is the design's own -thesis under test. The loop's structure — mechanical delivery of governing sets, closing extraction -predicates, GWTs and SHACL as dense verdicts, retry on halt — is the generator/checker arrangement -(upstream H3), and the framework predicts that **model capability matters less as operational -evaluability rises**. Small-model viability is therefore the fidelity-ceiling claim (§13.10 — -delivery bounds proxy quality before skill enters) run as an experiment: if a small model with -mechanically delivered governance approaches a large model without it, the arrangement is doing what -the framework says arrangements do. Economic closure says the rest: a retry-based arrangement is -economically closed only if generation is cheap — small models are not the fallback, they are what -makes the loop's economics close. - -**Protocol.** Same corpus, same delivered governance, same verdicts; vary only the model. Ladder from -a small instruct model up through the current open-weight coding models; measure the §9.1 evidence -outputs per rung — proxy fidelity, halt rate, malformed-call rate, review yield — plus cost per -accepted change. The interesting result is the *knee*: the rung below which the arrangement stops -compensating. A **comparison arm** may join the ladder at G4: the Copilot SDK runtime (§7.8) on the -same declared acts, conditional on open item 13. Its result reads as arrangement-versus-arrangement, -not rung-versus-rung — report it beside the knee, never on the same axis. - -**Rungs (ruled at G-1 Gate 2; all on the verified Scaleway catalogue, structured outputs + tool -calling confirmed per model card; prices are ground with a drift rate — read 2026-08-17, € per 1M -tokens in/out):** - -| Rung | Model | Rationale | Price (as-of 2026-08-17) | -|---|---|---|---| -| 1 — smallest viable instruct | `gemma-4-26b-a4b-it` (4B active) | wins the session's small-rung candidates on active-params pricing for a coding task: same input price as `qwen3.6-35b-a3b` (3B active) at a third of its output price; `pixtral-12b-2409` excluded as vision-oriented, not coding | 0.25 / 0.50 | -| 2 — mid open coder | `qwen3-coder-30b-a3b-instruct` (30B, A3B, 128k ctx) | code-specialised | 0.20 / 0.80 | -| 3 — best open agentic | `glm-5.2` (256k ctx) | the catalogue's best open-weight long-horizon/coding model at release (June 2026) | 1.80 / 5.50 | -| 4 — cost-alternate (when budget allows) | `deepseek-v4-flash-0731` | cost-efficient long-horizon point between rungs 2 and 3 | 0.40 / 0.80 (0.08 cached input) | - -**The working set is the biggest lever on the knee.** The explore phase is disproportionately why -agentic coding needs large models: long-horizon search over a repo is what small models are worst at. -The declaration removes it — the remaining work, editing within a delivered working set against -delivered predicates, is exactly the shape the ladder predicts small models can hold. Exploration acts -per session (§7.2) is the metric that shows whether the removal held. - -**Rung zero exists and is Feature 3.** Extraction runs with no model at all (§5.2) — constructively -closed, rule-derived, model bill zero. The ladder is therefore a Feature 2 instrument only, and the -track's model spend concentrates entirely where generation is genuinely generative. Rung zero is also -the ladder's control: it marks what the arrangement produces before any model capability enters. - -**Small-model accommodations, each typed by the framework:** - -- **Schema enforcement over trust.** Tool calls are constrained by structured outputs, not by asking - nicely. Malformed-call rate is recorded per rung — it is the absorption signal, not noise. -- **Delivered form adapts per arrangement (Q28).** Small contexts are small-`C` arrangements: the act - projection must be compact, precision over recall hardening from a preference into a budget. The - `delivered_form` field already records what was rendered; per-rung form variants are legitimate and - logged. -- **Extraction needs almost no model.** Feature 3's high-assurance rows are deterministic (LSP → - mapping); the model enters only at synonym and usage-inference rows. The ladder is a loop - experiment; extraction cost is near-zero at every rung. -- **A failed rung is a finding.** A model that cannot hold the tool schema, or improvises past - delivered governance at a high rate, is an arrangement whose absorbable forms exclude the current - rendering — Q28's fourth failure mode, measured. The remedy menu is the Q23 table: change the form - before changing the model. - -### 7.8 Executor alternatives — the loop as an arrangement parameter - -The governed loop (§7.1–7.6) is the **primary executor** and the design's evidence engine: per-act -mechanical delivery, the emitted-proxy schema, and the halt set are the product under test. This -section types the alternative without weakening that. - -**Candidate: GitHub Copilot SDK** (agent runtime; official Rust crate; verified 2026-08-17). -Architecture: SDK client ↔ Copilot CLI in server mode over JSON-RPC. - -Shape A is the governed-surface integration described in the table below: first-party tools -disabled, our three surfaces registered as custom tools, and mechanical delivery run inside the -permission handler. Shape B, the fallback if open item 13's checks (a)–(c) fail, is the SDK with its -own tools and no delivery — an ungoverned-baseline arm rather than an executor alternative. - -The integration shape that preserves the governance surface: - -| Loop element | SDK mechanism | -|---|---| -| Tool surfaces (ground / decisions / LSP only) | disable first-party tools; register ours as custom tools | -| Mechanical delivery per act (§7.2) | the per-call **permission handler** as the interception point: extract position, retrieve governing set, deliver, then approve | -| Emitted proxy as required argument (§7.5) | required structured argument on the custom tool schema | -| Halts (§7.6) | denial with surfaced reason | -| Act log (§4.2) | read from the JSON-RPC event stream | -| Declaration seeding (§6.1) | session input | - -**What is ceded, typed honestly:** the inner planner's context management and prompting are the -runtime's own — an arrangement whose absorbable forms and retrieval habits we do not author. The -bound: an agent holding only our tools can only act through governed surfaces, so escape through -un-governed tooling is structurally excluded; escape through the planner's context handling is not, -and lands in the delivered-vs-emitted comparison like any other actor's. - -**Why bother: the comparison arm.** Same declared acts, two arrangements — our loop on open-model -rungs versus the Copilot runtime on its frontier catalogue — measured on the §9.1 outputs plus cost -per accepted change. That is the commercial thesis (*governance substitutes for capability*) against -the strongest available counter-arrangement, which is the comparison a customer will ask for -regardless. Economics differ in kind, not just price: per-prompt billing against a Copilot allowance -versus per-token — for retry-heavy governed sessions the flat-allowance shape may undercut even the -cheap rungs. Prices and billing models are ground with a drift rate; date every figure. - -**Gate: verification before build.** The arm is conditional on five checks (open item 13). If any of -the first three fails, Shape A cannot carry the governance surface and the SDK is dropped to an -ungoverned-baseline arm only — still useful, differently typed. - -## 8. Layer and repo boundaries - -| Concern | Layer | Where it lives | -|---|---|---| -| Ontology vocabulary, extractors, SHACL shapes | Layer 1 (synchronic) | product-cli, `product-core` | -| Registry (ratified triples) | Layer 1 content, Layer 2 history | the **registry repository** (org-level, new); endpoint is a rebuildable projection | -| Act log, session bundle, halts | Layer 2 (ledger) | product-cli, L-track ledger extended with the Act type | -| Decisions proposed | Layer 2 | ledger, existing path | -| Canon | — | untouched by this track | - -The Act log is a new ledger table, not a claim-graph addition. Acts are numerous; they do not enter -the canon graph. - -## 9. Success criteria - -Ordered. The first is the reason to build. - -1. **Evidence.** After N sessions, the delivered-vs-proxy corpus supports or falsifies: whether the - model's improvisations beyond the delivered set are frequent (ruling 22), where extraction fails - to close and how fast those axes mature *(re-pinned at G-1: ruling 12 is answered in canon — - the axis-type mark is a maturity state, not a fixed type (`DDD-ground-01`, the matched-pair - evidence, ~2.5 weeks nameable→resolvable); the open question is now the transition rate over a - larger corpus, not the type)*, and whether halts cluster by - region (Q23 remedy routing). Report, not verdict; the rulings are Emil's. -2. **Review yield.** Proposed decisions accepted per session; proposed Readings accepted; halts that - led to a filed decision. If reviewers reject nearly everything, the loop is proposing badly and the - delivered form is wrong before the model is. -3. **Code.** Edits pass their emitted predicates and the existing test suites. Third, deliberately. - -4. **The ladder (§7.7).** Cost per accepted change by model rung, and where the knee sits. If the -knee is low — small models suffice under delivered governance — that is the framework's most -commercially legible result to date, and it is the loop demonstrating its own thesis. - -And for G0 specifically: **bootstrapping yield** — proportion of proposed triples accepted per -assurance row; number of cross-codebase contradictions surfaced; whether the shared-domain shape, once -ratified, validates all three codebases or reveals divergence the teams did not know about. The last -is the finding the G0 corpus exists to produce. - -## 10. Dependencies on unratified constructs - -The PRD leans on the following holding-note items. Each is named so that a ruling against it can be -traced to the affected section: - -*(Re-pinned at G-1: the table gains a canon-status column — statuses read against upstream `110bf10` -(v5.5.0) and downstream `4848b9e`. "Not filed" means the construct remains holding-note-only and the -dependency stands as originally flagged; the fallback column applies only on rejection, and nothing -was rejected.)* - -| Construct | Section | Canon at v5.5.0 | If ruled against | -|---|---|---|---| -| Position / region retrieval (Q2, Q19) | §7.2 | **partially filed** — the gate half is `DDD-ground-01`, ratified amended (predicate-or-explicit-universal; axes marked mechanically-/judgement-evaluable; axes one implementation, predicates general); the retrieval mechanism itself is not filed | the loop degrades to unfiltered decision listing — build stops | -| Extraction verification (Q20) | §7.2 step 2, §7.3 | not filed — the closure-as-filing-commitment consequence is anchored by `DDD-ground-01`'s marking clause; open ruling 13 (extractor decision files) open | halts on closure failure become warnings; corpus loses ruling-12 evidence | -| Reading three-tuple (Q11 amended) | §4.1 | not filed — assurance enters canon only as `DDD-ground-02`'s orthogonal property of ground; the per-reading tuple is note-only, and the provenance value set is not in canon at all (§4.1 note) | assurance halt (§7.6 last row) is dropped | -| Trust decision backing (Q27) | §4.1, §7.4 | not filed — open ruling 27 open | reviewer acceptance of Readings loses its typed meaning; keep the mechanism, drop the name | -| Emitted proxy (§13.10, ruling 22) | §7.5 | not filed as obligation (open ruling 22 open) — but the comparison's stake is canon: `DDD-delivery-02`, `term:presumed-discharge` | the tool schema still requires the field; the *interpretation* as proxy is dropped | -| Registry = existing ontology (ruling 25, A1) | §3, §4.2 | not filed — open ruling 25 explicitly open (the fork this PRD waits behind); A1 stands as assumption | a mapping layer is added between the two vocabularies; §7.2 step 3 queries through it | -| Triangulation-with-independence (Q27 amended) | §5.3 | not filed | the assurance upgrade for cross-codebase agreement is dropped; agreement stays a review signal only | -| Decay-of-relevance under a pin (Q21) | §5.5 | not filed — open rulings 14 and 15 open | disappearing-symbol detection stays a diff, unnamed | - -Ratified canon the PRD stands on without qualification: operational closure, the source/assurance -split, the four stores as partition, accountability completeness, C_resolve. *(Re-pinned at G-1: -provenance typing is removed from this list — the four-value set was not located in live canon at -v5.5.0 and is re-marked as unratified holding-note vocabulary; see §4.1.)* Canon the PRD now -additionally stands on, filed at v5.5.0: `DDD-ground-01…04`, `DDD-delivery-01…04`, `term:delivery`, -`term:undelivered`, `term:presumed-discharge` (all draft/projected — filed, not yet settled). - -## 11. Phasing - -| Phase | Delivers | Gate | -|---|---|---| -| G0 | **First registry instance generated from the template** (parameters supplied at generation; birth provenance in its first commit; founding decision filed by its ratifier); ~~Oxigraph 0.5 upgrade as task one~~ **— DONE, landed before G0 entry (PR #46): the `oxigraph = "0.4"` pin is retired and the workspace is on 0.5**; local embedded store over a clone; Reading and Act types; PROV-O wiring; **code extractor over the corpus — C# first (probed and closed 2026-08-17), Kotlin second *conditional on the import probe below*; Swift deferred as an arrangement limit (§5.4)**; proposal graphs as registry branches; per-triple review; **shared-domain intersection only if the Kotlin leg lands (n = 2 qualifier, §5.3) — otherwise G0 is single-source and the gate below changes** | **G0-entry checklist, as re-verified 2026-08-17:** generate the first instance (parameters supplied then); ~~Oxigraph 0.5 upgrade~~ **DISCHARGED**; **Roslyn `typeHierarchy` probe — DISCHARGED** (answers both directions on `acme-app/backend` @ `83c9339a`; `definition` also confirmed; wiring only, §5.4); **Android Gradle read — DISCHARGED** (`acme-app/android` @ `67340f69`: Gradle 8.14.3, AGP 8.13.0, six variants enumerate cleanly under Gradle); **kotlin-lsp workspace-import probe — OPEN**, the single remaining entry condition and the one that decides n = 2 versus single-source; ~~the iOS build completing on Emil's machine~~ **struck as a gate — re-filed as a deferred leg (§5.4, arrangement limit)**. **Gate, if the Kotlin leg lands:** Emil ratifies a first shared-domain ontology **by merging**; contradiction count reported; the "should be identical" decision filed with the extractor as its discharge. **Gate, if it does not — the single-source substitute:** first ontology ratified per-triple from one codebase; the extractor re-run at a second, later ref with the diff reported as candidates-added / symbols-disappeared (exercising §5.5 and the Q21 decay mechanism without a second language); **bootstrapping yield reported per assurance row** (the one §9 criterion that does not need n ≥ 2); and the "should be identical" decision **filed but explicitly left undischarged**, with its falsifier named and the reason recorded, so it is a known open rather than a silent gap | -| G1 | TUI compose + enhance + declare over the ratified ontology (`prompt.submit` extractor; enhance panel; ground-state line; declarations persisted, not yet executed) | panel renders for a corpus of real prompts; precision measured by Emil's hand-check; a declared act round-trips to disk | -| G2 | Execute stage: the loop (ground + decisions surfaces, no LSP) consumes declared acts against one verified provider; sessions produce bundles | first declaration executed end to end; first bundle reviewed; act log complete | -| G3 | LSP surface; code-edit acts with emitted predicates; halts complete | first code change lands through review | -| G3.5 | **Central endpoint** (container, Entra ID) for arrangements that cannot clone; connection-as-trust-decision config (§4.4) | one non-local arrangement reads the registry through it | -| G4 | Evidence report from N sessions against §9.1, **including the model ladder (§7.7)** run on at least three rungs; comparison arm per §7.8 if open item 13 cleared and Emil rules it in (open item 14) | Emil reads; rulings 12 and 22 get their evidence; the knee is reported | - -G4 is the point. G3 is where it looks like a product. **G0 is where the first real finding lands** — -three codebases that should carry one domain, and the extractor saying whether they do. - -## 12. Open before G0 - -Emil's rulings needed, in order of how much they change the design: - -1. **A1 / ruling 25** — registry is the existing ontology extended. If separate, §4.2 and §7.2 gain - a mapping layer. -2. **Act-site set for G0** — `prompt.submit` only, or `lsp.read` too, so G0 exercises subsumption on - code entities from the start. -3. **The α default per act kind** — code edits high, reads low; the numbers are yours. -4. **Whether observations (§4.3) should carry any structure at all**, or stay free text by design so - nothing about them can be mistaken for a proposal. -5. **Corpus for G0** — the app's three repos are the obvious choice; confirm. *(Struck: "which repo - is authoritative" — a repo is a projection of the domain and cannot be its authority; the authority - is the retro-filed decision set the reviewer ratifies. See §5.3.)* -6. **Independence declarations for the G0 corpus** — whether any client model was generated from or - copied from another, so §5.3's assurance upgrade is applied only where earned. -7. **Registry repository location and name** — **re-typed at G-1 Gate 3, not answered**: the - skeleton is a **parameterised template** (`docs/g-track/registry-template/`, versioned), and - ownership is a **generation parameter, answered per instance** — the owning organisation is the - accountable-principal field of every trust decision referencing that instance; the base IRI - derives from a host the owner controls durably; the ratifier is named at generation. The - G-track's first instance is generated at G0 entry, its parameters supplied then, and its first - commit records its birth provenance (template version, parameters, generated-by, date). -8. ~~**Store verification**~~ — **answered at G-1** (2026-08-17): Oxigraph holds (0.5.9, active, - SPARQL 1.1 Query/Update, named graphs). **The 0.5 upgrade is no longer pending — it landed - before G0 entry as PR #46** (§3, §11); the item is fully closed, not merely answered. -9. ~~**Second provider**~~ — **closed Scaleway-only at G-1** (2026-08-17): GitHub Models retired - 2026-07-30; Copilot's official surfaces are agent runtimes, not OpenAI-compatible endpoints. - Closed per this PRD's own fallback (§3). -10. ~~**Ladder rungs**~~ — **ruled at G-1 Gate 2**: `gemma-4-26b-a4b-it` / `qwen3-coder-30b-a3b` / - `glm-5.2`, plus `deepseek-v4-flash-0731` as the cost-alternate fourth point when budget allows - (§7.7, prices as-of 2026-08-17). -11. ~~**Reasoner mechanics**~~ — **ruled at G-1 Gate 2**: CONSTRUCT-to-fixpoint on Oxigraph as - primary, `reasonable` as named fallback, `revisit_if` on projection-build wall-time at G0 - scale (`g-dec-02`); ontology matching reimplemented as a Rust lexical/structural core with - LogMap once-offline as calibration oracle (§3, §5.2). -12. **TUI framework** — ratatui **verified at G-1** (framework half answered). Still open: whether declarations persist as files in - the working tree (reviewable, diffable, re-executable) or only in the act log — the file option - makes a declaration a reusable artifact and is the default position. -13. **Copilot SDK verification (G2 entry, before any Shape A build):** (a) first-party tools can be - fully disabled — none reachable; (b) custom tools support required structured arguments (the - emitted-proxy field is schema-enforced, not requested); (c) the permission handler sees full - arguments pre-execution and can inject delivery before approval; (d) BYOK — whether arbitrary - OpenAI-compatible base URLs are accepted (if yes, their runtime on our Scaleway rungs collapses - §7.8's arm and the ladder into one experiment; the docs name OpenAI, Azure AI Foundry, and - Anthropic — arbitrary endpoints unverified); (e) the JSON-RPC event stream carries enough - per-call detail to populate the §4.2 act log without gaps. Each check dated, against the pinned - SDK version. Gated at G2 entry rather than G4 deliberately: checks (a)–(c) determine typing, not - readiness — a failure redirects the SDK from executor alternative to ungoverned baseline - (Shape B), and that redirect must be known before G2's provider work commits. -14. **Comparison-arm scope** — whether the arm runs at G4 with the ladder or waits for a later - campaign; and whether Shape B (SDK with its own tools, no delivery) runs alongside as the - industry-default control. Emil rules at G4 planning. -15. **Widening the declaration-level subset** — *(opened 2026-08-20 by the two-axis session; the - G0 report's §7.2, now load-bearing.)* §5.2's domain-relevance column is **declared-empty**: the - extractor computes it for no row, because everything that would compute it — generated-code - attributes, a generated-file header, visibility — sits outside the declaration-level six. Each - widening trades constructive closure for relevance, so it is **a filed decision with the closure - consequence stated**, never a quiet extension. Named candidates, cheapest first: the visibility - facts the C# adapter's own layer already derives from source text; generated-file markers; a - path-convention rule over `reg:sourcePath` (which needs no new operation at all, and was - deliberately *not* taken by the two-axis session because a claim that a folder name means - "generated" is a convention judgement that belongs here). Until this is filed, neither rejected - group is filtered — only made visible and batchable. -16. **The simple-name collision defect** — *(opened 2026-08-20, measured.)* The `properties` row - resolves a member's range by matching the **simple name** of its declared type against the - corpus. G0 measured 0 cross-module collisions on the three-project subset and recorded the risk; - the full solution carries **21**, so the row now conflates two types wherever a name collides. - `definition` is wired, answers, and is consumed by no row — it is the exact-resolution route and - spending it is the fix. Scoped as: *route range resolution through `definition` where a simple - name is ambiguous, keeping the name match where it is not.* Not a widening of the subset — - `definition` is already inside it. -17. **Retry-then-record for probe verdicts** — *(opened 2026-08-20.)* A verdict is a Reading - (§5.2), and a single non-answer currently becomes a standing "unavailable" that gates a row. - Propose the retry shape — how many attempts, how spaced, how the attempts are written into the - run evidence so the verdict's assurance is visible — and rule on it before any row's gating - depends on an operation that has ever timed out. -18. **Tick-rate vocabulary shape** — scalar versus distribution, the unit, and how burstiness is - expressed where it is carried. Emil rules before implementation; the model's honesty depends on - a consumer never mistaking a mean for a bound. -19. **Where tolerance is declared.** The horizon needs τ per act, and the act-site extractors do not - currently carry one. Whether tolerance is declared per act kind, per decision, or per act at - declaration time is a design ruling, and it gates any implementation of §5.6. -20. **Observation source.** Rate observation assumes retained acts to count. For arrangements that - are event-sourced this is a query; for those that are not, it is unavailable and rates stay - `declared` indefinitely. State that limit rather than implying observation is always reachable. -21. **Design-time provenance** — *(opened 2026-08-20 by the tick-rate addendum; a defect in the - addendum itself, of the same class as the G-1 mis-pin — a value used from conversation as - though it were filed.)* §5.6's stage table gives declared rates `provenance = declared`, which - is **not in §4.1's value set** (`controlled` / `observed` / `inferred` / `institutional`). - Three candidate resolutions: (a) **a fifth value** — a filing consequence, since the set is - track-owned under `g-dec-01` pending the Q25/Q27/Q30 wave, and this would be a second consumer - waiting on that wave alongside the existing one; (b) **`inferred`**, with the assumption - recorded in `assurance` rather than `provenance`; (c) **a distinct axis** for whether a value - was assumed or measured, on the grounds that assumed-versus-measured may be orthogonal to where - a value came from — "assumed at design time" and "inferred by rule" are different things, and - forcing them into one column is the conflation §5.2's second axis was added to correct. Emil - rules, with the wave's context; gates any implementation of §5.6. **Deliberately not folded - into item 18**: 18 is the rate's shape and stays inside the G-track, 21 is whether the - provenance value set gains a member and carries a canon consequence. - ---- - -*Not in this PRD by ruling: the sweep, the map, the projection matrix, arrangement profiles, intent -as an object, ontology growth by the model, resolution of cross-codebase disagreement by tooling. Each -is a consumer of what this track emits, or a decision the reviewer owns.* diff --git a/docs/g-track/registry-generator-design-2026-08-18.md b/docs/g-track/registry-generator-design-2026-08-18.md deleted file mode 100644 index 107fed64..00000000 --- a/docs/g-track/registry-generator-design-2026-08-18.md +++ /dev/null @@ -1,380 +0,0 @@ -# The registry generator — design, for Gate 1 ratification - -**Session:** registry generator (G-track), 2026-08-18. **Repository:** `Hafeok/product-cli`, -branch `claude/registry-generator-mechanism-wf1dsr`. -**Status:** proposal. Nothing is implemented; this document is the Gate 1 hold. - -**Inputs read:** `docs/g-track/registry-template/` (whole tree, version 0.1.0), -`docs/g-track/prd-ground-as-ontology.md` (§3 registry authority, §4.1 Reading), -the G-1 session report's Gate 3 rulings (§3.3), and the G0 handoff -*Founding content, ratified as text* — which records the generation this session replaces. - -## 0. What this is answering - -The G0 instance was generated by a shell one-liner. A parameter value — the ratifier's address — -carried a sigil the substitution engine re-read as code, and the corrupted base IRI propagated into -every file including the birth provenance. A human reading the output caught it. The base IRI is the -one registry parameter with no supersession path (g-dec-03): a changed IRI orphans rather than -supersedes. - -Two faults, not one: - -1. **String substitution re-reads values as code** in every engine that does it — `sed`'s `&` and - `\1`, the shell's `$` and backtick, a regex replacement string's group references. -2. **A scratchpad script cannot be tested.** Registry generation mints identity, records birth - provenance, and creates the artefact a founding decision is filed into. It is the act the track - stands on. - -The remedy is a subcommand with fixtures, not a better one-liner. The reason to build it now rather -than later is testability: a generator invocable from code turns the template's guarantees from -hand-verified-once into fixtures that run on every commit. - ---- - -## 1. Where it lives, and what it is called - -### 1.1 The invocation - -``` -product registry generate \ - --owner Hafeok \ - --repo ground-registry-g0 \ - --ratifier emil@okkels-klein.dk \ - --display-name 'Ground Registry (G0 validation)' \ - --base-iri 'tag:emil@okkels-klein.dk,2026-08-17:ground/' \ - --date 2026-08-17 \ - --generated-by 'product-cli registry generator (session …)' \ - --out ../ground-registry-g0 - -product registry check # read-only: the instance's own rules over a tree -``` - -`product`, not `ddd` and not `ledger`. The ground registry is neither a `.ddd/` decision store nor a -`.decisions/` ledger, and the generator writes **no** store in the current repository — it writes a -new repository elsewhere. `product` is the workspace's front CLI, the G-track is a product-cli -track, and the template it instantiates lives in this repo's `docs/`. The alternative considered was -a fourth binary (`registry`); rejected as a whole crate's worth of packaging for two verbs. - -`generate`, not `new` — the repo's usual verb for minting things. The template, the PRD and -`GENERATION.ttl` already name this act *generation*, with *generation parameters* and *birth -provenance*. A second word for one act is how vocabulary drifts. - -### 1.2 The code - -Slice + adapter, as the workspace does everywhere: - -| Path | Holds | Purity | -|---|---|---| -| `product-core/src/registry/mod.rs` | the slice's surface | pure | -| `…/registry/params.rs` | `RegistryParams` — the typed parameter struct, plus meaning-validation | pure | -| `…/registry/template.rs` | the embedded template: path → bytes manifest, `TEMPLATE_VERSION` | pure | -| `…/registry/substitute.rs` | the single-pass renderer (§2.2) | pure | -| `…/registry/verify.rs` | the generation gate (§4) | pure | -| `…/registry/plan.rs` | `plan_generation(&RegistryParams) -> GenerationPlan` | pure | -| `…/registry/apply.rs` | `apply_generation(&GenerationPlan, &Path)` — writes the tree, `git init`, one commit | I/O only | -| `…/registry/check/` | the instance conformance reader (§6): file rule + shapes | pure over a loaded tree | -| `product-cli/src/commands/registry.rs` | `RegistryCommands` (clap) + two thin handlers returning `CmdResult` | adapter | - -`Commands::Registry` sorts before `Commands::Release` (the `cli_subcommands_are_sorted` gate). -Every file stays under the 400-line CTX001 limit and carries a single-responsibility `//!` line. - -**No MCP mirror.** CLAUDE.md's step 7 attaches a `product_*` tool to commands that mutate the What -graph. This one mutates no `.product` graph in any repository; it writes a new tree outside the -workspace. Adding it to a phase-gated session surface would put repository minting inside an -authoring session, which is precisely the conflation §3 separates. - -**How the template is embedded.** `include_str!` over `docs/g-track/registry-template/…`, one entry -per file in a hand-maintained manifest — the pattern `product-cli/src/commands/skills.rs` and -`product-core/src/pf/*` already use for `schema/examples/`. The template stays where the PRD and the -G0 handoff pin it (`docs/g-track/registry-template @ `); no second copy is vendored, because -a second copy is a projection presenting as a source. A **manifest-drift test** walks the on-disk -template and fails if the file set or any byte differs from the embedded manifest, so adding a -template file without wiring it cannot pass silently. - ---- - -## 2. Parameter handling - -### 2.1 The surface - -Typed arguments. There is no `--set KEY=VALUE`, no substitution table on the command line, and no -parameter file — an untyped table is what lets an unwired placeholder or a mistyped key through. - -| Flag | Type | Token in the template | Lands in | -|---|---|---|---| -| `--owner` | string | `{{OWNER_ORG}}` | `README.md`, `GENERATION.ttl` | -| `--repo` | string | `{{REPO_NAME}}` | `README.md`, `GENERATION.ttl` | -| `--ratifier` | string | `{{RATIFIER}}` | `README.md`, `GENERATION.ttl` | -| `--display-name` | string | `{{DISPLAY_NAME}}` | `README.md`, `GENERATION.ttl` | -| `--base-iri` | string | `https://REGISTRY-HOST.example/ns#` | `shapes/*.ttl`, `graphs/**/*.ttl`, `.github/workflows/validate.yml`, `GENERATION.ttl` | -| `--date` | `YYYY-MM-DD` | `{{MINT_DATE}}` | `GENERATION.ttl`; also the commit's author/committer date | -| `--generated-by` | string | `{{GENERATED_BY}}` | `GENERATION.ttl` (`prov:wasAttributedTo`) | -| *(derived)* | — | `{{TEMPLATE_VERSION}}` | `GENERATION.ttl` | -| *(derived)* | — | `{{GENERATOR_VERSION}}` | `GENERATION.ttl` | -| `--out` | path | — | the target directory; substituted nowhere | - -Two notes on the table: - -- **The base IRI's placeholder is a literal example IRI, not a moustache.** G-1 Gate 3 kept it - IRI-valid so the template's own Turtle parses and the template validates as-is. That is a - property of the *token*, not an exception to the mechanism: the renderer's token table holds - `https://REGISTRY-HOST.example/ns#` alongside `{{OWNER_ORG}}` and treats them identically. -- **`{{REPO_NAME}}` currently appears only in TEMPLATE.md's own parameter table** — i.e. it reaches - no generated content, so a round-trip check (§4) would have nothing to assert about it. Gate 3 - gives it a home in `GENERATION.ttl`, where the repository name belongs anyway. - -**Meaning-validation, never escaping.** Before rendering: no parameter is empty; `--date` parses as -a calendar date (chrono); `--base-iri` is an absolute IRI ending in `#` or `/`, checked by asking -the real Turtle parser (oxigraph) to parse a one-line probe document with it as a prefix — not by a -regex, and not by a character allow-list. The mechanism must carry any bytes verbatim; a validator -that rejects sigils would be an escaping rule in disguise, and escaping rules are what produced the -G0 bug. - -### 2.2 The renderer, and why it cannot re-read a value - -One pass over the template text. The scanner walks the input with a cursor, matching the longest -token from a closed table at each position; on a match it **copies the parameter's bytes to the -output and advances the cursor past the token**. The cursor never moves backwards, and the output is -never re-scanned. - -The invariant, stated so a test can assert it: *the output is the concatenation of literal spans of -the template and byte-for-byte copies of parameter values.* Consequences — - -- A value containing `&`, `\1`, `$0`, a backslash, a pipe, or a slash is data. There is no - replacement grammar to re-read it in. -- A value that itself contains `{{OWNER_ORG}}` is emitted, not substituted. Sequential - `str::replace` calls do **not** have this property: a value inserted by an earlier replace is in - the buffer the later replaces scan. -- Substitution order is irrelevant, because there is no order. - -`str::replace` in a loop would be nearly right and is the honest fallback; the single-pass scanner -is ~60 lines and closes the last hole, so it is what is proposed. - -**No templating dependency.** `handlebars`, `tinytemplate`, `minijinja` all bring an expression -language, an escaping mode, and a partials/lookup mechanism — surface whose *purpose* is to -interpret its inputs. The workspace has no templating crate today (`author/domain.rs` uses a bare -`include_str!(…).replace("{{PRODUCT}}", …)`), and auditing a template engine's replacement -semantics costs more than the sixty lines it would replace. This is the one place in the track where -"the engine re-read the value" is the recorded failure; adding an engine is the wrong direction. - ---- - -## 3. The generate / publish split - -**Generation is local and offline. Publication is a separate act, and is not a subcommand.** - -`product registry generate`: - -1. builds the whole rendered tree **in memory**; -2. runs the verification gate (§4) — on failure it aborts, having written nothing; -3. creates `--out` (which must not exist, or must be an empty directory — the generator never merges - into an existing tree); -4. writes every file, atomically, only under `--out`; template paths are relative and are rejected if - they normalise outside the target; -5. runs `git init`, `git add -A`, and **one commit** — the birth-provenance commit — with - `git -C -c user.name=… -c user.email=… -c commit.gpgsign=false`, `GIT_CONFIG_NOSYSTEM=1`, - and `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE` pinned to the mint date. No global git config is read - or written, no ambient identity is required, and the commit is deterministic: the same parameters - and the same generator produce the same tree bytes and the same commit id. -6. prints the two commands that would publish it, and stops: - -``` -git -C remote add origin git@github.com:/.git -git -C push -u origin main -``` - -No network call. No GitHub API. No remote is configured. No repository is created under any -organisation. `product registry check` is read-only. - -The boundary is where it is because minting identity and publishing it are acts with different -authority and different consequences — and because only generation can be on the test path. A -fixture is a `TempDir` holding a local git repository; if publication rode along inside `generate`, -the untested half would ride on the tested half's back, which is how G0's one-liner shipped a -corrupted base IRI with a green-looking run. - -*(Scope note: `Hafeok/ground-registry-g0` stays empty until G0 resumes. This session pushes to its -own branch and nowhere else.)* - ---- - -## 4. Verification as a gate - -After rendering, **before the first byte is written to disk**, over the in-memory tree. A failure -aborts generation with a non-zero exit and no target directory — not a warning, and not a partial -tree for someone to inspect. - -**Check A — every parameter round-trips byte-identically.** The renderer records, per file, the byte -offset and length of every value it emitted. The gate reads those spans back out of the rendered -bytes and asserts each equals its parameter exactly; then it asserts, per parameter, that the number -of emitted sites equals the number of token occurrences the template holds. A value that was -truncated, re-read, re-escaped, or partially overwritten fails here. *This is the check that would -have caught the G0 bug, loudly, at generation time.* - -**Check B — zero surviving placeholders.** Every rendered file is scanned for any `{{IDENT}}` and for -the base-IRI sentinel `REGISTRY-HOST.example`. Any hit aborts. This catches the failure Check A -cannot see: a placeholder in the template that no parameter is wired to. A new template placeholder -therefore fails generation until it is given a typed argument — the drift is impossible to ship. - -**The one exemption, asserted rather than tolerated:** `TEMPLATE.md` travels **verbatim**. The gate -skips it for Check B and instead asserts it is byte-identical to the template's own copy. That is -the instance's record of what its placeholders were, and its re-pin hook when the template moves. - ---- - -## 5. `GENERATION.ttl` — birth provenance - -Today the template's `GENERATION.ttl` is a comment block describing a shape to be filled by hand. -It becomes a **parameterised Turtle file** filled by the same renderer and checked by the same gate — -so the birth provenance is generated by the mechanism, not typed beside it (the G0 corruption reached -the birth provenance too). - -It records, as one `prov:Activity` (the generation act) and one `reg:Registry`: - -| Recorded | From | -|---|---| -| `reg:templateVersion` | the template's own version constant | -| `reg:generatorVersion` | the `product-cli` crate version | -| `reg:owner`, `reg:repository`, `reg:ratifier`, `reg:displayName` | the four string parameters | -| the base IRI | the `@prefix reg:` of the file itself | -| `reg:mintDate` / `prov:generatedAtTime` | `--date` | -| `prov:wasAttributedTo` | `--generated-by` | - -Recording the generator's version *alongside* the template's is the point: an instance can re-pin -against either moving. The file stays valid Turtle in the template (the base-IRI placeholder is -IRI-valid), so the template continues to parse as-is. - ---- - -## 6. Testing the instance's own rules, and the honest cost - -The fixture *"instance CI passes on a fresh instance"* needs the two rules the instance's CI runs: -the file rule, and SHACL over `shapes/*.ttl`. The instance's CI runs them in Python (`pyshacl`). - -Requiring `python` + `pyshacl` inside `cargo t` was considered and rejected: it makes the gate depend -on a toolchain no developer machine is promised, and the usual mitigation — skip when absent — turns -a gate into a habit, which is the failure this whole session exists to remove. - -So `product registry check ` reads the instance's own files and evaluates them natively: - -- **File rule** — every `graphs/**/*.ttl` carries exactly one assertion **or** exactly one decision - (the Gate 3 generalisation), parsed with oxigraph. -- **Shapes** — a deliberately small SHACL subset, compiled to SPARQL and run through the existing - `pf::sparql_rules` surface: `sh:NodeShape` + `sh:targetClass`, `sh:property` with `sh:path`, - `sh:minCount`, `sh:maxCount`, `sh:nodeKind`, `sh:in`, `sh:datatype`, `sh:message`, and - `sh:sparql`/`sh:select` (which `shapes/reading.ttl` already uses). The shapes files stay the single - statement of the constraints; nothing is restated in Rust. -- **Fail-closed.** Any SHACL construct outside that subset, encountered in a shapes file, makes the - check **fail** with "unsupported construct", never pass. A shape the reader cannot evaluate is - reported as unevaluated, so a future template shape cannot be vacuously green. - -**The residual cost, stated rather than hidden:** two readers of one rule set — `pyshacl` in the -instance, this reader in the fixture. Fail-closed evaluation bounds the divergence to *constructs the -reader refuses*, never to *constraints it silently ignores*. See open question Q2 for the alternative. - ---- - -## 7. Teardown - -- Every fixture generates into its own `tempfile::TempDir` (already a `product-core` dev-dependency); - the directory is the whole of the generation's footprint. -- The generator writes only under `--out`: paths come from the embedded manifest, are relative, and - are rejected if they normalise outside the target. No `$HOME`, no global git config, no workspace - file, no `/tmp` scratch. -- `git` is invoked with `-C `, `GIT_CONFIG_NOSYSTEM=1` and inline `-c user.*` — it neither reads - nor writes the developer's identity. -- Fixtures assert the residue directly: the parent of the target holds nothing but the target; after - the `TempDir` drops, the path is gone. -- Two generations in one test run go to two temporary directories, run in either order, and produce - byte-identical trees for identical parameters (determinism, asserted). - ---- - -## 8. The fixture list proposed for Gate 2 - -| Test | Asserts | -|---|---| -| `parameters_round_trip` | every parameter byte-identical at every site in the generated tree | -| `g0_regression_sigil_in_ratifier_address` | the G0 case exactly: `emil@okkels-klein.dk` and `tag:emil@okkels-klein.dk,2026-08-17:ground/` survive intact — named for the bug it exists to prevent | -| `hostile_parameter_values` | values carrying `&`, `\1`, `$0`, `\`, `|`, `/`, and a literal `{{OWNER_ORG}}` all emit verbatim and none is re-read | -| `no_surviving_placeholders` | zero `{{…}}` and zero `REGISTRY-HOST.example` across the tree, TEMPLATE.md excepted | -| `template_md_travels_verbatim` | TEMPLATE.md byte-identical to the template's own copy | -| `generation_ttl_is_complete` | template version, generator version, all parameters, generated-by, date, `prov:wasAttributedTo` — parsed as Turtle and queried, not string-matched | -| `fresh_instance_passes_its_own_rules` | file rule + shapes clean on a freshly generated tree (§6) | -| `negative_institutional_reading_without_trust_decision` | **fails** | -| `negative_two_assertions_in_one_file` | **fails** | -| `negative_decision_missing_required_field` | **fails** | -| `unsupported_shacl_construct_fails_closed` | an unevaluable shape fails rather than passes | -| `gate_aborts_before_writing` | an unwired placeholder in the template leaves **no** target directory behind | -| `refuses_non_empty_target` | generation into an occupied directory refuses | -| `generation_is_deterministic` | same parameters → identical bytes and identical commit id | -| `teardown_leaves_no_residue` | nothing outside the temporary directory; two generations do not interfere | -| `embedded_template_matches_on_disk` | manifest-drift guard on `docs/g-track/registry-template/` | - -The negative cases carry the same weight as the positive ones: they are the template's guarantees, -and until now they were verified by hand, once. - ---- - -## 9. Template changes proposed for Gate 3 - -Folded into the template proper, version **0.1.0 → 0.2.0** (the change is behavioural for its output): - -1. **`shapes/decision.ttl`** — a `reg:Decision` requires `reg:title`, `reg:resolution`, `reg:region`, - `reg:falsifier`, `reg:ratifiedBy`, `reg:status`, `reg:made`. `reg:basis`, `reg:acceptedCost` and - `reg:revisitIf` are deliberately **not** required: a decision may honestly have none, and - demanding them produces filler. (Field names follow the G0 handoff's filed texts.) -2. **The CI file rule generalised** — from a filename exemption to the class rule it stood in for: - every `graphs/**/*.ttl` carries exactly one assertion **or** exactly one decision. The zero-triple - tolerance goes with the empty slot: `graphs/canonical/founding-decision.ttl` is no longer shipped - as an empty file. The slot stays a **named path** documented in README/TEMPLATE.md, created by the - founding decision's own first merge — which keeps a freshly generated instance's CI clean without - an exemption. -3. **TEMPLATE.md step 1 disambiguated** — TEMPLATE.md itself travels **verbatim**, unsubstituted, as - the instance's record of what its placeholders were. The current wording ("minus this file's - version header context — the file itself travels") reads both ways. -4. **The base-IRI row documents two routes**, with their trade-offs: (a) a host the owner controls - durably, resolvable from day one, committing the owner to keep resolving it for the registry's - lifetime; (b) location-independent minting (`tag:` per RFC 4151, or `urn:`) with the HTTP form - published later as a projection via a rebasing parameter — g-dec-03's route, which defers a - commitment that has no supersession path at the cost of an internal-to-published mapping at - projection build. -5. **`GENERATION.ttl` becomes parameterised** (§5), and `{{REPO_NAME}}` gains its home there. - ---- - -## 10. Gates, and what this session does not touch - -Green before every hold: `cargo t`, `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used`, -`cargo xtask check`. Commit and push before reporting at each hold. - -Out of scope, unchanged: resuming G0; the extractor; publishing any instance to any remote -(`Hafeok/ground-registry-g0` stays empty); any PRD edit; canon. The founding texts are ratified and -carry forward verbatim, with the same five parameters and the same mint date, 2026-08-17. - ---- - -## 11. Open questions for the ratifier - -**Q1 — the empty founding-decision slot.** §9.2 removes `graphs/canonical/founding-decision.ttl` as a -shipped empty file, keeping the slot as a documented path. This is read from "the zero-triple -tolerance goes with the empty slot". If instead the empty file should ship, the class rule needs a -zero-triple tolerance and a fresh instance's CI passes only by that tolerance — the exemption -returns in a different coat. **Recommendation: drop the empty file.** - -**Q2 — one reader or two.** The instance's CI could run `product registry check` from a pinned -released binary instead of `pyshacl`, collapsing §6's two readers into one. It would also make every -instance's CI depend on a tool the registry is supposed to outlive. **Recommendation: keep `pyshacl` -in the instance, keep the fail-closed native reader in the fixtures, and record the divergence risk -rather than trading it for a tool dependency.** - -**Q3 — a decision exemplar in the template.** `graphs/canonical/_exemplar.ttl` demonstrates an -assertion; `shapes/decision.ttl` arrives at Gate 3 with no exemplar to validate against in the -template itself. Adding `_exemplar-decision.ttl` would keep "the template validates as-is" true for -both halves of the class rule. It is a template addition beyond what Gate 3 sanctions, so it is -asked rather than assumed. **Recommendation: add it.** - -**Q4 — `--generated-by` is required, with no default.** Defaulting it to the ambient git identity -would make generation depend on unrecorded environment state. Required means it is always in the -birth provenance because someone typed it. **Recommendation: keep it required.** - -**Status line: the design is proposed; nothing is implemented; the mechanism is held at Gate 1.** diff --git a/docs/g-track/registry-generator-session-2026-08-18.md b/docs/g-track/registry-generator-session-2026-08-18.md deleted file mode 100644 index cb4fb8c1..00000000 --- a/docs/g-track/registry-generator-session-2026-08-18.md +++ /dev/null @@ -1,259 +0,0 @@ -# The registry generator — session report (G-track, 2026-08-18) - -**Repository:** `Hafeok/product-cli`, branch `claude/registry-generator-mechanism-wf1dsr`. -**Gates:** 1 design · 2 implementation · 3 template changes · 4 close. All four ratified by -emil@okkels-klein.dk. **Design document:** `docs/g-track/registry-generator-design-2026-08-18.md`. - -**Status: the generator is the mechanism; G0 regenerates through it.** - ---- - -## 1. Why this session existed - -The G0 session generated a registry instance with a shell one-liner and hit a **silent corruption of -the base IRI**: a sigil in a parameter value was re-read as code by the substitution engine, and the -wrong value propagated into every file including the birth provenance. A human reading the output -caught it. The base IRI is the one registry parameter with no supersession path — a changed IRI -orphans identifiers rather than superseding them (`g-dec-03`). - -Two faults, not one. String substitution re-reads values as code in every engine that does it, and a -scratchpad script cannot be tested. Registry generation mints identity, records birth provenance, and -creates the artefact a founding decision is filed into. The remedy is a subcommand with fixtures. - ---- - -## 2. The finding: the generator paid for itself before it minted anything - -`graphs/canonical/_exemplar.ttl` carried its provenance attribution as -`` — a **second occurrence of the placeholder host**, -outside the `/ns#` base every other file used. Substituting the base IRI alone would have left that -IRI pointing at `REGISTRY-HOST.example` in every generated instance's data file: a corrupted host in -ratified content, of exactly the class that produced the G0 bug, and invisible to anyone reading the -diff for the parameter they supplied. - -The gate's Check B found it at Gate 2, before any tree was written. **Correction applied:** the -attribution now sits in the instance's own namespace, `reg:agent-g1-session`, leaving one host token -in the template. This is the claim made for building the mechanism, demonstrated rather than argued. - ---- - -## 3. The subcommand surface - -``` -product registry generate --owner --repo --ratifier \ - --display-name --base-iri --date \ - --generated-by --out - -product registry check -``` - -Slice + adapter, as the workspace does everywhere: the pure slice is `product-core/src/registry/` -(`params` · `template` · `substitute` · `verify` · `plan` · `apply` · `check/`), the thin adapter is -`product-cli/src/commands/registry.rs`. No MCP mirror: the command mutates no `.product` graph in any -repository, and repository minting does not belong inside a phase-gated authoring session. - -The template stays at `docs/g-track/registry-template/` — the path the PRD and every instance's -provenance pin — and is embedded by `include_str!` with a manifest-drift test, so a template file -added without wiring fails a test rather than vanishing from generated instances. - -### The public surface, as the governance gate read it - -The DDD contract-surface gate failed the first CI run with **151 undischarged changes** — a fair -reading of what had been published: every internal type of the renderer, the gate and the shapes -reader stood as public API, plus a test-support module that is scaffolding rather than surface. - -Two things were wrong, and both were fixed. The slice's public surface is now its **re-export list** — -the acts (`plan_generation`, `apply_generation`, `check_instance`, `evaluate`) with the types they -carry — and the modules beneath are crate-internal, so a caller depends on the acts and never on how -the template is rendered or how the gate is spelled. That took the governed surface from 151 events -to **59**, which is the surface actually intended. Those 59 are discharged by ten seam declarations -under `.ddd/seams/`, each carrying what a caller learns at that boundary *and what the boundary -cannot do* — the apply seam states that publishing is not among its powers, the check seam states its -own limit as a second reader of pySHACL's rules, the params seam states that validation judges -meaning and never character safety. - -One lint surfaced with the narrowing rather than being introduced by it: clippy's -`enum_variant_names` does not fire on public enums, so the shapes reader's constraint enum was -renamed `Kind` → `Rule` to keep its variants mirroring SHACL's own constraint-component names. - -### The generate / publish split - -**Generation is local and offline.** It renders in memory, gates, refuses a target that is not empty, -writes only beneath `--out`, then `git init` and **one** birth commit with inline identity, no system -or global git config, and author/committer dates pinned to the mint date — so the same parameters -through the same generator produce the same tree bytes and the same commit id. - -**Publication is a separate act and is not a subcommand.** No remote is configured, no network call is -made, no repository is created anywhere. The two commands that would publish are printed for a human -to run. The boundary is where it is because minting identity and publishing it answer to different -authorities, and because only the local half can be a fixture — if publication rode inside `generate`, -the untested half would ride on the tested half's back, which is how G0's one-liner shipped a -corrupted base IRI with a green-looking run. - ---- - -## 4. The substitution mechanism - -Typed arguments; no `--set KEY=VALUE` table, no parameter file. The renderer walks the template **once**: -at each character boundary it matches the longest token from a closed table, copies the parameter's -bytes to the output, records the span it wrote, and advances the cursor past the token. The cursor -never moves backwards and the output is never re-scanned. - -The invariant, stated so a test can assert it: *the output is the concatenation of literal spans of -the template with byte-for-byte copies of parameter values.* Therefore `&`, `\1`, `$0`, a backslash, a -pipe, a backtick, `$(…)` are data — there is no replacement grammar to re-read them in — and -substitution order is irrelevant, because there is no order. - -Sequential `str::replace` is the mechanism that would nearly have worked and does not: a value -inserted by an earlier replacement sits in the buffer that later replacements scan. The fixture -`sequential_replace_would_have_re_read_it` runs both on the same input and shows the naive one -producing `SECOND` where the renderer produces the value that was supplied. That is a bug this session -would have shipped, kept as a test rather than a comment. - -**No templating dependency.** Every candidate engine brings an expression language and an escaping -mode — surface whose purpose is to interpret its inputs. The renderer is ~60 lines and closes the last -hole; auditing an engine's replacement semantics would cost more than the lines it replaced. - ---- - -## 5. The verification gate - -Runs on the in-memory tree **before the first byte reaches disk**. A failure is a refusal, not a -warning: no target directory is created at all. - -- **Check A — round-trip.** The renderer's recorded spans are read back out of the rendered bytes and - compared to the parameters; the per-parameter tally is cross-checked against a naive occurrence - count over the template (two mechanisms counting the same thing); every parameter must reach the - output at least once. This is what would have caught the G0 bug, loudly, at generation time. -- **Check B — no survivors.** No `{{IDENT}}` and no base-IRI sentinel may remain. A placeholder the - template carries without a typed parameter therefore fails generation rather than shipping. -- **`TEMPLATE.md` is asserted, not exempted** — byte-identical to the template's own copy, because it - travels verbatim as the instance's record of what its placeholders were. - -### The value-span masking rule - -Check B reads the template text that *survived* rendering — the rendered file with every recorded -value span masked out — not the raw output. The first run of the hostile-value fixture refused a -display name containing `{{OWNER_ORG}}`. Refusing it would have been fail-closed but wrong: **"values -are data" that stops holding once a value looks like a template is not a property at all.** Masking -keeps both guarantees whole — an unwired placeholder *outside* a value still aborts, and a value is -data end to end. The same reasoning applies to the base-IRI sentinel: a supplied IRI containing -`REGISTRY-HOST.example` is the parameter, not a survivor. - ---- - -## 6. Fixtures — 70 new tests, all passing - -52 unit tests in `product-core/src/registry/`, 17 generation fixtures in -`product-cli/tests/registry_generator.rs`, 1 divergence measure in -`product-cli/tests/registry_shacl_divergence.rs`. - -| Fixture | Asserts | Result | -|---|---|---| -| `parameters_round_trip_byte_identically` | every parameter byte-identical at every site | pass | -| `g0_regression_a_sigil_in_the_ratifiers_address_survives` | strike out every whole value; **no fragment** of the address or base IRI remains anywhere — the shape the G0 corruption took | pass | -| `the_g0_handoff_parameters_mint_the_handed_over_instance` | the handoff's five parameters + mint date; base IRI `tag:emil@okkels-klein.dk,2026-08-17:ground/` byte for byte, in the prefix, the `reg:baseIri` literal, the shapes, the exemplars | pass | -| `hostile_parameter_values_are_data` | `&`, `\1`, `$0`, `` ` ``, `$(…)`, `|`, `/`, a literal `{{OWNER_ORG}}` | pass | -| `sequential_replace_would_have_re_read_it` | the bug that would have shipped, shown failing | pass | -| `no_placeholder_survives_the_generated_tree` | zero across the tree | pass | -| `template_md_travels_verbatim` | byte-identical to the template's copy, placeholders intact | pass | -| `the_birth_provenance_is_complete_as_rdf` | queried as RDF, not string-matched: both versions, every parameter, the agent | pass | -| `a_fresh_instance_passes_its_own_rules` | file rule + shapes clean on a fresh instance | pass | -| Negative shapes | institutional Reading without a trust decision · two assertions in one file · decision missing its falsifier · decision dated by a bare string · provenance outside the vocabulary · an empty file | all **fail** as required | -| `unsupported_shacl_construct_fails_closed` · `an_unreadable_shape_fails_closed_with_its_own_heading` | an unreadable shape fails, in its own section, with no violations section | pass | -| `a_refused_parameter_writes_nothing` | gate refuses → **no target directory** | pass | -| `generation_refuses_a_non_empty_target` | prior content untouched | pass | -| `generation_is_deterministic` | identical bytes **and** identical commit id | pass | -| `two_generations_in_one_run_do_not_interfere` · `generation_leaves_no_residue_outside_its_target` | teardown provable | pass | -| `generation_configures_no_remote` | no remote; publish commands printed, never run | pass | -| `embedded_template_matches_on_disk` · version-constant drift | manifest and header cannot drift | pass | - -Workspace gates green at every hold: `cargo t` (1352 tests, 0 failed), `cargo clippy --workspace -- -D -warnings -D clippy::unwrap_used`, `cargo xtask check`. - ---- - -## 7. The template at 0.2.0 - -One bump covering every change this session made — the template is not released between gates, so two -bumps would record a state that never existed. - -- **`shapes/decision.ttl`** — title, resolution, region, falsifier, ratifier, status, date required; - `basis` / `acceptedCost` / `revisitIf` deliberately **not**, because a decision may honestly have - none and demanding them produces filler. `reg:made` is constrained to `xsd:date`. `reg:ratifiedBy` - is left unconstrained as to node kind: whether a ratifier is named by literal or IRI depends on - whether the owner has a durable identifier for people, which is an instance's ruling — the G0 - instance uses an email literal, an institutional instance might use an IRI. -- **`graphs/canonical/_exemplar-decision.ttl`** — a conforming example, so a ratifier learns the form - from a file rather than from SHACL. It carries `{{RATIFIER}}` so the form looks like theirs, with a - status stating plainly that it is not ground. -- **The CI file rule** — generalised from a filename exemption to the class rule it stood in for: - exactly one assertion **or** exactly one decision per `graphs/**/*.ttl`, no exemptions. The - zero-triple tolerance went with the empty founding-decision file, which is no longer shipped: the - slot is a path the ratifier creates. A rule with an exception decays. -- **TEMPLATE.md step 1** no longer reads both ways — the file travels **verbatim**, unsubstituted. -- **The base IRI documents two routes** with their trade-offs: a host the owner controls durably - (resolvable from day one, and a real commitment through rebrands and transfers), or - location-independent minting per `g-dec-03` (`tag:`/`urn:`, HTTP form published later as a - projection through a rebasing parameter, accepted cost an internal-to-published mapping). Neither is - the default. The README records that **which route an instance took is a decision filed in that - registry, not a fact recoverable from the IRI** — a `tag:` IRI records the mint date and authority - but not whether the deferral was deliberate or a durable host was rejected. -- **`GENERATION.ttl` is parameterised**, filled by the same renderer under the same gate, recording - the generator's version alongside the template's, and correcting the PROV: `prov:wasGeneratedBy` the - act, `prov:wasAttributedTo` the agent. - ---- - -## 8. The pySHACL divergence measure - -The instance's own CI runs `pyshacl`; the fixtures run a fail-closed native reader that evaluates a -defined SHACL subset compiled to SPARQL over oxigraph, because a gate depending on a Python toolchain -becomes a habit the moment it is skipped when absent. Anything outside the subset is reported as -**unevaluable** and fails the check — never silently skipped, and reported distinctly from data -violating a shape. - -Two readers of one rule set is a **standing finding**, so it is measured rather than assumed small. -`the_native_reader_agrees_with_pyshacl` runs both over nine cases whenever pySHACL is present and -fails on any disagreement; when absent it says on stderr that the divergence was not measured. - -**Run in this session** — pySHACL 0.40.1, nine cases: shipped assertion exemplar, shipped decision -exemplar, conforming Reading, institutional without a trust decision, institutional with one, -provenance outside the vocabulary, assertion without a predicate, decision missing its falsifier, -decision dated by a bare string. **No disagreement.** Non-vacuity confirmed: pySHACL independently -reports `Conforms: False` on the negatives, and a fresh 0.2.0 instance was additionally validated by -the instance workflow's own two steps run verbatim (`pyshacl -a` over `shapes/`, and the workflow's -rdflib file-rule script) — both clean. - ---- - -## 9. What G0 must do differently when it resumes - -1. **Regenerate through the subcommand.** No shell substitution, no hand-editing of the tree. The - invocation is in §3 with the handoff's parameters; the `the_g0_handoff_parameters_...` fixture - pins it. -2. **The same five parameters and the same mint date, 2026-08-17.** The mint date records when the - authority was demonstrably controlled, not when the tree was written; it does not move because the - tree was rewritten. -3. **File the two ratified texts verbatim** — the founding decision and `g-dec-03`, from - *Founding content, ratified as text*. They were ratified as text, not as the tree they were - written into; a paraphrase at this handoff is the drift the founding decision's own - non-graduation clause guards against. -4. **The founding decision creates its file.** `graphs/canonical/founding-decision.ttl` no longer - ships as an empty slot; the ratifier's first merge creates it, conforming to - `shapes/decision.ttl`. -5. **Do not re-apply the two template additions as instance-local.** `shapes/decision.ttl` and the - generalised file rule are in the template at 0.2.0; an instance that re-adds them locally forks - the template. -6. **The instance records template 0.2.0 and the generator's version.** Re-pinning is possible - against either. -7. **Publishing stays a separate, explicit act.** `Hafeok/ground-registry-g0` is still empty; the - generator prints the two commands and pushes nothing. - ---- - -## 10. What this session did not touch - -G0 itself; the extractor; any instance published to any remote (`Hafeok/ground-registry-g0` remains -empty); the PRD; canon. No repository was created under any organisation. The session pushed to its -own branch and nowhere else. diff --git a/docs/g-track/registry-template/.github/workflows/validate.yml b/docs/g-track/registry-template/.github/workflows/validate.yml deleted file mode 100644 index d008b5db..00000000 --- a/docs/g-track/registry-template/.github/workflows/validate.yml +++ /dev/null @@ -1,108 +0,0 @@ -# SHACL validation on every change (PRD §3, authority row; G-1 session prompt step 3.2). -# Shapes start minimal — the §4.1 Reading tuple constraint, structural -# well-formedness, the decision shape — and grow at G0. -# -# Format: Turtle (G-1 Gate 3 ruling) — the authority file is the assertion, -# SHACL reads it directly, zero conversion in CI. -# -# File rule (one rule, no exemptions): -# - every graphs/**/*.ttl carries exactly one assertion OR exactly one -# decision. Nothing else, and never both. -# - `_`-prefixed files are exemplars: validated like any other file, excluded -# from projection builds. -# - the rule is scoped to graphs/. runs/ holds per-run *evidence*, not -# ratified content: it carries no assertion and no decision, which is why -# it lives outside graphs/ rather than inside it with an exemption. -# -# SHACL runs over the JOIN — graphs/ + runs/ + vocabulary/ — because that is -# what a consumer reads, and because the derivation half of an assertion's -# shape lives in the run evidence. Validating graphs/ alone would pass shapes -# the projection then fails, which is precisely the interaction 0.3.0's -# class-targeted shapes exist to keep from recurring. -# -# The rule used to exempt graphs/canonical/founding-decision.ttl by filename so -# an empty slot could ship. The exemption was generalised into the rule it stood -# in for, and the empty file went with it: the founding decision is created by -# the ratifier who files it, so there is no empty file to tolerate. - -name: validate -on: - push: - pull_request: - -jobs: - shacl: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install validators - run: pip install pyshacl rdflib - - name: One assertion or one decision per file - run: | - python - <<'EOF' - import glob, sys - from rdflib import Graph - from rdflib.namespace import RDF, Namespace - REG = Namespace("https://REGISTRY-HOST.example/ns#") - bad = [] - for path in glob.glob("graphs/**/*.ttl", recursive=True): - g = Graph(); g.parse(path, format="turtle") - assertions = set(g.subjects(RDF.type, REG.Assertion)) - decisions = set(g.subjects(RDF.type, REG.Decision)) - counts = (len(assertions), len(decisions)) - if counts not in ((1, 0), (0, 1)): - bad.append( - f"{path}: {counts[0]} assertion(s), {counts[1]} decision(s) " - "— exactly one assertion or exactly one decision required" - ) - if bad: - print("\n".join(bad)); sys.exit(1) - print("one-assertion-or-one-decision-per-file: ok") - EOF - - name: Sidecar entries resolve - run: | - python - <<'EOF' - import glob, re, sys - from rdflib import Graph - from rdflib.namespace import RDF, Namespace - REG = Namespace("https://REGISTRY-HOST.example/ns#") - ratified = set() - for path in glob.glob("graphs/**/*.ttl", recursive=True): - g = Graph(); g.parse(path, format="turtle") - ratified |= {str(s) for s in g.subjects(RDF.type, REG.Assertion)} - runs = sorted(glob.glob("runs/**/*.ttl", recursive=True)) - if not runs: - print("sidecar entries resolve: no run evidence yet"); sys.exit(0) - total = 0 - for path in runs: - g = Graph(); g.parse(path, format="turtle") - named = {str(s) for s in g.subjects(REG.derivedByRule, None)} - orphans = sorted(named - ratified) - total += len(orphans) - print(f"{path}: {len(named)} entr(ies), {len(orphans)} orphan(ed)") - for o in orphans: - print(f" orphan: {o}") - # Identity is content-addressed, so an orphan is a real finding: a run - # against the wrong instance, or evidence for a rejected proposal. - sys.exit(1 if total else 0) - EOF - - name: SHACL - run: | - python - <<'EOF' - import glob, sys - from pyshacl import validate - from rdflib import Graph - data = Graph() - for pattern in ("graphs/**/*.ttl", "runs/**/*.ttl", "vocabulary/**/*.ttl"): - for path in glob.glob(pattern, recursive=True): - data.parse(path, format="turtle") - shapes = Graph() - for path in glob.glob("shapes/*.ttl"): - shapes.parse(path, format="turtle") - conforms, _, text = validate(data, shacl_graph=shapes, advanced=True) - print(text) - sys.exit(0 if conforms else 1) - EOF diff --git a/docs/g-track/registry-template/GENERATION.ttl b/docs/g-track/registry-template/GENERATION.ttl deleted file mode 100644 index 9998f51d..00000000 --- a/docs/g-track/registry-template/GENERATION.ttl +++ /dev/null @@ -1,42 +0,0 @@ -# Birth provenance of this instance — written by the generation act. -# -# `product registry generate` fills this file through the same single-pass -# renderer, under the same verification gate, as every other file in the tree: -# the birth provenance is produced by the mechanism, never typed beside it. -# -# It records the template version, the generator's own version, every parameter -# supplied, who generated it, and the mint date — which is when the authority -# was demonstrably controlled, not when the tree was written. Recording both -# versions is what lets the instance re-pin when either one moves. -# -# The base IRI is a generation parameter; the placeholder is IRI-valid so the -# template itself parses. - -@prefix reg: . -@prefix prov: . -@prefix xsd: . - -reg:this-instance - a reg:Registry ; - reg:displayName "{{DISPLAY_NAME}}" ; - reg:owner "{{OWNER_ORG}}" ; - reg:repository "{{REPO_NAME}}" ; - reg:ratifier "{{RATIFIER}}" ; - reg:baseIri "https://REGISTRY-HOST.example/ns#" ; - reg:mintDate "{{MINT_DATE}}"^^xsd:date ; - reg:templateVersion "{{TEMPLATE_VERSION}}" ; - reg:generatorVersion "{{GENERATOR_VERSION}}" ; - prov:wasGeneratedBy reg:generation-act ; - prov:wasAttributedTo reg:generating-agent . - -reg:generation-act - a prov:Activity ; - reg:templateVersion "{{TEMPLATE_VERSION}}" ; - reg:generatorVersion "{{GENERATOR_VERSION}}" ; - prov:wasAssociatedWith reg:generating-agent ; - prov:generatedAtTime "{{MINT_DATE}}T00:00:00Z"^^xsd:dateTime ; - prov:generated reg:this-instance . - -reg:generating-agent - a prov:Agent ; - reg:name "{{GENERATED_BY}}" . diff --git a/docs/g-track/registry-template/README.md b/docs/g-track/registry-template/README.md deleted file mode 100644 index 1c840b5d..00000000 --- a/docs/g-track/registry-template/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# {{DISPLAY_NAME}} — a ground registry - -The canonical store of {{OWNER_ORG}}'s ratified ground: entities, relations, axes, trust -decisions. This repository is the **authority**; every SPARQL endpoint built from it is a -projection pinned to a ref, never a source. The design is the G-track PRD's §3 -(`product-cli/docs/g-track/prd-ground-as-ontology.md`) — this page states the mechanics and cites -it rather than restating it. This instance was generated from the registry template; its birth -provenance (template version, parameters, generated-by, date) is `GENERATION.ttl`. - -## Structure - -| Path | Holds | -|---|---| -| `graphs//` | one named graph per directory; **one assertion per file, in Turtle** (the per-claim file pattern applied to triples — the pattern transfers: per-assertion files, PR review, supersession; G-1 Gate 3 ruled the format) | -| `graphs/canonical/` | the ratified graph | -| `graphs/canonical/founding-decision.ttl` | **the founding-decision slot** — a path, not a shipped file: {{OWNER_ORG}}'s decision to keep a registry, filed by {{RATIFIER}} as the first ratified content, creates it. The form is `shapes/decision.ttl`, with a conforming example in `graphs/canonical/_exemplar-decision.ttl` | -| `shapes/` | SHACL shapes; CI runs them on every change | -| `scripts/build-projection.sh` | ref in, store build out (stub until G0) | -| `GENERATION.ttl` | this instance's birth provenance | - -There is no `proposals/` directory, deliberately. **Proposals are branches, not directories**: a -proposal graph (an extraction run, a session's proposed Readings) lands as per-triple files on a -branch, named-graph directory included, `prov:wasAttributedTo` its producer and pinned to its -source ref. - -## Ratification - -1. A proposal branch is opened; its files are the proposed triples, nothing else. -2. Review is **per triple, never per run** — each file is a line in the PR diff, accepted or - rejected individually (PRD §5.1, §5.6: wholesale acceptance is manufactured ground). -3. **{{RATIFIER}} merges.** The merge is the ratification act; supersession is a new file plus a - retraction marker, never a rewrite of history. -4. The endpoint rebuilds from the merge ref (`scripts/build-projection.sh`); every Reading it - serves carries that ref (PRD §4.1 — as-of semantics for free). - -## Validation - -CI runs SHACL on every change (`.github/workflows/validate.yml`). The shapes start minimal and -grow at G0: - -- **Reading tuple** (`shapes/reading.ttl`): the PRD §4.1 constraint — a Reading carries value, - as-of, provenance, assurance; `institutional` provenance **requires** a `trust_decision` - reference; the provenance value set is track vocabulary per G-track decision `g-dec-01` - (superseded the moment canon files provenance typing). -- **Structural well-formedness** (`shapes/structural.ttl`): every data file parses; an assertion - names exactly one subject, predicate, object. -- **The decision shape** (`shapes/decision.ttl`): a decision carries title, resolution, region, - falsifier, ratifier, status, date. `basis`, `acceptedCost` and `revisitIf` are deliberately not - required — a decision may honestly have none, and demanding them produces filler. -- **One assertion or one decision per file**, across every `graphs/**/*.ttl`. One rule, no filename - exemptions. - -## Ownership - -Ownership is a **generation parameter** of this instance: {{OWNER_ORG}} owns this registry, and -that ownership is the accountable-principal field of every trust decision that references it -(PRD §4.4 — connecting is a trust decision, Q27). - -The base IRI is the one parameter with no supersession path: a changed IRI orphans identifiers -rather than superseding them. It was settled at generation by one of the two routes the template -documents — a host {{OWNER_ORG}} controls durably, or location-independent minting (`tag:`/`urn:`) -with the resolvable HTTP form produced later as a projection. Which route this instance took, and -why, is a decision filed in this registry, not a fact recoverable from the IRI alone. diff --git a/docs/g-track/registry-template/TEMPLATE.md b/docs/g-track/registry-template/TEMPLATE.md deleted file mode 100644 index 721139f8..00000000 --- a/docs/g-track/registry-template/TEMPLATE.md +++ /dev/null @@ -1,129 +0,0 @@ -# Registry template - -**Template version: 0.3.0** (G-1 session 2026-08-17; revised by the registry-generator session -2026-08-18 and by the two-axis session 2026-08-20; versioned with product-cli — instances pin this -version and re-pin when it moves, by the same re-derive-at-a-named-commit discipline the G-track -itself runs). - -**0.3.0 is backward compatible with content ratified at 0.2.0.** No file changes and none becomes -non-conformant. The two grading axes and the derivation record are constrained on the class -`reg:GradedAssertion`, which the writer stamps explicitly and nothing entails; an assertion written -at 0.2.0 does not carry it, is not targeted, and keeps its bytes. See *The two axes* below. - -This directory is a **parameterised template for any ground registry**. It is not a repository -and names no instance: everything instance-specific is a generation parameter, supplied when an -instance is generated — never filled in the template (G-1 Gate 3 ruling). - -## Generation parameters - -| Parameter | Placeholder in the template | Filled at generation | -|---|---|---| -| Owning organisation | `{{OWNER_ORG}}` | the accountable-principal field of every trust decision that will reference the instance | -| Repository name | `{{REPO_NAME}}` | — | -| Ratifier | `{{RATIFIER}}` | the named person whose merge is ratification for this instance | -| Registry display name | `{{DISPLAY_NAME}}` | — | -| Base IRI | `https://REGISTRY-HOST.example/ns#` (parses as a valid IRI, so the template validates as-is) | the base every identifier is minted under. **Two routes, below** — it is the one parameter with no supersession path, so the choice is made deliberately or deferred deliberately | -| Mint date | `{{MINT_DATE}}` | when the authority was demonstrably controlled — **not** when the tree was written. It does not move when an instance is regenerated | -| Generated by | `{{GENERATED_BY}}` | who ran the generation; recorded as the agent in the birth provenance | -| Template version | `{{TEMPLATE_VERSION}}` | derived — this file's version, written into `GENERATION.ttl` | -| Generator version | `{{GENERATOR_VERSION}}` | derived — the generator's own version, so an instance can re-pin when *either* moves | - -### The base IRI: two routes - -The base IRI is the one registry parameter with **no supersession path** — a changed IRI orphans -identifiers rather than superseding them. There are two honest ways to settle it, and an instance -picks one at generation: - -1. **Durable host.** Mint under `https:///…`, resolvable from day one. - The commitment is real: the owner must keep that host resolving for the registry's lifetime, - through rebrands, domain transfers and hosting changes. Choose it when the owner's durable - domain is already settled and outlives any particular deployment. -2. **Location-independent, published later** (the route `g-dec-03` establishes). Mint under an - RFC 4151 `tag:` or a `urn:` — identity that names no host and therefore cannot be orphaned by - one. The resolvable HTTP form is then a **projection** of identity, produced by the projection - build through a rebasing parameter when a durable domain exists, never by rewriting stored - assertions. The accepted cost is an internal-to-published identity mapping at projection - build; the cost is worth paying when the owner's durable domain is not yet determined, because - the alternative is an unrecoverable commitment rather than a convenience. - -Neither route is the template's default. An instance generated before its owner's durable domain -is settled should take route 2 and record the deferral as a filed decision in its own graph. - -## Generating an instance - -Use the generator — `product registry generate` in product-cli. It renders this template with typed -parameters through a single-pass substitution that never re-reads a value as code, holds the result -at a verification gate, and writes the tree only if the gate passes. Hand-substitution with `sed` or -a shell one-liner is how the first generated instance acquired a silently corrupted base IRI. - -``` -product registry generate \ - --owner --repo --ratifier --display-name \ - --base-iri --date --generated-by --out -``` - -1. **This file travels verbatim.** `TEMPLATE.md` is copied into the instance **unsubstituted**: its - placeholders stay as placeholders, because the instance's record of *what its parameters were* - is what lets it re-pin when the template moves. Every other file is substituted. (This was - ambiguous in 0.1.0 and is now stated: no parameter is ever written into this file.) -2. **The gate refuses rather than warns.** Before anything is written: every supplied parameter must - appear in the output byte-identical to its input, and no placeholder may survive anywhere in the - tree. A failure aborts generation, leaving no directory behind. -3. **The birth provenance is generated, not typed.** `GENERATION.ttl` is a parameterised file filled - by the same mechanism under the same gate: template version, generator version, every parameter, - the generating agent, the mint date — `prov:wasGeneratedBy` the generation act, - `prov:wasAttributedTo` its agent. This is what lets the instance re-pin when either version moves. -4. **Generation is local; publishing is a separate act.** The generator initialises a repository and - records one birth commit. It configures no remote and pushes nothing. -5. **The founding decision is the instance's first ratified content.** It lands at - `graphs/canonical/founding-decision.ttl` — a **path, not a shipped file**. The owning - organisation's decision to keep a registry, filed by its ratifier, creates it. The template - provides the form (`shapes/decision.ttl`, with a conforming example in - `graphs/canonical/_exemplar-decision.ttl`); only the instance can fill it. - -## What CI enforces in a generated instance - -- **One assertion or one decision per file**, across every `graphs/**/*.ttl`. One rule, no filename - exemptions: 0.1.0 exempted the empty founding-decision slot, and generalising the exemption into - the rule it stood in for is what removed both the carve-out and the empty file. -- **The shapes in `shapes/`**: the §4.1 Reading tuple (`reading.ttl`), structural well-formedness of - an assertion (`structural.ttl`), what a decision must carry (`decision.ttl`), and the two grading - axes with their derivation record (`derivation.ttl`). -- **The shapes run over the join**, not over `graphs/` alone: `graphs/`, `runs/` and `vocabulary/` - are loaded together, because that is what a consumer reads and because the derivation half of an - assertion's shape lives in the run evidence. The **file rule** stays scoped to `graphs/`. -- **Sidecar entries resolve.** Every assertion id a `runs/` file names must exist under `graphs/`. - Identity is content-addressed, so an orphan is a real finding — a run against the wrong instance, - or evidence for a proposal that was rejected — and it is reported per run rather than assumed away. - -## The two axes, and where derivation lives (0.3.0) - -`reg:assuranceGrade` measured one property while being read as two. It is split: - -| | Predicate | Value set | Where it lives | -|---|---|---|---| -| Axis 1 — **derivation confidence** | `reg:derivationConfidence` | `high · mid · low · not-derivable` | the assertion file | -| Axis 2 — **domain relevance** | `reg:domainRelevance` | `high · low · unknown` | the assertion file | -| Axis 2's basis | `reg:relevanceBasis` | `computed · defaulted · not-computed · not-applicable` | the assertion file | -| **Derivation record** | `reg:derivedByRule` · `reg:standsOn` · `reg:memberKind` · `reg:containerKind` · `reg:sourcePath` | see `vocabulary/reg.ttl` | `runs/-at-.ttl` | - -Axis 1's value set is unchanged from `reg:assuranceGrade`, deliberately: that column was already -measuring derivation and nothing else, so the split renames it rather than re-marking it. -`vocabulary/reg.ttl` declares the two predicates `owl:equivalentProperty`, so **one query answers -over both generations** and no ratified assertion is rewritten to gain the new name. - -Axis 2 is not ordinal. `unknown` is the absence of a verdict, not a value between `high` and `low`, -which is why it carries a basis: *unknown because nothing computed it* and *unknown because the row -proposes nothing* are different findings. - -**Derivation is evidence, not ratified claim.** It lives in the per-run evidence layer under -`runs/`, keyed by the assertion's own content-addressed id, leaving `graphs/` pure ratified -assertion. Two things follow. A cohort ratified before the split can be given its derivation by -**re-running the deterministic extractor at the pinned ref** — no byte of ratified content changes. -And because the id is the join key, an entry either matches an assertion or is an orphan, which is a -check rather than an assumption. - -## Instance generation is a G0-entry step - -The G-track's first instance is generated at G0 entry, its parameters supplied then. No instance -exists at G-1, by ruling. diff --git a/docs/g-track/registry-template/graphs/canonical/_exemplar-decision.ttl b/docs/g-track/registry-template/graphs/canonical/_exemplar-decision.ttl deleted file mode 100644 index 3222fd30..00000000 --- a/docs/g-track/registry-template/graphs/canonical/_exemplar-decision.ttl +++ /dev/null @@ -1,25 +0,0 @@ -# Exemplar — one decision per file, in Turtle. Underscore prefix marks it an -# exemplar: CI validates it, the projection build excludes `_`-prefixed files. -# -# It exists so the decision shape has a conforming example in the template -# itself: a ratifier learns the form from this file rather than from SHACL. -# The optional fields — basis, acceptedCost, revisitIf — are shown here, and -# are not required; a decision honestly having none of them still conforms. - -@prefix reg: . -@prefix prov: . -@prefix xsd: . - -reg:decision-0001 - a reg:Decision ; - reg:title "Exemplar: assertions are reviewed per triple" ; - reg:resolution "Proposed assertions are accepted or rejected one file at a time. A run is not a unit of acceptance; wholesale acceptance of an extraction run is manufactured ground." ; - reg:region "This exemplar only. It governs nothing: it is the template's worked example of the decision form." ; - reg:falsifier "A merge accepting a proposal branch without per-file review, treated afterwards as ratified ground." ; - reg:basis "Per-triple review is what makes ratification an act rather than a formality." ; - reg:acceptedCost "Review effort proportional to the number of proposed triples." ; - reg:revisitIf "Extraction volume makes per-triple review impossible in practice at some scale, and that scale is reached." ; - reg:ratifiedBy "{{RATIFIER}}" ; - reg:status "exemplar — not ratified, and not ground" ; - reg:made "2026-08-17"^^xsd:date ; - prov:wasAttributedTo reg:agent-registry-template . diff --git a/docs/g-track/registry-template/graphs/canonical/_exemplar.ttl b/docs/g-track/registry-template/graphs/canonical/_exemplar.ttl deleted file mode 100644 index a4c2c13a..00000000 --- a/docs/g-track/registry-template/graphs/canonical/_exemplar.ttl +++ /dev/null @@ -1,23 +0,0 @@ -# Exemplar — one assertion per file, in Turtle (the format ruled at G-1 Gate 3). -# Underscore prefix marks it an exemplar: CI validates it, the projection build -# excludes `_`-prefixed files. Base IRI is a generation parameter (TEMPLATE.md); -# the placeholder parses as a valid IRI so the template validates as-is. -# -# The file carries exactly one asserted triple. Its Reading annotations ride on -# the assertion node (RDF reification kept deliberately boring until the -# vocabulary ruling A1 lands a better idiom). - -@prefix reg: . -@prefix prov: . -@prefix xsd: . - -reg:assertion-0001 - a reg:Assertion , reg:Reading ; - reg:subject reg:Order ; - reg:predicate reg:hasPart ; - reg:object reg:OrderLine ; - reg:value "Order hasPart OrderLine" ; - reg:asOf "2026-08-17T00:00:00Z"^^xsd:dateTime ; - reg:provenance reg:controlled ; - reg:assurance "exemplar — hand-authored" ; - prov:wasAttributedTo reg:agent-g1-session . diff --git a/docs/g-track/registry-template/scripts/build-projection.sh b/docs/g-track/registry-template/scripts/build-projection.sh deleted file mode 100755 index cde47625..00000000 --- a/docs/g-track/registry-template/scripts/build-projection.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# Projection build — takes a ref, emits a store build. STUB until G0. -# -# The endpoint is a projection of this repository, never the source (PRD §3, -# Q30's diagnostic applied to our own database). Every Reading the endpoint -# serves carries the ref this build was made from (PRD §4.1). -set -euo pipefail - -REF="${1:?usage: build-projection.sh [out-dir]}" -OUT="${2:-build/${REF}}" - -# TODO(G0), in order: -# 1. Materialise the tree at $REF (git worktree/archive — never the working tree). -# 2. Load graphs//*.ttl into an Oxigraph store build at $OUT, -# one named graph per directory; skip `_`-prefixed exemplars. -# 2b. Join the other two layers (template 0.3.0): -# - runs/*.ttl — per-run extraction evidence, keyed by assertion id. The -# authority repo keeps assertion and evidence apart; the projection is -# where a consumer reads them together, so derivation is queryable here -# without either layer borrowing the other's status. -# - vocabulary/reg.ttl — the registry's own terms, including the -# `reg:assuranceGrade owl:equivalentProperty reg:derivationConfidence` -# axiom that lets one query answer over both generations of assertion. -# The Rust form of this join is product_core::ground::project, whose -# fixtures validate the projection rather than the repo alone: a shape -# keyed on predicate presence passes pre-entailment and fails here. -# 3. Inference hook — ruled at G-1 Gate 2, recorded as g-dec-02: -# CONSTRUCT-to-fixpoint on Oxigraph (the pf::sparql_rules shape; no new -# dependency), `reasonable` crate as named fallback; revisit_if: fixpoint -# wall-time exceeds the projection-build budget at G0 scale. Every entailed -# triple carries prov:wasDerivedFrom (asserted triples + rule id) and -# assurance = the rule. Entailments never enter this repository. -# 4. Stamp the build with $REF; the endpoint serves nothing unstamped. - -echo "stub: projection build for ref '${REF}' -> '${OUT}' — not implemented before G0" >&2 -exit 64 diff --git a/docs/g-track/registry-template/shapes/decision.ttl b/docs/g-track/registry-template/shapes/decision.ttl deleted file mode 100644 index 5d1c8de1..00000000 --- a/docs/g-track/registry-template/shapes/decision.ttl +++ /dev/null @@ -1,62 +0,0 @@ -# Decision shape — what a ratified decision must carry to be one. -# -# Required: title, resolution, region, falsifier, ratifier, status, date. A -# decision without a region governs nothing in particular; one without a -# falsifier cannot be shown wrong, and an unfalsifiable decision is a -# preference wearing a decision's clothes. -# -# Deliberately NOT required: basis, acceptedCost, revisitIf. A decision may -# honestly have none of them — no cost worth recording, no condition that -# would reopen it — and demanding them produces filler, which is worse than -# their absence because filler reads as considered. -# -# `reg:ratifiedBy` is left unconstrained as to node kind: whether a ratifier is -# named by literal or by IRI is an instance's ruling, not the template's. - -@prefix sh: . -@prefix xsd: . -@prefix reg: . - -reg:DecisionShape - a sh:NodeShape ; - sh:targetClass reg:Decision ; - sh:property [ - sh:path reg:title ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:message "A decision carries exactly one title." ; - ] ; - sh:property [ - sh:path reg:resolution ; - sh:minCount 1 ; - sh:message "A decision carries its resolution — what was decided." ; - ] ; - sh:property [ - sh:path reg:region ; - sh:minCount 1 ; - sh:message "A decision carries the region it governs; an unregioned decision governs nothing in particular." ; - ] ; - sh:property [ - sh:path reg:falsifier ; - sh:minCount 1 ; - sh:message "A decision carries a falsifier — what would show it wrong." ; - ] ; - sh:property [ - sh:path reg:ratifiedBy ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:message "A decision names exactly one ratifier; ratification is one person's merge." ; - ] ; - sh:property [ - sh:path reg:status ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:message "A decision carries exactly one status." ; - ] ; - sh:property [ - sh:path reg:made ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:datatype xsd:date ; - sh:message "A decision carries exactly one date it was made, as xsd:date." ; - ] . diff --git a/docs/g-track/registry-template/shapes/derivation.ttl b/docs/g-track/registry-template/shapes/derivation.ttl deleted file mode 100644 index 0ee12868..00000000 --- a/docs/g-track/registry-template/shapes/derivation.ttl +++ /dev/null @@ -1,112 +0,0 @@ -# The two grading axes, and the derivation record that makes a class of -# rejection findable. Template 0.3.0. -# -# Why this shape targets reg:GradedAssertion and not "carries a confidence": -# -# The vocabulary declares reg:assuranceGrade owl:equivalentProperty -# reg:derivationConfidence, so every assertion ratified before 0.3.0 gains a -# reg:derivationConfidence *at the projection*. A shape keyed on that -# predicate's presence would therefore pass in the authority repo — where -# validation runs before entailment — and fail the moment anyone validated -# the projection. Backward compatibility would hold only by accident of when -# validation happens to run. -# -# reg:GradedAssertion is stamped by the writer and entailed by nothing. The -# shape then says what it means: *assertions written at 0.3.0 or later*. -# Pre-0.3.0 assertions are untargeted, stay conformant, and keep their bytes. -# -# The derivation predicates live in runs/, so the SHACL step loads graphs/, -# runs/ and vocabulary/ together — the join is what is validated, which is also -# what a consumer reads. - -@prefix sh: . -@prefix xsd: . -@prefix reg: . - -reg:GradedAssertionShape - a sh:NodeShape ; - sh:targetClass reg:GradedAssertion ; - - # Axis 1 — how reliably the instrument read the code. - sh:property [ - sh:path reg:derivationConfidence ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:in ( reg:high reg:mid reg:low reg:not-derivable ) ; - sh:message "A graded assertion carries exactly one derivation confidence (§5.2 axis 1)." ; - ] ; - - # Axis 2 — whether it is the kind of thing the registry is for. Never - # optional: an absent relevance is what let a whole rejected class read as - # safe, and `unknown` is the honest mark, not the missing one. - sh:property [ - sh:path reg:domainRelevance ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:in ( reg:high reg:low reg:unknown ) ; - sh:message "A graded assertion carries exactly one domain relevance (§5.2 axis 2); unknown is a mark, not an omission." ; - ] ; - sh:property [ - sh:path reg:relevanceBasis ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:in ( reg:computed reg:defaulted reg:not-computed reg:not-applicable ) ; - sh:message "A relevance mark carries its basis, so `unknown` never reads as a verdict." ; - ] ; - - sh:property [ - sh:path reg:templateGeneration ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:message "A graded assertion names the template generation whose form it was written in." ; - ] ; - - # The derivation record. These sit in the run sidecar, so this half of the - # shape is what the join has to satisfy — an assertion whose evidence never - # arrived fails here rather than passing quietly. - sh:property [ - sh:path reg:mappingRow ; - sh:minCount 1 ; - sh:message "A graded assertion names the §5.2 row that proposed it." ; - ] ; - sh:property [ - sh:path reg:derivedByRule ; - sh:minCount 1 ; - sh:message "A graded assertion names the rule that produced it — the row alone is too coarse to batch on." ; - ] ; - sh:property [ - sh:path reg:standsOn ; - sh:minCount 1 ; - sh:message "A graded assertion names at least one probed operation it stood on." ; - ] . - -# A capability-probe verdict, recorded in the run evidence. -# -# Typed reg:Reading in the vocabulary, so `reading.ttl` already demands its -# value, as-of, provenance and assurance. What this shape adds is the two -# things that make a verdict a verdict rather than a note: which operation it -# is about, and what the call actually did. -# -# It exists because a verdict is *not* a standing fact about the server. The -# same operation has answered on one run and timed out on another over the -# same corpus at the same ref. Two runs that disagree must therefore differ -# visibly — a reviewer can tell "this row found nothing" from "this row was -# gated off that day" only if both verdicts are on the record with their -# as-of. -reg:ProbeVerdictShape - a sh:NodeShape ; - sh:targetClass reg:ProbeVerdict ; - sh:property [ - sh:path reg:operation ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:nodeKind sh:IRI ; - sh:message "A probe verdict names exactly one operation, as an IRI." ; - ] ; - sh:property [ - sh:path reg:answered ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:datatype xsd:boolean ; - sh:message "A probe verdict states whether a live call answered — the verdict is the call, never the advertisement." ; - ] . diff --git a/docs/g-track/registry-template/shapes/reading.ttl b/docs/g-track/registry-template/shapes/reading.ttl deleted file mode 100644 index 9f2aeac7..00000000 --- a/docs/g-track/registry-template/shapes/reading.ttl +++ /dev/null @@ -1,51 +0,0 @@ -# Reading tuple constraint — PRD §4.1, staged minimal per the G-1 session prompt. -# Provenance value set is track vocabulary per g-dec-01 (product-cli -# docs/g-track/decisions/), revisit_if pinned to the Q25/Q27/Q30 filing wave. -# Grows at G0; nothing here is the full §4.2 act vocabulary. - -@prefix sh: . -@prefix reg: . - -reg:ReadingShape - a sh:NodeShape ; - sh:targetClass reg:Reading ; - sh:property [ - sh:path reg:value ; - sh:minCount 1 ; - sh:message "A Reading carries a value (PRD §4.1)." ; - ] ; - sh:property [ - sh:path reg:asOf ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:message "A Reading carries exactly one as-of — clock, not context (PRD §4.1)." ; - ] ; - sh:property [ - sh:path reg:provenance ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:in ( reg:controlled reg:observed reg:inferred reg:institutional ) ; - sh:message "Provenance is one of controlled/observed/inferred/institutional (g-dec-01)." ; - ] ; - sh:property [ - sh:path reg:assurance ; - sh:minCount 1 ; - sh:message "A Reading carries the assurance at which it was made (PRD §4.1)." ; - ] . - -# institutional requires a trust decision (Q27; PRD §4.1: fails SHACL without it). -reg:InstitutionalReadingShape - a sh:NodeShape ; - sh:targetClass reg:Reading ; - sh:sparql [ - a sh:SPARQLConstraint ; - sh:message "institutional provenance requires a trust_decision reference (PRD §4.1, Q27)." ; - sh:prefixes reg: ; - sh:select """ - PREFIX reg: - SELECT $this WHERE { - $this reg:provenance reg:institutional . - FILTER NOT EXISTS { $this reg:trust_decision ?td . } - } - """ ; - ] . diff --git a/docs/g-track/registry-template/shapes/structural.ttl b/docs/g-track/registry-template/shapes/structural.ttl deleted file mode 100644 index ff9b7ee0..00000000 --- a/docs/g-track/registry-template/shapes/structural.ttl +++ /dev/null @@ -1,30 +0,0 @@ -# Structural well-formedness — staged minimal. The one-assertion-per-file rule -# is a file-level property CI checks directly (workflow step "one assertion per -# file"); SHACL here covers what the graph itself can state. - -@prefix sh: . -@prefix reg: . - -reg:AssertionShape - a sh:NodeShape ; - sh:targetClass reg:Assertion ; - sh:property [ - sh:path reg:subject ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:nodeKind sh:IRI ; - sh:message "An assertion names exactly one subject, as an IRI." ; - ] ; - sh:property [ - sh:path reg:predicate ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:nodeKind sh:IRI ; - sh:message "An assertion names exactly one predicate, as an IRI." ; - ] ; - sh:property [ - sh:path reg:object ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:message "An assertion names exactly one object." ; - ] . diff --git a/docs/g-track/registry-template/vocabulary/reg.ttl b/docs/g-track/registry-template/vocabulary/reg.ttl deleted file mode 100644 index 301475a4..00000000 --- a/docs/g-track/registry-template/vocabulary/reg.ttl +++ /dev/null @@ -1,135 +0,0 @@ -# The registry's own vocabulary — the terms an instance's graphs are written in. -# -# Loaded by the projection build, never by the file rule: nothing here is an -# assertion or a decision, so it does not live under graphs/. -# -# It carries exactly two kinds of thing: -# 1. the individuals the two grading axes and the derivation record point at, -# so a mark is an IRI a query can bind rather than a string; -# 2. the equivalence that lets one query answer over both generations of -# assertion — the backfill for axis 1, which costs no ratified byte. - -@prefix reg: . -@prefix rdf: . -@prefix rdfs: . -@prefix owl: . - -# --------------------------------------------------------------------------- -# Classes -# --------------------------------------------------------------------------- - -reg:Assertion a rdfs:Class ; rdfs:label "asserted triple, reified" . -reg:Reading a rdfs:Class ; rdfs:label "a ground reading (PRD §4.1)" . -reg:Decision a rdfs:Class ; rdfs:label "a filed decision" . -reg:ExtractionRun a rdfs:Class ; rdfs:label "one run of the code extractor" . - -# A capability-probe verdict — one operation, one live call, one instant. -# -# It is typed reg:Reading deliberately. Measured on one corpus at one ref, -# minutes apart, an operation timed out on one run and answered on the next: -# the verdict is what a call returned at an instant, not a standing fact about -# the server. The capability discipline and the Reading discipline are the -# same mechanism, so a verdict carries the same four-tuple every other reading -# carries — value, as-of, provenance, assurance — and the Reading shape -# enforces it. -reg:ProbeVerdict - a rdfs:Class ; - rdfs:subClassOf reg:Reading ; - rdfs:label "one operation's capability-probe verdict on one run" . - -# The generation marker. An assertion written by a 0.3.0-or-later extractor -# carries this type **explicitly**; nothing entails it. That is deliberate: -# the derivation shape targets this class rather than the presence of a -# predicate, because the projection's equivalence entailment can put a -# predicate on an assertion the writer never touched. -reg:GradedAssertion - a rdfs:Class ; - rdfs:subClassOf reg:Assertion ; - rdfs:label "an assertion carrying both grading axes" ; - rdfs:comment "Stamped by the writer, never inferred. An assertion without this type predates the two-axis split: its derivation confidence is its reg:assuranceGrade, and its domain relevance is unstated, which reads as reg:unknown." . - -# --------------------------------------------------------------------------- -# Axis 1 — derivation confidence: how reliably the instrument read the code -# --------------------------------------------------------------------------- - -reg:derivationConfidence - a rdf:Property ; - rdfs:label "derivation confidence" ; - rdfs:comment "How reliably the instrument read the code. Ordinal: high > mid > low > not-derivable." . - -# The predicate this one renames. The earlier column was already measuring -# derivation and nothing else, so the two are the same property under two -# names and every assertion ratified under 0.2.0 answers a confidence query -# through this axiom — no rewrite, no supersession, no re-confirmation. -reg:assuranceGrade - a rdf:Property ; - owl:equivalentProperty reg:derivationConfidence ; - rdfs:comment "Retained name, pre-0.3.0. Equivalent to reg:derivationConfidence: the split renamed the column rather than re-marking its rows." . - -reg:high a reg:ConfidenceMark ; rdfs:label "declared — a probed operation returned the thing itself" . -reg:mid a reg:ConfidenceMark ; rdfs:label "structural or inferred — a rule took a step the language does not" . -reg:low a reg:ConfidenceMark ; rdfs:label "heuristic — a lexical rule, no server semantics behind it" . -reg:not-derivable a reg:ConfidenceMark ; rdfs:label "no instrument on this surface, at any grade" . - -# --------------------------------------------------------------------------- -# Axis 2 — domain relevance: whether the result is what the registry is for -# --------------------------------------------------------------------------- - -reg:domainRelevance - a rdf:Property ; - rdfs:label "domain relevance" ; - rdfs:comment "Whether the assertion is the kind of thing the registry is for. NOT ordinal: unknown is the absence of a verdict, not a value between high and low." . - -reg:relevanceBasis - a rdf:Property ; - rdfs:label "relevance basis" ; - rdfs:comment "Where the relevance mark came from. A mark without its basis is unreadable: unknown-because-nothing-computed-it and unknown-because-the-row-proposes-nothing are different findings." . - -reg:unknown a reg:RelevanceMark ; rdfs:label "not computed — the extractor has no rule that decides it from what it read" . -# reg:high and reg:low above serve both axes: the marks are shared deliberately, -# so "high confidence, low relevance" reads as one sentence in one vocabulary. - -reg:computed a reg:BasisMark ; rdfs:label "a named rule decided it from facts the extractor read" . -reg:defaulted a reg:BasisMark ; rdfs:label "the row's own construction fixes it, independent of any codebase" . -reg:not-computed a reg:BasisMark ; rdfs:label "the extractor has no rule for it on this surface" . -reg:not-applicable a reg:BasisMark ; rdfs:label "the row proposes nothing, so no assertion carries a mark" . - -# --------------------------------------------------------------------------- -# The derivation record — evidence, carried in runs/, keyed by assertion id -# --------------------------------------------------------------------------- - -reg:derivedByRule a rdf:Property ; rdfs:label "the named rule inside the row that produced the assertion" . -reg:standsOn a rdf:Property ; rdfs:label "a declaration-level operation the assertion's facts came from" . -reg:memberKind a rdf:Property ; rdfs:label "the kind of member the assertion derives from, as the server reported it" . -reg:containerKind a rdf:Property ; rdfs:label "the kind of declaration containing what the assertion is about" . -reg:sourcePath a rdf:Property ; rdfs:label "repo-relative path of a declaration the assertion derives from" . -reg:mappingRow a rdf:Property ; rdfs:label "the §5.2 row that proposed the assertion" . -reg:templateGeneration a rdf:Property ; rdfs:label "the template version whose form the assertion was written in" . - -# Source kinds, as reported by the language server — reported, never judged. -# reg:field is the fact whose absence made a whole class of rejection -# unfindable in the ratified graph. -reg:property a reg:MemberKind ; rdfs:label "property" . -reg:field a reg:MemberKind ; rdfs:label "field" . -reg:enum-member a reg:MemberKind ; rdfs:label "enum member" . - -reg:class a reg:ContainerKind ; rdfs:label "class" . -reg:interface a reg:ContainerKind ; rdfs:label "interface" . -reg:struct a reg:ContainerKind ; rdfs:label "struct" . -reg:enum a reg:ContainerKind ; rdfs:label "enum" . -reg:record a reg:ContainerKind ; rdfs:label "record" . - -# The declaration-level subset, one individual per operation. An assertion -# standing on an operation the probe reported unavailable cannot exist, so -# these are what reg:standsOn binds. -reg:operation a rdf:Property ; rdfs:label "the operation a probe verdict is about" . -reg:answered a rdf:Property ; rdfs:label "did a live call return a usable result — the verdict" . -reg:advertised a rdf:Property ; rdfs:label "the ServerCapabilities flag as read, beside the verdict" . -reg:divergence a rdf:Property ; rdfs:label "how the advertisement diverged from the behaviour" . - -reg:op-documentSymbol a reg:Operation ; rdfs:label "textDocument/documentSymbol" . -reg:op-workspaceSymbol a reg:Operation ; rdfs:label "workspace/symbol" . -reg:op-typeHierarchy a reg:Operation ; rdfs:label "textDocument/prepareTypeHierarchy" . -reg:op-references a reg:Operation ; rdfs:label "textDocument/references" . -reg:op-definition a reg:Operation ; rdfs:label "textDocument/definition" . -reg:op-hover a reg:Operation ; rdfs:label "textDocument/hover" . diff --git a/docs/g-track/two-axis-session-2026-08-20.md b/docs/g-track/two-axis-session-2026-08-20.md deleted file mode 100644 index fc81da58..00000000 --- a/docs/g-track/two-axis-session-2026-08-20.md +++ /dev/null @@ -1,626 +0,0 @@ -# The second axis — derivation confidence and domain relevance: session record - -**Repository:** `Hafeok/product-cli`, branch `claude/two-axis-derivation-relevance`. -**Implements:** the top-ranked finding of `g0-session-report-2026-08-19.md` §7.1, with §7.4 arriving -at the same change from the review side. -**Corpus:** `corpus-backend`, read-only, at `3b0a56b3` — the **full solution** this time, not G0's -restorable three-project subset. -**Gates:** 1 design · 2 implement · 3 template and PRD · 4 close. - -**Naming discipline** holds: corpus repositories are `corpus-backend` / `corpus-android`, domain -identifiers abstracted structurally. Extracted content lives in the generated instance, never here. - ---- - -## 0. Environment — the condition that constrained G0 is cleared - -G0's mapping-rule verdicts are qualified in §5.2 as *provisional pending a full-solution run*, -because the corpus restored only on the three projects that need no private feed. This session runs -with feed access. Confirmed before any design was settled: - -| Check | Result | -|---|---| -| Corpus ref `3b0a56b3` resolvable | yes, after a fetch — the ref was not in the local clone | -| Full solution restores | **yes** — 16 of 16 projects, `dotnet restore` exit 0, no `NU1101`/auth failure | -| Private-feed packages resolved | yes — the private-prefix packages are in the local package cache | -| Corpus written to | **no** — the tree is a `git archive` export into a scratch directory; no worktree, no branch, no commit in the corpus repo | - -Scale, measured, as the denominator for every count below: - -| | source files (`.cs`, excluding `bin`/`obj`) | -|---|---| -| G0's subset (the three restorable projects) | 57 | -| Full solution `src/` | **951** | - -≈16.7×, which is the "roughly fifteen times" §7.4 predicted. The G0 verdicts therefore stop being -provisional at Gate 2, and any row that *behaves* differently at full scale is a finding. - ---- - -## 1. Gate 1 — the design - -Proposed, not implemented. The template-change and backfill answers are Emil's rulings. - -### 1.1 What the finding actually demands - -§5.2 carries one `Assurance` column. G0 measured that it is really two properties wearing one name, -and that they are independent rather than correlated: **31 assertions at the top grade, every -mechanical check passing, all 31 wrong to ratify** (Group B — private serialisation state on -converter types), plus 18 more at mixed grades (Group A — generated schema-change code). - -The forcing argument is recoverability. Group A is findable after the fact through `reg:evidence` -paths. **Group B is not findable at all** — nothing in a ratified assertion records that its property -came from a member the server reported as a *field*, so the count had to be taken from the -extractor's own JSON. A reviewer cannot locate the class of error they would need to re-examine. -That is ground that cannot be audited. - -Read carefully, the demand splits in two, and the split governs the whole design: - -- **The two marks** stop the table claiming a property it never computed. -- **The derivation record** is what makes a class of error *queryable*. It is the half that - discharges the forcing argument. - -Section 1.4 states plainly which half does the work at G0. It is not the half the finding's headline -suggests. - -### 1.2 The two axes, and their value sets - -**Axis 1 — derivation confidence.** How reliably the instrument read the code. Ordinal. - -| Mark | Meaning | -|---|---| -| `high` | Declared. A probed LSP operation returned the thing itself; the language's semantics say what it is | -| `mid` | Structural or inferred. The server answered, and a mapping rule took a step the language does not itself take | -| `low` | Heuristic. A lexical rule over identifier text; no server semantics behind it | -| `not-derivable` | No instrument on this surface at any grade | - -**The value set is unchanged from today's `Assurance` column, deliberately.** The existing column -was already measuring derivation and nothing else — that is precisely G0's finding — so re-grading -axis 1 is a *renaming of the column, not a re-marking of its rows*. Every one of the 561 ratified -assertions keeps its mark, and its bytes. §1.6 turns that into the backfill answer. - -**Axis 2 — domain relevance.** Whether the result is the kind of thing the registry is for. - -| Mark | Meaning | -|---|---| -| `high` | The assertion states something about the domain | -| `low` | Read correctly; describes a mechanism of the software, not of the domain | -| `unknown` | **Not computed.** The extractor has no rule that decides it from what it read | - -Three values, not four: no middle mark is defined, because no rule produces one and an unused -middle is exactly the over-specification §7.8 warns about (*"the weakest thing that satisfies a -row's stated output is usually a sign the output was mis-specified"*). - -**The two axes are not the same kind of scale, and the design does not pretend otherwise.** -Confidence is ordinal — `high` really is above `mid`. Relevance is a *verdict* with an undetermined -state; `unknown` is not between `high` and `low`, it is the absence of a verdict. Forcing it into an -ordinal would reintroduce the conflation this change exists to remove. - -**Axis 2 carries its basis, because a mark without one is unreadable.** `reg:relevanceBasis`, one of: - -| Basis | Meaning | -|---|---| -| `computed` | A named rule decided it from facts the extractor read | -| `defaulted` | The row's own construction fixes it, independent of any codebase | -| `not-computed` | The extractor has no rule for it on this surface | -| `not-applicable` | The row proposes nothing, so no assertion carries a mark | - -### 1.3 The seven rows, re-graded - -| Row | Confidence | Relevance | Basis | One-line justification | -|---|---|---|---|---| -| `classes` | `high` | `unknown` | `not-computed` | The language says it is a type. Nothing in the declaration subset says it is a *domain* type — Group A's migration classes and Group B's converters are both perfectly ordinary declared types | -| `subclass` | `high` | `unknown` | `not-computed` | `typeHierarchy` answers on production declarations. Whether an inheritance edge is a domain taxonomy or a framework one is not in the answer | -| `properties` | `high` | `unknown` | `not-computed` | **The measured case.** 31 assertions here, top confidence, every check passing, all wrong to ratify. This row is the finding | -| `foreign-keys` | `not-derivable` | `unknown` | `not-applicable` | No instrument; the row proposes nothing, so no assertion carries a mark. The pair is stated so the row reads as *no instrument*, not as *no relevance* | -| `modules` | `mid` | `unknown` | `not-computed` | A project name is a fact about code arrangement. Whether it names a bounded context is a domain reading nobody has taken | -| `usage-relations` | `mid` | `unknown` | `not-computed` | The rule reads reference positions. Relevance rides on the two endpoints, and both are themselves `unknown` | -| `synonyms` | `low` | `unknown` | `not-computed` | A spelling of an identifier. §7.8 already suspects the row's stated output is mis-specified; a relevance verdict on it would be a second guess on top of a first | - -### 1.4 Relevance is `unknown` on every row, and that is the honest answer - -This is the part to rule on, so it is stated without softening. - -**At G0 the extractor computes domain relevance for no row.** Every relevance mark it writes is -`unknown`, with basis `not-computed`. The remedies §7.2 named — generated-code attributes, a -generated-file header, visibility — are all outside the declaration-level six, and widening that -subset is a separate filed decision that this session does not take. - -Three consequences, all of them intended: - -1. **§5.2 stops claiming relevance it never computed.** The current table's implication — declared - rows are where extraction closes, review earns its keep on the inferred ones — is now measured - false, and the two-axis table says so in a cell rather than in a footnote. -2. **The pair alone does not separate Group A from Group B.** With relevance uniformly `unknown`, - the pair distinguishes them only through confidence, and Group A spans all three confidences. - What separates them is the **derivation record** (§1.5) — which is what the forcing argument - asked for in the first place. The batching output therefore keys on the pair *and then* on the - derivation signature (§1.7). -3. **The axis is, today, a declared gap rather than a working discriminator.** Naming it that way is - the point. An axis that defaulted to a plausible `high` would read as computed, which is - `term:presumed-discharge` reappearing inside our own instrument for the second time in this - track. - -#### The one rule that would move it, and why it is not taken here - -There is exactly one relevance rule available without a new LSP operation: **a path-convention rule** -— mark `low` where every evidence path of an assertion lies under a generated-code folder -convention. It uses only facts the extractor already holds (`Declaration::file`), so it does not -widen the LSP subset, and it would move Group A to `high`/`low`, which is the prompt's own worked -example. - -**Recommendation: do not take it in this session.** It is a convention judgement — a claim that a -folder name means "generated" — and it belongs in the filed subset-widening decision with its -closure consequence stated, exactly as §7.2 requires of the other three remedies. Taking it here -would smuggle a filtering rule into a change whose stated scope is *making the groups visible*. - -**Emil's ruling wanted:** accept uniform `unknown` (recommended), or rule the path-convention rule -in — in which case the `classes`/`modules`/`synonyms`/`usage-relations` rows gain `computed` relevance -for generated code and the pair separates the two groups on its own. - -### 1.5 The derivation record — the half that discharges the forcing argument - -What must be recorded so both rejected groups become queryable, in the template's vocabulary, riding -on the assertion node. - -**Shape: flat, typed predicates on the assertion node.** Not a separate node, not a blank node: - -- g-dec-04 fixes assertion identity as `sha256(subject, predicate, object)`, and a Reading that rides - on the assertion node shares that identity. Riding on the node is therefore free — it cannot - perturb identity, and §2.4's fixture proves it rather than assuming it. -- The writer emits **no blank node at all** by construction (G0 §8), and the projection drops any - quad carrying one. A blank-node record would be silently lost. -- A separately minted derivation node would put a second typed subject in the file. The instance's - CI file rule counts assertions and decisions, so it would pass — but the rule exists to keep a - file's meaning one thing, and generalising a carve-out is what 0.2.0 spent its last revision - undoing. - -| Predicate | Object | Present when | Why it is here | -|---|---|---|---| -| `reg:mappingRow` | literal row id | always | already emitted at 0.2.0; now formally part of the record | -| `reg:derivedByRule` | literal rule name | always | **the rule**, not just the row — a row may carry several (`declared-member`, `member-range`, `usage-construct`, `identifier-word-split`) | -| `reg:standsOn` | `reg:op-`, repeatable | always | **the LSP operation(s)** the assertion's facts came from, as probed. An assertion that stands on an unprobed operation cannot exist, and the record says which | -| `reg:memberKind` | `reg:property` \| `reg:field` \| `reg:enum-member` | member-derived assertions | **Group B becomes one query.** This is the fact whose absence made Group B unfindable | -| `reg:containerKind` | `reg:class` \| `reg:interface` \| `reg:struct` \| `reg:enum` \| `reg:record` | assertions about a declared type or its member | the source-kind fact the reader actually saw | -| `reg:sourcePath` | literal, repo-relative | declaration-derived assertions | **Group A becomes one query.** `reg:evidence` already carries paths, but mixed with type names and rule names; this is the typed, queryable form | - -**The design principle, stated so it is reviewable:** *record the facts, and let the reviewer's -query — not the extractor's rule — do the classification.* Group A is recoverable by a predicate over -`reg:sourcePath`; Group B by `reg:memberKind reg:field`. Neither requires the extractor to judge -anything, neither filters, and neither widens the subset. - -**Template version: 0.2.0 → 0.3.0.** New vocabulary individuals, new optional predicates, new shapes. - -**Backward compatible with the 561 ratified assertions: yes, and by construction rather than by -luck.** The new shape must not be a bare `sh:minCount 1` on the new predicates, or every ratified -file fails CI; but it must also not be plainly optional, or a future run could omit them silently — -the presumed-discharge shape again. So the constraint is **conditional on the generation marker**: - -> An assertion carrying `reg:derivationConfidence` MUST also carry `reg:domainRelevance`, -> `reg:relevanceBasis`, `reg:derivedByRule`, and at least one `reg:standsOn`. - -Pre-0.3.0 assertions carry no `reg:derivationConfidence`, so they are not targeted and stay -conformant, byte-unchanged. Post-0.3.0 assertions are fully constrained. **No file in the instance -changes, and none becomes non-conformant** — so no instance migration is needed and none is -proposed. - -### 1.6 Backfill — what the 561 can and cannot gain - -Supersede-never-rewrite applies; the re-confirmation problem is g-dec-04's and is not solved here. - -| | Disposition | -|---|---| -| **Axis 1 (confidence)** | **Gained retroactively, without a byte changing.** The template declares `reg:assuranceGrade owl:equivalentProperty reg:derivationConfidence` in its vocabulary. The projection build runs OWL-RL, so every ratified assertion answers a confidence query. The value set is unchanged, so the answers are the marks they already carry | -| **Axis 2 (relevance)** | **Already correct, by absence.** Relevance is `unknown` for everything the extractor produces at G0, and a missing `reg:domainRelevance` is indistinguishable from `unknown`. The template states that reading for the pre-0.3.0 generation in one line. No rewrite, and no dishonesty | -| **Derivation record** | **Cannot be gained on the assertion node.** The facts were never stored. Recomputing them means re-running the extractor, and writing them into ratified files is a rewrite. Superseding is not available either: identity is the triple, so a "fresh" assertion carrying derivation is *the same assertion* — supersession has nothing to point at. This is g-dec-04's accepted cost arriving from a second direction | - -**The honest disposition, and the narrowest option, are different things — both are stated.** - -- *Forward-only* (recommended default): the pre-0.3.0 cohort carries no derivation, permanently. It - is identifiable — the absence of `reg:derivationConfidence` names it exactly — but a reviewer - auditing Group B **inside the ratified set still cannot find its members**. That limit survives - this change and must be recorded in the PRD rather than papered over. -- *The narrowest option that would lift it*: a **run sidecar** — one file per extraction run, - outside `graphs/`, recording the derivation record for every assertion the run produced, keyed by - assertion id. It rewrites nothing, adds no assertion, and creates no new node in a graph file. The - instance's file rule applies to `graphs/**/*.ttl` only, so a `runs/` file is mechanically - available where g-dec-04's rejected option was not. Cost: a second place derivation lives, and a - file whose relationship to ratification is "evidence about", not "ratified content". - -**Emil's ruling wanted:** forward-only (recommended), or the run sidecar. - -### 1.7 Review batching — the volume answer and the correctness answer - -G0's 561 files were already at the edge of per-file review; the full solution is ~16.7× the corpus. -Wholesale merge is manufactured ground by §5.6, so the answer is batching. - -**The reviewer sees a batch, not a file.** A batch is the set of assertions sharing a -**derivation signature**: - -``` -(derivation confidence, domain relevance, row, rule, member kind, container kind) -``` - -and carries: the pair, the row and rule, the operations the batch stands on, the count, a bounded -sample rendered as the reviewer reads a triple, and the full id list so the batch is actionable -rather than merely descriptive. - -Ordering is total and deterministic — confidence descending, then relevance, then row, rule, -member kind, container kind — so two runs over the same facts emit the same batches in the same -order, and a batch listing diffs cleanly across refs. - -**The batching output is on the plan's JSON, not behind `#[serde(skip)]`.** G0 §7.6 is explicit that -the extractor's account of its own limits is the reviewer's batching key and belongs in the output -contract. That mistake is not repeated. - -### 1.8 What this change does not do - -It does not filter Group A. It does not filter Group B. It does not widen the declaration subset. -It makes both groups **visible and batchable**, and it stops the table claiming a property nobody -computed. Filtering needs the widened subset, which is a separate filed decision. - -### 1.9 Gates at this hold - -`cargo t` · `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` · `cargo xtask check` · -`ddd validate` · the contract-surface gate. Results recorded at the hold. - ---- - -## 2. Gate 2 — implemented - -Emil's Gate 1 rulings, as taken: **uniform `unknown`** (relevance is declared-empty, not undeclared); -**the run sidecar** as the per-source evidence layer, with retroactive population as a Gate 2 -deliverable; and the entailment/shape defect fixed by targeting the generation marker rather than -predicate presence. - -### 2.1 What was built - -| Piece | Where | What it does | -|---|---|---| -| The two axes | `product-core/src/ground/axes.rs` | `DerivationConfidence` (the old value set, renamed) · `DomainRelevance` (`high`/`low`/`unknown`, no `Ord`, because `unknown` is not a middle) · `RelevanceBasis` | -| The re-graded table | `ground/rows.rs` | seven rows, each carrying the pair with its basis | -| The derivation record | `ground/derivation.rs` | rule · probed operations · member kind · container kind · source paths | -| The evidence layer | `ground/sidecar.rs` | `runs/-at-.ttl`, keyed by assertion id, with the orphan check | -| Review batching | `ground/batch.rs` | batches by `(confidence, relevance, row, rule, member kind, container kind)`, totally ordered | -| The projection join | `ground/projection.rs` | loads `graphs/` ∪ `runs/` ∪ `vocabulary/`, applies the equivalence entailment, validates or queries the result | -| Template 0.3.0 | `docs/g-track/registry-template/` | `vocabulary/reg.ttl` · `shapes/derivation.ttl` · CI joins the layers and checks orphans | - -**`reg:mappingRow` is not repeated in the sidecar.** The canonical file already carries it, and -duplicating a fact across layers is how two layers drift. - -### 2.2 The defect Emil found, and how it is now impossible to reintroduce - -`reg:assuranceGrade owl:equivalentProperty reg:derivationConfidence` means the projection -*manufactures* `reg:derivationConfidence` on every pre-0.3.0 assertion. A shape keyed on that -predicate's presence would have passed in the authority repo — where validation runs pre-entailment -— and failed the moment anyone validated the projection. - -The shapes target **`reg:GradedAssertion`**, a class the writer stamps and nothing entails. The -fixture `entailment_does_not_drag_the_pre_split_cohort_into_the_new_shape` asserts three things in -order, so it cannot pass for the wrong reason: - -1. the equivalence *does* fire — one pre-0.3.0 assertion gains `reg:derivationConfidence`; -2. nothing gains `reg:GradedAssertion`; -3. `validate_projection` finds nothing, with a non-zero constraint count. - -A second fixture validates the **projection**, not the repo, for the 0.3.0 case: an assertion whose -run evidence is absent fails loudly on the derivation half rather than passing quietly. - -### 2.3 Fixtures - -| Prompt's requirement | Fixture | Result | -|---|---|---| -| Both grades on every assertion | `propose::every_proposal_carries_both_marks`, `plan::no_assertion_carries_one_mark_without_the_other`, `rows::every_row_carries_both_axes` | pass | -| Unknown relevance representable, distinct from high | `propose::unknown_relevance_is_distinct_from_high`, `rows::no_row_claims_a_relevance_it_did_not_compute` | pass | -| Derivation queryable — both groups recoverable from the graph | `projection::the_field_derived_group_is_a_query_over_the_projection` (Group B) · `…the_generated_code_group_is_a_query_over_recorded_paths` (Group A) · `…the_two_groups_are_separable_from_one_another` | pass | -| G0's two groups re-derived as distinguishable batches | `batch::field_derived_assertions_batch_apart_from_property_derived_ones` — asserts the *kinds* separate, never a count | pass | -| Batching output stable | `batch::batching_is_deterministic_under_input_order`, `plan::the_plan_carries_a_stable_batching_of_every_assertion` | pass | -| **Idempotence preserved** | `mint::the_two_axes_and_the_derivation_stay_out_of_the_identity`, `plan::a_re_run_proposes_nothing_that_is_already_ratified` | pass | -| The join is checked | `sidecar::an_entry_with_no_assertion_behind_it_is_an_orphan`, `…orphans_are_counted_per_run_over_an_instance_tree` | pass | - -**The identity row, verified rather than assumed.** Two assertions differing in confidence, -relevance, basis, rule, operations, member kind, container kind and source path — differing in -*everything the split added* — mint one id, because `g-dec-04` hashes the triple and nothing else. -The instance's history is unaffected, and no ratified assertion becomes a fresh proposal. - -### 2.4 The full-solution re-run - -`corpus-backend` at `3b0a56b3`, **all 16 projects restored**, 951 source files read through -`roslyn-language-server 5.11.0-1.26380.4` — the same server build G0 used. Written into a working -copy of the G0 instance, so the 561 ratified assertions are present and the run had to behave as a -re-run rather than a first run. - -``` -967 declarations · 174 hierarchy edges · 6 305 reference sites -9 662 assertions read · 561 already ratified · 9 101 proposed -18 review batches · 0 orphaned evidence entries -shapes: conformant — 42 constraints over the join (graphs ∪ runs ∪ vocabulary) -``` - -#### Per-row: does the full solution confirm what the subset showed? - -Counts differ because the corpus is ≈16.7× larger; that is expected and is not the question. The -question is **behaviour**. - -| Row | G0 (subset) | Full solution | Behaviour | -|---|---|---|---| -| `classes` | fired — 58 | fired — 942 | **confirmed** | -| `subclass` | fired — 17 | fired — 172 | **confirmed** | -| `properties` | fired — 215 | fired — 4 007 | **confirmed** | -| `foreign-keys` | not-derivable, absence reported | not-derivable, absence reported | **confirmed** — still no navigation property, still no FK attribute, at seven times the projects | -| `modules` | fired — 86 | fired — 1 514 | **confirmed**, with a table correction — see finding 2 | -| `usage-relations` | fired — 130 | fired — 2 103 | **confirmed** | -| `synonyms` | fired — 55 | fired — 924 | **confirmed** | - -Six of seven rows fired; no row found nothing; **no row was gated off**. The §5.2 mapping verdicts -stand as written, and their *provisional pending a full-solution run* qualifier is discharged. - -Two structural readings also confirmed at scale: - -- **`subclass-transitive` still fires zero**, over 172 hierarchy edges rather than 17. Every declared - derivation in this corpus is one level, type → marker interface. §7.3's "correct and idle" is not - a small-sample artefact. -- **The restorable-subgraph limit disappears.** G0 counted supertypes named by an edge but declared - outside the loaded subset; at full solution that count is **0**. Nothing is named that is not - declared — which is what loading the whole solution was supposed to buy. - -#### Three findings the subset could not have produced - -**1. `workspace/symbol` availability is not stable across identical runs.** Two runs of the same -binary against the same corpus at the same ref, minutes apart: - -| | `workspace/symbol` | -|---|---| -| Run 1 (first full-solution read) | advertised `true`, **did not answer within 15 s** — recorded unavailable, `divergence: over-advertised`; five of six operations available | -| Run 2 (the idempotence re-run) | advertised `true`, **answered — 7 symbols**; six of six available | -| Run 3 (verifying the probe-verdict change) | advertised `true`, **answered — 7 symbols**; six of six available | - -So the run-1 result is a **timeout under load, not a capability the server lacks**, and the honest -statement is sharper than "over-advertises at scale": **the probe's own verdict is run-dependent.** -An operation can be recorded unavailable on one run and available on the next, and a mapping row -gated on it would then yield different results on two runs that differ in nothing a reviewer can -see. G0's discipline — probe the operation, never the advertisement — holds and is vindicated; what -it gains here is that a probe verdict is a *reading*, with the same run-scoped honesty every other -reading in this track carries. - -Not overstated: this is one operation, three runs, on one corpus — one non-answer and two answers. -It is enough to say the verdict varies; it is nowhere near enough to characterise the distribution, -which is exactly why the remedy is to *record* verdicts rather than to reason about them. What it does settle is that *the subset -could not have shown this at all* — 57 files never loaded the server enough to time out. - -**2. §5.2's `modules` row does not stand on `workspaceSymbol`, whatever the table says.** Run 1 is -the natural experiment: `workspace/symbol` was unavailable and the row still fired **1 514** -assertions, because the implementation reads module containment from `documentSymbol`. The -measurement holds regardless of *why* the operation failed. On the subset both answered, so the -divergence between table and implementation could not surface. **The table's LSP-operations cell for -this row is wrong and should read `documentSymbol`.** Corrected in this session's PRD edit. - -The two findings compound: a row gated on an operation whose probe verdict varies would have -produced 1 514 assertions on one run and a gated-off row on the next. This row was not gated on it — -by accident of the implementation, not by the table's design. - -**3. §7.5's collision prediction came true.** G0 measured **0** cross-module simple-name collisions -and noted that name-based range resolution was safe *on that corpus*, with `definition` held as the -exact-resolution route "if collisions appear at full-solution scale". At full solution there are -**21** — a mix of same-named DTO types across projects, per-project `Program` and `Version` types, -same-named extension-method holders, and paired handler/interface names. - -So the `properties` row's range resolution **is now unsafe on this corpus**: a member typed with a -colliding simple name resolves against whichever declaration the name matches, and there are 21 -names where that is a coin toss. `definition` remains wired and **unspent** — it is the only -operation no row consumes. This is a real defect, it is out of this session's scope, and it should -be filed: *range resolution matches on simple name; at full-solution scale the corpus contains 21 -names where that conflates two types; the fix is the already-wired `definition` route.* - -#### The fixpoint, against `g-dec-02`'s revisit condition - -``` -4 rounds · 3 rule parses · 12 executions · 509 ms -seed 35 032 triples · 2 103 proposed · 3 561 entailed - usage-direct 2 103 over 1 productive round - usage-transitive 3 561 over 3 productive rounds [entailed, not proposed] - subclass-transitive 0 over 0 productive rounds [entailed, not proposed] -``` - -Parse-once holds (3 parses, 12 executions). **The ratio G0 said to watch has inverted**: 130 → 109 -on the subset (0.84), 2 103 → 3 561 at full solution (1.69). The transitive closure now exceeds the -direct set, which is the quadratic behaviour `g-dec-02`'s `revisit_if` names. Wall-time is 509 ms and -nowhere near a projection-build budget, so **the condition is not met and the decision does not -flip** — but the direction is now measured rather than assumed, and the next scale step is where it -would be tested. - -### 2.5 The two groups, recovered from the graph - -The forcing argument, discharged against the real ratified cohort rather than a fixture. Both -queries run over recorded facts; neither asks the extractor to judge anything. - -| | Query | Full solution | **Inside the 561 ratified** | G0 reported | -|---|---|---|---|---| -| **Group B** | `reg:memberKind = reg:field` | 195 | **31** | 31 | -| **Group A** | any `reg:sourcePath` under a generated-migration folder | 18 | **18** | 18 | - -**Both counts reproduce G0's exactly**, and both were reached by query where G0 had to take them -from the extractor's own JSON — Group B by a hand count that was wrong twice before the notes were -made queryable. Group A's 18 fall across four rules (`declared-type` 4, `module-containment` 6, -`identifier-word-split` 4, `usage-construct` 4), which is the "spread across four rows" G0 described, -now enumerable. - -**Every one of the 561 ratified assertions has derivation: 0 without.** Emil's ruling that the facts -are recomputable is confirmed on the real cohort — one deterministic re-run at the pinned ref, and -the pre-0.3.0 generation became auditable without a byte of ratified content changing. - -One honest note on Group A's predicate. A first pass required *every* source path to sit under a -migrations folder and recovered 14 of the 18; the four it missed are `usage-construct` edges whose -second endpoint is a non-migration type. Relaxing the predicate to *any* path recovers all 18. That -is the design working as intended rather than a bug — **the extractor records the paths and the -reviewer writes the predicate** — but it is worth recording that the predicate is a real choice with -a real effect on what a batch contains. - -### 2.6 Idempotence, end to end at full-solution scale - -The row the prompt said to watch, run as a whole second extraction rather than only as a fixture. -The same binary, the same corpus at the same ref, against the instance the first run had just -populated: - -``` -0 assertion(s) proposed across 1 file(s) -9 662 assertion(s) already ratified in the canonical graph — not re-proposed -sidecar entries: every derivation record resolves to an assertion in the graph -nothing proposed — every one of the 9 662 triple(s) this run read is already ratified -``` - -**No tracked file in the instance changed** (`git status --porcelain` shows 0 modified). The single -planned file was the run sidecar, byte-identical to the one already on disk, so nothing was written. - -So the new fields did **not** enter the assertion hash: g-dec-04's identity is unchanged, the -instance's history is intact, and the change is idempotent at 9 662 assertions rather than at the -handful a unit fixture exercises. The fixpoint's wall-time moved (509 ms → 307 ms) while its triple -counts did not, which is the right shape — time varies, the graph does not. - -### 2.7 A finding about the instance, not about the extractor - -G0 ruled 512 accepted and 49 rejected, and recorded that the merge acts were the ratifier's and -pending. **The instance holds all 561.** PR #2 (`Proposal: 561 assertions…`) merged 561 assertion -files and no later commit removes any, so the ruled rejection of Group A and Group B is not -reflected in the ratified graph. - -Reported as an observation, not a conclusion about intent — and Emil supplied the account at Gate 3 -ratification: the ruling was delivered as prose in a chat message, the ratification act was a merge -button, and **nothing checked that the second carried the first**. The merge reads as acceptance of -a reviewed set; what was ratified was the unreviewed superset. `term:presumed-discharge` at the -ratifier's own seat, in the one path §5.6 did not describe — not wholesale merge *instead of* -review, but wholesale merge *after* it, which looks like the discharged form and is not. - -**Disposition, ruled: retraction, not deletion, and not this session.** The 49 are ratified ground -today. They are removed by a **retraction decision** filed in the instance — the members named by -id, the basis naming G0's Gate 4 ruling, the assertion files removed in that same act, so history -retains them and the decision records why. Supersession at the file level rather than a rewrite. -Emil drafts it: it is a ratifier's act with a ruling behind it, and this session does not file -decisions into the registry on his behalf. - -**The mechanism failed, not only the person**, so the general clause goes in the PRD (§5.5.2): a -ratification act must be checkable against the ruling it discharges — a stated accept/reject set by -id before the merge, and the merged tree diffed against it. Per-triple review that ends in one -undifferentiated merge is per-triple review in name only. - -**Recoverability, paid out.** The argument for recording derivation was made in the abstract at G0. -The 49 were unfindable in the ratified graph until this change and are now two queries — the first -real defect in the ratified set the argument has repaid. - -### 2.8 Probe verdicts, recorded — finding 1 generalised - -Ruled at Gate 3: a probe verdict is itself a **Reading**, not a fact about the server. The capability -discipline and the Reading discipline turn out to be the same mechanism, and the run evidence now -says so. Each verdict is its own node in `runs/`, typed `reg:ProbeVerdict , reg:Reading`, carrying: - -``` -reg:probe-corpus-backend--workspaceSymbol - a reg:ProbeVerdict , reg:Reading ; - reg:operation reg:op-workspaceSymbol ; - reg:answered false ; - reg:value "unavailable — error: language server timeout: … after 15s" ; - reg:asOf "…"^^xsd:dateTime ; - reg:provenance reg:observed ; - reg:assurance "capability probe — one live call …; a verdict at this instant, - not a standing fact about the server" ; - reg:advertised true ; - reg:divergence "over-advertised" ; - prov:wasGeneratedBy reg:extraction-corpus-backend- . -``` - -Because the node's IRI is qualified by corpus and ref but the *value* is per run, two runs that -disagree about one operation are directly comparable rather than merely different — which is the -whole point: a reviewer can tell **"this row found nothing"** from **"this row was gated off that -day"**. Typing it `reg:Reading` is load-bearing rather than decorative: the instance's existing -`reading.ttl` shape then demands the four-tuple, so a verdict written as a bare boolean fails CI. - -Four fixtures carry it: the verdict is written as a Reading; an unavailable verdict records the -advertisement it disagreed with; two runs disagreeing about one operation produce different files -with the same verdict IRI; and verdicts sort by operation so the file stays byte-stable. - -**Verified on the corpus, not only in fixtures.** A third full-solution run wrote all six verdicts -into the run evidence, and `product registry check` over the instance holding them is conformant at -**48 constraints** — six more than before, which is the probe-verdict shape being read and -evaluated rather than skipped. - -**Retry-then-record is proposed, not built,** per the ruling. An operation recorded unavailable -after one timeout is a low-assurance reading treated as a verdict; the retry shape — how many, how -spaced, how the attempts are written — is PRD §12 item 17, to be ruled before any row's gating -depends on an operation that has ever timed out. - -### 2.9 Gates at this hold - -| Gate | Result | -|---|---| -| `cargo t` | **1 495 passed, 0 failed** (baseline 1 454 — 41 new) | -| `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` | exit 0 | -| `cargo xtask check` | 0 errors | -| `ddd validate` | conformant — 277 entries, 0 warnings | -| Contract surface (`ddd diff-contracts main`) | **85 events, 0 undischarged** — 81 bindings filed, five new seams given their own `verdict_knowledge` | -| `product registry check` on the produced instance | conformant — 9 667 data files, 42 constraints over the join | -| File length ≤ 400 · function length · single-responsibility | no hard-limit violation | - ---- - -## 3. Gate 3 — the template, the PRD - -### 3.1 Template 0.3.0 - -| Change | File | -|---|---| -| The registry's own vocabulary — axis individuals, source-kind individuals, the six declaration-level operations, `reg:ProbeVerdict` as a `reg:Reading`, and the equivalence axiom | **new** `vocabulary/reg.ttl` | -| The two axes with the derivation record, constrained; the probe-verdict shape | **new** `shapes/derivation.ttl` | -| SHACL runs over `graphs/` ∪ `runs/` ∪ `vocabulary/`; the file rule stays scoped to `graphs/`; a new **sidecar-entries-resolve** step reports orphans per run | `.github/workflows/validate.yml` | -| The two axes, where derivation lives, why the split is compatible | `TEMPLATE.md` | -| The join named as a projection-build step, with its Rust form | `scripts/build-projection.sh` | - -**Backward compatible with the ratified instance — no ruling needed, and none taken.** Verified, not -argued: `product registry check` over the working copy holding all 561 pre-0.3.0 assertions plus the -new run evidence reports conformant, 42 constraints, file rule clean. No file in the instance -changed. - -The compatibility rests on one decision, and it is the one Emil caught at Gate 1. The shapes target -the **class** `reg:GradedAssertion`, stamped by the writer and entailed by nothing — not the presence -of `reg:derivationConfidence`, which the projection's equivalence entailment puts on every pre-0.3.0 -assertion. A predicate-keyed shape would have been conformant in the authority repo and -non-conformant at the projection, and would have held only by accident of when validation runs. - -### 3.2 PRD edits - -- **§4.1** — the Reading tuple gains `derivation_confidence`, `domain_relevance`, `relevance_basis`. -- **§5.2** — the two-axis table replaces the single assurance column, with G0's evidence cited and - the relevance column stated as **declared-empty** rather than undeclared, in those words. -- **§5.2** — the derivation record, the evidence layer, and the three consequences (identity - untouched, the older cohort auditable, the join checked where it is read). -- **§5.2** — the provisional qualifier on the mapping verdicts **discharged**, with the full-solution - confirmation stated. -- **§5.2** — the `modules` row's LSP cell corrected from `workspaceSymbol` to `documentSymbol`, - measured. -- **§5.2** — the `properties` row records the open **simple-name collision** defect and names - `definition` as the fix. -- **§5.2** — *Capability flags are claims* gains the second over-advertising case, with the - server × workspace-size nuance. -- **§5.2** — the under-advertising bullet **corrected**: it cited Roslyn, which advertises - `typeHierarchyProvider: true` under both handshakes and never under-advertised as a server. The - case is `csharp-ls`, which does not advertise the provider at all. *(The correction owed from G0 - Gate 2.)* -- **§5.5.1** — review batched by the pair with the derivation as sub-key; the batching belongs in the - output contract, not the logs. -- **§5.5.2** — **a ratification act must be checkable against the ruling it discharges**: a stated - accept/reject set by id before the merge, the merged tree diffed against it, and removal by - retraction rather than deletion. -- **§5.2** — *Capability flags are claims* gains the ruling that **a probe verdict is itself a - Reading**, that every verdict is recorded in the run evidence with its as-of, and that a single - unavailable verdict is not final. -- **§5.2** — both readings of the Group A path predicate (`every` recovers 14 of 18, `any` recovers - 18 of 18) recorded so the next reviewer inherits the choice rather than rediscovering it. -- **§12** — new open item **16**, the simple-name collision defect, scoped to the `definition` - route; new open item **17**, retry-then-record for probe verdicts. -- **§12** — new open item 15: **widening the declaration-level subset**, with the closure trade - stated and the path-convention rule named as a candidate deliberately not taken here. - -### 3.3 What this change does not do - -It does not filter Group A. It does not filter Group B. It does not widen the declaration subset. -It makes both groups visible and batchable, and it stops the table claiming a property nobody -computed. Filing the widening is PRD §12 item 15. diff --git a/docs/guide.md b/docs/guide.md deleted file mode 100644 index 28283ae2..00000000 --- a/docs/guide.md +++ /dev/null @@ -1,248 +0,0 @@ -# The Developer Guide — working through the framework day to day - -> **Non-normative.** This guide describes *a* way to run the Product Framework day -> to day. It is **not** part of the specification and adds no conformance rules — -> the spec is deliberately silent on practice (cadence, ceremonies, who, when; -> [spec §7.3](docs/product-framework.md#73-model-not-practice)). Everything here -> traces to something the spec already defines; this is the spec's **authoring -> order** ([§2.1](docs/product-framework.md#21-authoring-order--the-funnel-as-a-process)) -> turned into the activities a person actually performs. Your team may run it -> differently and still be fully conformant. - -## The shape of the day - -A feature lands on your desk — usually as an informal description, a UI sketch, or -a sentence from a stakeholder. You move it through four stages: - -``` - (a request arrives) - │ - ┌────▼─────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ - │ INTAKE │ ──▶ │ WHAT │ ──▶ │ HOW │ ──▶ │ BUILD │ - │ before │ │ meaning │ │ realise │ │ make it │ - │ the What │ │ │ │ │ │ real │ - └──────────┘ └────┬─────┘ └──────────┘ └────┬─────┘ - ▲ │ - └──────────────────────────────────┘ - feedback: intent-reliance, data-divergence, - state/Decider justification send you upstream -``` - -Each stage has **one question** you are answering, a few **concrete steps**, and a -clear **exit criterion** — you do not move on until it is met. The constraint that -fixes this order is not invented here: each stage's artifacts *derive from* the -previous stage's, so you cannot run them in another order without authoring -something before the thing it depends on exists (spec §2.1). - -The single most important habit: **hard thinking moves upstream.** Every problem -is cheaper to fix the further left you catch it. A behaviour bug found in the What -(by simulating a Decider) costs a sentence; the same bug found in Build costs a -day. The whole point of the funnel is to make the left stages carry the weight so -the right stages are determined. - ---- - -## Stage 0 — Intake (before the What) - -> **The question: *Where on the model does this live, and what is the one-sentence -> intent?*** - -A feature description is **not** a What. It is raw and ambiguous; the What is -precise, typed, and derived. Intake is the short translation step that turns a -request into a tracked piece of work located on the existing graph. Keep it -lightweight — this is triage, not modelling. - -1. **Locate it on the graph.** A feature is *a slice of the event model* - (spec §7.1), so the first question is always "where does this slice live?" - Which **system** ([§3.2.5](docs/product-framework.md#325-the-system--what-is-being-described))? - Which **bounded context** ([§3.1](docs/product-framework.md#31-the-domain-model--structure-and-data))? - Which existing **flows** does it touch or extend? -2. **Triage new-vs-change.** Is this genuinely new behaviour, or a change to - behaviour that already exists? A change means you start from the existing - Deciders/flows and amend; new behaviour means a fresh slice. -3. **Extract the intent — one sentence.** State what the user is trying to - accomplish, in the ubiquitous language. Write it down knowing it is **debt**: - intent is the marked residue of what the model does not yet determine (§3.2.1), - and the rest of the process is the act of discharging it into modelled elements. - If you cannot state the intent in one sentence, the request is not yet ready — - send it back, do not model around the ambiguity. - -**Exit criterion:** you can name the system, the context, the flow(s) involved, and -a one-sentence intent. If a request is too vague to place on the graph, it is not -ready for the What. - ---- - -## Stage 1 — What (the meaning) - -> **The question: *What happens, what decides it, what does the user see — and does -> it simulate?*** - -This is where the weight is. You are building a precise, typed description that -every role can read and agree on, **before any code.** Run the spec's authoring -order (§2.1, steps 1–5) as activities: - -1. **Confirm the system and the domain slice.** Name the entities, relations, and - **invariants** the feature touches (§3.1). If the feature needs constitutive - data (valid options, categories), declare it as **reference data** now — events - and invariants will reference it. -2. **Model the events and flows — the Event Modeling step.** Lay the timeline of - what happens, and identify which **pattern** each slice is - ([§3.2.0](docs/product-framework.md#320-building-blocks-and-patterns)): a - **Command** (`Trigger→Command→Event(s)`), a **View** (`Event(s)→View`), an - **Automation** (the system acting on itself), or a **Translation** (one system - telling another). Name the Trigger's source: user, external, or automated. -3. **Write the Deciders and Projectors for the interesting behaviour** (§3.3, - §3.4). The Decider is where the business rules live — its rejections *are* the - invariants. Carry on the aggregate exactly the **invariant-bearing state** a - Decider must read at decision time, and nothing else (§3.4). Trivial CRUD needs - no Decider; do not invent ceremony. -4. **Simulate — the cheapest gate.** Before writing any code, run the Decider and - Projector against scenarios drawn from the flow (a *given* of prior events, a - *when* command, a *then* of expected events/state). Prove they are **sound and - complete**: invalid commands rejected for the right reason, valid ones producing - the right events, no View needing a field no event supplies. This is the single - highest-leverage habit in the whole process — a behaviour defect caught here - costs a sentence. -5. **Specify the UI steps and page-graph position** (§3.2.1, §3.2.4). Type each - interaction against an **Abstract Interaction Object** (`single-select`, - `trigger-action`, …), never a concrete control. Declare the projection it - surfaces, the commands it offers, its transitions, and its modelled meaning - (emphasis, state meanings, accessibility, content references). Drive the intent - down: anything you used the intent to decide must become a modelled element. - -**Run the self-audit as you go.** The model tells you where it is incomplete: - -- **State justification** (§3.4): every field on an aggregate must be read by some - Decider. An unread field is a finding — dead state, or (more often) an - *unmodelled invariant* you have not written yet. -- **Decider justification** (§3.4): every Decider must have at least one reachable - rejection. A Decider that never rejects is a finding — trivial behaviour that - needs no Decider, or a missing invariant. -- **Intent-reliance** (§3.2.1): if you leaned on the one-sentence intent to settle - a realisation choice, that choice was a missing model element. Model it, retire - that part of the intent. - -**Exit criterion:** the What is a typed slice of the graph; every Decider and -Projector **simulates sound and complete**; the self-audit checks pass (no -unjustified state, no toothless Decider, intent driven down). You have written no -code, and you are confident the behaviour is right. - ---- - -## Stage 2 — How (the realisation) - -> **The question: *What realises this, on what substrate, against which -> contracts?*** - -The What is settled; now bind it to realisation. You are writing *against* a fixed -What, never alongside an unsettled one (§2.1, step 6). - -1. **Bind to the contracts** (§4.2). Confirm the application contract holds — - decision logic in a pure, isolable core, so it can be checked against the - Decider. Record the runtime/deployment identity (domain, bundle, runtime) for - this instance. -2. **Place it in the repository layout model** (§4.3). Know where each file the - feature needs is legal, and what spine the slice requires (its tests, its - contracts). The layout model both scaffolds and verifies. -3. **Reify the UI** (§4.5). For each AIO, choose the concrete component per - **context of use** — and remember the **interaction class** (GUI/TUI) gates - which contexts apply (§3.2.2): a GUI brings form factor and pointer/touch; a TUI - brings terminal capabilities. Bind to the design system; style via tokens, never - literals. Resolve **content** keys against the content store per locale (§4.6). - Where an AIO cannot honestly cross to a target class, declare it *unreifiable* — - a recorded gap, not a silent one. - - On the CLI this is the `product design-system` family: `add ` vendors - a §11.3 manifest (declaration half **plus** its implementation bundle — per-target - component sources, token values per theme, templates) under - `.product/design-systems//`; `validate` runs the wholeness + bundle checks; - `couple` is the §11.2 coverage check; `bind ` records the choice on the How - contract. Once bound, every `product codegen` backend resolves it: the coupling - becomes a *plan-time gate* (a coverage gap fails the emit), the reified tree - carries `design-system.g.json` (the reify(AIO, context) → CIO map by value) + - `tokens.g.css`, and `product codegen web` renders one on-system page per UI step, - styled exclusively through tokens. `product codegen check` catches design-system - drift exactly as it catches graph drift. -4. **Generate the interface contracts** from the domain model (§4.4) — OpenAPI, - AsyncAPI, and so on — never hand-written, so the published surface cannot drift - from the meaning. - -**Exit criterion:** every AIO has a reifying component for each declared context; -every content key resolves for each locale; the interface contracts are generated; -the layout model knows where the feature's files belong. The realisation is fully -specified and nothing about *how* is left to chance at build time. - ---- - -## Stage 3 — Build (make it real) - -> **The question: *Does it pass, and is 'done' computed?*** - -Now the work units run — each a single bounded transformation from a frozen input -to one artifact (§5), referencing the What and How by pointer, never re-deciding -them. - -> **Build runs *across* a seam.** The work unit is where the framework's -> specification ends and execution begins (§5.1). The specification side **emits** -> a frozen work unit — by value, with a hash identity — and an **executor** returns -> a **verdict event**. The framework fixes that contract and nothing on the far -> side: scheduling, batching, and *how* the realisation is produced are the -> executor's concern. You may run the framework's own executor or any other that -> honours the seam — they are interchangeable. The steps below are what happens -> on the execution side of that seam. - -1. **Scaffold from the layout model** — it tells the generator where to place what. -2. **Implement against the oracles.** The Decider and Projector you simulated in - Stage 1 are now the **oracles** the realised code is checked against — the - realised behaviour must produce *identical* outputs to them (§6.3). -3. **Run the verifications, cheapest first** (§6.3): **layout conformance** (tree - shape) → **behavioural conformance** (matches the Decider/Projector) → - **seam** (screen agrees with its UI step) → **domain & data conformance** - (structure matches; real data validates). Each is a deterministic gate; a - failure stops the build, with no override-by-assertion. -4. **Let "done" be computed, not judged** (§7.2). A feature is done when every flow - passes behavioural conformance, every concept in its *derived* footprint passes - domain conformance, every verification citing one of its elements is green, and - its acceptance criteria pass. You do not *decide* it is done; the predicate - says so. - -**Exit criterion:** all required verifications are green, and `feature_done` computes -true. Ship it. - ---- - -## The loop back — why this is a cycle, not a line - -The arrows go forward, but three signals send you upstream, and a healthy team -treats them as routine, not failure: - -- **Intent-reliance rate** (§3.2.1) — you are still leaning on prose intent to - decide UI. Go back to the What and model what was missing. -- **State / Decider justification** (§3.4) — orphaned state or a never-rejecting - Decider has surfaced an *unmodelled invariant*. Go back and write the rule. -- **Data-divergence rate** (§3.1) — and this one can fire *after you ship*: - production data no longer satisfies the declared structure. Either the data is - wrong, **or the spec has gone stale** — go back to the domain model and - reconcile. This is the framework's one continuous, in-production signal. - -Feedback re-enters the funnel as a fresh forward pass — you do not run the stages -backwards, you run them forward again over the gap that was found (§2.1). Over -time the model gets more complete and the right-hand stages get more determined: -that is the *reproducibility → measurement → improvement* chain -([§1](docs/product-framework.md#1-purpose-and-scope)) made into a daily habit. - ---- - -## The one-page cheat sheet - -| Stage | The question | You produce | Exit when | -|---|---|---|---| -| **Intake** | Where on the model does this live, and what's the one-sentence intent? | a located request + intent | you can name system, context, flow(s), intent | -| **What** | What happens, what decides it, what does the user see — and does it simulate? | a typed event-model slice + Deciders/Projectors + UI steps | simulations pass; self-audit clean; no code yet | -| **How** | What realises this, on what substrate, against which contracts? | reification, content, contracts, layout placement | every AIO reified, every key resolved, contracts generated | -| **Build** | Does it pass, and is 'done' computed? | realised code + green verifications | all verifications green; `feature_done` = true | - -> **If you remember one thing:** simulate the Decider before you write code. Most of -> what the framework buys you is paid out at that single moment — the behaviour is -> proven right while a fix still costs a sentence. diff --git a/docs/live-wire-plan.md b/docs/live-wire-plan.md deleted file mode 100644 index 75cf3532..00000000 --- a/docs/live-wire-plan.md +++ /dev/null @@ -1,168 +0,0 @@ -# Live-Wire Completion Plan — the 1.7.0 explorer ⇄ the live graph - -## Status (as implemented) - -`GET /api/pf` projects the live graph into `window.PF`; `app.jsx` fetches it before -first render + on the SSE `changed` tick, merging the fields in `PF_LIVE_KEYS`. - -**Live (verified headless against the real product-cli graph):** -- Graph: Everything -- What: Systems map · Domain ER · Flows · Deciders · Scenarios · Projectors -- UI §3.2: AIO catalog (live reification + WCAG) · Pages (page graph) · Steps (spec sheets) · Screens (render contract) -- How §4: Systems (blueprint / DeployableUnits / why-cascade) · Patterns · contracts · standards · **Layout rules** -- Build §5–6: Work units (live SPMC bundles) -- Delivery §7: Features (real §7.2 done) · Versions -- Per-node conformance dots computed from real verdicts (feature_done / decider conform / release) - -**Still on demo data — the genuine data boundary (the graph carries no such data):** -- **Data** (refData / oracle) — 0 reference-sets / production-datasets in the graph. -- **Content** — 0 content-stores modelled (renders live-but-empty). -- **Composition** (§4.5 Atomic-Design narrative) · **Process** (H1–H6 companion doc) — fixed narrative, not graph-derived. -- **Reification** (design-system manifest) — derivable from design-systems + reification-rules; not yet projected. -- **Verifications** (§6.3) — the required kinds are framework-universal (demo is representative); per-product standings need real verification results. -- **Layout's repo-tree pane** — needs a live repository scan against the layout rules. - -Every view renders (never crashes). Each remaining item becomes live the same way -the shipped ones did: project its field → add the key to `PF_LIVE_KEYS`. - -## Write-side per-product scoping (done) -The shared resolver `product_core::pf::paths::product_base(repo_root, product)` -is the single source of truth for both reads (the projection) and writes (CLI + -MCP): the **root product** (product.toml name) uses `.product/`, every other -product is scoped to `.product/products//`. -- **CLI** — every authoring command honours `--product`: `feature`, - `deployable-unit`, `decider`, `projector`, `deliverable`, `release`, `target`, - `build`, `blueprint`, `how`, `cell`, `work-unit`. They resolve their artifact - dir through `shared::artifact_dir(product, kind)` (→ `product_base`), so - `--product acme` writes/reads under `.product/products/acme/` and the default - product stays global. Cross-module helpers (`deliverable::load*`/`save`, - `decider::deciders_dir`/`conformed_set`, `projector::projectors_dir`, - `build::load_work_units`) all take the resolved product. -- **MCP** — the handler mirrors resolve the same base via `pf_mcp::pbase(args, - repo_root)` (the `product` arg, else the config name, else the shared - `.product/`), the write/read twin of `shared::artifact_dir`. Covers the - delivery, decider, projector, primitive, deployable-unit, how (read+write), - cell/work-unit scaffold, and blueprint families. `pdir` is now test-only. -- **Verified** — `product decider derive order --product acme` lands in - `.product/products/acme/deciders/`; `decider list` (default) stays global, - `decider list --product acme` is scoped, no leak. The showcase derives acme's - Ordering Deciders (`scripts/showcase-acme-how.sh`) so its Behaviour view is - live too. - -## Per-product scoping + product picker -- **Per-product artifacts.** The projection resolves a per-product base - `.product/products//` (falling back to the shared `.product/` for the - self-hosted product-cli), so a product's How/Delivery/Build artifacts — - blueprint, DeployableUnits, how-contract, features/deliverables/releases/ - targets, work-units, deciders — are its own. ACME ships its own - `acme-storefront` blueprint + 3 DeployableUnits + why-cascade + deliverables - (`scripts/showcase-acme-how.sh`), so its How/Delivery views are acme-specific - while product-cli's stay unchanged. -- **Product picker.** `/api/pf` + `/api/graph` take `?product=`; the banner shows - a picker whenever more than one product has a captured What graph, switching - product-cli ⇄ acme (and any future product). - -## Since then -- **Live repo-scan** for the §4.3 Layout tree (`pf_view/pf_repo.rs`) — walks the - real repo files the blueprint's layout rules cover, attributes each to its rule, - emits an indented tree with per-file verdicts. Layout is now fully live. -- **ACME Shop showcase/test product** (`scripts/showcase-acme.sh`) — authors a - strict-conformant second product (51 nodes: 2 domains, 2 systems, entities/VOs/ - invariants, full event model, AIOs/WCAG/reification/reference-data, ui-steps/ - page-graph, a cross-system journey, quality demands) into - `.product/products/acme/` via the real CLI, exercising the whole authoring - surface end-to-end. View it in the explorer at **`/?product=acme`** (the - `/api/pf` + `/api/graph` handlers now take a `?product=` override). This also - makes **Data** (reference-sets) and the UI reification views live for acme. - -## North star (definition of done) -Every view renders the real `.product/` graph, refreshes over SSE, and shows honest -conformance — with **no bundled demo data consumed at runtime** (demo becomes an -offline-only fallback). `PF_LIVE_KEYS` contains every top-level field; a contract test -guards backend↔UI drift. - -## The core finding -The new UI's views are **bespoke layouts hardcoded to the acme demo** (fixed pixel -coordinates, demo ids — e.g. `DomainGraph` uses a `PLACE[id]` map + literal "Ordering" -title; `SystemsMap` used `pos.acme`). They are *not* generic data-driven renderers. -"Complete live-wire" therefore = **rewrite each view to auto-layout live data** -(`SystemsMap` is the template) **+** project the remaining data fields. - -## Guiding principles -- **Each view ships end-to-end in one slice**: project its data → rewrite the view - data-driven → add its key to `PF_LIVE_KEYS` → verify headless (0 console errors + - a live-data assertion) → commit. Never half-wire (that caused the Versions crash). -- **Backend stays under the fitness gates**: split `pf_view/` into per-section modules - (`pf_flows`, `pf_how`, add `pf_ui`, `pf_build`, `pf_delivery`, `conformance`); - functions ≤40 stmts, files ≤400 lines. -- **Layout once, reuse everywhere**: a shared auto-layout toolkit makes each view - rewrite ~40 lines, not ~150. - ---- - -## Phase 0 — Foundations (unblocks all rewrites) -| Item | Where | Notes | -|---|---|---| -| Layout toolkit | `assets/ui/shared.jsx` → `PFUI.layout` | `rowLayout`, `gridLayout`, `layeredColumns` (longest-path, already in `pf_flows`), `orthogonalEdges`/routing. Ported/generalized from the old `view.html`. | -| Conformance model | new `pf_view/conformance.rs` | `conformance_of(id) -> described\|realised\|verified\|delivered` from graph + `.product` verdicts (feature_done §7.2, decider `.conform.json`, deliverable acceptance, release membership). Replaces hardcoded `"realised"`. | -| PF contract test | `pf_view` tests | Golden test: `/api/pf` emits every key the UI reads (drift guard). | - -## Phase 1 — The old-view trio (Domain, Flows, Graph) -| View | Backend | Frontend | Effort | -|---|---|---|---| -| Domain ER | `domain` ✅ (add all-contexts + selector) | rewrite `DomainGraph.jsx` auto-layout; read `contextId` for title | M | -| Flows | `flows` ✅ (computed lanes/cols) | rewrite `FlowsTimeline.jsx` to consume computed layout | M | -| Everything | ✅ (live systems/domains) | verify `GraphView`/`buildGraph`; add to allowlist | S | - -## Phase 2 — Behaviour (Deciders, Scenarios) -| View | Backend | Frontend | Effort | -|---|---|---|---| -| Deciders | enrich `deciders`: per-command `handles` (from `logic`), real `stateRead`/`rejections`/`coverage` | rewrite `DecidersView` | M | -| Scenarios | new: `scenarios` from each Decider's `scenarios` (+ simulate verdicts) | rewrite `ScenariosView` | M | - -## Phase 3 — Delivery (Features, Versions) -| View | Backend | Frontend | Effort | -|---|---|---|---| -| Features | enrich `delivery.features`: friendly name, real `done` (`pf::done::feature_done`), derived footprint closure, per-clause status | rewrite `FeaturesView` | M | -| Versions | enrich `delivery.versions` from product/how versions + targets; derive `bump`/`diff` | rewrite `VersionsView` | M | - -## Phase 4 — The How (§4) -| View | Backend (`pf_how`) | Frontend | Effort | -|---|---|---|---| -| How · Systems | ✅ (enrich patterns.files/rules from layout; real blueprint→system) | rewrite `HowViews` blueprint/DU/cascade | M | -| Patterns | `patterns` ✅ | rewrite | S | -| Layout | new: `how.layout` from blueprint `layout.yaml` + `repoTree` (real repo scan vs rules) | rewrite | M | -| Composition / Reification | new: `how.contracts`, `how.standards`, `composition`, `manifest` from app/infra contracts + design-systems + reification-rules | rewrite | L | -| Process | new: `howProcess` (H1–H6 from binding state) | rewrite `HowProcessView` | M | - -## Phase 5 — UI section (§3.2) — largest; new `pf_view/pf_ui.rs` -| View | Backend | Frontend | Effort | -|---|---|---|---| -| AIOs | `aios` + `aioUsage` | rewrite `AIOCatalogView` | M | -| Steps | `stepSpecs` from wireframe-steps | rewrite `UIStepsView` | M | -| Pages | `pageGraph` from application-roots + navigate edges | rewrite `PageGraphView` | M | -| Screens | `contract` (screen composition) from steps + reification + cios | rewrite `ScreenPreview` | L | -| Content | content store + `resolveContent` | rewrite `ContentView` | M | -| (data) | `wcag`, `refData`, `oracle` from wcag-criteria / reference-sets / primitives | consumed above | M | - -## Phase 6 — Build (§5–6) -| View | Backend (`pf_view/pf_build.rs`) | Frontend | Effort | -|---|---|---|---| -| Work units | `workUnits` from `.product/work-units/` (bundle/hash) + build verdicts | rewrite `BuildViews` | M | -| Verifications | `verificationKinds` §6.3 + real standings | rewrite `BuildViews` | M | - -## Phase 7 — Completeness & hardening -- Remove `PF_LIVE_KEYS` gate (all keys live); demo = offline fallback only. -- Empty-state polish for every view (e.g. 0 DeployableUnits). -- Model blueprint→system→DeployableUnit edges explicitly (graph can't currently say which blueprint realises which system). -- SSE reconnect/backoff + indicator. -- Contract-drift test in CI; `/api/pf` shape smoke test. - -## Sequencing -`0 → 1 → 3 → 2 → 4 → 5 → 6 → 7` (Delivery before Behaviour: mostly ready + high-visibility). -Total ≈ 20 view rewrites + ~18 projection fields + conformance/layout foundations. - -## Decisions (defaults, pending confirmation) -- **Execution:** Phase 0 with review, then autonomous run through Phases 1–6, check in at phase boundaries. -- **Conformance:** computed from real verdicts (Phase 0 `conformance.rs`). -- **Tracking:** this doc + a tracked task list. diff --git a/docs/predicate-format.md b/docs/predicate-format.md deleted file mode 100644 index 01c0238e..00000000 --- a/docs/predicate-format.md +++ /dev/null @@ -1,96 +0,0 @@ -# Predicate Entry Format — v1 - -Schema for filing a predicate instance in the catalog. An entry instantiates the definition in -`predicate-definition.md`; every required element there appears as a required field here. - -Entries are definitional objects. **They carry no status field.** Closure findings are claims, -filed separately, referenced by id. This split is enforced structurally, not by convention. - ---- - -## Schema - -```yaml -predicate: - id: # stable identifier: pred// - format: 1 # entry format version, independent of content - name: # short human name - statement: # the acceptance relation in one sentence: - # accept c iff - - artifact_class: # what c ranges over — exactly one of: - # code | configuration | execution | interface | data | composition - # | html-css (the governed markup+styling pair, admitted at M7) - - ground: # what the check must see; each item carries provenance - - fact: # the required information - provenance: # controlled | observed | inferred | institutional - - tolerance: # the declared bound; without this the entry is invalid - rejection_witness: # what a reject produces — evidence a violation exists - - depends_on: [] # predicate ids this one presupposes (catalog graph edges) - - closure_claims: [] # claim ids only — closure lives in the claim graph, with - # status and falsifiers, never in this entry - - proxy_gap: # optional; for gameable predicates: the value this - # predicate proxies and how they can diverge - - notes: # optional -``` - -## Field rules - -- **id** is permanent. Renames create a new entry with a supersedes note. -- **statement** names both `c` and `G`. A statement that mentions no ground is suspect. -- **artifact_class** disambiguates same-named predicates. Race-freedom over `execution` and - race-freedom over `code` are two entries. -- **ground** with an `observed` or `inferred` item cannot be closed at build time by any - arrangement. This follows from the signature; no claim is needed to establish it. -- **depends_on** is acyclic. Composites do not get entries; they decompose into edges. -- **closure_claims** may be empty. An entry with no closure claims is a defined predicate - nobody has yet made a finding about — valid, and honest. - ---- - -## Example - -```yaml -predicate: - id: pred/data/shape-conformance - format: 1 - name: Data-shape conformance - statement: > - Accept payload c iff c conforms exactly to the declared schema in G, - with no missing, extra, or type-mismatched fields. - - artifact_class: data - - ground: - - fact: the declared schema for the boundary - provenance: controlled - - fact: the payload as received at the boundary - provenance: observed - - tolerance: exact structural match; no coercion, no unknown-field passthrough - - rejection_witness: the first violating field path and the violated constraint - - depends_on: - - pred/data/well-formedness # payload must parse before shape can be judged - - closure_claims: - - DDD-cat-01 # "TypeScript strict mode operationally closes this at compile - # time for values that never cross a serialisation boundary" - - DDD-cat-02 # "runtime schema validation operationally closes this at the - # boundary, at per-request cost" - - proxy_gap: > - Conformance proxies validity. A payload can conform to the schema and still be - semantically invalid; that residue belongs to domain-invariant predicates. - - notes: > - The observed-ground item is why no static arrangement closes this for external - input: the payload does not exist at build time. -``` diff --git a/docs/product-framework-open.md b/docs/product-framework-open.md deleted file mode 100644 index 5dc41d1f..00000000 --- a/docs/product-framework-open.md +++ /dev/null @@ -1,1179 +0,0 @@ -# The Product Framework - -**An open standard for specifying software as What, How, and Delivery.** - -*A conformant instantiation of the Two Pillars Specification Framework. Version 1.9.1 — open specification.* - ---- - -> **What this is.** An **open, catalog-agnostic standard** for describing a software product as three connected models — **What** it is, **How** it is built, and how it is **Delivered** — in a form that is reproducible, verifiable, and traceable. It defines the **shapes and rules**, not any particular product. Anyone can build catalogs, blueprints, and tooling against it. -> -> It is a conformant instantiation of the **Two Pillars Specification Framework** for software; every construct maps to a named Two Pillars concept ([§8](#8-conformance-to-the-two-pillars)). - -*Published under a dual license: the specification text under CC BY 4.0, and accompanying shapes/code under Apache-2.0 ([License](#license)). This document describes the format; it ships no proprietary catalog content.* - ---- - -## Contents - -1. [Purpose and scope](#1-purpose-and-scope) -2. [The three models and the split](#2-the-three-models-and-the-split) · [2.1 Authoring order](#21-authoring-order--the-funnel-as-a-process) -3. [The What — structure and behaviour](#3-the-what--structure-and-behaviour) · [3.0 The product and its boundary](#30-the-product-and-its-boundary) · [3.0.1 Journeys](#301-journeys--flows-that-span-systems) -4. [The How — realising the What](#4-the-how--realising-the-what) -5. [Work units and the rationale trace](#5-work-units-and-the-rationale-trace) · [5.1 The Build seam](#51-the-build-seam--the-work-unit-emission-contract) · [5.2 The code-generation seam](#52-the-code-generation-seam--the-codegen-manifest-and-file-plan) -6. [Verification — the conformance bar](#6-verification--the-conformance-bar) -7. [Delivery — bringing the What to a verifiable 'done'](#7-delivery--bringing-the-what-to-a-verifiable-done) -8. [Conformance to the Two Pillars](#8-conformance-to-the-two-pillars) -9. [Encoding and the derivation contract](#9-encoding-and-the-derivation-contract) -10. [Conformance rules (normative summary)](#10-conformance-rules-normative-summary) -11. [Design System Conformance Profile](#11-design-system-conformance-profile) — 🅿 *Preview* -12. [Content Store Conformance Profile](#12-content-store-conformance-profile) — 🅿 *Preview* -13. [Data Conformance Profile](#13-data-conformance-profile) — 🅿 *Preview* -14. [Authoring Scope Conformance Profile](#14-authoring-scope-conformance-profile) — 🅿 *Preview* - ---- - -## 1. Purpose and scope - -Software is usually described in scattered, human-only artifacts — a wiki of requirements, diagrams that drift from the code, tickets that lose their rationale. This framework defines a single, connected, machine-readable way to describe a product so that the description can **drive generation, gate verification, and explain itself** — and so that delivery against it has a precise, queryable notion of "done." - -It rests on one chain of dependencies: **reproducibility → measurement → improvement.** You cannot measure what you cannot reproduce; you cannot improve what you cannot measure. The framework's purpose is to make a product description reproducible and verifiable, so the rest follows. - -**In scope** - -- The structure of the three models — What, How, Delivery — and the typed links between them. -- The conformance rules every instance must satisfy. -- The mapping to the Two Pillars ([§8](#8-conformance-to-the-two-pillars)). - -**Out of scope (deliberately)** - -- Any specific product, blueprint, or pattern library — those are built *on* the framework, not part of it. -- Quality-criteria content (the actual audits/checks) — the framework requires that conformant verifications exist and meet a strength bar ([§6](#6-verification--the-conformance-bar)); it does not supply them. -- Delivery cadence and team practice — the framework defines the delivery *model*, not how an organization runs it ([§7](#7-delivery--bringing-the-what-to-a-verifiable-done)). - -> **The open/closed line.** This framework is the **empty form and its rules**. The forms you fill in — your domain models, your reusable How (blueprints), and especially your verification libraries — are yours. The framework is designed so that adopting it never requires disclosing them. - ---- - -## 2. The three models and the split - -A product is described by three models, in strict seniority. Each derives from or serves the ones above it. - -``` -WHAT what the system is and does — owned by product & design - | domain model (structure) + event model (behaviour) - | ----- the split: business meaning -> technical realisation ----- -HOW how the code expresses the What — owned by engineering - | decisions / principles / patterns + contracts + interface & screen standards - v -DELIVERY how the What is brought to a verifiable 'done' — model only - features / releases as partitions of the What; done as a predicate -``` - -**The split is the central idea.** Above it lives **meaning**, expressed in the language of the business and owned by the people who own the product. Below it lives **realisation**, owned by engineering. The What is authored and agreed **before** the How is written. The line between them is where business meaning becomes technical reality, and keeping it explicit is what lets non-engineers own the What and engineers own the How without either guessing the other's intent. - -**Everything is one graph.** All three models, and the links between them, form a single machine-readable graph (RDF is the reference encoding; [§9](#9-encoding-and-the-derivation-contract)). "Describe this system" is therefore a query, not a stale document, and impact analysis ("what depends on this?") is a graph traversal. This is not a stylistic preference but a **conformance obligation**: the graph nature is what makes every other contract in this framework checkable, and it is specified — abstractly, technology-independently — by the parent Two Pillars **Representation Contract**, which any conformant substrate must satisfy (addressable typed nodes and edges; traversable derivation edges; footprint, blast radius, and `done` computable over the graph; views derivable as projections, never separately maintained; the whole specification queryable). The framework instantiates that contract; it does not redefine it. A "specification" in this framework that is not a real, queryable, derivation-preserving graph — a pile of documents that merely *describe* one — is **not conformant**, however complete its content (§10). - -### 2.1 Authoring order — the funnel as a process - -The seniority above is not only a layering; it dictates an **order in which the artifacts must come into existence**, because each is *derived from* the ones before it (§9). You cannot author a Decider before the events it is derived from exist, nor a reification rule before the AIO it reifies, nor run data conformance before the structure is declared. The order is therefore **not a practice the framework invents** — it is the topological reading of the derivation contract, the same way build order is read from the graph (§7.2). This section is normative about that *order*; it stays silent, like §7.3, about *practice* — cadence, ceremonies, who authors, when, in what tool. Those are organization-specific. What follows is forced by dependency, not by methodology. - -The organizing principle is the **funnel**: constraint density rises toward the value action, and the most constrained, most *shared* things are authored first so everything downstream can reference rather than re-decide them. Hard reasoning concentrates upstream; what remains downstream is increasingly determined. - -**The forward order (each step derives from the ones above):** - -1. **Product, domains, and domain structure** (§3.0, §3.1). Name the product (the boundary) and the domains it owns; within each domain, the shared meaning — bounded contexts, entities, relations, invariants as validatable shapes. This is the most shared layer; everything references it. It is also adoptable *alone* (§13): a structure plus production data already yields continuous data conformance, before any behaviour is modelled. -2. **Reference data** (§3.1). Declare the constitutive instance data behaviour will depend on (valid methods, categories, taxonomies), since events and invariants reference it. -3. **Events, Deciders, and Projectors — the domain's behaviour** (§3.2–§3.4). Model what happens over the structure — the domain-typed events, commands, read models (with their state spaces) — and make the interesting behaviour and non-trivial projections executable. Behaviour belongs to the **domain**, so it is authored once and shared by every system that references the domain. Deciders/Projectors are *derivable only once events exist*, and are **simulated sound and complete before any realisation** — the first gate, and the cheapest. Genuinely irreducible computation is named and bounded as a primitive here (§3.5). -4. **Systems and their flows** (§3.2.5). Name the systems (surfaces) that reference the domains, and lay out each system's flows — journeys that *arrange* the domain behaviour modelled in step 3, referencing its commands and events rather than re-deciding them. A flow belongs to one system; the behaviour it invokes is shared. -5. **UI steps, AIOs, and the page graph** (§3.2.1–§3.2.4). Where there are screens, derive each step's data and actions from the projections and commands already modelled, typed against AIOs; lay out the page graph. Intent is permitted here but is *debt to be driven down* (§3.2.1), not licence to decide downstream. -6. **The How** (§4). Only now resolve the abstractions the What declared: reification rules per context (§4.5), content per locale (§4.6), the contracts and layout model (§4.2–§4.3), interface standards (§4.4). The How is written *against* a settled What, never alongside an unsettled one. - -**Verification is interleaved, not a final phase.** The gates do not wait for the end. Behavioural simulation runs *before* realisation (step 4); layout conformance is the cheapest gate and runs *first* among the realisation checks; data conformance runs *continuously* once structure exists (step 1). Each narrowing of the funnel has its own gate, authored alongside the artifact it protects (§6). A conformant process never reaches "now we test" — it tests at every step, soonest where cheapest. - -**The feedback loop — when the cycle sends you back upstream.** The order is a forward chain, but it is not a waterfall, because two signals re-enter it as fresh input: - -- the **intent-reliance rate** (§3.2.1) — a measure of *under*-specification. A high rate means a downstream author had to lean on prose intent to decide something; the remedy is to go *back upstream* and model what was missing (an AIO, a context, a reification rule), then retire the intent. Feedback re-enters the funnel as a new forward pass, not as an inversion of it. -- the **data-divergence rate** (§3.1) — a measure of *over*-confidence, and the one signal that can fire *after* deployment. Rising divergence means production data no longer satisfies the declared structure: either the data is wrong or **the spec has gone stale**, and the loop returns to step 1 to reconcile the model with reality. - -Both signals turn the order into a **cycle**: author down the funnel, gate at each step, and let the two metrics pull you back up when the model says too little or claims too much. This is the reproducibility → measurement → improvement chain (§1) made procedural — the spec improves because its own gaps and staleness are measured as you work and after you ship. - -> **What this section does not say.** It fixes the *order* artifacts come into being, because the derivation contract forces it. It does not fix *when* on a calendar, *who* does each step, how big a slice is, or how the work is reviewed and scheduled — that is practice (§7.3), and it remains the organization's to choose. Deviate from the *order* and you will author something before the thing it derives from exists; deviate from the *practice* and you are simply running the framework your own way. *(A non-normative companion, `GUIDE.md`, turns this order into an everyday developer flow — Intake → What → How → Build — for teams who want a worked starting point; it adds no conformance rules.)* - ---- - -## 3. The What — structure and behaviour - -The What is the specification every role reads and agrees on. It is organised by a three-level ownership hierarchy that fixes *where each part lives* and *what is shared* — so that several surfaces of one product draw on the same concepts and the same rules rather than re-deciding them. - -**Product → Domains + Systems.** At the top is the **product** — the thing being built. A product **owns one or more domains** (the meaning and rules of its entities) and **one or more systems** (the surfaces that use them). A **system references whole domains**; it does not own them. This is what makes domain concepts genuinely shared: they are owned one level up, by the product, not duplicated inside each system. - -``` -Product the thing being built (§3.0) - ├─ owns ─▶ Domain(s) meaning + rules — entities, structure, data, - │ invariants, AND behaviour (events, Deciders, Projectors) - └─ owns ─▶ System(s) surfaces — page graph, flows, UI steps, platform/class - └─ references ─▶ Domain(s) (whole domains; finer slicing is by - bounded context *inside* a domain) -``` - -The dividing line is a single principle: **the domain owns meaning and rules; a system owns surface and journey.** Everything true *regardless of who is looking* — what a `Refund` is, that `refund_total ≤ paid_total`, which events an `Order` emits — belongs to the **domain**. Everything about *a particular way of using* the domain — which screens, which navigation, which platform — belongs to the **system**. The decisive test: **if two systems would disagree about it, it is system-level; if they must agree, it is domain-level.** Behaviour must agree (a refund is a refund in the customer app and the admin tool alike), so behaviour lives in the domain; navigation differs between them, so it lives in the system. - -The payoff is that two systems over the same domain share not just *vocabulary* but *rules* — the same Deciders, the same invariants, the same events — and differ only in how they surface them. "Use the same concepts across systems" taken to its end: same concepts *and* same behaviour, authored once. - -### 3.0 The product and its boundary - -The **product** is the root of the What: the named thing being built, the owner of the domains and systems that compose it. It carries little of its own — a name and purpose — because its job is *ownership and boundary*, not behaviour: - -- it **owns one or more domains** (§3.1) — the shared meaning and rules. Multiple domains are bounded from each other (an `Ordering` domain and a `Catalog` domain), with cross-domain correspondences declared, never assumed. -- it **owns one or more systems** (§3.2.5) — the surfaces. A single-system product is the common case; a product becomes multi-system when one body of meaning is surfaced more than one way (a customer app, an admin website, an operator CLI). -- it is the home for **product-wide concerns**: the direction the product is moving toward (§7.3), product-level quality demands (§3.6), and **journeys** (§3.0.1) — end-to-end paths that span systems — sit here, because they are properties of *the product*, not of any one system. - -The product's boundary is exactly the set of domains and systems it owns. A second product is a second root: it may *reference* a shared domain across a declared boundary (the Translation pattern, §3.2.0, is how its systems would communicate), but it does not absorb it. The product node answers "what, in total, is being built, and what shared meaning holds it together?" — a question the framework previously left to folklore. - -#### 3.0.1 Journeys — flows that span systems - -A flow belongs to exactly one system (§3.2.5). But a *business* process often does not: a customer places an order in the shop, and staff fulfil it in the admin tool — one journey, two systems. Modelled only as two disconnected per-system flows, the end-to-end path is invisible; no one can trace "order to fulfilment" as a whole. A **journey** makes that path first-class **without re-coupling the systems**. - -A journey is a **product-level, derived composition**: a declared path through single-system flows, linked at their crossing points, where **every crossing is a Translation** (§3.2.0). It owns nothing — it references flows and Translations that already exist — so it adds *visibility*, not ownership or new behaviour. Flows stay single-system and system-owned; the journey is a view *over* them, owned by the product because only the product sees across its systems. - -``` -product "order to fulfilment" journey - acme-shop flow place-order ──┐ - │ ⟶ Translation: ev-order-placed (acme-shop) - │ → cmd-accept-fulfilment (acme-admin) - acme-admin flow fulfil-order ◀─┘ -``` - -The rules that keep it from undoing the system boundary (§3.0): - -- **Every crossing is a Translation, or it is a finding.** A journey may not hop between systems by any other means — not a direct reference to another system's screen, not a shared table, not an assumed call. The only sanctioned cross-system link is the event-driven Translation channel (§3.2.0): the crossing reads an event from the source system and issues a command into the target. This is what guarantees a journey adds *no new coupling* — it composes exactly the links the framework already permits. -- **A journey references; it does not contain.** It names flows (by reference) and the Translations between them. It introduces no new flow, no new behaviour, no new screen. Remove a journey and nothing else changes — the flows and Translations it pointed at are untouched. This is the same derived-view discipline as a feature (a referenced subgraph, §7.1) and direction (a computed gap, §7.3). -- **Systems stay independent.** Because the only link is a Translation reading published events, `acme-shop` still does not depend on `acme-admin`'s internals — the journey sees the path, but neither system's spec reaches into the other. Either system can be specified, realised, and versioned alone; the journey simply records that their flows compose into a larger path. - -The payoff is end-to-end visibility that disconnected flows cannot give: a journey is a path through Translations, so the framework's **value-anchoring** (every path terminates in a value action, §6/§7) and **blast-radius** (the footprint of a change, a pure graph traversal) now extend *across* systems. "What is the full footprint of order-to-fulfilment?" becomes a single traversal spanning the shop and the admin tool — which a pile of per-system flows could never answer. A journey is to cross-system process what a feature is to single-system behaviour: a derived, referenced, checkable subgraph of the graph. - -### 3.1 The domain model — structure, data, and behaviour - -A **domain** is owned by the product and **referenced by the systems that use it**. It is the home of *meaning and rules*: the shared definition of **what concepts mean**, how they relate, and **how they behave** — entities, structure, data, invariants, *and* the events, Deciders, and Projectors that act on them (§3.2–§3.4). Behaviour lives here, not in a system, because a rule is true wherever its concept is used: the refund invariant holds in every system that touches `Refund`. A system arranges domain behaviour into journeys (§3.2.5); it does not own or alter it. - -A domain model has **two sides**, and conflating them is a common defect: - -- the **structure** — what *constitutes* a valid model: the entity types, relations, cardinalities, and invariants. This is what developers usually mean by "domain model" (the DDD, type-level sense), and it is the SHACL-*shape* side of the reference encoding (§9). -- the **data** — the actual populated facts: *which* products exist, what they are called, the real entities. This is the side data practitioners work from; they validate a model against what the data *actually is*, and they catch what the structure side cannot see (a field that is null in 90% of rows, an enum value the schema never declared, two "distinct" entities that are one row in practice). It is the RDF-*instance* side of the same encoding. - -A model trusted only from the structure side is asserted top-down and never reconciled with ground truth — which is exactly why data practitioners distrust developer domain models. The framework therefore makes **both sides first-class**, and distinguishes two kinds of data by their relationship to the model. - -**Structure — conformance requirements:** - -- **Bounded contexts, not one flat model.** Within a context a term has exactly one meaning (the **ubiquitous language**); the same word may mean different things in different contexts. Cross-context correspondences are **explicit declared mappings**, never assumed. (This is what resolves "is a User a Customer?" rather than restating the confusion.) -- **Entities, relations, value objects, invariants.** Relations carry cardinality **and rationale**. Invariants are stated as machine-checkable constraints. An **entity** has identity that persists while its attributes change; a **value object** is defined wholly by its attributes and is immutable (a `Money`, an `OrderNo`). Which fields an entity carries is not a free choice: an aggregate holds *exactly its invariant-bearing state* — what its Deciders must read at decision time — and everything else a screen wants is a Projector's output, not a stored field (§3.4). -- **Machine-readable, with a constraint language.** The model must be expressible as a graph with validatable shapes (RDF + SHACL is the reference; an equivalent is conformant). A diagram alone is not a conformant domain model — it cannot generate or validate. - -**Data — reference data is What; production data is an oracle:** - -- **Reference data is part of the What.** Some instance data is *constitutive* — the system's behaviour is undefined without it: the set of valid shipping methods, the tax categories, the product taxonomy, the currencies accepted. This is **reference data**, and it lives in the What because the model and behaviour reference it: a `single-select` of shipping options (§3.2.2) draws its options from declared reference data, not from thin air, and an invariant like "tax category must be one of the declared set" is checkable only because the set is modelled. Reference data is versioned and governed like the rest of the What. -- **Production data is the validation oracle, not part of the What.** The live entities — the 4,200 products, the real orders — are *not* specification; they are *populated* facts the running system holds. But they serve a role the framework values: they are an **oracle the structure is checked against.** "Does real data conform to the declared shapes?" is a verification — **data conformance** (§6.3) — and it catches model defects nothing else will. A structure that no production data satisfies is as suspect as a Decider no scenario exercises: the model earns trust by real data passing its shapes, not by being declared. This is the data practitioner's lens given teeth. - -> **Data conformance reads in both directions — and never closes.** Every other conformance check in the framework runs one way: the artifact is judged against the spec, and a failure means the *artifact* is wrong. Data conformance is the first check whose failure is **genuinely ambiguous**: when production data violates a shape, either the data is wrong *or the spec is stale*, and the framework does not presume which. That ambiguity is the mechanism, not a weakness — it is the one place the running system can **indict the specification.** A shape that real data persistently violates may not be catching bad data; it may be reporting that the model no longer describes the system, and the correct response may be to fix the spec, not the data. This inverts the usual authority: elsewhere the spec is the truth the world is held to; here, production data is a *witness* against a stale spec. - -> **Authored once, asserted continuously.** Every other verification is a build-time gate — it runs once, before acceptance. Data-conformance shapes are different in kind: authored once in the What, they can be evaluated against live data **forever** — on every write, or on a schedule — so the same artifact is both a build-time gate on the model's coherence *and* a standing **production monitor**. This dual-temporality (write once, assert continuously) is what lets a system know, at any moment, whether the data it holds is valid against what it believes should be true. - -> **Spec staleness becomes measurable — the data-divergence rate.** Because the assertion runs continuously, the rate at which production data diverges from the declared model is a direct, ongoing signal of how far the spec has fallen behind reality. This is the **data-divergence rate**, a first-class metric and the *over-confidence* counterpart to the intent-reliance rate (§3.2.1): where intent-reliance measures under-specification (the model says too little), data-divergence measures over-confidence (the model claims something reality no longer supports). A spec with a rising divergence rate is a spec going stale — now *visible as it happens*, rather than discovered when something breaks. This closes the reproducibility → measurement → improvement chain (§1) onto the spec's own fidelity. - -> **The constitutive/populated test.** If changing a datum changes what the system *means* (remove a shipping method and a behaviour becomes impossible), it is **reference data** and belongs in the What. If it is just more rows of the same kind (one more product), it is **production data** and belongs to the running system, where it serves as the conformance oracle. The line is not "is it data" but "is it specification." - -### 3.2 The event model — behaviour - -The description of **what happens over time**, peer to the domain model, in the same graph. Conformance requirements: - -- **Built from domain-typed primitives.** Triggers, events, commands, read models (Views), and UI steps each reference domain concepts: a **trigger** issues a command, an event **changes** an entity, a command **targets** an aggregate, a read model **projects** entities. Behaviour may not reference structure that does not exist. The blocks wire into four named patterns (§3.2.0). A read model also declares (or makes inferable from its query shape) its **state space** — at minimum `present`, plus any of `loading`, `empty`, or `failed` it can actually exhibit: a projection over a possibly-empty collection can be `empty`, one backed by something that can be pending can be `loading`, one whose delivery can fail can be `failed`; a guaranteed-singleton, synchronous projection has none of these. The state space is mostly *derived* from the projection's shape and declared only where the shape does not settle it. -- **Depth is proportional to behavioural complexity.** Concepts with rich or historical behaviour get full event models; simple create/read/update/delete concepts get a minimal one or none. The framework requires that the *interesting* behaviour be modelled, not that every triviality be ceremony. -- **Owned by product and design.** Its natural form — a timeline with interface steps — is readable and signable by non-engineers; it is the bridge between concepts and screens. - -> **Why structure and behaviour are one graph.** An event is never free-floating: it always changes a domain entity. Modelling them in one graph means structure and behaviour cannot drift apart, and a single question — "what happens to this concept?" — returns both its shape and the flows that change it. - -#### 3.2.0 Building blocks and patterns - -The event model is built from **four building blocks**, wired into **four patterns** (the framework adopts the Event Modeling vocabulary here, because it is a precise, widely understood naming of shapes the framework already uses; the patterns name the *topology* of the event graph, while the Decider (§3.3) and Projector (§3.4) supply the *executable semantics* inside the Command and View blocks). The blocks: - -- **Trigger** — *what initiates a command.* A Trigger has exactly one of three **sources**: a **user** (through a UI — this is the UI step, §3.2.1), an **external caller** (an API request from outside the system), or an **automated process** (a step that reads a View and issues a command with no human in the loop). Generalising "what issues a command" into one block is what makes automation and cross-system communication expressible as patterns rather than special cases — a robot and a user are the *same shape*, differing only in source. -- **Command** — an intention to change state, carrying its parameters. A Command targets an aggregate and is decided by that aggregate's Decider (§3.3), which accepts it into events or rejects it. -- **Event** — a business fact that happened and was recorded. The irreducible unit of truth; everything else is derived from events. Named in the past tense, in the ubiquitous language, carrying the data the fact entails. -- **View** — a query that reads, interprets, and curates already-recorded events for a consumer (a UI step, a report, or an automated Trigger). A View is a read model, made executable by a Projector (§3.4). Its defining honesty: **a View can present only information some event already carries** — if a View needs a field no event supplies, the model is incomplete, surfaced before any code (the Projector's event-coverage check, §3.4). - -> **Payload schemas — the wire contract of a Command or Event.** A Command or Event MAY declare a **payload schema**: a list of named fields, each optionally typed from the datatype vocabulary (`string · integer · number · boolean · date`). §3.2.2 already binds an input AIO to a "command payload field" whose type "comes from the domain model" — the payload schema is where that field is *declared*, so a payload's shape is **authored**, not inferred from Decider logic or scenarios. With one system a payload is a private convenience; between two systems (a service and an app) it is a **public wire contract** and must be authored, not guessed. Where a Decider's or Projector's scenarios also exercise a field, a **declared type takes precedence** over anything inferable, and input AIOs (§3.2.2) derive their fields and validation from this schema. The datatype vocabulary above — named on the domain model's data shapes (§3.1) — is thereby the *single* normative type vocabulary for entity attributes, data-shape constraints, and payload fields alike; the payload schema is encoded exactly as the entity-attribute shape is (the `has_field` link, §9). - -The blocks compose into exactly four **patterns**, each a named, checkable shape: - -| Pattern | Shape | Meaning | -| --- | --- | --- | -| **Command** | `Trigger → Command → Event(s)` | a state change, from what initiates it to the facts it records | -| **View** | `Event(s) → View` | recorded facts curated for a consumer; reads only events that exist | -| **Automation** | `Event(s) → View → Trigger(automated) → Command → Event(s)` | the system acting on itself — a process observes a View and issues a Command | -| **Translation** | `Event(s) [system A] → View → Trigger(automated) → Command → Event(s) [system B]` | one system telling another that something happened | - -- **Command and View** are the everyday shapes, and the framework already enforced them under other names: a Command pattern is a Decider invocation (`decide` produces the events); a View pattern is the Projector's no-foreign-events / event-coverage discipline (§3.4). Naming them makes the shapes themselves queryable — *every Command traces to a Trigger and produces ≥1 event; every View reads only declared events* — as conformance checks (§6.3). -- **Automation is the normative way to model self-acting behaviour.** When the system should *do something automatically*, it is modelled as an Automation pattern: an automated Trigger watches a View and issues a Command, exactly as a user would — observe, then act. The discipline the pattern enforces is that **the automated Trigger contains no business logic**: it reads a View (typically a *to-do list* projection — one row per outstanding action) and, for each row, issues one Command; the resulting Event updates the View so the row is struck off and the same work is never triggered twice. All decision-making stays in the Decider the Command reaches; the Trigger only *notices and calls*. This keeps automated behaviour as verifiable as user behaviour — same Decider, same simulation, same gates — with the only addition being the View-driven trigger. -- **Translation is the normative way sibling systems communicate.** Where a What describes several systems over one domain (§3.2.5), a system tells another that something happened **only** through a Translation pattern: an automated Trigger reads a View over the *source* system's events and issues a Command into the *target* system, producing the target's own events. The framework imposes the pattern's standing restriction: **the read side reads events from exactly one source system; the write side may fan out** to several targets (a publish/subscribe translation). This makes cross-system coupling an explicit, directional, traceable edge in the graph — never an implicit shared table or an assumed call — and it is the event-model counterpart to the federation discipline the domain side already requires (no shared mutable state across systems; communication is by recorded fact). - -> **Slicing — the unit of work.** A single pattern instance on the graph is a **slice**: the smallest coherent unit that can be handed to an implementer, because it carries everything from trigger to recorded fact. This is the same object the framework calls a work unit (§5) — Event Modeling's "slice" and the framework's work unit are the same atom, named from two traditions. A slice is implementable precisely because it is a complete pattern: a Trigger, the Command it issues, the Decider that decides it, and the Events that result (or a View and the events it reads). A **flow** (§3.2.4) is a connected chain of such slices, and a **feature** (§7.1) is a subgraph of one or more flows — so the containment is *feature ⊇ flow ⊇ slice*. "Slice" is reserved for this atom throughout; a feature is a subgraph, never itself a slice. - -#### 3.2.1 The UI step — the What of a screen - -A **UI step** is the point in a flow where a person sees something and acts. It is **What**, owned by product and design: it says what a screen is *for* and *means*, never how it looks. How it is composed and rendered is the screen-composition contract in the How (§4.5); the two meet at a verified seam. The split is not a matter of discipline but of **type**: a UI step may reference only **Abstract Interaction Objects** (§3.2.2) — `single-select`, `trigger-action`, `text-entry`, and the like — never a concrete control (a dropdown, a button) from a design system. An AIO is meaning; a concrete control is realisation. Because the two are distinct kinds of node in the graph, "a UI step naming a dropdown" is not a style lapse to be caught by review — it is a structural violation a verification rejects (§6.3), the same way the layout allowlist rejects a misplaced file. The fusion the framework forbids (§2) is made impossible by construction, not discouraged by instruction. - -A conformant UI step declares two layers. The first is the **buildable core** — four facts, each already a node or edge in the model, so the step is an *annotation on the flow*, not a new artifact: - -- **Intent** — one sentence, in the ubiquitous language, for what the user is trying to accomplish here. Intent is **not modelled specification** — it is the marked residue of what the model does not yet determine (see *Intent is specification debt* below). It is permitted, but it is a liability to be driven down, never the field that "gives the implementer good judgment." -- **Information shown** — the read-model **projection** this step surfaces (it `projects`, §3.2), presented through one or more **display AIOs** (`display-value`, `display-collection`, …). The What names *which projection* and *which abstract interaction*, never how it is laid out. ("Surfaces the order-summary projection as a `display-collection`" — not "in a table.") -- **Actions available** — the **commands** the user may issue here (exactly the commands valid at this step — the ones a Decider `handles`, §3.3), each offered through an **action or input AIO** (`trigger-action`, `single-select`, `text-entry`, …). The What names *which decision* and *which abstract interaction*, never the control. ("`single-select` of a shipping option, `trigger-action` to confirm" — not "a dropdown and a primary button.") -- **Transitions** — given an action or a resulting event, which page follows. A transition is a `navigate` edge in the application's single **page graph** (§3.2.4); the step names its position in that graph. (These edges were undersold as "a flow's internal topology" — they are in fact the edges of the whole application's navigation.) - -The second layer is **modelled meaning** — declarations that shape realisation without prescribing it. These are *not* residue: each is a checkable model element the seam verification (§6.3) enforces, and each is a thing intent gets *promoted into* (see below): - -- **Emphasis** — what matters most at this step, so the How knows what to make prominent. ("The total owed is the decisive figure" — not "make it large and bold.") -- **Projection states as meaning** — a projection is not always a present value; it can occupy any state in its declared **state space** (§3.2), and what each state *means to the user* is a behavioural fact, not styling. A UI step's state annotations are both **constrained** and **covering**: it may annotate only states the projection actually has (no "empty" meaning for a projection that cannot be empty), and it must give a meaning to *every* state the projection can be in, or explicitly waive one with a reason. This is the UI analogue of the Decider's command-coverage rule (§3.3) — exhaustiveness over the projection's state alphabet — and the dangerous case it catches is the *forgotten* state: a projection that can fail whose screen never says what failure means. The waiver is the escape hatch a Decider doesn't get, because some states are legitimately ignorable (a load too fast to perceive); the waiver must say why. For `failed` specifically, the *meaning* is What ("the user must know it can't be shown and how to recover"); the failure *mechanism* is How — the same split as an accessibility criterion versus its discharge (§3.2.3). -- **Accessibility obligations** — *not* prose about what "must be perceivable," but a set of **WCAG 2.2 success criteria** the step must satisfy, referenced as ingested entities (§3.2.3). Most are inherited from the AIOs the step uses (a `text-entry` carries its labelling criteria, an image-bearing `display-value` carries 1.1.1 Non-text Content); a step may add screen-specific criteria. This replaces the old free-text "accessibility intent": a criterion is a checkable entity with a known verification type, where prose was an unverifiable wish. -- **Content references** — the standing, authored words a screen carries that are *not* projected data and *not* a control label: a heading, explanatory body copy, the prose of an empty or error state, help text, legal text. These are referenced by **content key with a declared role** (heading / body / empty-message / error-message / help / legal / …), never written as literal strings in the UI step. The role and the obligation are What ("this page needs a heading that states its purpose; its empty state needs a message conveying the way forward"); the words themselves are resolved by the How against a **content store** (§4.6). This is the same move as tokens-not-literals and AIOs-not-widgets, applied a fourth time to *copy* — and for the same reason: a literal baked into the What cannot be translated, swapped, or verified, while a keyed reference can. A page may consist *entirely* of content references and no projection or command — a pure-content page (an "about" or informational page) is simply a page whose interaction is "read," needing no new page kind. - -> **Intent is specification debt, not implementer autonomy.** It is tempting to call intent the field that lets an implementer exercise good judgment — but that would run the funnel principle backwards. Everywhere else the framework drives unmodelled specification *out*, concentrating hard reasoning upstream so execution is determined; a UI step that leaned on an implementer's taste to decide its realisation would be exactly the under-specification the funnel is meant to expose. So intent is treated as what it is: **specification that is not modelled yet.** It is allowed (the framework refines incrementally; not everything can be modelled upfront), but it is a *measured liability*, governed by one rule — -> -> **Whatever intent was used to decide must be promoted into the model.** If an implementer consulted intent to choose a control, a grouping, or what to stress, that choice was a missing model element — a missing AIO, a missing context of use, a missing reification rule (§4.5), or a missing emphasis/state/accessibility declaration above. The remedy is to model it as data and *retire that part of the intent*. Intent therefore shrinks monotonically toward a pure statement of purpose; the residue that remains is only the irreducibly-human "why this step exists," itself a candidate for the task/flow model later. -> -> **The metric.** Because intent is debt, it is measurable, which is what makes it improvable (reproducibility → measurement → improvement, §1): the **intent-reliance rate** — how often realising a screen required consulting intent to settle something the model did not determine. Zero means the model fully determines the UI; a high rate means the funnel is leaking at the UI stage, the precise UI analogue of an implementer role needing an over-capable model. Driving the rate down *is* the act of moving UI specification from prose into the graph. - -> **Why this is symmetric to the Decider — and where intent sits.** The Decider (§3.3) makes behaviour *executable from the model*: its signature is derived, and the one thing genuinely authored is its decision *logic* — real, irreducible specification. The UI step makes a screen *buildable from the model* in the same way: its data and actions are derived, and the genuinely authored specification is the **modelled** part — the AIO typing, the emphasis/state/accessibility declarations, and (in the How) the reification rules. *That* is the honest peer of Decider logic: modelled choices, checked not assumed. **Intent is not part of this symmetry.** It is not authored specification of equal standing; it is the residue that has not been modelled yet, marked as debt and slated for promotion into exactly those modelled elements. Both modelled sides impose a checked How-side constraint — the Decider constrains the pure core (§4.2), the UI step constrains the screen composition (§4.5) — and this is the funnel principle for UI: the What fixes what the screen *means* as modelled data, leaving the How only *how it presents*, with intent measuring how far that modelling still has to go. - -#### 3.2.2 Abstract Interaction Objects and the context of use - -The vocabulary a UI step is typed against is the set of **Abstract Interaction Objects (AIOs)** — the *kinds of interaction* a user can have, independent of any device, modality, or design system. This is the Abstract User Interface layer of the established four-level model the framework follows for UI (Task & Concepts → Abstract UI → Concrete UI → Final UI); the domain model and flows are the Task & Concepts layer, AIOs are the Abstract UI layer, the design system's components (§4.5) are the Concrete UI layer, and running code is the Final UI. Adopting this layering is what lets one What drive several different concrete UIs without changing its meaning. - -**An AIO is meaning, not a widget.** `single-select` means "choose exactly one from a set"; it is *not* a dropdown, a radio group, or a segmented control — those are the concrete forms it can take. The distinction is load-bearing precisely because the same abstract interaction reifies to different controls in different situations: a `single-select` over three options on a tablet is well served by a segmented control, the same `single-select` over forty options on a phone by a searchable list. The choice between them is a real UX decision — but it is a decision about *realisation in a context*, not about what the step means, which is why it belongs below the What, not in it. - -**The base set is normative; the set is extensible.** A conformant instance recognises a stable core of AIOs, and may register additional ones against the same definition (an AIO is a named, modality-independent kind of user interaction, with a declared arity over domain data where applicable). The core, at minimum: - -| AIO | Means | Typed over | -|---|---|---| -| `trigger-action` | invoke an operation | a command | -| `single-select` | choose one from a set | a command parameter / a domain enumeration | -| `multi-select` | choose any number from a set | a command parameter / a collection | -| `text-entry` · `numeric-entry` · `date-entry` | supply a typed value | a command's payload field (its type from the domain model) | -| `display-value` | show a single datum | a projected field | -| `display-collection` | show many of a kind | a projected collection | -| `navigate` | move between interaction spaces | a transition (§3.2.1) | -| `edit` | revise an existing value in place | a field of a projected entity + the command that updates it | - -An adopter adding, say, `range-select` or `reorder` declares it the same way and it becomes referenceable; the core stays small so that the common cases are interoperable across instances. - -**Input AIOs derive their shape from the domain model.** This closes the form/data gap: a `text-entry` or `single-select` is not a free-floating field — it is bound to a **command payload field**, and that field's type, constraints, and (for selects) its allowed values come from the domain model (§3.1). A form is therefore a composition of input AIOs over a command's payload, derived — its fields are exactly the command's parameters, its validation exactly the domain invariants — not authored on the screen. What the user can *enter* is fixed by the command they are composing, just as what they can *do* is fixed by the commands valid at the step. - -**The context of use is a declared model.** Per the four-level model, context-of-use enters at the Abstract→Concrete boundary, not above it: the AUI is deliberately context-independent, and the *same* AIO is reified differently depending on context. A conformant instance declares its relevant **contexts of use** — at minimum **form factor** (e.g. phone, tablet, desktop), **modality** (e.g. pointer, touch, voice), and **platform** (e.g. iOS, Android, web), and optionally user type and physical environment — as named elements. **Platform is a context dimension like any other**: the same `single-select` reifies to a UIKit picker on iOS, a Material control on Android, and a `