@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.
- package/CHANGELOG.md +1013 -0
- package/LICENSE +674 -0
- package/README.md +458 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +264 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/index-args.test.d.ts +2 -0
- package/dist/__tests__/index-args.test.d.ts.map +1 -0
- package/dist/__tests__/index-args.test.js +43 -0
- package/dist/__tests__/index-args.test.js.map +1 -0
- package/dist/__tests__/index-entrypoint.test.d.ts +2 -0
- package/dist/__tests__/index-entrypoint.test.d.ts.map +1 -0
- package/dist/__tests__/index-entrypoint.test.js +23 -0
- package/dist/__tests__/index-entrypoint.test.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +188 -0
- package/dist/config.js.map +1 -0
- package/dist/core/__tests__/adversarial-settings.test.d.ts +2 -0
- package/dist/core/__tests__/adversarial-settings.test.d.ts.map +1 -0
- package/dist/core/__tests__/adversarial-settings.test.js +361 -0
- package/dist/core/__tests__/adversarial-settings.test.js.map +1 -0
- package/dist/core/__tests__/deferral-storage.test.d.ts +2 -0
- package/dist/core/__tests__/deferral-storage.test.d.ts.map +1 -0
- package/dist/core/__tests__/deferral-storage.test.js +181 -0
- package/dist/core/__tests__/deferral-storage.test.js.map +1 -0
- package/dist/core/__tests__/git-utils.test.d.ts +2 -0
- package/dist/core/__tests__/git-utils.test.d.ts.map +1 -0
- package/dist/core/__tests__/git-utils.test.js +179 -0
- package/dist/core/__tests__/git-utils.test.js.map +1 -0
- package/dist/core/__tests__/hygiene-signals.test.d.ts +2 -0
- package/dist/core/__tests__/hygiene-signals.test.d.ts.map +1 -0
- package/dist/core/__tests__/hygiene-signals.test.js +200 -0
- package/dist/core/__tests__/hygiene-signals.test.js.map +1 -0
- package/dist/core/__tests__/mdx-validator.test.d.ts +2 -0
- package/dist/core/__tests__/mdx-validator.test.d.ts.map +1 -0
- package/dist/core/__tests__/mdx-validator.test.js +42 -0
- package/dist/core/__tests__/mdx-validator.test.js.map +1 -0
- package/dist/core/__tests__/path-denylist.test.d.ts +2 -0
- package/dist/core/__tests__/path-denylist.test.d.ts.map +1 -0
- package/dist/core/__tests__/path-denylist.test.js +242 -0
- package/dist/core/__tests__/path-denylist.test.js.map +1 -0
- package/dist/core/__tests__/path-utils.test.d.ts +2 -0
- package/dist/core/__tests__/path-utils.test.d.ts.map +1 -0
- package/dist/core/__tests__/path-utils.test.js +342 -0
- package/dist/core/__tests__/path-utils.test.js.map +1 -0
- package/dist/core/__tests__/project-registry.test.d.ts +2 -0
- package/dist/core/__tests__/project-registry.test.d.ts.map +1 -0
- package/dist/core/__tests__/project-registry.test.js +62 -0
- package/dist/core/__tests__/project-registry.test.js.map +1 -0
- package/dist/core/__tests__/security-utils.test.d.ts +2 -0
- package/dist/core/__tests__/security-utils.test.d.ts.map +1 -0
- package/dist/core/__tests__/security-utils.test.js +643 -0
- package/dist/core/__tests__/security-utils.test.js.map +1 -0
- package/dist/core/__tests__/task-diff.test.d.ts +2 -0
- package/dist/core/__tests__/task-diff.test.d.ts.map +1 -0
- package/dist/core/__tests__/task-diff.test.js +287 -0
- package/dist/core/__tests__/task-diff.test.js.map +1 -0
- package/dist/core/__tests__/task-review-manager.test.d.ts +2 -0
- package/dist/core/__tests__/task-review-manager.test.d.ts.map +1 -0
- package/dist/core/__tests__/task-review-manager.test.js +235 -0
- package/dist/core/__tests__/task-review-manager.test.js.map +1 -0
- package/dist/core/__tests__/task-validator.test.d.ts +2 -0
- package/dist/core/__tests__/task-validator.test.d.ts.map +1 -0
- package/dist/core/__tests__/task-validator.test.js +237 -0
- package/dist/core/__tests__/task-validator.test.js.map +1 -0
- package/dist/core/__tests__/typecheck.test.d.ts +2 -0
- package/dist/core/__tests__/typecheck.test.d.ts.map +1 -0
- package/dist/core/__tests__/typecheck.test.js +558 -0
- package/dist/core/__tests__/typecheck.test.js.map +1 -0
- package/dist/core/adversarial-settings.d.ts +23 -0
- package/dist/core/adversarial-settings.d.ts.map +1 -0
- package/dist/core/adversarial-settings.js +148 -0
- package/dist/core/adversarial-settings.js.map +1 -0
- package/dist/core/archive-service.d.ts +10 -0
- package/dist/core/archive-service.d.ts.map +1 -0
- package/dist/core/archive-service.js +99 -0
- package/dist/core/archive-service.js.map +1 -0
- package/dist/core/dashboard-session.d.ts +49 -0
- package/dist/core/dashboard-session.d.ts.map +1 -0
- package/dist/core/dashboard-session.js +132 -0
- package/dist/core/dashboard-session.js.map +1 -0
- package/dist/core/deferral-storage.d.ts +32 -0
- package/dist/core/deferral-storage.d.ts.map +1 -0
- package/dist/core/deferral-storage.js +232 -0
- package/dist/core/deferral-storage.js.map +1 -0
- package/dist/core/git-utils.d.ts +25 -0
- package/dist/core/git-utils.d.ts.map +1 -0
- package/dist/core/git-utils.js +87 -0
- package/dist/core/git-utils.js.map +1 -0
- package/dist/core/global-dir.d.ts +44 -0
- package/dist/core/global-dir.d.ts.map +1 -0
- package/dist/core/global-dir.js +74 -0
- package/dist/core/global-dir.js.map +1 -0
- package/dist/core/hygiene-signals.d.ts +8 -0
- package/dist/core/hygiene-signals.d.ts.map +1 -0
- package/dist/core/hygiene-signals.js +41 -0
- package/dist/core/hygiene-signals.js.map +1 -0
- package/dist/core/implementation-log-migrator.d.ts +41 -0
- package/dist/core/implementation-log-migrator.d.ts.map +1 -0
- package/dist/core/implementation-log-migrator.js +258 -0
- package/dist/core/implementation-log-migrator.js.map +1 -0
- package/dist/core/mdx-validator.d.ts +14 -0
- package/dist/core/mdx-validator.d.ts.map +1 -0
- package/dist/core/mdx-validator.js +34 -0
- package/dist/core/mdx-validator.js.map +1 -0
- package/dist/core/parser.d.ts +11 -0
- package/dist/core/parser.d.ts.map +1 -0
- package/dist/core/parser.js +126 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/path-denylist.d.ts +8 -0
- package/dist/core/path-denylist.d.ts.map +1 -0
- package/dist/core/path-denylist.js +107 -0
- package/dist/core/path-denylist.js.map +1 -0
- package/dist/core/path-utils.d.ts +69 -0
- package/dist/core/path-utils.d.ts.map +1 -0
- package/dist/core/path-utils.js +306 -0
- package/dist/core/path-utils.js.map +1 -0
- package/dist/core/project-registry.d.ts +94 -0
- package/dist/core/project-registry.d.ts.map +1 -0
- package/dist/core/project-registry.js +297 -0
- package/dist/core/project-registry.js.map +1 -0
- package/dist/core/security-utils.d.ts +97 -0
- package/dist/core/security-utils.d.ts.map +1 -0
- package/dist/core/security-utils.js +264 -0
- package/dist/core/security-utils.js.map +1 -0
- package/dist/core/task-diff.d.ts +15 -0
- package/dist/core/task-diff.d.ts.map +1 -0
- package/dist/core/task-diff.js +136 -0
- package/dist/core/task-diff.js.map +1 -0
- package/dist/core/task-parser.d.ts +63 -0
- package/dist/core/task-parser.d.ts.map +1 -0
- package/dist/core/task-parser.js +332 -0
- package/dist/core/task-parser.js.map +1 -0
- package/dist/core/task-review-manager.d.ts +56 -0
- package/dist/core/task-review-manager.d.ts.map +1 -0
- package/dist/core/task-review-manager.js +281 -0
- package/dist/core/task-review-manager.js.map +1 -0
- package/dist/core/task-validator.d.ts +35 -0
- package/dist/core/task-validator.d.ts.map +1 -0
- package/dist/core/task-validator.js +236 -0
- package/dist/core/task-validator.js.map +1 -0
- package/dist/core/typecheck.d.ts +33 -0
- package/dist/core/typecheck.d.ts.map +1 -0
- package/dist/core/typecheck.js +375 -0
- package/dist/core/typecheck.js.map +1 -0
- package/dist/core/workspace-initializer.d.ts +16 -0
- package/dist/core/workspace-initializer.d.ts.map +1 -0
- package/dist/core/workspace-initializer.js +167 -0
- package/dist/core/workspace-initializer.js.map +1 -0
- package/dist/dashboard/__tests__/adversarial-display-state.test.d.ts +2 -0
- package/dist/dashboard/__tests__/adversarial-display-state.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/adversarial-display-state.test.js +59 -0
- package/dist/dashboard/__tests__/adversarial-display-state.test.js.map +1 -0
- package/dist/dashboard/__tests__/adversarial-endpoints.test.d.ts +2 -0
- package/dist/dashboard/__tests__/adversarial-endpoints.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/adversarial-endpoints.test.js +296 -0
- package/dist/dashboard/__tests__/adversarial-endpoints.test.js.map +1 -0
- package/dist/dashboard/__tests__/adversarial-runner.test.d.ts +2 -0
- package/dist/dashboard/__tests__/adversarial-runner.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/adversarial-runner.test.js +315 -0
- package/dist/dashboard/__tests__/adversarial-runner.test.js.map +1 -0
- package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts +2 -0
- package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js +78 -0
- package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js.map +1 -0
- package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts +2 -0
- package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/multi-server-approvals-content.test.js +115 -0
- package/dist/dashboard/__tests__/multi-server-approvals-content.test.js.map +1 -0
- package/dist/dashboard/__tests__/multi-server.test.d.ts +2 -0
- package/dist/dashboard/__tests__/multi-server.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/multi-server.test.js +388 -0
- package/dist/dashboard/__tests__/multi-server.test.js.map +1 -0
- package/dist/dashboard/__tests__/task-review-runner.test.d.ts +2 -0
- package/dist/dashboard/__tests__/task-review-runner.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/task-review-runner.test.js +255 -0
- package/dist/dashboard/__tests__/task-review-runner.test.js.map +1 -0
- package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts +2 -0
- package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts.map +1 -0
- package/dist/dashboard/__tests__/watcher-error-handling.test.js +118 -0
- package/dist/dashboard/__tests__/watcher-error-handling.test.js.map +1 -0
- package/dist/dashboard/adversarial-display-state.d.ts +38 -0
- package/dist/dashboard/adversarial-display-state.d.ts.map +1 -0
- package/dist/dashboard/adversarial-display-state.js +45 -0
- package/dist/dashboard/adversarial-display-state.js.map +1 -0
- package/dist/dashboard/adversarial-runner.d.ts +44 -0
- package/dist/dashboard/adversarial-runner.d.ts.map +1 -0
- package/dist/dashboard/adversarial-runner.js +168 -0
- package/dist/dashboard/adversarial-runner.js.map +1 -0
- package/dist/dashboard/approval-storage.d.ts +139 -0
- package/dist/dashboard/approval-storage.d.ts.map +1 -0
- package/dist/dashboard/approval-storage.js +608 -0
- package/dist/dashboard/approval-storage.js.map +1 -0
- package/dist/dashboard/execution-history-manager.d.ts +52 -0
- package/dist/dashboard/execution-history-manager.d.ts.map +1 -0
- package/dist/dashboard/execution-history-manager.js +161 -0
- package/dist/dashboard/execution-history-manager.js.map +1 -0
- package/dist/dashboard/implementation-log-manager.d.ts +102 -0
- package/dist/dashboard/implementation-log-manager.d.ts.map +1 -0
- package/dist/dashboard/implementation-log-manager.js +594 -0
- package/dist/dashboard/implementation-log-manager.js.map +1 -0
- package/dist/dashboard/job-scheduler.d.ts +91 -0
- package/dist/dashboard/job-scheduler.d.ts.map +1 -0
- package/dist/dashboard/job-scheduler.js +321 -0
- package/dist/dashboard/job-scheduler.js.map +1 -0
- package/dist/dashboard/multi-server.d.ts +45 -0
- package/dist/dashboard/multi-server.d.ts.map +1 -0
- package/dist/dashboard/multi-server.js +1927 -0
- package/dist/dashboard/multi-server.js.map +1 -0
- package/dist/dashboard/parser.d.ts +18 -0
- package/dist/dashboard/parser.d.ts.map +1 -0
- package/dist/dashboard/parser.js +243 -0
- package/dist/dashboard/parser.js.map +1 -0
- package/dist/dashboard/project-manager.d.ts +82 -0
- package/dist/dashboard/project-manager.d.ts.map +1 -0
- package/dist/dashboard/project-manager.js +257 -0
- package/dist/dashboard/project-manager.js.map +1 -0
- package/dist/dashboard/public/assets/Inter-Bold-CD3Pr7BX.woff2 +0 -0
- package/dist/dashboard/public/assets/Inter-Medium-B_8v_WHh.woff2 +0 -0
- package/dist/dashboard/public/assets/Inter-Regular-DRVdRqcI.woff2 +0 -0
- package/dist/dashboard/public/assets/Inter-SemiBold-CtskMddL.woff2 +0 -0
- package/dist/dashboard/public/assets/JetBrainsMono-Bold-D4WEaHbo.woff2 +0 -0
- package/dist/dashboard/public/assets/JetBrainsMono-Medium-3S3k2nMz.woff2 +0 -0
- package/dist/dashboard/public/assets/JetBrainsMono-Regular-BQaDgvhP.woff2 +0 -0
- package/dist/dashboard/public/assets/Tableau10-B-NsZVaP.js +1 -0
- package/dist/dashboard/public/assets/apl-B4CMkyY2.js +1 -0
- package/dist/dashboard/public/assets/arc-A04OOED4.js +1 -0
- package/dist/dashboard/public/assets/array-BKyUJesY.js +1 -0
- package/dist/dashboard/public/assets/asciiarmor-Df11BRmG.js +1 -0
- package/dist/dashboard/public/assets/asn1-EdZsLKOL.js +1 -0
- package/dist/dashboard/public/assets/asterisk-B-8jnY81.js +1 -0
- package/dist/dashboard/public/assets/blockDiagram-c4efeb88--RobHnfG.js +118 -0
- package/dist/dashboard/public/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/dist/dashboard/public/assets/c4Diagram-c83219d4-tY8lPXxy.js +10 -0
- package/dist/dashboard/public/assets/channel-Cg2ZlORc.js +1 -0
- package/dist/dashboard/public/assets/classDiagram-beda092f-DBiupBFm.js +2 -0
- package/dist/dashboard/public/assets/classDiagram-v2-2358418a-FM2jRAtm.js +2 -0
- package/dist/dashboard/public/assets/clike-B9uivgTg.js +1 -0
- package/dist/dashboard/public/assets/clojure-BMjYHr_A.js +1 -0
- package/dist/dashboard/public/assets/clone-CCO9qihz.js +1 -0
- package/dist/dashboard/public/assets/cmake-BQqOBYOt.js +1 -0
- package/dist/dashboard/public/assets/cobol-CWcv1MsR.js +1 -0
- package/dist/dashboard/public/assets/coffeescript-S37ZYGWr.js +1 -0
- package/dist/dashboard/public/assets/commonlisp-DBKNyK5s.js +1 -0
- package/dist/dashboard/public/assets/createText-1719965b-C_nyx2v1.js +7 -0
- package/dist/dashboard/public/assets/crystal-SjHAIU92.js +1 -0
- package/dist/dashboard/public/assets/css-BnMrqG3P.js +1 -0
- package/dist/dashboard/public/assets/cypher-C_CwsFkJ.js +1 -0
- package/dist/dashboard/public/assets/d-pRatUO7H.js +1 -0
- package/dist/dashboard/public/assets/diff-DbItnlRl.js +1 -0
- package/dist/dashboard/public/assets/dockerfile-BKs6k2Af.js +1 -0
- package/dist/dashboard/public/assets/dtd-DF_7sFjM.js +1 -0
- package/dist/dashboard/public/assets/dylan-DwRh75JA.js +1 -0
- package/dist/dashboard/public/assets/ebnf-CDyGwa7X.js +1 -0
- package/dist/dashboard/public/assets/ecl-Cabwm37j.js +1 -0
- package/dist/dashboard/public/assets/edges-96097737-BsffumDq.js +4 -0
- package/dist/dashboard/public/assets/eiffel-CnydiIhH.js +1 -0
- package/dist/dashboard/public/assets/elm-vLlmbW-K.js +1 -0
- package/dist/dashboard/public/assets/erDiagram-0228fc6a-BK-uOJy1.js +51 -0
- package/dist/dashboard/public/assets/erlang-BNw1qcRV.js +1 -0
- package/dist/dashboard/public/assets/factor-kuTfRLto.js +1 -0
- package/dist/dashboard/public/assets/fcl-Kvtd6kyn.js +1 -0
- package/dist/dashboard/public/assets/flowDb-c6c81e3f-BC8_H7l7.js +10 -0
- package/dist/dashboard/public/assets/flowDiagram-50d868cf-0XbjRISj.js +4 -0
- package/dist/dashboard/public/assets/flowDiagram-v2-4f6560a1-CXE3CwgW.js +1 -0
- package/dist/dashboard/public/assets/flowchart-elk-definition-6af322e1-CCvs3UPr.js +139 -0
- package/dist/dashboard/public/assets/forth-Ffai-XNe.js +1 -0
- package/dist/dashboard/public/assets/fortran-DYz_wnZ1.js +1 -0
- package/dist/dashboard/public/assets/ganttDiagram-a2739b55-Yp2gfoGL.js +257 -0
- package/dist/dashboard/public/assets/gas-Bneqetm1.js +1 -0
- package/dist/dashboard/public/assets/gherkin-heZmZLOM.js +1 -0
- package/dist/dashboard/public/assets/gitGraphDiagram-82fe8481-njx_NoNr.js +70 -0
- package/dist/dashboard/public/assets/graph-COR8Ljm7.js +1 -0
- package/dist/dashboard/public/assets/groovy-D9Dt4D0W.js +1 -0
- package/dist/dashboard/public/assets/haskell-Cw1EW3IL.js +1 -0
- package/dist/dashboard/public/assets/haxe-H-WmDvRZ.js +1 -0
- package/dist/dashboard/public/assets/http-DBlCnlav.js +1 -0
- package/dist/dashboard/public/assets/idl-BEugSyMb.js +1 -0
- package/dist/dashboard/public/assets/index-0abMV41b.js +1 -0
- package/dist/dashboard/public/assets/index-5325376f-cWsAocic.js +1 -0
- package/dist/dashboard/public/assets/index-BH121EUE.js +1 -0
- package/dist/dashboard/public/assets/index-BMLdkmFP.js +1 -0
- package/dist/dashboard/public/assets/index-BYgbrt4G.js +1 -0
- package/dist/dashboard/public/assets/index-BwFDzEd4.js +3 -0
- package/dist/dashboard/public/assets/index-CS7gsYFH.js +1 -0
- package/dist/dashboard/public/assets/index-Cc6cUpKS.js +1 -0
- package/dist/dashboard/public/assets/index-CgeymaoH.js +1 -0
- package/dist/dashboard/public/assets/index-Cl5FjFWx.js +1 -0
- package/dist/dashboard/public/assets/index-CnN2VPRa.js +1 -0
- package/dist/dashboard/public/assets/index-DDWkdUEb.js +1 -0
- package/dist/dashboard/public/assets/index-DLUjIeO9.js +1 -0
- package/dist/dashboard/public/assets/index-Dm3-5ZYh.js +319 -0
- package/dist/dashboard/public/assets/index-Ds4s2dDD.js +2 -0
- package/dist/dashboard/public/assets/index-DvrmwdIJ.js +1 -0
- package/dist/dashboard/public/assets/index-MTVs_iIW.js +7 -0
- package/dist/dashboard/public/assets/index-UGm8eYTB.js +1 -0
- package/dist/dashboard/public/assets/index-Zi39sM21.css +1 -0
- package/dist/dashboard/public/assets/index-iL66igAo.js +1 -0
- package/dist/dashboard/public/assets/infoDiagram-8eee0895-TQXQOcbQ.js +7 -0
- package/dist/dashboard/public/assets/init-Gi6I4Gst.js +1 -0
- package/dist/dashboard/public/assets/javascript-iXu5QeM3.js +1 -0
- package/dist/dashboard/public/assets/journeyDiagram-c64418c1-C7AIhJZ8.js +139 -0
- package/dist/dashboard/public/assets/julia-DuME0IfC.js +1 -0
- package/dist/dashboard/public/assets/katex-XbL3y5x-.js +261 -0
- package/dist/dashboard/public/assets/layout-BzInGx9g.js +1 -0
- package/dist/dashboard/public/assets/line-BvqpNAKs.js +1 -0
- package/dist/dashboard/public/assets/linear-CMco2PTv.js +1 -0
- package/dist/dashboard/public/assets/livescript-BwQOo05w.js +1 -0
- package/dist/dashboard/public/assets/lua-BgMRiT3U.js +1 -0
- package/dist/dashboard/public/assets/mathematica-DTrFuWx2.js +1 -0
- package/dist/dashboard/public/assets/mbox-CNhZ1qSd.js +1 -0
- package/dist/dashboard/public/assets/mindmap-definition-8da855dc-CeJT8t3A.js +415 -0
- package/dist/dashboard/public/assets/mirc-CjQqDB4T.js +1 -0
- package/dist/dashboard/public/assets/mllike-CXdrOF99.js +1 -0
- package/dist/dashboard/public/assets/modelica-Dc1JOy9r.js +1 -0
- package/dist/dashboard/public/assets/mscgen-BA5vi2Kp.js +1 -0
- package/dist/dashboard/public/assets/mumps-BT43cFF4.js +1 -0
- package/dist/dashboard/public/assets/nginx-DdIZxoE0.js +1 -0
- package/dist/dashboard/public/assets/nsis-LdVXkNf5.js +1 -0
- package/dist/dashboard/public/assets/ntriples-BfvgReVJ.js +1 -0
- package/dist/dashboard/public/assets/octave-Ck1zUtKM.js +1 -0
- package/dist/dashboard/public/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/dashboard/public/assets/oz-BzwKVEFT.js +1 -0
- package/dist/dashboard/public/assets/pascal--L3eBynH.js +1 -0
- package/dist/dashboard/public/assets/path-CbwjOpE9.js +1 -0
- package/dist/dashboard/public/assets/perl-CdXCOZ3F.js +1 -0
- package/dist/dashboard/public/assets/pieDiagram-a8764435-DYvb2wsa.js +35 -0
- package/dist/dashboard/public/assets/pig-CevX1Tat.js +1 -0
- package/dist/dashboard/public/assets/powershell-CFHJl5sT.js +1 -0
- package/dist/dashboard/public/assets/properties-C78fOPTZ.js +1 -0
- package/dist/dashboard/public/assets/protobuf-ChK-085T.js +1 -0
- package/dist/dashboard/public/assets/pug-DeIclll2.js +1 -0
- package/dist/dashboard/public/assets/puppet-DMA9R1ak.js +1 -0
- package/dist/dashboard/public/assets/python-BuPzkPfP.js +1 -0
- package/dist/dashboard/public/assets/q-pXgVlZs6.js +1 -0
- package/dist/dashboard/public/assets/quadrantDiagram-1e28029f-DpKQgDsX.js +7 -0
- package/dist/dashboard/public/assets/r-B6wPVr8A.js +1 -0
- package/dist/dashboard/public/assets/requirementDiagram-08caed73-C_q-bfsF.js +52 -0
- package/dist/dashboard/public/assets/rpm-CTu-6PCP.js +1 -0
- package/dist/dashboard/public/assets/ruby-B2Rjki9n.js +1 -0
- package/dist/dashboard/public/assets/sankeyDiagram-a04cb91d-0kwrmzfI.js +8 -0
- package/dist/dashboard/public/assets/sas-B4kiWyti.js +1 -0
- package/dist/dashboard/public/assets/scheme-C41bIUwD.js +1 -0
- package/dist/dashboard/public/assets/sequenceDiagram-c5b8d532-i5S3JNF0.js +122 -0
- package/dist/dashboard/public/assets/shell-CjFT_Tl9.js +1 -0
- package/dist/dashboard/public/assets/sieve-C3Gn_uJK.js +1 -0
- package/dist/dashboard/public/assets/simple-mode-GW_nhZxv.js +1 -0
- package/dist/dashboard/public/assets/smalltalk-CnHTOXQT.js +1 -0
- package/dist/dashboard/public/assets/solr-DehyRSwq.js +1 -0
- package/dist/dashboard/public/assets/sparql-DkYu6x3z.js +1 -0
- package/dist/dashboard/public/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/dist/dashboard/public/assets/sql-D0XecflT.js +1 -0
- package/dist/dashboard/public/assets/stateDiagram-1ecb1508-COE2ffbQ.js +1 -0
- package/dist/dashboard/public/assets/stateDiagram-v2-c2b004d7-DMAS8qJy.js +1 -0
- package/dist/dashboard/public/assets/stex-C3f8Ysf7.js +1 -0
- package/dist/dashboard/public/assets/styles-b4e223ce-M0Oa_txo.js +160 -0
- package/dist/dashboard/public/assets/styles-ca3715f6-DddhRVVB.js +207 -0
- package/dist/dashboard/public/assets/styles-d45a18b0-ByKhWZca.js +116 -0
- package/dist/dashboard/public/assets/stylus-B533Al4x.js +1 -0
- package/dist/dashboard/public/assets/svgDrawCommon-b86b1483-DIF6Vn69.js +1 -0
- package/dist/dashboard/public/assets/swift-BzpIVaGY.js +1 -0
- package/dist/dashboard/public/assets/tcl-DVfN8rqt.js +1 -0
- package/dist/dashboard/public/assets/textile-CnDTJFAw.js +1 -0
- package/dist/dashboard/public/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/dist/dashboard/public/assets/tiki-DGYXhP31.js +1 -0
- package/dist/dashboard/public/assets/timeline-definition-faaaa080-DC1Bdpu_.js +61 -0
- package/dist/dashboard/public/assets/toml-Bm5Em-hy.js +1 -0
- package/dist/dashboard/public/assets/troff-wAsdV37c.js +1 -0
- package/dist/dashboard/public/assets/ttcn-CfJYG6tj.js +1 -0
- package/dist/dashboard/public/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/dist/dashboard/public/assets/turtle-B1tBg_DP.js +1 -0
- package/dist/dashboard/public/assets/vb-CmGdzxic.js +1 -0
- package/dist/dashboard/public/assets/vbscript-BuJXcnF6.js +1 -0
- package/dist/dashboard/public/assets/velocity-D8B20fx6.js +1 -0
- package/dist/dashboard/public/assets/verilog-C6RDOZhf.js +1 -0
- package/dist/dashboard/public/assets/vhdl-lSbBsy5d.js +1 -0
- package/dist/dashboard/public/assets/webidl-ZXfAyPTL.js +1 -0
- package/dist/dashboard/public/assets/xquery-DzFWVndE.js +1 -0
- package/dist/dashboard/public/assets/xychartDiagram-f5964ef8-BIMyvG9y.js +7 -0
- package/dist/dashboard/public/assets/yacas-BJ4BC0dw.js +1 -0
- package/dist/dashboard/public/assets/z80-Hz9HOZM7.js +1 -0
- package/dist/dashboard/public/claude-icon-dark.svg +1 -0
- package/dist/dashboard/public/claude-icon.svg +1 -0
- package/dist/dashboard/public/index.html +16 -0
- package/dist/dashboard/settings-manager.d.ts +47 -0
- package/dist/dashboard/settings-manager.d.ts.map +1 -0
- package/dist/dashboard/settings-manager.js +180 -0
- package/dist/dashboard/settings-manager.js.map +1 -0
- package/dist/dashboard/task-review-runner.d.ts +42 -0
- package/dist/dashboard/task-review-runner.d.ts.map +1 -0
- package/dist/dashboard/task-review-runner.js +375 -0
- package/dist/dashboard/task-review-runner.js.map +1 -0
- package/dist/dashboard/utils.d.ts +31 -0
- package/dist/dashboard/utils.d.ts.map +1 -0
- package/dist/dashboard/utils.js +102 -0
- package/dist/dashboard/utils.js.map +1 -0
- package/dist/dashboard/watcher.d.ts +32 -0
- package/dist/dashboard/watcher.d.ts.map +1 -0
- package/dist/dashboard/watcher.js +173 -0
- package/dist/dashboard/watcher.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +380 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown/templates/design-template.md +96 -0
- package/dist/markdown/templates/product-template.md +51 -0
- package/dist/markdown/templates/requirements-template.md +50 -0
- package/dist/markdown/templates/structure-template.md +145 -0
- package/dist/markdown/templates/tasks-template.md +139 -0
- package/dist/markdown/templates/tech-template.md +99 -0
- package/dist/prompts/create-decomposition.d.ts +3 -0
- package/dist/prompts/create-decomposition.d.ts.map +1 -0
- package/dist/prompts/create-decomposition.js +122 -0
- package/dist/prompts/create-decomposition.js.map +1 -0
- package/dist/prompts/create-spec.d.ts +3 -0
- package/dist/prompts/create-spec.d.ts.map +1 -0
- package/dist/prompts/create-spec.js +93 -0
- package/dist/prompts/create-spec.js.map +1 -0
- package/dist/prompts/create-steering-doc.d.ts +3 -0
- package/dist/prompts/create-steering-doc.d.ts.map +1 -0
- package/dist/prompts/create-steering-doc.js +73 -0
- package/dist/prompts/create-steering-doc.js.map +1 -0
- package/dist/prompts/implement-task.d.ts +3 -0
- package/dist/prompts/implement-task.d.ts.map +1 -0
- package/dist/prompts/implement-task.js +173 -0
- package/dist/prompts/implement-task.js.map +1 -0
- package/dist/prompts/index.d.ts +15 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +51 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/inject-spec-workflow-guide.d.ts +3 -0
- package/dist/prompts/inject-spec-workflow-guide.d.ts.map +1 -0
- package/dist/prompts/inject-spec-workflow-guide.js +47 -0
- package/dist/prompts/inject-spec-workflow-guide.js.map +1 -0
- package/dist/prompts/inject-steering-guide.d.ts +3 -0
- package/dist/prompts/inject-steering-guide.d.ts.map +1 -0
- package/dist/prompts/inject-steering-guide.js +51 -0
- package/dist/prompts/inject-steering-guide.js.map +1 -0
- package/dist/prompts/refresh-tasks.d.ts +3 -0
- package/dist/prompts/refresh-tasks.d.ts.map +1 -0
- package/dist/prompts/refresh-tasks.js +224 -0
- package/dist/prompts/refresh-tasks.js.map +1 -0
- package/dist/prompts/spec-status.d.ts +3 -0
- package/dist/prompts/spec-status.d.ts.map +1 -0
- package/dist/prompts/spec-status.js +75 -0
- package/dist/prompts/spec-status.js.map +1 -0
- package/dist/prompts/types.d.ts +13 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +2 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +175 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/__tests__/adversarial-response.test.d.ts +2 -0
- package/dist/tools/__tests__/adversarial-response.test.d.ts.map +1 -0
- package/dist/tools/__tests__/adversarial-response.test.js +144 -0
- package/dist/tools/__tests__/adversarial-response.test.js.map +1 -0
- package/dist/tools/__tests__/adversarial-review.test.d.ts +2 -0
- package/dist/tools/__tests__/adversarial-review.test.d.ts.map +1 -0
- package/dist/tools/__tests__/adversarial-review.test.js +318 -0
- package/dist/tools/__tests__/adversarial-review.test.js.map +1 -0
- package/dist/tools/__tests__/decomposition-guide.test.d.ts +2 -0
- package/dist/tools/__tests__/decomposition-guide.test.d.ts.map +1 -0
- package/dist/tools/__tests__/decomposition-guide.test.js +25 -0
- package/dist/tools/__tests__/decomposition-guide.test.js.map +1 -0
- package/dist/tools/__tests__/deferrals.test.d.ts +2 -0
- package/dist/tools/__tests__/deferrals.test.d.ts.map +1 -0
- package/dist/tools/__tests__/deferrals.test.js +151 -0
- package/dist/tools/__tests__/deferrals.test.js.map +1 -0
- package/dist/tools/__tests__/get-task-review.test.d.ts +2 -0
- package/dist/tools/__tests__/get-task-review.test.d.ts.map +1 -0
- package/dist/tools/__tests__/get-task-review.test.js +81 -0
- package/dist/tools/__tests__/get-task-review.test.js.map +1 -0
- package/dist/tools/__tests__/projectPath.test.d.ts +2 -0
- package/dist/tools/__tests__/projectPath.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projectPath.test.js +187 -0
- package/dist/tools/__tests__/projectPath.test.js.map +1 -0
- package/dist/tools/__tests__/review-task.test.d.ts +2 -0
- package/dist/tools/__tests__/review-task.test.d.ts.map +1 -0
- package/dist/tools/__tests__/review-task.test.js +1097 -0
- package/dist/tools/__tests__/review-task.test.js.map +1 -0
- package/dist/tools/adversarial-response.d.ts +6 -0
- package/dist/tools/adversarial-response.d.ts.map +1 -0
- package/dist/tools/adversarial-response.js +206 -0
- package/dist/tools/adversarial-response.js.map +1 -0
- package/dist/tools/adversarial-review.d.ts +21 -0
- package/dist/tools/adversarial-review.d.ts.map +1 -0
- package/dist/tools/adversarial-review.js +491 -0
- package/dist/tools/adversarial-review.js.map +1 -0
- package/dist/tools/approvals.d.ts +14 -0
- package/dist/tools/approvals.d.ts.map +1 -0
- package/dist/tools/approvals.js +505 -0
- package/dist/tools/approvals.js.map +1 -0
- package/dist/tools/decomposition-guide.d.ts +6 -0
- package/dist/tools/decomposition-guide.d.ts.map +1 -0
- package/dist/tools/decomposition-guide.js +163 -0
- package/dist/tools/decomposition-guide.js.map +1 -0
- package/dist/tools/deferrals.d.ts +5 -0
- package/dist/tools/deferrals.d.ts.map +1 -0
- package/dist/tools/deferrals.js +229 -0
- package/dist/tools/deferrals.js.map +1 -0
- package/dist/tools/get-task-review.d.ts +5 -0
- package/dist/tools/get-task-review.d.ts.map +1 -0
- package/dist/tools/get-task-review.js +136 -0
- package/dist/tools/get-task-review.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +82 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/log-implementation.d.ts +5 -0
- package/dist/tools/log-implementation.d.ts.map +1 -0
- package/dist/tools/log-implementation.js +398 -0
- package/dist/tools/log-implementation.js.map +1 -0
- package/dist/tools/review-task.d.ts +58 -0
- package/dist/tools/review-task.d.ts.map +1 -0
- package/dist/tools/review-task.js +617 -0
- package/dist/tools/review-task.js.map +1 -0
- package/dist/tools/spec-status.d.ts +5 -0
- package/dist/tools/spec-status.d.ts.map +1 -0
- package/dist/tools/spec-status.js +235 -0
- package/dist/tools/spec-status.js.map +1 -0
- package/dist/tools/spec-workflow-guide.d.ts +5 -0
- package/dist/tools/spec-workflow-guide.d.ts.map +1 -0
- package/dist/tools/spec-workflow-guide.js +396 -0
- package/dist/tools/spec-workflow-guide.js.map +1 -0
- package/dist/tools/steering-guide.d.ts +5 -0
- package/dist/tools/steering-guide.d.ts.map +1 -0
- package/dist/tools/steering-guide.js +192 -0
- package/dist/tools/steering-guide.js.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- 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
|