harness-everything 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +48 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/config.json +9 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/config.json +20 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/expected.json +12 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r003.md +26 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/valid/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/valid/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r001.body.md +39 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-dropped.body.md +35 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-incomplete.body.md +37 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-one-replaced.body.md +39 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-overlap.body.md +39 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/docs/adr/GRPH-001.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/docs/adr/GRPH-002.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/docs/adr/GRPH-001.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/docs/adr/GRPH-002.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/docs/adr/GRPH-010.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/docs/adr/legacy/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/docs/adr/GRAPH-007.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/docs/adr/legacy/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/docs/adr/legacy/v1/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/docs/adr/legacy/v2/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-003.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-004.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-005.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-006.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-007.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/docs/adr/GRPH-020.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/docs/adr/GRPH-021.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/docs/adr/GRPH-003.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/docs/prd/graph.md +10 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/expected.json +1 -0
- package/ci/fixtures/doc-obligations/derive/acceptance-only/base/docs/prd/graph.md +22 -0
- package/ci/fixtures/doc-obligations/derive/acceptance-only/candidate/docs/prd/graph.md +22 -0
- package/ci/fixtures/doc-obligations/derive/alias-only/base/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/alias-only/candidate/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision/candidate/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/candidate/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/inventory.json +7 -0
- package/ci/fixtures/doc-obligations/derive/attribution-impossible/base/ci/mechanism-open.test.js +3 -0
- package/ci/fixtures/doc-obligations/derive/attribution-impossible/candidate/ci/mechanism-open.test.js.fixture +2 -0
- package/ci/fixtures/doc-obligations/derive/clause-context-typo/base/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/clause-context-typo/candidate/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/code-only/base/src/lint.js +1 -0
- package/ci/fixtures/doc-obligations/derive/code-only/candidate/src/lint.js +1 -0
- package/ci/fixtures/doc-obligations/derive/comment-only-test/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/comment-only-test/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/computed-member-assert/base/ci/mechanism-c1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/computed-member-assert/candidate/ci/mechanism-c1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/computed-member-variable/base/ci/mechanism-c1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/computed-member-variable/candidate/ci/mechanism-c1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/config.json +11 -0
- package/ci/fixtures/doc-obligations/derive/decision-fence-backtick/base/docs/adr/GRPH-010.md +20 -0
- package/ci/fixtures/doc-obligations/derive/decision-fence-backtick/candidate/docs/adr/GRPH-010.md +20 -0
- package/ci/fixtures/doc-obligations/derive/decision-fence-tilde/base/docs/adr/GRPH-010.md +20 -0
- package/ci/fixtures/doc-obligations/derive/decision-fence-tilde/candidate/docs/adr/GRPH-010.md +20 -0
- package/ci/fixtures/doc-obligations/derive/deep-strict-equal/base/ci/mechanism-s1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/deep-strict-equal/candidate/ci/mechanism-s1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/deleted-adr/base/docs/adr/GRPH-008.md +14 -0
- package/ci/fixtures/doc-obligations/derive/deleted-prd/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/deleted-prd/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/deleted-prd/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/derived-rename/base/docs/index.md +3 -0
- package/ci/fixtures/doc-obligations/derive/derived-rename/base/src/model/a.js +1 -0
- package/ci/fixtures/doc-obligations/derive/derived-rename/candidate/docs/index.md +3 -0
- package/ci/fixtures/doc-obligations/derive/derived-rename/candidate/src/model/b.js +1 -0
- package/ci/fixtures/doc-obligations/derive/derived-rename/config.json +7 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/base/docs/index.md +3 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/base/docs/model-summary.md +3 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/base/src/model.js +1 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/candidate/docs/index.md +3 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/candidate/docs/model-summary.md +3 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/candidate/src/model.js +1 -0
- package/ci/fixtures/doc-obligations/derive/derived-two-outputs/config.json +10 -0
- package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/base/docs/prd/graph-one/r001.md +19 -0
- package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/base/docs/prd/graph-two/r001.md +19 -0
- package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/candidate/docs/prd/graph-one/r001.md +19 -0
- package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/candidate/docs/prd/graph-two/r001.md +19 -0
- package/ci/fixtures/doc-obligations/derive/docs-only/base/docs/reference/guide.md +3 -0
- package/ci/fixtures/doc-obligations/derive/docs-only/candidate/docs/reference/guide.md +3 -0
- package/ci/fixtures/doc-obligations/derive/dup-req-id/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/dup-req-id/base/docs/prd/graph-a.md +20 -0
- package/ci/fixtures/doc-obligations/derive/dup-req-id/base/docs/prd/graph-b.md +20 -0
- package/ci/fixtures/doc-obligations/derive/dup-req-id/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/dup-req-id/candidate/docs/prd/graph-a.md +20 -0
- package/ci/fixtures/doc-obligations/derive/dup-req-id/candidate/docs/prd/graph-b.md +20 -0
- package/ci/fixtures/doc-obligations/derive/expect-computed-literal/base/ci/mechanism-e.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/expect-computed-literal/candidate/ci/mechanism-e.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/expect-computed-matcher/base/ci/mechanism-e.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/expect-computed-matcher/candidate/ci/mechanism-e.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/expect-not-to-equal/base/ci/mechanism-s1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/expect-not-to-equal/candidate/ci/mechanism-s1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/formatting-only-test/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/formatting-only-test/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/helper-delegated-test/base/docs/prd/graph.md +18 -0
- package/ci/fixtures/doc-obligations/derive/helper-delegated-test/candidate/ci/mechanism-helper.test.js +1 -0
- package/ci/fixtures/doc-obligations/derive/helper-delegated-test/candidate/docs/prd/graph.md +18 -0
- package/ci/fixtures/doc-obligations/derive/historical-only-req/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/historical-only-req/base/docs/prd/graph/r001.md +25 -0
- package/ci/fixtures/doc-obligations/derive/historical-only-req/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/historical-only-req/candidate/docs/prd/graph/r001.md +25 -0
- package/ci/fixtures/doc-obligations/derive/historical-only-req/candidate/docs/prd/graph/r002.md +23 -0
- package/ci/fixtures/doc-obligations/derive/in-place-current/base/ci/mechanism-inplace.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/in-place-current/base/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/in-place-current/base/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-obligations/derive/in-place-current/candidate/ci/mechanism-inplace.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/in-place-current/candidate/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/in-place-current/candidate/docs/prd/graph/r002.md +28 -0
- package/ci/fixtures/doc-obligations/derive/in-place-historical/base/ci/mechanism-inplace.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/in-place-historical/base/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/in-place-historical/base/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-obligations/derive/in-place-historical/candidate/ci/mechanism-inplace.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/in-place-historical/candidate/docs/prd/graph/r001.md +22 -0
- package/ci/fixtures/doc-obligations/derive/in-place-historical/candidate/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-obligations/derive/invalid-clause/base/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/invalid-clause/candidate/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/legacy-untagged-fallback/base/ci/mechanism-legacy.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/legacy-untagged-fallback/candidate/ci/mechanism-legacy.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/literal-whitespace-assert/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/literal-whitespace-assert/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/malformed-citation-attribution/base/ci/mechanism-bad.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/malformed-citation-attribution/candidate/ci/mechanism-bad.test.js.fixture +6 -0
- package/ci/fixtures/doc-obligations/derive/malformed-prd/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/malformed-prd/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/malformed-prd/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/malformed-prd/candidate/docs/prd/graph.md +11 -0
- package/ci/fixtures/doc-obligations/derive/missing-approval/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/missing-approval/candidate/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/mixed-attribution/base/ci/mechanism-mixed.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/mixed-attribution/base/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/mixed-attribution/candidate/ci/mechanism-mixed.test.js.fixture +5 -0
- package/ci/fixtures/doc-obligations/derive/mixed-attribution/candidate/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/mixed-docs-code/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/mixed-docs-code/base/docs/reference/guide.md +3 -0
- package/ci/fixtures/doc-obligations/derive/mixed-docs-code/base/src/lint.js +1 -0
- package/ci/fixtures/doc-obligations/derive/mixed-docs-code/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/mixed-docs-code/candidate/docs/reference/guide.md +3 -0
- package/ci/fixtures/doc-obligations/derive/mixed-docs-code/candidate/src/lint.js +1 -0
- package/ci/fixtures/doc-obligations/derive/multiline-comment-only/base/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-comment-only/candidate/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-expect/base/ci/mechanism-multi.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/multiline-expect/candidate/ci/mechanism-multi.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/multiline-formatting-only/base/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-formatting-only/candidate/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/base/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/base/docs/prd/graph.md +18 -0
- package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/candidate/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/candidate/docs/prd/graph.md +18 -0
- package/ci/fixtures/doc-obligations/derive/multiline-strictequal/base/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/multiline-strictequal/candidate/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/new-prd/candidate/docs/prd/graph-new.md +20 -0
- package/ci/fixtures/doc-obligations/derive/new-reference/candidate/docs/reference/new-note.md +3 -0
- package/ci/fixtures/doc-obligations/derive/new-req-test/candidate/ci/mechanism-new.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/orphan-req-test/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/orphan-req-test/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/orphan-req-test/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/orphan-req-test/candidate/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/partial-revision/base/ci/mechanism-partial.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/partial-revision/base/docs/prd/graph/r001.md +24 -0
- package/ci/fixtures/doc-obligations/derive/partial-revision/candidate/ci/mechanism-partial.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/partial-revision/candidate/docs/prd/graph/r001.md +24 -0
- package/ci/fixtures/doc-obligations/derive/partial-revision/candidate/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-obligations/derive/regex-literal-whitespace/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/regex-literal-whitespace/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/rename/base/docs/reference/old-guide.md +3 -0
- package/ci/fixtures/doc-obligations/derive/rename/candidate/docs/reference/guide.md +3 -0
- package/ci/fixtures/doc-obligations/derive/replacement-target/base/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/replacement-target/candidate/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/req-context-typo/base/docs/prd/graph.md +26 -0
- package/ci/fixtures/doc-obligations/derive/req-context-typo/candidate/docs/prd/graph.md +26 -0
- package/ci/fixtures/doc-obligations/derive/req-revision/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/req-revision/base/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/req-revision/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/req-revision/candidate/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/req-revision/candidate/docs/prd/graph/r002.md +28 -0
- package/ci/fixtures/doc-obligations/derive/req-revision-in-place/base/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/req-revision-in-place/candidate/docs/prd/graph/r001.md +22 -0
- package/ci/fixtures/doc-obligations/derive/retained-revision-chain/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/retained-revision-chain/base/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/retained-revision-chain/base/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-obligations/derive/retained-revision-chain/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/retained-revision-chain/candidate/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-obligations/derive/retained-revision-chain/candidate/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-obligations/derive/string-literal-tag/base/ci/mechanism-str.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/string-literal-tag/candidate/ci/mechanism-str.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/tag-inside-test-call/base/ci/mechanism-inside.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/tag-inside-test-call/candidate/ci/mechanism-inside.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/tagged-test/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/tagged-test/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/tagged-test-prd/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/tagged-test-prd/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/tagged-test-prd/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/tagged-test-prd/candidate/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/template-literal-tag/base/ci/mechanism-tpl.test.js +4 -0
- package/ci/fixtures/doc-obligations/derive/template-literal-tag/base/docs/prd/graph.md +18 -0
- package/ci/fixtures/doc-obligations/derive/template-literal-tag/candidate/ci/mechanism-tpl.test.js +4 -0
- package/ci/fixtures/doc-obligations/derive/template-literal-tag/candidate/docs/prd/graph.md +18 -0
- package/ci/fixtures/doc-obligations/derive/template-literal-whitespace/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/template-literal-whitespace/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/title-only-rename/base/ci/mechanism-title.test.js +3 -0
- package/ci/fixtures/doc-obligations/derive/title-only-rename/candidate/ci/mechanism-title.test.js +3 -0
- package/ci/fixtures/doc-obligations/derive/title-only-unrelated-computed/base/ci/mechanism-u.test.js +4 -0
- package/ci/fixtures/doc-obligations/derive/title-only-unrelated-computed/candidate/ci/mechanism-u.test.js +4 -0
- package/ci/fixtures/doc-obligations/derive/top-level-no-file-tag/base/ci/mechanism-top.test.js +4 -0
- package/ci/fixtures/doc-obligations/derive/top-level-no-file-tag/candidate/ci/mechanism-top.test.js +4 -0
- package/ci/fixtures/doc-obligations/derive/trailing-comment-test/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/trailing-comment-test/base/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/trailing-comment-test/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/trailing-comment-test/candidate/docs/prd/graph.md +20 -0
- package/ci/fixtures/doc-obligations/derive/two-tests-one-file/base/ci/mechanism-two.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/two-tests-one-file/base/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/two-tests-one-file/candidate/ci/mechanism-two.test.js +6 -0
- package/ci/fixtures/doc-obligations/derive/two-tests-one-file/candidate/docs/prd/graph.md +24 -0
- package/ci/fixtures/doc-obligations/derive/typo-context/base/docs/adr/GRPH-007.md +14 -0
- package/ci/fixtures/doc-obligations/derive/typo-context/candidate/docs/adr/GRPH-007.md +14 -0
- package/ci/fixtures/doc-obligations/derive/typo-decision/base/docs/adr/GRPH-007.md +14 -0
- package/ci/fixtures/doc-obligations/derive/typo-decision/candidate/docs/adr/GRPH-007.md +14 -0
- package/ci/fixtures/doc-obligations/derive/unbalanced-statement/base/ci/mechanism-multi.test.js +5 -0
- package/ci/fixtures/doc-obligations/derive/unbalanced-statement/candidate/ci/mechanism-multi.test.js.fixture +4 -0
- package/ci/fixtures/doc-obligations/derive/unknown-adr/base/docs/adr/GRPH-009.md +14 -0
- package/ci/fixtures/doc-obligations/derive/unknown-adr/candidate/docs/adr/GRPH-009.md +12 -0
- package/ci/fixtures/doc-obligations/derive/unsupported-assertion-syntax/base/ci/mechanism-s1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/unsupported-assertion-syntax/candidate/ci/mechanism-s1.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/whitespace-only-test/base/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/derive/whitespace-only-test/candidate/ci/mechanism-lint.test.js +2 -0
- package/ci/fixtures/doc-obligations/evidence/base/docs/adr/GRPH-007.md +15 -0
- package/ci/fixtures/doc-obligations/evidence/base/docs/prd/PRD-GRPH-001.md +25 -0
- package/ci/fixtures/doc-obligations/evidence/candidate/docs/adr/GRPH-007.md +15 -0
- package/ci/fixtures/doc-obligations/evidence/candidate/docs/prd/PRD-GRPH-001.md +25 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +759 -0
- package/ci/mechanism-2ax-doc-lifecycle.test.js +1061 -0
- package/ci/mechanism-2ay-prd-revisions.test.js +2891 -0
- package/ci/mechanism-325-3a-cross.test.js +795 -0
- package/ci/mechanism-325-3a-doc-evidence.test.js +1172 -0
- package/ci/mechanism-325-3a-doc-obligations.test.js +742 -0
- package/ci/mechanism-pr334-haiku.test.js +298 -0
- package/ci/mechanism-pr334-luna.test.js +192 -0
- package/ci/mechanism-pr334-opencode.test.js +313 -0
- package/ci/mechanism-pr334-r2-boundary.test.js +239 -0
- package/ci/mechanism-pr334-r2-resolution.test.js +243 -0
- package/ci/mechanism-pr334-r2-scope.test.js +127 -0
- package/ci/mechanism-pr334-r3-boundary.test.js +356 -0
- package/ci/mechanism-pr334-r3-citations.test.js +205 -0
- package/ci/mechanism-pr334-r3-headings.test.js +268 -0
- package/docs/document-lifecycle.md +262 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +471 -0
- package/harness-everything/scripts/doc-lifecycle.js +2401 -0
- package/harness-everything/scripts/doc-obligations.js +1571 -0
- package/harness-everything/scripts/project-docs-resolver.js +764 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +437 -30
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +471 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +2401 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +1571 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +764 -0
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +437 -30
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +168 -54
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +749 -96
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +168 -54
- package/to-spec/scripts/project-docs-resolver.js +749 -96
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// PR #334 round 3 — heading boundary regressions in section extraction.
|
|
4
|
+
//
|
|
5
|
+
// extractSection in doc-obligations.js recognized only column-zero ATX headings. A
|
|
6
|
+
// CommonMark-valid Decision headings indented 1..3 spaces were invisible, so a real
|
|
7
|
+
// Decision edit reported quality-review and conflict-check N/A. Related boundary
|
|
8
|
+
// controls cover Setext Decision headings and tab-indented fence lookalikes.
|
|
9
|
+
//
|
|
10
|
+
// Authoritative rule: https://spec.commonmark.org/0.31.2/#atx-headings
|
|
11
|
+
// * 0-3 ASCII spaces of indentation, then 1-6 "#", then a space/tab or end of line;
|
|
12
|
+
// * 4 spaces or a leading tab is code indentation, not a heading;
|
|
13
|
+
// * a closing sequence must be preceded by a space/tab.
|
|
14
|
+
//
|
|
15
|
+
// Scope is bounded to Decision section boundaries. This is not a CommonMark parser.
|
|
16
|
+
// Fence and code boundaries are exercised as regression controls.
|
|
17
|
+
//
|
|
18
|
+
// Every case runs the full deriveDocFacts -> deriveDocObligations pipeline. Expectations
|
|
19
|
+
// come from the spec and the D6.5 normative-change rule, not from the implementation.
|
|
20
|
+
|
|
21
|
+
const assert = require('assert');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const helper = require('./test-helper');
|
|
25
|
+
|
|
26
|
+
const modulePath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-obligations.js');
|
|
27
|
+
const fixtureDir = path.join(__dirname, 'fixtures', 'doc-obligations', 'derive');
|
|
28
|
+
const CONFIG = JSON.parse(fs.readFileSync(path.join(fixtureDir, 'config.json'), 'utf8'));
|
|
29
|
+
const ADR = 'docs/adr/GRPH-007.md';
|
|
30
|
+
|
|
31
|
+
let deriveDocFacts;
|
|
32
|
+
let deriveDocObligations;
|
|
33
|
+
try {
|
|
34
|
+
({ deriveDocFacts, deriveDocObligations } = require(modulePath));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
helper.check('pr334-r3-headings. derive module is available', false, error.message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('\n[pr334-r3-headings] ATX heading indentation in Decision section extraction...');
|
|
40
|
+
|
|
41
|
+
function readFixture(caseName, side, file = 'GRPH-007.md') {
|
|
42
|
+
return fs.readFileSync(path.join(fixtureDir, caseName, side, 'docs', 'adr', file), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Existing typo-decision fixture: one Decision body word changes "lint" -> "lnt".
|
|
46
|
+
const TYPO_BASE = readFixture('typo-decision', 'base');
|
|
47
|
+
const TYPO_CANDIDATE = readFixture('typo-decision', 'candidate');
|
|
48
|
+
|
|
49
|
+
function adr(body) {
|
|
50
|
+
return ['---', 'id: GRPH-007', 'role: adr', 'status: accepted', '---', '# GRPH-007 Lint first', '', body, ''].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function replaceOnce(text, from, to) {
|
|
54
|
+
assert.strictEqual(text.split(from).length, 2, `fixture text contains exactly one "${from}"`);
|
|
55
|
+
return text.replace(from, to);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function indentDecision(text, prefix) {
|
|
59
|
+
return replaceOnce(text, '## Decision', `${prefix}## Decision`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applicability(base, candidate, file = ADR) {
|
|
63
|
+
const result = deriveDocFacts({
|
|
64
|
+
base: { files: { [file]: base } },
|
|
65
|
+
candidate: { files: { [file]: candidate } },
|
|
66
|
+
config: CONFIG
|
|
67
|
+
});
|
|
68
|
+
const obligations = deriveDocObligations(result.facts);
|
|
69
|
+
return new Map(obligations.map(item => [item.capability, item.applicability]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The normative capabilities must agree with each other; doc-lint and link-check always apply to a governed edit.
|
|
73
|
+
function assertDecisionEdit(name, base, candidate, want, file = ADR) {
|
|
74
|
+
const map = applicability(base, candidate, file);
|
|
75
|
+
assert.strictEqual(map.get('doc-lint'), 'applicable', `${name}: doc-lint`);
|
|
76
|
+
assert.strictEqual(map.get('link-check'), 'applicable', `${name}: link-check`);
|
|
77
|
+
assert.strictEqual(map.get('quality-review'), want, `${name}: quality-review`);
|
|
78
|
+
assert.strictEqual(map.get('conflict-check'), want, `${name}: conflict-check`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkCase(name, run) {
|
|
82
|
+
try {
|
|
83
|
+
run();
|
|
84
|
+
helper.check(name, true);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
helper.check(name, false, error && error.message ? error.message : String(error));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Matrix 1: indentation of the real Decision heading. 0-3 spaces are headings; 4 spaces and a leading tab are code.
|
|
91
|
+
for (const [label, prefix] of [['0 spaces', ''], ['1 space', ' '], ['2 spaces', ' '], ['3 spaces', ' ']]) {
|
|
92
|
+
checkCase(`pr334-r3 indent matrix: Decision heading with ${label} is a heading; Decision edit is normative`, () => {
|
|
93
|
+
assertDecisionEdit(label, indentDecision(TYPO_BASE, prefix), indentDecision(TYPO_CANDIDATE, prefix), 'applicable');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [label, prefix] of [['4 spaces', ' '], ['a leading tab', '\t']]) {
|
|
98
|
+
checkCase(`pr334-r3 indent matrix: Decision heading with ${label} is code, not a heading; the edit is non-normative`, () => {
|
|
99
|
+
assertDecisionEdit(label, indentDecision(TYPO_BASE, prefix), indentDecision(TYPO_CANDIDATE, prefix), 'N/A');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Matrix 2: fences. Existing fixtures keep their behavior; an indented fenced fake heading stays ignored.
|
|
104
|
+
for (const caseName of ['decision-fence-backtick', 'decision-fence-tilde']) {
|
|
105
|
+
checkCase(`pr334-r3 fence control: ${caseName} fixture still treats the real Decision as normative`, () => {
|
|
106
|
+
assertDecisionEdit(caseName, readFixture(caseName, 'base', 'GRPH-010.md'), readFixture(caseName, 'candidate', 'GRPH-010.md'), 'applicable', 'docs/adr/GRPH-010.md');
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
checkCase('pr334-r3 fence: a 3-space indented fenced "## Decision" is ignored; the real Decision edit stays applicable (not unknown)', () => {
|
|
111
|
+
const base = adr('## Context\n\nWe need lint.\n\n ```md\n ## Decision\n ```\n\n## Decision\n\nRun lint first.');
|
|
112
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
113
|
+
assertDecisionEdit('indented fence', base, candidate, 'applicable');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
checkCase('pr334-r3 fence: an indented tilde fence does not hide the following 3-space Decision heading', () => {
|
|
117
|
+
const base = adr('## Context\n\nWe need lint.\n\n ~~~md\n# Example\n ~~~\n\n ## Decision\n\nRun lint first.');
|
|
118
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
119
|
+
assertDecisionEdit('indented tilde fence', base, candidate, 'applicable');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
checkCase('pr334-r3 fence: a tab-indented fence-looking line does not hide a following Decision heading', () => {
|
|
123
|
+
const base = adr('## Context\n\nWe need lint.\n\n\t```md\n ## Decision\n\nRun lint first.');
|
|
124
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
125
|
+
assertDecisionEdit('tab-indented opener', base, candidate, 'applicable');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
checkCase('pr334-r3 fence: a tab-indented closer inside a real fence does not expose a fake Decision heading', () => {
|
|
129
|
+
const base = adr('## Context\n\nWe need lint.\n\n```md\n \t```\n## Decision\n\nFake text.\n```\n\n## Decision\n\nRun lint first.');
|
|
130
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
131
|
+
assertDecisionEdit('tab-indented closer', base, candidate, 'applicable');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
checkCase('pr334-r3 fence: a backtick in a backtick-fence info string invalidates the opener, so Decision is visible', () => {
|
|
135
|
+
const base = adr(['## Context', '', '```js `', '', '## Decision', '', 'Run lint first.'].join('\n'));
|
|
136
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
137
|
+
assertDecisionEdit('malformed backtick info', base, candidate, 'applicable');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
checkCase('pr334-r3 fence: a tilde-fence info string may contain a backtick and still hides its fake Decision', () => {
|
|
141
|
+
const base = adr(['## Context', '', '~~~js `', '## Decision', 'Fake text.', '~~~', '', '## Decision', '', 'Run lint first.'].join('\n'));
|
|
142
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
143
|
+
assertDecisionEdit('tilde info backtick', base, candidate, 'applicable');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
checkCase('pr334-r3 setext: a suspected standalone Setext Decision yields unknown, never N/A', () => {
|
|
147
|
+
const base = adr('## Context\n\nWe need lint.\n\nDecision\n--------\n\nRun lint first.');
|
|
148
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
149
|
+
assertDecisionEdit('Setext Decision', base, candidate, 'unknown');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
checkCase('pr334-r3 setext: a multiline Setext paragraph ending in "Decision" stays unknown rather than a false Decision section', () => {
|
|
153
|
+
const base = adr('## Context\n\nWe need lint.\n\nContext prose\nDecision\n--------\n\nRun lint first.');
|
|
154
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
155
|
+
assertDecisionEdit('multiline Setext title', base, candidate, 'unknown');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
checkCase('pr334-r3 setext: a Setext-looking Decision inside a fence is ignored', () => {
|
|
159
|
+
const base = adr('## Context\n\nWe need lint.\n\n```md\nDecision\n--------\nFake text.\n```\n\n## Decision\n\nRun lint first.');
|
|
160
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
161
|
+
assertDecisionEdit('Setext in fence', base, candidate, 'applicable');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
checkCase('pr334-r3 setext: an unsupported Setext boundary inside Decision yields unknown instead of guessing its paragraph scope', () => {
|
|
165
|
+
const base = adr('## Decision\n\nRun lint first.\n\nNext\n---\n\nLater.');
|
|
166
|
+
const candidate = base.replace('Next\n---', 'Next steps\n---');
|
|
167
|
+
assertDecisionEdit('unsupported Setext boundary', base, candidate, 'unknown');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
checkCase('pr334-r3 thematic break: a rule after the ATX Decision remains inside the section', () => {
|
|
171
|
+
const base = adr('## Decision\n\n---\n\nRun lint first.');
|
|
172
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
173
|
+
assertDecisionEdit('thematic break after ATX', base, candidate, 'applicable');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
checkCase('pr334-r3 thematic break: a rule after a fenced block does not turn its closing fence into a Setext heading', () => {
|
|
177
|
+
const base = adr('## Decision\n\n```js\nexample();\n```\n---\nUse A.');
|
|
178
|
+
const candidate = base.replace('Use A.', 'Use B.');
|
|
179
|
+
assertDecisionEdit('thematic break after fence', base, candidate, 'applicable');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
checkCase('pr334-r3 setext: four-space-indented Setext-looking text is code, not a Decision heading', () => {
|
|
183
|
+
const base = adr('## Context\n\nWe need lint.\n\n Decision\n --------\n\nRun lint first.');
|
|
184
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
185
|
+
assertDecisionEdit('Setext in code', base, candidate, 'N/A');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Matrix 3: section termination and inclusion.
|
|
189
|
+
checkCase('pr334-r3 termination: a 3-space subsection (### Consequences) is included in the Decision section', () => {
|
|
190
|
+
const base = adr(' ## Decision\n\nRun lint first.\n\n ### Consequences\n\nLint is cheap.');
|
|
191
|
+
const candidate = base.replace('Lint is cheap.', 'Lnt is cheap.');
|
|
192
|
+
assertDecisionEdit('subsection', base, candidate, 'applicable');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
checkCase('pr334-r3 termination: a 3-space same-level heading (## Next) ends the Decision section; an edit there is non-normative', () => {
|
|
196
|
+
const base = adr(' ## Decision\n\nRun lint first.\n\n ## Next\n\nLint later.');
|
|
197
|
+
const candidate = base.replace('Lint later.', 'Lnt later.');
|
|
198
|
+
assertDecisionEdit('same-level terminator', base, candidate, 'N/A');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
checkCase('pr334-r3 termination: a 1-space higher-level heading (# Appendix) ends the Decision section; an edit there is non-normative', () => {
|
|
202
|
+
const base = adr(' ## Decision\n\nRun lint first.\n\n # Appendix\n\nLint notes.');
|
|
203
|
+
const candidate = base.replace('Lint notes.', 'Lnt notes.');
|
|
204
|
+
assertDecisionEdit('higher-level terminator', base, candidate, 'N/A');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
checkCase('pr334-r3 termination: an empty "##" heading line ends the Decision section; an edit after it is non-normative', () => {
|
|
208
|
+
const base = adr(' ## Decision\n\nRun lint first.\n\n##\n\nLint notes.');
|
|
209
|
+
const candidate = base.replace('Lint notes.', 'Lnt notes.');
|
|
210
|
+
assertDecisionEdit('empty terminator', base, candidate, 'N/A');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
checkCase('pr334-r3 termination: a 4-space indented "## Example" is code and does not end the Decision section', () => {
|
|
214
|
+
const base = adr(' ## Decision\n\nRun lint first.\n\n ## Example\n\nLint sample.');
|
|
215
|
+
const candidate = base.replace('Lint sample.', 'Lnt sample.');
|
|
216
|
+
assertDecisionEdit('code-indented non-terminator', base, candidate, 'applicable');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Matrix 4: duplicate Decision headings are ambiguous, whatever the indentation.
|
|
220
|
+
checkCase('pr334-r3 duplicate: a column-0 Decision plus a 2-space Decision is unknown, never N/A or applicable', () => {
|
|
221
|
+
const base = adr('## Decision\n\nRun lint first.\n\n ## Decision\n\nOther.');
|
|
222
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
223
|
+
assertDecisionEdit('duplicate', base, candidate, 'unknown');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Matrix 5: closing sequences and separators, per the ATX rule. Only space/tab separators and preceding-space closings count.
|
|
227
|
+
checkCase('pr334-r3 closing: a 2-space "## Decision ##" closing sequence is the Decision heading', () => {
|
|
228
|
+
const base = adr(' ## Decision ##\n\nRun lint first.');
|
|
229
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
230
|
+
assertDecisionEdit('closing', base, candidate, 'applicable');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
checkCase('pr334-r3 closing: "## Decision#" has no preceding space, so the title is "Decision#" and Decision is absent', () => {
|
|
234
|
+
const base = adr('## Decision#\n\nRun lint first.');
|
|
235
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
236
|
+
assertDecisionEdit('glued hash', base, candidate, 'N/A');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
checkCase('pr334-r3 separator: a tab after the hashes ("##\\tDecision") is a valid separator', () => {
|
|
240
|
+
const base = adr('##\tDecision\n\nRun lint first.');
|
|
241
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
242
|
+
assertDecisionEdit('tab separator', base, candidate, 'applicable');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
checkCase('pr334-r3 separator: a no-break space after the hashes is not a CommonMark separator, so it is not a heading', () => {
|
|
246
|
+
const base = adr('## Decision\n\nRun lint first.');
|
|
247
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
248
|
+
assertDecisionEdit('nbsp separator', base, candidate, 'N/A');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
checkCase('pr334-r3 separator: "##Decision" with no separator is not a heading (control)', () => {
|
|
252
|
+
const base = adr('##Decision\n\nRun lint first.');
|
|
253
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
254
|
+
assertDecisionEdit('no separator', base, candidate, 'N/A');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
checkCase('pr334-r3 level: seven hashes is not an ATX heading (control)', () => {
|
|
258
|
+
const base = adr('####### Decision\n\nRun lint first.');
|
|
259
|
+
const candidate = base.replace('Run lint first.', 'Run lnt first.');
|
|
260
|
+
assertDecisionEdit('seven hashes', base, candidate, 'N/A');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Positive control: the column-0 typo-decision fixture is unchanged by this fix.
|
|
264
|
+
checkCase('pr334-r3 control: the existing column-0 typo-decision fixture stays normative', () => {
|
|
265
|
+
assertDecisionEdit('typo-decision', TYPO_BASE, TYPO_CANDIDATE, 'applicable');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
helper.finish();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Document Lifecycle Contract (#325)
|
|
2
|
+
|
|
3
|
+
Contract for the documentation family: project docs configuration (D5), document roles and revision metadata (D6.1–D6.3), domain-scoped IDs (D6.8) and the generated index. Owner decisions live in [#325](https://github.com/dyphn1/Harness-everything/issues/325); this file is the implementation contract that tests cite.
|
|
4
|
+
|
|
5
|
+
Documentation is a **consumer** of the #320 workflow/obligation abstractions. Nothing here is a second router, a standing maintenance loop or a cognitive lock: every check is a pure function that reports diagnostics.
|
|
6
|
+
|
|
7
|
+
## Scope of this slice
|
|
8
|
+
|
|
9
|
+
| Phase | Status here |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 0 — contract + RED fixtures | This file; fixtures under `ci/fixtures/doc-lifecycle/` |
|
|
12
|
+
| 1a — `project-docs.json` loader | `multi-agent-workspace/scripts/project-docs-resolver.js` |
|
|
13
|
+
| 2 — roles, IDs, aliases, legacy, supersession, index, D6.9 PRD revisions | `harness-everything/scripts/doc-lifecycle.js` |
|
|
14
|
+
| 3a — `doc-change` / `requirement-sync` pure derivation and evidence producer | `harness-everything/scripts/doc-obligations.js` (facts and obligations), `harness-everything/scripts/doc-evidence.js` (`checkDocObligations`). Pilot-local: not wired into workflow disposition yet. H3 and H4 have deterministic tests; H1 and H2 are deferred to 3c. Known limits: the attested-envelope fields are caller-asserted (`configDigest` is checked for presence only, not against the parsed config); `candidateDigest` covers only `docs/prd/`, so custom PRD roots and task-scope binding are not yet verified; deleted and renamed paths fail closed (#335) |
|
|
15
|
+
| 1b, 3b–3c (provider suggestions, reconciliation, H1–H2 evaluation) | Not started: blocked by #320 family metadata, D1 obligation schema and D3 evidence states. Known gap: deleted and old rename paths cannot get candidate-bound evidence, so their MUSTs fail closed |
|
|
16
|
+
| 4 — derived-view generators, `describes:` drift | Not started |
|
|
17
|
+
|
|
18
|
+
## Configuration (D5)
|
|
19
|
+
|
|
20
|
+
| Source | Rule |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `.agents/harness-everything/project-docs.json` | Committed, host-neutral. When present it **wholly overrides** host-manifest `projectDocs` (no field merge); warn `project-overrides-host` when a host value differs |
|
|
23
|
+
| Invalid project file | Status `invalid` with diagnostics; no fallback to host manifests; config-dependent MUSTs report unresolved |
|
|
24
|
+
| No project file, active host known and declares `projectDocs` | Status `host`; record that manifest as the source |
|
|
25
|
+
| No project file, active host known but its manifest has no `projectDocs` | **Owner decision 2026-10-17:** `none`, recording the active host; never inherit another host's config. Warn and suggest `project-docs.json` |
|
|
26
|
+
| No project file, host unknown | All declaring hosts canonically equal → `host` (all sources recorded); they differ → `ambiguous`; none → `none` |
|
|
27
|
+
| AGENTS.md | Never a configuration source |
|
|
28
|
+
|
|
29
|
+
- Canonical comparison: object keys sorted recursively, array order preserved, whitespace irrelevant.
|
|
30
|
+
- The project file may also carry the existing zone fields (`decisionLocation`, `domainLocation`, `architectureLocation` and their aliases). Zone resolution uses the project file when present, so the override is total.
|
|
31
|
+
- Host manifests that fail to parse are skipped with an `invalid-host-config` diagnostic naming the source; they never count as agreeing or disagreeing.
|
|
32
|
+
- **Generator security (MUST):** reading or validating config never executes `derived[].generator`. Execution belongs to an authorized verification step only.
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"governed": [
|
|
37
|
+
{ "path": "docs/prd/**", "role": "prd" },
|
|
38
|
+
{ "path": "docs/adr/**", "role": "adr" }
|
|
39
|
+
],
|
|
40
|
+
"domains": ["GRPH", "IFCE"],
|
|
41
|
+
"legacy": ["docs/adr/legacy/**"],
|
|
42
|
+
"derived": [{ "path": "docs/roadmap/index.md", "generator": "npm run docs:index" }],
|
|
43
|
+
"tickets": "tracker"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Field | Validation |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `governed` | Array of `{ path: glob, role }`; `role` from the fixed vocabulary below |
|
|
50
|
+
| `domains` | Array of unique `^[A-Z][A-Z0-9]{1,9}$`. Reserved for ID prefixes: a legacy string value in a host manifest is read as `domainLocation` with a `deprecated-domains-string` warning; in `project-docs.json` it is a schema error |
|
|
51
|
+
| `legacy` | Array of globs |
|
|
52
|
+
| `derived` | Array of `{ path, generator }`, both non-empty strings |
|
|
53
|
+
| `tickets` | `"tracker"` or a repo-relative path |
|
|
54
|
+
| Unknown keys | Warning, not error |
|
|
55
|
+
|
|
56
|
+
Globs are repo-relative POSIX paths supporting `*` (one segment) and `**` (any depth).
|
|
57
|
+
|
|
58
|
+
## Roles and status (D6.1, D6.3)
|
|
59
|
+
|
|
60
|
+
| Role | Status vocabulary | `id` |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `prd` | `draft` / `approved` / `effective` / `rejected` / `withdrawn` (D6.9); pre-migration `active` / `superseded` warn unchanged and error when added or changed | Required: `PRD-<DOMAIN>-NNN` plus `revision` (D6.9) |
|
|
63
|
+
| `adr` | `proposed` / `accepted` / `rejected` / `superseded` | Required: `<DOMAIN>-NNN` |
|
|
64
|
+
| `ticket` | `open` / `blocked` / `done` / `withdrawn` | Optional |
|
|
65
|
+
| `reference` | `active` / `deprecated` | Optional |
|
|
66
|
+
| `log`, `report` | `final` / `archived` | Optional |
|
|
67
|
+
|
|
68
|
+
- Role comes from the first matching `governed` entry; a frontmatter `role` must agree with it.
|
|
69
|
+
- **Legacy immutability:** Legacy files remain outside the active ID namespace. With an observed trusted base, a path is frozen only when some base source's `legacy` matches it and no base source positively governs it after that source's own legacy exclusion; parsed frontmatter stays deep-equal while body/editorial/link edits remain allowed. A surviving path is reclassified when it is no longer legacy under the same per-source rule for candidate sources and emits `legacy-frozen-modified`; deletion/move relies on existing escalation for surviving explicit `legacy:ID@path` references. Without a trusted base, legacy immutability is not-observed or unavailable.
|
|
70
|
+
- Frontmatter fields: `id`, `role`, `status`, `supersedes`, `aliases`. `superseded_by` and `amends` are not stored; `superseded_by` is derived in the index.
|
|
71
|
+
- **Ratchet:** schema checks (role/status/ID) apply to governed files named in `changedPaths`. Full mode is explicit (`--all` / `{ all: true }`); the CLI refuses to run without `--changed`, `--changed-path` or `--all`. Cross-file integrity diagnostics are errors when any involved file is changed (deleted and renamed paths count as changed), warnings otherwise. Warnings that affect resolution still appear in the index's unresolved list. CLI `--changed` entries resolve against `--workspace`, normalize to repo-relative POSIX paths, and fail as usage errors when outside the workspace; `--changed` takes one comma-separated argument and cannot represent filenames containing commas, so repeat `--changed-path <path>` (one occurrence per path, never comma-split, normalizes and resolves like the CSV form, and mutually exclusive with `--all`; may be combined with `--changed`) when a filename contains a comma. Occurrences of either changed selector accumulate: repeated `--changed` or `--changed-path` flags add to the changed set instead of overwriting earlier declarations. The CSV form trims whitespace around its comma-separated items; `--changed-path` preserves commas and outer whitespace while resolving paths and normalizing separators. Both `/` and `\` are separators on every host; literal backslashes in POSIX filenames cannot be selected through these CLI flags — use `--all` or the API `changedPaths` for those files. Relative `--config` paths also resolve from `--workspace`. Without a trusted base, escalation for deleted or renamed targets covers explicit `legacy:ID@path` selectors only; ID-only references escalate when a trusted base (`--base` or a supplied inventory, see D6.9) maps the ID to a changed path.
|
|
72
|
+
- Parsing conventions: role comes from the first matching `governed` entry; frontmatter is the leading `---` block in a YAML subset (scalars, inline `[a, b]` lists, `- a` block lists); references inside fenced code blocks are ignored.
|
|
73
|
+
|
|
74
|
+
## IDs, aliases and references (D6.2, D6.8)
|
|
75
|
+
|
|
76
|
+
- ADR: `<DOMAIN>-NNN` (three or more digits). REQ: `REQ-<DOMAIN>-NNN`, defined only in a PRD as a heading `REQ-<DOMAIN>-NNN` at the start of a Markdown heading line (`### REQ-GRPH-012: …`).
|
|
77
|
+
- **REQ aliases — owner decision 2026-10-17:** declared on the REQ heading itself, `### REQ-GRAPH-012 (aliases: REQ-GRPH-012, REQ-GRPH-099): title`. Each alias resolves directly to that REQ; collision rules match document aliases. Document-level `aliases` never rename REQs.
|
|
78
|
+
- `DOMAIN` must be declared in `domains`. Aliases keep their historical domain, which need not be declared.
|
|
79
|
+
- `aliases: [...]` lists every previous ID; each alias resolves **directly** to the declaring document. Reject duplicate canonical IDs, an alias equal to another active canonical ID or alias, and unresolved references.
|
|
80
|
+
|
|
81
|
+
| Reference | Resolution |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `GRPH-007` | Active canonical IDs and aliases only; never falls through to legacy |
|
|
84
|
+
| `legacy:GRPH-007` | The unique legacy document with that ID. Several matches → `ambiguous-legacy-ref` listing every candidate path; never the first match |
|
|
85
|
+
| `legacy:GRPH-007@docs/adr/legacy/v2/GRPH-007.md` | **Owner decision 2026-10-17:** the repo-relative path is the archive discriminator; resolves only when that file is legacy and carries that ID |
|
|
86
|
+
|
|
87
|
+
Selector grammar: `legacy:` + ID, optionally `@` + path, split at the **first** `@`. The path is a repo-relative POSIX path compared byte-exactly with the collected path (case-sensitive on every OS). Backslashes, leading `/`, drive letters, `.` or `..` segments, and paths outside the workspace are `invalid-ref`; they are never normalized into a match. A renamed archive path leaves old selectors `unresolved-ref` until updated.
|
|
88
|
+
|
|
89
|
+
- A legacy document's ID is its frontmatter `id`, else its file stem when that matches an ID pattern. Duplicate legacy IDs are allowed; only bare references to them are ambiguous.
|
|
90
|
+
- An active alias equal to a legacy ID is allowed (the rename case).
|
|
91
|
+
- Aliases are strings matching an ADR or REQ ID pattern. The `legacy:` prefix is reserved for references and is `invalid-alias` inside `aliases`, as are self-aliases and non-strings.
|
|
92
|
+
|
|
93
|
+
## Supersession (D6.3, D6.4)
|
|
94
|
+
|
|
95
|
+
- `supersedes` is written on the new document and means real normative replacement, never an identity rename.
|
|
96
|
+
- Active target: the target must carry `status: superseded`, else `supersede-status-mismatch`. This is the only required two-file edit.
|
|
97
|
+
- **Legacy target — owner decision 2026-10-17 (external projection):** reference it with an explicit `legacy:` ref that resolves uniquely. The legacy file is never edited; the generated index projects its status as superseded by the active document. This is the explicit legacy exception to both the D6.3 two-file lint and the D6.6 "ADR superseded → old status `superseded`" event: for legacy targets the obligation is met by the projection. The index shows the stored status and the projected effective status side by side, with the source edge.
|
|
98
|
+
- A plain ref that matches only a legacy ID is `unresolved-ref` with a hint to use `legacy:`.
|
|
99
|
+
- Self-supersession and supersession cycles are errors.
|
|
100
|
+
- **Branching and eligibility — owner decision 2026-10-17:** a target may have several successors; the index lists them sorted. Only edges from an `accepted` ADR or an `active` PRD are effective: they require the active target's `superseded` status and drive legacy projection. Edges from other statuses (`draft`, `proposed`, …) are shown as pending, do not require the target's status change, and do not project.
|
|
101
|
+
- Only explicit `supersedes` or a recorded owner decision resolves a conflict.
|
|
102
|
+
- **Recorded conflicts — owner decision 2026-10-17:** semantic PRD/ADR contradictions are not detected from prose. Whoever finds one records `conflicts_with: [<ID>]` on either document; the pair stays in the unresolved list (`recorded-conflict`, both IDs and paths) until an effective `supersedes` between them exists. Owner adjudication is recorded by that `supersedes` edge or by removing the entry in the same change that updates the canonical document. Structural conflicts are detected automatically.
|
|
103
|
+
|
|
104
|
+
## Diagnostics
|
|
105
|
+
|
|
106
|
+
Each diagnostic is `{ code, severity, path, id?, message, candidates? }`, sorted by `path`, `code`, `id`, then `message`.
|
|
107
|
+
|
|
108
|
+
Anchors for cross-file diagnostics: `supersede-*`, `unresolved-ref`, `ambiguous-legacy-ref`, `invalid-ref` and `recorded-conflict` attach to the document that declares the reference or edge. `legacy-frozen-modified` attaches to the trusted-base archive path. `duplicate-id`, `alias-collision` and `duplicate-req` emit one diagnostic on **every** involved document, each listing the others in `candidates`.
|
|
109
|
+
|
|
110
|
+
| Code | Trigger |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `malformed-frontmatter` | Unparseable frontmatter block |
|
|
113
|
+
| `role-mismatch` | Frontmatter `role` differs from path role |
|
|
114
|
+
| `missing-status` / `invalid-status` | Status absent or outside the role vocabulary |
|
|
115
|
+
| `missing-id` / `invalid-id` / `unknown-domain` | ADR ID absent, malformed, or domain undeclared |
|
|
116
|
+
| `duplicate-id` | Two active documents share a canonical ID |
|
|
117
|
+
| `alias-collision` | Alias equals another active canonical ID or alias |
|
|
118
|
+
| `duplicate-req` / `missing-req` | REQ defined twice in active PRDs, or referenced in a governed doc but never defined |
|
|
119
|
+
| `unresolved-ref` / `ambiguous-legacy-ref` / `invalid-ref` | Reference resolves to nothing, to several legacy documents, or is malformed |
|
|
120
|
+
| `legacy-frozen-modified` | With a trusted base, legacy frontmatter changed or a surviving archive was reclassified out of legacy scope (error) |
|
|
121
|
+
| `invalid-alias` | Alias is not a string ID, is self-referential, or uses the reserved `legacy:` prefix |
|
|
122
|
+
| `invalid-host-config` | A host manifest's `projectDocs` failed to parse or validate |
|
|
123
|
+
| `supersede-status-mismatch` / `supersede-cycle` | Effective edge to an active target that is not `superseded`; self or cyclic supersession |
|
|
124
|
+
| `recorded-conflict` | `conflicts_with` pair without an effective `supersedes` between them |
|
|
125
|
+
|
|
126
|
+
## Contract-style PRD revisions (D6.9)
|
|
127
|
+
|
|
128
|
+
**Owner direction 2026-10-17:** an effective PRD is a contract. A change creates a new **complete** revision copied from the current effective revision; the effective snapshot is never edited in place. This replaces the D6.1 "living PRD" model for PRDs only; ADRs, tickets and other roles are unchanged. Where this section conflicts with earlier PRD rules (optional PRD `id`, PRD status vocabulary, PRD `supersedes`), this section wins.
|
|
129
|
+
|
|
130
|
+
### Identity and graph
|
|
131
|
+
|
|
132
|
+
| Concept | Rule |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| Logical PRD | `id: PRD-<DOMAIN>-NNN`, shared by every revision; grouped by canonical identity (aliases preserved) |
|
|
135
|
+
| Revision | `revision: <N>` positive integer, no leading zeros; canonical form `PRD-GRPH-001@r3` |
|
|
136
|
+
| Parent | `previous_revision: <N>`; must name an existing revision of the same logical PRD with a smaller number. Exactly one baseline (no parent) per logical PRD |
|
|
137
|
+
| Layout | Not mandated (recommended `docs/prd/<slug>/r003.md`); identity comes from frontmatter only |
|
|
138
|
+
| Uniqueness | `(logical id, revision)` unique. Repeated logical IDs and inherited aliases across revisions are not `duplicate-id` / `alias-collision` |
|
|
139
|
+
|
|
140
|
+
### Status, approval, activation
|
|
141
|
+
|
|
142
|
+
| Stored status | Editable | Notes |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `draft` | Yes | Never authoritative; drafts may fork |
|
|
145
|
+
| `approved` | Activation fields only | Approval bound to content |
|
|
146
|
+
| `effective` | **No**, from the change that sets it | Frozen artifact |
|
|
147
|
+
| `rejected` / `withdrawn` | Not enforced in this slice | Never selectable as current |
|
|
148
|
+
|
|
149
|
+
- **Approval record (owner decision 2026-10-17):** `approval: { by, at, ref, content_digest }`. `by` is a human or an independent cold verifier, never the drafting agent. `ref` is a URL or an in-repo record path (offline-checkable). `content_digest` covers the revision body plus non-activation frontmatter; a content edit after approval invalidates it (`approval-content-mismatch`).
|
|
150
|
+
- **Activation:** sets `status: effective` and `effective_at`; only these fields may change between `approved` and `effective`. The file path must also stay the same as in the trusted approved baseline. An `approved` revision is frozen except for activation; any other edit, including rebinding `approval`, requires returning it to `draft` and re-approving (`invalid-transition`). Deleting an approved revision or moving it outside the candidate inventory is also an `invalid-transition`. Timestamps are ISO 8601 with timezone; `effective_at` must not precede `approval.at`. Activation requires the parent to be the current effective revision (`stale-draft-base`).
|
|
151
|
+
- **Current effective** is derived after validating the whole graph: a connected chain of `effective` revisions from the baseline; two effective children of one parent → `revision-fork`; no effective revision → explicit pre-activation state. Older effective revisions are derived `historical`; their files are never edited.
|
|
152
|
+
|
|
153
|
+
### Digests and evidence envelope
|
|
154
|
+
|
|
155
|
+
- **Stored bytes:** the bytes Git stores for the file (after `git add` end-of-line normalization). Base content comes from the trusted base tree; candidate content is normalized the same way. Comparisons may use Git blob object IDs; frontmatter digests are `sha256:<hex>` over stored bytes. Pure API callers pass stored bytes directly.
|
|
156
|
+
- **Artifact digest** (`previous_digest`, immutability): sha256 over the complete stored bytes of the revision file.
|
|
157
|
+
- **Approval content digest** (`approval.content_digest`): sha256 over `JSON(frontmatter without status, effective_at, approval; keys sorted; UTF-8) + "\n" + body` (body in stored bytes). Excluding `approval` avoids self-reference; excluding activation fields lets activation keep the digest valid.
|
|
158
|
+
- **Independence:** `drafted_by` (required on `approved` and `effective` revisions; missing → `approval-missing`) names the drafting author; `approval.by` equal to `drafted_by` → `approval-not-independent`. `approval.role` is `human` or `cold-verifier`. The checker verifies the record's presence, binding and independence, not that a review actually happened.
|
|
159
|
+
- **Evidence envelope:** `analyzeDocs(docs, config, opts)` returns `{ diagnostics, evidence }` with `evidence.immutability` = `observed` | `not-observed` | `unavailable`. `lintDocs` stays the diagnostics-only projection. CLI JSON prints the envelope; exit 1 only for error diagnostics, and `not-observed` is always printed, never folded into success.
|
|
160
|
+
- **Offline base inventory:** `analyzeDocs(docs, config, { baseInventory: [{ id, revision, path, status, bytes }] })` supplies stored baseline bytes for offline checks. For Git-equivalent legacy freezing, supply either per-entry boolean `legacy` classifications computed under the per-source rule or `baseConfigSources`; when both are supplied, `baseConfigSources` takes precedence and applies the source rule. The supplied historical sources also govern migration checks, REQ identity lookup, and changed-scope calculations. Without either, legacy classification is not established for unflagged rows, which are not frozen, and `evidence.legacyImmutability` reports `not-observed` while byte-based `evidence.immutability` remains `observed`. Candidate classification uses `options.configSources` when supplied, otherwise `config`; equivalent trusted bytes and authority inputs must yield the same diagnostics as `--base`.
|
|
161
|
+
- Tests isolate Git (no global/system config, hooks, templates or signing; explicit `core.autocrlf` and attributes) and compute expected digests independently of the system under test.
|
|
162
|
+
|
|
163
|
+
### Immutability evidence
|
|
164
|
+
|
|
165
|
+
- **Byte basis (owner decision 2026-10-17):** digests use the repository-stored content (Git blob bytes, or an exported snapshot of them), so checkout line-ending conversion cannot change identity.
|
|
166
|
+
- `previous_digest` in each revision is an **internal consistency** check only; editing a parent and its child's digest together stays self-consistent.
|
|
167
|
+
- **Immutability** needs a trusted baseline: CLI `--base <git-rev>` or a supplied snapshot inventory. Base and candidate inventories are compared by `(logical id, revision)` and by path: a base-effective revision must remain present with identical bytes regardless of status edits, moves, deletions or config exclusions (`immutable-revision-modified`, `immutable-revision-deleted`). Without a trusted baseline the result is `not-observed`, never pass. The immutability scope is the **union** of governed, legacy and PRD globs from the current config and from every config source in the base tree (`project-docs.json`, each host manifest's `projectDocs`, and the base copy of an explicit `--config` path); narrowing the current config never removes a base-effective revision from checking. Stored-byte normalization follows Git's text/binary decision (including NUL detection for `text=auto` and for `core.autocrlf` without attributes); content Git stores unchanged is compared unchanged. Git is read with argument arrays, never shell interpolation; missing history is never fetched.
|
|
168
|
+
|
|
169
|
+
### REQ continuity
|
|
170
|
+
|
|
171
|
+
- Every REQ of the parent appears in the child unless accounted for by `removed_reqs: [...]` or a `replaces` edge; otherwise `req-dropped`. Each REQ block needs non-empty acceptance content (`incomplete-snapshot`). ID continuity is structural evidence only; semantic completeness still needs review.
|
|
172
|
+
- Unchanged and editorial REQs keep their IDs. A REQ whose text changed is reported as changed and needs a review classification (normative vs editorial); IDs are never recycled for unrelated meaning.
|
|
173
|
+
- Heading clauses: `### REQ-GRPH-020 (replaces: REQ-GRPH-012, REQ-GRPH-013; aliases: REQ-GRAPH-020): title`. Clauses are `;`-separated, comma lists, each at most once; unknown or duplicate clauses are errors. `replaces` allows splits and merges; a REQ ID may not be both alias and replaced target; replacement cycles are errors.
|
|
174
|
+
- `duplicate-req` applies within each revision and across the current effective revisions of different logical PRDs.
|
|
175
|
+
- Plain `REQ-…` resolves only in current effective revisions; a removed or replaced REQ is `unresolved-ref` with a historical/replacement hint, never auto-followed.
|
|
176
|
+
|
|
177
|
+
### References
|
|
178
|
+
|
|
179
|
+
| Ref | Resolves to |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `PRD-GRPH-001` | Current effective revision |
|
|
182
|
+
| `PRD-GRPH-001@r2` | Exactly revision 2, returned with status, digest and provenance |
|
|
183
|
+
| `PRD-GRPH-001@r2#REQ-GRPH-012` | That REQ inside revision 2 (aliases resolved as of r2) |
|
|
184
|
+
|
|
185
|
+
- Parse the namespace first: `legacy:` refs keep their exact path grammar; `@r<N>` and `#REQ` apply only to PRD logical IDs. Whole-token anchored parsing; a valid prefix of an invalid ref never resolves.
|
|
186
|
+
- Pins to `draft`/`approved` revisions are provisional and never satisfy governed-implementation evidence; audit evidence pins an effective revision plus its digest.
|
|
187
|
+
|
|
188
|
+
### Cross-PRD replacement and migration
|
|
189
|
+
|
|
190
|
+
- **Retirement eligibility:** only an `effective` source revision retires a target or resolves a recorded conflict; edges from `draft`/`approved`/`rejected` revisions are pending (same rule as ADR effective edges).
|
|
191
|
+
- **Retirement is a historical event (owner decision 2026-10-17):** once any revision that was effective declares the edge, the target stays retired even if later revisions of the source omit it; omission never revives a target. The index records the revision that issued the retirement.
|
|
192
|
+
- **Retirement (owner decision 2026-10-17):** `supersedes: [PRD-GRPH-001@r4]` on another logical PRD retires that contract by external projection, like legacy targets: no file is edited, splits and merges are allowed, the exact source/target revisions are recorded, and the retired PRD stays addressable.
|
|
193
|
+
- **Baseline adoption (owner decision 2026-10-17):** an existing non-legacy living PRD gets one reviewed metadata-only edit adding `revision: 1`, approval and a `migrated_from_digest` of its prior content, recorded in a migration record. The baseline frontmatter names the record with `migration_record: <repo-relative path>`; the record is JSON `{ record: "prd-baseline-migration", id, revision, source, migrated_from_digest, approved_by }` and must match the frontmatter and the base content of `source` (`migration-record-missing` / `migration-record-mismatch`). `baseline` status never exempts later edits. Legacy PRDs stay frozen outside the graph.
|
|
194
|
+
|
|
195
|
+
### Diagnostics (D6.9)
|
|
196
|
+
|
|
197
|
+
`invalid-prd-id`, `invalid-revision`, `duplicate-revision`, `missing-parent`, `multiple-baselines`, `revision-cycle`, `revision-fork`, `stale-draft-base`, `invalid-transition`, `approval-missing`, `approval-content-mismatch`, `revision-digest-mismatch`, `immutable-revision-modified`, `immutable-revision-deleted`, `req-dropped`, `incomplete-snapshot`, `invalid-req-clause`, `replacement-alias-overlap`, `replacement-cycle`. `not-observed` is an evidence disposition, not a diagnostic and not a pass.
|
|
198
|
+
|
|
199
|
+
## D6.5 — `doc-change` applicability by diff facts (Decided; revised 2026-10-16)
|
|
200
|
+
|
|
201
|
+
Not a single class enum. The diff yields **facts**; required checks are the **union** of what each fact triggers. If a fact cannot be determined, assume it is present (`unknown` never downgrades to trivial).
|
|
202
|
+
|
|
203
|
+
| Fact (from diff) | Triggers |
|
|
204
|
+
| --- | --- |
|
|
205
|
+
| Any governed file touched | Lint (role/status/ID/alias); in-repo link check |
|
|
206
|
+
| Text/format only (no fact below) | Nothing more |
|
|
207
|
+
| New governed file | Update-over-create check; outline confirmation (MUST for PRD/ADR, SHOULD otherwise) |
|
|
208
|
+
| Declared derived source affected | Regenerate + regenerate-equals |
|
|
209
|
+
| **Normative field**: frontmatter `status`/`supersedes`, PRD REQ block/acceptance, ADR *Decision* section | Quality review (MUST); D6.4 conflict check |
|
|
210
|
+
| Governed reference/guide content | Prose > 50% reminder (SHOULD) |
|
|
211
|
+
| Code or REQ-citing test touched | `requirement-sync` evaluation (D6.6) |
|
|
212
|
+
|
|
213
|
+
Decision extraction recognizes ATX boundaries with 0–3 leading spaces and ignores fenced or indented code. A suspected Setext Decision heading, or an unsupported Setext boundary within an ATX Decision section, yields `unknown`; paragraph/container context is not inferred.
|
|
214
|
+
|
|
215
|
+
- Example: a typo in an ADR's *Context* section triggers only lint + links; the same typo in its *Decision* section is normative.
|
|
216
|
+
- Claimed scope vs diff facts mismatch → claim/evidence contradiction (#320 D3), only where the diff supports it; visible, never a lock.
|
|
217
|
+
- Commit still requires existing workflow authorization; no automatic commit.
|
|
218
|
+
|
|
219
|
+
## D6.6 — Event-driven maintenance (Decided)
|
|
220
|
+
|
|
221
|
+
**`requirement-sync` detection** (builds on D6.2: tests cite REQ IDs):
|
|
222
|
+
|
|
223
|
+
| Observation | Disposition |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| A test citing `REQ-X` is added or its assertions change | **Candidate signal**, not proof → **MUST** update/supersede REQ-X in the PRD in the same change, or record a structured `no-req-change` (reason + cited evidence, e.g. unchanged acceptance, passing test result) |
|
|
226
|
+
| An existing test's cited REQ identity set is added, removed, or retargeted, or the changed call cannot be associated with its prior identity | Applicability/scope `unknown`, never N/A; retain candidate REQ identities and the test path. If removal leaves no candidate identity, retain the prior REQ ID as a removed identity. A citation-only change is not by itself a `new-test` or `assertion-change` trigger |
|
|
227
|
+
| A legacy file-level REQ tag is added, removed, or retargeted on a modified test file | Applicability/scope `unknown`, never N/A; retain the changed identity and test path, even when stripped comments leave assertion tokens unchanged |
|
|
228
|
+
| A test title changes or reorders REQ IDs while preserving the same cited identity set, or an ordinary comment changes | No trigger → `requirement-sync` N/A |
|
|
229
|
+
| Ordinary comment-only or whitespace-only edit that preserves the cited REQ identity set | Not a trigger → `requirement-sync` N/A |
|
|
230
|
+
| REQ-citing test change whose assertion change cannot be determined | `unknown`; never a fail |
|
|
231
|
+
| Triggered MUST, **attested envelope** (all of the following), cited REQ resolved, **not** revised, **no** valid `no-req-change` waiver | **Observed fail** (`req-sync-no-revision-or-waiver`). Not a Stop hook, lock, or completion policy |
|
|
232
|
+
| Triggered MUST, `complete` inventory but envelope incomplete, mismatched, or unattested | `none` with `req-sync-envelope-unattested`. Never a fail |
|
|
233
|
+
| Attested envelope = `complete` and `waiverInventoryComplete` both `true`; `baseDigest` equal to the baseline digest; `candidateDigest` equal to the digest of the candidate PRD set; `configDigest` present; non-empty cited REQ list | Caller-attested. The derivation copies it and never infers it. Not independent proof that the caller collected the inventory completely |
|
|
234
|
+
| Unsupported or stale `no-req-change` waiver | Unverified (`unsupported-waiver`). It never satisfies the MUST. It does not by itself establish the failure, and an independently attested unmet obligation may still fail |
|
|
235
|
+
| `no-req-change` without adequate evidence | `not-observed` / unverified; never an automatic pass. An invalid waiver is diagnosed and does not satisfy the MUST |
|
|
236
|
+
| Cited REQ missing, ambiguous, or unresolved | `unresolved` with `req-unresolved`; never a pass, never an observed fail. "Ambiguous" means declared in two distinct current logical PRDs. Retained revisions of one PRD resolve to the current effective revision |
|
|
237
|
+
| Incomplete execution record (no command, no provenance, non-integer `exitCode`) | `execution-record-incomplete`; never a pass |
|
|
238
|
+
| Code changes, no REQ-citing test touched | `not-observed` (#320 D3 evidence state), not a failure; diff alone never proves behavior unchanged |
|
|
239
|
+
| Agent claims REQ-X affected, no supporting evidence | Claim/evidence contradiction; reminder, no lock |
|
|
240
|
+
|
|
241
|
+
| Event | Obligation | Strength | Enforced by |
|
|
242
|
+
| --- | --- | --- | --- |
|
|
243
|
+
| REQ touched (above) | `requirement-sync` | MUST | New rule |
|
|
244
|
+
| `describes:` fingerprint drift | Reminder | SHOULD | D5 #4 |
|
|
245
|
+
| Ticket closed | In-repo tickets: `status: done`; regenerate index. `tickets: "tracker"`: no in-repo ticket status is generated or inferred offline | MUST | D5 #1 + D6.3 lint |
|
|
246
|
+
| ADR superseded | Old ADR `status: superseded` | MUST | D6.3 lint |
|
|
247
|
+
| New defect | Goes to tracker or the declared `tickets` path | MUST | Lint reminder when a ticket is added under a `done` phase |
|
|
248
|
+
| Report finished | `final`; `archived` once its work closes | SHOULD | Reminder |
|
|
249
|
+
|
|
250
|
+
- All events are checked at the existing workflow's `verification` step; no standing maintenance loop.
|
|
251
|
+
- Checks are pure and generators deterministic, so reruns are idempotent.
|
|
252
|
+
- `requirement-sync` is the only genuinely new rule; the rest reuse D5 and D6.3 checks.
|
|
253
|
+
|
|
254
|
+
## Packaging
|
|
255
|
+
|
|
256
|
+
Source checkout, Codex plugin and Claude plugin copy whole skill trees. Callers locate `doc-lifecycle.js` and the resolver relative to their own skill directory, never the source-tree root. A to-spec-only install must use the same resolver implementation (copied byte-identically and checked), and a broken shared module is an error, never a silent fallback to older host scanning.
|
|
257
|
+
|
|
258
|
+
## Generated index
|
|
259
|
+
|
|
260
|
+
`buildIndex` returns Markdown with no timestamps or environment data, so two runs over the same tree are byte-identical. Sections: active documents (sorted by ID then path, with derived `superseded by`), legacy documents (sorted by ID then path, with projected status), and the unresolved list (resolution-affecting error diagnostics).
|
|
261
|
+
|
|
262
|
+
The shared `AUTHORITY_BLOCKER_DIAGNOSTICS` taxonomy covers `invalid-prd-id`, `invalid-revision`, `invalid-status`, `invalid-alias`, `duplicate-revision`, `missing-parent`, `multiple-baselines`, `revision-cycle`, `revision-fork`, `revision-digest-mismatch`, `req-dropped`, `incomplete-snapshot`, `invalid-req-clause`, `duplicate-req`, `replacement-alias-overlap`, `replacement-cycle`, `approval-missing`, `approval-not-independent`, `approval-content-mismatch`, `invalid-transition`, `stale-draft-base`, `migration-record-missing`, `migration-record-mismatch`, `immutable-revision-modified`, `immutable-revision-deleted`, `legacy-frozen-modified`, `duplicate-id`, `alias-collision`, `unknown-domain`, `invalid-id` and `role-mismatch`. Retirement eligibility uses the validated effective prefix and blocks on these authority findings regardless of ratchet severity; this prevents a known identity, alias or REQ collision from authorizing a new retirement. Namespace and REQ collisions are computed before the final prefix and supersession/conflict projection. Index current-state display uses the same blocker taxonomy with error severity: an error on the graph-selected current revision or a required effective ancestor produces `state: invalid; current: none`, while warnings stay visible in Unresolved without revoking `valid/current`. A pending descendant cannot discard an earlier valid effective prefix or its historical retirement event. The unresolved list also includes resolution diagnostics `malformed-frontmatter`, `missing-id`, `missing-req`, `unresolved-ref`, `ambiguous-legacy-ref`, `invalid-ref`, `missing-status`, `supersede-status-mismatch`, `supersede-cycle`, `recorded-conflict` and `prd-migration-required`; these remain visible at their reported severity but do not themselves block retirement eligibility or current-state display. When `buildIndex` receives no `diagnostics`, it runs `analyzeDocs` with the supplied options (including trusted-base inputs) and renders that result; callers that provide precomputed diagnostics must provide diagnostics from the same analysis context, with its graph and validated-prefix set, for equivalent retirement and current-state output.
|