@planu/cli 4.14.1 → 5.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 (424) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/LICENSE +55 -12
  3. package/README.md +10 -2
  4. package/dist/cli/commands/install.d.ts +1 -12
  5. package/dist/cli/commands/install.js +9 -65
  6. package/dist/cli/commands/job.js +4 -3
  7. package/dist/cli/commands/spec.js +2 -2
  8. package/dist/cli/commands/validate.js +2 -2
  9. package/dist/cli/router.js +1 -3
  10. package/dist/config/environment-schema.json +1 -3
  11. package/dist/config/official-sdd-tools.d.ts +4 -0
  12. package/dist/config/official-sdd-tools.js +35 -0
  13. package/dist/config/product-public.d.ts +139 -3
  14. package/dist/config/product-public.js +8 -1
  15. package/dist/config/product-public.json +7 -36
  16. package/dist/config/registries/hosts/codex.json +20 -363
  17. package/dist/config/release-policy.json +84 -2
  18. package/dist/config/runtime-policy.json +12 -1
  19. package/dist/config/skill-templates/planu-resume-work.md +2 -2
  20. package/dist/core/index.d.ts +1 -1
  21. package/dist/core/spec-validator.d.ts +4 -4
  22. package/dist/core/spec-validator.js +1 -1
  23. package/dist/engine/abortable-process-runner.d.ts +20 -2
  24. package/dist/engine/abortable-process-runner.js +243 -74
  25. package/dist/engine/ambiguity-scorer.js +4 -26
  26. package/dist/engine/auth/scope-mapper.js +1 -7
  27. package/dist/engine/autopilot/state-updater.js +4 -4
  28. package/dist/engine/cascade-hooks/hooks/review-enricher.hook.js +9 -6
  29. package/dist/engine/cascade-hooks/state-drift-detector.d.ts +3 -3
  30. package/dist/engine/cascade-hooks/state-drift-detector.js +1 -1
  31. package/dist/engine/ci-generator/planu-config.js +1 -7
  32. package/dist/engine/clarification-gate/gate.d.ts +3 -3
  33. package/dist/engine/code-scanner/layer-scanner.js +6 -22
  34. package/dist/engine/core-bridge.d.ts +3 -3
  35. package/dist/engine/cost-guardrails.d.ts +1 -1
  36. package/dist/engine/cost-guardrails.js +1 -1
  37. package/dist/engine/criterion-identity.d.ts +1 -0
  38. package/dist/engine/criterion-identity.js +7 -0
  39. package/dist/engine/dashboard/data-loader.js +2 -7
  40. package/dist/engine/dashboard/templates-layout.js +0 -3
  41. package/dist/engine/dashboard/templates-project.js +1 -13
  42. package/dist/engine/docs-site-generator/data-collector.d.ts +1 -1
  43. package/dist/engine/docs-site-generator/data-collector.js +7 -28
  44. package/dist/engine/docs-site-generator/tools-renderer.js +3 -10
  45. package/dist/engine/evidence-gates/artifact-reader.d.ts +10 -1
  46. package/dist/engine/evidence-gates/artifact-reader.js +115 -2
  47. package/dist/engine/evidence-gates/lifecycle-gate.js +4 -3
  48. package/dist/engine/evidence-gates/reconciliation-freshness.d.ts +11 -0
  49. package/dist/engine/evidence-gates/reconciliation-freshness.js +72 -0
  50. package/dist/engine/evidence-index/done-drift.d.ts +1 -1
  51. package/dist/engine/evidence-index/done-drift.js +40 -18
  52. package/dist/engine/execution/context.js +3 -0
  53. package/dist/engine/execution/durable-job-heartbeat-runtime.d.ts +4 -0
  54. package/dist/engine/execution/durable-job-heartbeat-runtime.js +400 -0
  55. package/dist/engine/execution/job-runtime.d.ts +30 -0
  56. package/dist/engine/execution/job-runtime.js +114 -6
  57. package/dist/engine/execution/public-job-sanitizer.d.ts +4 -0
  58. package/dist/engine/execution/public-job-sanitizer.js +31 -0
  59. package/dist/engine/execution/validate-job-executor.d.ts +12 -1
  60. package/dist/engine/execution/validate-job-executor.js +549 -86
  61. package/dist/engine/execution/validate-job-payload.js +107 -50
  62. package/dist/engine/execution/validate-job-recovery.js +1 -1
  63. package/dist/engine/execution/validate-worker-runtime.d.ts +5 -0
  64. package/dist/engine/execution/validate-worker-runtime.js +90 -2
  65. package/dist/engine/execution/validate-worker-thread.js +17 -24
  66. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -1
  67. package/dist/engine/handoff-artifacts/schemas.js +33 -0
  68. package/dist/engine/handoff-packager.d.ts +19 -0
  69. package/dist/engine/handoff-packager.js +415 -13
  70. package/dist/engine/hooks/effort-configurator.js +1 -1
  71. package/dist/engine/impact-detector/test-break-predictor.js +3 -3
  72. package/dist/engine/impact-detector/tool-registration.js +1 -1
  73. package/dist/engine/implementation-contract/renderer.js +23 -8
  74. package/dist/engine/lifecycle-reconciliation-io.d.ts +11 -0
  75. package/dist/engine/lifecycle-reconciliation-io.js +180 -0
  76. package/dist/engine/lifecycle-reconciliation.d.ts +16 -0
  77. package/dist/engine/lifecycle-reconciliation.js +406 -0
  78. package/dist/engine/local-first/tool-classification.js +2 -0
  79. package/dist/engine/macos-keychain-native-port.d.ts +5 -0
  80. package/dist/engine/macos-keychain-native-port.js +19 -7
  81. package/dist/engine/migrations/remove-commercial-state.d.ts +3 -0
  82. package/dist/engine/migrations/remove-commercial-state.js +343 -0
  83. package/dist/engine/network-policy.js +1 -0
  84. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  85. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +10 -10
  86. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  87. package/dist/engine/planu-core.darwin-x64.node.sbom.json +10 -10
  88. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  89. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +10 -10
  90. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  91. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +10 -10
  92. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  93. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +10 -10
  94. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  95. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +10 -10
  96. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  97. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +10 -10
  98. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  99. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +10 -10
  100. package/dist/engine/readiness-checker.d.ts +1 -16
  101. package/dist/engine/readiness-checker.js +27 -88
  102. package/dist/engine/runtime-policy.d.ts +2 -0
  103. package/dist/engine/runtime-policy.js +42 -5
  104. package/dist/engine/runtime-security/rate-limiter.js +0 -1
  105. package/dist/engine/safety/contained-project-file.d.ts +3 -0
  106. package/dist/engine/safety/contained-project-file.js +23 -0
  107. package/dist/engine/scope-boundaries/contradiction-checker.js +63 -1
  108. package/dist/engine/scope-boundaries/scope-validator.js +45 -7
  109. package/dist/engine/session/checkpoint-writer.js +0 -9
  110. package/dist/engine/spec-format/acceptance-criteria.d.ts +6 -2
  111. package/dist/engine/spec-format/acceptance-criteria.js +77 -18
  112. package/dist/engine/spec-format/markdown-sections.d.ts +14 -0
  113. package/dist/engine/spec-format/markdown-sections.js +105 -0
  114. package/dist/engine/spec-format/read-technical-section.js +33 -5
  115. package/dist/engine/spec-format/technical-md-populator.js +3 -2
  116. package/dist/engine/spec-format/unified-spec-builder.d.ts +7 -0
  117. package/dist/engine/spec-format/unified-spec-builder.js +202 -0
  118. package/dist/engine/spec-migrator/canonical-deduplicator.d.ts +18 -0
  119. package/dist/engine/spec-migrator/canonical-deduplicator.js +479 -0
  120. package/dist/engine/spec-migrator/filesystem-import.d.ts +4 -8
  121. package/dist/engine/spec-migrator/filesystem-import.js +121 -14
  122. package/dist/engine/spec-migrator/frontmatter-parser.js +32 -15
  123. package/dist/engine/spec-migrator/index.d.ts +1 -0
  124. package/dist/engine/spec-migrator/index.js +1 -0
  125. package/dist/engine/spec-state-machine/transition-spec.d.ts +6 -1
  126. package/dist/engine/spec-state-machine/transition-spec.js +10 -1
  127. package/dist/engine/technical-enricher/index.js +16 -8
  128. package/dist/engine/technical-enricher/render-enriched.d.ts +1 -0
  129. package/dist/engine/technical-enricher/render-enriched.js +42 -14
  130. package/dist/engine/telemetry/health-checker.js +53 -17
  131. package/dist/engine/telemetry/telemetry-client.js +26 -8
  132. package/dist/engine/text-signal-boundaries.d.ts +2 -0
  133. package/dist/engine/text-signal-boundaries.js +25 -2
  134. package/dist/engine/time/relative-date.d.ts +3 -0
  135. package/dist/engine/time/relative-date.js +16 -0
  136. package/dist/engine/timing/budget.d.ts +11 -0
  137. package/dist/engine/timing/budget.js +27 -9
  138. package/dist/engine/token-optimizer/analytics.d.ts +1 -1
  139. package/dist/engine/token-optimizer/analytics.js +5 -6
  140. package/dist/engine/usage-tracker/core.d.ts +2 -2
  141. package/dist/engine/usage-tracker/core.js +1 -2
  142. package/dist/engine/usage-tracker/index.d.ts +0 -3
  143. package/dist/engine/usage-tracker/index.js +0 -3
  144. package/dist/engine/usage-tracker/stats.d.ts +2 -2
  145. package/dist/engine/usage-tracker/stats.js +1 -28
  146. package/dist/engine/validation/durable-validation.d.ts +1 -1
  147. package/dist/engine/validation/durable-validation.js +3 -82
  148. package/dist/engine/validation/validation-freshness.d.ts +11 -1
  149. package/dist/engine/validation/validation-freshness.js +231 -91
  150. package/dist/engine/validation/validation-receipt.d.ts +9 -6
  151. package/dist/engine/validation/validation-receipt.js +84 -24
  152. package/dist/engine/validation/validation-source-digest.d.ts +2 -0
  153. package/dist/engine/validation/validation-source-digest.js +86 -0
  154. package/dist/engine/validation/validation-submission.d.ts +5 -4
  155. package/dist/engine/validation/validation-submission.js +25 -11
  156. package/dist/engine/validation/validation-worktree.d.ts +21 -0
  157. package/dist/engine/validation/validation-worktree.js +260 -0
  158. package/dist/engine/validation-impact-planner.d.ts +14 -2
  159. package/dist/engine/validation-impact-planner.js +0 -3
  160. package/dist/engine/validator/dor-dod.d.ts +1 -0
  161. package/dist/engine/validator/dor-dod.js +5 -3
  162. package/dist/engine/validator/extractors.d.ts +1 -5
  163. package/dist/engine/validator/extractors.js +24 -69
  164. package/dist/engine/validator/spec-compliance-runner.d.ts +12 -24
  165. package/dist/engine/validator/spec-compliance-runner.js +429 -211
  166. package/dist/engine/validator.js +35 -22
  167. package/dist/index.js +4 -16
  168. package/dist/resources/usage-stats-resource.js +2 -18
  169. package/dist/security/adapters/linux-secret-service.js +7 -3
  170. package/dist/security/adapters/windows-credential-manager.js +6 -4
  171. package/dist/server/routes/specs.js +10 -0
  172. package/dist/storage/base-store.d.ts +1 -1
  173. package/dist/storage/base-store.js +1 -1
  174. package/dist/storage/feedback-remote.d.ts +2 -3
  175. package/dist/storage/feedback-remote.js +45 -31
  176. package/dist/storage/global-store.d.ts +1 -17
  177. package/dist/storage/global-store.js +1 -42
  178. package/dist/storage/index.d.ts +0 -2
  179. package/dist/storage/index.js +0 -2
  180. package/dist/storage/migrations/canonical-storage.js +124 -11
  181. package/dist/storage/runtime-db.d.ts +9 -0
  182. package/dist/storage/runtime-db.js +64 -8
  183. package/dist/storage/spec-store.d.ts +14 -2
  184. package/dist/storage/spec-store.js +104 -14
  185. package/dist/storage/status-store/atomic-status-write.d.ts +5 -0
  186. package/dist/storage/status-store/atomic-status-write.js +57 -0
  187. package/dist/storage/status-store/file-lock.js +30 -11
  188. package/dist/storage/status-store/self-healing.js +3 -3
  189. package/dist/storage/status-store/version-sync.d.ts +2 -1
  190. package/dist/storage/status-store/version-sync.js +35 -40
  191. package/dist/storage/usage-store.d.ts +1 -5
  192. package/dist/storage/usage-store.js +0 -35
  193. package/dist/tools/auto-update-handler.d.ts +3 -3
  194. package/dist/tools/auto-update-handler.js +3 -3
  195. package/dist/tools/challenge-spec/resilience-challenge-scenarios.d.ts +2 -1
  196. package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +10 -7
  197. package/dist/tools/challenge-spec/scenario-collector.js +1 -1
  198. package/dist/tools/challenge-spec/scenarios-data.js +1 -1
  199. package/dist/tools/challenge-spec/scenarios-utils.d.ts +1 -1
  200. package/dist/tools/challenge-spec/scenarios-utils.js +35 -11
  201. package/dist/tools/challenge-spec.js +5 -5
  202. package/dist/tools/check-readiness.js +1 -1
  203. package/dist/tools/configure-telemetry.js +30 -2
  204. package/dist/tools/create-spec/spec-builder.js +0 -11
  205. package/dist/tools/create-spec.d.ts +1 -0
  206. package/dist/tools/create-spec.js +444 -239
  207. package/dist/tools/git/branch-ops.d.ts +1 -1
  208. package/dist/tools/git/branch-ops.js +3 -2
  209. package/dist/tools/init-project/handler.js +10 -12
  210. package/dist/tools/init-project/host-assets-writer.js +26 -1
  211. package/dist/tools/init-project/lint-scaffolder.d.ts +3 -3
  212. package/dist/tools/init-project/migration-runner.d.ts +2 -4
  213. package/dist/tools/init-project/migration-runner.js +3 -2
  214. package/dist/tools/init-project/portable-index-reconciler.d.ts +4 -0
  215. package/dist/tools/init-project/portable-index-reconciler.js +14 -0
  216. package/dist/tools/init-project/result-builder.js +1 -3
  217. package/dist/tools/jobs/handlers.js +38 -6
  218. package/dist/tools/list-specs.js +5 -26
  219. package/dist/tools/merge-risk-handler.js +2 -2
  220. package/dist/tools/package-handoff.js +2 -0
  221. package/dist/tools/reconcile-spec.js +0 -27
  222. package/dist/tools/register-agent-squad-tools.js +2 -2
  223. package/dist/tools/register-platform-tools/design-stack-tools.js +11 -11
  224. package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +7 -7
  225. package/dist/tools/register-sdd-tools.d.ts +1 -1
  226. package/dist/tools/register-sdd-tools.js +2 -29
  227. package/dist/tools/register-spec-tools/analysis-tools.js +8 -8
  228. package/dist/tools/register-spec-tools/core-spec-tools.js +17 -8
  229. package/dist/tools/safe-handler.d.ts +5 -13
  230. package/dist/tools/safe-handler.js +7 -23
  231. package/dist/tools/schemas/index.d.ts +1 -1
  232. package/dist/tools/schemas/index.js +1 -1
  233. package/dist/tools/schemas/output-schemas.d.ts +0 -23
  234. package/dist/tools/schemas/output-schemas.js +0 -28
  235. package/dist/tools/schemas/spec.d.ts +4 -0
  236. package/dist/tools/schemas/spec.js +3 -0
  237. package/dist/tools/schemas/validate-output-schema.d.ts +11 -0
  238. package/dist/tools/schemas/validate-output-schema.js +7 -0
  239. package/dist/tools/spec-prompt-handler.js +3 -3
  240. package/dist/tools/sync-spec-state-handler.d.ts +1 -1
  241. package/dist/tools/sync-spec-state-handler.js +1 -1
  242. package/dist/tools/tool-entry.d.ts +4 -4
  243. package/dist/tools/tool-entry.js +3 -3
  244. package/dist/tools/tool-registry/core-tools.js +31 -15
  245. package/dist/tools/tool-registry/group-infra.js +27 -110
  246. package/dist/tools/tool-registry/group-quality-compliance.js +26 -26
  247. package/dist/tools/tool-registry/inventory.d.ts +2 -0
  248. package/dist/tools/tool-registry/inventory.js +2 -0
  249. package/dist/tools/tool-registry-helpers.d.ts +3 -3
  250. package/dist/tools/tool-registry-helpers.js +3 -3
  251. package/dist/tools/tool-usage-report/registered-tools.d.ts +1 -1
  252. package/dist/tools/tool-usage-report/registered-tools.js +3 -15
  253. package/dist/tools/update-status/approval-gates.js +3 -0
  254. package/dist/tools/update-status/done-receipt-verifier.js +40 -27
  255. package/dist/tools/update-status/evidence-gate.js +36 -1
  256. package/dist/tools/update-status/file-sync.d.ts +2 -1
  257. package/dist/tools/update-status/file-sync.js +57 -6
  258. package/dist/tools/update-status/index.d.ts +2 -0
  259. package/dist/tools/update-status/index.js +95 -58
  260. package/dist/tools/update-status/response-builder.js +3 -3
  261. package/dist/tools/update-status/side-effects.d.ts +2 -0
  262. package/dist/tools/update-status/side-effects.js +2 -2
  263. package/dist/tools/update-status/transition-guard.js +32 -15
  264. package/dist/tools/update-status-actions.d.ts +4 -2
  265. package/dist/tools/update-status-actions.js +16 -14
  266. package/dist/tools/usage-report.js +1 -16
  267. package/dist/tools/usage-stats.js +1 -18
  268. package/dist/tools/usage-tracking.d.ts +4 -0
  269. package/dist/tools/usage-tracking.js +20 -0
  270. package/dist/tools/validate-lint.d.ts +1 -0
  271. package/dist/tools/validate-lint.js +1 -1
  272. package/dist/tools/validate.d.ts +2 -1
  273. package/dist/tools/validate.js +114 -34
  274. package/dist/transports/http-transport.js +6 -8
  275. package/dist/transports/mcp-types.d.ts +0 -2
  276. package/dist/transports/session-context.js +1 -1
  277. package/dist/types/cascade-hooks.d.ts +1 -1
  278. package/dist/types/ci.d.ts +0 -4
  279. package/dist/types/clarification-token.d.ts +1 -1
  280. package/dist/types/claude-code-runtime.d.ts +0 -27
  281. package/dist/types/commercial-migration.d.ts +17 -0
  282. package/dist/types/commercial-migration.js +2 -0
  283. package/dist/types/common/primitives.d.ts +0 -2
  284. package/dist/types/core-bridge.d.ts +4 -10
  285. package/dist/types/dashboard.d.ts +3 -7
  286. package/dist/types/docs.d.ts +0 -1
  287. package/dist/types/durable-job.d.ts +4 -4
  288. package/dist/types/durable-validation.d.ts +100 -2
  289. package/dist/types/durable-validation.js +165 -1
  290. package/dist/types/errors/structured-error.d.ts +3 -3
  291. package/dist/types/estimation.d.ts +2 -0
  292. package/dist/types/evidence-gates.d.ts +9 -1
  293. package/dist/types/execution.d.ts +2 -0
  294. package/dist/types/gemini-integration.d.ts +0 -11
  295. package/dist/types/handoff-artifacts.d.ts +41 -0
  296. package/dist/types/hooks-advanced.d.ts +0 -13
  297. package/dist/types/impact-detection.d.ts +2 -2
  298. package/dist/types/index.d.ts +2 -6
  299. package/dist/types/index.js +1 -5
  300. package/dist/types/kiro-integration.d.ts +0 -6
  301. package/dist/types/network-policy.d.ts +1 -1
  302. package/dist/types/project/lint-scaffolder.d.ts +1 -1
  303. package/dist/types/runtime-policy.d.ts +11 -0
  304. package/dist/types/session-safeguard.d.ts +0 -31
  305. package/dist/types/spec/canonical-repair.d.ts +81 -0
  306. package/dist/types/spec/canonical-repair.js +2 -0
  307. package/dist/types/spec/core.d.ts +14 -0
  308. package/dist/types/spec/index.d.ts +1 -0
  309. package/dist/types/spec/index.js +1 -0
  310. package/dist/types/spec/inputs.d.ts +29 -0
  311. package/dist/types/spec-format.d.ts +24 -0
  312. package/dist/types/telemetry.d.ts +4 -1
  313. package/dist/types/transport.d.ts +0 -2
  314. package/dist/types/usage.d.ts +1 -36
  315. package/dist/types/usage.js +1 -1
  316. package/dist/types/validate-worker.d.ts +6 -0
  317. package/dist/types/validation-receipt.d.ts +1 -1
  318. package/dist/types/validation.d.ts +2 -2
  319. package/package.json +16 -12
  320. package/planu-native.json +1 -1
  321. package/planu-plugin.json +23 -40
  322. package/scripts/lib/pending-release-file.mjs +93 -0
  323. package/src/i18n/messages/en.json +0 -41
  324. package/src/i18n/messages/es.json +0 -41
  325. package/src/i18n/messages/pt.json +0 -41
  326. package/dist/cli/commands/activate.d.ts +0 -14
  327. package/dist/cli/commands/activate.js +0 -174
  328. package/dist/config/license-plans.json +0 -251
  329. package/dist/config/product-public.ts +0 -148
  330. package/dist/config/server-instructions.ts +0 -121
  331. package/dist/config/version-defaults.ts +0 -76
  332. package/dist/config/version.ts +0 -21
  333. package/dist/engine/ai-integration/gemini/settings-generator.d.ts +0 -4
  334. package/dist/engine/ai-integration/gemini/settings-generator.js +0 -195
  335. package/dist/engine/ai-integration/kiro/hooks-generator.d.ts +0 -6
  336. package/dist/engine/ai-integration/kiro/hooks-generator.js +0 -75
  337. package/dist/engine/hooks/full-spectrum-generator.d.ts +0 -32
  338. package/dist/engine/hooks/full-spectrum-generator.js +0 -117
  339. package/dist/engine/license-validator/config-loader.d.ts +0 -5
  340. package/dist/engine/license-validator/config-loader.js +0 -16
  341. package/dist/engine/license-validator/core.d.ts +0 -15
  342. package/dist/engine/license-validator/core.js +0 -151
  343. package/dist/engine/license-validator/fingerprint.d.ts +0 -3
  344. package/dist/engine/license-validator/fingerprint.js +0 -17
  345. package/dist/engine/license-validator/lemon-squeezy.d.ts +0 -5
  346. package/dist/engine/license-validator/lemon-squeezy.js +0 -55
  347. package/dist/engine/license-validator.d.ts +0 -5
  348. package/dist/engine/license-validator.js +0 -6
  349. package/dist/engine/product-intelligence/index.d.ts +0 -7
  350. package/dist/engine/product-intelligence/index.js +0 -211
  351. package/dist/engine/session/auto-save.d.ts +0 -18
  352. package/dist/engine/session/auto-save.js +0 -98
  353. package/dist/engine/session/context-snapshot.d.ts +0 -8
  354. package/dist/engine/session/context-snapshot.js +0 -49
  355. package/dist/engine/session/memory-sync.d.ts +0 -12
  356. package/dist/engine/session/memory-sync.js +0 -34
  357. package/dist/engine/session/session-diff.d.ts +0 -11
  358. package/dist/engine/session/session-diff.js +0 -42
  359. package/dist/engine/session/session-merge.d.ts +0 -12
  360. package/dist/engine/session/session-merge.js +0 -55
  361. package/dist/engine/session-safeguard/checkpoint-runner.d.ts +0 -4
  362. package/dist/engine/session-safeguard/checkpoint-runner.js +0 -70
  363. package/dist/engine/session-safeguard/learnings-buffer.d.ts +0 -15
  364. package/dist/engine/session-safeguard/learnings-buffer.js +0 -91
  365. package/dist/engine/session-safeguard/precompaction-drain.d.ts +0 -10
  366. package/dist/engine/session-safeguard/precompaction-drain.js +0 -82
  367. package/dist/engine/session-safeguard/unpushed-detector.d.ts +0 -7
  368. package/dist/engine/session-safeguard/unpushed-detector.js +0 -59
  369. package/dist/engine/spec-changelog/core.d.ts +0 -16
  370. package/dist/engine/spec-changelog/core.js +0 -176
  371. package/dist/engine/spec-changelog/diff.d.ts +0 -18
  372. package/dist/engine/spec-changelog/diff.js +0 -121
  373. package/dist/engine/spec-changelog/index.d.ts +0 -3
  374. package/dist/engine/spec-changelog/index.js +0 -4
  375. package/dist/engine/trial-engine.d.ts +0 -5
  376. package/dist/engine/trial-engine.js +0 -14
  377. package/dist/engine/usage-tracker/rate-check.d.ts +0 -7
  378. package/dist/engine/usage-tracker/rate-check.js +0 -18
  379. package/dist/engine/usage-tracker/rate-limiter.d.ts +0 -14
  380. package/dist/engine/usage-tracker/rate-limiter.js +0 -73
  381. package/dist/engine/usage-tracker/trial.d.ts +0 -27
  382. package/dist/engine/usage-tracker/trial.js +0 -49
  383. package/dist/engine/webhook/event-handlers.d.ts +0 -25
  384. package/dist/engine/webhook/event-handlers.js +0 -89
  385. package/dist/engine/webhook/index.d.ts +0 -4
  386. package/dist/engine/webhook/index.js +0 -5
  387. package/dist/engine/webhook/server.d.ts +0 -12
  388. package/dist/engine/webhook/server.js +0 -249
  389. package/dist/engine/webhook/signature.d.ts +0 -16
  390. package/dist/engine/webhook/signature.js +0 -49
  391. package/dist/storage/license-store.d.ts +0 -6
  392. package/dist/storage/license-store.js +0 -63
  393. package/dist/storage/session-store.d.ts +0 -20
  394. package/dist/storage/session-store.js +0 -136
  395. package/dist/tools/activate-license.d.ts +0 -6
  396. package/dist/tools/activate-license.js +0 -132
  397. package/dist/tools/checkpoint-handler.d.ts +0 -3
  398. package/dist/tools/checkpoint-handler.js +0 -130
  399. package/dist/tools/export-session.d.ts +0 -3
  400. package/dist/tools/export-session.js +0 -90
  401. package/dist/tools/license-gate.d.ts +0 -18
  402. package/dist/tools/license-gate.js +0 -222
  403. package/dist/tools/license-status.d.ts +0 -3
  404. package/dist/tools/license-status.js +0 -93
  405. package/dist/tools/list-sessions.d.ts +0 -3
  406. package/dist/tools/list-sessions.js +0 -46
  407. package/dist/tools/planu-session-checkpoint.d.ts +0 -6
  408. package/dist/tools/planu-session-checkpoint.js +0 -42
  409. package/dist/tools/restore-session.d.ts +0 -3
  410. package/dist/tools/restore-session.js +0 -71
  411. package/dist/tools/spec-history.d.ts +0 -3
  412. package/dist/tools/spec-history.js +0 -59
  413. package/dist/tools/webhook.d.ts +0 -6
  414. package/dist/tools/webhook.js +0 -150
  415. package/dist/types/changelog.d.ts +0 -49
  416. package/dist/types/changelog.js +0 -3
  417. package/dist/types/licensing.d.ts +0 -109
  418. package/dist/types/licensing.js +0 -4
  419. package/dist/types/product-intelligence.d.ts +0 -60
  420. package/dist/types/product-intelligence.js +0 -3
  421. package/dist/types/session.d.ts +0 -84
  422. package/dist/types/session.js +0 -3
  423. package/dist/types/webhook.d.ts +0 -62
  424. package/dist/types/webhook.js +0 -4
