harness-everything 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +2 -0
  4. package/AGENTS.md +3 -0
  5. package/CHANGELOG.md +42 -0
  6. package/ci/fixtures/doc-obligations/derive/acceptance-only/base/docs/prd/graph.md +22 -0
  7. package/ci/fixtures/doc-obligations/derive/acceptance-only/candidate/docs/prd/graph.md +22 -0
  8. package/ci/fixtures/doc-obligations/derive/alias-only/base/docs/prd/graph.md +24 -0
  9. package/ci/fixtures/doc-obligations/derive/alias-only/candidate/docs/prd/graph.md +24 -0
  10. package/ci/fixtures/doc-obligations/derive/assertion-no-revision/base/ci/mechanism-lint.test.js +2 -0
  11. package/ci/fixtures/doc-obligations/derive/assertion-no-revision/base/docs/prd/graph.md +20 -0
  12. package/ci/fixtures/doc-obligations/derive/assertion-no-revision/candidate/ci/mechanism-lint.test.js +2 -0
  13. package/ci/fixtures/doc-obligations/derive/assertion-no-revision/candidate/docs/prd/graph.md +20 -0
  14. package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/base/ci/mechanism-lint.test.js +2 -0
  15. package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/base/docs/prd/graph.md +20 -0
  16. package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/candidate/ci/mechanism-lint.test.js +2 -0
  17. package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/candidate/docs/prd/graph.md +20 -0
  18. package/ci/fixtures/doc-obligations/derive/assertion-no-revision-attested/inventory.json +7 -0
  19. package/ci/fixtures/doc-obligations/derive/attribution-impossible/base/ci/mechanism-open.test.js +3 -0
  20. package/ci/fixtures/doc-obligations/derive/attribution-impossible/candidate/ci/mechanism-open.test.js.fixture +2 -0
  21. package/ci/fixtures/doc-obligations/derive/clause-context-typo/base/docs/prd/graph.md +24 -0
  22. package/ci/fixtures/doc-obligations/derive/clause-context-typo/candidate/docs/prd/graph.md +24 -0
  23. package/ci/fixtures/doc-obligations/derive/code-only/base/src/lint.js +1 -0
  24. package/ci/fixtures/doc-obligations/derive/code-only/candidate/src/lint.js +1 -0
  25. package/ci/fixtures/doc-obligations/derive/comment-only-test/base/ci/mechanism-lint.test.js +2 -0
  26. package/ci/fixtures/doc-obligations/derive/comment-only-test/candidate/ci/mechanism-lint.test.js +2 -0
  27. package/ci/fixtures/doc-obligations/derive/computed-member-assert/base/ci/mechanism-c1.test.js +2 -0
  28. package/ci/fixtures/doc-obligations/derive/computed-member-assert/candidate/ci/mechanism-c1.test.js +2 -0
  29. package/ci/fixtures/doc-obligations/derive/computed-member-variable/base/ci/mechanism-c1.test.js +2 -0
  30. package/ci/fixtures/doc-obligations/derive/computed-member-variable/candidate/ci/mechanism-c1.test.js +2 -0
  31. package/ci/fixtures/doc-obligations/derive/config.json +11 -0
  32. package/ci/fixtures/doc-obligations/derive/decision-fence-backtick/base/docs/adr/GRPH-010.md +20 -0
  33. package/ci/fixtures/doc-obligations/derive/decision-fence-backtick/candidate/docs/adr/GRPH-010.md +20 -0
  34. package/ci/fixtures/doc-obligations/derive/decision-fence-tilde/base/docs/adr/GRPH-010.md +20 -0
  35. package/ci/fixtures/doc-obligations/derive/decision-fence-tilde/candidate/docs/adr/GRPH-010.md +20 -0
  36. package/ci/fixtures/doc-obligations/derive/deep-strict-equal/base/ci/mechanism-s1.test.js +2 -0
  37. package/ci/fixtures/doc-obligations/derive/deep-strict-equal/candidate/ci/mechanism-s1.test.js +2 -0
  38. package/ci/fixtures/doc-obligations/derive/deleted-adr/base/docs/adr/GRPH-008.md +14 -0
  39. package/ci/fixtures/doc-obligations/derive/deleted-prd/base/ci/mechanism-lint.test.js +2 -0
  40. package/ci/fixtures/doc-obligations/derive/deleted-prd/base/docs/prd/graph.md +20 -0
  41. package/ci/fixtures/doc-obligations/derive/deleted-prd/candidate/ci/mechanism-lint.test.js +2 -0
  42. package/ci/fixtures/doc-obligations/derive/derived-rename/base/docs/index.md +3 -0
  43. package/ci/fixtures/doc-obligations/derive/derived-rename/base/src/model/a.js +1 -0
  44. package/ci/fixtures/doc-obligations/derive/derived-rename/candidate/docs/index.md +3 -0
  45. package/ci/fixtures/doc-obligations/derive/derived-rename/candidate/src/model/b.js +1 -0
  46. package/ci/fixtures/doc-obligations/derive/derived-rename/config.json +7 -0
  47. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/base/docs/index.md +3 -0
  48. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/base/docs/model-summary.md +3 -0
  49. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/base/src/model.js +1 -0
  50. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/candidate/docs/index.md +3 -0
  51. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/candidate/docs/model-summary.md +3 -0
  52. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/candidate/src/model.js +1 -0
  53. package/ci/fixtures/doc-obligations/derive/derived-two-outputs/config.json +10 -0
  54. package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/base/ci/mechanism-lint.test.js +2 -0
  55. package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/base/docs/prd/graph-one/r001.md +19 -0
  56. package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/base/docs/prd/graph-two/r001.md +19 -0
  57. package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/candidate/ci/mechanism-lint.test.js +2 -0
  58. package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/candidate/docs/prd/graph-one/r001.md +19 -0
  59. package/ci/fixtures/doc-obligations/derive/distinct-logical-prds/candidate/docs/prd/graph-two/r001.md +19 -0
  60. package/ci/fixtures/doc-obligations/derive/docs-only/base/docs/reference/guide.md +3 -0
  61. package/ci/fixtures/doc-obligations/derive/docs-only/candidate/docs/reference/guide.md +3 -0
  62. package/ci/fixtures/doc-obligations/derive/dup-req-id/base/ci/mechanism-lint.test.js +2 -0
  63. package/ci/fixtures/doc-obligations/derive/dup-req-id/base/docs/prd/graph-a.md +20 -0
  64. package/ci/fixtures/doc-obligations/derive/dup-req-id/base/docs/prd/graph-b.md +20 -0
  65. package/ci/fixtures/doc-obligations/derive/dup-req-id/candidate/ci/mechanism-lint.test.js +2 -0
  66. package/ci/fixtures/doc-obligations/derive/dup-req-id/candidate/docs/prd/graph-a.md +20 -0
  67. package/ci/fixtures/doc-obligations/derive/dup-req-id/candidate/docs/prd/graph-b.md +20 -0
  68. package/ci/fixtures/doc-obligations/derive/expect-computed-literal/base/ci/mechanism-e.test.js +2 -0
  69. package/ci/fixtures/doc-obligations/derive/expect-computed-literal/candidate/ci/mechanism-e.test.js +2 -0
  70. package/ci/fixtures/doc-obligations/derive/expect-computed-matcher/base/ci/mechanism-e.test.js +2 -0
  71. package/ci/fixtures/doc-obligations/derive/expect-computed-matcher/candidate/ci/mechanism-e.test.js +2 -0
  72. package/ci/fixtures/doc-obligations/derive/expect-not-to-equal/base/ci/mechanism-s1.test.js +2 -0
  73. package/ci/fixtures/doc-obligations/derive/expect-not-to-equal/candidate/ci/mechanism-s1.test.js +2 -0
  74. package/ci/fixtures/doc-obligations/derive/formatting-only-test/base/ci/mechanism-lint.test.js +2 -0
  75. package/ci/fixtures/doc-obligations/derive/formatting-only-test/candidate/ci/mechanism-lint.test.js +2 -0
  76. package/ci/fixtures/doc-obligations/derive/helper-delegated-test/base/docs/prd/graph.md +18 -0
  77. package/ci/fixtures/doc-obligations/derive/helper-delegated-test/candidate/ci/mechanism-helper.test.js +1 -0
  78. package/ci/fixtures/doc-obligations/derive/helper-delegated-test/candidate/docs/prd/graph.md +18 -0
  79. package/ci/fixtures/doc-obligations/derive/historical-only-req/base/ci/mechanism-lint.test.js +2 -0
  80. package/ci/fixtures/doc-obligations/derive/historical-only-req/base/docs/prd/graph/r001.md +25 -0
  81. package/ci/fixtures/doc-obligations/derive/historical-only-req/candidate/ci/mechanism-lint.test.js +2 -0
  82. package/ci/fixtures/doc-obligations/derive/historical-only-req/candidate/docs/prd/graph/r001.md +25 -0
  83. package/ci/fixtures/doc-obligations/derive/historical-only-req/candidate/docs/prd/graph/r002.md +23 -0
  84. package/ci/fixtures/doc-obligations/derive/in-place-current/base/ci/mechanism-inplace.test.js +2 -0
  85. package/ci/fixtures/doc-obligations/derive/in-place-current/base/docs/prd/graph/r001.md +20 -0
  86. package/ci/fixtures/doc-obligations/derive/in-place-current/base/docs/prd/graph/r002.md +26 -0
  87. package/ci/fixtures/doc-obligations/derive/in-place-current/candidate/ci/mechanism-inplace.test.js +2 -0
  88. package/ci/fixtures/doc-obligations/derive/in-place-current/candidate/docs/prd/graph/r001.md +20 -0
  89. package/ci/fixtures/doc-obligations/derive/in-place-current/candidate/docs/prd/graph/r002.md +28 -0
  90. package/ci/fixtures/doc-obligations/derive/in-place-historical/base/ci/mechanism-inplace.test.js +2 -0
  91. package/ci/fixtures/doc-obligations/derive/in-place-historical/base/docs/prd/graph/r001.md +20 -0
  92. package/ci/fixtures/doc-obligations/derive/in-place-historical/base/docs/prd/graph/r002.md +26 -0
  93. package/ci/fixtures/doc-obligations/derive/in-place-historical/candidate/ci/mechanism-inplace.test.js +2 -0
  94. package/ci/fixtures/doc-obligations/derive/in-place-historical/candidate/docs/prd/graph/r001.md +22 -0
  95. package/ci/fixtures/doc-obligations/derive/in-place-historical/candidate/docs/prd/graph/r002.md +26 -0
  96. package/ci/fixtures/doc-obligations/derive/invalid-clause/base/docs/prd/graph.md +24 -0
  97. package/ci/fixtures/doc-obligations/derive/invalid-clause/candidate/docs/prd/graph.md +24 -0
  98. package/ci/fixtures/doc-obligations/derive/legacy-untagged-fallback/base/ci/mechanism-legacy.test.js +6 -0
  99. package/ci/fixtures/doc-obligations/derive/legacy-untagged-fallback/candidate/ci/mechanism-legacy.test.js +6 -0
  100. package/ci/fixtures/doc-obligations/derive/literal-whitespace-assert/base/ci/mechanism-lint.test.js +2 -0
  101. package/ci/fixtures/doc-obligations/derive/literal-whitespace-assert/candidate/ci/mechanism-lint.test.js +2 -0
  102. package/ci/fixtures/doc-obligations/derive/malformed-citation-attribution/base/ci/mechanism-bad.test.js +6 -0
  103. package/ci/fixtures/doc-obligations/derive/malformed-citation-attribution/candidate/ci/mechanism-bad.test.js.fixture +6 -0
  104. package/ci/fixtures/doc-obligations/derive/malformed-prd/base/ci/mechanism-lint.test.js +2 -0
  105. package/ci/fixtures/doc-obligations/derive/malformed-prd/base/docs/prd/graph.md +20 -0
  106. package/ci/fixtures/doc-obligations/derive/malformed-prd/candidate/ci/mechanism-lint.test.js +2 -0
  107. package/ci/fixtures/doc-obligations/derive/malformed-prd/candidate/docs/prd/graph.md +11 -0
  108. package/ci/fixtures/doc-obligations/derive/missing-approval/base/docs/prd/graph.md +20 -0
  109. package/ci/fixtures/doc-obligations/derive/missing-approval/candidate/docs/prd/graph.md +20 -0
  110. package/ci/fixtures/doc-obligations/derive/mixed-attribution/base/ci/mechanism-mixed.test.js +6 -0
  111. package/ci/fixtures/doc-obligations/derive/mixed-attribution/base/docs/prd/graph.md +24 -0
  112. package/ci/fixtures/doc-obligations/derive/mixed-attribution/candidate/ci/mechanism-mixed.test.js.fixture +5 -0
  113. package/ci/fixtures/doc-obligations/derive/mixed-attribution/candidate/docs/prd/graph.md +24 -0
  114. package/ci/fixtures/doc-obligations/derive/mixed-docs-code/base/ci/mechanism-lint.test.js +2 -0
  115. package/ci/fixtures/doc-obligations/derive/mixed-docs-code/base/docs/reference/guide.md +3 -0
  116. package/ci/fixtures/doc-obligations/derive/mixed-docs-code/base/src/lint.js +1 -0
  117. package/ci/fixtures/doc-obligations/derive/mixed-docs-code/candidate/ci/mechanism-lint.test.js +2 -0
  118. package/ci/fixtures/doc-obligations/derive/mixed-docs-code/candidate/docs/reference/guide.md +3 -0
  119. package/ci/fixtures/doc-obligations/derive/mixed-docs-code/candidate/src/lint.js +1 -0
  120. package/ci/fixtures/doc-obligations/derive/multiline-comment-only/base/ci/mechanism-multi.test.js +5 -0
  121. package/ci/fixtures/doc-obligations/derive/multiline-comment-only/candidate/ci/mechanism-multi.test.js +5 -0
  122. package/ci/fixtures/doc-obligations/derive/multiline-expect/base/ci/mechanism-multi.test.js +6 -0
  123. package/ci/fixtures/doc-obligations/derive/multiline-expect/candidate/ci/mechanism-multi.test.js +6 -0
  124. package/ci/fixtures/doc-obligations/derive/multiline-formatting-only/base/ci/mechanism-multi.test.js +5 -0
  125. package/ci/fixtures/doc-obligations/derive/multiline-formatting-only/candidate/ci/mechanism-multi.test.js +5 -0
  126. package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/base/ci/mechanism-multi.test.js +5 -0
  127. package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/base/docs/prd/graph.md +18 -0
  128. package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/candidate/ci/mechanism-multi.test.js +5 -0
  129. package/ci/fixtures/doc-obligations/derive/multiline-numeric-prd/candidate/docs/prd/graph.md +18 -0
  130. package/ci/fixtures/doc-obligations/derive/multiline-strictequal/base/ci/mechanism-multi.test.js +5 -0
  131. package/ci/fixtures/doc-obligations/derive/multiline-strictequal/candidate/ci/mechanism-multi.test.js +5 -0
  132. package/ci/fixtures/doc-obligations/derive/new-prd/candidate/docs/prd/graph-new.md +20 -0
  133. package/ci/fixtures/doc-obligations/derive/new-reference/candidate/docs/reference/new-note.md +3 -0
  134. package/ci/fixtures/doc-obligations/derive/new-req-test/candidate/ci/mechanism-new.test.js +2 -0
  135. package/ci/fixtures/doc-obligations/derive/orphan-req-test/base/ci/mechanism-lint.test.js +2 -0
  136. package/ci/fixtures/doc-obligations/derive/orphan-req-test/base/docs/prd/graph.md +20 -0
  137. package/ci/fixtures/doc-obligations/derive/orphan-req-test/candidate/ci/mechanism-lint.test.js +2 -0
  138. package/ci/fixtures/doc-obligations/derive/orphan-req-test/candidate/docs/prd/graph.md +20 -0
  139. package/ci/fixtures/doc-obligations/derive/partial-revision/base/ci/mechanism-partial.test.js +5 -0
  140. package/ci/fixtures/doc-obligations/derive/partial-revision/base/docs/prd/graph/r001.md +24 -0
  141. package/ci/fixtures/doc-obligations/derive/partial-revision/candidate/ci/mechanism-partial.test.js +5 -0
  142. package/ci/fixtures/doc-obligations/derive/partial-revision/candidate/docs/prd/graph/r001.md +24 -0
  143. package/ci/fixtures/doc-obligations/derive/partial-revision/candidate/docs/prd/graph/r002.md +26 -0
  144. package/ci/fixtures/doc-obligations/derive/regex-literal-whitespace/base/ci/mechanism-lint.test.js +2 -0
  145. package/ci/fixtures/doc-obligations/derive/regex-literal-whitespace/candidate/ci/mechanism-lint.test.js +2 -0
  146. package/ci/fixtures/doc-obligations/derive/rename/base/docs/reference/old-guide.md +3 -0
  147. package/ci/fixtures/doc-obligations/derive/rename/candidate/docs/reference/guide.md +3 -0
  148. package/ci/fixtures/doc-obligations/derive/replacement-target/base/docs/prd/graph.md +24 -0
  149. package/ci/fixtures/doc-obligations/derive/replacement-target/candidate/docs/prd/graph.md +24 -0
  150. package/ci/fixtures/doc-obligations/derive/req-context-typo/base/docs/prd/graph.md +26 -0
  151. package/ci/fixtures/doc-obligations/derive/req-context-typo/candidate/docs/prd/graph.md +26 -0
  152. package/ci/fixtures/doc-obligations/derive/req-revision/base/ci/mechanism-lint.test.js +2 -0
  153. package/ci/fixtures/doc-obligations/derive/req-revision/base/docs/prd/graph/r001.md +20 -0
  154. package/ci/fixtures/doc-obligations/derive/req-revision/candidate/ci/mechanism-lint.test.js +2 -0
  155. package/ci/fixtures/doc-obligations/derive/req-revision/candidate/docs/prd/graph/r001.md +20 -0
  156. package/ci/fixtures/doc-obligations/derive/req-revision/candidate/docs/prd/graph/r002.md +28 -0
  157. package/ci/fixtures/doc-obligations/derive/req-revision-in-place/base/docs/prd/graph/r001.md +20 -0
  158. package/ci/fixtures/doc-obligations/derive/req-revision-in-place/candidate/docs/prd/graph/r001.md +22 -0
  159. package/ci/fixtures/doc-obligations/derive/retained-revision-chain/base/ci/mechanism-lint.test.js +2 -0
  160. package/ci/fixtures/doc-obligations/derive/retained-revision-chain/base/docs/prd/graph/r001.md +20 -0
  161. package/ci/fixtures/doc-obligations/derive/retained-revision-chain/base/docs/prd/graph/r002.md +26 -0
  162. package/ci/fixtures/doc-obligations/derive/retained-revision-chain/candidate/ci/mechanism-lint.test.js +2 -0
  163. package/ci/fixtures/doc-obligations/derive/retained-revision-chain/candidate/docs/prd/graph/r001.md +20 -0
  164. package/ci/fixtures/doc-obligations/derive/retained-revision-chain/candidate/docs/prd/graph/r002.md +26 -0
  165. package/ci/fixtures/doc-obligations/derive/string-literal-tag/base/ci/mechanism-str.test.js +2 -0
  166. package/ci/fixtures/doc-obligations/derive/string-literal-tag/candidate/ci/mechanism-str.test.js +2 -0
  167. package/ci/fixtures/doc-obligations/derive/tag-inside-test-call/base/ci/mechanism-inside.test.js +5 -0
  168. package/ci/fixtures/doc-obligations/derive/tag-inside-test-call/candidate/ci/mechanism-inside.test.js +5 -0
  169. package/ci/fixtures/doc-obligations/derive/tagged-test/base/ci/mechanism-lint.test.js +2 -0
  170. package/ci/fixtures/doc-obligations/derive/tagged-test/candidate/ci/mechanism-lint.test.js +2 -0
  171. package/ci/fixtures/doc-obligations/derive/tagged-test-prd/base/ci/mechanism-lint.test.js +2 -0
  172. package/ci/fixtures/doc-obligations/derive/tagged-test-prd/base/docs/prd/graph.md +20 -0
  173. package/ci/fixtures/doc-obligations/derive/tagged-test-prd/candidate/ci/mechanism-lint.test.js +2 -0
  174. package/ci/fixtures/doc-obligations/derive/tagged-test-prd/candidate/docs/prd/graph.md +20 -0
  175. package/ci/fixtures/doc-obligations/derive/template-literal-tag/base/ci/mechanism-tpl.test.js +4 -0
  176. package/ci/fixtures/doc-obligations/derive/template-literal-tag/base/docs/prd/graph.md +18 -0
  177. package/ci/fixtures/doc-obligations/derive/template-literal-tag/candidate/ci/mechanism-tpl.test.js +4 -0
  178. package/ci/fixtures/doc-obligations/derive/template-literal-tag/candidate/docs/prd/graph.md +18 -0
  179. package/ci/fixtures/doc-obligations/derive/template-literal-whitespace/base/ci/mechanism-lint.test.js +2 -0
  180. package/ci/fixtures/doc-obligations/derive/template-literal-whitespace/candidate/ci/mechanism-lint.test.js +2 -0
  181. package/ci/fixtures/doc-obligations/derive/title-only-rename/base/ci/mechanism-title.test.js +3 -0
  182. package/ci/fixtures/doc-obligations/derive/title-only-rename/candidate/ci/mechanism-title.test.js +3 -0
  183. package/ci/fixtures/doc-obligations/derive/title-only-unrelated-computed/base/ci/mechanism-u.test.js +4 -0
  184. package/ci/fixtures/doc-obligations/derive/title-only-unrelated-computed/candidate/ci/mechanism-u.test.js +4 -0
  185. package/ci/fixtures/doc-obligations/derive/top-level-no-file-tag/base/ci/mechanism-top.test.js +4 -0
  186. package/ci/fixtures/doc-obligations/derive/top-level-no-file-tag/candidate/ci/mechanism-top.test.js +4 -0
  187. package/ci/fixtures/doc-obligations/derive/trailing-comment-test/base/ci/mechanism-lint.test.js +2 -0
  188. package/ci/fixtures/doc-obligations/derive/trailing-comment-test/base/docs/prd/graph.md +20 -0
  189. package/ci/fixtures/doc-obligations/derive/trailing-comment-test/candidate/ci/mechanism-lint.test.js +2 -0
  190. package/ci/fixtures/doc-obligations/derive/trailing-comment-test/candidate/docs/prd/graph.md +20 -0
  191. package/ci/fixtures/doc-obligations/derive/two-tests-one-file/base/ci/mechanism-two.test.js +6 -0
  192. package/ci/fixtures/doc-obligations/derive/two-tests-one-file/base/docs/prd/graph.md +24 -0
  193. package/ci/fixtures/doc-obligations/derive/two-tests-one-file/candidate/ci/mechanism-two.test.js +6 -0
  194. package/ci/fixtures/doc-obligations/derive/two-tests-one-file/candidate/docs/prd/graph.md +24 -0
  195. package/ci/fixtures/doc-obligations/derive/typo-context/base/docs/adr/GRPH-007.md +14 -0
  196. package/ci/fixtures/doc-obligations/derive/typo-context/candidate/docs/adr/GRPH-007.md +14 -0
  197. package/ci/fixtures/doc-obligations/derive/typo-decision/base/docs/adr/GRPH-007.md +14 -0
  198. package/ci/fixtures/doc-obligations/derive/typo-decision/candidate/docs/adr/GRPH-007.md +14 -0
  199. package/ci/fixtures/doc-obligations/derive/unbalanced-statement/base/ci/mechanism-multi.test.js +5 -0
  200. package/ci/fixtures/doc-obligations/derive/unbalanced-statement/candidate/ci/mechanism-multi.test.js.fixture +4 -0
  201. package/ci/fixtures/doc-obligations/derive/unknown-adr/base/docs/adr/GRPH-009.md +14 -0
  202. package/ci/fixtures/doc-obligations/derive/unknown-adr/candidate/docs/adr/GRPH-009.md +12 -0
  203. package/ci/fixtures/doc-obligations/derive/unsupported-assertion-syntax/base/ci/mechanism-s1.test.js +2 -0
  204. package/ci/fixtures/doc-obligations/derive/unsupported-assertion-syntax/candidate/ci/mechanism-s1.test.js +2 -0
  205. package/ci/fixtures/doc-obligations/derive/whitespace-only-test/base/ci/mechanism-lint.test.js +2 -0
  206. package/ci/fixtures/doc-obligations/derive/whitespace-only-test/candidate/ci/mechanism-lint.test.js +2 -0
  207. package/ci/fixtures/doc-obligations/evidence/base/docs/adr/GRPH-007.md +15 -0
  208. package/ci/fixtures/doc-obligations/evidence/base/docs/prd/PRD-GRPH-001.md +25 -0
  209. package/ci/fixtures/doc-obligations/evidence/candidate/docs/adr/GRPH-007.md +15 -0
  210. package/ci/fixtures/doc-obligations/evidence/candidate/docs/prd/PRD-GRPH-001.md +25 -0
  211. package/ci/mechanism-325-3a-cross.test.js +795 -0
  212. package/ci/mechanism-325-3a-doc-evidence.test.js +1172 -0
  213. package/ci/mechanism-325-3a-doc-obligations.test.js +742 -0
  214. package/ci/mechanism-36-verify-timeout.test.js +534 -0
  215. package/ci/mechanism-37-portability-lint.test.js +71 -0
  216. package/ci/mechanism-38-verify-all.test.js +112 -0
  217. package/ci/mechanism-39-opencode-verify-outcomes.test.js +374 -0
  218. package/ci/mechanism-pr334-haiku.test.js +298 -0
  219. package/ci/mechanism-pr334-luna.test.js +192 -0
  220. package/ci/mechanism-pr334-opencode.test.js +313 -0
  221. package/ci/mechanism-pr334-r2-boundary.test.js +239 -0
  222. package/ci/mechanism-pr334-r2-resolution.test.js +243 -0
  223. package/ci/mechanism-pr334-r2-scope.test.js +127 -0
  224. package/ci/mechanism-pr334-r3-boundary.test.js +356 -0
  225. package/ci/mechanism-pr334-r3-citations.test.js +205 -0
  226. package/ci/mechanism-pr334-r3-headings.test.js +268 -0
  227. package/ci/portability-lint.js +288 -0
  228. package/docs/document-lifecycle.md +57 -1
  229. package/docs/repository-contract.md +1 -1
  230. package/harness-everything/SKILL.md +1 -1
  231. package/harness-everything/scripts/doc-evidence.js +471 -0
  232. package/harness-everything/scripts/doc-lifecycle.js +1 -1
  233. package/harness-everything/scripts/doc-obligations.js +1571 -0
  234. package/opencode-plugin/README.md +33 -2
  235. package/opencode-plugin/hooks/verify.js +87 -11
  236. package/opencode-plugin/index.mjs +216 -25
  237. package/opencode-plugin/plugin.json +1 -1
  238. package/package.json +3 -1
  239. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  240. package/plugins/harness-everything/plugin.json +1 -1
  241. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  242. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +471 -0
  243. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +1 -1
  244. package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +1571 -0
  245. package/sbom.cdx.json +7 -7
  246. package/scripts/verify-all.js +314 -0
