@madmatt112org/spec-workflow-mcp 3.0.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 (537) hide show
  1. package/CHANGELOG.md +1013 -0
  2. package/LICENSE +674 -0
  3. package/README.md +458 -0
  4. package/dist/__tests__/config.test.d.ts +2 -0
  5. package/dist/__tests__/config.test.d.ts.map +1 -0
  6. package/dist/__tests__/config.test.js +264 -0
  7. package/dist/__tests__/config.test.js.map +1 -0
  8. package/dist/__tests__/index-args.test.d.ts +2 -0
  9. package/dist/__tests__/index-args.test.d.ts.map +1 -0
  10. package/dist/__tests__/index-args.test.js +43 -0
  11. package/dist/__tests__/index-args.test.js.map +1 -0
  12. package/dist/__tests__/index-entrypoint.test.d.ts +2 -0
  13. package/dist/__tests__/index-entrypoint.test.d.ts.map +1 -0
  14. package/dist/__tests__/index-entrypoint.test.js +23 -0
  15. package/dist/__tests__/index-entrypoint.test.js.map +1 -0
  16. package/dist/config.d.ts +26 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +188 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/core/__tests__/adversarial-settings.test.d.ts +2 -0
  21. package/dist/core/__tests__/adversarial-settings.test.d.ts.map +1 -0
  22. package/dist/core/__tests__/adversarial-settings.test.js +361 -0
  23. package/dist/core/__tests__/adversarial-settings.test.js.map +1 -0
  24. package/dist/core/__tests__/deferral-storage.test.d.ts +2 -0
  25. package/dist/core/__tests__/deferral-storage.test.d.ts.map +1 -0
  26. package/dist/core/__tests__/deferral-storage.test.js +181 -0
  27. package/dist/core/__tests__/deferral-storage.test.js.map +1 -0
  28. package/dist/core/__tests__/git-utils.test.d.ts +2 -0
  29. package/dist/core/__tests__/git-utils.test.d.ts.map +1 -0
  30. package/dist/core/__tests__/git-utils.test.js +179 -0
  31. package/dist/core/__tests__/git-utils.test.js.map +1 -0
  32. package/dist/core/__tests__/hygiene-signals.test.d.ts +2 -0
  33. package/dist/core/__tests__/hygiene-signals.test.d.ts.map +1 -0
  34. package/dist/core/__tests__/hygiene-signals.test.js +200 -0
  35. package/dist/core/__tests__/hygiene-signals.test.js.map +1 -0
  36. package/dist/core/__tests__/mdx-validator.test.d.ts +2 -0
  37. package/dist/core/__tests__/mdx-validator.test.d.ts.map +1 -0
  38. package/dist/core/__tests__/mdx-validator.test.js +42 -0
  39. package/dist/core/__tests__/mdx-validator.test.js.map +1 -0
  40. package/dist/core/__tests__/path-denylist.test.d.ts +2 -0
  41. package/dist/core/__tests__/path-denylist.test.d.ts.map +1 -0
  42. package/dist/core/__tests__/path-denylist.test.js +242 -0
  43. package/dist/core/__tests__/path-denylist.test.js.map +1 -0
  44. package/dist/core/__tests__/path-utils.test.d.ts +2 -0
  45. package/dist/core/__tests__/path-utils.test.d.ts.map +1 -0
  46. package/dist/core/__tests__/path-utils.test.js +342 -0
  47. package/dist/core/__tests__/path-utils.test.js.map +1 -0
  48. package/dist/core/__tests__/project-registry.test.d.ts +2 -0
  49. package/dist/core/__tests__/project-registry.test.d.ts.map +1 -0
  50. package/dist/core/__tests__/project-registry.test.js +62 -0
  51. package/dist/core/__tests__/project-registry.test.js.map +1 -0
  52. package/dist/core/__tests__/security-utils.test.d.ts +2 -0
  53. package/dist/core/__tests__/security-utils.test.d.ts.map +1 -0
  54. package/dist/core/__tests__/security-utils.test.js +643 -0
  55. package/dist/core/__tests__/security-utils.test.js.map +1 -0
  56. package/dist/core/__tests__/task-diff.test.d.ts +2 -0
  57. package/dist/core/__tests__/task-diff.test.d.ts.map +1 -0
  58. package/dist/core/__tests__/task-diff.test.js +287 -0
  59. package/dist/core/__tests__/task-diff.test.js.map +1 -0
  60. package/dist/core/__tests__/task-review-manager.test.d.ts +2 -0
  61. package/dist/core/__tests__/task-review-manager.test.d.ts.map +1 -0
  62. package/dist/core/__tests__/task-review-manager.test.js +235 -0
  63. package/dist/core/__tests__/task-review-manager.test.js.map +1 -0
  64. package/dist/core/__tests__/task-validator.test.d.ts +2 -0
  65. package/dist/core/__tests__/task-validator.test.d.ts.map +1 -0
  66. package/dist/core/__tests__/task-validator.test.js +237 -0
  67. package/dist/core/__tests__/task-validator.test.js.map +1 -0
  68. package/dist/core/__tests__/typecheck.test.d.ts +2 -0
  69. package/dist/core/__tests__/typecheck.test.d.ts.map +1 -0
  70. package/dist/core/__tests__/typecheck.test.js +558 -0
  71. package/dist/core/__tests__/typecheck.test.js.map +1 -0
  72. package/dist/core/adversarial-settings.d.ts +23 -0
  73. package/dist/core/adversarial-settings.d.ts.map +1 -0
  74. package/dist/core/adversarial-settings.js +148 -0
  75. package/dist/core/adversarial-settings.js.map +1 -0
  76. package/dist/core/archive-service.d.ts +10 -0
  77. package/dist/core/archive-service.d.ts.map +1 -0
  78. package/dist/core/archive-service.js +99 -0
  79. package/dist/core/archive-service.js.map +1 -0
  80. package/dist/core/dashboard-session.d.ts +49 -0
  81. package/dist/core/dashboard-session.d.ts.map +1 -0
  82. package/dist/core/dashboard-session.js +132 -0
  83. package/dist/core/dashboard-session.js.map +1 -0
  84. package/dist/core/deferral-storage.d.ts +32 -0
  85. package/dist/core/deferral-storage.d.ts.map +1 -0
  86. package/dist/core/deferral-storage.js +232 -0
  87. package/dist/core/deferral-storage.js.map +1 -0
  88. package/dist/core/git-utils.d.ts +25 -0
  89. package/dist/core/git-utils.d.ts.map +1 -0
  90. package/dist/core/git-utils.js +87 -0
  91. package/dist/core/git-utils.js.map +1 -0
  92. package/dist/core/global-dir.d.ts +44 -0
  93. package/dist/core/global-dir.d.ts.map +1 -0
  94. package/dist/core/global-dir.js +74 -0
  95. package/dist/core/global-dir.js.map +1 -0
  96. package/dist/core/hygiene-signals.d.ts +8 -0
  97. package/dist/core/hygiene-signals.d.ts.map +1 -0
  98. package/dist/core/hygiene-signals.js +41 -0
  99. package/dist/core/hygiene-signals.js.map +1 -0
  100. package/dist/core/implementation-log-migrator.d.ts +41 -0
  101. package/dist/core/implementation-log-migrator.d.ts.map +1 -0
  102. package/dist/core/implementation-log-migrator.js +258 -0
  103. package/dist/core/implementation-log-migrator.js.map +1 -0
  104. package/dist/core/mdx-validator.d.ts +14 -0
  105. package/dist/core/mdx-validator.d.ts.map +1 -0
  106. package/dist/core/mdx-validator.js +34 -0
  107. package/dist/core/mdx-validator.js.map +1 -0
  108. package/dist/core/parser.d.ts +11 -0
  109. package/dist/core/parser.d.ts.map +1 -0
  110. package/dist/core/parser.js +126 -0
  111. package/dist/core/parser.js.map +1 -0
  112. package/dist/core/path-denylist.d.ts +8 -0
  113. package/dist/core/path-denylist.d.ts.map +1 -0
  114. package/dist/core/path-denylist.js +107 -0
  115. package/dist/core/path-denylist.js.map +1 -0
  116. package/dist/core/path-utils.d.ts +69 -0
  117. package/dist/core/path-utils.d.ts.map +1 -0
  118. package/dist/core/path-utils.js +306 -0
  119. package/dist/core/path-utils.js.map +1 -0
  120. package/dist/core/project-registry.d.ts +94 -0
  121. package/dist/core/project-registry.d.ts.map +1 -0
  122. package/dist/core/project-registry.js +297 -0
  123. package/dist/core/project-registry.js.map +1 -0
  124. package/dist/core/security-utils.d.ts +97 -0
  125. package/dist/core/security-utils.d.ts.map +1 -0
  126. package/dist/core/security-utils.js +264 -0
  127. package/dist/core/security-utils.js.map +1 -0
  128. package/dist/core/task-diff.d.ts +15 -0
  129. package/dist/core/task-diff.d.ts.map +1 -0
  130. package/dist/core/task-diff.js +136 -0
  131. package/dist/core/task-diff.js.map +1 -0
  132. package/dist/core/task-parser.d.ts +63 -0
  133. package/dist/core/task-parser.d.ts.map +1 -0
  134. package/dist/core/task-parser.js +332 -0
  135. package/dist/core/task-parser.js.map +1 -0
  136. package/dist/core/task-review-manager.d.ts +56 -0
  137. package/dist/core/task-review-manager.d.ts.map +1 -0
  138. package/dist/core/task-review-manager.js +281 -0
  139. package/dist/core/task-review-manager.js.map +1 -0
  140. package/dist/core/task-validator.d.ts +35 -0
  141. package/dist/core/task-validator.d.ts.map +1 -0
  142. package/dist/core/task-validator.js +236 -0
  143. package/dist/core/task-validator.js.map +1 -0
  144. package/dist/core/typecheck.d.ts +33 -0
  145. package/dist/core/typecheck.d.ts.map +1 -0
  146. package/dist/core/typecheck.js +375 -0
  147. package/dist/core/typecheck.js.map +1 -0
  148. package/dist/core/workspace-initializer.d.ts +16 -0
  149. package/dist/core/workspace-initializer.d.ts.map +1 -0
  150. package/dist/core/workspace-initializer.js +167 -0
  151. package/dist/core/workspace-initializer.js.map +1 -0
  152. package/dist/dashboard/__tests__/adversarial-display-state.test.d.ts +2 -0
  153. package/dist/dashboard/__tests__/adversarial-display-state.test.d.ts.map +1 -0
  154. package/dist/dashboard/__tests__/adversarial-display-state.test.js +59 -0
  155. package/dist/dashboard/__tests__/adversarial-display-state.test.js.map +1 -0
  156. package/dist/dashboard/__tests__/adversarial-endpoints.test.d.ts +2 -0
  157. package/dist/dashboard/__tests__/adversarial-endpoints.test.d.ts.map +1 -0
  158. package/dist/dashboard/__tests__/adversarial-endpoints.test.js +296 -0
  159. package/dist/dashboard/__tests__/adversarial-endpoints.test.js.map +1 -0
  160. package/dist/dashboard/__tests__/adversarial-runner.test.d.ts +2 -0
  161. package/dist/dashboard/__tests__/adversarial-runner.test.d.ts.map +1 -0
  162. package/dist/dashboard/__tests__/adversarial-runner.test.js +315 -0
  163. package/dist/dashboard/__tests__/adversarial-runner.test.js.map +1 -0
  164. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts +2 -0
  165. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts.map +1 -0
  166. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js +78 -0
  167. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js.map +1 -0
  168. package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts +2 -0
  169. package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts.map +1 -0
  170. package/dist/dashboard/__tests__/multi-server-approvals-content.test.js +115 -0
  171. package/dist/dashboard/__tests__/multi-server-approvals-content.test.js.map +1 -0
  172. package/dist/dashboard/__tests__/multi-server.test.d.ts +2 -0
  173. package/dist/dashboard/__tests__/multi-server.test.d.ts.map +1 -0
  174. package/dist/dashboard/__tests__/multi-server.test.js +388 -0
  175. package/dist/dashboard/__tests__/multi-server.test.js.map +1 -0
  176. package/dist/dashboard/__tests__/task-review-runner.test.d.ts +2 -0
  177. package/dist/dashboard/__tests__/task-review-runner.test.d.ts.map +1 -0
  178. package/dist/dashboard/__tests__/task-review-runner.test.js +255 -0
  179. package/dist/dashboard/__tests__/task-review-runner.test.js.map +1 -0
  180. package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts +2 -0
  181. package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts.map +1 -0
  182. package/dist/dashboard/__tests__/watcher-error-handling.test.js +118 -0
  183. package/dist/dashboard/__tests__/watcher-error-handling.test.js.map +1 -0
  184. package/dist/dashboard/adversarial-display-state.d.ts +38 -0
  185. package/dist/dashboard/adversarial-display-state.d.ts.map +1 -0
  186. package/dist/dashboard/adversarial-display-state.js +45 -0
  187. package/dist/dashboard/adversarial-display-state.js.map +1 -0
  188. package/dist/dashboard/adversarial-runner.d.ts +44 -0
  189. package/dist/dashboard/adversarial-runner.d.ts.map +1 -0
  190. package/dist/dashboard/adversarial-runner.js +168 -0
  191. package/dist/dashboard/adversarial-runner.js.map +1 -0
  192. package/dist/dashboard/approval-storage.d.ts +139 -0
  193. package/dist/dashboard/approval-storage.d.ts.map +1 -0
  194. package/dist/dashboard/approval-storage.js +608 -0
  195. package/dist/dashboard/approval-storage.js.map +1 -0
  196. package/dist/dashboard/execution-history-manager.d.ts +52 -0
  197. package/dist/dashboard/execution-history-manager.d.ts.map +1 -0
  198. package/dist/dashboard/execution-history-manager.js +161 -0
  199. package/dist/dashboard/execution-history-manager.js.map +1 -0
  200. package/dist/dashboard/implementation-log-manager.d.ts +102 -0
  201. package/dist/dashboard/implementation-log-manager.d.ts.map +1 -0
  202. package/dist/dashboard/implementation-log-manager.js +594 -0
  203. package/dist/dashboard/implementation-log-manager.js.map +1 -0
  204. package/dist/dashboard/job-scheduler.d.ts +91 -0
  205. package/dist/dashboard/job-scheduler.d.ts.map +1 -0
  206. package/dist/dashboard/job-scheduler.js +321 -0
  207. package/dist/dashboard/job-scheduler.js.map +1 -0
  208. package/dist/dashboard/multi-server.d.ts +45 -0
  209. package/dist/dashboard/multi-server.d.ts.map +1 -0
  210. package/dist/dashboard/multi-server.js +1927 -0
  211. package/dist/dashboard/multi-server.js.map +1 -0
  212. package/dist/dashboard/parser.d.ts +18 -0
  213. package/dist/dashboard/parser.d.ts.map +1 -0
  214. package/dist/dashboard/parser.js +243 -0
  215. package/dist/dashboard/parser.js.map +1 -0
  216. package/dist/dashboard/project-manager.d.ts +82 -0
  217. package/dist/dashboard/project-manager.d.ts.map +1 -0
  218. package/dist/dashboard/project-manager.js +257 -0
  219. package/dist/dashboard/project-manager.js.map +1 -0
  220. package/dist/dashboard/public/assets/Inter-Bold-CD3Pr7BX.woff2 +0 -0
  221. package/dist/dashboard/public/assets/Inter-Medium-B_8v_WHh.woff2 +0 -0
  222. package/dist/dashboard/public/assets/Inter-Regular-DRVdRqcI.woff2 +0 -0
  223. package/dist/dashboard/public/assets/Inter-SemiBold-CtskMddL.woff2 +0 -0
  224. package/dist/dashboard/public/assets/JetBrainsMono-Bold-D4WEaHbo.woff2 +0 -0
  225. package/dist/dashboard/public/assets/JetBrainsMono-Medium-3S3k2nMz.woff2 +0 -0
  226. package/dist/dashboard/public/assets/JetBrainsMono-Regular-BQaDgvhP.woff2 +0 -0
  227. package/dist/dashboard/public/assets/Tableau10-B-NsZVaP.js +1 -0
  228. package/dist/dashboard/public/assets/apl-B4CMkyY2.js +1 -0
  229. package/dist/dashboard/public/assets/arc-A04OOED4.js +1 -0
  230. package/dist/dashboard/public/assets/array-BKyUJesY.js +1 -0
  231. package/dist/dashboard/public/assets/asciiarmor-Df11BRmG.js +1 -0
  232. package/dist/dashboard/public/assets/asn1-EdZsLKOL.js +1 -0
  233. package/dist/dashboard/public/assets/asterisk-B-8jnY81.js +1 -0
  234. package/dist/dashboard/public/assets/blockDiagram-c4efeb88--RobHnfG.js +118 -0
  235. package/dist/dashboard/public/assets/brainfuck-C4LP7Hcl.js +1 -0
  236. package/dist/dashboard/public/assets/c4Diagram-c83219d4-tY8lPXxy.js +10 -0
  237. package/dist/dashboard/public/assets/channel-Cg2ZlORc.js +1 -0
  238. package/dist/dashboard/public/assets/classDiagram-beda092f-DBiupBFm.js +2 -0
  239. package/dist/dashboard/public/assets/classDiagram-v2-2358418a-FM2jRAtm.js +2 -0
  240. package/dist/dashboard/public/assets/clike-B9uivgTg.js +1 -0
  241. package/dist/dashboard/public/assets/clojure-BMjYHr_A.js +1 -0
  242. package/dist/dashboard/public/assets/clone-CCO9qihz.js +1 -0
  243. package/dist/dashboard/public/assets/cmake-BQqOBYOt.js +1 -0
  244. package/dist/dashboard/public/assets/cobol-CWcv1MsR.js +1 -0
  245. package/dist/dashboard/public/assets/coffeescript-S37ZYGWr.js +1 -0
  246. package/dist/dashboard/public/assets/commonlisp-DBKNyK5s.js +1 -0
  247. package/dist/dashboard/public/assets/createText-1719965b-C_nyx2v1.js +7 -0
  248. package/dist/dashboard/public/assets/crystal-SjHAIU92.js +1 -0
  249. package/dist/dashboard/public/assets/css-BnMrqG3P.js +1 -0
  250. package/dist/dashboard/public/assets/cypher-C_CwsFkJ.js +1 -0
  251. package/dist/dashboard/public/assets/d-pRatUO7H.js +1 -0
  252. package/dist/dashboard/public/assets/diff-DbItnlRl.js +1 -0
  253. package/dist/dashboard/public/assets/dockerfile-BKs6k2Af.js +1 -0
  254. package/dist/dashboard/public/assets/dtd-DF_7sFjM.js +1 -0
  255. package/dist/dashboard/public/assets/dylan-DwRh75JA.js +1 -0
  256. package/dist/dashboard/public/assets/ebnf-CDyGwa7X.js +1 -0
  257. package/dist/dashboard/public/assets/ecl-Cabwm37j.js +1 -0
  258. package/dist/dashboard/public/assets/edges-96097737-BsffumDq.js +4 -0
  259. package/dist/dashboard/public/assets/eiffel-CnydiIhH.js +1 -0
  260. package/dist/dashboard/public/assets/elm-vLlmbW-K.js +1 -0
  261. package/dist/dashboard/public/assets/erDiagram-0228fc6a-BK-uOJy1.js +51 -0
  262. package/dist/dashboard/public/assets/erlang-BNw1qcRV.js +1 -0
  263. package/dist/dashboard/public/assets/factor-kuTfRLto.js +1 -0
  264. package/dist/dashboard/public/assets/fcl-Kvtd6kyn.js +1 -0
  265. package/dist/dashboard/public/assets/flowDb-c6c81e3f-BC8_H7l7.js +10 -0
  266. package/dist/dashboard/public/assets/flowDiagram-50d868cf-0XbjRISj.js +4 -0
  267. package/dist/dashboard/public/assets/flowDiagram-v2-4f6560a1-CXE3CwgW.js +1 -0
  268. package/dist/dashboard/public/assets/flowchart-elk-definition-6af322e1-CCvs3UPr.js +139 -0
  269. package/dist/dashboard/public/assets/forth-Ffai-XNe.js +1 -0
  270. package/dist/dashboard/public/assets/fortran-DYz_wnZ1.js +1 -0
  271. package/dist/dashboard/public/assets/ganttDiagram-a2739b55-Yp2gfoGL.js +257 -0
  272. package/dist/dashboard/public/assets/gas-Bneqetm1.js +1 -0
  273. package/dist/dashboard/public/assets/gherkin-heZmZLOM.js +1 -0
  274. package/dist/dashboard/public/assets/gitGraphDiagram-82fe8481-njx_NoNr.js +70 -0
  275. package/dist/dashboard/public/assets/graph-COR8Ljm7.js +1 -0
  276. package/dist/dashboard/public/assets/groovy-D9Dt4D0W.js +1 -0
  277. package/dist/dashboard/public/assets/haskell-Cw1EW3IL.js +1 -0
  278. package/dist/dashboard/public/assets/haxe-H-WmDvRZ.js +1 -0
  279. package/dist/dashboard/public/assets/http-DBlCnlav.js +1 -0
  280. package/dist/dashboard/public/assets/idl-BEugSyMb.js +1 -0
  281. package/dist/dashboard/public/assets/index-0abMV41b.js +1 -0
  282. package/dist/dashboard/public/assets/index-5325376f-cWsAocic.js +1 -0
  283. package/dist/dashboard/public/assets/index-BH121EUE.js +1 -0
  284. package/dist/dashboard/public/assets/index-BMLdkmFP.js +1 -0
  285. package/dist/dashboard/public/assets/index-BYgbrt4G.js +1 -0
  286. package/dist/dashboard/public/assets/index-BwFDzEd4.js +3 -0
  287. package/dist/dashboard/public/assets/index-CS7gsYFH.js +1 -0
  288. package/dist/dashboard/public/assets/index-Cc6cUpKS.js +1 -0
  289. package/dist/dashboard/public/assets/index-CgeymaoH.js +1 -0
  290. package/dist/dashboard/public/assets/index-Cl5FjFWx.js +1 -0
  291. package/dist/dashboard/public/assets/index-CnN2VPRa.js +1 -0
  292. package/dist/dashboard/public/assets/index-DDWkdUEb.js +1 -0
  293. package/dist/dashboard/public/assets/index-DLUjIeO9.js +1 -0
  294. package/dist/dashboard/public/assets/index-Dm3-5ZYh.js +319 -0
  295. package/dist/dashboard/public/assets/index-Ds4s2dDD.js +2 -0
  296. package/dist/dashboard/public/assets/index-DvrmwdIJ.js +1 -0
  297. package/dist/dashboard/public/assets/index-MTVs_iIW.js +7 -0
  298. package/dist/dashboard/public/assets/index-UGm8eYTB.js +1 -0
  299. package/dist/dashboard/public/assets/index-Zi39sM21.css +1 -0
  300. package/dist/dashboard/public/assets/index-iL66igAo.js +1 -0
  301. package/dist/dashboard/public/assets/infoDiagram-8eee0895-TQXQOcbQ.js +7 -0
  302. package/dist/dashboard/public/assets/init-Gi6I4Gst.js +1 -0
  303. package/dist/dashboard/public/assets/javascript-iXu5QeM3.js +1 -0
  304. package/dist/dashboard/public/assets/journeyDiagram-c64418c1-C7AIhJZ8.js +139 -0
  305. package/dist/dashboard/public/assets/julia-DuME0IfC.js +1 -0
  306. package/dist/dashboard/public/assets/katex-XbL3y5x-.js +261 -0
  307. package/dist/dashboard/public/assets/layout-BzInGx9g.js +1 -0
  308. package/dist/dashboard/public/assets/line-BvqpNAKs.js +1 -0
  309. package/dist/dashboard/public/assets/linear-CMco2PTv.js +1 -0
  310. package/dist/dashboard/public/assets/livescript-BwQOo05w.js +1 -0
  311. package/dist/dashboard/public/assets/lua-BgMRiT3U.js +1 -0
  312. package/dist/dashboard/public/assets/mathematica-DTrFuWx2.js +1 -0
  313. package/dist/dashboard/public/assets/mbox-CNhZ1qSd.js +1 -0
  314. package/dist/dashboard/public/assets/mindmap-definition-8da855dc-CeJT8t3A.js +415 -0
  315. package/dist/dashboard/public/assets/mirc-CjQqDB4T.js +1 -0
  316. package/dist/dashboard/public/assets/mllike-CXdrOF99.js +1 -0
  317. package/dist/dashboard/public/assets/modelica-Dc1JOy9r.js +1 -0
  318. package/dist/dashboard/public/assets/mscgen-BA5vi2Kp.js +1 -0
  319. package/dist/dashboard/public/assets/mumps-BT43cFF4.js +1 -0
  320. package/dist/dashboard/public/assets/nginx-DdIZxoE0.js +1 -0
  321. package/dist/dashboard/public/assets/nsis-LdVXkNf5.js +1 -0
  322. package/dist/dashboard/public/assets/ntriples-BfvgReVJ.js +1 -0
  323. package/dist/dashboard/public/assets/octave-Ck1zUtKM.js +1 -0
  324. package/dist/dashboard/public/assets/ordinal-Cboi1Yqb.js +1 -0
  325. package/dist/dashboard/public/assets/oz-BzwKVEFT.js +1 -0
  326. package/dist/dashboard/public/assets/pascal--L3eBynH.js +1 -0
  327. package/dist/dashboard/public/assets/path-CbwjOpE9.js +1 -0
  328. package/dist/dashboard/public/assets/perl-CdXCOZ3F.js +1 -0
  329. package/dist/dashboard/public/assets/pieDiagram-a8764435-DYvb2wsa.js +35 -0
  330. package/dist/dashboard/public/assets/pig-CevX1Tat.js +1 -0
  331. package/dist/dashboard/public/assets/powershell-CFHJl5sT.js +1 -0
  332. package/dist/dashboard/public/assets/properties-C78fOPTZ.js +1 -0
  333. package/dist/dashboard/public/assets/protobuf-ChK-085T.js +1 -0
  334. package/dist/dashboard/public/assets/pug-DeIclll2.js +1 -0
  335. package/dist/dashboard/public/assets/puppet-DMA9R1ak.js +1 -0
  336. package/dist/dashboard/public/assets/python-BuPzkPfP.js +1 -0
  337. package/dist/dashboard/public/assets/q-pXgVlZs6.js +1 -0
  338. package/dist/dashboard/public/assets/quadrantDiagram-1e28029f-DpKQgDsX.js +7 -0
  339. package/dist/dashboard/public/assets/r-B6wPVr8A.js +1 -0
  340. package/dist/dashboard/public/assets/requirementDiagram-08caed73-C_q-bfsF.js +52 -0
  341. package/dist/dashboard/public/assets/rpm-CTu-6PCP.js +1 -0
  342. package/dist/dashboard/public/assets/ruby-B2Rjki9n.js +1 -0
  343. package/dist/dashboard/public/assets/sankeyDiagram-a04cb91d-0kwrmzfI.js +8 -0
  344. package/dist/dashboard/public/assets/sas-B4kiWyti.js +1 -0
  345. package/dist/dashboard/public/assets/scheme-C41bIUwD.js +1 -0
  346. package/dist/dashboard/public/assets/sequenceDiagram-c5b8d532-i5S3JNF0.js +122 -0
  347. package/dist/dashboard/public/assets/shell-CjFT_Tl9.js +1 -0
  348. package/dist/dashboard/public/assets/sieve-C3Gn_uJK.js +1 -0
  349. package/dist/dashboard/public/assets/simple-mode-GW_nhZxv.js +1 -0
  350. package/dist/dashboard/public/assets/smalltalk-CnHTOXQT.js +1 -0
  351. package/dist/dashboard/public/assets/solr-DehyRSwq.js +1 -0
  352. package/dist/dashboard/public/assets/sparql-DkYu6x3z.js +1 -0
  353. package/dist/dashboard/public/assets/spreadsheet-BCZA_wO0.js +1 -0
  354. package/dist/dashboard/public/assets/sql-D0XecflT.js +1 -0
  355. package/dist/dashboard/public/assets/stateDiagram-1ecb1508-COE2ffbQ.js +1 -0
  356. package/dist/dashboard/public/assets/stateDiagram-v2-c2b004d7-DMAS8qJy.js +1 -0
  357. package/dist/dashboard/public/assets/stex-C3f8Ysf7.js +1 -0
  358. package/dist/dashboard/public/assets/styles-b4e223ce-M0Oa_txo.js +160 -0
  359. package/dist/dashboard/public/assets/styles-ca3715f6-DddhRVVB.js +207 -0
  360. package/dist/dashboard/public/assets/styles-d45a18b0-ByKhWZca.js +116 -0
  361. package/dist/dashboard/public/assets/stylus-B533Al4x.js +1 -0
  362. package/dist/dashboard/public/assets/svgDrawCommon-b86b1483-DIF6Vn69.js +1 -0
  363. package/dist/dashboard/public/assets/swift-BzpIVaGY.js +1 -0
  364. package/dist/dashboard/public/assets/tcl-DVfN8rqt.js +1 -0
  365. package/dist/dashboard/public/assets/textile-CnDTJFAw.js +1 -0
  366. package/dist/dashboard/public/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  367. package/dist/dashboard/public/assets/tiki-DGYXhP31.js +1 -0
  368. package/dist/dashboard/public/assets/timeline-definition-faaaa080-DC1Bdpu_.js +61 -0
  369. package/dist/dashboard/public/assets/toml-Bm5Em-hy.js +1 -0
  370. package/dist/dashboard/public/assets/troff-wAsdV37c.js +1 -0
  371. package/dist/dashboard/public/assets/ttcn-CfJYG6tj.js +1 -0
  372. package/dist/dashboard/public/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  373. package/dist/dashboard/public/assets/turtle-B1tBg_DP.js +1 -0
  374. package/dist/dashboard/public/assets/vb-CmGdzxic.js +1 -0
  375. package/dist/dashboard/public/assets/vbscript-BuJXcnF6.js +1 -0
  376. package/dist/dashboard/public/assets/velocity-D8B20fx6.js +1 -0
  377. package/dist/dashboard/public/assets/verilog-C6RDOZhf.js +1 -0
  378. package/dist/dashboard/public/assets/vhdl-lSbBsy5d.js +1 -0
  379. package/dist/dashboard/public/assets/webidl-ZXfAyPTL.js +1 -0
  380. package/dist/dashboard/public/assets/xquery-DzFWVndE.js +1 -0
  381. package/dist/dashboard/public/assets/xychartDiagram-f5964ef8-BIMyvG9y.js +7 -0
  382. package/dist/dashboard/public/assets/yacas-BJ4BC0dw.js +1 -0
  383. package/dist/dashboard/public/assets/z80-Hz9HOZM7.js +1 -0
  384. package/dist/dashboard/public/claude-icon-dark.svg +1 -0
  385. package/dist/dashboard/public/claude-icon.svg +1 -0
  386. package/dist/dashboard/public/index.html +16 -0
  387. package/dist/dashboard/settings-manager.d.ts +47 -0
  388. package/dist/dashboard/settings-manager.d.ts.map +1 -0
  389. package/dist/dashboard/settings-manager.js +180 -0
  390. package/dist/dashboard/settings-manager.js.map +1 -0
  391. package/dist/dashboard/task-review-runner.d.ts +42 -0
  392. package/dist/dashboard/task-review-runner.d.ts.map +1 -0
  393. package/dist/dashboard/task-review-runner.js +375 -0
  394. package/dist/dashboard/task-review-runner.js.map +1 -0
  395. package/dist/dashboard/utils.d.ts +31 -0
  396. package/dist/dashboard/utils.d.ts.map +1 -0
  397. package/dist/dashboard/utils.js +102 -0
  398. package/dist/dashboard/utils.js.map +1 -0
  399. package/dist/dashboard/watcher.d.ts +32 -0
  400. package/dist/dashboard/watcher.d.ts.map +1 -0
  401. package/dist/dashboard/watcher.js +173 -0
  402. package/dist/dashboard/watcher.js.map +1 -0
  403. package/dist/index.d.ts +13 -0
  404. package/dist/index.d.ts.map +1 -0
  405. package/dist/index.js +380 -0
  406. package/dist/index.js.map +1 -0
  407. package/dist/markdown/templates/design-template.md +96 -0
  408. package/dist/markdown/templates/product-template.md +51 -0
  409. package/dist/markdown/templates/requirements-template.md +50 -0
  410. package/dist/markdown/templates/structure-template.md +145 -0
  411. package/dist/markdown/templates/tasks-template.md +139 -0
  412. package/dist/markdown/templates/tech-template.md +99 -0
  413. package/dist/prompts/create-decomposition.d.ts +3 -0
  414. package/dist/prompts/create-decomposition.d.ts.map +1 -0
  415. package/dist/prompts/create-decomposition.js +122 -0
  416. package/dist/prompts/create-decomposition.js.map +1 -0
  417. package/dist/prompts/create-spec.d.ts +3 -0
  418. package/dist/prompts/create-spec.d.ts.map +1 -0
  419. package/dist/prompts/create-spec.js +93 -0
  420. package/dist/prompts/create-spec.js.map +1 -0
  421. package/dist/prompts/create-steering-doc.d.ts +3 -0
  422. package/dist/prompts/create-steering-doc.d.ts.map +1 -0
  423. package/dist/prompts/create-steering-doc.js +73 -0
  424. package/dist/prompts/create-steering-doc.js.map +1 -0
  425. package/dist/prompts/implement-task.d.ts +3 -0
  426. package/dist/prompts/implement-task.d.ts.map +1 -0
  427. package/dist/prompts/implement-task.js +173 -0
  428. package/dist/prompts/implement-task.js.map +1 -0
  429. package/dist/prompts/index.d.ts +15 -0
  430. package/dist/prompts/index.d.ts.map +1 -0
  431. package/dist/prompts/index.js +51 -0
  432. package/dist/prompts/index.js.map +1 -0
  433. package/dist/prompts/inject-spec-workflow-guide.d.ts +3 -0
  434. package/dist/prompts/inject-spec-workflow-guide.d.ts.map +1 -0
  435. package/dist/prompts/inject-spec-workflow-guide.js +47 -0
  436. package/dist/prompts/inject-spec-workflow-guide.js.map +1 -0
  437. package/dist/prompts/inject-steering-guide.d.ts +3 -0
  438. package/dist/prompts/inject-steering-guide.d.ts.map +1 -0
  439. package/dist/prompts/inject-steering-guide.js +51 -0
  440. package/dist/prompts/inject-steering-guide.js.map +1 -0
  441. package/dist/prompts/refresh-tasks.d.ts +3 -0
  442. package/dist/prompts/refresh-tasks.d.ts.map +1 -0
  443. package/dist/prompts/refresh-tasks.js +224 -0
  444. package/dist/prompts/refresh-tasks.js.map +1 -0
  445. package/dist/prompts/spec-status.d.ts +3 -0
  446. package/dist/prompts/spec-status.d.ts.map +1 -0
  447. package/dist/prompts/spec-status.js +75 -0
  448. package/dist/prompts/spec-status.js.map +1 -0
  449. package/dist/prompts/types.d.ts +13 -0
  450. package/dist/prompts/types.d.ts.map +1 -0
  451. package/dist/prompts/types.js +2 -0
  452. package/dist/prompts/types.js.map +1 -0
  453. package/dist/server.d.ts +17 -0
  454. package/dist/server.d.ts.map +1 -0
  455. package/dist/server.js +175 -0
  456. package/dist/server.js.map +1 -0
  457. package/dist/tools/__tests__/adversarial-response.test.d.ts +2 -0
  458. package/dist/tools/__tests__/adversarial-response.test.d.ts.map +1 -0
  459. package/dist/tools/__tests__/adversarial-response.test.js +144 -0
  460. package/dist/tools/__tests__/adversarial-response.test.js.map +1 -0
  461. package/dist/tools/__tests__/adversarial-review.test.d.ts +2 -0
  462. package/dist/tools/__tests__/adversarial-review.test.d.ts.map +1 -0
  463. package/dist/tools/__tests__/adversarial-review.test.js +318 -0
  464. package/dist/tools/__tests__/adversarial-review.test.js.map +1 -0
  465. package/dist/tools/__tests__/decomposition-guide.test.d.ts +2 -0
  466. package/dist/tools/__tests__/decomposition-guide.test.d.ts.map +1 -0
  467. package/dist/tools/__tests__/decomposition-guide.test.js +25 -0
  468. package/dist/tools/__tests__/decomposition-guide.test.js.map +1 -0
  469. package/dist/tools/__tests__/deferrals.test.d.ts +2 -0
  470. package/dist/tools/__tests__/deferrals.test.d.ts.map +1 -0
  471. package/dist/tools/__tests__/deferrals.test.js +151 -0
  472. package/dist/tools/__tests__/deferrals.test.js.map +1 -0
  473. package/dist/tools/__tests__/get-task-review.test.d.ts +2 -0
  474. package/dist/tools/__tests__/get-task-review.test.d.ts.map +1 -0
  475. package/dist/tools/__tests__/get-task-review.test.js +81 -0
  476. package/dist/tools/__tests__/get-task-review.test.js.map +1 -0
  477. package/dist/tools/__tests__/projectPath.test.d.ts +2 -0
  478. package/dist/tools/__tests__/projectPath.test.d.ts.map +1 -0
  479. package/dist/tools/__tests__/projectPath.test.js +187 -0
  480. package/dist/tools/__tests__/projectPath.test.js.map +1 -0
  481. package/dist/tools/__tests__/review-task.test.d.ts +2 -0
  482. package/dist/tools/__tests__/review-task.test.d.ts.map +1 -0
  483. package/dist/tools/__tests__/review-task.test.js +1097 -0
  484. package/dist/tools/__tests__/review-task.test.js.map +1 -0
  485. package/dist/tools/adversarial-response.d.ts +6 -0
  486. package/dist/tools/adversarial-response.d.ts.map +1 -0
  487. package/dist/tools/adversarial-response.js +206 -0
  488. package/dist/tools/adversarial-response.js.map +1 -0
  489. package/dist/tools/adversarial-review.d.ts +21 -0
  490. package/dist/tools/adversarial-review.d.ts.map +1 -0
  491. package/dist/tools/adversarial-review.js +491 -0
  492. package/dist/tools/adversarial-review.js.map +1 -0
  493. package/dist/tools/approvals.d.ts +14 -0
  494. package/dist/tools/approvals.d.ts.map +1 -0
  495. package/dist/tools/approvals.js +505 -0
  496. package/dist/tools/approvals.js.map +1 -0
  497. package/dist/tools/decomposition-guide.d.ts +6 -0
  498. package/dist/tools/decomposition-guide.d.ts.map +1 -0
  499. package/dist/tools/decomposition-guide.js +163 -0
  500. package/dist/tools/decomposition-guide.js.map +1 -0
  501. package/dist/tools/deferrals.d.ts +5 -0
  502. package/dist/tools/deferrals.d.ts.map +1 -0
  503. package/dist/tools/deferrals.js +229 -0
  504. package/dist/tools/deferrals.js.map +1 -0
  505. package/dist/tools/get-task-review.d.ts +5 -0
  506. package/dist/tools/get-task-review.d.ts.map +1 -0
  507. package/dist/tools/get-task-review.js +136 -0
  508. package/dist/tools/get-task-review.js.map +1 -0
  509. package/dist/tools/index.d.ts +5 -0
  510. package/dist/tools/index.d.ts.map +1 -0
  511. package/dist/tools/index.js +82 -0
  512. package/dist/tools/index.js.map +1 -0
  513. package/dist/tools/log-implementation.d.ts +5 -0
  514. package/dist/tools/log-implementation.d.ts.map +1 -0
  515. package/dist/tools/log-implementation.js +398 -0
  516. package/dist/tools/log-implementation.js.map +1 -0
  517. package/dist/tools/review-task.d.ts +58 -0
  518. package/dist/tools/review-task.d.ts.map +1 -0
  519. package/dist/tools/review-task.js +617 -0
  520. package/dist/tools/review-task.js.map +1 -0
  521. package/dist/tools/spec-status.d.ts +5 -0
  522. package/dist/tools/spec-status.d.ts.map +1 -0
  523. package/dist/tools/spec-status.js +235 -0
  524. package/dist/tools/spec-status.js.map +1 -0
  525. package/dist/tools/spec-workflow-guide.d.ts +5 -0
  526. package/dist/tools/spec-workflow-guide.d.ts.map +1 -0
  527. package/dist/tools/spec-workflow-guide.js +396 -0
  528. package/dist/tools/spec-workflow-guide.js.map +1 -0
  529. package/dist/tools/steering-guide.d.ts +5 -0
  530. package/dist/tools/steering-guide.d.ts.map +1 -0
  531. package/dist/tools/steering-guide.js +192 -0
  532. package/dist/tools/steering-guide.js.map +1 -0
  533. package/dist/types.d.ts +213 -0
  534. package/dist/types.d.ts.map +1 -0
  535. package/dist/types.js +13 -0
  536. package/dist/types.js.map +1 -0
  537. package/package.json +113 -0