@@ -1,65 +1,55 @@
1
+ /* eslint-disable max-lines -- runner adapters and exact command evidence stay co-located for auditability */
2
+ import { isAbsolute, join, normalize, relative, sep } from 'node:path';
3
+ import { PROCESS_TERMINATION_GRACE_MS, runAbortableProcess } from '../abortable-process-runner.js';
4
+ import { remainingExecutionMs } from '../execution/context.js';
5
+ import { getRuntimePolicy } from '../runtime-policy.js';
6
+ import { resolveContainedProjectFile } from '../safety/contained-project-file.js';
1
7
  import { technologyValue } from '../technology-registry.js';
2
- // engine/validator/spec-compliance-runner.ts — SPEC-732: vitest-backed spec-compliance runner
3
- //
4
- // Replaces the text-matching approach in validateSpec with actual test execution.
5
- // Parses the `tests` array from each frontmatter scenario, spawns vitest, and
6
- // produces a per-scenario verdict based on real test outcomes.
7
- import { access } from 'node:fs/promises';
8
- import { join } from 'node:path';
9
8
  import { readMatchingValidationEvidence } from '../validation-evidence-ledger.js';
10
- import { runAbortableProcess } from '../abortable-process-runner.js';
11
- // ---------------------------------------------------------------------------
12
- // Frontmatter scenario parser
13
- // ---------------------------------------------------------------------------
14
- /**
15
- * Parse scenarios from a raw spec.md content string.
16
- * Handles the multi-line YAML block that the simple KV parser cannot handle.
17
- *
18
- * Expected frontmatter shape:
19
- * ```yaml
20
- * scenarios:
21
- * - title: "Scenario A"
22
- * done: false
23
- * tests:
24
- * - path: tests/foo.test.ts
25
- * line: 42
26
- * ```
27
- */
28
- // eslint-disable-next-line complexity, max-lines-per-function -- YAML frontmatter has many structural variants; each branch handles a distinct schema shape (object + string test forms — SPEC-777)
9
+ import { readEvidenceArtifacts, traceabilityRowHasCurrentCommandEvidence, } from '../evidence-gates/artifact-reader.js';
10
+ import { normalizeCriterionText } from '../criterion-identity.js';
11
+ /** A sanitized terminal adapter failure that cannot be mistaken for a failed test verdict. */
12
+ export class ComplianceCommandTerminalError extends Error {
13
+ code;
14
+ constructor(code) {
15
+ super(`[Planu] ${code}`);
16
+ this.code = code;
17
+ this.name = 'ComplianceCommandTerminalError';
18
+ }
19
+ }
20
+ // eslint-disable-next-line complexity, max-lines-per-function -- parses the supported scenario YAML subset without executing it
29
21
  export function parseFrontmatterScenarios(raw) {
30
- const FRONTMATTER_BLOCK_RE = /^---\n([\s\S]*?)\n---\n/;
31
- const match = FRONTMATTER_BLOCK_RE.exec(raw);
22
+ const match = /^---\n([\s\S]*?)\n---\n/.exec(raw);
32
23
  if (!match?.[1]) {
33
24
  return [];
34
25
  }
35
- const block = match[1];
36
- const lines = block.split('\n');
37
26
  const scenarios = [];
38
27
  let inScenarios = false;
39
28
  let currentScenario = null;
40
29
  let inTests = false;
41
- for (const line of lines) {
30
+ let inSteps = false;
31
+ let currentStep;
32
+ for (const line of match[1].split('\n')) {
42
33
  const trimmed = line.trimEnd();
43
- // Top-level `scenarios:` key
44
34
  if (/^scenarios:\s*$/.test(trimmed)) {
45
35
  inScenarios = true;
46
36
  inTests = false;
37
+ inSteps = false;
47
38
  continue;
48
39
  }
49
- // Another top-level key — exit scenarios block
50
40
  if (inScenarios && /^\S/.test(trimmed) && !trimmed.startsWith('scenarios:')) {
51
41
  if (currentScenario) {
52
42
  scenarios.push(currentScenario);
53
- currentScenario = null;
54
43
  }
44
+ currentScenario = null;
55
45
  inScenarios = false;
56
46
  inTests = false;
47
+ inSteps = false;
57
48
  continue;
58
49
  }
59
50
  if (!inScenarios) {
60
51
  continue;
61
52
  }
62
- // New scenario entry ` - title: ...`
63
53
  const titleMatch = /^\s{2}-\s+title:\s*["']?(.+?)["']?\s*$/.exec(trimmed);
64
54
  if (titleMatch?.[1]) {
65
55
  if (currentScenario) {
@@ -67,53 +57,75 @@ export function parseFrontmatterScenarios(raw) {
67
57
  }
68
58
  currentScenario = { title: titleMatch[1], done: false };
69
59
  inTests = false;
60
+ inSteps = false;
70
61
  continue;
71
62
  }
72
63
  if (!currentScenario) {
73
64
  continue;
74
65
  }
75
- // ` done: true/false`
76
66
  const doneMatch = /^\s+done:\s*(true|false)\s*$/.exec(trimmed);
77
67
  if (doneMatch?.[1]) {
78
68
  currentScenario.done = doneMatch[1] === 'true';
79
69
  continue;
80
70
  }
81
- // ` tests:` sub-key
82
71
  if (/^\s+tests:\s*$/.test(trimmed)) {
83
- currentScenario.tests = currentScenario.tests ?? [];
72
+ currentScenario.tests ??= [];
84
73
  inTests = true;
74
+ inSteps = false;
75
+ continue;
76
+ }
77
+ if (/^\s+steps:\s*$/.test(trimmed)) {
78
+ currentScenario.steps ??= [];
79
+ inSteps = true;
80
+ inTests = false;
81
+ currentStep = undefined;
85
82
  continue;
86
83
  }
87
- if (inTests) {
88
- // Test entry ` - path: ...` (object form)
89
- const pathMatch = /^\s+-\s+path:\s*["']?(.+?)["']?\s*$/.exec(trimmed);
90
- if (pathMatch?.[1]) {
91
- const testLink = { path: pathMatch[1] };
92
- currentScenario.tests ??= [];
93
- currentScenario.tests.push(testLink);
84
+ if (inSteps) {
85
+ const keywordMatch = /^\s+-\s+keyword:\s*["']?(.+?)["']?\s*$/.exec(trimmed);
86
+ if (keywordMatch?.[1]) {
87
+ currentStep = { keyword: keywordMatch[1], text: '' };
88
+ currentScenario.steps ??= [];
89
+ currentScenario.steps.push(currentStep);
94
90
  continue;
95
91
  }
96
- // Test entry ` - "tests/foo.test.ts"` or ` - tests/foo.test.ts` (string form)
97
- const stringMatch = /^\s+-\s+["']?([^'":\s][^'"]*?)["']?\s*$/.exec(trimmed);
98
- if (stringMatch?.[1] && !stringMatch[1].includes(':')) {
99
- const testLink = { path: stringMatch[1] };
100
- currentScenario.tests ??= [];
101
- currentScenario.tests.push(testLink);
92
+ const textMatch = /^\s+text:\s*["']?(.+?)["']?\s*$/.exec(trimmed);
93
+ if (textMatch?.[1] && currentStep) {
94
+ currentStep.text = textMatch[1];
102
95
  continue;
103
96
  }
104
- // ` line: N` — attach to last test link
105
- const lineMatch = /^\s+line:\s*(\d+)\s*$/.exec(trimmed);
106
- if (lineMatch?.[1] && currentScenario.tests && currentScenario.tests.length > 0) {
107
- const last = currentScenario.tests[currentScenario.tests.length - 1];
108
- if (last) {
109
- last.line = parseInt(lineMatch[1], 10);
110
- }
111
- continue;
97
+ }
98
+ if (!inTests) {
99
+ continue;
100
+ }
101
+ const pathMatch = /^\s+-\s+path:\s*["']?(.+?)["']?\s*$/.exec(trimmed);
102
+ if (pathMatch?.[1]) {
103
+ currentScenario.tests ??= [];
104
+ currentScenario.tests.push({ path: pathMatch[1] });
105
+ continue;
106
+ }
107
+ const stringMatch = /^\s+-\s+["']?([^'":\s][^'"]*?)["']?\s*$/.exec(trimmed);
108
+ if (stringMatch?.[1] && !stringMatch[1].includes(':')) {
109
+ currentScenario.tests ??= [];
110
+ currentScenario.tests.push({ path: stringMatch[1] });
111
+ continue;
112
+ }
113
+ const metadataMatch = /^\s+(line|runner|config):\s*["']?(.+?)["']?\s*$/.exec(trimmed);
114
+ const last = currentScenario.tests?.at(-1);
115
+ if (metadataMatch?.[1] && metadataMatch[2] && last) {
116
+ if (metadataMatch[1] === 'line' && /^\d+$/.test(metadataMatch[2])) {
117
+ last.line = Number.parseInt(metadataMatch[2], 10);
112
118
  }
113
- // Another indented key that is not a test entry — exit inTests
114
- if (/^\s{4}\S/.test(line)) {
115
- inTests = false;
119
+ else if (metadataMatch[1] === 'runner') {
120
+ last.runner = metadataMatch[2];
116
121
  }
122
+ else if (metadataMatch[1] === 'config') {
123
+ last.config = metadataMatch[2];
124
+ }
125
+ continue;
126
+ }
127
+ if (/^\s{4}\S/.test(line)) {
128
+ inTests = false;
117
129
  }
118
130
  }
119
131
  if (currentScenario) {
@@ -121,63 +133,291 @@ export function parseFrontmatterScenarios(raw) {
121
133
  }
122
134
  return scenarios;
123
135
  }
124
- /**
125
- * Spawn vitest with --reporter=json and collect the JSON output.
126
- * Returns null on timeout or spawn error.
127
- */
128
- function runVitest(testPaths, projectPath, timeoutMs = 120_000, signal) {
129
- const args = [
136
+ function validRelativePath(candidate) {
137
+ if (candidate.trim() !== candidate ||
138
+ candidate.length === 0 ||
139
+ isAbsolute(candidate) ||
140
+ candidate.includes('\\') ||
141
+ normalize(candidate) !== candidate) {
142
+ return false;
143
+ }
144
+ return !candidate.split('/').some((segment) => segment === '..' || segment.startsWith('-'));
145
+ }
146
+ async function resolveSafeFile(projectPath, candidate) {
147
+ if (!validRelativePath(candidate)) {
148
+ throw new Error('unsafe project-relative path');
149
+ }
150
+ const physical = await resolveContainedProjectFile(projectPath, join(projectPath, candidate));
151
+ const relativePath = relative(projectPath, physical);
152
+ if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
153
+ throw new Error('path escapes project');
154
+ }
155
+ return physical;
156
+ }
157
+ async function resolveReportedFile(projectPath, candidate) {
158
+ if (candidate.trim() !== candidate || candidate.length === 0 || candidate.includes('\\')) {
159
+ throw new Error('unsafe reported path');
160
+ }
161
+ const physical = await resolveContainedProjectFile(projectPath, isAbsolute(candidate) ? candidate : join(projectPath, candidate));
162
+ const pathFromRoot = relative(projectPath, physical);
163
+ if (pathFromRoot === '..' || pathFromRoot.startsWith(`..${sep}`) || isAbsolute(pathFromRoot)) {
164
+ throw new Error('reported path escapes project');
165
+ }
166
+ return physical;
167
+ }
168
+ async function matchPlaywrightLink(reportedPath, links, projectPath) {
169
+ if (isAbsolute(reportedPath)) {
170
+ const physical = await resolveReportedFile(projectPath, reportedPath);
171
+ const candidates = links.filter((link) => link.physicalPath === physical);
172
+ return candidates.length === 1 ? (candidates[0] ?? null) : null;
173
+ }
174
+ if (!validRelativePath(reportedPath)) {
175
+ return null;
176
+ }
177
+ const candidates = links.filter((link) => link.path === reportedPath || link.path.endsWith(`/${reportedPath}`));
178
+ return candidates.length === 1 ? (candidates[0] ?? null) : null;
179
+ }
180
+ function parseJsonOutput(stdout) {
181
+ const jsonStart = stdout.indexOf('{');
182
+ if (jsonStart === -1) {
183
+ return null;
184
+ }
185
+ try {
186
+ return JSON.parse(stdout.slice(jsonStart));
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ function vitestFilePath(result) {
193
+ for (const candidate of [result.testFilePath, result.name, result.filepath, result.filePath]) {
194
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
195
+ return candidate;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ function effectiveComplianceTimeoutMs() {
201
+ const policy = getRuntimePolicy().validation;
202
+ const availableMs = remainingExecutionMs() - policy.receiptPublicationDeadlineMs - PROCESS_TERMINATION_GRACE_MS;
203
+ if (availableMs <= 0) {
204
+ throw new ComplianceCommandTerminalError('COMPLIANCE_COMMAND_DEADLINE_EXHAUSTED');
205
+ }
206
+ return Math.min(policy.complianceCommandTimeoutMs, availableMs);
207
+ }
208
+ function throwForTerminalProcessFailure(result) {
209
+ if (result.aborted) {
210
+ throw new ComplianceCommandTerminalError('COMPLIANCE_COMMAND_ABORTED');
211
+ }
212
+ if (result.timedOut) {
213
+ throw new ComplianceCommandTerminalError('COMPLIANCE_COMMAND_TIMED_OUT');
214
+ }
215
+ }
216
+ async function runVitestAdapter(links, projectPath, signal) {
217
+ const statuses = new Map();
218
+ const result = await runAbortableProcess('pnpm', [
130
219
  'exec',
131
220
  technologyValue('technology-vitest-a9127f'),
132
221
  '--reporter=json',
133
222
  '--run',
134
- ...testPaths,
135
- ];
136
- return runAbortableProcess('pnpm', args, {
223
+ ...links.map((link) => link.path),
224
+ ], {
137
225
  cwd: projectPath,
138
- timeoutMs,
226
+ timeoutMs: effectiveComplianceTimeoutMs(),
139
227
  maxBufferBytes: 64 * 1024 * 1024,
140
228
  signal,
141
- }).then((result) => {
142
- if (result.status !== 0 || result.error) {
143
- return null;
229
+ });
230
+ throwForTerminalProcessFailure(result);
231
+ if (result.error) {
232
+ return { statuses, detail: 'runner failed' };
233
+ }
234
+ const report = parseJsonOutput(result.stdout);
235
+ if (!report || !Array.isArray(report.testResults)) {
236
+ return { statuses, detail: 'unparseable runner output' };
237
+ }
238
+ for (const testResult of report.testResults) {
239
+ const reportedPath = vitestFilePath(testResult);
240
+ if (!reportedPath || !Array.isArray(testResult.assertionResults)) {
241
+ continue;
144
242
  }
243
+ const relativePath = isAbsolute(reportedPath)
244
+ ? relative(projectPath, reportedPath)
245
+ : reportedPath;
246
+ if (!links.some((link) => link.path === relativePath)) {
247
+ continue;
248
+ }
249
+ const assertions = testResult.assertionResults;
250
+ statuses.set(relativePath, assertions.length > 0 && assertions.every((item) => item.status === 'passed')
251
+ ? 'pass'
252
+ : 'fail');
253
+ }
254
+ return { statuses, detail: 'vitest JSON' };
255
+ }
256
+ function collectPlaywrightFiles(suites, output = new Map()) {
257
+ for (const suite of suites) {
258
+ if (suite.file) {
259
+ const statuses = output.get(suite.file) ?? [];
260
+ for (const spec of suite.specs ?? []) {
261
+ for (const test of spec.tests ?? []) {
262
+ for (const result of test.results ?? []) {
263
+ if (typeof result.status === 'string') {
264
+ statuses.push(result.status);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ output.set(suite.file, statuses);
270
+ }
271
+ collectPlaywrightFiles(suite.suites ?? [], output);
272
+ }
273
+ return output;
274
+ }
275
+ async function runPlaywrightAdapter(links, projectPath, signal) {
276
+ const statuses = new Map();
277
+ const config = links[0]?.config;
278
+ if (!config) {
279
+ return { statuses, detail: 'missing config' };
280
+ }
281
+ const result = await runAbortableProcess('pnpm', [
282
+ 'exec',
283
+ technologyValue('technology-playwright-7827e8'),
284
+ 'test',
285
+ '--config',
286
+ config,
287
+ '--reporter=json',
288
+ ...links.map((link) => link.path),
289
+ ], {
290
+ cwd: projectPath,
291
+ timeoutMs: effectiveComplianceTimeoutMs(),
292
+ maxBufferBytes: 64 * 1024 * 1024,
293
+ signal,
294
+ });
295
+ throwForTerminalProcessFailure(result);
296
+ if (result.error) {
297
+ return { statuses, detail: 'runner failed' };
298
+ }
299
+ const report = parseJsonOutput(result.stdout);
300
+ if (!report || !Array.isArray(report.suites)) {
301
+ return { statuses, detail: 'unparseable runner output' };
302
+ }
303
+ const reportedFiles = collectPlaywrightFiles(report.suites);
304
+ for (const [reportedPath, testStatuses] of reportedFiles) {
145
305
  try {
146
- const jsonStart = result.stdout.indexOf('{');
147
- return jsonStart === -1
148
- ? null
149
- : JSON.parse(result.stdout.slice(jsonStart));
306
+ const link = await matchPlaywrightLink(reportedPath, links, projectPath);
307
+ if (!link) {
308
+ continue;
309
+ }
310
+ statuses.set(link.path, testStatuses.length === 0
311
+ ? 'missing'
312
+ : testStatuses.every((status) => status === 'passed')
313
+ ? 'pass'
314
+ : 'fail');
150
315
  }
151
316
  catch {
152
- return null;
317
+ // Untrusted reporter paths never become evidence.
153
318
  }
154
- });
319
+ }
320
+ return { statuses, detail: 'playwright JSON' };
155
321
  }
156
- function resolveVitestTestFilePath(result) {
157
- // Vitest JSON returns the file path under `testResults[].name` in v4.
158
- const candidates = [result.testFilePath, result.name, result.filepath, result.filePath];
159
- for (const candidate of candidates) {
160
- if (typeof candidate === 'string' && candidate.trim().length > 0) {
161
- return candidate;
322
+ async function resolveLink(link, projectPath) {
323
+ const runner = link.runner ?? technologyValue('technology-vitest-a9127f');
324
+ const base = {
325
+ path: link.path,
326
+ physicalPath: '',
327
+ runner,
328
+ ...(link.config ? { config: link.config } : {}),
329
+ };
330
+ if (runner !== technologyValue('technology-vitest-a9127f') &&
331
+ runner !== technologyValue('technology-playwright-7827e8')) {
332
+ return { ...base, error: `unsupported runner: ${runner}` };
333
+ }
334
+ if (runner === technologyValue('technology-vitest-a9127f') && link.config) {
335
+ return { ...base, error: `${runner} links do not accept config metadata` };
336
+ }
337
+ if (runner === technologyValue('technology-playwright-7827e8') && !link.config) {
338
+ return { ...base, error: `${runner} links require config metadata` };
339
+ }
340
+ try {
341
+ const physicalPath = await resolveSafeFile(projectPath, link.path);
342
+ if (link.config) {
343
+ await resolveSafeFile(projectPath, link.config);
162
344
  }
345
+ return { ...base, physicalPath };
163
346
  }
164
- return null;
347
+ catch (error) {
348
+ const message = error.code === 'ENOENT'
349
+ ? 'file not found'
350
+ : error instanceof Error
351
+ ? error.message
352
+ : 'unsafe or missing path';
353
+ return { ...base, error: message };
354
+ }
355
+ }
356
+ function groupKey(link) {
357
+ return `${link.runner}\0${link.config ?? ''}`;
358
+ }
359
+ const EXECUTABLE_TEST_FILE = /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/u;
360
+ function isExecutableTestLink(link) {
361
+ return link.runner !== undefined || EXECUTABLE_TEST_FILE.test(link.path);
362
+ }
363
+ function normalizedEvidencePath(path) {
364
+ return path.trim().replaceAll('\\', '/').replace(/^\.\//u, '');
365
+ }
366
+ function commandEvidenceCoversPath(row, path) {
367
+ return (row?.commandEvidence?.relevantPaths
368
+ ?.map(normalizedEvidencePath)
369
+ .includes(normalizedEvidencePath(path)) === true);
165
370
  }
166
- function toProjectRelativeTestPath(testFilePath, projectPath) {
167
- const prefix = projectPath.endsWith('/') ? projectPath : `${projectPath}/`;
168
- return testFilePath.startsWith(prefix) ? testFilePath.slice(prefix.length) : testFilePath;
371
+ function canonicalScenarioCriterion(scenario) {
372
+ const steps = scenario.steps ?? [];
373
+ return steps.length > 0
374
+ ? steps.map((step) => `${step.keyword.toUpperCase()} ${step.text}`).join(' ')
375
+ : undefined;
169
376
  }
170
- function resolveScenarioTestPath(link) {
171
- return typeof link.path === 'string' && link.path.trim().length > 0 ? link.path : null;
377
+ function resolveExactCommandRow(exactRows, scenario) {
378
+ const identities = new Set([scenario.title, canonicalScenarioCriterion(scenario)]
379
+ .filter((value) => typeof value === 'string')
380
+ .map(normalizeCriterionText)
381
+ .filter(Boolean));
382
+ let resolved;
383
+ for (const identity of identities) {
384
+ if (!exactRows.has(identity)) {
385
+ continue;
386
+ }
387
+ const candidate = exactRows.get(identity);
388
+ if (candidate === undefined || (resolved !== undefined && resolved !== candidate)) {
389
+ return undefined;
390
+ }
391
+ resolved = candidate;
392
+ }
393
+ return resolved;
394
+ }
395
+ async function loadExactCommandRows(spec, projectPath, canonicalProjectId) {
396
+ const exactRows = new Map();
397
+ if (!canonicalProjectId) {
398
+ return exactRows;
399
+ }
400
+ const artifacts = await readEvidenceArtifacts({
401
+ spec,
402
+ projectId: canonicalProjectId,
403
+ specId: spec.id,
404
+ projectPath,
405
+ }).catch(() => undefined);
406
+ for (const row of artifacts?.traceabilityMatrix?.rows ?? []) {
407
+ if (!traceabilityRowHasCurrentCommandEvidence(row, { projectId: canonicalProjectId, projectPath })) {
408
+ continue;
409
+ }
410
+ const identities = [row.acceptanceCriterion, row.scenario]
411
+ .filter((value) => typeof value === 'string')
412
+ .map(normalizeCriterionText)
413
+ .filter(Boolean);
414
+ for (const identity of new Set(identities)) {
415
+ exactRows.set(identity, exactRows.has(identity) && exactRows.get(identity) !== row ? undefined : row);
416
+ }
417
+ }
418
+ return exactRows;
172
419
  }
173
- // ---------------------------------------------------------------------------
174
- // Public API
175
- // ---------------------------------------------------------------------------
176
- /**
177
- * Run vitest-backed spec-compliance for the given spec.
178
- * Each frontmatter scenario must have a `tests` array; missing links count as `fail`.
179
- */
180
- // eslint-disable-next-line max-lines-per-function, complexity -- spec compliance runner: each section handles a discrete verification phase (file access, scenario parsing, test execution, verdict aggregation)
420
+ // eslint-disable-next-line max-lines-per-function -- coordinates validation, adapters, and isolated per-scenario evidence
181
421
  export async function runSpecCompliance(spec, projectPath, signal, canonicalProjectId) {
182
422
  const { readFile } = await import('node:fs/promises');
183
423
  let raw;
@@ -185,41 +425,41 @@ export async function runSpecCompliance(spec, projectPath, signal, canonicalProj
185
425
  raw = await readFile(spec.specPath, 'utf-8');
186
426
  }
187
427
  catch {
188
- // No spec file — return empty compliance
189
428
  return { dimensionScore: 0, perScenario: [], command: '' };
190
429
  }
191
430
  const scenarios = parseFrontmatterScenarios(raw);
192
431
  if (scenarios.length === 0) {
193
432
  return { dimensionScore: 0, perScenario: [], command: '' };
194
433
  }
195
- // ── Phase 1: collect all unique test paths ────────────────────────────────
196
- const allTestPaths = new Set();
197
- for (const scenario of scenarios) {
198
- for (const link of scenario.tests ?? []) {
199
- const linkPath = resolveScenarioTestPath(link);
200
- if (linkPath) {
201
- allTestPaths.add(linkPath);
202
- }
203
- }
204
- }
205
- const command = allTestPaths.size > 0
206
- ? `vitest --reporter=json --run ${[...allTestPaths].join(' ')}`
207
- : 'vitest --reporter=json --run (no test files linked)';
208
- // ── Phase 2: check file existence ────────────────────────────────────────
209
- const existenceMap = new Map();
210
- for (const p of allTestPaths) {
211
- const fullPath = join(projectPath, p);
434
+ const allLinks = scenarios.flatMap((scenario) => scenario.tests ?? []);
435
+ const executableLinks = allLinks.filter(isExecutableTestLink);
436
+ const commandLinks = allLinks.filter((link) => !isExecutableTestLink(link));
437
+ const resolvedLinks = await Promise.all(executableLinks.map((link) => resolveLink(link, projectPath)));
438
+ const resolvedBySource = new Map(executableLinks.map((link, index) => [link, resolvedLinks[index]]));
439
+ const commandLinkErrors = new Map();
440
+ await Promise.all(commandLinks.map(async (link) => {
212
441
  try {
213
- await access(fullPath);
214
- existenceMap.set(p, true);
442
+ await resolveSafeFile(projectPath, link.path);
215
443
  }
216
- catch {
217
- existenceMap.set(p, false);
444
+ catch (error) {
445
+ commandLinkErrors.set(link, error.code === 'ENOENT'
446
+ ? 'file not found'
447
+ : error instanceof Error
448
+ ? error.message
449
+ : 'unsafe or missing path');
218
450
  }
451
+ }));
452
+ const exactCommandRows = await loadExactCommandRows(spec, projectPath, canonicalProjectId);
453
+ const validLinks = resolvedLinks.filter((link) => !link.error);
454
+ const canonicalLinks = new Map();
455
+ for (const link of validLinks) {
456
+ canonicalLinks.set(`${groupKey(link)}\0${link.path}\0${link.physicalPath}`, link);
457
+ }
458
+ const groups = new Map();
459
+ for (const link of canonicalLinks.values()) {
460
+ groups.set(groupKey(link), [...(groups.get(groupKey(link)) ?? []), link]);
219
461
  }
220
- // ── Phase 3: run vitest for existing paths ───────────────────────────────
221
- const existingPaths = [...allTestPaths].filter((p) => existenceMap.get(p) === true);
222
- let vitestResult = null;
462
+ const statusByGroup = new Map();
223
463
  const fullTestReceipt = canonicalProjectId
224
464
  ? readMatchingValidationEvidence(canonicalProjectId, {
225
465
  projectPath,
@@ -228,98 +468,76 @@ export async function runSpecCompliance(spec, projectPath, signal, canonicalProj
228
468
  scope: ['src', 'tests'],
229
469
  })
230
470
  : null;
231
- if (existingPaths.length > 0 && !fullTestReceipt) {
232
- vitestResult = await runVitest(existingPaths, projectPath, 120_000, signal);
233
- }
234
- // Build a map: testFilePath -> pass/fail
235
- const testFileStatus = new Map();
236
- if (fullTestReceipt) {
237
- for (const path of existingPaths) {
238
- testFileStatus.set(path, 'pass');
471
+ for (const [key, links] of groups) {
472
+ if (links[0]?.runner === technologyValue('technology-vitest-a9127f') && fullTestReceipt) {
473
+ statusByGroup.set(key, {
474
+ statuses: new Map(links.map((link) => [link.path, 'pass'])),
475
+ detail: 'matching full-test receipt',
476
+ });
239
477
  }
240
- }
241
- if (vitestResult) {
242
- for (const tr of vitestResult.testResults) {
243
- const testFilePath = resolveVitestTestFilePath(tr);
244
- if (!testFilePath) {
245
- continue;
246
- }
247
- const allPassed = tr.assertionResults.every((r) => r.status === 'passed');
248
- testFileStatus.set(testFilePath, allPassed ? 'pass' : 'fail');
249
- // Also index by relative path
250
- const relPath = toProjectRelativeTestPath(testFilePath, projectPath);
251
- testFileStatus.set(relPath, allPassed ? 'pass' : 'fail');
252
- }
253
- }
254
- // ── Phase 4: build per-scenario verdicts ─────────────────────────────────
255
- const perScenario = [];
256
- let passCount = 0;
257
- for (const scenario of scenarios) {
258
- const links = scenario.tests ?? [];
259
- const evidence = [];
260
- let scenarioVerdict;
261
- if (links.length === 0) {
262
- evidence.push('No test links — scenario has no tests array');
263
- scenarioVerdict = 'missing';
478
+ else if (links[0]?.runner === technologyValue('technology-vitest-a9127f')) {
479
+ statusByGroup.set(key, await runVitestAdapter(links, projectPath, signal));
264
480
  }
265
481
  else {
266
- // Check all links
267
- const verdicts = [];
268
- for (const link of links) {
269
- const linkPath = resolveScenarioTestPath(link);
270
- if (!linkPath) {
271
- evidence.push('Malformed test link: missing path');
272
- verdicts.push('missing');
273
- continue;
274
- }
275
- const exists = existenceMap.get(linkPath) !== false;
276
- if (!exists) {
277
- evidence.push(`${linkPath}: file not found`);
278
- verdicts.push('missing');
279
- continue;
280
- }
281
- const status = testFileStatus.get(linkPath) ?? (vitestResult || fullTestReceipt ? 'fail' : 'missing');
282
- if (status === 'pass') {
283
- evidence.push(`${linkPath}: passed`);
284
- verdicts.push('pass');
285
- }
286
- else if (status === 'missing') {
287
- evidence.push(`${linkPath}: not found in vitest output`);
288
- verdicts.push('missing');
289
- }
290
- else {
291
- evidence.push(`${linkPath}: failed`);
292
- verdicts.push('fail');
482
+ statusByGroup.set(key, await runPlaywrightAdapter(links, projectPath, signal));
483
+ }
484
+ }
485
+ const commands = [...groups.values()].map((links) => {
486
+ const first = links[0];
487
+ if (first?.runner === technologyValue('technology-playwright-7827e8')) {
488
+ return `${first.runner} test --config ${first.config ?? ''} --reporter=json ${links.map((link) => link.path).join(' ')}`;
489
+ }
490
+ return `${technologyValue('technology-vitest-a9127f')} --reporter=json --run ${links.map((link) => link.path).join(' ')}`;
491
+ });
492
+ const perScenario = scenarios.map((scenario) => {
493
+ if (!scenario.tests?.length) {
494
+ return {
495
+ title: scenario.title,
496
+ verdict: 'missing',
497
+ evidence: ['No test links scenario has no tests array'],
498
+ };
499
+ }
500
+ const evidence = [];
501
+ const commandRow = resolveExactCommandRow(exactCommandRows, scenario);
502
+ const verdicts = scenario.tests.map((source) => {
503
+ if (!isExecutableTestLink(source)) {
504
+ const error = commandLinkErrors.get(source);
505
+ if (error) {
506
+ evidence.push(`${source.path || '(missing path)'}: ${error}`);
507
+ return 'missing';
293
508
  }
509
+ const verdict = commandEvidenceCoversPath(commandRow, source.path) ? 'pass' : 'missing';
510
+ evidence.push(`${source.path}: ${verdict === 'pass' ? 'passed by current command evidence' : 'missing current command evidence'}`);
511
+ return verdict;
294
512
  }
295
- // Scenario passes only when ALL linked tests pass
296
- if (verdicts.every((v) => v === 'pass')) {
297
- scenarioVerdict = 'pass';
513
+ const link = resolvedBySource.get(source);
514
+ if (!link || link.error) {
515
+ evidence.push(`${source.path || '(missing path)'}: ${link?.error ?? 'malformed test link'}`);
516
+ return 'missing';
298
517
  }
299
- else if (verdicts.some((v) => v === 'missing')) {
300
- scenarioVerdict = 'missing';
301
- }
302
- else {
303
- scenarioVerdict = 'fail';
304
- }
305
- }
306
- if (scenarioVerdict === 'pass') {
307
- passCount++;
308
- }
309
- perScenario.push({ title: scenario.title, verdict: scenarioVerdict, evidence });
310
- }
311
- const dimensionScore = scenarios.length > 0 ? Math.round((passCount / scenarios.length) * 100) : 0;
312
- return { dimensionScore, perScenario, command };
518
+ const adapter = statusByGroup.get(groupKey(link));
519
+ const verdict = adapter?.statuses.get(link.path) ?? 'missing';
520
+ evidence.push(`${link.path}: ${verdict === 'pass' ? 'passed' : verdict === 'fail' ? 'failed' : `not found in ${adapter?.detail ?? 'runner output'}`}`);
521
+ return verdict;
522
+ });
523
+ const verdict = verdicts.every((item) => item === 'pass')
524
+ ? 'pass'
525
+ : verdicts.some((item) => item === 'missing')
526
+ ? 'missing'
527
+ : 'fail';
528
+ return { title: scenario.title, verdict, evidence };
529
+ });
530
+ const passCount = perScenario.filter((scenario) => scenario.verdict === 'pass').length;
531
+ return {
532
+ dimensionScore: Math.round((passCount / scenarios.length) * 100),
533
+ perScenario,
534
+ command: commands.join(' && ') ||
535
+ `${technologyValue('technology-vitest-a9127f')} --reporter=json --run (no test files linked)`,
536
+ };
313
537
  }
314
- // ---------------------------------------------------------------------------
315
- // check_readiness gate: scenarios_missing_tests
316
- // ---------------------------------------------------------------------------
317
- /**
318
- * Returns a list of scenario titles that lack a `tests` array.
319
- * Used by check-readiness to block `approved` when scenarios are untested.
320
- */
321
538
  export function findScenariosWithoutTests(raw) {
322
- const scenarios = parseFrontmatterScenarios(raw);
323
- return scenarios.filter((s) => !s.tests || s.tests.length === 0).map((s) => s.title);
539
+ return parseFrontmatterScenarios(raw)
540
+ .filter((scenario) => !scenario.tests?.length)
541
+ .map((scenario) => scenario.title);
324
542
  }
325
543
  //# sourceMappingURL=spec-compliance-runner.js.map