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,759 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
6
|
+
const helper = require('./test-helper');
|
|
7
|
+
const resolverPath = path.join(__dirname, '..', 'multi-agent-workspace', 'scripts', 'project-docs-resolver.js');
|
|
8
|
+
const toSpecResolverPath = path.join(__dirname, '..', 'to-spec', 'scripts', 'project-docs-resolver.js');
|
|
9
|
+
const harnessResolverPath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'project-docs-resolver.js');
|
|
10
|
+
const toSpecCheckPath = path.join(__dirname, '..', 'to-spec', 'scripts', 'check-project-docs.js');
|
|
11
|
+
const resolver = require(resolverPath);
|
|
12
|
+
const toSpecResolver = require(toSpecResolverPath);
|
|
13
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2aw-project-docs-'));
|
|
14
|
+
const PROJECT_CONFIG = ['.agents', 'harness-everything', 'project-docs.json'];
|
|
15
|
+
const HOSTS = Object.freeze({
|
|
16
|
+
claude: '.claude',
|
|
17
|
+
cursor: '.cursor',
|
|
18
|
+
github: '.github',
|
|
19
|
+
codex: '.codex',
|
|
20
|
+
continue: '.continue'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
process.on('exit', () => fs.rmSync(tempRoot, { recursive: true, force: true }));
|
|
24
|
+
|
|
25
|
+
console.log('\n[2aw] project-docs.json configuration loader...');
|
|
26
|
+
|
|
27
|
+
function workspace(name) {
|
|
28
|
+
const root = path.join(tempRoot, name);
|
|
29
|
+
fs.mkdirSync(root, { recursive: true });
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configPath(root) {
|
|
34
|
+
return path.join(root, ...PROJECT_CONFIG);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeProjectConfig(root, value, raw) {
|
|
38
|
+
const file = configPath(root);
|
|
39
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
+
fs.writeFileSync(file, raw === undefined ? JSON.stringify(value, null, 2) : raw, 'utf8');
|
|
41
|
+
return file;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeHostConfig(root, host, value, raw) {
|
|
45
|
+
const home = HOSTS[host];
|
|
46
|
+
if (!home) throw new Error(`unknown fixture host: ${host}`);
|
|
47
|
+
const file = path.join(root, home, 'harness-everything', 'manifest.json');
|
|
48
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
49
|
+
fs.writeFileSync(file, raw === undefined ? JSON.stringify({ projectDocs: value }, null, 2) : raw, 'utf8');
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function relative(root, file) {
|
|
54
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function warningCodes(result) {
|
|
58
|
+
return (result.warnings || []).map(item => typeof item === 'string' ? item : item.code);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function diagnosticCodes(result) {
|
|
62
|
+
return (result.diagnostics || []).map(item => typeof item === 'string' ? item : item.code);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkCase(name, run) {
|
|
66
|
+
try {
|
|
67
|
+
run();
|
|
68
|
+
helper.check(name, true);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
helper.check(name, false, error && error.stack ? error.stack : String(error));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resolveDocsConfig = resolver.resolveDocsConfig;
|
|
75
|
+
const validConfig = {
|
|
76
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
77
|
+
domains: ['GRPH', 'IFCE'],
|
|
78
|
+
legacy: ['docs/adr/legacy/**'],
|
|
79
|
+
derived: [{ path: 'docs/roadmap/index.md', generator: 'npm run docs:index' }],
|
|
80
|
+
tickets: 'tracker'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
checkCase('2aw. project file wholly overrides a differing host config and warns', () => {
|
|
84
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
85
|
+
const root = workspace('project-overrides-host');
|
|
86
|
+
const host = {
|
|
87
|
+
governed: [{ path: 'docs/old/**', role: 'reference' }],
|
|
88
|
+
domains: ['GRPH'],
|
|
89
|
+
legacy: ['old/**'],
|
|
90
|
+
tickets: 'tasks/tickets'
|
|
91
|
+
};
|
|
92
|
+
const project = {
|
|
93
|
+
governed: [{ path: 'docs/new/**', role: 'prd' }],
|
|
94
|
+
domains: ['IFCE'],
|
|
95
|
+
tickets: 'tracker'
|
|
96
|
+
};
|
|
97
|
+
const projectFile = writeProjectConfig(root, project);
|
|
98
|
+
writeHostConfig(root, 'claude', host);
|
|
99
|
+
const result = resolveDocsConfig(root, { activeHost: 'claude' });
|
|
100
|
+
assert.strictEqual(result.status, 'project');
|
|
101
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
102
|
+
assert.deepStrictEqual(result.config, project);
|
|
103
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(result.config, 'legacy'), 'host-only field must not merge');
|
|
104
|
+
assert.ok(warningCodes(result).includes('project-overrides-host'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
checkCase('2aw. identical project and host configs do not warn despite key order and whitespace', () => {
|
|
108
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
109
|
+
const root = workspace('project-matches-host');
|
|
110
|
+
const host = { governed: validConfig.governed, domains: validConfig.domains, legacy: validConfig.legacy, derived: validConfig.derived, tickets: validConfig.tickets };
|
|
111
|
+
const project = { tickets: 'tracker', derived: validConfig.derived, legacy: validConfig.legacy, domains: validConfig.domains, governed: validConfig.governed };
|
|
112
|
+
const projectFile = writeProjectConfig(root, null, ` {\n "tickets" : "tracker",\n "derived" : ${JSON.stringify(project.derived)},\n "legacy" : ${JSON.stringify(project.legacy)},\n "domains" : ${JSON.stringify(project.domains)},\n "governed" : ${JSON.stringify(project.governed)}\n}\n`);
|
|
113
|
+
writeHostConfig(root, 'codex', host);
|
|
114
|
+
const result = resolveDocsConfig(root, { activeHost: 'codex' });
|
|
115
|
+
assert.strictEqual(result.status, 'project');
|
|
116
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
117
|
+
assert.ok(!warningCodes(result).includes('project-overrides-host'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
checkCase('2aw. invalid JSON project config is invalid and never falls back to host', () => {
|
|
121
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
122
|
+
const root = workspace('invalid-json');
|
|
123
|
+
const projectFile = writeProjectConfig(root, null, '{ broken json');
|
|
124
|
+
writeHostConfig(root, 'claude', validConfig);
|
|
125
|
+
const result = resolveDocsConfig(root, { activeHost: 'claude' });
|
|
126
|
+
assert.strictEqual(result.status, 'invalid');
|
|
127
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
128
|
+
assert.strictEqual(result.config, null);
|
|
129
|
+
assert.ok(result.diagnostics.length > 0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
checkCase('2aw. schema-invalid project config is invalid and never falls back to host', () => {
|
|
133
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
134
|
+
const root = workspace('invalid-schema');
|
|
135
|
+
const projectFile = writeProjectConfig(root, { ...validConfig, governed: [{ path: 'docs/**', role: 'memo' }] });
|
|
136
|
+
writeHostConfig(root, 'claude', validConfig);
|
|
137
|
+
const result = resolveDocsConfig(root, { activeHost: 'claude' });
|
|
138
|
+
assert.strictEqual(result.status, 'invalid');
|
|
139
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
140
|
+
assert.strictEqual(result.config, null);
|
|
141
|
+
assert.ok(diagnosticCodes(result).includes('invalid-governed-role'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
checkCase('2aw amendment 1. project domains string is a schema error', () => {
|
|
145
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
146
|
+
const root = workspace('project-domains-string');
|
|
147
|
+
const projectFile = writeProjectConfig(root, { ...validConfig, domains: 'records/domains' });
|
|
148
|
+
const result = resolveDocsConfig(root);
|
|
149
|
+
assert.strictEqual(result.status, 'invalid');
|
|
150
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
151
|
+
assert.ok(diagnosticCodes(result).includes('invalid-domains'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
checkCase('2aw. project schema accepts all governed roles and warns on unknown keys', () => {
|
|
155
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
156
|
+
const root = workspace('valid-schema');
|
|
157
|
+
const project = {
|
|
158
|
+
governed: ['prd', 'adr', 'ticket', 'reference', 'log', 'report'].map(role => ({ path: `docs/${role}/**`, role })),
|
|
159
|
+
domains: ['GRPH', 'IFCE'],
|
|
160
|
+
legacy: ['docs/archive/**'],
|
|
161
|
+
derived: [{ path: 'docs/index.md', generator: 'node scripts/build-index.js' }],
|
|
162
|
+
tickets: 'tasks/tickets',
|
|
163
|
+
futureSetting: true
|
|
164
|
+
};
|
|
165
|
+
writeProjectConfig(root, project);
|
|
166
|
+
const result = resolveDocsConfig(root);
|
|
167
|
+
assert.strictEqual(result.status, 'project');
|
|
168
|
+
assert.deepStrictEqual(result.config, project);
|
|
169
|
+
assert.ok(warningCodes(result).includes('unknown-config-key'));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
checkCase('2aw. domains must match the declared format and be unique', () => {
|
|
173
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
174
|
+
const malformed = workspace('invalid-domain-format');
|
|
175
|
+
writeProjectConfig(malformed, { ...validConfig, domains: ['G'] });
|
|
176
|
+
assert.strictEqual(resolveDocsConfig(malformed).status, 'invalid');
|
|
177
|
+
const duplicate = workspace('duplicate-domain');
|
|
178
|
+
writeProjectConfig(duplicate, { ...validConfig, domains: ['GRPH', 'GRPH'] });
|
|
179
|
+
assert.strictEqual(resolveDocsConfig(duplicate).status, 'invalid');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
checkCase('2aw. derived entries require non-empty path and generator strings', () => {
|
|
183
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
184
|
+
const root = workspace('invalid-derived');
|
|
185
|
+
writeProjectConfig(root, { ...validConfig, derived: [{ path: 'docs/index.md', generator: '' }] });
|
|
186
|
+
const result = resolveDocsConfig(root);
|
|
187
|
+
assert.strictEqual(result.status, 'invalid');
|
|
188
|
+
assert.ok(diagnosticCodes(result).includes('invalid-derived-entry'));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
checkCase('2aw. tickets accepts tracker or a repo-relative path and rejects absolute paths', () => {
|
|
192
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
193
|
+
const tracker = workspace('tickets-tracker');
|
|
194
|
+
writeProjectConfig(tracker, { ...validConfig, tickets: 'tracker' });
|
|
195
|
+
assert.strictEqual(resolveDocsConfig(tracker).status, 'project');
|
|
196
|
+
const relativePath = workspace('tickets-path');
|
|
197
|
+
writeProjectConfig(relativePath, { ...validConfig, tickets: 'tasks/tickets' });
|
|
198
|
+
assert.strictEqual(resolveDocsConfig(relativePath).status, 'project');
|
|
199
|
+
const absolutePath = workspace('tickets-absolute');
|
|
200
|
+
writeProjectConfig(absolutePath, { ...validConfig, tickets: path.resolve(tempRoot, 'tickets') });
|
|
201
|
+
assert.strictEqual(resolveDocsConfig(absolutePath).status, 'invalid');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
checkCase('2aw. a known active host uses only its projectDocs manifest', () => {
|
|
205
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
206
|
+
const root = workspace('active-host');
|
|
207
|
+
const selected = { ...validConfig, domains: ['IFCE'] };
|
|
208
|
+
const other = { ...validConfig, domains: ['GRPH'] };
|
|
209
|
+
const selectedFile = writeHostConfig(root, 'codex', selected);
|
|
210
|
+
writeHostConfig(root, 'claude', other);
|
|
211
|
+
const result = resolveDocsConfig(root, { activeHost: 'codex' });
|
|
212
|
+
assert.strictEqual(result.status, 'host');
|
|
213
|
+
assert.deepStrictEqual(result.sources, [relative(root, selectedFile)]);
|
|
214
|
+
assert.deepStrictEqual(result.config, selected);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
checkCase('2aw amendment 1. a silent active host returns none and recommends project-docs.json', () => {
|
|
218
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
219
|
+
const root = workspace('active-host-silent');
|
|
220
|
+
const activeFile = writeHostConfig(root, 'codex', null, '{}');
|
|
221
|
+
writeHostConfig(root, 'claude', validConfig);
|
|
222
|
+
const result = resolveDocsConfig(root, { activeHost: 'codex' });
|
|
223
|
+
assert.strictEqual(result.status, 'none');
|
|
224
|
+
assert.deepStrictEqual(result.sources, [relative(root, activeFile)]);
|
|
225
|
+
const suggestion = (result.warnings || []).find(item => (typeof item === 'string' ? item : item.code) === 'missing-project-docs-config');
|
|
226
|
+
assert.ok(suggestion, 'missing project config warning is required');
|
|
227
|
+
assert.match(typeof suggestion === 'string' ? suggestion : suggestion.message, /\.agents\/harness-everything\/project-docs\.json/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
checkCase('2aw amendment 1. host domains string becomes domainLocation with a deprecation warning', () => {
|
|
231
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
232
|
+
const root = workspace('host-domains-string');
|
|
233
|
+
const sourceFile = writeHostConfig(root, 'claude', { domains: 'records/domain' });
|
|
234
|
+
const result = resolveDocsConfig(root, { activeHost: 'claude' });
|
|
235
|
+
assert.strictEqual(result.status, 'host');
|
|
236
|
+
assert.deepStrictEqual(result.sources, [relative(root, sourceFile)]);
|
|
237
|
+
assert.strictEqual(result.config.domainLocation, 'records/domain');
|
|
238
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(result.config, 'domains'), 'legacy path must not become the domain ID list');
|
|
239
|
+
assert.ok(warningCodes(result).includes('deprecated-domains-string'));
|
|
240
|
+
const zones = resolver.resolveProjectDocs(root);
|
|
241
|
+
assert.strictEqual(zones.paths.domain.path, path.join(root, 'records/domain'));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
checkCase('2aw amendment 1. invalid host configs are diagnosed and excluded from unknown-host comparison', () => {
|
|
245
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
246
|
+
const root = workspace('unknown-host-invalid-manifests');
|
|
247
|
+
const validFile = writeHostConfig(root, 'claude', validConfig);
|
|
248
|
+
const invalidSchemaFile = writeHostConfig(root, 'codex', { ...validConfig, domains: ['bad'] });
|
|
249
|
+
const invalidJsonFile = writeHostConfig(root, 'cursor', null, '{ invalid json');
|
|
250
|
+
const result = resolveDocsConfig(root, { activeHost: 'unknown' });
|
|
251
|
+
assert.strictEqual(result.status, 'host');
|
|
252
|
+
assert.deepStrictEqual(result.sources, [relative(root, validFile)]);
|
|
253
|
+
const invalidDiagnostics = (result.diagnostics || []).filter(item => (typeof item === 'string' ? item : item.code) === 'invalid-host-config');
|
|
254
|
+
assert.strictEqual(invalidDiagnostics.length, 2);
|
|
255
|
+
assert.deepStrictEqual(invalidDiagnostics.map(item => item.path).sort(), [relative(root, invalidJsonFile), relative(root, invalidSchemaFile)].sort());
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
checkCase('2aw. unknown host accepts canonically equal configs and records every source', () => {
|
|
259
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
260
|
+
const root = workspace('unknown-host-equal');
|
|
261
|
+
const first = { governed: validConfig.governed, domains: validConfig.domains, tickets: validConfig.tickets };
|
|
262
|
+
const second = { tickets: 'tracker', domains: ['GRPH', 'IFCE'], governed: [{ role: 'prd', path: 'docs/prd/**' }] };
|
|
263
|
+
const firstFile = writeHostConfig(root, 'claude', first);
|
|
264
|
+
const secondFile = writeHostConfig(root, 'codex', second, ` { "projectDocs" : { "tickets":"tracker", "domains":["GRPH", "IFCE"], "governed":[{"role":"prd", "path":"docs/prd/**"}] } } `);
|
|
265
|
+
const result = resolveDocsConfig(root, { activeHost: 'unknown' });
|
|
266
|
+
assert.strictEqual(result.status, 'host');
|
|
267
|
+
assert.deepStrictEqual(result.sources, [relative(root, firstFile), relative(root, secondFile)]);
|
|
268
|
+
assert.deepStrictEqual(result.config, first);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
checkCase('2aw. unknown host reports conflicting host configs as ambiguous with null config', () => {
|
|
272
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
273
|
+
const root = workspace('unknown-host-conflict');
|
|
274
|
+
writeHostConfig(root, 'claude', { ...validConfig, domains: ['GRPH'] });
|
|
275
|
+
writeHostConfig(root, 'codex', { ...validConfig, domains: ['IFCE'] });
|
|
276
|
+
const result = resolveDocsConfig(root, { activeHost: 'unknown' });
|
|
277
|
+
assert.strictEqual(result.status, 'ambiguous');
|
|
278
|
+
assert.strictEqual(result.config, null);
|
|
279
|
+
assert.strictEqual(result.sources.length, 2);
|
|
280
|
+
assert.ok(result.diagnostics.length > 0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
checkCase('2aw. no project or host config returns none', () => {
|
|
284
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
285
|
+
const result = resolveDocsConfig(workspace('none'));
|
|
286
|
+
assert.strictEqual(result.status, 'none');
|
|
287
|
+
assert.deepStrictEqual(result.sources, []);
|
|
288
|
+
assert.strictEqual(result.config, null);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
checkCase('2aw. reading and validating a generator never executes it', () => {
|
|
292
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
293
|
+
const root = workspace('generator-security');
|
|
294
|
+
const marker = path.join(tempRoot, 'generator-executed.marker');
|
|
295
|
+
const generatorScript = path.join(root, 'write-generator-marker.js');
|
|
296
|
+
fs.writeFileSync(generatorScript, `require('fs').writeFileSync(${JSON.stringify(marker)}, 'executed');\n`, 'utf8');
|
|
297
|
+
const quoteShellArg = value => process.platform === 'win32'
|
|
298
|
+
? `"${String(value).replace(/"/g, '""')}"`
|
|
299
|
+
: `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
300
|
+
const generator = `${quoteShellArg(process.execPath)} ${quoteShellArg(generatorScript)}`;
|
|
301
|
+
execFileSync(process.execPath, [generatorScript]);
|
|
302
|
+
assert.strictEqual(fs.readFileSync(marker, 'utf8'), 'executed', 'sentinel generator must work when invoked directly');
|
|
303
|
+
fs.unlinkSync(marker);
|
|
304
|
+
const project = { ...validConfig, derived: [{ path: 'docs/index.md', generator }] };
|
|
305
|
+
writeProjectConfig(root, project);
|
|
306
|
+
assert.ok(!fs.existsSync(marker));
|
|
307
|
+
const result = resolveDocsConfig(root);
|
|
308
|
+
assert.strictEqual(typeof resolver.validateDocsConfig, 'function', 'validateDocsConfig export is required');
|
|
309
|
+
const validation = resolver.validateDocsConfig(project);
|
|
310
|
+
assert.strictEqual(validation.valid, true);
|
|
311
|
+
assert.strictEqual(result.status, 'project');
|
|
312
|
+
assert.ok(!fs.existsSync(marker), 'generator marker must remain absent after resolve/validate');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
checkCase('2aw. repeated project config resolution is deterministic', () => {
|
|
316
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
317
|
+
const root = workspace('deterministic');
|
|
318
|
+
writeProjectConfig(root, validConfig);
|
|
319
|
+
assert.deepStrictEqual(resolveDocsConfig(root), resolveDocsConfig(root));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
checkCase('2aw. project file zone fields drive existing zone resolution', () => {
|
|
323
|
+
const root = workspace('project-zones');
|
|
324
|
+
const project = {
|
|
325
|
+
...validConfig,
|
|
326
|
+
decisionLocation: 'new/decisions',
|
|
327
|
+
domainLocation: 'new/domains',
|
|
328
|
+
architectureLocation: 'new/architecture'
|
|
329
|
+
};
|
|
330
|
+
writeProjectConfig(root, project);
|
|
331
|
+
writeHostConfig(root, 'claude', {
|
|
332
|
+
decisionLocation: 'old/decisions',
|
|
333
|
+
domainLocation: 'old/domains',
|
|
334
|
+
architectureLocation: 'old/architecture'
|
|
335
|
+
});
|
|
336
|
+
const result = resolver.resolveProjectDocs(root);
|
|
337
|
+
assert.strictEqual(result.paths.decision.path, path.join(root, 'new/decisions'));
|
|
338
|
+
assert.strictEqual(result.paths.domain.path, path.join(root, 'new/domains'));
|
|
339
|
+
assert.strictEqual(result.paths.architecture.path, path.join(root, 'new/architecture'));
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
checkCase('2aw. to-spec ships the same standalone resolver implementation', () => {
|
|
343
|
+
const source = fs.readFileSync(toSpecResolverPath, 'utf8');
|
|
344
|
+
assert.strictEqual(source, fs.readFileSync(resolverPath, 'utf8'));
|
|
345
|
+
assert.ok(!source.includes("require('../../multi-agent-workspace"), 'standalone resolver must not defer to a sibling skill');
|
|
346
|
+
assert.strictEqual(typeof toSpecResolver.resolveDocsConfig, 'function');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
checkCase('2aw dogfood. harness-everything ships the byte-identical standalone resolver', () => {
|
|
350
|
+
const source = fs.readFileSync(harnessResolverPath, 'utf8');
|
|
351
|
+
assert.strictEqual(source, fs.readFileSync(resolverPath, 'utf8'));
|
|
352
|
+
assert.ok(!source.includes("require('../../multi-agent-workspace"), 'standalone resolver must not defer to a sibling skill');
|
|
353
|
+
const harnessResolver = require(harnessResolverPath);
|
|
354
|
+
assert.strictEqual(typeof harnessResolver.validateDocsConfig, 'function');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
checkCase('2aw fix. caller APIs share active-host selection and keep ambiguous or silent hosts unselected', () => {
|
|
358
|
+
const root = workspace('caller-host-selection');
|
|
359
|
+
writeHostConfig(root, 'claude', { decisionLocation: 'claude-decisions' });
|
|
360
|
+
writeHostConfig(root, 'codex', { decisionLocation: 'codex-decisions' });
|
|
361
|
+
|
|
362
|
+
const selected = resolver.readProjectDocs(root, { activeHost: 'codex' });
|
|
363
|
+
assert.strictEqual(selected.status, 'host');
|
|
364
|
+
assert.deepStrictEqual(selected.projectDocs, { decisionLocation: 'codex-decisions' });
|
|
365
|
+
const selectedZones = resolver.resolveProjectDocs(root, { activeHost: 'codex' });
|
|
366
|
+
assert.strictEqual(selectedZones.paths.decision.relativePath, 'codex-decisions');
|
|
367
|
+
|
|
368
|
+
const ambiguous = resolver.readProjectDocs(root);
|
|
369
|
+
assert.strictEqual(ambiguous.status, 'ambiguous');
|
|
370
|
+
assert.strictEqual(ambiguous.projectDocs, null);
|
|
371
|
+
assert.ok(ambiguous.diagnostics.some(item => item.code === 'ambiguous-host-project-docs'));
|
|
372
|
+
const ambiguousZones = resolver.resolveProjectDocs(root);
|
|
373
|
+
assert.strictEqual(ambiguousZones.projectDocsStatus, 'ambiguous');
|
|
374
|
+
assert.strictEqual(ambiguousZones.projectDocs, null);
|
|
375
|
+
assert.ok(ambiguousZones.projectDocsDiagnostics.some(item => item.code === 'ambiguous-host-project-docs'));
|
|
376
|
+
assert.ok(!['claude-decisions', 'codex-decisions'].includes(ambiguousZones.paths.decision.relativePath));
|
|
377
|
+
|
|
378
|
+
const silent = workspace('caller-host-silent');
|
|
379
|
+
writeHostConfig(silent, 'claude', { decisionLocation: 'claude-decisions' });
|
|
380
|
+
writeHostConfig(silent, 'codex', null, '{}');
|
|
381
|
+
const silentRead = resolver.readProjectDocs(silent, { activeHost: 'codex' });
|
|
382
|
+
assert.strictEqual(silentRead.status, 'none');
|
|
383
|
+
assert.strictEqual(silentRead.projectDocs, null);
|
|
384
|
+
const silentZones = resolver.resolveProjectDocs(silent, { activeHost: 'codex' });
|
|
385
|
+
assert.strictEqual(silentZones.projectDocsStatus, 'none');
|
|
386
|
+
assert.ok(!silentZones.paths.decision.relativePath.includes('claude-decisions'));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
checkCase('2aw fix. invalid project files never authorize zone paths and surface status at callers', () => {
|
|
390
|
+
const host = { decisionLocation: 'host-decisions', tracker: 'tracker', issueDefinition: 'host-template.md' };
|
|
391
|
+
for (const [name, raw, expectedCode] of [
|
|
392
|
+
['malformed', '{ malformed json', 'invalid-project-config-json'],
|
|
393
|
+
['schema-invalid', null, 'invalid-domains']
|
|
394
|
+
]) {
|
|
395
|
+
const root = workspace(`invalid-project-caller-${name}`);
|
|
396
|
+
writeHostConfig(root, 'claude', host);
|
|
397
|
+
const project = name === 'schema-invalid'
|
|
398
|
+
? { domains: 'bad', decisionLocation: `invalid-${name}-decisions` }
|
|
399
|
+
: null;
|
|
400
|
+
writeProjectConfig(root, project, raw === null ? undefined : raw);
|
|
401
|
+
const read = resolver.readProjectDocs(root, { activeHost: 'claude' });
|
|
402
|
+
assert.strictEqual(read.status, 'invalid');
|
|
403
|
+
assert.strictEqual(read.projectDocs, null);
|
|
404
|
+
assert.ok(read.diagnostics.some(item => item.code === expectedCode));
|
|
405
|
+
const zones = resolver.resolveProjectDocs(root, { activeHost: 'claude' });
|
|
406
|
+
assert.strictEqual(zones.projectDocsStatus, 'invalid');
|
|
407
|
+
assert.strictEqual(zones.projectDocs, null);
|
|
408
|
+
assert.ok(zones.projectDocsDiagnostics.some(item => item.code === expectedCode));
|
|
409
|
+
assert.notStrictEqual(zones.paths.decision.relativePath, project && project.decisionLocation);
|
|
410
|
+
assert.notStrictEqual(zones.paths.decision.relativePath, 'host-decisions');
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
checkCase('2aw consensus-C5. validateDocsConfig rejects non-canonical globs with field-specific diagnostics', () => {
|
|
415
|
+
for (const [field, buildValue, diagCode] of [
|
|
416
|
+
['governed', v => [{ path: v, role: 'prd' }], 'invalid-governed-entry'],
|
|
417
|
+
['legacy', v => [v], 'invalid-legacy-entry'],
|
|
418
|
+
['derived', v => [{ path: v, generator: 'npm run docs:index' }], 'invalid-derived-entry'],
|
|
419
|
+
]) {
|
|
420
|
+
for (const value of ['./docs/**', '/docs/**', 'docs\\prd\\**', 'docs/../x', 'docs\\prd']) {
|
|
421
|
+
const root = workspace(`consensus-c5-${field}-${value.length}`);
|
|
422
|
+
const config = {
|
|
423
|
+
docLocation: 'docs/',
|
|
424
|
+
domains: ['GRPH'],
|
|
425
|
+
[field]: buildValue(value),
|
|
426
|
+
};
|
|
427
|
+
const validation = resolver.validateDocsConfig(config);
|
|
428
|
+
assert.strictEqual(validation.valid, false, `${field} value ${JSON.stringify(value)} must be rejected`);
|
|
429
|
+
const diag = validation.diagnostics.find(item => item.code === diagCode);
|
|
430
|
+
assert.ok(diag, `expected diagnostic ${diagCode} for ${field} value ${JSON.stringify(value)}; got ${JSON.stringify(validation.diagnostics)}`);
|
|
431
|
+
assert.strictEqual(diag.severity, 'error');
|
|
432
|
+
assert.match(diag.message, new RegExp(field === 'legacy' ? `legacy\\[[0-9]+\\]` : `${field}\\[[0-9]+\\]\\.path`), 'diagnostic must name the offending field');
|
|
433
|
+
assert.ok(diag.message.includes(JSON.stringify(value)), `diagnostic must quote the offending value; got: ${diag.message}`);
|
|
434
|
+
assert.match(diag.message, /canonical repo-relative|forward slashes|leading "\.\/"|leading "\/"|"\.\." segments/i, `diagnostic must state the rule; got: ${diag.message}`);
|
|
435
|
+
|
|
436
|
+
const read = resolver.readProjectDocs(root, { activeHost: 'claude' });
|
|
437
|
+
writeProjectConfig(root, config);
|
|
438
|
+
const afterWrite = resolver.readProjectDocs(root, { activeHost: 'claude' });
|
|
439
|
+
assert.strictEqual(afterWrite.status, 'invalid', `non-canonical ${field} value must leave the selection invalid`);
|
|
440
|
+
assert.strictEqual(afterWrite.projectDocs, null);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const acceptable = resolver.validateDocsConfig({
|
|
445
|
+
docLocation: 'docs/',
|
|
446
|
+
domains: ['GRPH'],
|
|
447
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
448
|
+
legacy: ['docs/**'],
|
|
449
|
+
derived: [{ path: 'docs/gen/index.md', generator: 'npm run docs:index' }],
|
|
450
|
+
});
|
|
451
|
+
assert.strictEqual(acceptable.valid, true, `canonical globs must be accepted: ${JSON.stringify(acceptable.diagnostics)}`);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
checkCase('2aw fix-325. validateDocsConfig rejects globs with empty path segments and accepts canonical globs', () => {
|
|
455
|
+
for (const [field, buildValue, diagCode] of [
|
|
456
|
+
['governed', v => [{ path: v, role: 'prd' }], 'invalid-governed-entry'],
|
|
457
|
+
['legacy', v => [v], 'invalid-legacy-entry'],
|
|
458
|
+
['derived', v => [{ path: v, generator: 'npm run docs:index' }], 'invalid-derived-entry'],
|
|
459
|
+
]) {
|
|
460
|
+
for (const value of ['docs//prd/**', 'docs/prd//**', 'docs/prd/']) {
|
|
461
|
+
const validation = resolver.validateDocsConfig({
|
|
462
|
+
docLocation: 'docs/',
|
|
463
|
+
domains: ['GRPH'],
|
|
464
|
+
[field]: buildValue(value),
|
|
465
|
+
});
|
|
466
|
+
assert.strictEqual(validation.valid, false, `${field} value ${JSON.stringify(value)} must be rejected`);
|
|
467
|
+
const diag = validation.diagnostics.find(item => item.code === diagCode);
|
|
468
|
+
assert.ok(diag, `expected diagnostic ${diagCode} for ${field} value ${JSON.stringify(value)}; got ${JSON.stringify(validation.diagnostics)}`);
|
|
469
|
+
assert.strictEqual(diag.severity, 'error');
|
|
470
|
+
assert.ok(diag.message.includes('must not contain empty path segments'), `diagnostic must state the empty-segment rule; got: ${diag.message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const canonical = resolver.validateDocsConfig({
|
|
475
|
+
docLocation: 'docs/',
|
|
476
|
+
domains: ['GRPH'],
|
|
477
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
478
|
+
});
|
|
479
|
+
assert.strictEqual(canonical.valid, true, `docs/prd/** must stay valid: ${JSON.stringify(canonical.diagnostics)}`);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
checkCase('2aw fix-327. validateDocsConfig rejects internal dot path segments and accepts canonical globs', () => {
|
|
483
|
+
for (const [field, buildValue, diagCode] of [
|
|
484
|
+
['governed', v => [{ path: v, role: 'adr' }], 'invalid-governed-entry'],
|
|
485
|
+
['legacy', v => [v], 'invalid-legacy-entry'],
|
|
486
|
+
['derived', v => [{ path: v, generator: 'npm run docs:index' }], 'invalid-derived-entry'],
|
|
487
|
+
]) {
|
|
488
|
+
for (const value of ['docs/./adr/**', 'docs/adr/./**']) {
|
|
489
|
+
const validation = resolver.validateDocsConfig({
|
|
490
|
+
docLocation: 'docs/',
|
|
491
|
+
domains: ['GRPH'],
|
|
492
|
+
[field]: buildValue(value),
|
|
493
|
+
});
|
|
494
|
+
assert.strictEqual(validation.valid, false, `${field} value ${JSON.stringify(value)} must be rejected`);
|
|
495
|
+
const diag = validation.diagnostics.find(item => item.code === diagCode);
|
|
496
|
+
assert.ok(diag, `expected diagnostic ${diagCode} for ${field} value ${JSON.stringify(value)}; got ${JSON.stringify(validation.diagnostics)}`);
|
|
497
|
+
assert.strictEqual(diag.severity, 'error');
|
|
498
|
+
assert.ok(diag.message.includes('must not contain "." path segments'), `diagnostic must state the internal-dot rule; got: ${diag.message}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const canonical = resolver.validateDocsConfig({
|
|
503
|
+
docLocation: 'docs/',
|
|
504
|
+
domains: ['GRPH'],
|
|
505
|
+
governed: [{ path: 'docs/adr/**', role: 'adr' }],
|
|
506
|
+
legacy: ['docs/adr/**'],
|
|
507
|
+
derived: [{ path: 'docs/gen/index.md', generator: 'npm run docs:index' }],
|
|
508
|
+
});
|
|
509
|
+
assert.strictEqual(canonical.valid, true, `docs/adr/** must stay valid: ${JSON.stringify(canonical.diagnostics)}`);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
checkCase('2aw fix-327. resolver selection rejects a governed internal-dot glob as invalid with no config', () => {
|
|
513
|
+
const root = workspace('invalid-internal-dot-selection');
|
|
514
|
+
const projectFile = writeProjectConfig(root, { ...validConfig, governed: [{ path: 'docs/./adr/**', role: 'adr' }] });
|
|
515
|
+
const result = resolveDocsConfig(root);
|
|
516
|
+
assert.strictEqual(result.status, 'invalid');
|
|
517
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
518
|
+
assert.strictEqual(result.config, null);
|
|
519
|
+
assert.ok(diagnosticCodes(result).includes('invalid-governed-entry'), `expected invalid-governed-entry; got ${JSON.stringify(diagnosticCodes(result))}`);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
checkCase('2aw fix-325. resolver selection rejects a governed empty-segment glob as invalid with no config', () => {
|
|
523
|
+
assert.strictEqual(typeof resolveDocsConfig, 'function', 'resolveDocsConfig export is required');
|
|
524
|
+
const root = workspace('invalid-empty-segment-selection');
|
|
525
|
+
const projectFile = writeProjectConfig(root, { ...validConfig, governed: [{ path: 'docs//prd/**', role: 'prd' }] });
|
|
526
|
+
const result = resolveDocsConfig(root);
|
|
527
|
+
assert.strictEqual(result.status, 'invalid');
|
|
528
|
+
assert.deepStrictEqual(result.sources, [relative(root, projectFile)]);
|
|
529
|
+
assert.strictEqual(result.config, null);
|
|
530
|
+
assert.ok(diagnosticCodes(result).includes('invalid-governed-entry'), `expected invalid-governed-entry; got ${JSON.stringify(diagnosticCodes(result))}`);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
checkCase('2aw owner-p2. heuristic inference applies only when the selection status is none', () => {
|
|
534
|
+
const project = workspace('owner-p2-inference-project');
|
|
535
|
+
const projectFull = {};
|
|
536
|
+
for (const field of ['docLocation', 'tracker', 'issueDefinition']) {
|
|
537
|
+
if (field === 'docLocation') projectFull[field] = field === 'docLocation' ? 'specs/' : `p-${field}`;
|
|
538
|
+
else projectFull[field] = `p-${field}`;
|
|
539
|
+
}
|
|
540
|
+
writeProjectConfig(project, projectFull);
|
|
541
|
+
fs.mkdirSync(path.join(project, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
|
|
542
|
+
const resolved = resolver.detectProjectDocsConventions(project);
|
|
543
|
+
assert.strictEqual(resolved.status, 'project');
|
|
544
|
+
assert.deepStrictEqual(resolved.missing, [], 'fully configured project selection reports no missing fields');
|
|
545
|
+
assert.strictEqual(resolved.tracker, 'p-tracker', 'configured tracker is returned verbatim');
|
|
546
|
+
|
|
547
|
+
const partial = workspace('owner-p2-inference-partial');
|
|
548
|
+
writeProjectConfig(partial, { docLocation: 'specs/' });
|
|
549
|
+
fs.mkdirSync(path.join(partial, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
|
|
550
|
+
const partialResult = toSpecResolver.detectProjectDocsConventions(partial);
|
|
551
|
+
assert.strictEqual(partialResult.status, 'project');
|
|
552
|
+
assert.strictEqual(partialResult.docLocation, 'specs/', 'configured field kept');
|
|
553
|
+
assert.strictEqual(partialResult.tracker, undefined, 'host conventions must not be inferred for a project selection');
|
|
554
|
+
assert.strictEqual(partialResult.issueDefinition, undefined, 'issue definition must not be inferred for a project selection');
|
|
555
|
+
assert.deepStrictEqual(partialResult.missing, ['tracker', 'issueDefinition'], 'missing-field list is returned');
|
|
556
|
+
|
|
557
|
+
const selectedHost = workspace('owner-p2-inference-host');
|
|
558
|
+
writeHostConfig(selectedHost, 'claude', { docLocation: 'specs/' });
|
|
559
|
+
fs.mkdirSync(path.join(selectedHost, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
|
|
560
|
+
const hostResult = resolver.detectProjectDocsConventions(selectedHost, { activeHost: 'claude' });
|
|
561
|
+
assert.strictEqual(hostResult.status, 'host');
|
|
562
|
+
assert.strictEqual(hostResult.tracker, undefined, 'host conventions must not be inferred for a selected host');
|
|
563
|
+
assert.deepStrictEqual(hostResult.missing, ['tracker', 'issueDefinition'], 'missing-field list is returned for host selections');
|
|
564
|
+
|
|
565
|
+
const empty = workspace('owner-p2-inference-empty');
|
|
566
|
+
writeProjectConfig(empty, {});
|
|
567
|
+
fs.mkdirSync(path.join(empty, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
|
|
568
|
+
fs.mkdirSync(path.join(empty, 'docs'), { recursive: true });
|
|
569
|
+
const emptyResult = resolver.detectProjectDocsConventions(empty);
|
|
570
|
+
assert.strictEqual(emptyResult.status, 'project', 'an empty project file stays selected');
|
|
571
|
+
assert.deepStrictEqual(emptyResult.missing, ['docLocation', 'tracker', 'issueDefinition'], 'empty selection stays incomplete');
|
|
572
|
+
|
|
573
|
+
const plain = workspace('owner-p2-inference-none');
|
|
574
|
+
fs.mkdirSync(path.join(plain, 'docs'), { recursive: true });
|
|
575
|
+
const noneResult = resolver.detectProjectDocsConventions(plain);
|
|
576
|
+
assert.strictEqual(noneResult.status, 'none');
|
|
577
|
+
assert.strictEqual(noneResult.docLocation, 'docs/', 'status none still permits workspace inference');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
checkCase('2aw owner-p2. CLI exits 1 listing the actual missing fields for selected/incomplete configs', () => {
|
|
581
|
+
const partial = workspace('owner-p2-cli-partial');
|
|
582
|
+
writeProjectConfig(partial, { docLocation: 'specs/' });
|
|
583
|
+
fs.mkdirSync(path.join(partial, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
|
|
584
|
+
const partialRun = runToSpecCheck(partial);
|
|
585
|
+
assert.strictEqual(partialRun.status, 1, `partial selected config must fail: ${partialRun.stdout}`);
|
|
586
|
+
assert.match(partialRun.stdout, /missing fields: tracker, issueDefinition/);
|
|
587
|
+
assert.ok(!partialRun.stdout.includes('GitHub Issues via .github/ISSUE_TEMPLATE/'), 'invented tracker must not appear');
|
|
588
|
+
|
|
589
|
+
const empty = workspace('owner-p2-cli-empty');
|
|
590
|
+
writeProjectConfig(empty, {});
|
|
591
|
+
fs.mkdirSync(path.join(empty, '.github', 'ISSUE_TEMPLATE'), { recursive: true });
|
|
592
|
+
fs.mkdirSync(path.join(empty, 'docs'), { recursive: true });
|
|
593
|
+
const emptyRun = runToSpecCheck(empty);
|
|
594
|
+
assert.strictEqual(emptyRun.status, 1, `empty {} selection must stay selected and incomplete: ${emptyRun.stdout}`);
|
|
595
|
+
assert.match(emptyRun.stdout, /Selected project projectDocs configuration/i);
|
|
596
|
+
assert.match(emptyRun.stdout, /missing fields: docLocation, tracker, issueDefinition/);
|
|
597
|
+
|
|
598
|
+
const ambiguousRoot = workspace('owner-p2-cli-ambiguous');
|
|
599
|
+
writeHostConfig(ambiguousRoot, 'claude', { docLocation: 'a-docs', tracker: 'a-tracker', issueDefinition: 'a-def' });
|
|
600
|
+
writeHostConfig(ambiguousRoot, 'codex', { docLocation: 'b-docs', tracker: 'b-tracker', issueDefinition: 'b-def' });
|
|
601
|
+
const ambiguousRun = runToSpecCheck(ambiguousRoot);
|
|
602
|
+
assert.strictEqual(ambiguousRun.status, 1);
|
|
603
|
+
assert.match(ambiguousRun.stdout + ambiguousRun.stderr, /ambiguous/i);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
checkCase('2aw owner-p2. init rejects whitespace-only required values consistently with the resolver', () => {
|
|
607
|
+
const projectRoot = workspace('owner-p2-init-whitespace-project');
|
|
608
|
+
const project = { docLocation: 'specs/' };
|
|
609
|
+
const projectFile = writeProjectConfig(projectRoot, project, `${JSON.stringify(project, null, 2)}\n`);
|
|
610
|
+
writeHostConfig(projectRoot, 'claude', { skills: true });
|
|
611
|
+
const hostFile = path.join(projectRoot, '.claude', 'harness-everything', 'manifest.json');
|
|
612
|
+
const projectBefore = fs.readFileSync(projectFile, 'utf8');
|
|
613
|
+
const hostBefore = fs.readFileSync(hostFile, 'utf8');
|
|
614
|
+
const result = runToSpecInit(projectRoot, ['--doc-location', ' ', '--tracker', ' ', '--issue-definition', ' ']);
|
|
615
|
+
assert.strictEqual(result.status, 1, `whitespace-only flags must be rejected: ${result.stdout}${result.stderr}`);
|
|
616
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /whitespace-only|must not be empty/i);
|
|
617
|
+
assert.strictEqual(fs.readFileSync(projectFile, 'utf8'), projectBefore, 'whitespace-only init wrote the project file');
|
|
618
|
+
assert.strictEqual(fs.readFileSync(hostFile, 'utf8'), hostBefore, 'whitespace-only init wrote the host manifest');
|
|
619
|
+
const check = runToSpecCheck(projectRoot);
|
|
620
|
+
assert.strictEqual(check.status, 1, 'a whitespace-only init attempt must not make the config complete');
|
|
621
|
+
assert.match(check.stdout, /missing fields: tracker, issueDefinition/);
|
|
622
|
+
|
|
623
|
+
const hostRoot = workspace('owner-p2-init-whitespace-host');
|
|
624
|
+
const hostFile2 = writeHostConfig(hostRoot, 'claude', { skills: true });
|
|
625
|
+
const host2Before = fs.readFileSync(hostFile2, 'utf8');
|
|
626
|
+
const result2 = runToSpecInit(hostRoot, ['--doc-location', ' ', '--tracker', '\t\t', '--issue-definition', ' \n ']);
|
|
627
|
+
assert.strictEqual(result2.status, 1, `whitespace-only host init must be rejected: ${result2.stdout}${result2.stderr}`);
|
|
628
|
+
assert.strictEqual(fs.readFileSync(hostFile2, 'utf8'), host2Before, 'whitespace-only host init must write nothing');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
function runToSpecCheck(root) {
|
|
632
|
+
return spawnSync(process.execPath, [toSpecCheckPath, 'check', '--workspace', root], { encoding: 'utf8' });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function runToSpecInit(root, args) {
|
|
636
|
+
return spawnSync(process.execPath, [toSpecCheckPath, 'init', '--workspace', root, ...args], { encoding: 'utf8' });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
checkCase('2aw owner-p2. init fills only missing fields in an existing valid project file and preserves the rest', () => {
|
|
640
|
+
const root = workspace('owner-p2-init-project-fill');
|
|
641
|
+
const project = {
|
|
642
|
+
docLocation: 'specs/',
|
|
643
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
644
|
+
domains: ['GRPH'],
|
|
645
|
+
derived: [{ path: 'docs/roadmap/index.md', generator: 'npm run docs:index' }],
|
|
646
|
+
unknownKey: 'preserve-me'
|
|
647
|
+
};
|
|
648
|
+
const projectFile = writeProjectConfig(root, project, `${JSON.stringify(project, null, 2)}\n`);
|
|
649
|
+
const before = fs.readFileSync(projectFile, 'utf8');
|
|
650
|
+
const homesBefore = Object.values(HOSTS).map(h => path.join(root, h, 'harness-everything', 'manifest.json'));
|
|
651
|
+
const result = runToSpecInit(root, ['--doc-location', 'specs/', '--tracker', 'tracker/board', '--issue-definition', 'templates/issue.md']);
|
|
652
|
+
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
653
|
+
const after = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
654
|
+
assert.strictEqual(after.docLocation, 'specs/', 'configured docLocation preserved, not overwritten');
|
|
655
|
+
assert.strictEqual(after.tracker, 'tracker/board', 'missing tracker filled in the project file');
|
|
656
|
+
assert.strictEqual(after.issueDefinition, 'templates/issue.md', 'missing issueDefinition filled in the project file');
|
|
657
|
+
assert.deepStrictEqual(after.governed, project.governed, 'governed preserved');
|
|
658
|
+
assert.deepStrictEqual(after.domains, project.domains, 'domains preserved');
|
|
659
|
+
assert.deepStrictEqual(after.derived, project.derived, 'derived preserved');
|
|
660
|
+
assert.strictEqual(after.unknownKey, project.unknownKey, 'unknown keys preserved');
|
|
661
|
+
assert.ok(!result.stdout.includes('manifest.json'), 'init with a project file must not touch host manifests');
|
|
662
|
+
for (const hostFile of homesBefore) assert.ok(!fs.existsSync(hostFile), 'no host manifest may be created');
|
|
663
|
+
const check = runToSpecCheck(root);
|
|
664
|
+
assert.strictEqual(check.status, 0, `${check.stdout}`);
|
|
665
|
+
assert.match(check.stdout, /docLocation: specs\//);
|
|
666
|
+
assert.match(check.stdout, /tracker: tracker\/board/);
|
|
667
|
+
assert.match(check.stdout, /issueDefinition: templates\/issue\.md/);
|
|
668
|
+
const afterFirstRead = fs.readFileSync(projectFile, 'utf8');
|
|
669
|
+
const result2 = runToSpecInit(root, ['--doc-location', 'specs/', '--tracker', 'tracker/board', '--issue-definition', 'templates/issue.md']);
|
|
670
|
+
assert.strictEqual(result2.status, 0, `${result2.stdout}\n${result2.stderr}`);
|
|
671
|
+
assert.strictEqual(fs.readFileSync(projectFile, 'utf8'), afterFirstRead, 'repeat init stays byte-identical when nothing is missing');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
checkCase('2aw owner-p2. init rejects conflicting flags for configured project fields with no writes', () => {
|
|
675
|
+
const root = workspace('owner-p2-init-conflict');
|
|
676
|
+
const project = { docLocation: 'specs/', tracker: 'tracker/board' };
|
|
677
|
+
const projectFile = writeProjectConfig(root, project);
|
|
678
|
+
const before = fs.readFileSync(projectFile, 'utf8');
|
|
679
|
+
const result = runToSpecInit(root, ['--doc-location', 'other/', '--tracker', 'different', '--issue-definition', 'templates/issue.md']);
|
|
680
|
+
assert.strictEqual(result.status, 1, `conflicting flags must fail: ${result.stdout}${result.stderr}`);
|
|
681
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /doc-location/);
|
|
682
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /specs\//);
|
|
683
|
+
assert.strictEqual(fs.readFileSync(projectFile, 'utf8'), before, 'untouched on conflict');
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
checkCase('2aw owner-p2. init leaves malformed or schema-invalid project files untouched with guidance', () => {
|
|
687
|
+
const malformed = workspace('owner-p2-init-malformed');
|
|
688
|
+
const malformedFile = writeProjectConfig(malformed, null, '{ broken json');
|
|
689
|
+
const malformedBefore = fs.readFileSync(malformedFile, 'utf8');
|
|
690
|
+
const malformedResult = runToSpecInit(malformed, ['--doc-location', 'a/', '--tracker', 'b/', '--issue-definition', 'c']);
|
|
691
|
+
assert.strictEqual(malformedResult.status, 1, `malformed project JSON must fail: ${malformedResult.stdout}${malformedResult.stderr}`);
|
|
692
|
+
assert.match(`${malformedResult.stdout}\n${malformedResult.stderr}`, /project-docs\.json|valid JSON|malformed/i);
|
|
693
|
+
assert.strictEqual(fs.readFileSync(malformedFile, 'utf8'), malformedBefore, 'malformed file untouched');
|
|
694
|
+
|
|
695
|
+
const invalid = workspace('owner-p2-init-invalid-schema');
|
|
696
|
+
const invalidFile = writeProjectConfig(invalid, { ...validConfig, domains: 'bad' });
|
|
697
|
+
const invalidBefore = fs.readFileSync(invalidFile, 'utf8');
|
|
698
|
+
const invalidResult = runToSpecInit(invalid, ['--doc-location', 'a/', '--tracker', 'b/', '--issue-definition', 'c']);
|
|
699
|
+
assert.strictEqual(invalidResult.status, 1, `schema-invalid project file must fail: ${invalidResult.stdout}${invalidResult.stderr}`);
|
|
700
|
+
assert.match(`${invalidResult.stdout}\n${invalidResult.stderr}`, /invalid-domains/);
|
|
701
|
+
assert.strictEqual(fs.readFileSync(invalidFile, 'utf8'), invalidBefore, 'invalid schema file untouched');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
checkCase('2aw owner-p2. host-only init preserves existing host projectDocs keys and fills only missing fields', () => {
|
|
705
|
+
const root = workspace('owner-p2-init-host-preserve');
|
|
706
|
+
const hostFile = writeHostConfig(root, 'claude', {
|
|
707
|
+
governed: [{ path: 'docs/prd/**', role: 'prd' }],
|
|
708
|
+
domains: ['GRPH'],
|
|
709
|
+
docLocation: 'host-docs/',
|
|
710
|
+
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
711
|
+
});
|
|
712
|
+
const before = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
713
|
+
const result = runToSpecInit(root, ['--doc-location', 'host-docs/', '--tracker', 'host-tracker', '--issue-definition', 'host-template']);
|
|
714
|
+
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
715
|
+
const after = JSON.parse(fs.readFileSync(hostFile, 'utf8'));
|
|
716
|
+
assert.deepStrictEqual(after.projectDocs.governed, before.projectDocs.governed, 'host governed governance keys preserved');
|
|
717
|
+
assert.deepStrictEqual(after.projectDocs.domains, before.projectDocs.domains, 'host domains preserved');
|
|
718
|
+
assert.strictEqual(after.projectDocs.docLocation, 'host-docs/', 'configured docLocation preserved');
|
|
719
|
+
assert.strictEqual(after.projectDocs.tracker, 'host-tracker', 'missing tracker filled');
|
|
720
|
+
assert.strictEqual(after.projectDocs.issueDefinition, 'host-template', 'missing issueDefinition filled');
|
|
721
|
+
assert.strictEqual(after.projectDocs.updatedAt, before.projectDocs.updatedAt, 'existing updatedAt preserved');
|
|
722
|
+
const conflicting = runToSpecInit(root, ['--doc-location', 'host-docs/', '--tracker', 'different-tracker', '--issue-definition', 'host-template']);
|
|
723
|
+
assert.strictEqual(conflicting.status, 1, `conflicting host flag must fail: ${conflicting.stdout}${conflicting.stderr}`);
|
|
724
|
+
assert.match(`${conflicting.stdout}\n${conflicting.stderr}`, /tracker/);
|
|
725
|
+
assert.strictEqual(JSON.parse(fs.readFileSync(hostFile, 'utf8')).projectDocs.tracker, 'host-tracker', 'no overwrite on conflict');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
checkCase('2aw fix. to-spec CLI consumes complete project-only fields without inventing host defaults', () => {
|
|
729
|
+
const root = workspace('to-spec-project-only');
|
|
730
|
+
const project = { docLocation: 'specs/', tracker: 'tracker/board', issueDefinition: 'templates/issue.md' };
|
|
731
|
+
writeProjectConfig(root, project);
|
|
732
|
+
const result = runToSpecCheck(root);
|
|
733
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
734
|
+
assert.match(result.stdout, /docLocation: specs\//);
|
|
735
|
+
assert.match(result.stdout, /tracker: tracker\/board/);
|
|
736
|
+
assert.match(result.stdout, /issueDefinition: templates\/issue\.md/);
|
|
737
|
+
assert.ok(!result.stdout.includes('local markdown tickets'));
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
checkCase('2aw fix. to-spec CLI refuses conflicting hosts and invalid project override over a complete host', () => {
|
|
741
|
+
const conflict = workspace('to-spec-conflicting-hosts');
|
|
742
|
+
writeHostConfig(conflict, 'claude', { docLocation: 'claude-docs', tracker: 'claude-tracker', issueDefinition: 'claude-template' });
|
|
743
|
+
writeHostConfig(conflict, 'codex', { docLocation: 'codex-docs', tracker: 'codex-tracker', issueDefinition: 'codex-template' });
|
|
744
|
+
const ambiguous = runToSpecCheck(conflict);
|
|
745
|
+
assert.notStrictEqual(ambiguous.status, 0);
|
|
746
|
+
assert.match(`${ambiguous.stdout}\n${ambiguous.stderr}`, /ambiguous/i);
|
|
747
|
+
assert.match(`${ambiguous.stdout}\n${ambiguous.stderr}`, /ambiguous-host-project-docs/);
|
|
748
|
+
|
|
749
|
+
const invalid = workspace('to-spec-invalid-project-over-host');
|
|
750
|
+
writeHostConfig(invalid, 'claude', { docLocation: 'host-docs', tracker: 'host-tracker', issueDefinition: 'host-template' });
|
|
751
|
+
writeProjectConfig(invalid, { domains: 'invalid', docLocation: 'invalid-docs', tracker: 'invalid-tracker', issueDefinition: 'invalid-template' });
|
|
752
|
+
const rejected = runToSpecCheck(invalid);
|
|
753
|
+
assert.notStrictEqual(rejected.status, 0);
|
|
754
|
+
assert.match(`${rejected.stdout}\n${rejected.stderr}`, /invalid/i);
|
|
755
|
+
assert.match(`${rejected.stdout}\n${rejected.stderr}`, /invalid-domains/);
|
|
756
|
+
assert.ok(!rejected.stdout.includes('host-docs'));
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
helper.finish();
|