mandrel 2.0.0 → 2.2.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 (323) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +20 -9
  3. package/.agents/agents/story-worker.md +45 -48
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +60 -46
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +33 -57
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +17 -19
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/gherkin-standards.md +10 -0
  14. package/.agents/rules/git-conventions-reference.md +42 -51
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  16. package/.agents/schemas/agentrc.schema.json +35 -46
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  24. package/.agents/schemas/signal-event.schema.json +3 -3
  25. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  26. package/.agents/schemas/validation-evidence.schema.json +1 -1
  27. package/.agents/scripts/acceptance-eval.js +24 -68
  28. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  29. package/.agents/scripts/bootstrap.js +3 -3
  30. package/.agents/scripts/check-dead-exports.js +43 -104
  31. package/.agents/scripts/check-doc-links.js +2 -2
  32. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  33. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  34. package/.agents/scripts/deliver-recover.js +122 -0
  35. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  36. package/.agents/scripts/evidence-gate.js +20 -50
  37. package/.agents/scripts/generate-skills-index.js +17 -1
  38. package/.agents/scripts/generate-workflows-doc.js +4 -4
  39. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  40. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  41. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  42. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  43. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  44. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  45. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  46. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  47. package/.agents/scripts/lib/checks/index.js +1 -1
  48. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  49. package/.agents/scripts/lib/checks/state.js +17 -248
  50. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  51. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  52. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  53. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  54. package/.agents/scripts/lib/cli-args.js +23 -2
  55. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  56. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  57. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  58. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  59. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  60. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  61. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  62. package/.agents/scripts/lib/config/explain.js +10 -16
  63. package/.agents/scripts/lib/config/github.js +7 -5
  64. package/.agents/scripts/lib/config/limits.js +15 -25
  65. package/.agents/scripts/lib/config/quality.js +11 -14
  66. package/.agents/scripts/lib/config/runners.js +8 -21
  67. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  68. package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
  69. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  70. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  71. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  72. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  73. package/.agents/scripts/lib/duplicate-search.js +38 -7
  74. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  75. package/.agents/scripts/lib/format-generated-json.js +97 -0
  76. package/.agents/scripts/lib/framework-version.js +19 -189
  77. package/.agents/scripts/lib/gh-exec.js +8 -0
  78. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  79. package/.agents/scripts/lib/git-utils.js +0 -14
  80. package/.agents/scripts/lib/json-utils.js +1 -2
  81. package/.agents/scripts/lib/label-constants.js +0 -15
  82. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  83. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  84. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  85. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  86. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  87. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  88. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  89. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  91. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  92. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  93. package/.agents/scripts/lib/orchestration/code-review.js +70 -191
  94. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  95. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  96. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  97. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  98. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  99. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  100. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  101. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  102. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  103. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  105. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  106. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  107. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  108. package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
  109. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
  110. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
  111. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  112. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  114. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  115. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
  116. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  117. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  118. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  119. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  120. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  121. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  122. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  123. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  124. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  125. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  126. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  127. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  128. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  129. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  130. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  131. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  132. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  139. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  140. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  141. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  142. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  143. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  144. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  145. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  146. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  147. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
  148. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  149. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  150. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  151. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  152. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  153. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  154. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  155. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  156. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  157. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  158. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  159. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  160. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  161. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  162. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  163. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  164. package/.agents/scripts/lib/planning-corpus.js +12 -286
  165. package/.agents/scripts/lib/preflight-runner.js +2 -2
  166. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  167. package/.agents/scripts/lib/signals/index.js +4 -17
  168. package/.agents/scripts/lib/signals/read.js +35 -35
  169. package/.agents/scripts/lib/signals/schema.js +8 -11
  170. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  171. package/.agents/scripts/lib/signals/write.js +0 -1
  172. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  173. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  174. package/.agents/scripts/lib/story-adjacency.js +8 -7
  175. package/.agents/scripts/lib/story-body/story-body.js +81 -13
  176. package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
  177. package/.agents/scripts/lib/test-env.js +14 -1
  178. package/.agents/scripts/lib/test-tiers.js +0 -3
  179. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  180. package/.agents/scripts/lib/validation-evidence.js +31 -59
  181. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  182. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  183. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  184. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  185. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  186. package/.agents/scripts/plan-context.js +38 -7
  187. package/.agents/scripts/plan-critics.js +203 -0
  188. package/.agents/scripts/plan-persist.js +145 -35
  189. package/.agents/scripts/plan-run-epilogue.js +83 -38
  190. package/.agents/scripts/post-structured-comment.js +0 -38
  191. package/.agents/scripts/pr-watch-with-update.js +43 -22
  192. package/.agents/scripts/providers/github/compose.js +0 -1
  193. package/.agents/scripts/providers/github/errors.js +0 -19
  194. package/.agents/scripts/providers/github/issues.js +1 -11
  195. package/.agents/scripts/providers/github/mappers.js +5 -0
  196. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  197. package/.agents/scripts/providers/github/tickets.js +33 -153
  198. package/.agents/scripts/providers/github.js +17 -6
  199. package/.agents/scripts/quality-preview.js +13 -6
  200. package/.agents/scripts/resolve-stories.js +236 -0
  201. package/.agents/scripts/run-coverage.js +4 -1
  202. package/.agents/scripts/run-lint.js +2 -2
  203. package/.agents/scripts/run-verify.js +31 -2
  204. package/.agents/scripts/signals-view.js +9 -10
  205. package/.agents/scripts/single-story-close.js +173 -18
  206. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  207. package/.agents/scripts/single-story-init.js +6 -10
  208. package/.agents/scripts/stories-wave-tick.js +380 -53
  209. package/.agents/scripts/story-plan.js +3 -3
  210. package/.agents/scripts/update-ticket-state.js +8 -50
  211. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  212. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  213. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  214. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  215. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  216. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  217. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  218. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  219. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  220. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  221. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  222. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  223. package/.agents/skills/skills.index.json +2 -12
  224. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  225. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  226. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  227. package/.agents/workflows/audit-architecture.md +3 -4
  228. package/.agents/workflows/audit-clean-code.md +4 -4
  229. package/.agents/workflows/audit-documentation.md +4 -5
  230. package/.agents/workflows/audit-lighthouse.md +8 -0
  231. package/.agents/workflows/audit-navigability.md +10 -0
  232. package/.agents/workflows/audit-performance.md +2 -3
  233. package/.agents/workflows/audit-quality.md +8 -9
  234. package/.agents/workflows/audit-security.md +1 -2
  235. package/.agents/workflows/audit-seo.md +10 -0
  236. package/.agents/workflows/audit-ux-ui.md +7 -0
  237. package/.agents/workflows/deliver.md +133 -45
  238. package/.agents/workflows/git-cleanup.md +2 -2
  239. package/.agents/workflows/git-deliver.md +1 -1
  240. package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
  241. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
  242. package/.agents/workflows/helpers/code-review.md +14 -12
  243. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  244. package/.agents/workflows/helpers/deliver-story.md +209 -118
  245. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  246. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  247. package/.agents/workflows/plan.md +239 -19
  248. package/.agents/workflows/qa-assist.md +6 -6
  249. package/.agents/workflows/qa-explore.md +3 -3
  250. package/.agents/workflows/qa-run.md +1 -5
  251. package/bin/mandrel.js +12 -1
  252. package/docs/CHANGELOG.md +62 -0
  253. package/lib/cli/registry.js +262 -19
  254. package/lib/cli/sync-agents.js +157 -0
  255. package/lib/cli/sync-commands.js +115 -6
  256. package/lib/cli/sync.js +168 -6
  257. package/lib/cli/update.js +105 -8
  258. package/lib/cli/version-helpers.js +131 -0
  259. package/lib/migrations/README.md +7 -5
  260. package/lib/migrations/index.js +17 -9
  261. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  262. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  263. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  264. package/package.json +2 -2
  265. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  266. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  268. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  270. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  271. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  273. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  274. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  275. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  277. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  278. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  279. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  280. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  281. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  282. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  283. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  284. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  285. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  286. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  287. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  288. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  289. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  290. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  291. package/.agents/schemas/risk-verdict.schema.json +0 -53
  292. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  293. package/.agents/scripts/analyze-execution.js +0 -444
  294. package/.agents/scripts/check-prepush-recovery.js +0 -90
  295. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  296. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  297. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  298. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  299. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  300. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  301. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  302. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  303. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  304. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  305. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  306. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  307. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  308. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  309. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  310. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  311. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  312. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  313. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  314. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  315. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  316. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  317. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  318. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  319. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  320. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  321. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  322. package/.agents/scripts/resolve-plan-run.js +0 -117
  323. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -1,328 +0,0 @@