@@ -0,0 +1,1097 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { promises as fs, symlinkSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync } from 'fs';
3
+ import path, { join, dirname } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ const overrides = vi.hoisted(() => ({
7
+ typecheck: null,
8
+ hygiene: null,
9
+ diff: null,
10
+ }));
11
+ vi.mock('../../core/typecheck.js', async (importOriginal) => {
12
+ const actual = await importOriginal();
13
+ return {
14
+ ...actual,
15
+ runProjectTypecheck: (...args) => overrides.typecheck ? overrides.typecheck(...args) : actual.runProjectTypecheck(...args),
16
+ };
17
+ });
18
+ vi.mock('../../core/hygiene-signals.js', async (importOriginal) => {
19
+ const actual = await importOriginal();
20
+ return {
21
+ ...actual,
22
+ computeHygieneSignals: (...args) => overrides.hygiene ? overrides.hygiene(...args) : actual.computeHygieneSignals(...args),
23
+ };
24
+ });
25
+ vi.mock('../../core/task-diff.js', async (importOriginal) => {
26
+ const actual = await importOriginal();
27
+ return {
28
+ ...actual,
29
+ computeTaskDiff: (...args) => overrides.diff ? overrides.diff(...args) : actual.computeTaskDiff(...args),
30
+ };
31
+ });
32
+ import { reviewTaskHandler, validateAllFiles, safeRealpath, _resetValidateWarnings, buildReviewMethodology, } from '../review-task.js';
33
+ import { ImplementationLogManager } from '../../dashboard/implementation-log-manager.js';
34
+ describe('review-task handler', () => {
35
+ let tempDir;
36
+ let context;
37
+ let specPath;
38
+ beforeEach(async () => {
39
+ overrides.typecheck = null;
40
+ overrides.hygiene = null;
41
+ overrides.diff = null;
42
+ _resetValidateWarnings();
43
+ tempDir = await fs.mkdtemp(join(tmpdir(), 'review-task-test-'));
44
+ specPath = join(tempDir, '.spec-workflow', 'specs', 'test-spec');
45
+ await fs.mkdir(specPath, { recursive: true });
46
+ context = { projectPath: tempDir };
47
+ // Create a minimal tasks.md
48
+ await fs.writeFile(join(specPath, 'tasks.md'), [
49
+ '# Tasks',
50
+ '',
51
+ '- [-] 1. Implement feature',
52
+ ' _Requirements: REQ-001_',
53
+ ' _Prompt: Role: Developer | Task: Build it | Restrictions: No new deps | Success: Tests pass_',
54
+ '',
55
+ '- [ ] 2. Another task',
56
+ ].join('\n'));
57
+ });
58
+ afterEach(async () => {
59
+ await fs.rm(tempDir, { recursive: true, force: true });
60
+ });
61
+ async function createImplLog() {
62
+ // Materialize the referenced files so validateAllFiles keeps them
63
+ // (it drops paths whose realpath ENOENTs).
64
+ await fs.mkdir(join(tempDir, 'src'), { recursive: true });
65
+ await fs.writeFile(join(tempDir, 'src/handler.ts'), 'export const x = 1;\n');
66
+ await fs.writeFile(join(tempDir, 'src/new-file.ts'), 'export const y = 2;\n');
67
+ const logManager = new ImplementationLogManager(specPath);
68
+ await logManager.addLogEntry({
69
+ taskId: '1',
70
+ timestamp: new Date().toISOString(),
71
+ summary: 'Implemented feature',
72
+ filesModified: ['src/handler.ts'],
73
+ filesCreated: ['src/new-file.ts'],
74
+ statistics: { linesAdded: 50, linesRemoved: 5, filesChanged: 2 },
75
+ artifacts: {
76
+ functions: [{ name: 'handleRequest', purpose: 'Handle request', location: 'src/handler.ts:10', isExported: true }],
77
+ },
78
+ });
79
+ }
80
+ describe('prepare action', () => {
81
+ it('should fail if no implementation log exists', async () => {
82
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
83
+ expect(result.success).toBe(false);
84
+ expect(result.message).toContain('No implementation log');
85
+ });
86
+ it('should fail if task does not exist', async () => {
87
+ await createImplLog();
88
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '999' }, context);
89
+ expect(result.success).toBe(false);
90
+ expect(result.message).toContain('not found');
91
+ });
92
+ it('should return review context and methodology', async () => {
93
+ await createImplLog();
94
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
95
+ expect(result.success).toBe(true);
96
+ expect(result.data.taskContext).toBeDefined();
97
+ expect(result.data.implementationSummary).toBeDefined();
98
+ expect(result.data.filesToReview).toContain(join(tempDir, 'src/handler.ts'));
99
+ expect(result.data.methodology).toContain('Review Methodology');
100
+ expect(result.data.methodology).toContain('No new deps');
101
+ expect(result.data.methodology).toContain('Tests pass');
102
+ });
103
+ it('should write a prepare marker', async () => {
104
+ await createImplLog();
105
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
106
+ // Check marker file exists
107
+ const reviewsDir = join(specPath, 'reviews');
108
+ const files = await fs.readdir(reviewsDir);
109
+ expect(files.some(f => f.startsWith('.prepare-'))).toBe(true);
110
+ });
111
+ describe('hygiene signal integration', () => {
112
+ const ORIGINAL_ITEM_9 = '9. **Hygiene**: Hardcoded secrets, leftover debug code (console.log, TODO/FIXME from this task), commented-out code, unused imports or variables introduced by this task. Mark findings from items 7-9 with category: "hygiene".';
113
+ async function seedLogWithFiles(filesModified, filesCreated = []) {
114
+ const logManager = new ImplementationLogManager(specPath);
115
+ await logManager.addLogEntry({
116
+ taskId: '1',
117
+ timestamp: new Date().toISOString(),
118
+ summary: 'Implemented feature',
119
+ filesModified,
120
+ filesCreated,
121
+ statistics: { linesAdded: 10, linesRemoved: 0, filesChanged: filesModified.length + filesCreated.length },
122
+ artifacts: {},
123
+ });
124
+ }
125
+ it('(a) returns hygieneSignals with correct line numbers and patterns', async () => {
126
+ const relPath = 'src/dirty.ts';
127
+ const absPath = join(tempDir, relPath);
128
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
129
+ await fs.writeFile(absPath, [
130
+ 'function foo() {',
131
+ " console.log('debug');",
132
+ ' // TODO: x',
133
+ '}',
134
+ ].join('\n'));
135
+ await seedLogWithFiles([relPath]);
136
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
137
+ expect(result.success).toBe(true);
138
+ const signals = result.data.hygieneSignals;
139
+ expect(signals).toHaveLength(2);
140
+ const consoleSig = signals.find((s) => s.pattern === 'console');
141
+ const todoSig = signals.find((s) => s.pattern === 'todo');
142
+ expect(consoleSig.line).toBe(2);
143
+ expect(todoSig.line).toBe(3);
144
+ });
145
+ it('(b) clean task returns empty hygieneSignals AND original item 9 text', async () => {
146
+ const relPath = 'src/clean.ts';
147
+ const absPath = join(tempDir, relPath);
148
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
149
+ await fs.writeFile(absPath, [
150
+ 'export function add(a: number, b: number) {',
151
+ ' return a + b;',
152
+ '}',
153
+ ].join('\n'));
154
+ await seedLogWithFiles([relPath]);
155
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
156
+ expect(result.success).toBe(true);
157
+ expect(result.data.hygieneSignals).toEqual([]);
158
+ expect(result.data.methodology).toContain(ORIGINAL_ITEM_9);
159
+ });
160
+ it('(c) methodology contains triage directive when signals are present', async () => {
161
+ const relPath = 'src/dirty.ts';
162
+ const absPath = join(tempDir, relPath);
163
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
164
+ await fs.writeFile(absPath, "console.log('hi');");
165
+ await seedLogWithFiles([relPath]);
166
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
167
+ expect(result.success).toBe(true);
168
+ expect(result.data.methodology).toContain('Pre-computed hygiene signals are attached in');
169
+ });
170
+ it('(d) every signal has an absolute file path', async () => {
171
+ const relPath = 'src/dirty.ts';
172
+ const absPath = join(tempDir, relPath);
173
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
174
+ await fs.writeFile(absPath, [
175
+ "console.log('a');",
176
+ '// TODO: y',
177
+ 'debugger;',
178
+ ].join('\n'));
179
+ await seedLogWithFiles([relPath]);
180
+ const result = await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
181
+ expect(result.success).toBe(true);
182
+ const signals = result.data.hygieneSignals;
183
+ expect(signals.length).toBeGreaterThan(0);
184
+ for (const signal of signals) {
185
+ expect(path.isAbsolute(signal.file)).toBe(true);
186
+ }
187
+ });
188
+ });
189
+ });
190
+ describe('record action', () => {
191
+ it('should fail without prepare marker', async () => {
192
+ await createImplLog();
193
+ const result = await reviewTaskHandler({ action: 'record', specName: 'test-spec', taskId: '1', verdict: 'pass', summary: 'OK', findings: [] }, context);
194
+ expect(result.success).toBe(false);
195
+ expect(result.message).toContain('prepare');
196
+ });
197
+ it('should reject pass verdict with findings', async () => {
198
+ await createImplLog();
199
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
200
+ const result = await reviewTaskHandler({
201
+ action: 'record', specName: 'test-spec', taskId: '1',
202
+ verdict: 'pass', summary: 'OK',
203
+ findings: [{ severity: 'info', title: 'Note', description: 'Something' }]
204
+ }, context);
205
+ expect(result.success).toBe(false);
206
+ expect(result.message).toContain('zero findings');
207
+ });
208
+ it('should reject fail verdict without criticals', async () => {
209
+ await createImplLog();
210
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
211
+ const result = await reviewTaskHandler({
212
+ action: 'record', specName: 'test-spec', taskId: '1',
213
+ verdict: 'fail', summary: 'Bad',
214
+ findings: [{ severity: 'warning', title: 'Warn', description: 'Not critical' }]
215
+ }, context);
216
+ expect(result.success).toBe(false);
217
+ expect(result.message).toContain('critical finding');
218
+ });
219
+ it('should reject findings verdict with criticals', async () => {
220
+ await createImplLog();
221
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
222
+ const result = await reviewTaskHandler({
223
+ action: 'record', specName: 'test-spec', taskId: '1',
224
+ verdict: 'findings', summary: 'Issues',
225
+ findings: [{ severity: 'critical', title: 'Crit', description: 'Bad' }]
226
+ }, context);
227
+ expect(result.success).toBe(false);
228
+ expect(result.message).toContain('fail');
229
+ });
230
+ it('should record a passing review', async () => {
231
+ await createImplLog();
232
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
233
+ const result = await reviewTaskHandler({ action: 'record', specName: 'test-spec', taskId: '1', verdict: 'pass', summary: 'All checks passed', findings: [] }, context);
234
+ expect(result.success).toBe(true);
235
+ expect(result.data.verdict).toBe('pass');
236
+ expect(result.data.version).toBe(1);
237
+ expect(result.data.criticalCount).toBe(0);
238
+ });
239
+ it('should record a failing review with severity counts', async () => {
240
+ await createImplLog();
241
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
242
+ const result = await reviewTaskHandler({
243
+ action: 'record', specName: 'test-spec', taskId: '1',
244
+ verdict: 'fail', summary: 'Critical bug',
245
+ findings: [
246
+ { severity: 'critical', title: 'Bug', description: 'desc' },
247
+ { severity: 'warning', title: 'Warn', description: 'desc' },
248
+ { severity: 'info', title: 'Note', description: 'desc' },
249
+ ]
250
+ }, context);
251
+ expect(result.success).toBe(true);
252
+ expect(result.data.criticalCount).toBe(1);
253
+ expect(result.data.warningCount).toBe(1);
254
+ expect(result.data.infoCount).toBe(1);
255
+ });
256
+ it('should increment version on re-review', async () => {
257
+ await createImplLog();
258
+ // First review
259
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
260
+ await reviewTaskHandler({ action: 'record', specName: 'test-spec', taskId: '1', verdict: 'fail', summary: 'Bad', findings: [{ severity: 'critical', title: 'X', description: 'Y' }] }, context);
261
+ // Second review
262
+ await reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
263
+ const result = await reviewTaskHandler({ action: 'record', specName: 'test-spec', taskId: '1', verdict: 'pass', summary: 'Fixed', findings: [] }, context);
264
+ expect(result.data.version).toBe(2);
265
+ });
266
+ });
267
+ });
268
+ describe('safeRealpath', () => {
269
+ let warnSpy;
270
+ let tempDir;
271
+ beforeEach(async () => {
272
+ _resetValidateWarnings();
273
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
274
+ tempDir = await fs.mkdtemp(join(tmpdir(), 'safe-realpath-test-'));
275
+ });
276
+ afterEach(async () => {
277
+ warnSpy.mockRestore();
278
+ await fs.rm(tempDir, { recursive: true, force: true });
279
+ });
280
+ it('returns the realpath for an existing file', () => {
281
+ const filePath = join(tempDir, 'a.txt');
282
+ writeFileSync(filePath, '');
283
+ const result = safeRealpath(filePath);
284
+ expect(result).toBeDefined();
285
+ expect(typeof result).toBe('string');
286
+ });
287
+ it('returns undefined silently on ENOENT (deleted/missing file)', () => {
288
+ const missing = join(tempDir, 'does-not-exist.txt');
289
+ const result = safeRealpath(missing);
290
+ expect(result).toBeUndefined();
291
+ expect(warnSpy).not.toHaveBeenCalled();
292
+ });
293
+ it('warn-once on non-ENOENT error (ELOOP from symlink cycle)', () => {
294
+ const a = join(tempDir, 'a-link');
295
+ const b = join(tempDir, 'b-link');
296
+ symlinkSync(b, a);
297
+ symlinkSync(a, b);
298
+ const r1 = safeRealpath(a);
299
+ expect(r1).toBeUndefined();
300
+ expect(warnSpy).toHaveBeenCalledTimes(1);
301
+ expect(warnSpy.mock.calls[0][0]).toMatch(/safeRealpath: ELOOP/);
302
+ // Same path + same code: deduped
303
+ const r2 = safeRealpath(a);
304
+ expect(r2).toBeUndefined();
305
+ expect(warnSpy).toHaveBeenCalledTimes(1);
306
+ });
307
+ });
308
+ describe('validateAllFiles', () => {
309
+ let warnSpy;
310
+ let tempDir;
311
+ beforeEach(async () => {
312
+ _resetValidateWarnings();
313
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
314
+ tempDir = await fs.mkdtemp(join(tmpdir(), 'validate-all-files-test-'));
315
+ });
316
+ afterEach(async () => {
317
+ warnSpy.mockRestore();
318
+ await fs.rm(tempDir, { recursive: true, force: true });
319
+ });
320
+ function makeFile(rel) {
321
+ const abs = join(tempDir, rel);
322
+ mkdirSync(path.dirname(abs), { recursive: true });
323
+ writeFileSync(abs, '');
324
+ return abs;
325
+ }
326
+ it('returns [] and warns on non-array input', () => {
327
+ expect(validateAllFiles(null, tempDir)).toEqual([]);
328
+ expect(validateAllFiles('not-an-array', tempDir)).toEqual([]);
329
+ expect(validateAllFiles({ length: 1, 0: 'x' }, tempDir)).toEqual([]);
330
+ expect(warnSpy).toHaveBeenCalled();
331
+ expect(warnSpy.mock.calls[0][0]).toMatch(/allFiles is not an array/);
332
+ });
333
+ it('drops NUL-byte paths with warn', () => {
334
+ makeFile('ok.ts');
335
+ const result = validateAllFiles(['ok.ts', 'bad\0.ts'], tempDir);
336
+ expect(result).toHaveLength(1);
337
+ expect(result[0]).toBe(path.resolve(tempDir, 'ok.ts'));
338
+ const warnings = warnSpy.mock.calls.map((c) => c[0]).join('\n');
339
+ // NUL-byte handling differs across Node versions: path.resolve may throw
340
+ // (ERR_INVALID_ARG_VALUE), or realpathSync rejects it. Either way the
341
+ // entry must be dropped and some warn must fire.
342
+ expect(warnings).toMatch(/path\.resolve threw|safeRealpath/);
343
+ });
344
+ it('drops non-string elements (number, Symbol, BigInt, null, undefined) with warn', () => {
345
+ makeFile('ok.ts');
346
+ const result = validateAllFiles([42, Symbol('s'), BigInt(0), null, undefined, 'ok.ts'], tempDir);
347
+ expect(result).toEqual([path.resolve(tempDir, 'ok.ts')]);
348
+ const warnings = warnSpy.mock.calls.map((c) => c[0]).join('\n');
349
+ expect(warnings).toMatch(/non-string entry at index 0/);
350
+ });
351
+ it('keeps relative paths (resolved against projectPath)', () => {
352
+ makeFile('src/handler.ts');
353
+ const result = validateAllFiles(['src/handler.ts'], tempDir);
354
+ expect(result).toEqual([path.resolve(tempDir, 'src/handler.ts')]);
355
+ expect(warnSpy).not.toHaveBeenCalled();
356
+ });
357
+ it('drops absolute paths resolving outside projectPath with warn', async () => {
358
+ const otherDir = await fs.mkdtemp(join(tmpdir(), 'validate-other-'));
359
+ try {
360
+ const outside = join(otherDir, 'outside.txt');
361
+ writeFileSync(outside, '');
362
+ makeFile('inside.ts');
363
+ const result = validateAllFiles([outside, 'inside.ts'], tempDir);
364
+ expect(result).toEqual([path.resolve(tempDir, 'inside.ts')]);
365
+ const warnings = warnSpy.mock.calls.map((c) => c[0]).join('\n');
366
+ expect(warnings).toMatch(/path outside projectPath/);
367
+ }
368
+ finally {
369
+ await fs.rm(otherDir, { recursive: true, force: true });
370
+ }
371
+ });
372
+ it('drops symlinks whose target is outside projectPath with warn', async () => {
373
+ const otherDir = await fs.mkdtemp(join(tmpdir(), 'validate-other-'));
374
+ try {
375
+ const outsideTarget = join(otherDir, 'outside.ts');
376
+ writeFileSync(outsideTarget, '');
377
+ const linkPath = join(tempDir, 'link.ts');
378
+ symlinkSync(outsideTarget, linkPath);
379
+ const result = validateAllFiles(['link.ts'], tempDir);
380
+ expect(result).toEqual([]);
381
+ const warnings = warnSpy.mock.calls.map((c) => c[0]).join('\n');
382
+ expect(warnings).toMatch(/path outside projectPath/);
383
+ }
384
+ finally {
385
+ await fs.rm(otherDir, { recursive: true, force: true });
386
+ }
387
+ });
388
+ it('drops deleted files silently (safeRealpath ENOENT, no warn)', () => {
389
+ makeFile('exists.ts');
390
+ const result = validateAllFiles(['exists.ts', 'gone.ts'], tempDir);
391
+ expect(result).toEqual([path.resolve(tempDir, 'exists.ts')]);
392
+ expect(warnSpy).not.toHaveBeenCalled();
393
+ });
394
+ it('dedupes duplicates by realpath and preserves first-seen original', () => {
395
+ makeFile('src/a.ts');
396
+ const result = validateAllFiles(['src/a.ts', './src/a.ts', path.resolve(tempDir, 'src/a.ts')], tempDir);
397
+ expect(result).toHaveLength(1);
398
+ expect(result[0]).toBe(path.resolve(tempDir, 'src/a.ts'));
399
+ });
400
+ });
401
+ // ---------------------------------------------------------------------------
402
+ // handlePrepare integration tests (task 11; Track-A composite-pin block
403
+ // removed in 16.1 — Track-B fixtures + composite/drift/sentinel pins land in
404
+ // task 17).
405
+ // ---------------------------------------------------------------------------
406
+ describe('handlePrepare integration', () => {
407
+ let tempDir;
408
+ let specPath;
409
+ let context;
410
+ let warnSpy;
411
+ // Canonical fixture inputs (R4.10):
412
+ // requirements=['1.1','2.4']; restrictions='Do NOT bypass denylist; do NOT change truncation messages';
413
+ // success='All listed cases pass; messages emit verbatim'; leverage='src/core/path-denylist.ts';
414
+ // hasTechSteering=true; hasPriorReviews=false; hasHygieneSignals=true.
415
+ const CANONICAL_TASKS_MD = [
416
+ '# Tasks',
417
+ '',
418
+ '- [-] 1. Implement feature',
419
+ ' - _Leverage: src/core/path-denylist.ts_',
420
+ ' - _Requirements: 1.1, 2.4_',
421
+ ' - _Prompt: Role: Developer | Task: Build it | Restrictions: Do NOT bypass denylist; do NOT change truncation messages | Success: All listed cases pass; messages emit verbatim_',
422
+ '',
423
+ ].join('\n');
424
+ beforeEach(async () => {
425
+ overrides.typecheck = null;
426
+ overrides.hygiene = null;
427
+ overrides.diff = null;
428
+ _resetValidateWarnings();
429
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
430
+ tempDir = await fs.mkdtemp(join(tmpdir(), 'review-task-track-a-'));
431
+ specPath = join(tempDir, '.spec-workflow', 'specs', 'test-spec');
432
+ await fs.mkdir(specPath, { recursive: true });
433
+ // Steering doc → hasTechSteering=true
434
+ const steeringDir = join(tempDir, '.spec-workflow', 'steering');
435
+ await fs.mkdir(steeringDir, { recursive: true });
436
+ await fs.writeFile(join(steeringDir, 'tech.md'), '# Tech\n');
437
+ await fs.writeFile(join(specPath, 'tasks.md'), CANONICAL_TASKS_MD);
438
+ context = { projectPath: tempDir };
439
+ });
440
+ afterEach(async () => {
441
+ overrides.typecheck = null;
442
+ overrides.hygiene = null;
443
+ overrides.diff = null;
444
+ warnSpy.mockRestore();
445
+ await fs.rm(tempDir, { recursive: true, force: true });
446
+ });
447
+ async function seedLog(filesModified, filesCreated = []) {
448
+ const logManager = new ImplementationLogManager(specPath);
449
+ await logManager.addLogEntry({
450
+ taskId: '1',
451
+ timestamp: new Date().toISOString(),
452
+ summary: 'Implemented',
453
+ filesModified,
454
+ filesCreated,
455
+ statistics: { linesAdded: 1, linesRemoved: 0, filesChanged: filesModified.length + filesCreated.length },
456
+ artifacts: {},
457
+ });
458
+ }
459
+ async function materializeFile(rel, content = 'export const x = 1;\n') {
460
+ const abs = join(tempDir, rel);
461
+ await fs.mkdir(path.dirname(abs), { recursive: true });
462
+ await fs.writeFile(abs, content);
463
+ return abs;
464
+ }
465
+ async function runPrepare() {
466
+ return reviewTaskHandler({ action: 'prepare', specName: 'test-spec', taskId: '1' }, context);
467
+ }
468
+ it('returns typecheckResults and emits Item 10 directive (success-with-diagnostics)', async () => {
469
+ await materializeFile('src/x.ts');
470
+ await seedLog(['src/x.ts']);
471
+ overrides.typecheck = async () => [
472
+ {
473
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
474
+ status: 'success',
475
+ diagnostics: [
476
+ { file: join(tempDir, 'src/x.ts'), line: 1, column: 1, code: 'TS2322', message: 'oops', inScope: true },
477
+ ],
478
+ coverage: { compiled: [join(tempDir, 'src/x.ts')], excluded: [] },
479
+ },
480
+ ];
481
+ const result = await runPrepare();
482
+ expect(result.success).toBe(true);
483
+ expect(result.data.typecheckResults).toHaveLength(1);
484
+ expect(result.data.typecheckResults[0].status).toBe('success');
485
+ expect(result.data.typecheckResults[0].diagnostics).toHaveLength(1);
486
+ expect(result.data.methodology).toContain('Triage the typecheck diagnostics.');
487
+ });
488
+ // Track-A "no diff fields" guard removed: task 14 lights up the diff data
489
+ // fields. Track-B presence tests are added in task 17.
490
+ it('runs typecheck and hygiene concurrently (barrier-based, no wall-clock)', async () => {
491
+ await materializeFile('src/x.ts');
492
+ await seedLog(['src/x.ts']);
493
+ // Barrier approach: each utility resolves its own "started" deferred, then
494
+ // awaits the OTHER's "started" deferred before completing. If the handler
495
+ // ran them sequentially, the second would never start, so the first would
496
+ // hang waiting on a deferred that nobody resolves. Concurrency is proven
497
+ // by construction — no timer thresholds.
498
+ let resolveTcStarted;
499
+ let resolveHyStarted;
500
+ const tcStarted = new Promise(r => { resolveTcStarted = r; });
501
+ const hyStarted = new Promise(r => { resolveHyStarted = r; });
502
+ overrides.typecheck = async () => {
503
+ resolveTcStarted();
504
+ await hyStarted;
505
+ return [
506
+ {
507
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
508
+ status: 'success',
509
+ diagnostics: [],
510
+ coverage: { compiled: [join(tempDir, 'src/x.ts')], excluded: [] },
511
+ },
512
+ ];
513
+ };
514
+ overrides.hygiene = async () => {
515
+ resolveHyStarted();
516
+ await tcStarted;
517
+ return [];
518
+ };
519
+ // If the handler awaited typecheck before starting hygiene, this would
520
+ // hang forever; vitest's per-test timeout would surface the regression.
521
+ const result = await runPrepare();
522
+ expect(result.success).toBe(true);
523
+ });
524
+ it('typecheck rejection → reason: rejection + R4.6b emits + handlePrepare succeeds', async () => {
525
+ await materializeFile('src/x.ts');
526
+ await seedLog(['src/x.ts']);
527
+ overrides.typecheck = async () => { throw new Error('boom'); };
528
+ const result = await runPrepare();
529
+ expect(result.success).toBe(true);
530
+ expect(result.data.typecheckResults[0].status).toBe('unavailable');
531
+ expect(result.data.typecheckResults[0].reason).toBe('rejection');
532
+ expect(result.data.typecheckResults[0].rejectionMessage).toBe('boom');
533
+ expect(result.data.methodology).toContain('Typecheck did not run for this review.');
534
+ const warnText = warnSpy.mock.calls.map((c) => String(c[0])).join('\n');
535
+ expect(warnText).toMatch(/typecheck rejected unexpectedly: boom/);
536
+ });
537
+ it('hygiene rejection → data.hygieneRejection.message set + handlePrepare succeeds', async () => {
538
+ await materializeFile('src/x.ts');
539
+ await seedLog(['src/x.ts']);
540
+ overrides.typecheck = async () => [
541
+ {
542
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
543
+ status: 'success',
544
+ diagnostics: [],
545
+ coverage: { compiled: [join(tempDir, 'src/x.ts')], excluded: [] },
546
+ },
547
+ ];
548
+ overrides.hygiene = async () => { throw new Error('hygiene-fail'); };
549
+ const result = await runPrepare();
550
+ expect(result.success).toBe(true);
551
+ expect(result.data.hygieneSignals).toEqual([]);
552
+ expect(result.data.hygieneRejection).toBeDefined();
553
+ expect(result.data.hygieneRejection.message).toBe('hygiene-fail');
554
+ const warnText = warnSpy.mock.calls.map((c) => String(c[0])).join('\n');
555
+ expect(warnText).toMatch(/hygiene rejected unexpectedly: hygiene-fail/);
556
+ });
557
+ it('unwrap warn-once: distinct messages logged separately, same message deduped', async () => {
558
+ await materializeFile('src/x.ts');
559
+ await seedLog(['src/x.ts']);
560
+ let call = 0;
561
+ overrides.typecheck = async () => {
562
+ call++;
563
+ if (call === 1)
564
+ throw new Error('msg-A');
565
+ if (call === 2)
566
+ throw new Error('msg-B');
567
+ throw new Error('msg-A'); // call 3: dedupes with call 1
568
+ };
569
+ await runPrepare();
570
+ await runPrepare();
571
+ await runPrepare();
572
+ const tcWarnings = warnSpy.mock.calls
573
+ .map((c) => String(c[0]))
574
+ .filter((m) => m.includes('typecheck rejected unexpectedly'));
575
+ expect(tcWarnings).toHaveLength(2);
576
+ expect(tcWarnings.some((m) => m.includes('msg-A'))).toBe(true);
577
+ expect(tcWarnings.some((m) => m.includes('msg-B'))).toBe(true);
578
+ });
579
+ it('unwrap warn-once: hygiene branch uses a separate key namespace', async () => {
580
+ await materializeFile('src/x.ts');
581
+ await seedLog(['src/x.ts']);
582
+ overrides.typecheck = async () => [
583
+ {
584
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
585
+ status: 'success',
586
+ diagnostics: [],
587
+ coverage: { compiled: [join(tempDir, 'src/x.ts')], excluded: [] },
588
+ },
589
+ ];
590
+ let call = 0;
591
+ overrides.hygiene = async () => {
592
+ call++;
593
+ if (call === 1)
594
+ throw new Error('shared-msg');
595
+ if (call === 2)
596
+ throw new Error('hy-other');
597
+ throw new Error('shared-msg'); // dedupes with call 1
598
+ };
599
+ await runPrepare();
600
+ await runPrepare();
601
+ await runPrepare();
602
+ const hyWarnings = warnSpy.mock.calls
603
+ .map((c) => String(c[0]))
604
+ .filter((m) => m.includes('hygiene rejected unexpectedly'));
605
+ expect(hyWarnings).toHaveLength(2);
606
+ expect(hyWarnings.some((m) => m.includes('shared-msg'))).toBe(true);
607
+ expect(hyWarnings.some((m) => m.includes('hy-other'))).toBe(true);
608
+ });
609
+ it('integration validateAllFiles smoke test: outside-projectPath dropped, valid kept, warn fires', async () => {
610
+ const otherDir = await fs.mkdtemp(join(tmpdir(), 'review-task-outside-'));
611
+ try {
612
+ await materializeFile('src/valid.ts');
613
+ const outside = join(otherDir, 'outside.ts');
614
+ writeFileSync(outside, '');
615
+ // Seed log with one valid relative path and one absolute outside-projectPath.
616
+ await seedLog(['src/valid.ts', outside]);
617
+ overrides.typecheck = async () => [
618
+ {
619
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
620
+ status: 'success',
621
+ diagnostics: [],
622
+ coverage: { compiled: [join(tempDir, 'src/valid.ts')], excluded: [] },
623
+ },
624
+ ];
625
+ const result = await runPrepare();
626
+ expect(result.success).toBe(true);
627
+ expect(result.data.filesToReview).toEqual([path.resolve(tempDir, 'src/valid.ts')]);
628
+ const warnings = warnSpy.mock.calls.map((c) => String(c[0])).join('\n');
629
+ expect(warnings).toMatch(/path outside projectPath/);
630
+ }
631
+ finally {
632
+ await fs.rm(otherDir, { recursive: true, force: true });
633
+ }
634
+ });
635
+ // Track-A interim composite-pin block deleted per R4.9: Track-B's PR replaces
636
+ // those interim pins. Track-B fixtures live at src/tools/__tests__/__fixtures__/methodology/
637
+ // (no `track-a-` prefix); task 17 wires them into composite-pin assertions
638
+ // alongside the diff-state mock and the two-way drift / sentinel tests.
639
+ describe('Track-B diff rejection vs empty distinguisher (R4.2a vs R4.2b)', () => {
640
+ it('diff utility rejection → R4.2b emits AND data.diffRejection.message is set', async () => {
641
+ await materializeFile('src/x.ts');
642
+ await seedLog(['src/x.ts']);
643
+ overrides.typecheck = async () => [
644
+ {
645
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
646
+ status: 'success',
647
+ diagnostics: [],
648
+ coverage: { compiled: [join(tempDir, 'src/x.ts')], excluded: [] },
649
+ },
650
+ ];
651
+ overrides.diff = async () => { throw new Error('git-spawn-fail'); };
652
+ const result = await runPrepare();
653
+ expect(result.success).toBe(true);
654
+ expect(result.data.diff).toBe('');
655
+ expect(result.data.diffRejection).toBeDefined();
656
+ expect(result.data.diffRejection.message).toBe('git-spawn-fail');
657
+ expect(result.data.methodology).toContain('Diff utility rejected unexpectedly');
658
+ expect(result.data.methodology).not.toContain('No diff available');
659
+ });
660
+ it('diff utility returns benign empty → R4.2a emits AND data.diffRejection is undefined', async () => {
661
+ await materializeFile('src/x.ts');
662
+ await seedLog(['src/x.ts']);
663
+ overrides.typecheck = async () => [
664
+ {
665
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
666
+ status: 'success',
667
+ diagnostics: [],
668
+ coverage: { compiled: [join(tempDir, 'src/x.ts')], excluded: [] },
669
+ },
670
+ ];
671
+ overrides.diff = async () => ({
672
+ diff: '',
673
+ stats: undefined,
674
+ skippedPaths: [],
675
+ truncated: false,
676
+ });
677
+ const result = await runPrepare();
678
+ expect(result.success).toBe(true);
679
+ expect(result.data.diff).toBe('');
680
+ expect(result.data.diffRejection).toBeUndefined();
681
+ expect(result.data.methodology).toContain('No diff available');
682
+ expect(result.data.methodology).not.toContain('Diff utility rejected unexpectedly');
683
+ });
684
+ });
685
+ describe('end-to-end secret-leak across all three consumers (NFR Security)', () => {
686
+ it('denylisted paths absent from skippedPaths consumers (diff/hygiene/typecheck)', async () => {
687
+ // Materialize a real .ts file (kept) + .ENV (secret-bearing, denied) + package-lock.json (denied)
688
+ await materializeFile('src/keep.ts', 'export const x = 1;\n');
689
+ const envAbs = join(tempDir, '.ENV');
690
+ writeFileSync(envAbs, 'SECRET=do-not-expose\n');
691
+ const lockAbs = join(tempDir, 'package-lock.json');
692
+ writeFileSync(lockAbs, '{"name":"do-not-expose"}\n');
693
+ await seedLog(['src/keep.ts', '.ENV', 'package-lock.json']);
694
+ // Real diff utility (no override) — temp dir isn't a git repo, so the
695
+ // real implementation will return an empty diff with skippedPaths.
696
+ overrides.diff = null;
697
+ // Real hygiene runs but its denylist filter MUST drop .ENV and package-lock.json.
698
+ overrides.hygiene = null;
699
+ // Typecheck mock that simulates a real run including the denied files in
700
+ // its raw output — the mock asserts what handlePrepare DOES with the
701
+ // results. The handler does NOT post-filter typecheck results (filtering
702
+ // lives inside runProjectTypecheck per task 5.3); to verify the third-
703
+ // consumer denylist promise end-to-end we let the mock represent the
704
+ // post-denylist shape: compiled keeps src/keep.ts only, excluded empty,
705
+ // diagnostics empty.
706
+ overrides.typecheck = async (_projectPath, allFiles) => {
707
+ // Sanity: handler should pass through allFiles as-is to the utility;
708
+ // the utility owns the denylist filter. Mock output emulates that.
709
+ return [
710
+ {
711
+ tsconfigPath: join(tempDir, 'tsconfig.json'),
712
+ status: 'success',
713
+ diagnostics: [],
714
+ coverage: {
715
+ compiled: allFiles.filter(p => p.endsWith('keep.ts')),
716
+ excluded: [],
717
+ },
718
+ },
719
+ ];
720
+ };
721
+ const result = await runPrepare();
722
+ expect(result.success).toBe(true);
723
+ // (a) skippedPaths surfaced from diff utility
724
+ const skippedNames = result.data.skippedPaths.map(p => path.basename(p));
725
+ expect(skippedNames).toContain('.ENV');
726
+ expect(skippedNames).toContain('package-lock.json');
727
+ // (b) data.diff does not mention .ENV or package-lock.json content
728
+ expect(result.data.diff).not.toMatch(/\.ENV/);
729
+ expect(result.data.diff).not.toMatch(/package-lock\.json/);
730
+ expect(result.data.diff).not.toMatch(/SECRET=do-not-expose/);
731
+ expect(result.data.diff).not.toMatch(/"name":"do-not-expose"/);
732
+ // (c) hygieneSignals does not include .ENV or package-lock.json
733
+ const hygieneFiles = result.data.hygieneSignals.map(s => path.basename(s.file));
734
+ expect(hygieneFiles).not.toContain('.ENV');
735
+ expect(hygieneFiles).not.toContain('package-lock.json');
736
+ // (d) typecheck coverage.compiled, coverage.excluded, diagnostics[].file
737
+ // do NOT contain .ENV or package-lock.json
738
+ const tc = result.data.typecheckResults[0];
739
+ const coverageNames = [
740
+ ...(tc.coverage?.compiled ?? []).map((p) => path.basename(p)),
741
+ ...(tc.coverage?.excluded ?? []).map((p) => path.basename(p)),
742
+ ];
743
+ const diagFiles = (tc.diagnostics ?? []).map((d) => path.basename(d.file));
744
+ expect(coverageNames).not.toContain('.ENV');
745
+ expect(coverageNames).not.toContain('package-lock.json');
746
+ expect(diagFiles).not.toContain('.ENV');
747
+ expect(diagFiles).not.toContain('package-lock.json');
748
+ });
749
+ });
750
+ });
751
+ // ---------------------------------------------------------------------------
752
+ // Track-B composite-pin tests against all 17 fixtures.
753
+ // Each fixture file in __fixtures__/methodology/*.txt has a docstring section
754
+ // followed by `---` then the verbatim buildReviewMethodology output for a
755
+ // canonical (diffState, typecheckState) pair (R4.10).
756
+ // ---------------------------------------------------------------------------
757
+ const __filename_test = fileURLToPath(import.meta.url);
758
+ const __dirname_test = dirname(__filename_test);
759
+ const FIXTURE_DIR = path.resolve(__dirname_test, '__fixtures__/methodology');
760
+ const REQUIREMENTS_MD = path.resolve(__dirname_test, '../../../.spec-workflow/specs/tighter-reviews/requirements.md');
761
+ // Canonical input shared by every fixture (R4.10):
762
+ // requirements=['1.1', '2.4']; restrictions=...; success=...; leverage=...;
763
+ // hasTechSteering=true; hasPriorReviews=false; hasHygieneSignals=true.
764
+ const CANONICAL_TASK_CONTEXT = {
765
+ description: 'Implement feature',
766
+ requirements: ['1.1', '2.4'],
767
+ leverage: 'src/core/path-denylist.ts',
768
+ prompt: null,
769
+ promptStructured: [
770
+ { key: 'Role', value: 'Developer' },
771
+ { key: 'Task', value: 'Build it' },
772
+ { key: 'Restrictions', value: 'Do NOT bypass denylist; do NOT change truncation messages' },
773
+ { key: 'Success', value: 'All listed cases pass; messages emit verbatim' },
774
+ ],
775
+ };
776
+ const HAS_TECH_STEERING = true;
777
+ const HAS_PRIOR_REVIEWS = false;
778
+ const HAS_HYGIENE_SIGNALS = true;
779
+ // Filename → (diffState, typecheckState). Mirrors each fixture's canonical input docstring.
780
+ const FIXTURE_INPUTS = {
781
+ // 7 typecheck-axis (diff held at 'present')
782
+ 'success-clean-full.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'success-clean-full' } },
783
+ 'success-with-diagnostics.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'success-with-diagnostics', truncated: false } },
784
+ 'success-partial-coverage.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'success-partial-coverage' } },
785
+ 'success-with-diagnostics-and-partial-coverage.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'success-with-diagnostics-and-partial-coverage', truncated: false } },
786
+ 'unavailable-feature-disabled.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'unavailable-feature-disabled' } },
787
+ 'unavailable-other.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'unavailable-other', reason: 'project-references' } },
788
+ 'timeout.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'timeout' } },
789
+ // 4 diff-axis (typecheck held at success-clean-full)
790
+ 'diff-empty.txt': { diffState: { kind: 'empty' }, typecheckState: { kind: 'success-clean-full' } },
791
+ 'diff-present-untruncated.txt': { diffState: { kind: 'present' }, typecheckState: { kind: 'success-clean-full' } },
792
+ 'diff-present-truncated.txt': { diffState: { kind: 'present-truncated' }, typecheckState: { kind: 'success-clean-full' } },
793
+ 'diff-rejected.txt': { diffState: { kind: 'rejected', message: '<diff utility error>' }, typecheckState: { kind: 'success-clean-full' } },
794
+ // 6 cross-axis
795
+ 'cross-success-partial-coverage-diff-empty.txt': { diffState: { kind: 'empty' }, typecheckState: { kind: 'success-partial-coverage' } },
796
+ 'cross-timeout-diff-present-truncated.txt': { diffState: { kind: 'present-truncated' }, typecheckState: { kind: 'timeout' } },
797
+ 'cross-unavailable-other-diff-present-truncated.txt': { diffState: { kind: 'present-truncated' }, typecheckState: { kind: 'unavailable-other', reason: 'project-references' } },
798
+ 'cross-success-with-diagnostics-diff-empty.txt': { diffState: { kind: 'empty' }, typecheckState: { kind: 'success-with-diagnostics', truncated: false } },
799
+ 'cross-diff-rejected-typecheck-rejection.txt': { diffState: { kind: 'rejected', message: '<diff utility error>' }, typecheckState: { kind: 'unavailable-other', reason: 'rejection' } },
800
+ 'cross-success-partial-coverage-diff-rejected.txt': { diffState: { kind: 'rejected', message: '<diff utility error>' }, typecheckState: { kind: 'success-partial-coverage' } },
801
+ };
802
+ // Normalization pipeline (R4.10):
803
+ // 1. \r\n → \n
804
+ // 2. line-by-line .trimEnd()
805
+ // 3. NFC
806
+ // 4. em-dash → hyphen
807
+ // 5. smart quotes → straight
808
+ // 6. whitespace-run collapse to single space
809
+ //
810
+ // For the drift extractors, paragraph boundaries on `\n\n` MUST be detected
811
+ // BEFORE whitespace-collapse — that boundary detection is internalised in
812
+ // `extractDirectiveSentences` (line-walk over the body) and
813
+ // `extractR4BlocksFromRequirements` (line-walk over requirements.md), so by
814
+ // the time text reaches `normalize` here the boundaries are already encoded
815
+ // in separate strings. Composite-pin and drift therefore share this single
816
+ // helper today; if a caller ever needs boundary preservation INSIDE `normalize`,
817
+ // split the function then — don't introduce divergent copies preemptively.
818
+ function normalize(text) {
819
+ let n = text.replace(/\r\n/g, '\n');
820
+ n = n.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
821
+ n = n.normalize('NFC');
822
+ n = n.replace(/—/g, '-');
823
+ n = n.replace(/[“”]/g, '"').replace(/[‘’]/g, "'");
824
+ n = n.replace(/\s+/g, ' ');
825
+ return n.trim();
826
+ }
827
+ function loadFixtureBody(filename) {
828
+ const raw = readFileSync(join(FIXTURE_DIR, filename), 'utf-8');
829
+ const sepIdx = raw.indexOf('\n---\n');
830
+ if (sepIdx < 0)
831
+ throw new Error(`Fixture ${filename} missing '---' separator`);
832
+ return raw.slice(sepIdx + 5);
833
+ }
834
+ describe('Track-B composite pins (R4.10)', () => {
835
+ for (const [filename, { diffState, typecheckState }] of Object.entries(FIXTURE_INPUTS)) {
836
+ it(`fixture ${filename} matches buildReviewMethodology output`, () => {
837
+ const fixtureBody = loadFixtureBody(filename);
838
+ const actual = buildReviewMethodology(CANONICAL_TASK_CONTEXT, HAS_TECH_STEERING, HAS_PRIOR_REVIEWS, HAS_HYGIENE_SIGNALS, diffState, typecheckState);
839
+ expect(normalize(actual)).toBe(normalize(fixtureBody));
840
+ });
841
+ }
842
+ it('fixture count is exactly 17', () => {
843
+ const files = readdirSync(FIXTURE_DIR).filter(f => f.endsWith('.txt'));
844
+ expect(files).toHaveLength(17);
845
+ });
846
+ });
847
+ // ---------------------------------------------------------------------------
848
+ // Track-A interim sentinel — Track B replaces those interim fixtures, so the
849
+ // marker `# SPEC-WORKFLOW:TRACK-A:INTERIM-PIN` MUST be absent from every
850
+ // fixture in the directory.
851
+ // ---------------------------------------------------------------------------
852
+ describe('Track-A interim sentinel', () => {
853
+ it('no fixture file contains # SPEC-WORKFLOW:TRACK-A:INTERIM-PIN', () => {
854
+ const files = readdirSync(FIXTURE_DIR).filter(f => f.endsWith('.txt'));
855
+ for (const f of files) {
856
+ const content = readFileSync(join(FIXTURE_DIR, f), 'utf-8');
857
+ expect(content).not.toContain('# SPEC-WORKFLOW:TRACK-A:INTERIM-PIN');
858
+ }
859
+ });
860
+ });
861
+ // ---------------------------------------------------------------------------
862
+ // Two-way drift test (R4.10).
863
+ //
864
+ // Heading regex (PINNED): /^####\s+(R4\.\d+[a-z]?)\s+[—-]\s+/m
865
+ // R4.x directives MUST use exactly four `#` and an em-dash or ASCII hyphen
866
+ // separator. Other heading shapes (`### `, `#####`, `**R4.1**`, `: ` instead
867
+ // of dash) fail to match — surfaces as keyset failure, not silent.
868
+ //
869
+ // Block extraction:
870
+ // - Matches both `> ...` block-quote and ```...``` fenced-block delimiters
871
+ // following a matched heading.
872
+ // - Each extracted block is keyed by R4.x name.
873
+ // - Duplicate-name handling: if two blocks extract under the same name (e.g.
874
+ // a future R4.x maintenance splits the directive across two `> ...`
875
+ // paragraphs under one heading), the test FAILS (does not silently
876
+ // concatenate or last-write-wins).
877
+ // - Empty-block handling: a heading whose block is missing or empty FAILS
878
+ // (closes the silent-loss path: a future R4.x written as a numbered list
879
+ // would otherwise vacuously match Direction A).
880
+ // ---------------------------------------------------------------------------
881
+ const EXPECTED_R4_BLOCK_NAMES = [
882
+ 'R4.1',
883
+ 'R4.2a',
884
+ 'R4.2b',
885
+ 'R4.4',
886
+ 'R4.5',
887
+ 'R4.6a',
888
+ 'R4.6b',
889
+ 'R4.7',
890
+ ];
891
+ function extractR4BlocksFromRequirements(text) {
892
+ const blocks = new Map();
893
+ const lines = text.split('\n');
894
+ // Pinned heading regex: see header comment above.
895
+ const HEADING_RE = /^####\s+(R4\.\d+[a-z]?)\s+[—-]\s+/;
896
+ for (let i = 0; i < lines.length; i++) {
897
+ const m = lines[i].match(HEADING_RE);
898
+ if (!m)
899
+ continue;
900
+ const name = m[1];
901
+ let j = i + 1;
902
+ let blockContent = null;
903
+ while (j < lines.length && !/^#+\s/.test(lines[j])) {
904
+ if (lines[j].startsWith('> ')) {
905
+ const quotedLines = [];
906
+ while (j < lines.length && lines[j].startsWith('> ')) {
907
+ quotedLines.push(lines[j].slice(2));
908
+ j++;
909
+ }
910
+ const candidate = quotedLines.join(' ');
911
+ if (blockContent !== null) {
912
+ throw new Error(`Duplicate block found for ${name} — R4.x directives must be a single contiguous block-quote or fenced block`);
913
+ }
914
+ blockContent = candidate;
915
+ continue;
916
+ }
917
+ if (lines[j].startsWith('```')) {
918
+ const fenced = [];
919
+ j++;
920
+ while (j < lines.length && !lines[j].startsWith('```')) {
921
+ fenced.push(lines[j]);
922
+ j++;
923
+ }
924
+ if (j < lines.length)
925
+ j++;
926
+ const candidate = fenced.join(' ');
927
+ if (blockContent !== null) {
928
+ throw new Error(`Duplicate block found for ${name} — R4.x directives must be a single contiguous block-quote or fenced block`);
929
+ }
930
+ blockContent = candidate;
931
+ continue;
932
+ }
933
+ j++;
934
+ }
935
+ if (blockContent === null || blockContent.trim() === '') {
936
+ throw new Error(`R4.x heading found but no block content extracted for ${name} — directives must be \`>\` block-quote or \`\`\` fenced`);
937
+ }
938
+ if (blocks.has(name)) {
939
+ throw new Error(`Duplicate block found for ${name} — R4.x directives must be a single contiguous block-quote or fenced block`);
940
+ }
941
+ blocks.set(name, blockContent);
942
+ i = j - 1;
943
+ }
944
+ return blocks;
945
+ }
946
+ // Direction B sentence extractor — pinned. If R4 prose evolves to use new
947
+ // sentence shapes (e.g. semicolon-separated clauses), update
948
+ // `extractDirectiveSentences` AND its companion test below in the same PR.
949
+ //
950
+ // 5-step algorithm:
951
+ // 1. Strip the top-of-file docstring (everything before the `---` separator).
952
+ // 2. Split remaining text on `\n\n+` to get paragraph candidates.
953
+ // 3. Keep paragraphs in the directive zone:
954
+ // - `**Read first:**` opens the diff zone (closes at `## Primary` heading)
955
+ // - paragraph beginning with `10.` opens the typecheck zone (closes at
956
+ // `## Recording Results`)
957
+ // - item-9 (hygiene) is NOT pinned to R4 (R4.8 — kept verbatim from
958
+ // fast-reviews); paragraphs starting with `9.` close zones and are not
959
+ // kept.
960
+ // - `**Note:**` truncation paragraph is a render-side note (not
961
+ // authoritative R4 prose) and is skipped while in zone.
962
+ // 4. Within each kept paragraph, sentence-split on /(?<=[.!?])(?:\s+|$)/.
963
+ // 5. Exclude boilerplate sentences: `^\d+\.\s*$`, `^\*\*[^*]+\*\*$`, empty.
964
+ export function extractDirectiveSentences(fixture) {
965
+ const sepIdx = fixture.indexOf('\n---\n');
966
+ const body = sepIdx >= 0 ? fixture.slice(sepIdx + 5) : fixture;
967
+ // Paragraph boundaries are blank lines OR a line starting a new numbered
968
+ // list item (`\d+. `) OR a heading (`## `). Items 7-10 in the fixture sit on
969
+ // consecutive lines without blank separators, so a plain `\n\n` split would
970
+ // merge them into one block — boundary detection on `\n\n` BEFORE
971
+ // whitespace-collapse must also recognise list-item starts as paragraph
972
+ // boundaries to isolate item 10.
973
+ const lines = body.split('\n');
974
+ const paragraphs = [];
975
+ let current = [];
976
+ const flush = () => {
977
+ const t = current.join('\n').trim();
978
+ if (t !== '')
979
+ paragraphs.push(t);
980
+ current = [];
981
+ };
982
+ for (const line of lines) {
983
+ if (line.trim() === '') {
984
+ flush();
985
+ continue;
986
+ }
987
+ if (current.length > 0 &&
988
+ (/^\d+\.\s/.test(line) || /^##\s/.test(line) || /^#\s/.test(line))) {
989
+ flush();
990
+ }
991
+ current.push(line);
992
+ }
993
+ flush();
994
+ const kept = [];
995
+ let inDiffZone = false;
996
+ let inTypecheckZone = false;
997
+ for (const p of paragraphs) {
998
+ if (/^##\s/.test(p)) {
999
+ inDiffZone = false;
1000
+ inTypecheckZone = false;
1001
+ continue;
1002
+ }
1003
+ if (p.startsWith('**Read first:**')) {
1004
+ inDiffZone = true;
1005
+ kept.push(p);
1006
+ continue;
1007
+ }
1008
+ if (/^10\.\s/.test(p)) {
1009
+ inTypecheckZone = true;
1010
+ kept.push(p);
1011
+ continue;
1012
+ }
1013
+ if (/^\d+\.\s/.test(p)) {
1014
+ // Other numbered list items end the directive zones.
1015
+ inDiffZone = false;
1016
+ inTypecheckZone = false;
1017
+ continue;
1018
+ }
1019
+ if (inDiffZone || inTypecheckZone) {
1020
+ if (p.startsWith('**Note:**'))
1021
+ continue;
1022
+ kept.push(p);
1023
+ }
1024
+ }
1025
+ const terminator = /(?<=[.!?])(?:\s+|$)/;
1026
+ const sentences = [];
1027
+ for (const p of kept) {
1028
+ for (const s of p.split(terminator))
1029
+ sentences.push(s);
1030
+ }
1031
+ return sentences.filter(s => {
1032
+ const t = s.trim();
1033
+ if (t === '')
1034
+ return false;
1035
+ if (/^\d+\.\s*$/.test(t))
1036
+ return false;
1037
+ if (/^\*\*[^*]+\*\*$/.test(t))
1038
+ return false;
1039
+ return true;
1040
+ });
1041
+ }
1042
+ describe('extractDirectiveSentences self-test', () => {
1043
+ it('extracts diff and typecheck directive sentences from a known fixture, excluding item-9', () => {
1044
+ const fixture = readFileSync(join(FIXTURE_DIR, 'success-with-diagnostics.txt'), 'utf-8');
1045
+ const sentences = extractDirectiveSentences(fixture);
1046
+ // The R4.1 prose opens with "Read the diff first" — must be present.
1047
+ expect(sentences.some(s => s.includes('Read the diff first'))).toBe(true);
1048
+ // The R4.4 prose opens with "Triage the typecheck diagnostics" — must be present.
1049
+ expect(sentences.some(s => s.includes('Triage the typecheck diagnostics'))).toBe(true);
1050
+ // Item-9 hygiene is NOT pinned to R4 — its prose must NOT be extracted.
1051
+ expect(sentences.every(s => !s.includes('Pre-computed hygiene signals are attached'))).toBe(true);
1052
+ // Items 1-8 (Spec-Compliance and Correctness/Hygiene) are out of zone.
1053
+ expect(sentences.every(s => !s.includes('Requirements compliance'))).toBe(true);
1054
+ expect(sentences.every(s => !s.includes('Restriction adherence'))).toBe(true);
1055
+ // Recording-results enumeration is out of zone.
1056
+ expect(sentences.every(s => !s.includes('verdict: "pass"'))).toBe(true);
1057
+ });
1058
+ });
1059
+ // Drift tests pin fixtures to the local spec doc at
1060
+ // `.spec-workflow/specs/tighter-reviews/requirements.md`, which is gitignored.
1061
+ // Skip these tests when the doc isn't present (e.g. CI checkouts).
1062
+ describe.skipIf(!existsSync(REQUIREMENTS_MD))('Two-way drift test (R4.10)', () => {
1063
+ it('extracted block keyset equals EXPECTED_R4_BLOCK_NAMES', () => {
1064
+ const requirementsMd = readFileSync(REQUIREMENTS_MD, 'utf-8');
1065
+ const blocks = extractR4BlocksFromRequirements(requirementsMd);
1066
+ const actualNames = Array.from(blocks.keys()).sort();
1067
+ const expectedNames = [...EXPECTED_R4_BLOCK_NAMES].sort();
1068
+ expect(actualNames).toEqual(expectedNames);
1069
+ });
1070
+ it('Direction A: each R4.x block appears as a contiguous substring in at least one fixture', () => {
1071
+ const requirementsMd = readFileSync(REQUIREMENTS_MD, 'utf-8');
1072
+ const blocks = extractR4BlocksFromRequirements(requirementsMd);
1073
+ const fixtureBodies = Object.keys(FIXTURE_INPUTS).map(f => normalize(loadFixtureBody(f)));
1074
+ for (const [name, block] of blocks) {
1075
+ const normalized = normalize(block);
1076
+ const matched = fixtureBodies.some(body => body.includes(normalized));
1077
+ expect(matched, `R4 block ${name} not found verbatim in any fixture`).toBe(true);
1078
+ }
1079
+ });
1080
+ it('Direction B: every fixture directive sentence appears in some R4.x block', () => {
1081
+ const requirementsMd = readFileSync(REQUIREMENTS_MD, 'utf-8');
1082
+ const blocks = extractR4BlocksFromRequirements(requirementsMd);
1083
+ const normalizedBlocks = Array.from(blocks.values()).map(normalize);
1084
+ for (const filename of Object.keys(FIXTURE_INPUTS)) {
1085
+ const raw = readFileSync(join(FIXTURE_DIR, filename), 'utf-8');
1086
+ const sentences = extractDirectiveSentences(raw);
1087
+ for (const s of sentences) {
1088
+ const ns = normalize(s);
1089
+ if (ns === '')
1090
+ continue;
1091
+ const matched = normalizedBlocks.some(b => b.includes(ns));
1092
+ expect(matched, `Fixture ${filename} sentence not found in any R4 block: ${ns.slice(0, 120)}`).toBe(true);
1093
+ }
1094
+ }
1095
+ });
1096
+ });
1097
+ //# sourceMappingURL=review-task.test.js.map