@@ -0,0 +1,742 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const helper = require('./test-helper');
7
+
8
+ const modulePath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-obligations.js');
9
+ const fixtureDir = path.join(__dirname, 'fixtures', 'doc-obligations', 'derive');
10
+ let deriveDocFacts;
11
+ let deriveDocObligations;
12
+ try {
13
+ ({ deriveDocFacts, deriveDocObligations } = require(modulePath));
14
+ } catch (error) {
15
+ helper.check('325-3a. derive module is available', false, error.message);
16
+ }
17
+
18
+ console.log('\n[325-3a] doc-change obligation derivation (RED)...');
19
+
20
+ // Full catalog of doc-change obligation capabilities, with the default strength.
21
+ // Every case returns one entry per capability; applicability says whether it is
22
+ // triggered. Entries that are not triggered carry applicability "N/A".
23
+ const CATALOG = Object.freeze({
24
+ 'doc-lint': 'MUST',
25
+ 'link-check': 'MUST',
26
+ 'update-over-create': 'SHOULD',
27
+ 'outline-confirmation': 'MUST',
28
+ 'derived-regenerate': 'MUST',
29
+ 'quality-review': 'MUST',
30
+ 'conflict-check': 'MUST',
31
+ 'prose-reminder': 'SHOULD',
32
+ 'requirement-sync': 'MUST'
33
+ });
34
+
35
+ const CONFIG = JSON.parse(fs.readFileSync(path.join(fixtureDir, 'config.json'), 'utf8'));
36
+
37
+ function readTree(root) {
38
+ const files = {};
39
+ if (!fs.existsSync(root)) return files;
40
+ const walk = (dir) => {
41
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ const full = path.join(dir, entry.name);
43
+ if (entry.isDirectory()) walk(full);
44
+ else {
45
+ // A malformed payload is stored as <name>.fixture (so node --check skips it); it is read back as <name>.
46
+ const rel = path.relative(root, full).split(path.sep).join('/');
47
+ files[rel.endsWith('.fixture') ? rel.slice(0, -'.fixture'.length) : rel] = fs.readFileSync(full, 'utf8');
48
+ }
49
+ }
50
+ };
51
+ walk(root);
52
+ return files;
53
+ }
54
+
55
+ function loadCase(name) {
56
+ const caseDir = path.join(fixtureDir, name);
57
+ return { base: { files: readTree(path.join(caseDir, 'base')) }, candidate: { files: readTree(path.join(caseDir, 'candidate')) } };
58
+ }
59
+
60
+ // A case may carry its own config.json (used by the derived-view controls); other cases use the shared config.
61
+ function caseConfig(name) {
62
+ const own = path.join(fixtureDir, name, 'config.json');
63
+ return fs.existsSync(own) ? JSON.parse(fs.readFileSync(own, 'utf8')) : CONFIG;
64
+ }
65
+
66
+ function deriveCase(name) {
67
+ const input = loadCase(name);
68
+ const result = deriveDocFacts({ base: input.base, candidate: input.candidate, config: caseConfig(name) });
69
+ const obligations = deriveDocObligations(result.facts);
70
+ return { result, obligations };
71
+ }
72
+
73
+ // expected: { applicable: [...], unknown: [...], strength: { capability: 'MUST'|'SHOULD' } }
74
+ // Capabilities not listed in applicable/unknown must come back as N/A.
75
+ function assertCase(name, expected) {
76
+ const { obligations } = deriveCase(name);
77
+ assert.strictEqual(obligations.length, Object.keys(CATALOG).length, `${name}: one entry per catalog capability`);
78
+
79
+ const ids = obligations.map(item => item.id);
80
+ assert.strictEqual(new Set(ids).size, ids.length, `${name}: obligation ids are unique`);
81
+ for (const item of obligations) {
82
+ assert.ok(typeof item.id === 'string' && item.id.length > 0, `${name}: id is non-empty`);
83
+ assert.ok(Array.isArray(item.scope?.paths) && (item.applicability === 'N/A' || item.scope.paths.length > 0), `${name}: ${item.capability} scope.paths is an array, non-empty unless N/A`);
84
+ assert.ok(typeof item.acceptance === 'string' && item.acceptance.length > 0, `${name}: ${item.capability} acceptance is non-empty`);
85
+ }
86
+
87
+ const byCapability = new Map(obligations.map(item => [item.capability, item]));
88
+ const applicable = new Set(expected.applicable || []);
89
+ const unknown = new Set(expected.unknown || []);
90
+ for (const capability of Object.keys(CATALOG)) {
91
+ const item = byCapability.get(capability);
92
+ assert.ok(item, `${name}: catalog capability ${capability} is present`);
93
+ const want = applicable.has(capability) ? 'applicable' : unknown.has(capability) ? 'unknown' : 'N/A';
94
+ assert.strictEqual(item.applicability, want, `${name}: ${capability} applicability`);
95
+ if (want !== 'N/A') {
96
+ const strength = (expected.strength && expected.strength[capability]) || CATALOG[capability];
97
+ assert.strictEqual(item.strength, strength, `${name}: ${capability} strength`);
98
+ }
99
+ }
100
+ }
101
+
102
+ function checkCase(name, run) {
103
+ try {
104
+ run();
105
+ helper.check(name, true);
106
+ } catch (error) {
107
+ helper.check(name, false, error && error.message ? error.message : String(error));
108
+ }
109
+ }
110
+
111
+ // H4 table cases. Expectations are written from the spec (D5 obligations table,
112
+ // D6.5 diff-fact matrix, D6.6 requirement-sync rules), not from an implementation.
113
+
114
+ checkCase('325-3a H4 docs-only: lint, links and prose reminder; requirement-sync not forced (no TDD)', () => {
115
+ assertCase('docs-only', { applicable: ['doc-lint', 'link-check', 'prose-reminder'] });
116
+ });
117
+
118
+ checkCase('325-3a H4 mixed docs + code with REQ-citing test: requirement-sync is applicable', () => {
119
+ assertCase('mixed-docs-code', { applicable: ['doc-lint', 'link-check', 'prose-reminder', 'requirement-sync'] });
120
+ });
121
+
122
+ checkCase('325-3a H4 tagged test change (REQ-citing, assertion changed): requirement-sync candidate signal', () => {
123
+ assertCase('tagged-test', { applicable: ['requirement-sync'] });
124
+ });
125
+
126
+ checkCase('325-3a H4 typo-only in ADR Context: non-normative, only lint and links', () => {
127
+ assertCase('typo-context', { applicable: ['doc-lint', 'link-check'] });
128
+ });
129
+
130
+ checkCase('325-3a H4 typo in ADR Decision: normative, adds quality review and conflict check', () => {
131
+ assertCase('typo-decision', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
132
+ });
133
+
134
+ checkCase('325-3a H4 code-only uncertainty: requirement-sync unknown, never trivial', () => {
135
+ assertCase('code-only', { unknown: ['requirement-sync'] });
136
+ });
137
+
138
+ checkCase('325-3a H4 missing approval: normative PRD REQ change keeps quality review MUST applicable', () => {
139
+ assertCase('missing-approval', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
140
+ });
141
+
142
+ checkCase('325-3a untracked new PRD: outline confirmation is MUST for PRD, plus normative checks', () => {
143
+ assertCase('new-prd', {
144
+ applicable: ['doc-lint', 'link-check', 'update-over-create', 'outline-confirmation', 'quality-review', 'conflict-check'],
145
+ strength: { 'outline-confirmation': 'MUST' }
146
+ });
147
+ });
148
+
149
+ checkCase('325-3a untracked new reference doc: outline confirmation is only SHOULD outside PRD/ADR', () => {
150
+ assertCase('new-reference', {
151
+ applicable: ['doc-lint', 'link-check', 'update-over-create', 'outline-confirmation', 'prose-reminder'],
152
+ strength: { 'outline-confirmation': 'SHOULD' }
153
+ });
154
+ });
155
+
156
+ checkCase('325-3a rename with identical content: not a new file, lint and links only', () => {
157
+ assertCase('rename', { applicable: ['doc-lint', 'link-check'] });
158
+ });
159
+
160
+ checkCase('325-3a rename is reported in provenance as a move, not as separate add and delete', () => {
161
+ const { result } = deriveCase('rename');
162
+ const provenance = JSON.stringify(result.provenance);
163
+ assert.ok(/rename/i.test(provenance), `provenance records a rename: ${provenance}`);
164
+ assert.ok(provenance.includes('docs/reference/old-guide.md') && provenance.includes('docs/reference/guide.md'),
165
+ `provenance cites both rename endpoints: ${provenance}`);
166
+ });
167
+
168
+ checkCase('325-3a deletion of an accepted ADR (base-only path): removed Decision is normative', () => {
169
+ assertCase('deleted-adr', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
170
+ });
171
+
172
+ checkCase('325-3a unknown normative section keeps triggered checks, never trivial', () => {
173
+ assertCase('unknown-adr', {
174
+ applicable: ['doc-lint', 'link-check'],
175
+ unknown: ['quality-review', 'conflict-check']
176
+ });
177
+ });
178
+
179
+ checkCase('325-3a derivation is deterministic and does not mutate its inputs', () => {
180
+ const input = loadCase('mixed-docs-code');
181
+ const snapshot = JSON.stringify(input);
182
+ const first = deriveDocFacts({ base: input.base, candidate: input.candidate, config: CONFIG });
183
+ const second = deriveDocFacts({ base: input.base, candidate: input.candidate, config: CONFIG });
184
+ assert.deepStrictEqual(second, first, 'same inputs produce the same facts and provenance');
185
+ assert.deepStrictEqual(deriveDocObligations(first.facts), deriveDocObligations(second.facts), 'same facts produce the same obligations');
186
+ assert.strictEqual(JSON.stringify(input), snapshot, 'inputs are not mutated');
187
+ });
188
+
189
+ // Round 2 (§9A, §9B, §9C). Derivation expectations only; evidence is in the cross and evidence suites.
190
+
191
+ checkCase('325-3a acceptance-only edit under a final REQ nested #### Acceptance: quality-review and conflict-check applicable', () => {
192
+ assertCase('acceptance-only', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
193
+ });
194
+
195
+ checkCase('325-3a unchanged REQ with nested acceptance plus an unrelated Context typo: lint and links only', () => {
196
+ assertCase('req-context-typo', { applicable: ['doc-lint', 'link-check'] });
197
+ });
198
+
199
+ checkCase('325-3a requirement-sync scope names the cited PRD and the REQ-citing test (§9A)', () => {
200
+ const { obligations } = deriveCase('req-revision');
201
+ const item = obligations.find(o => o.capability === 'requirement-sync');
202
+ assert.ok(item, 'requirement-sync is in the catalog');
203
+ assert.strictEqual(item.applicability, 'applicable', 'a REQ-citing test change triggers requirement-sync');
204
+ assert.ok(item.scope.paths.includes('docs/prd/graph/r002.md'), `scope names the PRD holding the cited REQ: ${item.scope.paths}`);
205
+ assert.ok(item.scope.paths.includes('ci/mechanism-lint.test.js'), `scope names the REQ-citing test: ${item.scope.paths}`);
206
+ });
207
+
208
+ checkCase('325-3a requirement-sync scope names the REQ-citing test for a tagged-test-only change (§9A)', () => {
209
+ const { obligations } = deriveCase('tagged-test');
210
+ const item = obligations.find(o => o.capability === 'requirement-sync');
211
+ assert.ok(item.scope.paths.includes('ci/mechanism-lint.test.js'), `scope names the test: ${item.scope.paths}`);
212
+ });
213
+
214
+ checkCase('325-3a one changed source feeding two derived views: derived-regenerate scope names both outputs (§9C)', () => {
215
+ const { obligations } = deriveCase('derived-two-outputs');
216
+ const item = obligations.find(o => o.capability === 'derived-regenerate');
217
+ assert.ok(item, 'derived-regenerate is in the catalog');
218
+ assert.strictEqual(item.applicability, 'applicable', 'a changed derived source triggers regeneration');
219
+ assert.strictEqual(item.strength, 'MUST');
220
+ assert.ok(item.scope.paths.includes('docs/index.md'), `scope names the first declared output: ${item.scope.paths}`);
221
+ assert.ok(item.scope.paths.includes('docs/model-summary.md'), `scope names the second declared output: ${item.scope.paths}`);
222
+ });
223
+
224
+ // Round 3 (final review round 2, finding 1): heading clauses are part of the REQ's normative
225
+ // identity. The parser output (validClause, replaces, aliases) must reach the normative comparison.
226
+
227
+ checkCase('325-3a replacement target changed (REQ-GRPH-012 -> REQ-GRPH-013), id/title/body unchanged: quality-review and conflict-check applicable', () => {
228
+ assertCase('replacement-target', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
229
+ });
230
+
231
+ checkCase('325-3a alias-only clause change (aliases REQ-GRAPH-020 -> REQ-GRAPH-021), body unchanged: quality-review and conflict-check applicable', () => {
232
+ assertCase('alias-only', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
233
+ });
234
+
235
+ checkCase('325-3a invalid replacement clause stays visible: uninterpretable clause is unknown, not equal to a valid unchanged clause', () => {
236
+ // Parser output for the candidate: validClause false, replaces [] (see doc-lifecycle.js parseRequirementBlocks).
237
+ // A fact that cannot be determined keeps its triggered checks as unknown (D6.5), never N/A.
238
+ assertCase('invalid-clause', {
239
+ applicable: ['doc-lint', 'link-check'],
240
+ unknown: ['quality-review', 'conflict-check']
241
+ });
242
+ });
243
+
244
+ checkCase('325-3a control: identical replacement clause and body plus an unrelated Context typo: lint and links only', () => {
245
+ assertCase('clause-context-typo', { applicable: ['doc-lint', 'link-check'] });
246
+ });
247
+
248
+ // Round 4 (§10, owner choice b): derivation exposes assertionChanged and inventoryComplete.
249
+ // assertionChanged is pure text analysis of REQ-citing test changes; inventoryComplete is caller-asserted only.
250
+
251
+ function factsWithInventory(name, inventory) {
252
+ const input = loadCase(name);
253
+ return deriveDocFacts({ base: input.base, candidate: input.candidate, config: caseConfig(name), ...(inventory ? { inventory } : {}) });
254
+ }
255
+
256
+ checkCase('325-3a assertion edit in a REQ-citing test: facts.assertionChanged is true (§10)', () => {
257
+ const { facts } = factsWithInventory('tagged-test');
258
+ assert.strictEqual(facts.assertionChanged, true, 'an assert line changed in the REQ-citing test');
259
+ });
260
+
261
+ checkCase('325-3a comment-only edit in a REQ-citing test: facts.assertionChanged is false (§10)', () => {
262
+ const { facts } = factsWithInventory('comment-only-test');
263
+ assert.strictEqual(facts.assertionChanged, false, 'only the comment line changed; no assertion line did');
264
+ });
265
+
266
+ checkCase('325-3a requirement-sync obligation carries assertionChanged and inventoryComplete (complete inventory supplied) (§10)', () => {
267
+ const { facts } = factsWithInventory('tagged-test', { complete: true });
268
+ const item = deriveDocObligations(facts).find(o => o.capability === 'requirement-sync');
269
+ assert.ok(item, 'requirement-sync is in the catalog');
270
+ assert.strictEqual(item.assertionChanged, true, 'obligation carries assertionChanged');
271
+ assert.strictEqual(item.inventoryComplete, true, 'obligation carries the caller-asserted inventory completeness');
272
+ });
273
+
274
+ checkCase('325-3a inventory omitted: facts.inventoryComplete and requirement-sync inventoryComplete are false (§10)', () => {
275
+ const { facts } = factsWithInventory('tagged-test');
276
+ assert.strictEqual(facts.inventoryComplete, false, 'omitted inventory defaults to incomplete');
277
+ const item = deriveDocObligations(facts).find(o => o.capability === 'requirement-sync');
278
+ assert.strictEqual(item.inventoryComplete, false, 'the obligation inherits incomplete inventory');
279
+ });
280
+
281
+ // Round 5 (PR #334 review R1-R3). Derivation expectations only.
282
+
283
+ // R1: section extraction must be fence-aware. A "# Example" line inside a fenced block is not a heading,
284
+ // so an edit to real Decision text after the fence is a normative change.
285
+ checkCase('325-3a R1 backtick fence: a fenced "# Example" line does not truncate ADR Decision; Decision edit is normative', () => {
286
+ assertCase('decision-fence-backtick', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
287
+ });
288
+
289
+ checkCase('325-3a R1 tilde fence: a fenced "# Example" line does not truncate ADR Decision; Decision edit is normative', () => {
290
+ assertCase('decision-fence-tilde', { applicable: ['doc-lint', 'link-check', 'quality-review', 'conflict-check'] });
291
+ });
292
+
293
+ // R2: a content-identical path-only rename of a declared derived source is a dependency change.
294
+ checkCase('325-3a R2 rename-only of a declared derived source: derived-regenerate applicable, scoped to the output docs/index.md', () => {
295
+ const { obligations } = deriveCase('derived-rename');
296
+ const item = obligations.find(o => o.capability === 'derived-regenerate');
297
+ assert.strictEqual(item.applicability, 'applicable', 'a rename of a derived source triggers regeneration');
298
+ assert.deepStrictEqual(item.scope.paths, ['docs/index.md'], 'scope is the declared output only');
299
+ });
300
+
301
+ // R3: a REQ-citing test triggers requirement-sync only through a changed assertion or a new REQ-tagged test.
302
+ checkCase('325-3a R3 comment-only edit in a REQ-citing test: requirement-sync is N/A', () => {
303
+ const { obligations } = deriveCase('comment-only-test');
304
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
305
+ 'a comment-only edit is not an assertion change');
306
+ });
307
+
308
+ checkCase('325-3a R3 whitespace-only edit in a REQ-citing test: requirement-sync is N/A', () => {
309
+ const { obligations } = deriveCase('whitespace-only-test');
310
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
311
+ 'a whitespace-only edit is not an assertion change');
312
+ });
313
+
314
+ checkCase('325-3a R3 control: assertion changed in a REQ-citing test: requirement-sync applicable', () => {
315
+ const { obligations } = deriveCase('tagged-test');
316
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'applicable');
317
+ });
318
+
319
+ checkCase('325-3a R3 control: new REQ-tagged test file: requirement-sync applicable', () => {
320
+ const { obligations } = deriveCase('new-req-test');
321
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'applicable',
322
+ 'a new REQ-citing test is a trigger');
323
+ });
324
+
325
+ checkCase('325-3a R3 undetermined: a REQ-citing test that cannot be read as text gives assertionChanged unknown and requirement-sync unknown', () => {
326
+ const input = loadCase('tagged-test');
327
+ input.candidate.files['ci/mechanism-lint.test.js'] = null; // not readable as text
328
+ const { facts } = deriveDocFacts({ base: input.base, candidate: input.candidate, config: CONFIG });
329
+ assert.strictEqual(facts.assertionChanged, 'unknown', 'an undeterminable assertion change is unknown');
330
+ const item = deriveDocObligations(facts).find(o => o.capability === 'requirement-sync');
331
+ assert.strictEqual(item.applicability, 'unknown', 'requirement-sync stays unknown, never N/A or trivial');
332
+ });
333
+
334
+ // Round 6 (Sol round-3 review). Derivation expectations only.
335
+ // F1: current resolution authority comes only from the candidate PRD set. The exact resolution labels are
336
+ // not pinned here; the invariant is that a cited REQ which is not uniquely resolved in the candidate has no file.
337
+
338
+ function citedEntries(facts, id) {
339
+ return facts.citedReqs.filter(entry => entry && entry.id === id);
340
+ }
341
+
342
+ checkCase('325-3a F1 duplicate cited ID across two current PRDs: cited REQ is not resolved (file null) (round 6)', () => {
343
+ const { result } = deriveCase('dup-req-id');
344
+ const entries = citedEntries(result.facts, 'REQ-GRPH-001');
345
+ assert.ok(entries.length > 0, 'the cited id is reported');
346
+ assert.ok(entries.every(entry => entry.file === null), `ambiguous id has no file: ${JSON.stringify(entries)}`);
347
+ });
348
+
349
+ checkCase('325-3a F1 malformed current PRD over a valid base: no base fallback, cited REQ not resolved (file null) (round 6)', () => {
350
+ const { result } = deriveCase('malformed-prd');
351
+ const entries = citedEntries(result.facts, 'REQ-GRPH-001');
352
+ assert.ok(entries.length > 0, 'the cited id is reported');
353
+ assert.ok(entries.every(entry => entry.file === null), `a parse error is not authority from the base: ${JSON.stringify(entries)}`);
354
+ });
355
+
356
+ checkCase('325-3a F1 PRD deleted in the candidate, base still declares it: cited REQ not resolved (file null) (round 6)', () => {
357
+ const { result } = deriveCase('deleted-prd');
358
+ const entries = citedEntries(result.facts, 'REQ-GRPH-001');
359
+ assert.ok(entries.length > 0, 'the cited id is reported');
360
+ assert.ok(entries.every(entry => entry.file === null), `a base-only declaration is not current authority: ${JSON.stringify(entries)}`);
361
+ });
362
+
363
+ checkCase('325-3a F1 control: uniquely declared unchanged current PRD: cited REQ resolves to that file', () => {
364
+ const { result } = deriveCase('assertion-no-revision');
365
+ const entries = citedEntries(result.facts, 'REQ-GRPH-001');
366
+ assert.deepStrictEqual(entries.map(entry => entry.file), ['docs/prd/graph.md'], 'one current declaration resolves');
367
+ });
368
+
369
+ // F3: assertion detection is literal-aware. Whitespace inside string, template or regex literals is content.
370
+ // Formatting outside literals and trailing comments are not assertion changes.
371
+
372
+ for (const name of ['literal-whitespace-assert', 'template-literal-whitespace', 'regex-literal-whitespace']) {
373
+ checkCase(`325-3a F3 whitespace inside a literal (${name}) is content: assertionChanged is not false, requirement-sync not N/A`, () => {
374
+ const { result, obligations } = deriveCase(name);
375
+ assert.notStrictEqual(result.facts.assertionChanged, false, `a literal change is not formatting: ${result.facts.assertionChanged}`);
376
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
377
+ 'a literal change is not N/A');
378
+ });
379
+ }
380
+
381
+ checkCase('325-3a F3 trailing-comment-only edit after an unchanged assertion: assertionChanged false, requirement-sync N/A', () => {
382
+ const { result, obligations } = deriveCase('trailing-comment-test');
383
+ assert.strictEqual(result.facts.assertionChanged, false, 'only the trailing comment changed');
384
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A');
385
+ });
386
+
387
+ checkCase('325-3a F3 control: formatting outside literals and comments: assertionChanged false, requirement-sync N/A', () => {
388
+ const { result, obligations } = deriveCase('formatting-only-test');
389
+ assert.strictEqual(result.facts.assertionChanged, false, 'spacing outside literals is formatting');
390
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A');
391
+ });
392
+
393
+ checkCase('325-3a F3 control: numeric assertion edit: assertionChanged true', () => {
394
+ const { result } = deriveCase('tagged-test');
395
+ assert.strictEqual(result.facts.assertionChanged, true, 'a real assertion value changed');
396
+ });
397
+
398
+ // Round 7 (Sol round-4 finding 1): resolution uses the lifecycle's current effective namespace, not every
399
+ // candidate declaration. A retained revision chain of ONE logical PRD is not ambiguous. The exact resolution
400
+ // label is not pinned for the failure cases; the invariant is file null when the REQ is not current.
401
+
402
+ checkCase('325-3a R7 retained valid revision chain of one PRD: cited REQ-GRPH-011 resolves uniquely to the current effective revision r002 (round 7)', () => {
403
+ const { result } = deriveCase('retained-revision-chain');
404
+ const entries = citedEntries(result.facts, 'REQ-GRPH-011');
405
+ assert.deepStrictEqual(entries.map(entry => entry.file), ['docs/prd/graph/r002.md'],
406
+ `one current file, the effective revision: ${JSON.stringify(entries)}`);
407
+ assert.ok(entries.every(entry => entry.resolution !== 'ambiguous'), 'a retained revision is not ambiguous');
408
+ });
409
+
410
+ checkCase('325-3a R7 control: the same ID in two DISTINCT current logical PRDs is ambiguous, file null (round 7)', () => {
411
+ const { result } = deriveCase('distinct-logical-prds');
412
+ const entries = citedEntries(result.facts, 'REQ-GRPH-011');
413
+ assert.ok(entries.length > 0, 'the cited id is reported');
414
+ assert.ok(entries.every(entry => entry.file === null), `two distinct current logical PRDs are ambiguous: ${JSON.stringify(entries)}`);
415
+ });
416
+
417
+ checkCase('325-3a R7 historical-only ID: REQ-GRPH-013 exists only in the historical r001 and is removed by the current r002: unresolved, file null (round 7)', () => {
418
+ const { result } = deriveCase('historical-only-req');
419
+ const entries = citedEntries(result.facts, 'REQ-GRPH-013');
420
+ assert.ok(entries.length > 0, 'the cited id is reported');
421
+ assert.ok(entries.every(entry => entry.file === null),
422
+ `a historical or replaced revision is not current authority: ${JSON.stringify(entries)}`);
423
+ });
424
+
425
+ // Round 7 positive-fixture correction (Sol round-5 P2). The req-revision positive is a legal delivery:
426
+ // the base effective r001 is retained byte-identical, the candidate keeps r001 unchanged and adds r002.
427
+ // The old in-place rewrite of effective r001 is kept only as a named negative, rejected by the lifecycle.
428
+
429
+ let lifecycle = null;
430
+ try {
431
+ lifecycle = require(path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-lifecycle.js'));
432
+ } catch (error) {
433
+ helper.check('325-3a R7 lifecycle module is available', false, error.message);
434
+ }
435
+
436
+ function lifecycleInventory(root) {
437
+ return lifecycle.collectDocs(root, CONFIG).map(doc => ({
438
+ id: doc.frontmatter.id,
439
+ revision: doc.frontmatter.revision,
440
+ path: doc.path,
441
+ status: doc.frontmatter.status,
442
+ bytes: doc.storedBytes
443
+ }));
444
+ }
445
+
446
+ checkCase('325-3a R7 legal positive: the candidate analysis has no error diagnostic and REQ-GRPH-011 resolves to r002 (round 7)', () => {
447
+ const root = path.join(fixtureDir, 'req-revision');
448
+ const docs = lifecycle.collectDocs(path.join(root, 'candidate'), CONFIG);
449
+ const analysis = lifecycle.analyzeDocs(docs, CONFIG, { all: true, baseInventory: lifecycleInventory(path.join(root, 'base')) });
450
+ assert.deepStrictEqual(analysis.diagnostics.filter(item => item.severity === 'error'), [], 'the legal revision has no error diagnostic');
451
+ const resolved = lifecycle.resolveRef(docs, 'REQ-GRPH-011');
452
+ assert.strictEqual(resolved.doc && resolved.doc.path, 'docs/prd/graph/r002.md', 'REQ-GRPH-011 resolves to the current effective revision');
453
+ });
454
+
455
+ checkCase('325-3a R7 named negative: rewriting effective r001 in place is rejected by the lifecycle with immutable-revision-modified (round 7)', () => {
456
+ const root = path.join(fixtureDir, 'req-revision-in-place');
457
+ const docs = lifecycle.collectDocs(path.join(root, 'candidate'), CONFIG);
458
+ const analysis = lifecycle.analyzeDocs(docs, CONFIG, { all: true, baseInventory: lifecycleInventory(path.join(root, 'base')) });
459
+ assert.ok(analysis.diagnostics.some(item => item.code === 'immutable-revision-modified' && item.path === 'docs/prd/graph/r001.md'),
460
+ `the in-place rewrite is an immutable-revision-modified error: ${JSON.stringify(analysis.diagnostics.map(item => item.code))}`);
461
+ });
462
+
463
+ // Round 8 (PR #334 review 2, Q1, Q3, Q4). Derivation expectations only.
464
+
465
+ // Q1: a changed line is attributed to its enclosing statement. A multiline assertion call counts.
466
+ checkCase('325-3a Q1 multiline assert.strictEqual numeric change: assertionChanged is true (round 8)', () => {
467
+ const { result } = deriveCase('multiline-strictequal');
468
+ assert.strictEqual(result.facts.assertionChanged, true, 'the argument of an assertion call changed');
469
+ });
470
+
471
+ checkCase('325-3a Q1 multiline expect(...).toEqual(...) change: assertionChanged is true (round 8)', () => {
472
+ const { result } = deriveCase('multiline-expect');
473
+ assert.strictEqual(result.facts.assertionChanged, true, 'the argument of an expect call changed');
474
+ });
475
+
476
+ checkCase('325-3a Q1 comment-only edit inside the same multiline call: assertionChanged false, requirement-sync N/A (round 8 control)', () => {
477
+ const { result, obligations } = deriveCase('multiline-comment-only');
478
+ assert.strictEqual(result.facts.assertionChanged, false, 'only a trailing comment changed');
479
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A');
480
+ });
481
+
482
+ checkCase('325-3a Q1 formatting-only change inside the same multiline call: assertionChanged false, requirement-sync N/A (round 8 control)', () => {
483
+ const { result, obligations } = deriveCase('multiline-formatting-only');
484
+ assert.strictEqual(result.facts.assertionChanged, false, 'indentation outside literals is formatting');
485
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A');
486
+ });
487
+
488
+ checkCase('325-3a Q1 enclosing statement cannot be determined (unbalanced call): assertionChanged unknown, never false (round 8)', () => {
489
+ const { result, obligations } = deriveCase('unbalanced-statement');
490
+ assert.strictEqual(result.facts.assertionChanged, 'unknown', 'an undeterminable enclosing statement is unknown');
491
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
492
+ 'unknown never becomes trivial');
493
+ });
494
+
495
+ // Q3: the requirement-sync obligation carries the trigger that the fail lane needs.
496
+ checkCase('325-3a Q3 new REQ-citing test delegating to a helper: requirement-sync trigger is new-test (round 8)', () => {
497
+ const { obligations } = deriveCase('helper-delegated-test');
498
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').requirementSyncTrigger, 'new-test');
499
+ });
500
+
501
+ checkCase('325-3a Q3 changed assertion in a REQ-citing test: requirement-sync trigger is assertion-change (round 8)', () => {
502
+ const { obligations } = deriveCase('tagged-test');
503
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').requirementSyncTrigger, 'assertion-change');
504
+ });
505
+
506
+ checkCase('325-3a Q3 comment-only edit: requirement-sync trigger is null (round 8)', () => {
507
+ const { obligations } = deriveCase('comment-only-test');
508
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').requirementSyncTrigger, null);
509
+ });
510
+
511
+ // Q4: a changed assertion is attributed to its enclosing test call only.
512
+ checkCase('325-3a Q4 two tests in one file, only test A changed: citedReqs names REQ-GRPH-001 only (round 8)', () => {
513
+ const { result } = deriveCase('two-tests-one-file');
514
+ const ids = [...new Set(result.facts.citedReqs.map(entry => entry.id))].sort();
515
+ assert.deepStrictEqual(ids, ['REQ-GRPH-001'], `REQ-GRPH-002 is not cited by the changed test: ${JSON.stringify(result.facts.citedReqs)}`);
516
+ });
517
+
518
+ checkCase('325-3a Q4 attribution impossible (enclosing test call never closed): requirement-sync is unknown (round 8)', () => {
519
+ const { obligations } = deriveCase('attribution-impossible');
520
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'unknown',
521
+ 'an unattributable change is an explicit unknown scope, not a REQ claim');
522
+ });
523
+
524
+ // Round 9 (PR #334 review round 6). Derivation expectations only.
525
+
526
+ // P1: a mixed change (one known edit, one unattributable edit) makes the whole obligation unknown, not the known subset.
527
+ checkCase('325-3a P1 mixed attribution (known A edit, unattributable B edit): requirement-sync unknown, scope requirementIds unknown (round 9)', () => {
528
+ const { obligations } = deriveCase('mixed-attribution');
529
+ const item = obligations.find(o => o.capability === 'requirement-sync');
530
+ assert.strictEqual(item.applicability, 'unknown', 'a partly unattributable change is unknown');
531
+ assert.strictEqual(item.scope.requirementIds, 'unknown', 'the obligation does not claim the known subset as complete');
532
+ });
533
+
534
+ // P2: an enclosing test call that cites no REQ is unknown scope. It never borrows another test's REQ title.
535
+ checkCase('325-3a P2 legacy fallback: changed untagged test inside a file with a tagged unchanged test: unknown scope, no REQ-GRPH-001 claim (round 9)', () => {
536
+ const { result, obligations } = deriveCase('legacy-untagged-fallback');
537
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'unknown',
538
+ 'the changed statement has an enclosing test call that cites no REQ');
539
+ assert.ok(!result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-001'),
540
+ `REQ-GRPH-001 belongs to the unchanged test, not this one: ${JSON.stringify(result.facts.citedReqs)}`);
541
+ });
542
+
543
+ // Malformed attribution (mutation): an unreadable citation in the changed test must not populate citedReqs with other REQs.
544
+ checkCase('325-3a malformed attribution mutation: changed test with unreadable citation text is unknown scope, citedReqs does not contain REQ-GRPH-002 (round 9)', () => {
545
+ const { result, obligations } = deriveCase('malformed-citation-attribution');
546
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'unknown',
547
+ 'an unreadable citation is an explicit unknown scope');
548
+ assert.ok(!result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-002'),
549
+ `the unchanged test's REQ is not claimed: ${JSON.stringify(result.facts.citedReqs)}`);
550
+ });
551
+
552
+ // Round 10 (PR #334 review round 7). Derivation expectations only.
553
+
554
+ // F1: the unknown requirement scope is an explicit boolean on the obligation, so evidence can refuse it before any outcome branch.
555
+ checkCase('325-3a F1 derive: unknown requirement scope carries an explicit requirementScopeUnknown true marker (round 10)', () => {
556
+ const { obligations } = deriveCase('mixed-attribution');
557
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').requirementScopeUnknown, true,
558
+ 'the mixed change marks its requirement scope unknown explicitly');
559
+ });
560
+
561
+ checkCase('325-3a F1 derive: a known requirement scope carries an explicit requirementScopeUnknown false marker (round 10)', () => {
562
+ const { obligations } = deriveCase('tagged-test');
563
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').requirementScopeUnknown, false,
564
+ 'a fully attributed change has a known scope, stated explicitly');
565
+ });
566
+
567
+ // F2: the file-level fallback needs a real file-level tag. A title in another test never supplies a REQ.
568
+ checkCase('325-3a F2 unchanged tagged test + changed top-level assertion with no file-level tag: unknown, no REQ-GRPH-001 claim (round 10)', () => {
569
+ const { result, obligations } = deriveCase('top-level-no-file-tag');
570
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'unknown',
571
+ 'a top-level assertion with no file-level tag has unknown attribution');
572
+ assert.ok(!result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-001'),
573
+ `REQ-GRPH-001 comes from a test title, not a file-level tag: ${JSON.stringify(result.facts.citedReqs)}`);
574
+ });
575
+
576
+ // Round 11 (PR #334 review round 8). A file-level tag is a lexer-confirmed top-level comment, not a raw-line match.
577
+
578
+ checkCase('325-3a R11 tag-shaped line inside a template literal, changed unwrapped top-level assertion: unknown attribution, no claimed REQ-GRPH-001 (round 11)', () => {
579
+ const { result, obligations } = deriveCase('template-literal-tag');
580
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'unknown',
581
+ 'a template literal is not a file-level comment tag');
582
+ assert.ok(!result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-001'),
583
+ `the tag-shaped line is data, not a comment: ${JSON.stringify(result.facts.citedReqs)}`);
584
+ });
585
+
586
+ checkCase('325-3a R11 control: the same tag text inside a string literal is not a file tag: no claimed REQ-GRPH-001 (round 11)', () => {
587
+ const { result } = deriveCase('string-literal-tag');
588
+ assert.ok(!result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-001'),
589
+ `a string literal is not a comment: ${JSON.stringify(result.facts.citedReqs)}`);
590
+ });
591
+
592
+ checkCase('325-3a R11 string-literal tag with a real assertion change: assertionChanged is not false (round 11, found while writing the control)', () => {
593
+ const { result } = deriveCase('string-literal-tag');
594
+ assert.notStrictEqual(result.facts.assertionChanged, false,
595
+ 'the assertion value changed from 1 to 2, so the change is not formatting');
596
+ });
597
+
598
+ checkCase('325-3a R11 control: tag-shaped comment inside another test call is excluded: no claimed REQ-GRPH-001 (round 11)', () => {
599
+ const { result, obligations } = deriveCase('tag-inside-test-call');
600
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'unknown',
601
+ 'the only tag is inside another test call, so the top-level change has no file-level tag');
602
+ assert.ok(!result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-001'),
603
+ `a tag inside another test call is not a file-level tag: ${JSON.stringify(result.facts.citedReqs)}`);
604
+ });
605
+
606
+ checkCase('325-3a R11 control: a real standalone top-level comment tag with a changed unwrapped assertion: resolves to REQ-GRPH-001 (round 11)', () => {
607
+ const { result } = deriveCase('tagged-test');
608
+ assert.ok(result.facts.citedReqs.some(entry => entry.id === 'REQ-GRPH-001'),
609
+ `the real file-level tag is kept: ${JSON.stringify(result.facts.citedReqs)}`);
610
+ });
611
+
612
+ // Round 12 (PR #334 review round 9). Derivation expectations only.
613
+
614
+ // T1: a title-only change is not a new test and not an assertion change.
615
+ checkCase('325-3a T1 title-only rename of a REQ-citing test with an unchanged inline assertion: assertionChanged false, requirement-sync N/A, trigger not new-test (round 12)', () => {
616
+ const { result, obligations } = deriveCase('title-only-rename');
617
+ assert.strictEqual(result.facts.assertionChanged, false, 'the assertion call is unchanged; only the title changed');
618
+ const item = obligations.find(o => o.capability === 'requirement-sync');
619
+ assert.strictEqual(item.applicability, 'N/A', 'a title-only change does not trigger requirement-sync');
620
+ assert.strictEqual(item.requirementSyncTrigger, null, 'a renamed test is not a new test');
621
+ });
622
+
623
+ checkCase('325-3a T1 control: a genuinely added REQ-citing test delegating to a helper: trigger new-test (round 12)', () => {
624
+ const { obligations } = deriveCase('helper-delegated-test');
625
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').requirementSyncTrigger, 'new-test');
626
+ });
627
+
628
+ checkCase('325-3a T1 control: a body edit inside the assertion call: assertionChanged true (round 12)', () => {
629
+ const { result } = deriveCase('tagged-test');
630
+ assert.strictEqual(result.facts.assertionChanged, true, 'the assertion argument changed');
631
+ });
632
+
633
+ // T2: reqUpdates come only from a lifecycle successor revision. An in-place edit, or an edit to a retained
634
+ // historical revision, is not an update.
635
+ checkCase('325-3a T2 same-path in-place edit of the current effective revision r002: REQ-GRPH-011 is not in reqUpdates (round 12)', () => {
636
+ const { result } = deriveCase('in-place-current');
637
+ assert.ok(!result.facts.reqUpdates.some(entry => entry.id === 'REQ-GRPH-011'),
638
+ `an in-place edit is a req-mutation-not-revision, not an update: ${JSON.stringify(result.facts.reqUpdates)}`);
639
+ });
640
+
641
+ checkCase('325-3a T2 edit to a retained historical revision r001: REQ-GRPH-011 is not in reqUpdates (round 12)', () => {
642
+ const { result } = deriveCase('in-place-historical');
643
+ assert.ok(!result.facts.reqUpdates.some(entry => entry.id === 'REQ-GRPH-011'),
644
+ `a historical-revision edit is not a successor revision: ${JSON.stringify(result.facts.reqUpdates)}`);
645
+ });
646
+
647
+ checkCase('325-3a T2 control: a lifecycle successor r002 of r001 that changes REQ-GRPH-011: REQ-GRPH-011 is in reqUpdates at r002 (round 12)', () => {
648
+ const { result } = deriveCase('req-revision');
649
+ assert.ok(result.facts.reqUpdates.some(entry => entry.id === 'REQ-GRPH-011' && entry.file === 'docs/prd/graph/r002.md'),
650
+ `the successor revision is an update: ${JSON.stringify(result.facts.reqUpdates)}`);
651
+ });
652
+
653
+ // Round 13 (PR #334 review round 10). Derivation expectations only.
654
+
655
+ // S1: assertion detection must not narrow. Any assert.<member> call and the expect modifier chain count.
656
+ checkCase('325-3a S1 assert.deepStrictEqual numeric change: assertionChanged true (round 13)', () => {
657
+ const { result } = deriveCase('deep-strict-equal');
658
+ assert.strictEqual(result.facts.assertionChanged, true, 'the expected value of an assert.* call changed');
659
+ });
660
+
661
+ checkCase('325-3a S1 expect(...).not.toEqual numeric change through a modifier chain: assertionChanged true (round 13)', () => {
662
+ const { result } = deriveCase('expect-not-to-equal');
663
+ assert.strictEqual(result.facts.assertionChanged, true, 'the expected value of a modifier-chained expect changed');
664
+ });
665
+
666
+ checkCase('325-3a S1 unsupported assertion syntax (t.is, not a recognized form): assertionChanged unknown, never false (round 13)', () => {
667
+ const { result, obligations } = deriveCase('unsupported-assertion-syntax');
668
+ assert.strictEqual(result.facts.assertionChanged, 'unknown', 'an unrecognized assertion form is unknown');
669
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A', 'unknown never becomes trivial');
670
+ });
671
+
672
+ // S2: a REQ whose update is an in-place mutation of the current effective revision resolves as mutated, not resolved.
673
+ checkCase('325-3a S2 same-path in-place edit of the current effective revision: cited REQ-GRPH-011 is mutated, file null (round 13)', () => {
674
+ const { result } = deriveCase('in-place-current');
675
+ const entries = citedEntries(result.facts, 'REQ-GRPH-011');
676
+ assert.ok(entries.length > 0, 'the cited id is reported');
677
+ assert.ok(entries.every(entry => entry.file === null && entry.resolution === 'mutated'),
678
+ `an in-place mutation is not a resolved revision: ${JSON.stringify(entries)}`);
679
+ });
680
+
681
+ checkCase('325-3a S2 control: edit to a retained historical revision: cited REQ-GRPH-011 is unresolved, file null (round 13)', () => {
682
+ const { result } = deriveCase('in-place-historical');
683
+ const entries = citedEntries(result.facts, 'REQ-GRPH-011');
684
+ assert.ok(entries.length > 0 && entries.every(entry => entry.file === null),
685
+ `a historical mutation is not current authority: ${JSON.stringify(entries)}`);
686
+ });
687
+
688
+ checkCase('325-3a S2 control: successor revision r002 of r001 changing REQ-GRPH-011: cited REQ-GRPH-011 resolves to r002 (round 13)', () => {
689
+ const { result } = deriveCase('req-revision');
690
+ const entries = citedEntries(result.facts, 'REQ-GRPH-011');
691
+ assert.deepStrictEqual(entries.map(entry => entry.file), ['docs/prd/graph/r002.md'], 'the successor is the current authority');
692
+ });
693
+
694
+ // Round 14 (PR #334 review round 11, C1). A computed assertion member is not recognized, so it is unknown, never false.
695
+
696
+ checkCase('325-3a C1 computed assertion member assert[\'deepStrictEqual\'] with a numeric change: assertionChanged unknown, never false (round 14)', () => {
697
+ const { result, obligations } = deriveCase('computed-member-assert');
698
+ assert.strictEqual(result.facts.assertionChanged, 'unknown', 'a computed member is not recognized as an assertion');
699
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
700
+ 'unknown never becomes N/A');
701
+ });
702
+
703
+ checkCase('325-3a C1 computed assertion member assert[method] with a numeric change: assertionChanged unknown, never false (round 14)', () => {
704
+ const { result, obligations } = deriveCase('computed-member-variable');
705
+ assert.strictEqual(result.facts.assertionChanged, 'unknown', 'a variable member is not recognized as an assertion');
706
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
707
+ 'unknown never becomes N/A');
708
+ });
709
+
710
+ checkCase('325-3a C1 control: dot-member assert.equal with a numeric change: assertionChanged true (round 14)', () => {
711
+ const { result } = deriveCase('tagged-test');
712
+ assert.strictEqual(result.facts.assertionChanged, true, 'a dot member is recognized');
713
+ });
714
+
715
+ checkCase('325-3a C1 control: unchanged inline assertion with a title-only rename: assertionChanged false (round 14)', () => {
716
+ const { result } = deriveCase('title-only-rename');
717
+ assert.strictEqual(result.facts.assertionChanged, false, 'the assertion call is unchanged');
718
+ });
719
+
720
+ // Round 14 coverage (PR #334 review round 11). Both controls are expected to hold; they cover computed expect members
721
+ // and the statement boundary for a title-only rename in a file that also has an unrelated computed assert.
722
+
723
+ checkCase('325-3a C1 coverage: computed expect member expect(run())[matcher](...) with a numeric change: assertionChanged unknown, never false (round 14)', () => {
724
+ const { result, obligations } = deriveCase('expect-computed-matcher');
725
+ assert.strictEqual(result.facts.assertionChanged, 'unknown', 'a computed expect matcher is not recognized as an assertion');
726
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A', 'unknown never becomes N/A');
727
+ });
728
+
729
+ checkCase('325-3a C1 coverage: computed expect member expect(run())[\'toEqual\'](...) with a numeric change: assertionChanged unknown, never false (round 14)', () => {
730
+ const { result, obligations } = deriveCase('expect-computed-literal');
731
+ assert.strictEqual(result.facts.assertionChanged, 'unknown', 'a computed expect member is not recognized as an assertion');
732
+ assert.notStrictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A', 'unknown never becomes N/A');
733
+ });
734
+
735
+ checkCase('325-3a C1 coverage: title-only rename of a REQ-citing test with the same body, plus an unrelated computed assert in another statement: assertionChanged false, requirement-sync N/A (round 14)', () => {
736
+ const { result, obligations } = deriveCase('title-only-unrelated-computed');
737
+ assert.strictEqual(result.facts.assertionChanged, false, 'the test body and the unrelated computed assert are unchanged');
738
+ assert.strictEqual(obligations.find(o => o.capability === 'requirement-sync').applicability, 'N/A',
739
+ 'a title-only rename does not trigger requirement-sync');
740
+ });
741
+
742
+ helper.finish();