harness-everything 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -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-pr334-haiku.test.js +298 -0
- package/ci/mechanism-pr334-luna.test.js +192 -0
- package/ci/mechanism-pr334-opencode.test.js +313 -0
- package/ci/mechanism-pr334-r2-boundary.test.js +239 -0
- package/ci/mechanism-pr334-r2-resolution.test.js +243 -0
- package/ci/mechanism-pr334-r2-scope.test.js +127 -0
- package/ci/mechanism-pr334-r3-boundary.test.js +356 -0
- package/ci/mechanism-pr334-r3-citations.test.js +205 -0
- package/ci/mechanism-pr334-r3-headings.test.js +268 -0
- package/docs/document-lifecycle.md +57 -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/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +471 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +1571 -0
- package/sbom.cdx.json +7 -7
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const lifecycle = require('./doc-lifecycle');
|
|
4
|
+
|
|
5
|
+
const CATALOG = Object.freeze([
|
|
6
|
+
{ capability: 'doc-lint', strength: 'MUST', scope: 'Changed governed documents', acceptance: 'Run document lifecycle lint for every changed governed document.' },
|
|
7
|
+
{ capability: 'link-check', strength: 'MUST', scope: 'Changed governed documents', acceptance: 'Check in-repository links for every changed governed document.' },
|
|
8
|
+
{ capability: 'update-over-create', strength: 'SHOULD', scope: 'New governed documents', acceptance: 'Confirm an existing document could not be updated instead.' },
|
|
9
|
+
{ capability: 'outline-confirmation', strength: 'SHOULD', scope: 'New governed documents', acceptance: 'Confirm the new document outline before drafting.' },
|
|
10
|
+
{ capability: 'derived-regenerate', strength: 'MUST', scope: 'Affected declared derived views', acceptance: 'Regenerate the affected view and confirm it matches its source.' },
|
|
11
|
+
{ capability: 'quality-review', strength: 'MUST', scope: 'Normative document changes', acceptance: 'Review changes to normative document fields and content.' },
|
|
12
|
+
{ capability: 'conflict-check', strength: 'MUST', scope: 'Normative document changes', acceptance: 'Check normative changes for conflicts with current project documents.' },
|
|
13
|
+
{ capability: 'prose-reminder', strength: 'SHOULD', scope: 'Changed governed reference content', acceptance: 'Review the prose-to-structure reminder for reference content.' },
|
|
14
|
+
{ capability: 'requirement-sync', strength: 'MUST', scope: 'Code changes or changed REQ-citing tests', acceptance: 'Evaluate whether affected requirements need an update or supported no-change record.' }
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const CODE_EXTENSION = /\.(?:c|cc|cpp|h|hpp|m|mm|go|rs|java|kt|swift|js|jsx|mjs|cjs|ts|tsx|py|rb|php|sh|ps1|sql)$/i;
|
|
18
|
+
const TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|spec)(?:\/|$)|(?:^|\/)[^/]*(?:\.test|\.spec)\.[^/]+$/i;
|
|
19
|
+
const REQUIREMENT_ID = /\bREQ-[A-Z][A-Z0-9]{1,9}-\d{3,}\b/;
|
|
20
|
+
const REQUIREMENT_IDS = /\b(REQ-[A-Z][A-Z0-9]{1,9}-\d{3,})\b/g;
|
|
21
|
+
const MAX_LINE_DIFF_CELLS = 2_000_000;
|
|
22
|
+
const DOCUMENT_NORMATIVE_FIELDS = ['status', 'supersedes'];
|
|
23
|
+
const INVENTORY_ENVELOPE_FIELDS = [
|
|
24
|
+
'complete',
|
|
25
|
+
'waiverInventoryComplete',
|
|
26
|
+
'baseDigest',
|
|
27
|
+
'candidateDigest',
|
|
28
|
+
'configDigest'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function compareText(left, right) {
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizePath(value) {
|
|
36
|
+
return String(value).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function snapshotFiles(snapshot) {
|
|
40
|
+
const source = snapshot && snapshot.files && typeof snapshot.files === 'object' ? snapshot.files : {};
|
|
41
|
+
const files = {};
|
|
42
|
+
for (const path of Object.keys(source).sort(compareText)) {
|
|
43
|
+
if (typeof source[path] === 'string') files[normalizePath(path)] = source[path];
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isSnapshotInput(snapshot) {
|
|
49
|
+
return Boolean(snapshot && snapshot.files && typeof snapshot.files === 'object' && !Array.isArray(snapshot.files)
|
|
50
|
+
&& Object.values(snapshot.files).every(text => typeof text === 'string'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isConfigInput(config) {
|
|
54
|
+
return Boolean(config && typeof config === 'object' && !Array.isArray(config));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function copyInventoryEnvelope(inventory) {
|
|
58
|
+
if (!inventory || typeof inventory !== 'object' || Array.isArray(inventory)) return undefined;
|
|
59
|
+
const envelope = {};
|
|
60
|
+
for (const field of INVENTORY_ENVELOPE_FIELDS) {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(inventory, field)) envelope[field] = inventory[field];
|
|
62
|
+
}
|
|
63
|
+
return envelope;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function globToRegExp(glob) {
|
|
67
|
+
const source = normalizePath(glob);
|
|
68
|
+
let pattern = '^';
|
|
69
|
+
for (let index = 0; index < source.length; index++) {
|
|
70
|
+
const char = source[index];
|
|
71
|
+
if (char === '*' && source[index + 1] === '*') {
|
|
72
|
+
if (source[index + 2] === '/') {
|
|
73
|
+
pattern += '(?:.*/)?';
|
|
74
|
+
index += 2;
|
|
75
|
+
} else {
|
|
76
|
+
pattern += '.*';
|
|
77
|
+
index++;
|
|
78
|
+
}
|
|
79
|
+
} else if (char === '*') {
|
|
80
|
+
pattern += '[^/]*';
|
|
81
|
+
} else {
|
|
82
|
+
pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return new RegExp(`${pattern}$`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function matchesAny(path, globs) {
|
|
89
|
+
return Array.isArray(globs) && globs.some(glob => typeof glob === 'string' && globToRegExp(glob).test(path));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function governedEntry(path, config) {
|
|
93
|
+
const entries = Array.isArray(config && config.governed) ? config.governed : [];
|
|
94
|
+
return entries.find(entry => entry && typeof entry.path === 'string' && globToRegExp(entry.path).test(path)) || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parsedDocument(path, text, config) {
|
|
98
|
+
const parsed = lifecycle.parseFrontmatter(text);
|
|
99
|
+
const entry = governedEntry(path, config);
|
|
100
|
+
const legacy = matchesAny(path, config && config.legacy);
|
|
101
|
+
return {
|
|
102
|
+
path,
|
|
103
|
+
role: entry && typeof entry.role === 'string' ? entry.role : parsed.data && parsed.data.role || null,
|
|
104
|
+
legacy,
|
|
105
|
+
frontmatter: parsed.data,
|
|
106
|
+
body: parsed.body,
|
|
107
|
+
error: parsed.error,
|
|
108
|
+
storedBytes: Buffer.from(text, 'utf8')
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function analyzeSnapshotWithLifecycle(files, config) {
|
|
113
|
+
const docs = Object.keys(files).sort(compareText).map(path => parsedDocument(path, files[path], config));
|
|
114
|
+
const analysis = lifecycle.analyzeDocs(docs, config, { all: true });
|
|
115
|
+
return {
|
|
116
|
+
docs,
|
|
117
|
+
analysis,
|
|
118
|
+
summary: { diagnostics: analysis.diagnostics, evidence: analysis.evidence }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function analyzeSnapshot(files, config) {
|
|
123
|
+
return analyzeSnapshotWithLifecycle(files, config).summary;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findRenames(base, candidate) {
|
|
127
|
+
const oldOnly = Object.keys(base).filter(path => !Object.prototype.hasOwnProperty.call(candidate, path)).sort(compareText);
|
|
128
|
+
const newOnly = Object.keys(candidate).filter(path => !Object.prototype.hasOwnProperty.call(base, path)).sort(compareText);
|
|
129
|
+
const destinations = new Map();
|
|
130
|
+
for (const path of newOnly) {
|
|
131
|
+
const text = candidate[path];
|
|
132
|
+
if (!destinations.has(text)) destinations.set(text, []);
|
|
133
|
+
destinations.get(text).push(path);
|
|
134
|
+
}
|
|
135
|
+
const moves = [];
|
|
136
|
+
const consumedNew = new Set();
|
|
137
|
+
const consumedOld = new Set();
|
|
138
|
+
for (const from of oldOnly) {
|
|
139
|
+
const matches = destinations.get(base[from]);
|
|
140
|
+
const to = matches && matches.find(path => !consumedNew.has(path));
|
|
141
|
+
if (!to) continue;
|
|
142
|
+
moves.push({ kind: 'rename', from, to, baseText: base[from], candidateText: candidate[to] });
|
|
143
|
+
consumedOld.add(from);
|
|
144
|
+
consumedNew.add(to);
|
|
145
|
+
}
|
|
146
|
+
const changes = [...moves];
|
|
147
|
+
for (const path of Object.keys(base).sort(compareText)) {
|
|
148
|
+
if (Object.prototype.hasOwnProperty.call(candidate, path) && base[path] !== candidate[path]) {
|
|
149
|
+
changes.push({ kind: 'modified', path, baseText: base[path], candidateText: candidate[path] });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const path of oldOnly) {
|
|
153
|
+
if (!consumedOld.has(path)) changes.push({ kind: 'deleted', path, baseText: base[path] });
|
|
154
|
+
}
|
|
155
|
+
for (const path of newOnly) {
|
|
156
|
+
if (!consumedNew.has(path)) changes.push({ kind: 'added', path, candidateText: candidate[path] });
|
|
157
|
+
}
|
|
158
|
+
return changes.sort((left, right) => compareText(left.to || left.path || left.from, right.to || right.path || right.from)
|
|
159
|
+
|| compareText(left.kind, right.kind));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function changePaths(change) {
|
|
163
|
+
return change.kind === 'rename' ? [change.from, change.to] : [change.path];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function uniqueSorted(values) {
|
|
167
|
+
return [...new Set(values.filter(value => typeof value === 'string'))].sort(compareText);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const REGEX_PREFIX_KEYWORDS = new Set([
|
|
171
|
+
'await', 'case', 'delete', 'do', 'else', 'in', 'instanceof', 'new', 'of', 'return', 'throw',
|
|
172
|
+
'typeof', 'void', 'yield'
|
|
173
|
+
]);
|
|
174
|
+
const MULTI_CHAR_TOKENS = [
|
|
175
|
+
'>>>=', '===', '!==', '**=', '&&=', '||=', '??=', '>>>', '<<=', '>>=', '...', '=>', '==', '!=',
|
|
176
|
+
'<=', '>=', '++', '--', '**', '&&', '||', '??', '?.', '+=', '-=', '*=', '/=', '%=', '&=', '|=',
|
|
177
|
+
'^=', '<<', '>>'
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
function tokenizeTestSource(text) {
|
|
181
|
+
const source = String(text || '');
|
|
182
|
+
const tokens = [];
|
|
183
|
+
const comments = [];
|
|
184
|
+
let index = 0;
|
|
185
|
+
let line = 0;
|
|
186
|
+
let canStartRegex = true;
|
|
187
|
+
let valid = true;
|
|
188
|
+
|
|
189
|
+
const push = (type, value, tokenLine = line) => tokens.push({ type, value, line: tokenLine, index: tokens.length });
|
|
190
|
+
const bumpLine = char => {
|
|
191
|
+
if (char === '\n' || (char === '\r' && source[index + 1] !== '\n')) line++;
|
|
192
|
+
};
|
|
193
|
+
const skipEscapedChar = () => {
|
|
194
|
+
const escaped = source[index + 1];
|
|
195
|
+
if (escaped === '\r' && source[index + 2] === '\n') {
|
|
196
|
+
line++;
|
|
197
|
+
index += 3;
|
|
198
|
+
} else {
|
|
199
|
+
bumpLine(escaped);
|
|
200
|
+
index += 2;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const isIdentifierStart = char => Boolean(char) && /[A-Za-z_$]/.test(char);
|
|
204
|
+
const isIdentifierPart = char => Boolean(char) && /[A-Za-z0-9_$]/.test(char);
|
|
205
|
+
|
|
206
|
+
function addPunctuator(value, tokenLine) {
|
|
207
|
+
push('punctuator', value, tokenLine);
|
|
208
|
+
if (value === ')' || value === ']' || value === '}' || value === '.' || value === '?.' || value === '++' || value === '--') {
|
|
209
|
+
canStartRegex = false;
|
|
210
|
+
} else {
|
|
211
|
+
canStartRegex = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function scanString(quote) {
|
|
216
|
+
const start = index;
|
|
217
|
+
const startLine = line;
|
|
218
|
+
index++;
|
|
219
|
+
while (index < source.length) {
|
|
220
|
+
const char = source[index];
|
|
221
|
+
if (char === '\\') {
|
|
222
|
+
if (index + 1 >= source.length) return false;
|
|
223
|
+
skipEscapedChar();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === quote) {
|
|
227
|
+
index++;
|
|
228
|
+
push('string', source.slice(start, index), startLine);
|
|
229
|
+
canStartRegex = false;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (char === '\n' || char === '\r') return false;
|
|
233
|
+
index++;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function scanRegex() {
|
|
239
|
+
const start = index;
|
|
240
|
+
const startLine = line;
|
|
241
|
+
index++;
|
|
242
|
+
let inCharacterClass = false;
|
|
243
|
+
while (index < source.length) {
|
|
244
|
+
const char = source[index];
|
|
245
|
+
if (char === '\\') {
|
|
246
|
+
if (index + 1 >= source.length || source[index + 1] === '\n' || source[index + 1] === '\r') return false;
|
|
247
|
+
index += 2;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (char === '[') inCharacterClass = true;
|
|
251
|
+
else if (char === ']') inCharacterClass = false;
|
|
252
|
+
else if (char === '/' && !inCharacterClass) {
|
|
253
|
+
index++;
|
|
254
|
+
while (isIdentifierPart(source[index])) index++;
|
|
255
|
+
push('regex', source.slice(start, index), startLine);
|
|
256
|
+
canStartRegex = false;
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (char === '\n' || char === '\r') return false;
|
|
260
|
+
index++;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function scanTemplate() {
|
|
266
|
+
index++;
|
|
267
|
+
let chunkStart = index;
|
|
268
|
+
let chunkLine = line;
|
|
269
|
+
while (index < source.length) {
|
|
270
|
+
const char = source[index];
|
|
271
|
+
if (char === '\\') {
|
|
272
|
+
if (index + 1 >= source.length) return false;
|
|
273
|
+
skipEscapedChar();
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (char === '`') {
|
|
277
|
+
push('template', source.slice(chunkStart, index), chunkLine);
|
|
278
|
+
index++;
|
|
279
|
+
canStartRegex = false;
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (char === '$' && source[index + 1] === '{') {
|
|
283
|
+
push('template', source.slice(chunkStart, index), chunkLine);
|
|
284
|
+
const markerLine = line;
|
|
285
|
+
index += 2;
|
|
286
|
+
push('template-expression', '${', markerLine);
|
|
287
|
+
canStartRegex = true;
|
|
288
|
+
if (!scanCode(true)) return false;
|
|
289
|
+
chunkStart = index;
|
|
290
|
+
chunkLine = line;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
bumpLine(char);
|
|
294
|
+
index++;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scanCode(stopAtTemplateBrace = false) {
|
|
300
|
+
let braceDepth = 0;
|
|
301
|
+
let parenDepth = 0;
|
|
302
|
+
let bracketDepth = 0;
|
|
303
|
+
while (index < source.length) {
|
|
304
|
+
const char = source[index];
|
|
305
|
+
if (/\s/.test(char)) {
|
|
306
|
+
bumpLine(char);
|
|
307
|
+
index++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (char === '/' && source[index + 1] === '/') {
|
|
312
|
+
const start = index;
|
|
313
|
+
const commentLine = line;
|
|
314
|
+
const topLevel = !stopAtTemplateBrace && braceDepth === 0 && parenDepth === 0 && bracketDepth === 0;
|
|
315
|
+
index += 2;
|
|
316
|
+
const contentStart = index;
|
|
317
|
+
while (index < source.length && source[index] !== '\n' && source[index] !== '\r') index++;
|
|
318
|
+
comments.push({
|
|
319
|
+
kind: 'line', marker: '//', content: source.slice(contentStart, index),
|
|
320
|
+
start, end: index, line: commentLine, topLevel
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (char === '/' && source[index + 1] === '*') {
|
|
325
|
+
const start = index;
|
|
326
|
+
const commentLine = line;
|
|
327
|
+
const topLevel = !stopAtTemplateBrace && braceDepth === 0 && parenDepth === 0 && bracketDepth === 0;
|
|
328
|
+
index += 2;
|
|
329
|
+
const contentStart = index;
|
|
330
|
+
let closed = false;
|
|
331
|
+
while (index < source.length) {
|
|
332
|
+
if (source[index] === '*' && source[index + 1] === '/') {
|
|
333
|
+
const contentEnd = index;
|
|
334
|
+
index += 2;
|
|
335
|
+
closed = true;
|
|
336
|
+
comments.push({
|
|
337
|
+
kind: 'block', marker: '/*', content: source.slice(contentStart, contentEnd),
|
|
338
|
+
start, end: index, line: commentLine, topLevel
|
|
339
|
+
});
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
bumpLine(source[index]);
|
|
343
|
+
index++;
|
|
344
|
+
}
|
|
345
|
+
if (!closed) return false;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
// Preserve Python-style whole-line comments for the test paths this helper also accepts.
|
|
349
|
+
if (char === '#' && source.slice(0, index).split(/\r?\n/).pop().trim() === '') {
|
|
350
|
+
const start = index;
|
|
351
|
+
const commentLine = line;
|
|
352
|
+
const topLevel = !stopAtTemplateBrace && braceDepth === 0 && parenDepth === 0 && bracketDepth === 0;
|
|
353
|
+
index++;
|
|
354
|
+
const contentStart = index;
|
|
355
|
+
while (index < source.length && source[index] !== '\n' && source[index] !== '\r') index++;
|
|
356
|
+
comments.push({
|
|
357
|
+
kind: 'line', marker: '#', content: source.slice(contentStart, index),
|
|
358
|
+
start, end: index, line: commentLine, topLevel
|
|
359
|
+
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (stopAtTemplateBrace && char === '}' && braceDepth === 0) {
|
|
364
|
+
addPunctuator('}', line);
|
|
365
|
+
index++;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
if (char === '{') {
|
|
369
|
+
addPunctuator(char, line);
|
|
370
|
+
braceDepth++;
|
|
371
|
+
index++;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (char === '}' && braceDepth > 0) {
|
|
375
|
+
addPunctuator(char, line);
|
|
376
|
+
braceDepth--;
|
|
377
|
+
index++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (char === '(') {
|
|
381
|
+
addPunctuator(char, line);
|
|
382
|
+
parenDepth++;
|
|
383
|
+
index++;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (char === ')') {
|
|
387
|
+
if (parenDepth > 0) parenDepth--;
|
|
388
|
+
addPunctuator(char, line);
|
|
389
|
+
index++;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (char === '[') {
|
|
393
|
+
addPunctuator(char, line);
|
|
394
|
+
bracketDepth++;
|
|
395
|
+
index++;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (char === ']') {
|
|
399
|
+
if (bracketDepth > 0) bracketDepth--;
|
|
400
|
+
addPunctuator(char, line);
|
|
401
|
+
index++;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (char === "'" || char === '"') {
|
|
405
|
+
if (!scanString(char)) return false;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (char === '`') {
|
|
409
|
+
if (!scanTemplate()) return false;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (char === '/' && canStartRegex) {
|
|
413
|
+
if (!scanRegex()) return false;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (isIdentifierStart(char)) {
|
|
417
|
+
const start = index;
|
|
418
|
+
const startLine = line;
|
|
419
|
+
index++;
|
|
420
|
+
while (isIdentifierPart(source[index])) index++;
|
|
421
|
+
const value = source.slice(start, index);
|
|
422
|
+
push('identifier', value, startLine);
|
|
423
|
+
canStartRegex = REGEX_PREFIX_KEYWORDS.has(value);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (/[0-9]/.test(char)) {
|
|
427
|
+
const start = index;
|
|
428
|
+
const startLine = line;
|
|
429
|
+
index++;
|
|
430
|
+
while (/[A-Za-z0-9_$\.]/.test(source[index] || '')) index++;
|
|
431
|
+
push('number', source.slice(start, index), startLine);
|
|
432
|
+
canStartRegex = false;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const tokenLine = line;
|
|
437
|
+
const operator = MULTI_CHAR_TOKENS.find(value => source.startsWith(value, index));
|
|
438
|
+
const value = operator || char;
|
|
439
|
+
index += value.length;
|
|
440
|
+
addPunctuator(value, tokenLine);
|
|
441
|
+
}
|
|
442
|
+
return !stopAtTemplateBrace;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
valid = scanCode();
|
|
446
|
+
return { tokens, comments, valid };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function tokenKey(token) {
|
|
450
|
+
return `${token.type}\u0000${token.value}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function changedTokens(beforeTokens, afterTokens) {
|
|
454
|
+
let prefix = 0;
|
|
455
|
+
while (prefix < beforeTokens.length && prefix < afterTokens.length
|
|
456
|
+
&& tokenKey(beforeTokens[prefix]) === tokenKey(afterTokens[prefix])) prefix++;
|
|
457
|
+
let beforeEnd = beforeTokens.length;
|
|
458
|
+
let afterEnd = afterTokens.length;
|
|
459
|
+
while (beforeEnd > prefix && afterEnd > prefix
|
|
460
|
+
&& tokenKey(beforeTokens[beforeEnd - 1]) === tokenKey(afterTokens[afterEnd - 1])) {
|
|
461
|
+
beforeEnd--;
|
|
462
|
+
afterEnd--;
|
|
463
|
+
}
|
|
464
|
+
const removedCount = beforeEnd - prefix;
|
|
465
|
+
const addedCount = afterEnd - prefix;
|
|
466
|
+
const width = addedCount + 1;
|
|
467
|
+
if ((removedCount + 1) * width > MAX_LINE_DIFF_CELLS) return null;
|
|
468
|
+
|
|
469
|
+
const table = new Uint32Array((removedCount + 1) * width);
|
|
470
|
+
for (let oldIndex = removedCount - 1; oldIndex >= 0; oldIndex--) {
|
|
471
|
+
for (let newIndex = addedCount - 1; newIndex >= 0; newIndex--) {
|
|
472
|
+
const cell = oldIndex * width + newIndex;
|
|
473
|
+
table[cell] = tokenKey(beforeTokens[prefix + oldIndex]) === tokenKey(afterTokens[prefix + newIndex])
|
|
474
|
+
? table[(oldIndex + 1) * width + newIndex + 1] + 1
|
|
475
|
+
: Math.max(table[(oldIndex + 1) * width + newIndex], table[cell + 1]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const removed = [];
|
|
480
|
+
const added = [];
|
|
481
|
+
let oldIndex = 0;
|
|
482
|
+
let newIndex = 0;
|
|
483
|
+
while (oldIndex < removedCount && newIndex < addedCount) {
|
|
484
|
+
if (tokenKey(beforeTokens[prefix + oldIndex]) === tokenKey(afterTokens[prefix + newIndex])) {
|
|
485
|
+
oldIndex++;
|
|
486
|
+
newIndex++;
|
|
487
|
+
} else if (table[(oldIndex + 1) * width + newIndex] >= table[oldIndex * width + newIndex + 1]) {
|
|
488
|
+
removed.push(beforeTokens[prefix + oldIndex++]);
|
|
489
|
+
} else {
|
|
490
|
+
added.push(afterTokens[prefix + newIndex++]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
while (oldIndex < removedCount) removed.push(beforeTokens[prefix + oldIndex++]);
|
|
494
|
+
while (newIndex < addedCount) added.push(afterTokens[prefix + newIndex++]);
|
|
495
|
+
return { removed, added };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function snapshotText(snapshot, path) {
|
|
499
|
+
const files = snapshot && snapshot.files && typeof snapshot.files === 'object' ? snapshot.files : {};
|
|
500
|
+
const key = Object.keys(files).filter(candidate => normalizePath(candidate) === path).pop();
|
|
501
|
+
if (key === undefined) return { readable: true, text: undefined };
|
|
502
|
+
return typeof files[key] === 'string' ? { readable: true, text: files[key] } : { readable: false, text: undefined };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Build just enough structure to map changed tokens to statements and their
|
|
506
|
+
// nearest test()/it() call. Parentheses inside comments and literals are opaque
|
|
507
|
+
// tokens in this scan, so they cannot change the depth.
|
|
508
|
+
function scanTestSource(text) {
|
|
509
|
+
const lexical = tokenizeTestSource(text);
|
|
510
|
+
const tokens = lexical.tokens;
|
|
511
|
+
const parenDepthBefore = new Array(tokens.length);
|
|
512
|
+
const parenDepthAfter = new Array(tokens.length);
|
|
513
|
+
const braceDepthBefore = new Array(tokens.length);
|
|
514
|
+
const braceDepthAfter = new Array(tokens.length);
|
|
515
|
+
const parenPairs = new Map();
|
|
516
|
+
const openParens = [];
|
|
517
|
+
let braceDepth = 0;
|
|
518
|
+
let valid = lexical.valid;
|
|
519
|
+
|
|
520
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
521
|
+
const token = tokens[index];
|
|
522
|
+
parenDepthBefore[index] = openParens.length;
|
|
523
|
+
braceDepthBefore[index] = braceDepth;
|
|
524
|
+
if (token.type === 'template-expression' && token.value === '${') braceDepth++;
|
|
525
|
+
else if (token.type === 'punctuator') {
|
|
526
|
+
if (token.value === '(') openParens.push(index);
|
|
527
|
+
else if (token.value === ')') {
|
|
528
|
+
const openIndex = openParens.pop();
|
|
529
|
+
if (openIndex === undefined) valid = false;
|
|
530
|
+
else parenPairs.set(openIndex, index);
|
|
531
|
+
} else if (token.value === '{') braceDepth++;
|
|
532
|
+
else if (token.value === '}') {
|
|
533
|
+
braceDepth--;
|
|
534
|
+
if (braceDepth < 0) valid = false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
parenDepthAfter[index] = openParens.length;
|
|
538
|
+
braceDepthAfter[index] = braceDepth;
|
|
539
|
+
}
|
|
540
|
+
if (openParens.length || braceDepth !== 0) valid = false;
|
|
541
|
+
|
|
542
|
+
const scan = {
|
|
543
|
+
tokens,
|
|
544
|
+
comments: lexical.comments,
|
|
545
|
+
valid,
|
|
546
|
+
parenDepthBefore,
|
|
547
|
+
parenDepthAfter,
|
|
548
|
+
braceDepthBefore,
|
|
549
|
+
braceDepthAfter,
|
|
550
|
+
parenPairs,
|
|
551
|
+
calls: []
|
|
552
|
+
};
|
|
553
|
+
for (let index = 0; index < tokens.length - 1; index++) {
|
|
554
|
+
const token = tokens[index];
|
|
555
|
+
if (token.type !== 'identifier' || !['test', 'it'].includes(token.value)
|
|
556
|
+
|| tokens[index + 1].value !== '(') continue;
|
|
557
|
+
const openIndex = index + 1;
|
|
558
|
+
const closeIndex = parenPairs.get(openIndex);
|
|
559
|
+
const endIndex = Number.isInteger(closeIndex) ? closeIndex : tokens.length - 1;
|
|
560
|
+
const callArgumentDepth = parenDepthBefore[openIndex] + 1;
|
|
561
|
+
const callBraceDepth = braceDepthBefore[openIndex];
|
|
562
|
+
const ids = new Set();
|
|
563
|
+
for (let cursor = openIndex + 1; cursor <= endIndex; cursor++) {
|
|
564
|
+
const candidate = tokens[cursor];
|
|
565
|
+
if (!['string', 'template'].includes(candidate.type)
|
|
566
|
+
|| parenDepthBefore[cursor] !== callArgumentDepth
|
|
567
|
+
|| braceDepthBefore[cursor] !== callBraceDepth) continue;
|
|
568
|
+
for (const match of candidate.value.matchAll(REQUIREMENT_IDS)) ids.add(match[1]);
|
|
569
|
+
}
|
|
570
|
+
const signatureTokens = [];
|
|
571
|
+
let titleSeparator = -1;
|
|
572
|
+
for (let cursor = openIndex + 1; cursor <= endIndex; cursor++) {
|
|
573
|
+
if (tokens[cursor].value === ',' && parenDepthBefore[cursor] === callArgumentDepth
|
|
574
|
+
&& braceDepthBefore[cursor] === callBraceDepth) {
|
|
575
|
+
titleSeparator = cursor;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
signatureTokens.push(tokenKey(tokens[cursor]));
|
|
579
|
+
}
|
|
580
|
+
const bodyStartIndex = titleSeparator < 0 ? endIndex : titleSeparator + 1;
|
|
581
|
+
const bodyEndIndex = Number.isInteger(closeIndex) ? closeIndex - 1 : endIndex;
|
|
582
|
+
const bodySignature = tokens.slice(bodyStartIndex, bodyEndIndex + 1).map(tokenKey).join('\u0001');
|
|
583
|
+
scan.calls.push({
|
|
584
|
+
name: token.value,
|
|
585
|
+
startIndex: index,
|
|
586
|
+
openIndex,
|
|
587
|
+
closeIndex,
|
|
588
|
+
endIndex,
|
|
589
|
+
titleStartIndex: openIndex + 1,
|
|
590
|
+
titleEndIndex: titleSeparator < 0 ? bodyEndIndex : titleSeparator - 1,
|
|
591
|
+
complete: Number.isInteger(closeIndex),
|
|
592
|
+
ids: [...ids].sort(compareText),
|
|
593
|
+
signature: `${token.value}:${signatureTokens.join('\u0001')}`,
|
|
594
|
+
identitySignature: `${[...ids].sort(compareText).join(',')}\u0000${bodySignature}`
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return scan;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function continuesAcrossLine(previous, next) {
|
|
601
|
+
const continuationTokens = new Set([
|
|
602
|
+
'.', '?.', ',', '(', '[', '=', '=>', '+', '-', '*', '/', '%', '&&', '||', '??',
|
|
603
|
+
'===', '!==', '==', '!=', '<', '>', '<=', '>=', '|', '&', '?', ':', '...'
|
|
604
|
+
]);
|
|
605
|
+
return continuationTokens.has(previous.value) || continuationTokens.has(next.value);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function statementRangeForToken(scan, tokenIndex) {
|
|
609
|
+
if (!scan || !scan.valid || !Number.isInteger(tokenIndex)
|
|
610
|
+
|| tokenIndex < 0 || tokenIndex >= scan.tokens.length) return null;
|
|
611
|
+
const tokens = scan.tokens;
|
|
612
|
+
const targetBraceDepth = scan.braceDepthBefore[tokenIndex];
|
|
613
|
+
let startIndex = 0;
|
|
614
|
+
let baseParenDepth = 0;
|
|
615
|
+
|
|
616
|
+
for (let index = tokenIndex - 1; index >= 0; index--) {
|
|
617
|
+
const value = tokens[index].value;
|
|
618
|
+
if (scan.braceDepthAfter[index] !== targetBraceDepth) continue;
|
|
619
|
+
if (![';', '{', '}'].includes(value)) continue;
|
|
620
|
+
startIndex = index + 1;
|
|
621
|
+
baseParenDepth = scan.parenDepthAfter[index];
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
for (let index = startIndex; index < tokenIndex; index++) {
|
|
626
|
+
if (tokens[index].line >= tokens[index + 1].line
|
|
627
|
+
|| scan.braceDepthAfter[index] !== targetBraceDepth
|
|
628
|
+
|| scan.braceDepthBefore[index + 1] !== targetBraceDepth
|
|
629
|
+
|| scan.parenDepthAfter[index] !== baseParenDepth
|
|
630
|
+
|| scan.parenDepthBefore[index + 1] !== baseParenDepth
|
|
631
|
+
|| continuesAcrossLine(tokens[index], tokens[index + 1])) continue;
|
|
632
|
+
startIndex = index + 1;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let endIndex = tokens.length - 1;
|
|
636
|
+
for (let index = tokenIndex + 1; index < tokens.length; index++) {
|
|
637
|
+
if (tokens[index].value === ';' && scan.braceDepthAfter[index] === targetBraceDepth
|
|
638
|
+
&& scan.parenDepthAfter[index] === baseParenDepth) {
|
|
639
|
+
endIndex = index - 1;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
if (tokens[index].value === '}' && scan.braceDepthBefore[index] === targetBraceDepth
|
|
643
|
+
&& scan.braceDepthAfter[index] < targetBraceDepth) {
|
|
644
|
+
endIndex = index - 1;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
if (tokens[index - 1].line < tokens[index].line
|
|
648
|
+
&& scan.braceDepthAfter[index - 1] === targetBraceDepth
|
|
649
|
+
&& scan.braceDepthBefore[index] === targetBraceDepth
|
|
650
|
+
&& scan.parenDepthAfter[index - 1] === baseParenDepth
|
|
651
|
+
&& scan.parenDepthBefore[index] === baseParenDepth
|
|
652
|
+
&& !continuesAcrossLine(tokens[index - 1], tokens[index])) {
|
|
653
|
+
endIndex = index - 1;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (startIndex > tokenIndex || endIndex < tokenIndex) return null;
|
|
658
|
+
return { startIndex, endIndex };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function assertionCallContainsToken(scan, tokenIndex) {
|
|
662
|
+
if (!scan || !Number.isInteger(tokenIndex)) return false;
|
|
663
|
+
const tokens = scan.tokens;
|
|
664
|
+
const assertionMethods = new Set(['equal', 'deepEqual', 'ok', 'throws', 'strictEqual']);
|
|
665
|
+
const contains = (startIndex, openIndex) => {
|
|
666
|
+
const closeIndex = scan.parenPairs.get(openIndex);
|
|
667
|
+
return Number.isInteger(closeIndex) && tokenIndex >= startIndex && tokenIndex <= closeIndex;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
671
|
+
const token = tokens[index];
|
|
672
|
+
if (token.type !== 'identifier') continue;
|
|
673
|
+
if (token.value === 'assert') {
|
|
674
|
+
if (tokens[index + 1] && tokens[index + 1].value === '(' && contains(index, index + 1)) return true;
|
|
675
|
+
const memberIndex = index + 2;
|
|
676
|
+
const openIndex = index + 3;
|
|
677
|
+
if (tokens[index + 1] && ['.', '?.'].includes(tokens[index + 1].value)
|
|
678
|
+
&& tokens[memberIndex] && tokens[memberIndex].type === 'identifier'
|
|
679
|
+
&& tokens[openIndex] && tokens[openIndex].value === '(' && contains(index, openIndex)) return true;
|
|
680
|
+
}
|
|
681
|
+
if (token.value === 'expect' && tokens[index + 1] && tokens[index + 1].value === '(') {
|
|
682
|
+
let endIndex = scan.parenPairs.get(index + 1);
|
|
683
|
+
if (!Number.isInteger(endIndex)) continue;
|
|
684
|
+
let cursor = endIndex + 1;
|
|
685
|
+
while (tokens[cursor] && ['.', '?.'].includes(tokens[cursor].value)
|
|
686
|
+
&& tokens[cursor + 1] && tokens[cursor + 1].type === 'identifier') {
|
|
687
|
+
const memberIndex = cursor + 1;
|
|
688
|
+
const openIndex = memberIndex + 1;
|
|
689
|
+
if (tokens[openIndex] && tokens[openIndex].value === '(') {
|
|
690
|
+
const closeIndex = scan.parenPairs.get(openIndex);
|
|
691
|
+
if (!Number.isInteger(closeIndex)) break;
|
|
692
|
+
endIndex = closeIndex;
|
|
693
|
+
cursor = closeIndex + 1;
|
|
694
|
+
} else {
|
|
695
|
+
// Modifiers such as .not, .resolves, and .rejects are property links
|
|
696
|
+
// in the matcher chain; keep scanning for its eventual matcher call.
|
|
697
|
+
endIndex = memberIndex;
|
|
698
|
+
cursor = memberIndex + 1;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (tokenIndex >= index && tokenIndex <= endIndex) return true;
|
|
702
|
+
}
|
|
703
|
+
if (assertionMethods.has(token.value) && tokens[index - 1]
|
|
704
|
+
&& ['.', '?.'].includes(tokens[index - 1].value)
|
|
705
|
+
&& tokens[index + 1] && tokens[index + 1].value === '(' && contains(index - 1, index + 1)) return true;
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function computedAssertionCallContainsToken(scan, tokenIndex, range) {
|
|
711
|
+
if (!scan || !Number.isInteger(tokenIndex) || !range) return false;
|
|
712
|
+
const tokens = scan.tokens;
|
|
713
|
+
const findBracketClose = openIndex => {
|
|
714
|
+
let depth = 0;
|
|
715
|
+
for (let index = openIndex; index < tokens.length; index++) {
|
|
716
|
+
if (tokens[index].value === '[') depth++;
|
|
717
|
+
else if (tokens[index].value === ']') {
|
|
718
|
+
depth--;
|
|
719
|
+
if (depth === 0) return index;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
for (let startIndex = range.startIndex; startIndex <= range.endIndex; startIndex++) {
|
|
726
|
+
if (tokens[startIndex].type !== 'identifier'
|
|
727
|
+
|| !['assert', 'expect'].includes(tokens[startIndex].value)) continue;
|
|
728
|
+
let cursor = startIndex + 1;
|
|
729
|
+
let endIndex = startIndex;
|
|
730
|
+
let hasComputedMember = false;
|
|
731
|
+
let hasCall = false;
|
|
732
|
+
|
|
733
|
+
while (cursor <= range.endIndex) {
|
|
734
|
+
if (tokens[cursor].value === '(') {
|
|
735
|
+
const closeIndex = scan.parenPairs.get(cursor);
|
|
736
|
+
if (!Number.isInteger(closeIndex)) break;
|
|
737
|
+
hasCall = true;
|
|
738
|
+
endIndex = closeIndex;
|
|
739
|
+
cursor = closeIndex + 1;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (['.', '?.'].includes(tokens[cursor].value) && tokens[cursor + 1]
|
|
743
|
+
&& tokens[cursor + 1].value === '[') cursor++;
|
|
744
|
+
if (tokens[cursor].value === '[') {
|
|
745
|
+
const closeIndex = findBracketClose(cursor);
|
|
746
|
+
if (!Number.isInteger(closeIndex)) break;
|
|
747
|
+
hasComputedMember = true;
|
|
748
|
+
endIndex = closeIndex;
|
|
749
|
+
cursor = closeIndex + 1;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (['.', '?.'].includes(tokens[cursor].value)
|
|
753
|
+
&& tokens[cursor + 1] && tokens[cursor + 1].type === 'identifier') {
|
|
754
|
+
endIndex = cursor + 1;
|
|
755
|
+
cursor += 2;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (hasComputedMember && hasCall && tokenIndex >= startIndex && tokenIndex <= endIndex) return true;
|
|
762
|
+
}
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function unsupportedAssertionCallContainsToken(scan, tokenIndex, range) {
|
|
767
|
+
if (!scan || !Number.isInteger(tokenIndex) || !range) return false;
|
|
768
|
+
const tokens = scan.tokens;
|
|
769
|
+
const end = Math.min(range.endIndex, tokens.length - 1);
|
|
770
|
+
for (let index = range.startIndex; index <= end; index++) {
|
|
771
|
+
if (tokens[index].type !== 'identifier'
|
|
772
|
+
|| !tokens[index + 1] || !['.', '?.'].includes(tokens[index + 1].value)
|
|
773
|
+
|| !tokens[index + 2] || tokens[index + 2].type !== 'identifier'
|
|
774
|
+
|| !tokens[index + 3] || tokens[index + 3].value !== '(') continue;
|
|
775
|
+
const closeIndex = scan.parenPairs.get(index + 3);
|
|
776
|
+
if (Number.isInteger(closeIndex) && tokenIndex >= index && tokenIndex <= closeIndex) return true;
|
|
777
|
+
}
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
function statementHasAssertion(tokens, range) {
|
|
781
|
+
if (!range) return false;
|
|
782
|
+
for (let index = range.startIndex; index <= range.endIndex; index++) {
|
|
783
|
+
const token = tokens[index];
|
|
784
|
+
if (token.type !== 'identifier') continue;
|
|
785
|
+
if (token.value === 'assert') return true;
|
|
786
|
+
if (token.value === 'expect' && tokens[index + 1] && tokens[index + 1].value === '(') return true;
|
|
787
|
+
if (['equal', 'deepEqual', 'ok', 'throws', 'strictEqual'].includes(token.value)
|
|
788
|
+
&& tokens[index - 1] && ['.', '?.'].includes(tokens[index - 1].value)
|
|
789
|
+
&& tokens[index + 1] && tokens[index + 1].value === '(') return true;
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function testCallTitleContainsToken(scan, tokenIndex) {
|
|
795
|
+
return Boolean(scan && scan.calls.some(call => call.complete
|
|
796
|
+
&& tokenIndex >= call.titleStartIndex && tokenIndex <= call.titleEndIndex));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function nearestTestCall(scan, tokenIndex) {
|
|
800
|
+
return scan.calls.filter(call => call.complete && tokenIndex >= call.startIndex && tokenIndex <= call.closeIndex)
|
|
801
|
+
.sort((left, right) => (left.closeIndex - left.startIndex) - (right.closeIndex - right.startIndex)
|
|
802
|
+
|| right.startIndex - left.startIndex)[0] || null;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function testCallForChangedStatement(scan, tokenIndex, range) {
|
|
806
|
+
const enclosingCall = nearestTestCall(scan, tokenIndex);
|
|
807
|
+
if (enclosingCall) return enclosingCall;
|
|
808
|
+
if (!range) return null;
|
|
809
|
+
|
|
810
|
+
const endToken = scan.tokens[range.endIndex];
|
|
811
|
+
return scan.calls.filter(call => call.complete && call.startIndex === range.startIndex
|
|
812
|
+
&& (call.closeIndex === range.endIndex
|
|
813
|
+
|| (endToken && endToken.value === ';' && call.closeIndex === range.endIndex - 1)))
|
|
814
|
+
.sort((left, right) => (left.closeIndex - left.startIndex) - (right.closeIndex - right.startIndex)
|
|
815
|
+
|| right.startIndex - left.startIndex)[0] || null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function fileLevelRequirementTags(text, scan) {
|
|
819
|
+
if (typeof text !== 'string' || !scan || !scan.valid || !Array.isArray(scan.comments)) return [];
|
|
820
|
+
const tags = [];
|
|
821
|
+
|
|
822
|
+
for (const comment of scan.comments) {
|
|
823
|
+
if (!comment.topLevel) continue;
|
|
824
|
+
const insideTestCall = scan.calls.some(call => {
|
|
825
|
+
if (!call.complete) return false;
|
|
826
|
+
const startLine = scan.tokens[call.startIndex] && scan.tokens[call.startIndex].line;
|
|
827
|
+
const endLine = scan.tokens[call.closeIndex] && scan.tokens[call.closeIndex].line;
|
|
828
|
+
return Number.isInteger(startLine) && Number.isInteger(endLine)
|
|
829
|
+
&& comment.line >= startLine && comment.line <= endLine;
|
|
830
|
+
});
|
|
831
|
+
if (insideTestCall) continue;
|
|
832
|
+
|
|
833
|
+
const lineStart = text.lastIndexOf('\n', comment.start - 1) + 1;
|
|
834
|
+
const lineEnd = text.indexOf('\n', comment.end);
|
|
835
|
+
const end = lineEnd === -1 ? text.length : lineEnd;
|
|
836
|
+
if (text.slice(lineStart, comment.start).trim() || text.slice(comment.end, end).trim()) continue;
|
|
837
|
+
|
|
838
|
+
const content = comment.kind === 'block'
|
|
839
|
+
? comment.content.split(/\r?\n/).map(line => line.replace(/^\s*\* ?/, '').trim()).filter(Boolean).join(' ')
|
|
840
|
+
: comment.content;
|
|
841
|
+
const match = content.trim().match(/^(REQ-[A-Z][A-Z0-9]{1,9}-\d{3,})$/);
|
|
842
|
+
if (match) tags.push(match[1]);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return uniqueSorted(tags);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function requirementTestCallChanges(beforeScan, afterScan, changeKind) {
|
|
849
|
+
const newCallIndexes = new Set();
|
|
850
|
+
if (!afterScan) return { newCallIndexes, identityUnknown: false, identityChangedReqIds: [] };
|
|
851
|
+
if (changeKind === 'added') {
|
|
852
|
+
for (let index = 0; index < afterScan.calls.length; index++) {
|
|
853
|
+
if (afterScan.calls[index].ids.length > 0) newCallIndexes.add(index);
|
|
854
|
+
}
|
|
855
|
+
return { newCallIndexes, identityUnknown: false, identityChangedReqIds: [] };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const beforeCalls = beforeScan ? beforeScan.calls : [];
|
|
859
|
+
const unmatchedBefore = new Set(beforeCalls.map((_, index) => index));
|
|
860
|
+
const unmatchedAfter = new Set(afterScan.calls.map((_, index) => index));
|
|
861
|
+
for (const key of ['identitySignature', 'signature']) {
|
|
862
|
+
const available = new Map();
|
|
863
|
+
for (const index of unmatchedBefore) {
|
|
864
|
+
const call = beforeCalls[index];
|
|
865
|
+
const value = `${call.ids.join(',')}\u0000${call[key]}`;
|
|
866
|
+
if (!available.has(value)) available.set(value, []);
|
|
867
|
+
available.get(value).push(index);
|
|
868
|
+
}
|
|
869
|
+
for (const index of unmatchedAfter) {
|
|
870
|
+
const call = afterScan.calls[index];
|
|
871
|
+
const value = `${call.ids.join(',')}\u0000${call[key]}`;
|
|
872
|
+
const matches = available.get(value);
|
|
873
|
+
if (!matches || matches.length === 0) continue;
|
|
874
|
+
unmatchedAfter.delete(index);
|
|
875
|
+
unmatchedBefore.delete(matches.shift());
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const unmatchedBeforeReq = [...unmatchedBefore].filter(index => beforeCalls[index].ids.length > 0);
|
|
880
|
+
const unmatchedAfterReq = [...unmatchedAfter].filter(index => afterScan.calls[index].ids.length > 0);
|
|
881
|
+
const beforeRequirementTestCount = beforeCalls.filter(call => call.ids.length > 0).length;
|
|
882
|
+
const afterRequirementTestCount = afterScan.calls.filter(call => call.ids.length > 0).length;
|
|
883
|
+
const identityUnknown = unmatchedBeforeReq.length > 0
|
|
884
|
+
|| (unmatchedAfterReq.length > 0 && unmatchedBefore.size > 0);
|
|
885
|
+
let identityChangedReqIds = [];
|
|
886
|
+
if (identityUnknown && afterScan.valid) {
|
|
887
|
+
if (unmatchedAfterReq.length) {
|
|
888
|
+
identityChangedReqIds = uniqueSorted(unmatchedAfterReq.flatMap(index => afterScan.calls[index].ids));
|
|
889
|
+
} else if (beforeScan && beforeScan.valid) {
|
|
890
|
+
identityChangedReqIds = uniqueSorted(unmatchedBeforeReq.flatMap(index => beforeCalls[index].ids));
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!identityUnknown && unmatchedAfterReq.length && afterRequirementTestCount > beforeRequirementTestCount) {
|
|
894
|
+
for (const index of unmatchedAfterReq) newCallIndexes.add(index);
|
|
895
|
+
}
|
|
896
|
+
return { newCallIndexes, identityUnknown, identityChangedReqIds };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function analyzeTestChange(change, baseSnapshot, candidateSnapshot, beforeText, afterText) {
|
|
900
|
+
const result = {
|
|
901
|
+
assertionChanged: false,
|
|
902
|
+
testWithRequirement: false,
|
|
903
|
+
testAttributionUnknown: false,
|
|
904
|
+
testCitedIds: [],
|
|
905
|
+
newReqCitingTest: false
|
|
906
|
+
};
|
|
907
|
+
const oldPath = change.kind === 'rename' ? change.from : change.path;
|
|
908
|
+
const newPath = change.kind === 'rename' ? change.to : change.path;
|
|
909
|
+
if (![oldPath, newPath].some(path => typeof path === 'string' && TEST_PATH.test(path))
|
|
910
|
+
|| change.kind === 'rename' || !change.contentChanged) return result;
|
|
911
|
+
|
|
912
|
+
const before = snapshotText(baseSnapshot, oldPath);
|
|
913
|
+
const after = snapshotText(candidateSnapshot, newPath);
|
|
914
|
+
const beforeValue = before.readable ? before.text : undefined;
|
|
915
|
+
const afterValue = after.readable ? after.text : undefined;
|
|
916
|
+
const beforeScan = typeof beforeValue === 'string' ? scanTestSource(beforeValue) : null;
|
|
917
|
+
const afterScan = typeof afterValue === 'string' ? scanTestSource(afterValue) : null;
|
|
918
|
+
const beforeFileTagIds = fileLevelRequirementTags(beforeValue, beforeScan);
|
|
919
|
+
const afterFileTagIds = fileLevelRequirementTags(afterValue, afterScan);
|
|
920
|
+
const fileTagIdentityChanged = change.kind !== 'added'
|
|
921
|
+
&& (beforeFileTagIds.length !== afterFileTagIds.length
|
|
922
|
+
|| beforeFileTagIds.some((id, index) => id !== afterFileTagIds[index]));
|
|
923
|
+
const fileTagCandidateIds = fileTagIdentityChanged
|
|
924
|
+
? (afterFileTagIds.length ? afterFileTagIds : beforeFileTagIds)
|
|
925
|
+
: [];
|
|
926
|
+
const hasCitedTest = [beforeScan, afterScan].some(scan => scan && scan.calls.some(call => call.ids.length > 0));
|
|
927
|
+
const hasRequirementSignal = hasCitedTest || beforeFileTagIds.length > 0 || afterFileTagIds.length > 0;
|
|
928
|
+
result.testWithRequirement = hasRequirementSignal;
|
|
929
|
+
const testCallChanges = requirementTestCallChanges(beforeScan, afterScan, change.kind);
|
|
930
|
+
const identityCandidateIds = uniqueSorted([
|
|
931
|
+
...testCallChanges.identityChangedReqIds,
|
|
932
|
+
...fileTagCandidateIds
|
|
933
|
+
]);
|
|
934
|
+
result.newReqCitingTest = hasRequirementSignal
|
|
935
|
+
&& (testCallChanges.newCallIndexes.size > 0 || (change.kind === 'added' && afterFileTagIds.length === 1));
|
|
936
|
+
|
|
937
|
+
if (!before.readable || !after.readable || (beforeScan && !beforeScan.valid) || (afterScan && !afterScan.valid)) {
|
|
938
|
+
result.assertionChanged = 'unknown';
|
|
939
|
+
result.testAttributionUnknown = true;
|
|
940
|
+
result.testCitedIds = identityCandidateIds;
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
let delta;
|
|
945
|
+
if (change.kind === 'added') delta = { removed: [], added: afterScan ? afterScan.tokens : [] };
|
|
946
|
+
else if (change.kind === 'deleted') delta = { removed: beforeScan ? beforeScan.tokens : [], added: [] };
|
|
947
|
+
else delta = changedTokens(beforeScan.tokens, afterScan.tokens);
|
|
948
|
+
if (delta === null) {
|
|
949
|
+
result.assertionChanged = 'unknown';
|
|
950
|
+
result.testAttributionUnknown = true;
|
|
951
|
+
result.testCitedIds = identityCandidateIds;
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
result.testAttributionUnknown = testCallChanges.identityUnknown || fileTagIdentityChanged;
|
|
956
|
+
const citedIds = new Set(identityCandidateIds);
|
|
957
|
+
let assertionUnknown = false;
|
|
958
|
+
for (const [scan, changed, fileTagIds] of [
|
|
959
|
+
[beforeScan, delta.removed, beforeFileTagIds],
|
|
960
|
+
[afterScan, delta.added, afterFileTagIds]
|
|
961
|
+
]) {
|
|
962
|
+
if (!scan || !changed.length) continue;
|
|
963
|
+
for (const token of changed) {
|
|
964
|
+
const range = statementRangeForToken(scan, token.index);
|
|
965
|
+
if (!range) {
|
|
966
|
+
assertionUnknown = true;
|
|
967
|
+
result.testAttributionUnknown = true;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const assertionCallChanged = assertionCallContainsToken(scan, token.index);
|
|
971
|
+
const unsupportedAssertionCallChanged = unsupportedAssertionCallContainsToken(scan, token.index, range);
|
|
972
|
+
const computedAssertionCallChanged = computedAssertionCallContainsToken(scan, token.index, range);
|
|
973
|
+
const assertionStatement = statementHasAssertion(scan.tokens, range);
|
|
974
|
+
const assertionChangeSignal = assertionCallChanged || unsupportedAssertionCallChanged || computedAssertionCallChanged;
|
|
975
|
+
if (assertionCallChanged) result.assertionChanged = true;
|
|
976
|
+
else if (unsupportedAssertionCallChanged || computedAssertionCallChanged) assertionUnknown = true;
|
|
977
|
+
else if (!testCallTitleContainsToken(scan, token.index)
|
|
978
|
+
&& ['string', 'template', 'regex'].includes(token.type)) assertionUnknown = true;
|
|
979
|
+
|
|
980
|
+
const call = testCallForChangedStatement(scan, token.index, range);
|
|
981
|
+
if (call) {
|
|
982
|
+
const newTestSignal = scan === afterScan
|
|
983
|
+
&& testCallChanges.newCallIndexes.has(scan.calls.indexOf(call));
|
|
984
|
+
if (call.ids.length && (assertionChangeSignal || newTestSignal)) {
|
|
985
|
+
for (const id of call.ids) citedIds.add(id);
|
|
986
|
+
} else if (assertionChangeSignal || newTestSignal) {
|
|
987
|
+
result.testAttributionUnknown = true;
|
|
988
|
+
}
|
|
989
|
+
} else if (fileTagIds.length === 1 && assertionStatement && assertionChangeSignal) {
|
|
990
|
+
// Preserve the established single-REQ file-tag convention used by older
|
|
991
|
+
// unwrapped assertions; never borrow a different test call's title.
|
|
992
|
+
citedIds.add(fileTagIds[0]);
|
|
993
|
+
} else if (assertionChangeSignal) {
|
|
994
|
+
result.testAttributionUnknown = true;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
result.assertionChanged = result.assertionChanged === true ? true : assertionUnknown ? 'unknown' : false;
|
|
1000
|
+
result.testCitedIds = [...citedIds].sort(compareText);
|
|
1001
|
+
if (result.testCitedIds.length) result.testWithRequirement = true;
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function analyzeRequirementTestChanges(changes, baseSnapshot, candidateSnapshot) {
|
|
1006
|
+
let assertionChanged = false;
|
|
1007
|
+
let assertionUnknown = false;
|
|
1008
|
+
let newReqCitingTest = false;
|
|
1009
|
+
let scopeUnknown = false;
|
|
1010
|
+
for (const snapshot of [baseSnapshot, candidateSnapshot]) {
|
|
1011
|
+
const files = snapshot && snapshot.files && typeof snapshot.files === 'object' ? snapshot.files : {};
|
|
1012
|
+
if (Object.keys(files).some(path => TEST_PATH.test(normalizePath(path)) && typeof files[path] !== 'string')) {
|
|
1013
|
+
assertionUnknown = true;
|
|
1014
|
+
scopeUnknown = true;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const analyzed = changes.map(change => {
|
|
1018
|
+
const oldPath = change.kind === 'rename' ? change.from : change.path;
|
|
1019
|
+
const newPath = change.kind === 'rename' ? change.to : change.path;
|
|
1020
|
+
const beforeText = textAt(snapshotFiles(baseSnapshot), oldPath);
|
|
1021
|
+
const afterText = textAt(snapshotFiles(candidateSnapshot), newPath);
|
|
1022
|
+
const test = analyzeTestChange(change, baseSnapshot, candidateSnapshot, beforeText, afterText);
|
|
1023
|
+
if (test.assertionChanged === true) assertionChanged = true;
|
|
1024
|
+
else if (test.assertionChanged === 'unknown') assertionUnknown = true;
|
|
1025
|
+
newReqCitingTest = newReqCitingTest || test.newReqCitingTest;
|
|
1026
|
+
scopeUnknown = scopeUnknown || test.testAttributionUnknown;
|
|
1027
|
+
return { ...change, ...test };
|
|
1028
|
+
});
|
|
1029
|
+
return {
|
|
1030
|
+
changes: analyzed,
|
|
1031
|
+
assertionChanged: assertionChanged ? true : assertionUnknown ? 'unknown' : false,
|
|
1032
|
+
newReqCitingTest,
|
|
1033
|
+
scopeUnknown
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function textAt(files, path) {
|
|
1038
|
+
return path && Object.prototype.hasOwnProperty.call(files, path) ? files[path] : undefined;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function roleAt(path, text, config) {
|
|
1042
|
+
const entry = governedEntry(path, config);
|
|
1043
|
+
if (entry && typeof entry.role === 'string') return entry.role;
|
|
1044
|
+
if (text !== undefined) {
|
|
1045
|
+
const parsed = lifecycle.parseFrontmatter(text);
|
|
1046
|
+
if (parsed.data && typeof parsed.data.role === 'string') return parsed.data.role;
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function isGoverned(path, text, config) {
|
|
1052
|
+
return Boolean(governedEntry(path, config)) || matchesAny(path, config && config.legacy)
|
|
1053
|
+
|| Boolean(roleAt(path, text, config));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function extractSection(text, wantedTitle) {
|
|
1057
|
+
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
1058
|
+
const headings = [];
|
|
1059
|
+
const unsupportedSetextIndexes = [];
|
|
1060
|
+
let setextDecisionUnknown = false;
|
|
1061
|
+
let fence = null;
|
|
1062
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1063
|
+
const line = lines[index];
|
|
1064
|
+
if (fence) {
|
|
1065
|
+
const close = line.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
|
|
1066
|
+
if (close && close[1][0] === fence.character && close[1].length >= fence.length) fence = null;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const open = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
1070
|
+
const invalidBacktickInfo = open && open[1][0] === '`' && open[2].includes('`');
|
|
1071
|
+
if (open && !invalidBacktickInfo) {
|
|
1072
|
+
fence = { character: open[1][0], length: open[1].length };
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
const setextUnderline = line.match(/^ {0,3}(=+|-+)[ \t]*$/);
|
|
1076
|
+
if (setextUnderline && index > 0) {
|
|
1077
|
+
const previousLine = lines[index - 1];
|
|
1078
|
+
const setextTitle = previousLine.match(/^ {0,3}([^#\s].*?)[ \t]*$/);
|
|
1079
|
+
const fenceLine = /^ {0,3}(?:`{3,}|~{3,})/.test(previousLine);
|
|
1080
|
+
const thematicBreak = /^ {0,3}(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/.test(previousLine);
|
|
1081
|
+
if (setextTitle && !fenceLine && !thematicBreak) {
|
|
1082
|
+
const title = setextTitle[1].trim();
|
|
1083
|
+
if (title.toLowerCase() === wantedTitle.toLowerCase()) {
|
|
1084
|
+
// Without paragraph context, the visible line may be part of a
|
|
1085
|
+
// multi-line Setext heading. Preserve uncertainty instead of guessing.
|
|
1086
|
+
setextDecisionUnknown = true;
|
|
1087
|
+
} else {
|
|
1088
|
+
// Other Setext boundaries need paragraph/container context this bounded
|
|
1089
|
+
// extractor does not model, so an occurrence inside Decision is unknown.
|
|
1090
|
+
unsupportedSetextIndexes.push(index - 1);
|
|
1091
|
+
}
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// CommonMark ATX heading: 0-3 spaces, 1-6 "#", then a space/tab or end of line. A closing sequence needs a preceding space/tab.
|
|
1096
|
+
const match = line.match(/^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/);
|
|
1097
|
+
if (match) headings.push({ index, level: match[1].length, title: (match[2] || '').replace(/(?:^|[ \t]+)#+[ \t]*$/, '').trim() });
|
|
1098
|
+
}
|
|
1099
|
+
const found = headings.filter(item => item.title.toLowerCase() === wantedTitle.toLowerCase());
|
|
1100
|
+
if (setextDecisionUnknown || found.length > 1) return { state: 'unknown', text: '' };
|
|
1101
|
+
if (!found.length) return { state: 'absent', text: '' };
|
|
1102
|
+
const heading = found[0];
|
|
1103
|
+
const next = headings.find(item => item.index > heading.index && item.level <= heading.level);
|
|
1104
|
+
const end = next ? next.index : lines.length;
|
|
1105
|
+
if (unsupportedSetextIndexes.some(index => index > heading.index && index < end)) return { state: 'unknown', text: '' };
|
|
1106
|
+
return { state: 'present', text: lines.slice(heading.index + 1, end).join('\n').trim() };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function requirementProjection(item) {
|
|
1110
|
+
return {
|
|
1111
|
+
id: item.id,
|
|
1112
|
+
title: item.title,
|
|
1113
|
+
block: item.block,
|
|
1114
|
+
aliases: Array.isArray(item.aliases) ? [...item.aliases] : [],
|
|
1115
|
+
replaces: Array.isArray(item.replaces) ? [...item.replaces] : [],
|
|
1116
|
+
validClause: item.validClause === true
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function extractRequirementBlocks(text) {
|
|
1121
|
+
if (text === undefined) return [];
|
|
1122
|
+
const parsed = lifecycle.parseFrontmatter(text);
|
|
1123
|
+
if (parsed.error) return [];
|
|
1124
|
+
return lifecycle.parseRequirementBlocks({ body: parsed.body }).map(requirementProjection);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function requirementDeclarations(files, config) {
|
|
1128
|
+
const declarations = [];
|
|
1129
|
+
for (const path of Object.keys(files).sort(compareText)) {
|
|
1130
|
+
const text = files[path];
|
|
1131
|
+
if (roleAt(path, text, config) !== 'prd') continue;
|
|
1132
|
+
const parsed = lifecycle.parseFrontmatter(text);
|
|
1133
|
+
if (parsed.error) continue;
|
|
1134
|
+
for (const item of lifecycle.parseRequirementBlocks({ body: parsed.body })) {
|
|
1135
|
+
const projection = requirementProjection(item);
|
|
1136
|
+
declarations.push({ ...projection, file: path, text: JSON.stringify(projection) });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return declarations;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function compareRequirementBlocks(beforeReqs, afterReqs) {
|
|
1143
|
+
const byId = requirements => {
|
|
1144
|
+
const result = new Map();
|
|
1145
|
+
for (const item of requirements) {
|
|
1146
|
+
if (!result.has(item.id)) result.set(item.id, []);
|
|
1147
|
+
result.get(item.id).push(item);
|
|
1148
|
+
}
|
|
1149
|
+
return result;
|
|
1150
|
+
};
|
|
1151
|
+
const beforeById = byId(beforeReqs);
|
|
1152
|
+
const afterById = byId(afterReqs);
|
|
1153
|
+
const ids = uniqueSorted([...beforeById.keys(), ...afterById.keys()]);
|
|
1154
|
+
let changed = false;
|
|
1155
|
+
let unknown = false;
|
|
1156
|
+
for (const id of ids) {
|
|
1157
|
+
const before = beforeById.get(id) || [];
|
|
1158
|
+
const after = afterById.get(id) || [];
|
|
1159
|
+
if (JSON.stringify(before) === JSON.stringify(after)) continue;
|
|
1160
|
+
if ([...before, ...after].some(item => item.validClause !== true)) unknown = true;
|
|
1161
|
+
else changed = true;
|
|
1162
|
+
}
|
|
1163
|
+
return changed ? 'changed' : unknown ? 'unknown' : 'same';
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function changedRequirementFacts(baseFiles, candidateFiles, config, baseLifecycle, candidateLifecycle, candidateRevisionAnalysis) {
|
|
1167
|
+
const beforeById = new Map();
|
|
1168
|
+
for (const item of requirementDeclarations(baseFiles, config)) {
|
|
1169
|
+
if (!beforeById.has(item.id)) beforeById.set(item.id, []);
|
|
1170
|
+
beforeById.get(item.id).push(item);
|
|
1171
|
+
}
|
|
1172
|
+
const changed = requirementDeclarations(candidateFiles, config).filter(item => {
|
|
1173
|
+
const priorItems = beforeById.get(item.id) || [];
|
|
1174
|
+
const prior = priorItems.find(candidate => candidate.file === item.file) || priorItems[0];
|
|
1175
|
+
return !prior || prior.text !== item.text;
|
|
1176
|
+
});
|
|
1177
|
+
const baseDocsByPath = new Map(baseLifecycle.docs.map(doc => [doc.path, doc]));
|
|
1178
|
+
const candidateDocsByPath = new Map(candidateLifecycle.docs.map(doc => [doc.path, doc]));
|
|
1179
|
+
const baseRevisionIds = new Set(baseLifecycle.docs.map(revisionIdentity).filter(Boolean));
|
|
1180
|
+
const revisionGraph = candidateRevisionAnalysis && candidateRevisionAnalysis._graph
|
|
1181
|
+
&& candidateRevisionAnalysis._graph.parentByDoc instanceof Map ? candidateRevisionAnalysis._graph : null;
|
|
1182
|
+
const parentByDoc = revisionGraph ? revisionGraph.parentByDoc : new Map();
|
|
1183
|
+
const currentDocs = revisionGraph && revisionGraph.currentById instanceof Map
|
|
1184
|
+
? new Set(revisionGraph.currentById.values()) : new Set();
|
|
1185
|
+
const eligibleDocs = candidateRevisionAnalysis._retirementEligibleSources instanceof Set
|
|
1186
|
+
? candidateRevisionAnalysis._retirementEligibleSources : new Set();
|
|
1187
|
+
const reqUpdates = [];
|
|
1188
|
+
const reqMutationNotRevision = [];
|
|
1189
|
+
|
|
1190
|
+
const chainRootedInBaseRevision = doc => {
|
|
1191
|
+
const visited = new Set();
|
|
1192
|
+
for (let cursor = doc; cursor && !visited.has(cursor); cursor = parentByDoc.get(cursor)) {
|
|
1193
|
+
visited.add(cursor);
|
|
1194
|
+
const identity = revisionIdentity(cursor);
|
|
1195
|
+
if (identity && baseRevisionIds.has(identity)) return true;
|
|
1196
|
+
}
|
|
1197
|
+
return false;
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
for (const { id, file } of changed) {
|
|
1201
|
+
const candidateDoc = candidateDocsByPath.get(file);
|
|
1202
|
+
const revisionId = revisionIdentity(candidateDoc);
|
|
1203
|
+
const validNewEffectiveRevision = Boolean(candidateDoc && revisionId
|
|
1204
|
+
&& currentDocs.has(candidateDoc) && eligibleDocs.has(candidateDoc) && !baseRevisionIds.has(revisionId)
|
|
1205
|
+
&& (!beforeById.has(id) || chainRootedInBaseRevision(candidateDoc)));
|
|
1206
|
+
if (validNewEffectiveRevision) {
|
|
1207
|
+
reqUpdates.push({ id, file });
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (!beforeById.has(id)) continue;
|
|
1212
|
+
|
|
1213
|
+
const baseDoc = baseDocsByPath.get(file);
|
|
1214
|
+
reqMutationNotRevision.push({
|
|
1215
|
+
id,
|
|
1216
|
+
file,
|
|
1217
|
+
code: 'req-mutation-not-revision',
|
|
1218
|
+
...(revisionId && candidateDoc && baseDoc && revisionId === revisionIdentity(baseDoc) ? {} : { reason: 'not-a-base-rooted-successor' })
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const order = (left, right) => compareText(left.id, right.id) || compareText(left.file, right.file);
|
|
1223
|
+
reqUpdates.sort(order);
|
|
1224
|
+
reqMutationNotRevision.sort(order);
|
|
1225
|
+
return { reqUpdates, reqMutationNotRevision };
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function revisionIdentity(doc) {
|
|
1229
|
+
const data = doc && doc.frontmatter;
|
|
1230
|
+
if (!data || typeof data.id !== 'string' || !Number.isInteger(data.revision)) return null;
|
|
1231
|
+
return `${data.id}\u0000${data.revision}`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function lifecycleAnalysisAgainstBase(baseLifecycle, candidateLifecycle, config) {
|
|
1235
|
+
return lifecycle.analyzeDocs(candidateLifecycle.docs, config, {
|
|
1236
|
+
all: true,
|
|
1237
|
+
baseInventory: baseLifecycle.docs.map(doc => ({
|
|
1238
|
+
path: doc.path,
|
|
1239
|
+
bytes: doc.storedBytes,
|
|
1240
|
+
legacy: doc.legacy
|
|
1241
|
+
})),
|
|
1242
|
+
baseConfigSources: [config]
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function candidateRequirementResolution(id, candidateDocs, baseDeclarations, candidateAnalysis) {
|
|
1247
|
+
const diagnostics = candidateAnalysis && Array.isArray(candidateAnalysis.diagnostics)
|
|
1248
|
+
? candidateAnalysis.diagnostics : [];
|
|
1249
|
+
const graph = candidateAnalysis && candidateAnalysis._graph;
|
|
1250
|
+
const graphCurrentDocs = graph && graph.currentById instanceof Map
|
|
1251
|
+
? new Set(graph.currentById.values()) : new Set();
|
|
1252
|
+
const errorPaths = new Set(diagnostics.filter(item => item && item.severity === 'error'
|
|
1253
|
+
&& typeof item.path === 'string').map(item => normalizePath(item.path)));
|
|
1254
|
+
const hasRelevantError = baseDeclarations.some(item => item && item.id === id
|
|
1255
|
+
&& errorPaths.has(normalizePath(item.file)));
|
|
1256
|
+
if (hasRelevantError) return { file: null, resolution: 'analysis-error' };
|
|
1257
|
+
|
|
1258
|
+
const resolved = lifecycle.resolveRef(candidateDocs, id, { graph });
|
|
1259
|
+
if (resolved && resolved.doc && resolved.requirement) {
|
|
1260
|
+
const requirementId = resolved.requirement.id;
|
|
1261
|
+
const duplicateInCurrentDoc = diagnostics.some(item => item && item.severity === 'error'
|
|
1262
|
+
&& item.code === 'duplicate-req' && item.id === requirementId
|
|
1263
|
+
&& normalizePath(item.path || '') === normalizePath(resolved.doc.path));
|
|
1264
|
+
if (duplicateInCurrentDoc) return { file: null, resolution: 'ambiguous' };
|
|
1265
|
+
|
|
1266
|
+
const effectivePrefix = candidateAnalysis && candidateAnalysis._retirementEligibleSources;
|
|
1267
|
+
if (!graphCurrentDocs.has(resolved.doc) || !(effectivePrefix instanceof Set) || !effectivePrefix.has(resolved.doc)) {
|
|
1268
|
+
return { file: null, resolution: 'analysis-error' };
|
|
1269
|
+
}
|
|
1270
|
+
return { file: resolved.doc.path, resolution: 'resolved' };
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const candidates = resolved && Array.isArray(resolved.candidates) ? resolved.candidates : [];
|
|
1274
|
+
const currentLogicalIds = new Set(candidates.filter(doc => graphCurrentDocs.has(doc))
|
|
1275
|
+
.map(doc => graph.logicalIdByDoc.get(doc)).filter(logicalId => typeof logicalId === 'string'));
|
|
1276
|
+
if (currentLogicalIds.size > 1) return { file: null, resolution: 'ambiguous' };
|
|
1277
|
+
return { file: null, resolution: 'missing' };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function citedRequirementFacts(changes, baseFiles, candidateDocs, config, candidateAnalysis, reqMutationNotRevision = []) {
|
|
1281
|
+
const citedIds = new Set();
|
|
1282
|
+
for (const change of changes) {
|
|
1283
|
+
for (const id of change.testCitedIds || []) citedIds.add(id);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const mutatedIds = new Set((Array.isArray(reqMutationNotRevision) ? reqMutationNotRevision : [])
|
|
1287
|
+
.map(item => item && item.id).filter(id => typeof id === 'string'));
|
|
1288
|
+
const before = requirementDeclarations(baseFiles, config);
|
|
1289
|
+
const result = [];
|
|
1290
|
+
for (const id of [...citedIds].sort(compareText)) {
|
|
1291
|
+
const resolution = mutatedIds.has(id)
|
|
1292
|
+
? { file: null, resolution: 'mutated' }
|
|
1293
|
+
: candidateRequirementResolution(id, candidateDocs, before, candidateAnalysis);
|
|
1294
|
+
result.push({ id, ...resolution });
|
|
1295
|
+
}
|
|
1296
|
+
return result;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function derivedEntryAffected(change, item) {
|
|
1300
|
+
if (!item || typeof item.path !== 'string') return false;
|
|
1301
|
+
const paths = changePaths(change);
|
|
1302
|
+
const outputChanged = change.contentChanged && paths.some(path => globToRegExp(item.path).test(path));
|
|
1303
|
+
const sources = [item.source, ...(Array.isArray(item.sources) ? item.sources : [])]
|
|
1304
|
+
.filter(source => typeof source === 'string');
|
|
1305
|
+
const dependencyChanged = (change.contentChanged || change.kind === 'rename')
|
|
1306
|
+
&& paths.some(path => sources.some(source => globToRegExp(source).test(path)));
|
|
1307
|
+
return outputChanged || dependencyChanged;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function derivedOutputPaths(changes, config) {
|
|
1311
|
+
const entries = Array.isArray(config && config.derived) ? config.derived : [];
|
|
1312
|
+
return uniqueSorted(entries.filter(item => item && typeof item.path === 'string'
|
|
1313
|
+
&& changes.some(change => derivedEntryAffected(change, item))).map(item => item.path));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function normalizedFields(text) {
|
|
1317
|
+
if (text === undefined) return { state: 'absent', values: {} };
|
|
1318
|
+
const parsed = lifecycle.parseFrontmatter(text);
|
|
1319
|
+
return { state: parsed.error ? 'unknown' : 'present', values: parsed.data || {} };
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function fieldsChanged(beforeText, afterText) {
|
|
1323
|
+
const before = normalizedFields(beforeText);
|
|
1324
|
+
const after = normalizedFields(afterText);
|
|
1325
|
+
if (before.state === 'unknown' || after.state === 'unknown') return 'unknown';
|
|
1326
|
+
return DOCUMENT_NORMATIVE_FIELDS.some(key => {
|
|
1327
|
+
const hasBefore = Object.prototype.hasOwnProperty.call(before.values, key);
|
|
1328
|
+
const hasAfter = Object.prototype.hasOwnProperty.call(after.values, key);
|
|
1329
|
+
return hasBefore !== hasAfter || JSON.stringify(before.values[key]) !== JSON.stringify(after.values[key]);
|
|
1330
|
+
}) ? 'changed' : 'same';
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function normativeState(change, before, after, oldRole, newRole) {
|
|
1334
|
+
const role = newRole || oldRole;
|
|
1335
|
+
if (role !== 'prd' && role !== 'adr') return 'none';
|
|
1336
|
+
const fieldState = fieldsChanged(before, after);
|
|
1337
|
+
if (fieldState === 'changed') return 'changed';
|
|
1338
|
+
if (fieldState === 'unknown') return 'unknown';
|
|
1339
|
+
|
|
1340
|
+
if (role === 'prd') {
|
|
1341
|
+
const beforeReqs = extractRequirementBlocks(before);
|
|
1342
|
+
const afterReqs = extractRequirementBlocks(after);
|
|
1343
|
+
const reqState = compareRequirementBlocks(beforeReqs, afterReqs);
|
|
1344
|
+
if (reqState !== 'same') return reqState;
|
|
1345
|
+
const hasIncompleteReqHeading = [before, after].some(text => text !== undefined
|
|
1346
|
+
&& /^#{1,6}\s+REQ(?:\b|[-\s])/m.test(text)
|
|
1347
|
+
&& !REQUIREMENT_ID.test(text));
|
|
1348
|
+
return hasIncompleteReqHeading ? 'unknown' : 'none';
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const priorDecision = before === undefined ? { state: 'absent', text: '' } : extractSection(before, 'Decision');
|
|
1352
|
+
const nextDecision = after === undefined ? { state: 'absent', text: '' } : extractSection(after, 'Decision');
|
|
1353
|
+
if (priorDecision.state === 'unknown' || nextDecision.state === 'unknown') return 'unknown';
|
|
1354
|
+
if (priorDecision.state === 'present' && nextDecision.state === 'absent') {
|
|
1355
|
+
return change.kind === 'deleted' ? 'changed' : 'unknown';
|
|
1356
|
+
}
|
|
1357
|
+
if (priorDecision.state !== nextDecision.state || priorDecision.text !== nextDecision.text) return 'changed';
|
|
1358
|
+
return 'none';
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function deriveDocFacts({ base, candidate, config, inventory } = {}) {
|
|
1362
|
+
const inputKnown = isSnapshotInput(base) && isSnapshotInput(candidate) && isConfigInput(config);
|
|
1363
|
+
const effectiveConfig = isConfigInput(config) ? config : {};
|
|
1364
|
+
const inventoryComplete = Boolean(inventory && inventory.complete === true);
|
|
1365
|
+
const baseFiles = snapshotFiles(base);
|
|
1366
|
+
const candidateFiles = snapshotFiles(candidate);
|
|
1367
|
+
const baseLifecycle = analyzeSnapshotWithLifecycle(baseFiles, effectiveConfig);
|
|
1368
|
+
const candidateLifecycle = analyzeSnapshotWithLifecycle(candidateFiles, effectiveConfig);
|
|
1369
|
+
const candidateAnalysis = candidateLifecycle.summary;
|
|
1370
|
+
const candidateRevisionAnalysis = lifecycleAnalysisAgainstBase(baseLifecycle, candidateLifecycle, effectiveConfig);
|
|
1371
|
+
const changes = findRenames(baseFiles, candidateFiles);
|
|
1372
|
+
const contextualChanges = changes.map(change => {
|
|
1373
|
+
const oldPath = change.kind === 'rename' ? change.from : change.path;
|
|
1374
|
+
const newPath = change.kind === 'rename' ? change.to : change.path;
|
|
1375
|
+
const beforeText = textAt(baseFiles, oldPath);
|
|
1376
|
+
const afterText = textAt(candidateFiles, newPath);
|
|
1377
|
+
const oldRole = roleAt(oldPath, beforeText, effectiveConfig);
|
|
1378
|
+
const newRole = roleAt(newPath, afterText, effectiveConfig);
|
|
1379
|
+
const oldGoverned = isGoverned(oldPath, beforeText, effectiveConfig);
|
|
1380
|
+
const newGoverned = isGoverned(newPath, afterText, effectiveConfig);
|
|
1381
|
+
const contentChanged = change.kind !== 'rename' && beforeText !== afterText;
|
|
1382
|
+
const paths = changePaths(change);
|
|
1383
|
+
return {
|
|
1384
|
+
...change,
|
|
1385
|
+
oldPath,
|
|
1386
|
+
newPath,
|
|
1387
|
+
oldRole,
|
|
1388
|
+
newRole,
|
|
1389
|
+
oldGoverned,
|
|
1390
|
+
newGoverned,
|
|
1391
|
+
contentChanged,
|
|
1392
|
+
normative: contentChanged ? normativeState(change, beforeText, afterText, oldRole, newRole) : 'none',
|
|
1393
|
+
derivedAffected: (effectiveConfig && Array.isArray(effectiveConfig.derived) ? effectiveConfig.derived : [])
|
|
1394
|
+
.some(item => derivedEntryAffected({ ...change, contentChanged }, item)),
|
|
1395
|
+
testWithRequirement: contentChanged && paths.some(path => TEST_PATH.test(path))
|
|
1396
|
+
&& REQUIREMENT_ID.test(`${beforeText || ''}\n${afterText || ''}`),
|
|
1397
|
+
codeChanged: contentChanged && paths.some(path => CODE_EXTENSION.test(path) && !TEST_PATH.test(path)
|
|
1398
|
+
&& !isGoverned(path, textAt(baseFiles, path) ?? textAt(candidateFiles, path), effectiveConfig))
|
|
1399
|
+
};
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
const governedTouched = contextualChanges.some(change => change.oldGoverned || change.newGoverned);
|
|
1403
|
+
const newGovernedFiles = contextualChanges.filter(change => change.kind === 'added' && change.newGoverned);
|
|
1404
|
+
const normativeChanged = contextualChanges.some(change => change.normative === 'changed');
|
|
1405
|
+
const normativeUnknown = !normativeChanged && contextualChanges.some(change => change.normative === 'unknown');
|
|
1406
|
+
const referenceContentChanged = contextualChanges.some(change => change.contentChanged
|
|
1407
|
+
&& [change.oldRole, change.newRole].includes('reference')
|
|
1408
|
+
&& (change.oldGoverned || change.newGoverned));
|
|
1409
|
+
const requirementTestAnalysis = analyzeRequirementTestChanges(contextualChanges, base, candidate);
|
|
1410
|
+
const testChanges = requirementTestAnalysis.changes;
|
|
1411
|
+
const assertionChanged = requirementTestAnalysis.assertionChanged;
|
|
1412
|
+
const newReqCitingTest = requirementTestAnalysis.newReqCitingTest;
|
|
1413
|
+
const requirementSyncTrigger = newReqCitingTest ? 'new-test'
|
|
1414
|
+
: assertionChanged === true ? 'assertion-change' : null;
|
|
1415
|
+
const requirementSyncCandidate = requirementSyncTrigger !== null;
|
|
1416
|
+
const codeChanged = contextualChanges.some(change => change.codeChanged);
|
|
1417
|
+
const derivedAffected = contextualChanges.some(change => change.derivedAffected);
|
|
1418
|
+
const { reqUpdates, reqMutationNotRevision } = changedRequirementFacts(
|
|
1419
|
+
baseFiles, candidateFiles, effectiveConfig, baseLifecycle, candidateLifecycle, candidateRevisionAnalysis
|
|
1420
|
+
);
|
|
1421
|
+
const citedReqs = citedRequirementFacts(testChanges, baseFiles, candidateLifecycle.docs,
|
|
1422
|
+
effectiveConfig, candidateLifecycle.analysis, reqMutationNotRevision);
|
|
1423
|
+
const derivedPaths = derivedOutputPaths(contextualChanges, effectiveConfig);
|
|
1424
|
+
const baseAnalysis = baseLifecycle.summary;
|
|
1425
|
+
const inventoryEnvelope = copyInventoryEnvelope(inventory);
|
|
1426
|
+
|
|
1427
|
+
const facts = {
|
|
1428
|
+
schemaVersion: 1,
|
|
1429
|
+
inputKnown,
|
|
1430
|
+
changedPaths: {
|
|
1431
|
+
added: contextualChanges.filter(change => change.kind === 'added').map(change => change.path).sort(compareText),
|
|
1432
|
+
modified: contextualChanges.filter(change => change.kind === 'modified').map(change => change.path).sort(compareText),
|
|
1433
|
+
deleted: contextualChanges.filter(change => change.kind === 'deleted').map(change => change.path).sort(compareText),
|
|
1434
|
+
renamed: contextualChanges.filter(change => change.kind === 'rename')
|
|
1435
|
+
.map(change => ({ from: change.from, to: change.to }))
|
|
1436
|
+
.sort((left, right) => compareText(left.from, right.from) || compareText(left.to, right.to))
|
|
1437
|
+
},
|
|
1438
|
+
reqCitingTests: uniqueSorted(testChanges.filter(change => change.newReqCitingTest
|
|
1439
|
+
|| change.testCitedIds.length > 0 || change.testAttributionUnknown)
|
|
1440
|
+
.map(change => candidateFiles[change.newPath] !== undefined ? change.newPath : change.oldPath)),
|
|
1441
|
+
assertionChanged,
|
|
1442
|
+
requirementSyncTrigger,
|
|
1443
|
+
requirementScopeUnknown: requirementTestAnalysis.scopeUnknown,
|
|
1444
|
+
inventoryComplete,
|
|
1445
|
+
...(inventoryEnvelope === undefined ? {} : { inventory: inventoryEnvelope }),
|
|
1446
|
+
reqUpdates,
|
|
1447
|
+
reqMutationNotRevision,
|
|
1448
|
+
citedReqs,
|
|
1449
|
+
changes: testChanges.map(change => ({
|
|
1450
|
+
kind: change.kind,
|
|
1451
|
+
...(change.path ? { path: change.path } : {}),
|
|
1452
|
+
...(change.from ? { from: change.from } : {}),
|
|
1453
|
+
...(change.to ? { to: change.to } : {}),
|
|
1454
|
+
oldRole: change.oldRole,
|
|
1455
|
+
newRole: change.newRole,
|
|
1456
|
+
oldGoverned: change.oldGoverned,
|
|
1457
|
+
newGoverned: change.newGoverned,
|
|
1458
|
+
contentChanged: change.contentChanged,
|
|
1459
|
+
normative: change.normative,
|
|
1460
|
+
derivedAffected: change.derivedAffected,
|
|
1461
|
+
testWithRequirement: change.testWithRequirement,
|
|
1462
|
+
testAttributionUnknown: change.testAttributionUnknown,
|
|
1463
|
+
codeChanged: change.codeChanged
|
|
1464
|
+
})),
|
|
1465
|
+
governedTouched,
|
|
1466
|
+
governedPaths: uniqueSorted(contextualChanges.filter(change => change.oldGoverned || change.newGoverned)
|
|
1467
|
+
.flatMap(changePaths)),
|
|
1468
|
+
newGovernedPaths: newGovernedFiles.map(change => change.newPath).sort(compareText),
|
|
1469
|
+
derivedAffected,
|
|
1470
|
+
derivedPaths,
|
|
1471
|
+
normativeChanged,
|
|
1472
|
+
normativeUnknown,
|
|
1473
|
+
normativePaths: uniqueSorted(contextualChanges.filter(change => change.normative === 'changed' || change.normative === 'unknown')
|
|
1474
|
+
.flatMap(changePaths)),
|
|
1475
|
+
referenceContentChanged,
|
|
1476
|
+
referencePaths: uniqueSorted(contextualChanges.filter(change => change.contentChanged
|
|
1477
|
+
&& [change.oldRole, change.newRole].includes('reference')
|
|
1478
|
+
&& (change.oldGoverned || change.newGoverned)).flatMap(changePaths)),
|
|
1479
|
+
codeChanged,
|
|
1480
|
+
codePaths: uniqueSorted(contextualChanges.filter(change => change.codeChanged).flatMap(changePaths)),
|
|
1481
|
+
requirementSyncCandidate,
|
|
1482
|
+
documentAnalysis: { base: baseAnalysis, candidate: candidateAnalysis }
|
|
1483
|
+
};
|
|
1484
|
+
const provenance = {
|
|
1485
|
+
source: 'base/candidate file snapshots',
|
|
1486
|
+
changes: contextualChanges.map(change => change.kind === 'rename'
|
|
1487
|
+
? { kind: 'rename', from: change.from, to: change.to }
|
|
1488
|
+
: { kind: change.kind, path: change.path })
|
|
1489
|
+
};
|
|
1490
|
+
return { facts, provenance };
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function deriveDocObligations(facts) {
|
|
1494
|
+
const validFacts = Boolean(facts && facts.schemaVersion === 1 && facts.inputKnown === true);
|
|
1495
|
+
const applicable = new Set();
|
|
1496
|
+
const unknown = new Set();
|
|
1497
|
+
const strengths = new Map();
|
|
1498
|
+
if (!validFacts) {
|
|
1499
|
+
for (const { capability } of CATALOG) unknown.add(capability);
|
|
1500
|
+
} else {
|
|
1501
|
+
if (facts.governedTouched) {
|
|
1502
|
+
applicable.add('doc-lint');
|
|
1503
|
+
applicable.add('link-check');
|
|
1504
|
+
}
|
|
1505
|
+
if (facts.newGovernedPaths.length) {
|
|
1506
|
+
applicable.add('update-over-create');
|
|
1507
|
+
applicable.add('outline-confirmation');
|
|
1508
|
+
const newRoles = facts.changes.filter(change => change.kind === 'added' && facts.newGovernedPaths.includes(change.path))
|
|
1509
|
+
.map(change => change.newRole);
|
|
1510
|
+
if (newRoles.some(role => role === 'prd' || role === 'adr')) strengths.set('outline-confirmation', 'MUST');
|
|
1511
|
+
else if (newRoles.some(role => !role || !['reference', 'ticket', 'log', 'report'].includes(role))) {
|
|
1512
|
+
unknown.add('outline-confirmation');
|
|
1513
|
+
strengths.set('outline-confirmation', 'MUST');
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (facts.derivedAffected) applicable.add('derived-regenerate');
|
|
1517
|
+
if (facts.normativeChanged) {
|
|
1518
|
+
applicable.add('quality-review');
|
|
1519
|
+
applicable.add('conflict-check');
|
|
1520
|
+
} else if (facts.normativeUnknown) {
|
|
1521
|
+
unknown.add('quality-review');
|
|
1522
|
+
unknown.add('conflict-check');
|
|
1523
|
+
}
|
|
1524
|
+
if (facts.referenceContentChanged) applicable.add('prose-reminder');
|
|
1525
|
+
if (facts.requirementScopeUnknown) unknown.add('requirement-sync');
|
|
1526
|
+
else if (facts.requirementSyncCandidate) applicable.add('requirement-sync');
|
|
1527
|
+
else if (facts.assertionChanged === 'unknown' || facts.codeChanged) unknown.add('requirement-sync');
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const scopePaths = {
|
|
1531
|
+
'doc-lint': validFacts ? facts.governedPaths || [] : [],
|
|
1532
|
+
'link-check': validFacts ? facts.governedPaths || [] : [],
|
|
1533
|
+
'update-over-create': validFacts ? facts.newGovernedPaths || [] : [],
|
|
1534
|
+
'outline-confirmation': validFacts ? facts.newGovernedPaths || [] : [],
|
|
1535
|
+
'derived-regenerate': validFacts ? facts.derivedPaths || [] : [],
|
|
1536
|
+
'quality-review': validFacts ? facts.normativePaths || [] : [],
|
|
1537
|
+
'conflict-check': validFacts ? facts.normativePaths || [] : [],
|
|
1538
|
+
'prose-reminder': validFacts ? facts.referencePaths || [] : [],
|
|
1539
|
+
'requirement-sync': validFacts ? uniqueSorted([...(facts.codePaths || []), ...(facts.reqCitingTests || []),
|
|
1540
|
+
...(facts.citedReqs || []).map(item => item && item.file)]) : []
|
|
1541
|
+
};
|
|
1542
|
+
const inventoryEnvelope = validFacts ? copyInventoryEnvelope(facts.inventory) : undefined;
|
|
1543
|
+
const requirementFacts = {
|
|
1544
|
+
citedReqs: validFacts && Array.isArray(facts.citedReqs) ? facts.citedReqs : [],
|
|
1545
|
+
reqUpdates: validFacts && Array.isArray(facts.reqUpdates) ? facts.reqUpdates : [],
|
|
1546
|
+
reqMutationNotRevision: validFacts && Array.isArray(facts.reqMutationNotRevision)
|
|
1547
|
+
? facts.reqMutationNotRevision : [],
|
|
1548
|
+
assertionChanged: validFacts && (typeof facts.assertionChanged === 'boolean' || facts.assertionChanged === 'unknown')
|
|
1549
|
+
? facts.assertionChanged : 'unknown',
|
|
1550
|
+
requirementSyncTrigger: validFacts && ['new-test', 'assertion-change', null].includes(facts.requirementSyncTrigger)
|
|
1551
|
+
? facts.requirementSyncTrigger : null,
|
|
1552
|
+
requirementScopeUnknown: Boolean(validFacts && facts.requirementScopeUnknown === true),
|
|
1553
|
+
inventoryComplete: Boolean(validFacts && facts.inventoryComplete === true),
|
|
1554
|
+
...(inventoryEnvelope === undefined ? {} : { inventory: inventoryEnvelope })
|
|
1555
|
+
};
|
|
1556
|
+
return CATALOG.map(item => ({
|
|
1557
|
+
id: `doc-change:${item.capability}`,
|
|
1558
|
+
capability: item.capability,
|
|
1559
|
+
strength: strengths.get(item.capability) || item.strength,
|
|
1560
|
+
applicability: applicable.has(item.capability) ? 'applicable' : unknown.has(item.capability) ? 'unknown' : 'N/A',
|
|
1561
|
+
scope: {
|
|
1562
|
+
paths: uniqueSorted(scopePaths[item.capability] || []),
|
|
1563
|
+
...(item.capability === 'requirement-sync' && requirementFacts.requirementScopeUnknown
|
|
1564
|
+
? { requirementIds: 'unknown' } : {})
|
|
1565
|
+
},
|
|
1566
|
+
acceptance: item.acceptance,
|
|
1567
|
+
...(item.capability === 'requirement-sync' ? requirementFacts : {})
|
|
1568
|
+
}));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
module.exports = { deriveDocFacts, deriveDocObligations };
|