1
- /**
2
- * perf-report-readers.js — I/O readers + git-scrape helpers for the
3
- * structured perf-summary comments (Epic #1030 / Story #1123, split out
4
- * under Story #3350).
5
- *
6
- * Every export here touches the filesystem, the ticketing provider, or
7
- * `git log`. The pure renderers that format the report bodies live in the
8
- * sibling `perf-report-render.js`; the Story-/Epic-mode orchestrators in
9
- * `analyze-execution.js` wire these readers to those renderers.
10
- *
11
- * @see docs/data-dictionary.md §StoryPerfSummary, §EpicPerfReport
12
- */
13
-
14
- import fs from 'node:fs/promises';
15
- import { storyArtifactPath } from '../config/temp-paths.js';
16
- import { gitSpawn } from '../git-utils.js';
17
- import { Logger } from '../Logger.js';
18
- import { read as readSignals } from '../signals/read.js';
19
- import { concurrentMap } from '../util/concurrent-map.js';
20
- import { extractStoryPerfSummaryFromComment } from './perf-report-render.js';
21
- import { forEachLine } from './signals-writer.js';
22
-
23
- /**
24
- * Stream every lifecycle event under the Epic into an in-memory array
25
- * (Story #3025 / Task #3030). The perf-aggregator's wave-parallelism
26
- * reducer needs a single chronological iterable spanning wave-start /
27
- * wave-complete / state-transition events; the canonical reader walks
28
- * the epic-level signals.ndjson first, then each per-Story stream in
29
- * ascending Story-ID order, so that ordering matches the wave-tick
30
- * emit order at runtime.
31
- *
32
- * Returns `[]` on any reader failure so the analyzer keeps composing
33
- * its other report sections.
34
- */
35
- export async function readEpicLifecycleEvents(epicId, config, logger) {
36
- const events = [];
37
- try {
38
- for await (const evt of readSignals({ epic: epicId, config })) {
39
- if (evt && typeof evt === 'object') events.push(evt);
40
- }
41
- } catch (err) {
42
- logger?.warn?.(
43
- `[analyze-execution] readEpicLifecycleEvents(${epicId}) failed (non-fatal): ${
44
- err instanceof Error ? err.message : String(err)
45
- }`,
46
- );
47
- }
48
- return events;
49
- }
50
-
51
- /**
52
- * Read every NDJSON line from a Story's signals stream into an array.
53
- * Missing files resolve to `[]` so callers can treat absence as
54
- * "no signals yet" without a try/catch.
55
- */
56
- export async function readStorySignals(epicId, storyId, config) {
57
- const events = [];
58
- await forEachLine(
59
- epicId,
60
- storyId,
61
- (parsed) => {
62
- if (parsed && typeof parsed === 'object') events.push(parsed);
63
- },
64
- config,
65
- );
66
- return events;
67
- }
68
-
69
- /**
70
- * Best-effort read of the per-Story phase-timings JSON written by
71
- * `post-merge-close.js`. Returns `null` when the file is missing or
72
- * malformed — phaseTimingsMs in the StoryPerfSummary degrades to `{}`
73
- * rather than throwing.
74
- */
75
- export async function readPhaseTimings(epicId, storyId, config, overridePath) {
76
- const target =
77
- overridePath ??
78
- storyArtifactPath(epicId, storyId, 'phase-timings.json', config);
79
- try {
80
- const buf = await fs.readFile(target, 'utf8');
81
- const parsed = JSON.parse(buf);
82
- if (parsed && typeof parsed === 'object') return parsed;
83
- return null;
84
- } catch (err) {
85
- if (err && err.code === 'ENOENT') return null;
86
- Logger.warn(
87
- `[analyze-execution] could not parse phase-timings at ${target}: ${
88
- err instanceof Error ? err.message : String(err)
89
- }`,
90
- );
91
- return null;
92
- }
93
- }
94
-
95
- /**
96
- * Throw-away ghSpawnCount reader (Story #1795 / Epic #1788). Looks for
97
- * the `temp/epic-<eid>/stories/story-<sid>/gh-spawn-count.json` file written by
98
- * `close-validation.emitGhSpawnCount` immediately before the perf-summary
99
- * phase spawns this analyzer. Returns the integer count when present and
100
- * well-formed; `null` when absent or unparseable. Callers treat `null` as
101
- * "no signal" — the structured comment simply omits `ghSpawnCount`.
102
- */
103
- export async function readGhSpawnCount(epicId, storyId, config) {
104
- const target = storyArtifactPath(
105
- epicId,
106
- storyId,
107
- 'gh-spawn-count.json',
108
- config,
109
- );
110
- try {
111
- const buf = await fs.readFile(target, 'utf8');
112
- const parsed = JSON.parse(buf);
113
- const count = parsed?.ghSpawnCount;
114
- if (Number.isInteger(count) && count >= 0) return count;
115
- return null;
116
- } catch (err) {
117
- if (err && err.code === 'ENOENT') return null;
118
- Logger.warn(
119
- `[analyze-execution] could not parse gh-spawn-count at ${target}: ${
120
- err instanceof Error ? err.message : String(err)
121
- }`,
122
- );
123
- return null;
124
- }
125
- }
126
-
127
- /**
128
- * Fetch every `story-perf-summary` structured comment from the Stories
129
- * under an Epic. We rely on `provider.getSubTickets(epicId)` for child
130
- * enumeration (Stories carry `parent: #<epicId>` either via the native
131
- * sub-issue link or the body marker — `getSubTickets` reconciles both).
132
- * Missing comments are skipped (the Story may have been recut and not
133
- * closed yet).
134
- */
135
- export async function collectStorySummaries(provider, epicId, logger) {
136
- let stories;
137
- try {
138
- const children = await provider.getSubTickets(epicId);
139
- stories = Array.isArray(children) ? children : [];
140
- } catch (err) {
141
- logger.warn?.(
142
- `[analyze-execution] getSubTickets(${epicId}) failed: ${
143
- err instanceof Error ? err.message : String(err)
144
- }`,
145
- );
146
- return [];
147
- }
148
- // Filter to Story tickets — descendants include Tasks too. We only
149
- // want the per-Story summaries, so guard on `type::story`.
150
- const storyTickets = stories.filter(
151
- (t) =>
152
- Array.isArray(t?.labels) &&
153
- t.labels.some(
154
- (l) => (typeof l === 'string' ? l : l?.name) === 'type::story',
155
- ),
156
- );
157
-
158
- // Bounded-parallel comment fetch (Story #3990). Each Story's comment
159
- // thread is an independent paginated REST read through a fresh `gh`
160
- // spawn, so a serial loop pays N sequential round-trips. concurrentMap
161
- // preserves input→output index, keeping summaries in Story order; the
162
- // per-Story try/catch stays inside the mapper so one failed fetch
163
- // degrades to warn-and-skip without aborting the batch (mirrors
164
- // verifySingleResult in lib/orchestration/wave-record-io.js, #3024).
165
- const perStory = await concurrentMap(
166
- storyTickets,
167
- async (ticket) => {
168
- const id = Number(ticket.id ?? ticket.number);
169
- if (!Number.isInteger(id) || id < 1) return null;
170
- let comments;
171
- try {
172
- comments = (await provider.getTicketComments(id)) ?? [];
173
- } catch (err) {
174
- logger.warn?.(
175
- `[analyze-execution] getTicketComments(${id}) failed: ${
176
- err instanceof Error ? err.message : String(err)
177
- }`,
178
- );
179
- return null;
180
- }
181
- for (const c of comments) {
182
- const parsed = extractStoryPerfSummaryFromComment(c?.body);
183
- if (parsed) return parsed;
184
- }
185
- return null;
186
- },
187
- { concurrency: 4 },
188
- );
189
- return perStory.filter((s) => s !== null);
190
- }
191
-
192
- /**
193
- * Default git-log gatherer for the baseline-refresh-rate reporter
194
- * (Story #1400 / Task #1427). Reads commits on `epic/<id>` over the
195
- * trailing window and shapes them into the record format the pure
196
- * reporter expects. Returns `[]` on any spawn failure so the retro
197
- * comment keeps composing — the reporter then prints "no Story merges
198
- * in window" and the operator can investigate offline.
199
- *
200
- * Injectable via `runEpicMode({ gatherEpicCommitsFn })` so the unit test
201
- * can pin behavior without a temp git repo.
202
- */
203
- export function gatherEpicCommitsFromGit({ epicId, windowDays, cwd, logger }) {
204
- const ref = `epic/${epicId}`;
205
- const since = `${windowDays} days ago`;
206
- // Subject (%s) carries the Story-merge `(resolves #N)` and the
207
- // `baseline-refresh:` prefix; ISO commit date (%cI) drives the window
208
- // filter inside the reporter. Tab separator avoids subjects that
209
- // contain pipes.
210
- const res = gitSpawn(
211
- cwd,
212
- 'log',
213
- ref,
214
- `--since=${since}`,
215
- '--pretty=format:%H%x09%cI%x09%s',
216
- );
217
- if (res.status !== 0) {
218
- logger.warn?.(
219
- `[analyze-execution] git log ${ref} failed (non-fatal): ${res.stderr || res.stdout}`,
220
- );
221
- return [];
222
- }
223
- const lines = (res.stdout || '').split('\n').filter(Boolean);
224
- return lines.map((line) => {
225
- const [sha, isoDate, ...rest] = line.split('\t');
226
- return {
227
- sha,
228
- isoDate,
229
- subject: rest.join('\t'),
230
- epicId,
231
- };
232
- });
233
- }
234
-
235
- /**
236
- * Default friction aggregator for the Quality-gate friction block
237
- * (Story #1400 / Task #1429). Walks each child Story's signals.ndjson
238
- * stream and returns counts of `baseline-refresh-regression` records
239
- * plus the top offenders by file (and method, when present in the
240
- * `regressedFiles` / `crapOverCap` payloads).
241
- *
242
- * Aggregation runs against the existing `signals.ndjson` stream — no new
243
- * file format. Two upstream record shapes are walked:
244
- * - check-maintainability emits a flat `regressedFiles[]` of `{ file, current, baseline, drop }`.
245
- * - auto-refresh-runner emits `miOverCap[]` / `crapOverCap[]` rows
246
- * carrying `path`/`file` and (for crap) `method`.
247
- *
248
- * Injectable via `runEpicMode({ aggregateFrictionFn })` so the unit test
249
- * can pin behavior without seeding NDJSON files.
250
- */
251
- export async function aggregateBaselineFrictionFromSignals({
252
- epicId,
253
- storyIds,
254
- config,
255
- windowDays,
256
- now,
257
- }) {
258
- const cutoffMs = now().getTime() - windowDays * 24 * 60 * 60 * 1000;
259
- const offenders = new Map(); // key → { file, method, occurrences }
260
- const storiesAffected = new Set();
261
- let totalRecords = 0;
262
-
263
- for (const sid of storyIds) {
264
- let touched = false;
265
- await forEachLine(
266
- epicId,
267
- sid,
268
- (record) => {
269
- if (
270
- !record ||
271
- typeof record !== 'object' ||
272
- record.kind !== 'friction' ||
273
- record.category !== 'baseline-refresh-regression'
274
- ) {
275
- return;
276
- }
277
- const ts = Date.parse(record.ts);
278
- if (Number.isFinite(ts) && ts < cutoffMs) return;
279
-
280
- totalRecords += 1;
281
- touched = true;
282
-
283
- const flatFiles = Array.isArray(record.regressedFiles)
284
- ? record.regressedFiles.map((r) => ({ file: r.file }))
285
- : [];
286
- const miFiles = Array.isArray(record.miOverCap)
287
- ? record.miOverCap.map((r) => ({ file: r.path ?? r.file }))
288
- : [];
289
- const crapMethods = Array.isArray(record.crapOverCap)
290
- ? record.crapOverCap.map((r) => ({
291
- file: r.file,
292
- method: r.method,
293
- }))
294
- : [];
295
-
296
- for (const off of [...flatFiles, ...miFiles, ...crapMethods]) {
297
- if (!off || typeof off.file !== 'string' || off.file.length === 0) {
298
- continue;
299
- }
300
- const key = off.method ? `${off.file}::${off.method}` : off.file;
301
- const prev = offenders.get(key);
302
- if (prev) {
303
- prev.occurrences += 1;
304
- } else {
305
- offenders.set(key, {
306
- file: off.file,
307
- method: off.method ?? null,
308
- occurrences: 1,
309
- });
310
- }
311
- }
312
- },
313
- config,
314
- );
315
- if (touched) storiesAffected.add(sid);
316
- }
317
-
318
- const topOffenders = [...offenders.values()]
319
- .sort((a, b) => b.occurrences - a.occurrences)
320
- .slice(0, 10);
321
-
322
- return {
323
- totalRecords,
324
- storiesAffected: storiesAffected.size,
325
- topOffenders,
326
- windowDays,
327
- };
328
- }
@@ -1,182 +0,0 @@
1
- /**
2
- * perf-report-render.js — pure renderers + summary extraction for the
3
- * structured perf-summary comments (Epic #1030 / Story #1123, split out
4
- * under Story #3350).
5
- *
6
- * Every export here is a pure function: it depends only on its inputs and
7
- * performs no I/O. The Story-/Epic-mode orchestrators in
8
- * `analyze-execution.js` read NDJSON, phase-timings, and git-log records
9
- * via the sibling `perf-report-readers.js`, then hand the resulting plain
10
- * objects to these renderers. Keeping the render surface free of I/O makes
11
- * the report formatting unit-testable without fixtures.
12
- *
13
- * `extractStoryPerfSummaryFromComment` is the one mixed read/parse helper —
14
- * it parses a comment body that originated from `renderStoryBody`, so it
15
- * lives next to the renderer it mirrors. It delegates the fenced-JSON
16
- * extraction to `parseFencedJsonComment` (the shared structured-comment
17
- * parser) rather than open-coding the fence regex.
18
- *
19
- * @see docs/data-dictionary.md §StoryPerfSummary, §EpicPerfReport
20
- */
21
-
22
- import { parseFencedJsonComment } from '../orchestration/structured-comment-parser.js';
23
-
24
- export const STORY_PERF_TYPE = 'story-perf-summary';
25
- export const EPIC_PERF_TYPE = 'epic-perf-report';
26
- // Marker emitted by `upsertStructuredComment` for the story-perf-summary
27
- // type. Mirrors `structuredCommentMarker(STORY_PERF_TYPE)` so the Epic
28
- // rollup can detect summary comments without importing the renderer.
29
- export const STORY_PERF_MARKER = `<!-- ap:structured-comment type="${STORY_PERF_TYPE}" -->`;
30
-
31
- /**
32
- * Render a small operator-facing summary block that doubles as the
33
- * comment body. The fenced JSON payload is the canonical machine-
34
- * readable surface; the prose lines above it give a human a reason to
35
- * skim. The retro composer reads the fenced JSON, not the prose.
36
- */
37
- export function renderStoryBody(payload) {
38
- const friction = Object.entries(payload.frictionByCategory ?? {})
39
- .map(([k, v]) => `${k}=${v}`)
40
- .join(', ');
41
- const phaseRows = Object.entries(payload.phaseTimingsMs ?? {})
42
- .map(([k, v]) => `- \`${k}\`: ${v}ms`)
43
- .join('\n');
44
- const lines = [
45
- `### Story Perf Summary — Story #${payload.storyId} (Epic #${payload.epicId})`,
46
- '',
47
- `Closed at: \`${payload.closedAt}\``,
48
- '',
49
- friction.length > 0
50
- ? `**Friction:** ${friction}`
51
- : '**Friction:** none recorded',
52
- '',
53
- phaseRows.length > 0
54
- ? `**Phase timings:**\n${phaseRows}`
55
- : '**Phase timings:** none recorded',
56
- '',
57
- `**Rework:** ${payload.reworkScore.filesEditedBeyondThreshold} files beyond threshold`,
58
- `**Retries:** ${payload.retryDensity.retries} across ${payload.retryDensity.uniqueCommands} unique command(s)`,
59
- '',
60
- '```json',
61
- JSON.stringify(payload, null, 2),
62
- '```',
63
- ];
64
- return lines.join('\n');
65
- }
66
-
67
- /**
68
- * Render the optional Quality-gate friction summary block (Story #1400 /
69
- * Task #1429). Aggregates `baseline-refresh-regression` friction records
70
- * from the Stories' signals.ndjson streams over the trailing window,
71
- * counts them, and lists the top offenders by file/method.
72
- *
73
- * Output contract:
74
- * - `friction == null` → empty string (caller injected nothing).
75
- * - `friction.totalRecords === 0` → `**Quality gate friction:** none`
76
- * - otherwise → count line + bulleted top-offenders list
77
- *
78
- * The block reads the existing `signals.ndjson` stream — no new file
79
- * format. Aggregation is provided by `aggregateBaselineFrictionFromSignals`
80
- * which the unit test can stub via `runEpicMode({ aggregateFrictionFn })`.
81
- */
82
- export function renderQualityGateFrictionBlock(friction) {
83
- if (!friction) return '';
84
- const total = friction.totalRecords ?? 0;
85
- if (total === 0) {
86
- return ['', '**Quality gate friction:** none', ''].join('\n');
87
- }
88
- const offenders = (friction.topOffenders ?? [])
89
- .map((o) => {
90
- const where = o.method ? `${o.file} → ${o.method}` : o.file;
91
- return `- \`${where}\` — ${o.occurrences} occurrence(s)`;
92
- })
93
- .join('\n');
94
- return [
95
- '',
96
- `**Quality gate friction:** ${total} \`baseline-refresh-regression\` record(s) across ${friction.storiesAffected ?? 0} Story/Stories`,
97
- offenders.length > 0 ? `\nTop offenders:\n${offenders}` : '',
98
- '',
99
- ].join('\n');
100
- }
101
-
102
- /**
103
- * Render the per-Epic baseline-refresh-rate row (Story #1400 / Task #1427).
104
- * Returns an empty string when `refresh` is null so the caller can
105
- * concatenate unconditionally.
106
- */
107
- export function renderBaselineRefreshRateRow(refresh) {
108
- if (!refresh) return '';
109
- const row = (refresh.perEpic ?? [])[0];
110
- if (!row) {
111
- return [
112
- '',
113
- `**Baseline refresh discipline (trailing ${refresh.windowDays}d):** no Story merges in window`,
114
- '',
115
- ].join('\n');
116
- }
117
- const pct = (row.cleanMergeRate * 100).toFixed(1);
118
- const target = row.cleanMergeRate >= 0.9 ? '✅' : '⚠️';
119
- return [
120
- '',
121
- `**Baseline refresh discipline (trailing ${refresh.windowDays}d):** ${target} ${pct}% clean-merge rate (${row.storyMerges - row.baselineRefreshes}/${row.storyMerges} Stories landed without follow-up refresh; ${row.baselineRefreshes} \`baseline-refresh:\` commit(s))`,
122
- '',
123
- ].join('\n');
124
- }
125
-
126
- export function renderEpicBody(payload, extras = {}) {
127
- const counts = payload.signalCounts ?? {};
128
- const countLine = Object.entries(counts)
129
- .map(([k, v]) => `${k}=${v}`)
130
- .join(', ');
131
- const hotspots = (payload.topHotspots ?? [])
132
- .map(
133
- (h) =>
134
- `- \`${h.phase}\` — ${h.occurrences} occurrence(s), avg ratio ${h.avgRatio.toFixed(2)}`,
135
- )
136
- .join('\n');
137
- const friction = (payload.mostFrictionStories ?? [])
138
- .map((s) => `- Story #${s.storyId}: ${s.frictionCount} friction signal(s)`)
139
- .join('\n');
140
- const lines = [
141
- `### Epic Perf Report — Epic #${payload.epicId}`,
142
- '',
143
- `Generated at: \`${payload.generatedAt}\``,
144
- '',
145
- `**Signal counts:** ${countLine.length > 0 ? countLine : 'none'}`,
146
- '',
147
- hotspots.length > 0
148
- ? `**Top hotspots:**\n${hotspots}`
149
- : '**Top hotspots:** none recorded',
150
- '',
151
- friction.length > 0
152
- ? `**Most-friction Stories:**\n${friction}`
153
- : '**Most-friction Stories:** none recorded',
154
- renderBaselineRefreshRateRow(extras.baselineRefreshRate),
155
- renderQualityGateFrictionBlock(extras.qualityGateFriction),
156
- '',
157
- '```json',
158
- JSON.stringify(payload, null, 2),
159
- '```',
160
- ];
161
- return lines.join('\n');
162
- }
163
-
164
- /**
165
- * Extract a story-perf-summary payload from a comment body. The body
166
- * begins with the structured marker line, followed by markdown prose,
167
- * followed by a fenced ```json``` block carrying the canonical payload.
168
- * We delegate the fence extraction to `parseFencedJsonComment` so the
169
- * fenced-JSON regex lives in exactly one place.
170
- *
171
- * Returns `null` when the marker is missing, no fence is found, or the
172
- * fence does not parse — the caller treats absence as "no signal" and
173
- * keeps walking.
174
- */
175
- export function extractStoryPerfSummaryFromComment(body) {
176
- if (typeof body !== 'string' || !body.includes(STORY_PERF_MARKER)) {
177
- return null;
178
- }
179
- const parsed = parseFencedJsonComment({ body });
180
- if (parsed && parsed.kind === 'story-perf-summary') return parsed;
181
- return null;
182
- }
@@ -1,128 +0,0 @@
1
- /**
2
- * audit-lens-routing.js — risk-envelope → audit-lens roster helpers.
3
- *
4
- * Extracted from `code-review.js` so the review runner stays focused on
5
- * executing a review pass, while callers that only need risk→lens mapping
6
- * (ceremony routing, plan-run epilogue consumers, unit tests) import a
7
- * smaller pure module.
8
- */
9
-
10
- import { selectAuditStrategy } from '../dynamic-workflow/capability.js';
11
-
12
- /**
13
- * The axes whose presence (at `high` risk) routes a specific post-delivery
14
- * audit lens. Mirrors the audit-workflow names under
15
- * `.agents/workflows/audit-*.md`. Story #3939 broadened the routing so every
16
- * high-risk REQUIRED axis maps to a lens rather than leaving three of them
17
- * (`data-migration`, `destructive-mutation`, `billing`) routing nothing:
18
- *
19
- * - `security` → `audit-security` (auth/secret boundary)
20
- * - `public-api` → `audit-architecture` (the canonical
21
- * architectural axis; a breaking public surface)
22
- * - `data-migration` → `audit-quality` (migration correctness +
23
- * regression coverage)
24
- * - `destructive-mutation` → `audit-security` (irreversible mutation is
25
- * an auth/abuse boundary; co-routes with
26
- * `security`, de-duplicated)
27
- * - `billing` → `audit-privacy` (money + the PII/consent
28
- * surface that travels with it)
29
- * - `critical-workflow` → `audit-quality` (the load-bearing path
30
- * warrants the deepest coverage pass)
31
- *
32
- * The `visible-behavior` axis intentionally routes NO lens: it forces the
33
- * acceptance-spec disposition at plan time and no audit lens maps cleanly to
34
- * it. Any other axis (or a low/medium-risk axis) contributes no lens.
35
- *
36
- * Every key here MUST be a value in the `axis` enum of
37
- * `.agents/schemas/risk-verdict.schema.json` — the verdict-derived envelope
38
- * can only ever carry schema-valid axes, so a key absent from that enum is
39
- * unreachable dead routing (Story #3889 removed the unreachable
40
- * `architecture` key; the architectural axis is `public-api`).
41
- */
42
- const AXIS_TO_LENS = Object.freeze({
43
- security: 'audit-security',
44
- 'public-api': 'audit-architecture',
45
- 'data-migration': 'audit-quality',
46
- 'destructive-mutation': 'audit-security',
47
- billing: 'audit-privacy',
48
- 'critical-workflow': 'audit-quality',
49
- });
50
-
51
- /**
52
- * Stable output order for routed lenses so the lens list is deterministic
53
- * regardless of axis ordering in the verdict. Every lens any axis can route
54
- * appears here exactly once; `resolveAuditLenses` filters this canonical
55
- * order down to the matched set (Story #3939).
56
- */
57
- const LENS_ORDER = Object.freeze([
58
- 'audit-security',
59
- 'audit-architecture',
60
- 'audit-quality',
61
- 'audit-privacy',
62
- ]);
63
-
64
- /**
65
- * Resolve the set of post-delivery audit lenses a judged risk envelope routes.
66
- *
67
- * High-risk axes map to their audit lens via {@link AXIS_TO_LENS}; only axes
68
- * judged `high` contribute (a `low`/`medium` axis carries no lens). The result
69
- * is de-duplicated and stably ordered by {@link LENS_ORDER} so an envelope
70
- * listing the `public-api` axis more than once routes `['audit-architecture']`
71
- * once, not twice — and two distinct axes routing the same lens (e.g.
72
- * `security` + `destructive-mutation` both → `audit-security`) collapse to a
73
- * single entry. A low-risk envelope — or any envelope with no high-risk routed
74
- * axis — resolves to an empty array (no lens beyond the existing baseline
75
- * gates).
76
- *
77
- * Pure function — no I/O, no side effects.
78
- *
79
- * @param {{ axes?: Array<{ axis?: string, level?: string }> }} [envelope]
80
- * @returns {string[]} Ordered, de-duplicated audit-lens identifiers.
81
- */
82
- export function resolveAuditLenses(envelope = {}) {
83
- const axes = Array.isArray(envelope?.axes) ? envelope.axes : [];
84
- const matched = new Set();
85
- for (const entry of axes) {
86
- if (!entry || entry.level !== 'high') continue;
87
- const lens = AXIS_TO_LENS[entry.axis];
88
- if (lens) matched.add(lens);
89
- }
90
- return LENS_ORDER.filter((lens) => matched.has(lens));
91
- }
92
-
93
- /**
94
- * Build the post-delivery audit-lens execution plan for a judged risk
95
- * envelope. Each routed lens (see {@link resolveAuditLenses}) is paired with a
96
- * strategy decision from the **existing** `selectAuditStrategy` engine — no new
97
- * audit machinery is introduced. A low-risk envelope resolves to an empty
98
- * `lenses` array and runs no audit beyond the baseline gates (Story #3876).
99
- *
100
- * Pure with respect to the injected `selectAuditStrategyFn` (default is the
101
- * shared dynamic-workflow engine, which is itself pure over its snapshot).
102
- *
103
- * @param {{ axes?: Array<{ axis?: string, level?: string }> }} [envelope]
104
- * @param {{
105
- * snapshot?: object,
106
- * forceStrategy?: ('orchestrated'|'sequential'|null),
107
- * selectAuditStrategyFn?: typeof selectAuditStrategy,
108
- * }} [opts]
109
- * @returns {{ lenses: string[], plan: Array<{ lens: string, strategy: string, reason: string, forced: boolean }> }}
110
- */
111
- export function planAuditLenses(envelope = {}, opts = {}) {
112
- const {
113
- snapshot = {},
114
- forceStrategy = null,
115
- selectAuditStrategyFn = selectAuditStrategy,
116
- } = opts;
117
- const lenses = resolveAuditLenses(envelope);
118
- const plan = lenses.map((lens) => {
119
- const decision = selectAuditStrategyFn({ snapshot, forceStrategy });
120
- return {
121
- lens,
122
- strategy: decision.strategy,
123
- reason: decision.reason,
124
- forced: decision.forced,
125
- };
126
- });
127
- return { lenses, plan };
128
- }