harness-everything 0.34.0 → 0.36.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/.github/workflows/ci.yml +2 -0
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +42 -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-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-36-verify-timeout.test.js +534 -0
- package/ci/mechanism-37-portability-lint.test.js +71 -0
- package/ci/mechanism-38-verify-all.test.js +112 -0
- package/ci/mechanism-39-opencode-verify-outcomes.test.js +374 -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/ci/portability-lint.js +288 -0
- package/docs/document-lifecycle.md +57 -1
- package/docs/repository-contract.md +1 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +471 -0
- package/harness-everything/scripts/doc-lifecycle.js +1 -1
- package/harness-everything/scripts/doc-obligations.js +1571 -0
- package/opencode-plugin/README.md +33 -2
- package/opencode-plugin/hooks/verify.js +87 -11
- package/opencode-plugin/index.mjs +216 -25
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +3 -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 +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +1571 -0
- package/sbom.cdx.json +7 -7
- package/scripts/verify-all.js +314 -0
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const helper = require('./test-helper');
|
|
8
|
+
|
|
9
|
+
const modulePath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-evidence.js');
|
|
10
|
+
const fixtureDir = path.join(__dirname, 'fixtures', 'doc-obligations', 'evidence');
|
|
11
|
+
let checkDocObligations;
|
|
12
|
+
try {
|
|
13
|
+
({ checkDocObligations } = require(modulePath));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
helper.check('325-3a. evidence module is available', false, error.message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('\n[325-3a] doc-change evidence producer (RED)...');
|
|
19
|
+
|
|
20
|
+
function sha256(text) {
|
|
21
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Wire schema (scratchpad/325-3a/10-wire-schema.md §3; §8 overrides §4 and §6):
|
|
25
|
+
// checkDocObligations({ obligations, artifacts, baseline, executionRecords, reviewRecords })
|
|
26
|
+
// -> { diagnostics, results }; each result = { obligationId, observation, outcome,
|
|
27
|
+
// artifactDigest, baseDigest, provenance, recordRefs }.
|
|
28
|
+
// observation: "observed" | "unavailable" | "unresolved" | "not-observed" (separate from outcome)
|
|
29
|
+
// outcome: "pass" | "fail" | "none". Only observed+pass satisfies a MUST.
|
|
30
|
+
// §8 path binding: each record carries paths: [...] and artifactDigests: { "<path>": digest }.
|
|
31
|
+
// A MUST is satisfied only when EVERY path in obligation.scope.paths has an observed record
|
|
32
|
+
// bound to that path with the current candidate digest and the baseline baseDigest.
|
|
33
|
+
|
|
34
|
+
const ADR = 'docs/adr/GRPH-007.md';
|
|
35
|
+
const PRD = 'docs/prd/PRD-GRPH-001.md';
|
|
36
|
+
const CAND_TEXT = (rel) => fs.readFileSync(path.join(fixtureDir, 'candidate', rel), 'utf8');
|
|
37
|
+
const BASE_TEXT = (rel) => fs.readFileSync(path.join(fixtureDir, 'base', rel), 'utf8');
|
|
38
|
+
const BASE_DIGEST = sha256(BASE_TEXT(ADR)); // old/previous baseline digest
|
|
39
|
+
const ADR_DIGEST = sha256(CAND_TEXT(ADR)); // current candidate digest for the ADR
|
|
40
|
+
const PRD_DIGEST = sha256(CAND_TEXT(PRD)); // current candidate digest for the PRD
|
|
41
|
+
|
|
42
|
+
const CURRENT_BASELINE = Object.freeze({ digest: sha256(CAND_TEXT(ADR)) });
|
|
43
|
+
|
|
44
|
+
function obligation(capability, applicability, scopePaths, strength) {
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
id: capability,
|
|
47
|
+
capability,
|
|
48
|
+
strength: strength || 'MUST',
|
|
49
|
+
applicability,
|
|
50
|
+
scope: { paths: [...scopePaths] },
|
|
51
|
+
acceptance: 'Observed path-bound evidence with digests matching the current candidate and baseline'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function executionRecord(overrides) {
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
id: 'exec-1',
|
|
58
|
+
obligationId: 'quality-review',
|
|
59
|
+
command: 'node ci/linklint.js docs/adr/GRPH-007.md',
|
|
60
|
+
exitCode: 0,
|
|
61
|
+
paths: [ADR],
|
|
62
|
+
artifactDigests: { [ADR]: ADR_DIGEST },
|
|
63
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
64
|
+
provenance: 'execution record produced inside the verification step',
|
|
65
|
+
...(overrides || {})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function reviewRecord(overrides) {
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
id: 'review-1',
|
|
72
|
+
obligationId: 'quality-review',
|
|
73
|
+
kind: 'review',
|
|
74
|
+
verdict: 'approve',
|
|
75
|
+
summary: 'independent reviewer approved the Decision change',
|
|
76
|
+
paths: [ADR],
|
|
77
|
+
artifactDigests: { [ADR]: ADR_DIGEST },
|
|
78
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
79
|
+
provenance: 'review record bound to the current candidate and baseline',
|
|
80
|
+
citedRecordIds: [],
|
|
81
|
+
reason: undefined,
|
|
82
|
+
...(overrides || {})
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function run(input) {
|
|
87
|
+
return checkDocObligations(input);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resultFor(output, obligationId) {
|
|
91
|
+
return output.results.find(item => item.obligationId === obligationId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function checkCase(name, cases) {
|
|
95
|
+
try {
|
|
96
|
+
cases();
|
|
97
|
+
helper.check(name, true);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
helper.check(name, false, error && error.message ? error.message : String(error));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// H3 case: fabricated claim — the obligation is applicable and MUST, but the
|
|
104
|
+
// producer gets zero records of any kind. No pass, no observed evidence.
|
|
105
|
+
checkCase('325-3a H3 fabricated claim: no execution record, MUST cannot pass', () => {
|
|
106
|
+
const output = run({
|
|
107
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
108
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
109
|
+
baseline: CURRENT_BASELINE,
|
|
110
|
+
executionRecords: [],
|
|
111
|
+
reviewRecords: []
|
|
112
|
+
});
|
|
113
|
+
const result = resultFor(output, 'quality-review');
|
|
114
|
+
assert.ok(result, 'a result is reported for the unanswered obligation');
|
|
115
|
+
assert.ok(['unavailable', 'unresolved', 'not-observed'].includes(result.observation),
|
|
116
|
+
`no submitted evidence is not observed: ${result.observation}`);
|
|
117
|
+
assert.strictEqual(result.outcome, 'none', 'without evidence it never passes');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// H3 case: stale digest (§8 replaces the §4 oracle — stale = the record's
|
|
121
|
+
// artifactDigests[path] does not equal the current candidate digest for that path).
|
|
122
|
+
checkCase('325-3a H3 stale artifact digest: record made for the previous candidate is rejected', () => {
|
|
123
|
+
const output = run({
|
|
124
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
125
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
126
|
+
baseline: CURRENT_BASELINE,
|
|
127
|
+
executionRecords: [],
|
|
128
|
+
reviewRecords: [reviewRecord({ artifactDigests: { [ADR]: BASE_DIGEST } })]
|
|
129
|
+
});
|
|
130
|
+
const result = resultFor(output, 'quality-review');
|
|
131
|
+
assert.ok(result, 'a result is reported');
|
|
132
|
+
assert.ok(['unavailable', 'unresolved', 'not-observed'].includes(result.observation),
|
|
133
|
+
'stale-only evidence is not an available match');
|
|
134
|
+
assert.strictEqual(result.outcome, 'none', 'a record for the previous candidate cannot satisfy the MUST');
|
|
135
|
+
const diag = output.diagnostics.find(item => item.code === 'stale-artifact-digest');
|
|
136
|
+
assert.ok(diag, 'stale artifact digest is reported as a diagnostic');
|
|
137
|
+
assert.strictEqual(diag.obligationId, 'quality-review', 'diagnostic names the obligation');
|
|
138
|
+
assert.ok(typeof diag.message === 'string' && diag.message.length > 0, 'diagnostic carries a message');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// H3 case: stale base — the record was made against a different baseline.
|
|
142
|
+
checkCase('325-3a H3 stale base: record made against another baseline is rejected', () => {
|
|
143
|
+
const output = run({
|
|
144
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
145
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
146
|
+
baseline: CURRENT_BASELINE,
|
|
147
|
+
executionRecords: [],
|
|
148
|
+
reviewRecords: [reviewRecord({ baseDigest: BASE_DIGEST })]
|
|
149
|
+
});
|
|
150
|
+
const result = resultFor(output, 'quality-review');
|
|
151
|
+
assert.ok(result, 'a result is reported');
|
|
152
|
+
assert.ok(['unavailable', 'unresolved', 'not-observed'].includes(result.observation),
|
|
153
|
+
'stale base evidence is not an available match');
|
|
154
|
+
assert.strictEqual(result.outcome, 'none', 'a record for another baseline cannot satisfy the MUST');
|
|
155
|
+
const diag = output.diagnostics.find(item => item.code === 'stale-base-digest');
|
|
156
|
+
assert.ok(diag, 'stale base digest is reported as a diagnostic');
|
|
157
|
+
assert.strictEqual(diag.obligationId, 'quality-review', 'diagnostic names the obligation');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// H3 positive control (§8): a correctly bound current record is accepted even
|
|
161
|
+
// though candidate != base.
|
|
162
|
+
checkCase('325-3a H3 positive control: fresh path-bound evidence for an edited artifact passes', () => {
|
|
163
|
+
const output = run({
|
|
164
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
165
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
166
|
+
baseline: CURRENT_BASELINE,
|
|
167
|
+
executionRecords: [],
|
|
168
|
+
reviewRecords: [reviewRecord({})]
|
|
169
|
+
});
|
|
170
|
+
const result = resultFor(output, 'quality-review');
|
|
171
|
+
assert.ok(result, 'a result is reported');
|
|
172
|
+
assert.strictEqual(result.observation, 'observed');
|
|
173
|
+
assert.strictEqual(result.outcome, 'pass');
|
|
174
|
+
assert.ok(result.recordRefs.includes('review-1'), 'recordRefs cite the matching record');
|
|
175
|
+
assert.ok(JSON.stringify(result.provenance).includes('bound to the current candidate and baseline'),
|
|
176
|
+
'result provenance traces to the matching record');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// §8 path binding: every scope path needs a matched, path-bound record; a
|
|
180
|
+
// missing path leaves the MUST unsatisfied and a diagnostic names the path.
|
|
181
|
+
checkCase('325-3a H3 missing path: record covering only one scope path is a diagnostic naming the other', () => {
|
|
182
|
+
const output = run({
|
|
183
|
+
obligations: [obligation('quality-review', 'applicable', [ADR, PRD])],
|
|
184
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST }, [PRD]: { digest: PRD_DIGEST } },
|
|
185
|
+
baseline: CURRENT_BASELINE,
|
|
186
|
+
executionRecords: [],
|
|
187
|
+
reviewRecords: [reviewRecord({})]
|
|
188
|
+
});
|
|
189
|
+
const result = resultFor(output, 'quality-review');
|
|
190
|
+
assert.ok(result, 'a result is reported');
|
|
191
|
+
assert.strictEqual(result.outcome, 'none', 'the unmet path leaves the MUST unsatisfied');
|
|
192
|
+
const flat = JSON.stringify(output.diagnostics);
|
|
193
|
+
assert.ok(output.diagnostics.length > 0, 'a diagnostic is reported');
|
|
194
|
+
assert.ok(flat.includes(PRD), `the diagnostic names the missing path: ${flat}`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// §8 control: the same digest bound to an unrelated path cannot satisfy the
|
|
198
|
+
// obligation, and the intended path is reported as missing.
|
|
199
|
+
checkCase('325-3a H3 unrelated path binding: same digest on another path cannot satisfy', () => {
|
|
200
|
+
const output = run({
|
|
201
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
202
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST }, [PRD]: { digest: PRD_DIGEST } },
|
|
203
|
+
baseline: CURRENT_BASELINE,
|
|
204
|
+
executionRecords: [],
|
|
205
|
+
reviewRecords: [reviewRecord({ paths: [PRD], artifactDigests: { [PRD]: ADR_DIGEST } })]
|
|
206
|
+
});
|
|
207
|
+
const result = resultFor(output, 'quality-review');
|
|
208
|
+
assert.ok(result, 'a result is reported');
|
|
209
|
+
assert.strictEqual(result.outcome, 'none', 'the ADR path has no bound record');
|
|
210
|
+
const flat = JSON.stringify(output.diagnostics);
|
|
211
|
+
assert.ok(flat.includes(ADR), `the diagnostic names the unbound path: ${flat}`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// H3 case: absent verifier — a passing *command* record with the right
|
|
215
|
+
// obligationId is NOT enough; the MUST needs a review record (§8).
|
|
216
|
+
checkCase('325-3a H3 absent verifier: command-only record cannot satisfy the quality review MUST', () => {
|
|
217
|
+
const output = run({
|
|
218
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
219
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
220
|
+
baseline: CURRENT_BASELINE,
|
|
221
|
+
executionRecords: [executionRecord({})],
|
|
222
|
+
reviewRecords: []
|
|
223
|
+
});
|
|
224
|
+
const result = resultFor(output, 'quality-review');
|
|
225
|
+
assert.ok(result, 'a result is reported');
|
|
226
|
+
assert.ok(['unavailable', 'unresolved', 'not-observed'].includes(result.observation),
|
|
227
|
+
`command alone is unresolved/not-observed, not a pass: ${result.observation}`);
|
|
228
|
+
assert.strictEqual(result.outcome, 'none', 'a command exit 0 never satisfies an unreviewed MUST');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// H3 case: contradictory command results for the same obligation and binding.
|
|
232
|
+
checkCase('325-3a H3 contradictory command results fail closed', () => {
|
|
233
|
+
const output = run({
|
|
234
|
+
obligations: [obligation('link-check', 'applicable', [ADR])],
|
|
235
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
236
|
+
baseline: CURRENT_BASELINE,
|
|
237
|
+
executionRecords: [
|
|
238
|
+
executionRecord({ id: 'exec-pass', obligationId: 'link-check', exitCode: 0 }),
|
|
239
|
+
executionRecord({ id: 'exec-fail', obligationId: 'link-check', exitCode: 1 })
|
|
240
|
+
],
|
|
241
|
+
reviewRecords: []
|
|
242
|
+
});
|
|
243
|
+
const result = resultFor(output, 'link-check');
|
|
244
|
+
assert.ok(result, 'a result is reported');
|
|
245
|
+
assert.strictEqual(result.observation, 'observed', 'command results were observed');
|
|
246
|
+
assert.strictEqual(result.outcome, 'fail', 'contradiction fails closed, never a pass');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// H3 case: a single failed command result. Also proves that observation and
|
|
250
|
+
// outcome are two separate fields on one result.
|
|
251
|
+
checkCase('325-3a H3 failed command result: observed=fail is still observed and never passes', () => {
|
|
252
|
+
const output = run({
|
|
253
|
+
obligations: [obligation('link-check', 'applicable', [ADR])],
|
|
254
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
255
|
+
baseline: CURRENT_BASELINE,
|
|
256
|
+
executionRecords: [executionRecord({ id: 'exec-fail', obligationId: 'link-check', exitCode: 1 })],
|
|
257
|
+
reviewRecords: []
|
|
258
|
+
});
|
|
259
|
+
const result = resultFor(output, 'link-check');
|
|
260
|
+
assert.ok(result, 'a result is reported');
|
|
261
|
+
assert.strictEqual(result.observation, 'observed', 'the command ran: evidence is observed');
|
|
262
|
+
assert.strictEqual(result.outcome, 'fail');
|
|
263
|
+
assert.ok('observation' in result && 'outcome' in result, 'observation and outcome are separate fields');
|
|
264
|
+
assert.notStrictEqual(result.observation, result.outcome, 'observed fail is not conflated with pass');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// H3 case: unsupported waiver — no reason at all (§5, still valid under §8).
|
|
268
|
+
checkCase('325-3a H3 unsupported waiver: empty reason is a diagnostic, never a pass', () => {
|
|
269
|
+
const output = run({
|
|
270
|
+
obligations: [obligation('requirement-sync', 'applicable', [PRD])],
|
|
271
|
+
artifacts: { [PRD]: { digest: PRD_DIGEST } },
|
|
272
|
+
baseline: CURRENT_BASELINE,
|
|
273
|
+
executionRecords: [],
|
|
274
|
+
reviewRecords: [reviewRecord({
|
|
275
|
+
id: 'waiver-1',
|
|
276
|
+
obligationId: 'requirement-sync',
|
|
277
|
+
kind: 'waiver',
|
|
278
|
+
verdict: 'approve',
|
|
279
|
+
summary: ' ',
|
|
280
|
+
reason: '',
|
|
281
|
+
citedRecordIds: []
|
|
282
|
+
})]
|
|
283
|
+
});
|
|
284
|
+
const result = resultFor(output, 'requirement-sync');
|
|
285
|
+
assert.ok(result, 'a result is reported');
|
|
286
|
+
assert.strictEqual(result.outcome, 'none');
|
|
287
|
+
const diag = output.diagnostics.find(item => item.code === 'unsupported-waiver');
|
|
288
|
+
assert.ok(diag, 'unsupported waiver is reported as a diagnostic');
|
|
289
|
+
assert.strictEqual(diag.obligationId, 'requirement-sync', 'diagnostic names the obligation');
|
|
290
|
+
assert.ok(typeof diag.message === 'string' && diag.message.length > 0, 'diagnostic carries a message');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// H3 case: waiver with a reason but with citations that resolve to nothing.
|
|
294
|
+
checkCase('325-3a H3 unsupported waiver: reason cites no matching observed record stays unresolved', () => {
|
|
295
|
+
const output = run({
|
|
296
|
+
obligations: [obligation('requirement-sync', 'applicable', [PRD])],
|
|
297
|
+
artifacts: { [PRD]: { digest: PRD_DIGEST } },
|
|
298
|
+
baseline: CURRENT_BASELINE,
|
|
299
|
+
executionRecords: [],
|
|
300
|
+
reviewRecords: [reviewRecord({
|
|
301
|
+
id: 'waiver-1',
|
|
302
|
+
obligationId: 'requirement-sync',
|
|
303
|
+
kind: 'waiver',
|
|
304
|
+
verdict: 'approve',
|
|
305
|
+
summary: 'no-req-change claimed',
|
|
306
|
+
reason: 'acceptance criteria unchanged',
|
|
307
|
+
citedRecordIds: ['exec-does-not-exist']
|
|
308
|
+
})]
|
|
309
|
+
});
|
|
310
|
+
const result = resultFor(output, 'requirement-sync');
|
|
311
|
+
assert.ok(result, 'a result is reported');
|
|
312
|
+
assert.strictEqual(result.observation, 'unresolved', 'the cited record does not resolve');
|
|
313
|
+
assert.strictEqual(result.outcome, 'none', 'an unresolved citation never satisfies the MUST');
|
|
314
|
+
assert.ok(output.diagnostics.some(item => item.code === 'unsupported-waiver'), 'unsupported waiver is diagnosed');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// H3 positive control: waiver with a reason plus a cited, observed, path-bound
|
|
318
|
+
// record satisfies the obligation (structured no-req-change per D6.6).
|
|
319
|
+
checkCase('325-3a H3 waiver with reason and matching cited observed record passes', () => {
|
|
320
|
+
const output = run({
|
|
321
|
+
obligations: [{ ...obligation('requirement-sync', 'applicable', [PRD]), citedReqs: [{ id: 'REQ-GRPH-001', file: PRD, resolution: 'resolved' }], reqUpdates: [] }],
|
|
322
|
+
artifacts: { [PRD]: { digest: PRD_DIGEST } },
|
|
323
|
+
baseline: CURRENT_BASELINE,
|
|
324
|
+
executionRecords: [executionRecord({
|
|
325
|
+
id: 'exec-req',
|
|
326
|
+
obligationId: 'requirement-sync',
|
|
327
|
+
paths: [PRD],
|
|
328
|
+
artifactDigests: { [PRD]: PRD_DIGEST }
|
|
329
|
+
})],
|
|
330
|
+
reviewRecords: [reviewRecord({
|
|
331
|
+
id: 'waiver-1',
|
|
332
|
+
obligationId: 'requirement-sync',
|
|
333
|
+
kind: 'waiver',
|
|
334
|
+
verdict: 'approve',
|
|
335
|
+
summary: 'no-req-change accepted with cited evidence',
|
|
336
|
+
requirementIds: ['REQ-GRPH-001'],
|
|
337
|
+
reason: 'no-req-change: acceptance criteria unchanged, cited passing test',
|
|
338
|
+
citedRecordIds: ['exec-req'],
|
|
339
|
+
paths: [PRD],
|
|
340
|
+
artifactDigests: { [PRD]: PRD_DIGEST }
|
|
341
|
+
})]
|
|
342
|
+
});
|
|
343
|
+
const result = resultFor(output, 'requirement-sync');
|
|
344
|
+
assert.ok(result, 'a result is reported');
|
|
345
|
+
assert.strictEqual(result.observation, 'observed');
|
|
346
|
+
assert.strictEqual(result.outcome, 'pass');
|
|
347
|
+
assert.ok(result.recordRefs.includes('waiver-1'), 'result cites the waiver');
|
|
348
|
+
assert.ok(result.recordRefs.includes('exec-req'), 'result cites the supporting record');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// §8 review-record gate: a review record with perfect path binding but a
|
|
352
|
+
// non-approve verdict (or a missing/empty summary) is a diagnostic and no pass.
|
|
353
|
+
checkCase('325-3a H3 review gate: non-approve verdict or missing summary is a diagnostic, never a pass', () => {
|
|
354
|
+
for (const bad of [
|
|
355
|
+
reviewRecord({ verdict: 'reject' }),
|
|
356
|
+
reviewRecord({ summary: undefined }),
|
|
357
|
+
reviewRecord({ summary: '' })
|
|
358
|
+
]) {
|
|
359
|
+
const output = run({
|
|
360
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
361
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
362
|
+
baseline: CURRENT_BASELINE,
|
|
363
|
+
executionRecords: [],
|
|
364
|
+
reviewRecords: [bad]
|
|
365
|
+
});
|
|
366
|
+
const result = resultFor(output, 'quality-review');
|
|
367
|
+
assert.ok(result, `a result is reported for ${JSON.stringify(bad.verdict || bad.summary === undefined)}`);
|
|
368
|
+
assert.strictEqual(result.outcome, 'none', 'only an approve verdict with a summary can satisfy quality-review');
|
|
369
|
+
assert.ok(output.diagnostics.length > 0, 'the deficient review is visible as a diagnostic');
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// §8 code-only: applicability unknown -> observation not-observed, outcome none.
|
|
374
|
+
// Visible but NOT a failure; the pilot defines NO completion policy for this
|
|
375
|
+
// state (belongs to the later reconciliation contract). No lock is implied here.
|
|
376
|
+
checkCase('325-3a code-only unknown MUST stays not-observed, visible, and never passes (no policy implied)', () => {
|
|
377
|
+
const output = run({
|
|
378
|
+
obligations: [obligation('requirement-sync', 'unknown', ['src/lint.js'])],
|
|
379
|
+
artifacts: { 'src/lint.js': { digest: ADR_DIGEST } },
|
|
380
|
+
baseline: CURRENT_BASELINE,
|
|
381
|
+
executionRecords: [],
|
|
382
|
+
reviewRecords: []
|
|
383
|
+
});
|
|
384
|
+
const result = resultFor(output, 'requirement-sync');
|
|
385
|
+
assert.ok(result, 'the not-observed state stays visible as a result');
|
|
386
|
+
assert.strictEqual(result.observation, 'not-observed', 'unknown applicability is not-observed');
|
|
387
|
+
assert.strictEqual(result.outcome, 'none', 'not-observed is never a pass');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// §3: the full result field contract.
|
|
391
|
+
checkCase('325-3a result carries obligation id, observation, outcome, digests, provenance, recordRefs', () => {
|
|
392
|
+
const output = run({
|
|
393
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
394
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
395
|
+
baseline: CURRENT_BASELINE,
|
|
396
|
+
executionRecords: [],
|
|
397
|
+
reviewRecords: [reviewRecord({})]
|
|
398
|
+
});
|
|
399
|
+
const result = resultFor(output, 'quality-review');
|
|
400
|
+
assert.ok(result, 'a result is reported');
|
|
401
|
+
for (const field of ['obligationId', 'observation', 'outcome', 'provenance']) {
|
|
402
|
+
assert.ok(typeof result[field] === 'string' && result[field].length > 0, `${field} is a non-empty string`);
|
|
403
|
+
}
|
|
404
|
+
assert.ok(typeof result.artifactDigest === 'string' && result.artifactDigest.length > 0, 'artifactDigest is present');
|
|
405
|
+
assert.ok(typeof result.baseDigest === 'string' && result.baseDigest.length > 0, 'baseDigest is present');
|
|
406
|
+
assert.ok(Array.isArray(result.recordRefs), 'recordRefs is an array');
|
|
407
|
+
assert.ok(result.recordRefs.includes('review-1'), 'recordRefs cites the matching record');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// §3: diagnostics are structured.
|
|
411
|
+
checkCase('325-3a diagnostics are structured code/obligationId/message entries', () => {
|
|
412
|
+
const output = run({
|
|
413
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
414
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
415
|
+
baseline: CURRENT_BASELINE,
|
|
416
|
+
executionRecords: [],
|
|
417
|
+
reviewRecords: [reviewRecord({ artifactDigests: { [ADR]: BASE_DIGEST } })]
|
|
418
|
+
});
|
|
419
|
+
assert.ok(Array.isArray(output.diagnostics), 'diagnostics is an array');
|
|
420
|
+
assert.ok(output.diagnostics.length > 0, 'a rejection is visible in diagnostics');
|
|
421
|
+
for (const diag of output.diagnostics) {
|
|
422
|
+
assert.ok(typeof diag.code === 'string' && diag.code.length > 0, 'diagnostic code is a non-empty string');
|
|
423
|
+
assert.ok(diag.obligationId === undefined || typeof diag.obligationId === 'string', 'diagnostic obligationId is a string when present');
|
|
424
|
+
assert.ok(typeof diag.message === 'string' && diag.message.length > 0, 'diagnostic message is a non-empty string');
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// §4/§8: a record for an obligation that is not in the input list is unrelated
|
|
429
|
+
// and cannot satisfy anything; results never report a pass out of nowhere.
|
|
430
|
+
checkCase('325-3a unrelated record (other obligation) cannot satisfy anything', () => {
|
|
431
|
+
const output = run({
|
|
432
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
433
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
434
|
+
baseline: CURRENT_BASELINE,
|
|
435
|
+
executionRecords: [executionRecord({ id: 'exec-other', obligationId: 'doc-lint' })],
|
|
436
|
+
reviewRecords: []
|
|
437
|
+
});
|
|
438
|
+
assert.strictEqual(JSON.stringify(output.results).includes('"pass"'), false,
|
|
439
|
+
'no result reports a pass for an unrelated record');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// N/A obligations never pass; their representation (presence or absence in
|
|
443
|
+
// results) is implementation freedom under the wire schema.
|
|
444
|
+
checkCase('325-3a N/A obligations never produce a pass', () => {
|
|
445
|
+
const output = run({
|
|
446
|
+
obligations: [obligation('doc-lint', 'N/A', [ADR])],
|
|
447
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
448
|
+
baseline: CURRENT_BASELINE,
|
|
449
|
+
executionRecords: [executionRecord({ id: 'exec-1', obligationId: 'doc-lint' })],
|
|
450
|
+
reviewRecords: []
|
|
451
|
+
});
|
|
452
|
+
assert.strictEqual(JSON.stringify(output.results).includes('"pass"'), false,
|
|
453
|
+
'N/A never passes even with a matching record');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Orphan waiver: a waiver whose obligationId matches no obligation in the input
|
|
457
|
+
// is unrelated to every obligation. It must satisfy nothing, be diagnosed by id,
|
|
458
|
+
// and leave every result at outcome "none".
|
|
459
|
+
checkCase('325-3a orphan waiver: unmatched obligationId satisfies nothing, is diagnosed as waiver-unmatched, outcomes stay none', () => {
|
|
460
|
+
const output = run({
|
|
461
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
462
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
463
|
+
baseline: CURRENT_BASELINE,
|
|
464
|
+
executionRecords: [],
|
|
465
|
+
reviewRecords: [reviewRecord({
|
|
466
|
+
id: 'waiver-orphan',
|
|
467
|
+
obligationId: 'no-such-obligation',
|
|
468
|
+
kind: 'waiver',
|
|
469
|
+
verdict: 'approve',
|
|
470
|
+
summary: 'orphan waiver for an obligation that does not exist',
|
|
471
|
+
reason: 'no-req-change: acceptance criteria unchanged',
|
|
472
|
+
citedRecordIds: []
|
|
473
|
+
})]
|
|
474
|
+
});
|
|
475
|
+
const result = resultFor(output, 'quality-review');
|
|
476
|
+
assert.ok(result, 'the real obligation still gets a result');
|
|
477
|
+
assert.strictEqual(result.outcome, 'none', 'the orphan waiver satisfies no obligation');
|
|
478
|
+
const diag = output.diagnostics.find(item => item.code === 'waiver-unmatched');
|
|
479
|
+
assert.ok(diag, 'orphan waiver is reported with code waiver-unmatched');
|
|
480
|
+
assert.ok(JSON.stringify(diag).includes('waiver-orphan'), `diagnostic names the waiver id: ${JSON.stringify(diag)}`);
|
|
481
|
+
for (const item of output.results) {
|
|
482
|
+
assert.strictEqual(item.outcome, 'none', `${item.obligationId} outcome is none`);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Round 2 (D): an independent positive control. The baseline is the BASE ADR digest, not the
|
|
487
|
+
// candidate digest, so candidate != baseline and a record for the baseline cannot pass as current.
|
|
488
|
+
const BASE_BASELINE = Object.freeze({ digest: BASE_DIGEST });
|
|
489
|
+
|
|
490
|
+
checkCase('325-3a H3 independent-baseline positive control: baseline digest differs from candidate digest and fresh evidence passes', () => {
|
|
491
|
+
assert.notStrictEqual(BASE_DIGEST, ADR_DIGEST, 'the fixture base and candidate ADR digests differ');
|
|
492
|
+
const output = run({
|
|
493
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
494
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
495
|
+
baseline: BASE_BASELINE,
|
|
496
|
+
executionRecords: [],
|
|
497
|
+
reviewRecords: [reviewRecord({ baseDigest: BASE_DIGEST })]
|
|
498
|
+
});
|
|
499
|
+
const result = resultFor(output, 'quality-review');
|
|
500
|
+
assert.ok(result, 'a result is reported');
|
|
501
|
+
assert.strictEqual(result.observation, 'observed');
|
|
502
|
+
assert.strictEqual(result.outcome, 'pass', 'a record bound to the current candidate and the independent baseline passes');
|
|
503
|
+
assert.ok(result.recordRefs.includes('review-1'), 'recordRefs cite the matching record');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
checkCase('325-3a H3 independent-baseline control: a record bound to the candidate digest as its base is rejected', () => {
|
|
507
|
+
const output = run({
|
|
508
|
+
obligations: [obligation('quality-review', 'applicable', [ADR])],
|
|
509
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
510
|
+
baseline: BASE_BASELINE,
|
|
511
|
+
executionRecords: [],
|
|
512
|
+
reviewRecords: [reviewRecord({ baseDigest: ADR_DIGEST })]
|
|
513
|
+
});
|
|
514
|
+
const result = resultFor(output, 'quality-review');
|
|
515
|
+
assert.ok(result, 'a result is reported');
|
|
516
|
+
assert.strictEqual(result.outcome, 'none', 'a base digest that is not the selected baseline cannot satisfy the MUST');
|
|
517
|
+
assert.ok(output.diagnostics.some(item => item.code === 'stale-base-digest'), 'the mismatched base is diagnosed');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Round 2 (A, evidence level): the waiver record must itself be path-bound with current digests
|
|
521
|
+
// and a current baseDigest (§9A (b)). A stale waiver that cites current observed evidence is not a valid contributor.
|
|
522
|
+
checkCase('325-3a H3 waiver self-binding: a stale waiver citing current observed evidence cannot satisfy requirement-sync', () => {
|
|
523
|
+
const output = run({
|
|
524
|
+
obligations: [obligation('requirement-sync', 'applicable', [PRD])],
|
|
525
|
+
artifacts: { [PRD]: { digest: PRD_DIGEST } },
|
|
526
|
+
baseline: CURRENT_BASELINE,
|
|
527
|
+
executionRecords: [executionRecord({
|
|
528
|
+
id: 'exec-req',
|
|
529
|
+
obligationId: 'requirement-sync',
|
|
530
|
+
paths: [PRD],
|
|
531
|
+
artifactDigests: { [PRD]: PRD_DIGEST }
|
|
532
|
+
})],
|
|
533
|
+
reviewRecords: [reviewRecord({
|
|
534
|
+
id: 'waiver-stale',
|
|
535
|
+
obligationId: 'requirement-sync',
|
|
536
|
+
kind: 'waiver',
|
|
537
|
+
verdict: 'approve',
|
|
538
|
+
summary: 'no-req-change accepted with cited evidence',
|
|
539
|
+
reason: 'no-req-change: acceptance criteria unchanged, cited passing test',
|
|
540
|
+
citedRecordIds: ['exec-req'],
|
|
541
|
+
paths: [PRD],
|
|
542
|
+
artifactDigests: { [PRD]: PRD_DIGEST },
|
|
543
|
+
baseDigest: BASE_DIGEST
|
|
544
|
+
})]
|
|
545
|
+
});
|
|
546
|
+
const result = resultFor(output, 'requirement-sync');
|
|
547
|
+
assert.ok(result, 'a result is reported');
|
|
548
|
+
assert.strictEqual(result.outcome, 'none', 'the stale waiver is not a valid contributor, so the pass branch is not reached');
|
|
549
|
+
assert.ok(JSON.stringify(output.diagnostics).includes('waiver-stale'), 'the stale waiver is diagnosed by id');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Round 4 (§10): observed/fail for an unmet requirement-sync obligation, only when every §10 condition holds.
|
|
553
|
+
// The obligation carries assertionChanged, inventoryComplete and citedReqs/reqUpdates from derivation.
|
|
554
|
+
const TEST_PATH = 'ci/mechanism-lint.test.js';
|
|
555
|
+
const TEST_DIGEST = sha256('assert.equal(run(), 2);');
|
|
556
|
+
|
|
557
|
+
function reqSyncObligation(overrides) {
|
|
558
|
+
return Object.freeze({
|
|
559
|
+
id: 'doc-change:requirement-sync',
|
|
560
|
+
capability: 'requirement-sync',
|
|
561
|
+
strength: 'MUST',
|
|
562
|
+
applicability: 'applicable',
|
|
563
|
+
scope: { paths: [TEST_PATH, PRD] },
|
|
564
|
+
acceptance: 'Evaluate whether affected requirements need an update or supported no-change record.',
|
|
565
|
+
assertionChanged: true,
|
|
566
|
+
inventoryComplete: true,
|
|
567
|
+
citedReqs: [{ id: 'REQ-GRPH-001', file: PRD }],
|
|
568
|
+
reqUpdates: [],
|
|
569
|
+
...(overrides || {})
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function runReqSync(obligation, extra) {
|
|
574
|
+
return run({
|
|
575
|
+
obligations: [obligation],
|
|
576
|
+
artifacts: { [TEST_PATH]: { digest: TEST_DIGEST }, [PRD]: { digest: PRD_DIGEST } },
|
|
577
|
+
baseline: CURRENT_BASELINE,
|
|
578
|
+
executionRecords: [],
|
|
579
|
+
reviewRecords: [],
|
|
580
|
+
...(extra || {})
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Round 6 fix (inputs only): the round-4 §10 positives supply the F2 attested envelope, same shape as the
|
|
585
|
+
// round-6 fixture inventory.json. candidateDigest is the PRD-set digest of this case's artifacts (PRD only).
|
|
586
|
+
const ROUND4_ATTESTED_INVENTORY = Object.freeze({
|
|
587
|
+
complete: true,
|
|
588
|
+
waiverInventoryComplete: true,
|
|
589
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
590
|
+
candidateDigest: sha256(JSON.stringify([[PRD, PRD_DIGEST]])),
|
|
591
|
+
configDigest: sha256('shared derive config')
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
checkCase('325-3a req-sync fail: all §10 conditions hold, no revision, no waiver => observed/fail with req-sync-no-revision-or-waiver naming REQ and file', () => {
|
|
595
|
+
const output = runReqSync(reqSyncObligation({ inventory: ROUND4_ATTESTED_INVENTORY }));
|
|
596
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
597
|
+
assert.ok(result, 'a result is reported');
|
|
598
|
+
assert.strictEqual(result.observation, 'observed', 'the unmet obligation is an observed check');
|
|
599
|
+
assert.strictEqual(result.outcome, 'fail', 'the unmet obligation is an observed failure');
|
|
600
|
+
const diag = output.diagnostics.find(item => item.code === 'req-sync-no-revision-or-waiver');
|
|
601
|
+
assert.ok(diag, 'the failure is diagnosed');
|
|
602
|
+
assert.strictEqual(diag.obligationId, 'doc-change:requirement-sync', 'diagnostic names the obligation');
|
|
603
|
+
const flat = JSON.stringify(diag);
|
|
604
|
+
assert.ok(flat.includes('REQ-GRPH-001'), `diagnostic names the REQ id: ${flat}`);
|
|
605
|
+
assert.ok(flat.includes(PRD), `diagnostic names the file: ${flat}`);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
checkCase('325-3a req-sync negative: inventory incomplete is not a fail (§10)', () => {
|
|
609
|
+
const output = runReqSync(reqSyncObligation({ inventoryComplete: false }));
|
|
610
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'incomplete inventory never establishes absence');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
checkCase('325-3a req-sync negative: comment-only edit (assertionChanged false) is not a fail (§10)', () => {
|
|
614
|
+
const output = runReqSync(reqSyncObligation({ assertionChanged: false }));
|
|
615
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'a comment-only change is not an unmet assertion change');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
checkCase('325-3a req-sync negative: unknown assertion change is not a fail (§10)', () => {
|
|
619
|
+
const output = runReqSync(reqSyncObligation({ assertionChanged: 'unknown' }));
|
|
620
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'an undeterminable assertion change is not a fail');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
checkCase('325-3a req-sync negative: cited REQ with file null (not resolved in the inventory) is not a fail (§10)', () => {
|
|
624
|
+
const output = runReqSync(reqSyncObligation({ citedReqs: [{ id: 'REQ-GRPH-001', file: null }] }));
|
|
625
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'an unresolved REQ does not establish absence');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
checkCase('325-3a req-sync: invalid waiver with empty reason still fails, and also carries unsupported-waiver (§10 condition 5)', () => {
|
|
629
|
+
const output = runReqSync(reqSyncObligation({ inventory: ROUND4_ATTESTED_INVENTORY }), {
|
|
630
|
+
reviewRecords: [{
|
|
631
|
+
id: 'waiver-empty',
|
|
632
|
+
obligationId: 'doc-change:requirement-sync',
|
|
633
|
+
kind: 'waiver',
|
|
634
|
+
verdict: 'approve',
|
|
635
|
+
summary: 'no-req-change',
|
|
636
|
+
reason: '',
|
|
637
|
+
citedRecordIds: [],
|
|
638
|
+
paths: [TEST_PATH, PRD],
|
|
639
|
+
artifactDigests: { [TEST_PATH]: TEST_DIGEST, [PRD]: PRD_DIGEST },
|
|
640
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
641
|
+
provenance: 'waiver record bound to the current candidate and baseline'
|
|
642
|
+
}]
|
|
643
|
+
});
|
|
644
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
645
|
+
assert.strictEqual(result.outcome, 'fail', 'the unmet obligation still fails; the invalid waiver does not rescue it');
|
|
646
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-no-revision-or-waiver'), 'the failure is diagnosed');
|
|
647
|
+
assert.ok(output.diagnostics.some(item => item.code === 'unsupported-waiver' && item.obligationId === 'doc-change:requirement-sync'),
|
|
648
|
+
'the invalid waiver attempt is also diagnosed');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
checkCase('325-3a req-sync positive: valid revision alternative with matching evidence passes (existing behavior, not fail)', () => {
|
|
652
|
+
const output = runReqSync(reqSyncObligation({ reqUpdates: [{ id: 'REQ-GRPH-001', file: PRD }] }), {
|
|
653
|
+
executionRecords: [{
|
|
654
|
+
id: 'exec-req',
|
|
655
|
+
obligationId: 'doc-change:requirement-sync',
|
|
656
|
+
command: 'node ci/mechanism-lint.test.js',
|
|
657
|
+
exitCode: 0,
|
|
658
|
+
paths: [TEST_PATH, PRD],
|
|
659
|
+
artifactDigests: { [TEST_PATH]: TEST_DIGEST, [PRD]: PRD_DIGEST },
|
|
660
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
661
|
+
provenance: 'execution record bound to the current candidate and baseline'
|
|
662
|
+
}]
|
|
663
|
+
});
|
|
664
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
665
|
+
assert.strictEqual(result.observation, 'observed');
|
|
666
|
+
assert.strictEqual(result.outcome, 'pass', 'a satisfied revision alternative passes');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Round 5 (PR #334 review R4, R5). Evidence expectations only.
|
|
670
|
+
|
|
671
|
+
// R4: minimum execution-evidence schema. A bound record with exitCode 0 but no command or provenance is
|
|
672
|
+
// incomplete: a diagnostic, and it never passes.
|
|
673
|
+
checkCase('325-3a R4 incomplete execution record (bound, exitCode 0, no command or provenance): not pass, execution-record-incomplete', () => {
|
|
674
|
+
const output = run({
|
|
675
|
+
obligations: [obligation('doc-lint', 'applicable', [ADR])],
|
|
676
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
677
|
+
baseline: CURRENT_BASELINE,
|
|
678
|
+
executionRecords: [executionRecord({ id: 'exec-bare', obligationId: 'doc-lint', command: undefined, provenance: undefined })],
|
|
679
|
+
reviewRecords: []
|
|
680
|
+
});
|
|
681
|
+
const result = resultFor(output, 'doc-lint');
|
|
682
|
+
assert.ok(result, 'a result is reported');
|
|
683
|
+
assert.notStrictEqual(result.outcome, 'pass', 'an incomplete record never passes');
|
|
684
|
+
const diag = output.diagnostics.find(item => item.code === 'execution-record-incomplete');
|
|
685
|
+
assert.ok(diag, 'the incomplete record is diagnosed');
|
|
686
|
+
assert.ok(JSON.stringify(diag).includes('exec-bare'), `the diagnostic names the record: ${JSON.stringify(diag)}`);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
checkCase('325-3a R4 incomplete execution record (exitCode is a string "0"): not pass, execution-record-incomplete', () => {
|
|
690
|
+
const output = run({
|
|
691
|
+
obligations: [obligation('doc-lint', 'applicable', [ADR])],
|
|
692
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
693
|
+
baseline: CURRENT_BASELINE,
|
|
694
|
+
executionRecords: [executionRecord({ id: 'exec-string-exit', obligationId: 'doc-lint', exitCode: '0' })],
|
|
695
|
+
reviewRecords: []
|
|
696
|
+
});
|
|
697
|
+
const result = resultFor(output, 'doc-lint');
|
|
698
|
+
assert.notStrictEqual(result.outcome, 'pass', 'a non-integer exit code never passes');
|
|
699
|
+
assert.ok(output.diagnostics.some(item => item.code === 'execution-record-incomplete'), 'the record is diagnosed');
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
checkCase('325-3a R4 control: complete execution record (command, provenance, integer exitCode 0): doc-lint passes', () => {
|
|
703
|
+
const output = run({
|
|
704
|
+
obligations: [obligation('doc-lint', 'applicable', [ADR])],
|
|
705
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
706
|
+
baseline: CURRENT_BASELINE,
|
|
707
|
+
executionRecords: [executionRecord({ id: 'exec-complete', obligationId: 'doc-lint' })],
|
|
708
|
+
reviewRecords: []
|
|
709
|
+
});
|
|
710
|
+
const result = resultFor(output, 'doc-lint');
|
|
711
|
+
assert.strictEqual(result.observation, 'observed');
|
|
712
|
+
assert.strictEqual(result.outcome, 'pass', 'a complete bound record passes');
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// R5: a no-REQ-change waiver is allowed only for a resolved requirement (file non-null). An orphan id
|
|
716
|
+
// (no PRD declaration) is unresolved, with a targeted diagnostic, and never passes.
|
|
717
|
+
checkCase('325-3a R5 orphan REQ + valid current waiver + passing execution: not pass, req-unresolved names the REQ id', () => {
|
|
718
|
+
const output = runReqSync(reqSyncObligation({ citedReqs: [{ id: 'REQ-GRPH-099', file: null }] }), {
|
|
719
|
+
executionRecords: [{
|
|
720
|
+
id: 'exec-req',
|
|
721
|
+
obligationId: 'doc-change:requirement-sync',
|
|
722
|
+
command: 'node ci/mechanism-lint.test.js',
|
|
723
|
+
exitCode: 0,
|
|
724
|
+
paths: [TEST_PATH, PRD],
|
|
725
|
+
artifactDigests: { [TEST_PATH]: TEST_DIGEST, [PRD]: PRD_DIGEST },
|
|
726
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
727
|
+
provenance: 'execution record bound to the current candidate and baseline'
|
|
728
|
+
}],
|
|
729
|
+
reviewRecords: [{
|
|
730
|
+
id: 'waiver-orphan',
|
|
731
|
+
obligationId: 'doc-change:requirement-sync',
|
|
732
|
+
kind: 'waiver',
|
|
733
|
+
verdict: 'approve',
|
|
734
|
+
summary: 'no-req-change',
|
|
735
|
+
reason: 'no-req-change: acceptance criteria unchanged, cited passing test',
|
|
736
|
+
citedRecordIds: ['exec-req'],
|
|
737
|
+
paths: [TEST_PATH, PRD],
|
|
738
|
+
artifactDigests: { [TEST_PATH]: TEST_DIGEST, [PRD]: PRD_DIGEST },
|
|
739
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
740
|
+
provenance: 'waiver record bound to the current candidate and baseline'
|
|
741
|
+
}]
|
|
742
|
+
});
|
|
743
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
744
|
+
assert.ok(result, 'a result is reported');
|
|
745
|
+
assert.notStrictEqual(result.outcome, 'pass', 'an unresolved REQ never passes through a waiver');
|
|
746
|
+
const diag = output.diagnostics.find(item => item.code === 'req-unresolved');
|
|
747
|
+
assert.ok(diag, 'the unresolved REQ is diagnosed');
|
|
748
|
+
assert.ok(JSON.stringify(diag).includes('REQ-GRPH-099'), `the diagnostic names the REQ id: ${JSON.stringify(diag)}`);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Round 6 (Sol round-3 review F1, F2). The fail lane needs an attested envelope; the pass lane does not.
|
|
752
|
+
// Envelope: obligation.inventory = { complete, waiverInventoryComplete, baseDigest, candidateDigest, configDigest }.
|
|
753
|
+
// candidateDigest = sha256 over the sorted [path, digest] pairs of artifacts under docs/prd/.
|
|
754
|
+
|
|
755
|
+
function prdSetDigest(artifacts) {
|
|
756
|
+
return sha256(JSON.stringify(Object.keys(artifacts).filter(p => p.startsWith('docs/prd/')).sort()
|
|
757
|
+
.map(p => [p, artifacts[p].digest])));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const RS_ARTIFACTS = { [TEST_PATH]: { digest: TEST_DIGEST }, [PRD]: { digest: PRD_DIGEST } };
|
|
761
|
+
const ATTESTED_INVENTORY = Object.freeze({
|
|
762
|
+
complete: true,
|
|
763
|
+
waiverInventoryComplete: true,
|
|
764
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
765
|
+
candidateDigest: prdSetDigest(RS_ARTIFACTS),
|
|
766
|
+
configDigest: sha256('shared derive config')
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
function attestedReqSync(overrides) {
|
|
770
|
+
return reqSyncObligation({ inventory: ATTESTED_INVENTORY, ...(overrides || {}) });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function validWaiver(id, obligationId, citedRecordIds, requirementIds = ['REQ-GRPH-001']) {
|
|
774
|
+
return {
|
|
775
|
+
id,
|
|
776
|
+
obligationId,
|
|
777
|
+
kind: 'waiver',
|
|
778
|
+
verdict: 'approve',
|
|
779
|
+
summary: 'no-req-change',
|
|
780
|
+
reason: 'no-req-change: acceptance criteria unchanged, cited passing test',
|
|
781
|
+
requirementIds,
|
|
782
|
+
citedRecordIds,
|
|
783
|
+
paths: [TEST_PATH, PRD],
|
|
784
|
+
artifactDigests: { [TEST_PATH]: TEST_DIGEST, [PRD]: PRD_DIGEST },
|
|
785
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
786
|
+
provenance: 'waiver record bound to the current candidate and baseline'
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function boundExec(id) {
|
|
791
|
+
return {
|
|
792
|
+
id,
|
|
793
|
+
obligationId: 'doc-change:requirement-sync',
|
|
794
|
+
command: 'node ci/mechanism-lint.test.js',
|
|
795
|
+
exitCode: 0,
|
|
796
|
+
paths: [TEST_PATH, PRD],
|
|
797
|
+
artifactDigests: { [TEST_PATH]: TEST_DIGEST, [PRD]: PRD_DIGEST },
|
|
798
|
+
baseDigest: CURRENT_BASELINE.digest,
|
|
799
|
+
provenance: 'execution record bound to the current candidate and baseline'
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// F1: resolution authority.
|
|
804
|
+
checkCase('325-3a F1 ambiguous cited ID + updated declaration + valid waiver + passing execution: not pass, req-unresolved names the REQ (round 6)', () => {
|
|
805
|
+
const output = runReqSync(attestedReqSync({
|
|
806
|
+
citedReqs: [{ id: 'REQ-GRPH-001', file: PRD, resolution: 'ambiguous' }],
|
|
807
|
+
reqUpdates: [{ id: 'REQ-GRPH-001', file: PRD }]
|
|
808
|
+
}), {
|
|
809
|
+
executionRecords: [boundExec('exec-req')],
|
|
810
|
+
reviewRecords: [validWaiver('waiver-ambiguous', 'doc-change:requirement-sync', ['exec-req'])]
|
|
811
|
+
});
|
|
812
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
813
|
+
assert.ok(result, 'a result is reported');
|
|
814
|
+
assert.notStrictEqual(result.outcome, 'pass', 'an ambiguous REQ never passes, even with a single updated declaration');
|
|
815
|
+
const diag = output.diagnostics.find(item => item.code === 'req-unresolved');
|
|
816
|
+
assert.ok(diag && JSON.stringify(diag).includes('REQ-GRPH-001'), 'req-unresolved names the REQ id');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
checkCase('325-3a F1 malformed current PRD (analysis error) with complete attested inventory and no waiver: not a fail (round 6)', () => {
|
|
820
|
+
const output = runReqSync(attestedReqSync({
|
|
821
|
+
citedReqs: [{ id: 'REQ-GRPH-001', file: PRD, resolution: 'analysis-error' }]
|
|
822
|
+
}));
|
|
823
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail',
|
|
824
|
+
'an analysis error is unresolved, never an observed absence');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
checkCase('325-3a F1 candidate-deleted declaration (file from base fallback): not a fail (round 6)', () => {
|
|
828
|
+
const output = runReqSync(attestedReqSync({
|
|
829
|
+
citedReqs: [{ id: 'REQ-GRPH-001', file: PRD, resolution: 'missing' }]
|
|
830
|
+
}));
|
|
831
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail',
|
|
832
|
+
'a base-only declaration is not current authority');
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
checkCase('325-3a F1 control: uniquely resolved unchanged PRD + supported current waiver + passing execution: pass', () => {
|
|
836
|
+
const output = runReqSync(attestedReqSync({
|
|
837
|
+
citedReqs: [{ id: 'REQ-GRPH-001', file: PRD, resolution: 'resolved' }]
|
|
838
|
+
}), {
|
|
839
|
+
executionRecords: [boundExec('exec-req')],
|
|
840
|
+
reviewRecords: [validWaiver('waiver-resolved', 'doc-change:requirement-sync', ['exec-req'])]
|
|
841
|
+
});
|
|
842
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
843
|
+
assert.strictEqual(result.observation, 'observed');
|
|
844
|
+
assert.strictEqual(result.outcome, 'pass', 'a resolved REQ with a supported waiver passes');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// F2: the fail lane needs an attested envelope; every mismatch or missing field is none with req-sync-envelope-unattested.
|
|
848
|
+
checkCase('325-3a F2 empty citedReqs with attested envelope and the other fail conditions: not a fail (every() on [] is vacuous) (round 6)', () => {
|
|
849
|
+
const output = runReqSync(attestedReqSync({ citedReqs: [] }));
|
|
850
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'no cited REQ is not an observed absence');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
checkCase('325-3a F2 stale obligation base (envelope baseDigest differs from baseline): none, req-sync-envelope-unattested (round 6)', () => {
|
|
854
|
+
const output = runReqSync(attestedReqSync({
|
|
855
|
+
inventory: { ...ATTESTED_INVENTORY, baseDigest: sha256('previous base') }
|
|
856
|
+
}));
|
|
857
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'a stale base never establishes absence');
|
|
858
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-envelope-unattested'), 'the envelope mismatch is diagnosed');
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
checkCase('325-3a F2 absent baseline: not a fail, req-sync-envelope-unattested (round 6)', () => {
|
|
862
|
+
const output = run({
|
|
863
|
+
obligations: [attestedReqSync()],
|
|
864
|
+
artifacts: RS_ARTIFACTS,
|
|
865
|
+
baseline: undefined,
|
|
866
|
+
executionRecords: [],
|
|
867
|
+
reviewRecords: []
|
|
868
|
+
});
|
|
869
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'no baseline identity, no negative result');
|
|
870
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-envelope-unattested'), 'the missing baseline is diagnosed');
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
checkCase('325-3a F2 absent artifact identity for the PRD scope path: not a fail, req-sync-envelope-unattested (round 6)', () => {
|
|
874
|
+
const output = run({
|
|
875
|
+
obligations: [attestedReqSync()],
|
|
876
|
+
artifacts: { [TEST_PATH]: { digest: TEST_DIGEST } },
|
|
877
|
+
baseline: CURRENT_BASELINE,
|
|
878
|
+
executionRecords: [],
|
|
879
|
+
reviewRecords: []
|
|
880
|
+
});
|
|
881
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'no artifact identity, no negative result');
|
|
882
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-envelope-unattested'), 'the missing identity is diagnosed');
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
checkCase('325-3a F2 unobserved waiver inventory (waiverInventoryComplete false) with empty records: not a fail, req-sync-envelope-unattested (round 6)', () => {
|
|
886
|
+
const output = runReqSync(attestedReqSync({
|
|
887
|
+
inventory: { ...ATTESTED_INVENTORY, waiverInventoryComplete: false }
|
|
888
|
+
}));
|
|
889
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail',
|
|
890
|
+
'empty records are not an observed absence unless the waiver inventory is attested');
|
|
891
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-envelope-unattested'), 'the unobserved inventory is diagnosed');
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
checkCase('325-3a F2 missing configDigest: not a fail, req-sync-envelope-unattested (round 6)', () => {
|
|
895
|
+
const { configDigest, ...withoutConfig } = ATTESTED_INVENTORY;
|
|
896
|
+
const output = runReqSync(attestedReqSync({ inventory: withoutConfig }));
|
|
897
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'a missing config identity is unattested');
|
|
898
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-envelope-unattested'), 'the missing field is diagnosed');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
checkCase('325-3a F2 positive: fully attested current no-revision/no-waiver: observed/fail (round 6)', () => {
|
|
902
|
+
const output = runReqSync(attestedReqSync());
|
|
903
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
904
|
+
assert.strictEqual(result.observation, 'observed', 'the attested absence is an observed check');
|
|
905
|
+
assert.strictEqual(result.outcome, 'fail', 'the attested absence is an observed failure');
|
|
906
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-no-revision-or-waiver'), 'the failure is diagnosed');
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Round 8 (PR #334 review 2, Q2, Q3, Q4). Evidence expectations only.
|
|
910
|
+
|
|
911
|
+
// Q2: the absence fail is per cited REQ. A REQ that is neither revised nor covered by a valid waiver is missing.
|
|
912
|
+
const REQ_A = 'REQ-GRPH-001';
|
|
913
|
+
const REQ_B = 'REQ-GRPH-002';
|
|
914
|
+
const TWO_REQS = [{ id: REQ_A, file: PRD, resolution: 'resolved' }, { id: REQ_B, file: PRD, resolution: 'resolved' }];
|
|
915
|
+
|
|
916
|
+
checkCase('325-3a Q2 0 of 2 cited REQs revised, no waiver, attested: observed fail naming both REQs (round 8 control)', () => {
|
|
917
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [] }));
|
|
918
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
919
|
+
assert.strictEqual(result.outcome, 'fail', 'no cited REQ is revised or waived');
|
|
920
|
+
const diag = output.diagnostics.find(item => item.code === 'req-sync-no-revision-or-waiver');
|
|
921
|
+
assert.ok(diag && JSON.stringify(diag).includes(REQ_A) && JSON.stringify(diag).includes(REQ_B), 'both missing REQs are named');
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
checkCase('325-3a Q2 1 of 2 cited REQs revised, no waiver, attested: observed fail naming REQ-GRPH-002 (round 8)', () => {
|
|
925
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [{ id: REQ_A, file: PRD }] }));
|
|
926
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
927
|
+
assert.strictEqual(result.outcome, 'fail', 'REQ-GRPH-002 is neither revised nor waived');
|
|
928
|
+
const diag = output.diagnostics.find(item => item.code === 'req-sync-no-revision-or-waiver');
|
|
929
|
+
assert.ok(diag && JSON.stringify(diag).includes(REQ_B), 'the unrevised REQ is named');
|
|
930
|
+
assert.ok(diag && !JSON.stringify(diag).includes(REQ_A), 'the revised REQ is not named as missing');
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
checkCase('325-3a Q2 2 of 2 cited REQs revised with matching evidence: pass (round 8 control)', () => {
|
|
934
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [{ id: REQ_A, file: PRD }, { id: REQ_B, file: PRD }] }), {
|
|
935
|
+
executionRecords: [boundExec('exec-req')]
|
|
936
|
+
});
|
|
937
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'pass', 'every cited REQ is revised');
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
checkCase('325-3a Q2 1 of 2 cited REQs revised plus a valid covering waiver with matching evidence: pass (round 8 control)', () => {
|
|
941
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [{ id: REQ_A, file: PRD }] }), {
|
|
942
|
+
executionRecords: [boundExec('exec-req')],
|
|
943
|
+
reviewRecords: [validWaiver('waiver-covering', 'doc-change:requirement-sync', ['exec-req'], ['REQ-GRPH-002'])]
|
|
944
|
+
});
|
|
945
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'pass', 'a valid covering waiver satisfies every cited REQ');
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Q3: a new REQ-citing test that delegates to a helper reaches the fail lane through its trigger.
|
|
949
|
+
checkCase('325-3a Q3 new REQ-citing test delegating to a helper, complete envelope, no waiver: observed fail (round 8)', () => {
|
|
950
|
+
const output = runReqSync(attestedReqSync({ assertionChanged: false, requirementSyncTrigger: 'new-test' }));
|
|
951
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
952
|
+
assert.strictEqual(result.observation, 'observed', 'the unmet obligation is observed');
|
|
953
|
+
assert.strictEqual(result.outcome, 'fail', 'a new REQ-citing test with no revision or waiver fails');
|
|
954
|
+
assert.ok(output.diagnostics.some(item => item.code === 'req-sync-no-revision-or-waiver'), 'the failure is diagnosed');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
checkCase('325-3a Q3 same new helper-delegated test without the envelope: none, not fail (round 8 control)', () => {
|
|
958
|
+
const output = runReqSync(reqSyncObligation({ assertionChanged: false, requirementSyncTrigger: 'new-test' }));
|
|
959
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'the fail lane needs an attested envelope');
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// Q4: an unknown scope (attribution impossible) never fails, whatever else is attested.
|
|
963
|
+
checkCase('325-3a Q4 unknown scope (attribution impossible) with otherwise fail-shaped inputs: not fail (round 8)', () => {
|
|
964
|
+
const output = runReqSync(attestedReqSync({ applicability: 'unknown', citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }] }));
|
|
965
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'an unknown scope never establishes absence');
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// Round 9 (PR #334 review round 6). Evidence expectations only.
|
|
969
|
+
|
|
970
|
+
// P1: an unknown requirement scope never passes and never establishes absence, even with valid evidence for the known subset.
|
|
971
|
+
checkCase('325-3a P1 unknown requirement scope (requirementScopeUnknown true), attested, no evidence: not a fail, outcome none (round 9)', () => {
|
|
972
|
+
const output = runReqSync(attestedReqSync({ requirementScopeUnknown: true, citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }] }));
|
|
973
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none', 'an unknown scope yields none, never an absence fail');
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
checkCase('325-3a P1 unknown requirement scope (scope requirementIds unknown) with valid known-subset waiver and evidence: not pass, outcome none (round 9)', () => {
|
|
977
|
+
const output = runReqSync(attestedReqSync({
|
|
978
|
+
scope: { paths: [TEST_PATH, PRD], requirementIds: 'unknown' },
|
|
979
|
+
citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }]
|
|
980
|
+
}), {
|
|
981
|
+
executionRecords: [boundExec('exec-req')],
|
|
982
|
+
reviewRecords: [validWaiver('waiver-known', 'doc-change:requirement-sync', ['exec-req'])]
|
|
983
|
+
});
|
|
984
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
985
|
+
'valid evidence for the known subset does not pass an obligation whose scope is unknown');
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
checkCase('325-3a P1 unknown requirement scope with a revision of the known REQ and matching evidence: not pass, outcome none (round 9)', () => {
|
|
989
|
+
const output = runReqSync(attestedReqSync({
|
|
990
|
+
requirementScopeUnknown: true,
|
|
991
|
+
citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }],
|
|
992
|
+
reqUpdates: [{ id: REQ_A, file: PRD }]
|
|
993
|
+
}), {
|
|
994
|
+
executionRecords: [boundExec('exec-req')]
|
|
995
|
+
});
|
|
996
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
997
|
+
'a direct evidence input with an unknown scope is refused, even when the known REQ is revised');
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// P2: an explicit trigger is authoritative. Null and unknown never fail; a legacy absent field keeps the old behavior.
|
|
1001
|
+
checkCase('325-3a P2 explicit requirementSyncTrigger null with assertionChanged true, full attested fail-shaped inputs: not fail, outcome none (round 9)', () => {
|
|
1002
|
+
const output = runReqSync(attestedReqSync({ requirementSyncTrigger: null, assertionChanged: true }));
|
|
1003
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1004
|
+
'an explicit null trigger is not an active trigger');
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
checkCase('325-3a P2 explicit requirementSyncTrigger unknown with assertionChanged true, full attested fail-shaped inputs: not fail, outcome none (round 9)', () => {
|
|
1008
|
+
const output = runReqSync(attestedReqSync({ requirementSyncTrigger: 'unknown', assertionChanged: true }));
|
|
1009
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1010
|
+
'an unknown trigger never fails');
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
checkCase('325-3a P2 control: explicit assertion-change trigger, attested, no waiver: observed fail (round 9)', () => {
|
|
1014
|
+
const output = runReqSync(attestedReqSync({ requirementSyncTrigger: 'assertion-change', assertionChanged: true }));
|
|
1015
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'an active trigger still fails under the other conditions');
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
checkCase('325-3a P2 legacy absent-field positive: obligation without requirementSyncTrigger keeps the existing fail behavior (round 9)', () => {
|
|
1019
|
+
const output = runReqSync(attestedReqSync());
|
|
1020
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail',
|
|
1021
|
+
'the absent field is legacy input, and assertionChanged true may still fail');
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Round 10 (PR #334 review round 7, F1). An unknown requirement scope is refused before any outcome branch.
|
|
1025
|
+
function boundFailingExec(id) {
|
|
1026
|
+
return { ...boundExec(id), exitCode: 1, provenance: 'failing execution record bound to the current candidate and baseline' };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
checkCase('325-3a F1 unknown scope (requirementScopeUnknown true) with a bound failing execution: none, not observed fail (round 10)', () => {
|
|
1030
|
+
const output = runReqSync(attestedReqSync({ requirementScopeUnknown: true, citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }] }), {
|
|
1031
|
+
executionRecords: [boundFailingExec('exec-fail')]
|
|
1032
|
+
});
|
|
1033
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1034
|
+
'an unknown scope is refused before the failed-command branch');
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
checkCase('325-3a F1 unknown scope (scope requirementIds unknown) with a bound failing execution: none, not observed fail (round 10)', () => {
|
|
1038
|
+
const output = runReqSync(attestedReqSync({
|
|
1039
|
+
scope: { paths: [TEST_PATH, PRD], requirementIds: 'unknown' },
|
|
1040
|
+
citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }]
|
|
1041
|
+
}), {
|
|
1042
|
+
executionRecords: [boundFailingExec('exec-fail')]
|
|
1043
|
+
});
|
|
1044
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1045
|
+
'a scope marked unknown by requirementIds is refused before the failed-command branch');
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
checkCase('325-3a F1 unknown scope with contradictory current pass and fail records: none (round 10)', () => {
|
|
1049
|
+
const output = runReqSync(attestedReqSync({ requirementScopeUnknown: true, citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }] }), {
|
|
1050
|
+
executionRecords: [boundExec('exec-pass'), boundFailingExec('exec-fail')]
|
|
1051
|
+
});
|
|
1052
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1053
|
+
'contradictory records for an unknown scope yield none');
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
checkCase('325-3a F1 control: known scope with a bound failing execution: observed fail (round 10)', () => {
|
|
1057
|
+
const output = runReqSync(attestedReqSync({ requirementScopeUnknown: false, citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }] }), {
|
|
1058
|
+
executionRecords: [boundFailingExec('exec-fail')]
|
|
1059
|
+
});
|
|
1060
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail', 'a known scope keeps the failed-execution branch');
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
checkCase('325-3a F1 control: doc-lint with a bound failing execution (no unknown markers): observed fail, unchanged (round 10)', () => {
|
|
1064
|
+
const output = run({
|
|
1065
|
+
obligations: [obligation('doc-lint', 'applicable', [ADR])],
|
|
1066
|
+
artifacts: { [ADR]: { digest: ADR_DIGEST } },
|
|
1067
|
+
baseline: CURRENT_BASELINE,
|
|
1068
|
+
executionRecords: [executionRecord({ id: 'exec-lint-fail', obligationId: 'doc-lint', exitCode: 1 })],
|
|
1069
|
+
reviewRecords: []
|
|
1070
|
+
});
|
|
1071
|
+
const result = resultFor(output, 'doc-lint');
|
|
1072
|
+
assert.strictEqual(result.observation, 'observed');
|
|
1073
|
+
assert.strictEqual(result.outcome, 'fail', 'a non-requirement capability keeps observed fail on a bound failing execution');
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Round 12 (PR #334 review round 9, T3). A no-req-change waiver binds per requirement through requirementIds.
|
|
1077
|
+
checkCase('325-3a T3 A-only waiver with A revised and B unrevised: observed fail naming REQ-GRPH-002 only (round 12)', () => {
|
|
1078
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [{ id: REQ_A, file: PRD }] }), {
|
|
1079
|
+
executionRecords: [boundExec('exec-req')],
|
|
1080
|
+
reviewRecords: [validWaiver('waiver-a-only', 'doc-change:requirement-sync', ['exec-req'], [REQ_A])]
|
|
1081
|
+
});
|
|
1082
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
1083
|
+
assert.strictEqual(result.outcome, 'fail', 'a waiver that lists only REQ-GRPH-001 cannot cover REQ-GRPH-002');
|
|
1084
|
+
const diag = output.diagnostics.find(item => item.code === 'req-sync-no-revision-or-waiver');
|
|
1085
|
+
assert.ok(diag && JSON.stringify(diag).includes(REQ_B), 'REQ-GRPH-002 is named as unmet');
|
|
1086
|
+
assert.ok(diag && !JSON.stringify(diag).includes(REQ_A), 'the revised REQ-GRPH-001 is not named');
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
checkCase('325-3a T3 B-only waiver with A revised and matching evidence: pass (round 12 control)', () => {
|
|
1090
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [{ id: REQ_A, file: PRD }] }), {
|
|
1091
|
+
executionRecords: [boundExec('exec-req')],
|
|
1092
|
+
reviewRecords: [validWaiver('waiver-b-only', 'doc-change:requirement-sync', ['exec-req'], [REQ_B])]
|
|
1093
|
+
});
|
|
1094
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'pass', 'the waiver lists the one unrevised REQ');
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
checkCase('325-3a T3 waiver without requirementIds covers nothing: observed fail naming both unmet REQs (round 12)', () => {
|
|
1098
|
+
const legacy = validWaiver('waiver-legacy', 'doc-change:requirement-sync', ['exec-req']);
|
|
1099
|
+
delete legacy.requirementIds;
|
|
1100
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [] }), {
|
|
1101
|
+
executionRecords: [boundExec('exec-req')],
|
|
1102
|
+
reviewRecords: [legacy]
|
|
1103
|
+
});
|
|
1104
|
+
const result = resultFor(output, 'doc-change:requirement-sync');
|
|
1105
|
+
assert.strictEqual(result.outcome, 'fail', 'an unsupported waiver with no requirementIds satisfies nothing');
|
|
1106
|
+
const diag = output.diagnostics.find(item => item.code === 'req-sync-no-revision-or-waiver');
|
|
1107
|
+
assert.ok(diag && JSON.stringify(diag).includes(REQ_A) && JSON.stringify(diag).includes(REQ_B), 'both unmet REQs are named');
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
checkCase('325-3a T3 control: both cited REQs revised, no waiver, matching evidence: pass (round 12)', () => {
|
|
1111
|
+
const output = runReqSync(attestedReqSync({ citedReqs: TWO_REQS, reqUpdates: [{ id: REQ_A, file: PRD }, { id: REQ_B, file: PRD }] }), {
|
|
1112
|
+
executionRecords: [boundExec('exec-req')]
|
|
1113
|
+
});
|
|
1114
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'pass', 'every cited REQ is revised');
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// Round 13 (PR #334 review round 10). Evidence expectations only.
|
|
1118
|
+
|
|
1119
|
+
// S2: a mutated REQ is unresolved before any waiver or revision is considered, and is never an absence fail.
|
|
1120
|
+
checkCase('325-3a S2 mutated cited REQ with a fully valid per-REQ waiver and matching execution: none, not pass (round 13)', () => {
|
|
1121
|
+
const output = runReqSync(attestedReqSync({ citedReqs: [{ id: REQ_A, file: PRD, resolution: 'mutated' }] }), {
|
|
1122
|
+
executionRecords: [boundExec('exec-req')],
|
|
1123
|
+
reviewRecords: [validWaiver('waiver-mutated', 'doc-change:requirement-sync', ['exec-req'], [REQ_A])]
|
|
1124
|
+
});
|
|
1125
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1126
|
+
'an in-place mutation is refused before the waiver is considered');
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
checkCase('325-3a S2 mutated cited REQ with a complete attested envelope and no records: not an absence fail (round 13)', () => {
|
|
1130
|
+
const output = runReqSync(attestedReqSync({ citedReqs: [{ id: REQ_A, file: PRD, resolution: 'mutated' }] }));
|
|
1131
|
+
assert.notStrictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'fail',
|
|
1132
|
+
'a mutation is not an observed absence of a requirement');
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// S3: an unsupported waiver or missing identity data never passes, and is never an absence fail with unknown identity.
|
|
1136
|
+
checkCase('325-3a S3 H3-shaped waiver without requirementIds and without citedReqs or reqUpdates: none (round 13)', () => {
|
|
1137
|
+
const legacy = validWaiver('waiver-no-ids', 'requirement-sync', ['exec-req']);
|
|
1138
|
+
delete legacy.requirementIds;
|
|
1139
|
+
const output = run({
|
|
1140
|
+
obligations: [obligation('requirement-sync', 'applicable', [PRD])],
|
|
1141
|
+
artifacts: { [PRD]: { digest: PRD_DIGEST } },
|
|
1142
|
+
baseline: CURRENT_BASELINE,
|
|
1143
|
+
executionRecords: [executionRecord({ id: 'exec-req', obligationId: 'requirement-sync', paths: [PRD], artifactDigests: { [PRD]: PRD_DIGEST } })],
|
|
1144
|
+
reviewRecords: [legacy]
|
|
1145
|
+
});
|
|
1146
|
+
assert.strictEqual(resultFor(output, 'requirement-sync').outcome, 'none',
|
|
1147
|
+
'a waiver with no identity list and no identity data cannot satisfy anything');
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
checkCase('325-3a S3 empty cited list with a valid-looking waiver and matching evidence: none (round 13)', () => {
|
|
1151
|
+
const output = runReqSync(attestedReqSync({ citedReqs: [], reqUpdates: [] }), {
|
|
1152
|
+
executionRecords: [boundExec('exec-req')],
|
|
1153
|
+
reviewRecords: [validWaiver('waiver-empty-cited', 'doc-change:requirement-sync', ['exec-req'], [REQ_A])]
|
|
1154
|
+
});
|
|
1155
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1156
|
+
'an empty cited list gives no per-REQ coverage claim');
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
checkCase('325-3a S3 absent reqUpdates with a valid-looking waiver and matching evidence: none, unknown update data (round 13)', () => {
|
|
1160
|
+
const output = runReqSync(reqSyncObligation({
|
|
1161
|
+
inventory: ATTESTED_INVENTORY,
|
|
1162
|
+
citedReqs: [{ id: REQ_A, file: PRD, resolution: 'resolved' }],
|
|
1163
|
+
reqUpdates: undefined
|
|
1164
|
+
}), {
|
|
1165
|
+
executionRecords: [boundExec('exec-req')],
|
|
1166
|
+
reviewRecords: [validWaiver('waiver-no-updates', 'doc-change:requirement-sync', ['exec-req'], [REQ_A])]
|
|
1167
|
+
});
|
|
1168
|
+
assert.strictEqual(resultFor(output, 'doc-change:requirement-sync').outcome, 'none',
|
|
1169
|
+
'absent update data makes the obligation unknown: no pass and no absence fail');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
helper.finish();
|