mandrel 2.30.0 → 2.32.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 (253) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. package/.agents/workflows/helpers/signals.md +0 -112
@@ -67,7 +67,6 @@ const FRAMEWORK_RUNTIME_DEPS = [
67
67
  'js-yaml',
68
68
  'minimatch',
69
69
  'picomatch',
70
- 'string-argv',
71
70
  'typhonjs-escomplex',
72
71
  ];
73
72
 
@@ -112,17 +112,6 @@ export class ITicketingProvider {
112
112
  throw new Error('Not implemented: getTicketDependencies');
113
113
  }
114
114
 
115
- /**
116
- * Fetch recent comments across the repository.
117
- * Useful for auditing and visualization of agent telemetry.
118
- *
119
- * @param {number} [limit=100]
120
- * @returns {Promise<object[]>}
121
- */
122
- async getRecentComments(_limit = 100) {
123
- throw new Error('Not implemented: getRecentComments');
124
- }
125
-
126
115
  /**
127
116
  * Fetch all comments for a specific ticket.
128
117
  *
@@ -137,42 +126,6 @@ export class ITicketingProvider {
137
126
  // Write Operations
138
127
  // ---------------------------------------------------------------------------
139
128
 
140
- /**
141
- * Create a child ticket within an Epic's structural hierarchy.
142
- *
143
- * @param {number} parentId - GitHub Issue number of the immediate structural parent (e.g. Epic or Story).
144
- * @param {{
145
- * epicId: number,
146
- * title: string,
147
- * body: string,
148
- * labels: string[],
149
- * dependencies: number[]
150
- * }} ticketData - The ticket content and metadata.
151
- * @returns {Promise<{ id: number, url: string }>}
152
- */
153
-
154
- /**
155
- * Link an existing issue as a sub-issue of a parent.
156
- *
157
- * @param {number} parentId - GitHub Issue number of the parent.
158
- * @param {number} childId - GitHub internal database ID of the sub-issue.
159
- * @returns {Promise<void>}
160
- */
161
- async addSubIssue(_parentId, _childId) {
162
- throw new Error('Not implemented: addSubIssue');
163
- }
164
-
165
- /**
166
- * Remove a sub-issue link from a parent.
167
- *
168
- * @param {number} parentId - GitHub Issue number of the parent.
169
- * @param {number} childId - GitHub internal database ID of the sub-issue.
170
- * @returns {Promise<void>}
171
- */
172
- async removeSubIssue(_parentId, _childId) {
173
- throw new Error('Not implemented: removeSubIssue');
174
- }
175
-
176
129
  /**
177
130
  * Mutate labels, body (tasklist checkboxes), and assignees on a ticket.
178
131
  *
@@ -213,17 +166,6 @@ export class ITicketingProvider {
213
166
  throw new Error('Not implemented: deleteComment');
214
167
  }
215
168
 
216
- /**
217
- * Open a Pull Request linking the specified ticket.
218
- *
219
- * @param {string} branchName - The source branch for the PR.
220
- * @param {number} ticketId - GitHub Issue number to link.
221
- * @returns {Promise<{ number: number, url: string, htmlUrl: string }>}
222
- */
223
- async createPullRequest(_branchName, _ticketId) {
224
- throw new Error('Not implemented: createPullRequest');
225
- }
226
-
227
169
  // ---------------------------------------------------------------------------
228
170
  // Setup Operations (used by bootstrap)
229
171
  // ---------------------------------------------------------------------------
@@ -16,25 +16,25 @@
16
16
  * @module lib/audit-baselines/staleness
17
17
  */
18
18
 
19
- import { execFileSync } from 'node:child_process';
19
+ import { execFileCapture } from '../child-exec.js';
20
20
  import { KIND_SPECS } from './kinds.js';
21
21
  import { ageInDays } from './read.js';
22
22
 
23
23
  /**
24
24
  * Run a git command under `cwd`, returning trimmed stdout, or `null` on any
25
- * failure or empty result.
25
+ * failure or empty result. Buffer and shell policy come from the shared
26
+ * child-process surface ([`child-exec.js`](../child-exec.js)).
26
27
  *
27
28
  * @param {string[]} args
28
29
  * @param {{ cwd: string, run?: Function }} io
29
30
  * @returns {string | null}
30
31
  */
31
- function git(args, { cwd, run = execFileSync }) {
32
+ function git(args, { cwd, run }) {
32
33
  try {
33
34
  const stdout = String(
34
- run('git', args, {
35
+ execFileCapture('git', args, {
36
+ run,
35
37
  cwd,
36
- encoding: 'utf8',
37
- maxBuffer: 16 * 1024 * 1024,
38
38
  stdio: ['ignore', 'pipe', 'ignore'],
39
39
  }),
40
40
  ).trim();
@@ -16,7 +16,7 @@
16
16
  * @module lib/audit-baselines/trend
17
17
  */
18
18
 
19
- import { execFileSync } from 'node:child_process';
19
+ import { execFileCapture } from '../child-exec.js';
20
20
  import { trendRollupOf } from './kinds.js';
21
21
 
22
22
  /**
@@ -25,13 +25,13 @@ import { trendRollupOf } from './kinds.js';
25
25
  * @param {{ cwd: string, relPath: string, limit: number, run?: Function }} args
26
26
  * @returns {Array<{ sha: string, committedAt: string }>}
27
27
  */
28
- function listCommits({ cwd, relPath, limit, run = execFileSync }) {
28
+ function listCommits({ cwd, relPath, limit, run }) {
29
29
  let stdout;
30
30
  try {
31
- stdout = run(
31
+ stdout = execFileCapture(
32
32
  'git',
33
33
  ['log', `-n${limit}`, '--format=%H %cI', '--', relPath],
34
- { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
34
+ { run, cwd, stdio: ['ignore', 'pipe', 'ignore'] },
35
35
  );
36
36
  } catch {
37
37
  return [];
@@ -52,13 +52,12 @@ function listCommits({ cwd, relPath, limit, run = execFileSync }) {
52
52
  * @param {{ cwd: string, kind: string, sha: string, relPath: string, run?: Function }} args
53
53
  * @returns {object | null}
54
54
  */
55
- function rollupAt({ cwd, kind, sha, relPath, run = execFileSync }) {
55
+ function rollupAt({ cwd, kind, sha, relPath, run }) {
56
56
  let stdout;
57
57
  try {
58
- stdout = run('git', ['show', `${sha}:${relPath}`], {
58
+ stdout = execFileCapture('git', ['show', `${sha}:${relPath}`], {
59
+ run,
59
60
  cwd,
60
- encoding: 'utf8',
61
- maxBuffer: 64 * 1024 * 1024,
62
61
  stdio: ['ignore', 'pipe', 'ignore'],
63
62
  });
64
63
  } catch {
@@ -16,9 +16,9 @@
16
16
  * @module lib/audit-baselines/weights
17
17
  */
18
18
 
19
- import { execFileSync } from 'node:child_process';
20
19
  import fs from 'node:fs';
21
20
  import path from 'node:path';
21
+ import { execFileCapture } from '../child-exec.js';
22
22
  import { computeInDegree, resolveRepoGraph } from '../import-graph.js';
23
23
 
24
24
  /** Neutral multiplier every degraded weight collapses to. */
@@ -47,10 +47,10 @@ function saturate(count, half) {
47
47
  * `degraded` is true when git could not answer at all — not a git work
48
48
  * tree, no commits yet, or the binary is unavailable.
49
49
  */
50
- export function readChurn({ cwd, windowDays = 180, run = execFileSync }) {
50
+ export function readChurn({ cwd, windowDays = 180, run }) {
51
51
  let stdout;
52
52
  try {
53
- stdout = run(
53
+ stdout = execFileCapture(
54
54
  'git',
55
55
  [
56
56
  'log',
@@ -60,9 +60,8 @@ export function readChurn({ cwd, windowDays = 180, run = execFileSync }) {
60
60
  '--no-renames',
61
61
  ],
62
62
  {
63
+ run,
63
64
  cwd,
64
- encoding: 'utf8',
65
- maxBuffer: 64 * 1024 * 1024,
66
65
  // git narrates "not a git repository" on stderr; the degradation is
67
66
  // reported in the envelope, not shouted at the operator.
68
67
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -41,7 +41,7 @@ import path from 'node:path';
41
41
  import { AUDIT_LENSES } from '../audit-to-stories/audit-lenses.js';
42
42
  import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
43
43
  import { Logger } from '../Logger.js';
44
- import { estimateTokens } from '../orchestration/context-envelope.js';
44
+ import { estimateTokens } from '../orchestration/spec-spill.js';
45
45
  import {
46
46
  changeSetLacksSiblingTest,
47
47
  matchesAnyFilePattern,
@@ -249,7 +249,6 @@ export function buildStoryBody({ group, edges = [] }) {
249
249
  wide: null,
250
250
  reason_to_exist: null,
251
251
  depends_on: [],
252
- estimated_test_files: null,
253
252
  };
254
253
 
255
254
  // Serialize via the canonical serializer (no footer — depends_on is empty).
@@ -40,46 +40,26 @@
40
40
  * @module lib/baselines/envelope
41
41
  */
42
42
 
43
- import fs from 'node:fs';
44
- import path from 'node:path';
45
- import { fileURLToPath } from 'node:url';
46
- import Ajv from 'ajv';
47
- import addFormats from 'ajv-formats';
48
-
49
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
43
+ import {
44
+ BASELINE_KIND_SCHEMA_FILES,
45
+ buildBaselineSchemaAjv,
46
+ } from '../baseline-schema-registry.js';
50
47
 
51
48
  /**
52
- * Repo-relative `.agents/schemas/baselines/` directory. Resolved off the
53
- * module URL so the envelope works the same in the main checkout, in a
54
- * worktree, and inside CI's bare clone — none of those have a stable
55
- * `process.cwd()` relative to the schemas.
49
+ * Canonical list of kinds the shared envelope supports, **derived** from the
50
+ * schema registry rather than restated (Story #5002).
51
+ *
52
+ * It used to be a hand-kept literal that happened to mirror
53
+ * `BASELINE_KIND_SCHEMA_FILES` name for name. Deriving it makes
54
+ * "`kind` is known" and "`kind`'s schema is registered" the same fact, so
55
+ * `getValidator` below cannot be handed a kind the AJV instance never
56
+ * compiled — the not-registered branch is gone because the state is
57
+ * unreachable, not because it was hidden.
56
58
  */
57
- const SCHEMAS_DIR = path.resolve(
58
- __dirname,
59
- '..',
60
- '..',
61
- '..',
62
- 'schemas',
63
- 'baselines',
59
+ export const KNOWN_KINDS = Object.freeze(
60
+ BASELINE_KIND_SCHEMA_FILES.map((file) => file.replace(/\.schema\.json$/, '')),
64
61
  );
65
62
 
66
- /**
67
- * Canonical list of kinds the shared envelope supports. The writer's
68
- * per-kind module list lives in `kinds/index.js`; this constant is the
69
- * envelope's view of the same set, kept here to break the import cycle
70
- * (`kinds/<kind>` imports envelope; envelope only needs the names).
71
- */
72
- export const KNOWN_KINDS = Object.freeze([
73
- 'lint',
74
- 'coverage',
75
- 'crap',
76
- 'maintainability',
77
- 'mutation',
78
- 'lighthouse',
79
- 'bundle-size',
80
- 'duplication',
81
- ]);
82
-
83
63
  function schemaRefFor(kind) {
84
64
  return `.agents/schemas/baselines/${kind}.schema.json`;
85
65
  }
@@ -193,36 +173,37 @@ export function buildEnvelope({
193
173
  }
194
174
 
195
175
  /**
196
- * Lazy AJV instance compiled schemas are memoised so successive writes
197
- * during a single Node process don't re-compile the seven schemas.
176
+ * Lazy AJV instance, built by the canonical
177
+ * [`baseline-schema-registry`](../baseline-schema-registry.js) the ONE
178
+ * place a baselines-directory schema is read off disk and compiled
179
+ * (Story #5002). This module used to keep its own `SCHEMAS_DIR` + AJV +
180
+ * per-kind compile cache alongside the registry's, so the writer and
181
+ * `baselines/reader.js` validated the same envelope through two independently
182
+ * configured instances; a registration added to one was invisible to the
183
+ * other. One builder means the writer and the reader cannot disagree about
184
+ * what a valid envelope is.
185
+ *
186
+ * Memoised: building it reads eleven schema files, and a single baseline
187
+ * regeneration calls `assertEnvelope` once per write plus once per re-read.
198
188
  */
199
189
  let _ajv = null;
200
- const _validators = new Map();
201
-
202
- function getAjv() {
203
- if (_ajv) return _ajv;
204
- const ajv = new Ajv({ allErrors: true, strict: false });
205
- addFormats(ajv);
206
- const envelopeSchema = JSON.parse(
207
- fs.readFileSync(
208
- path.join(SCHEMAS_DIR, 'baseline-envelope.schema.json'),
209
- 'utf8',
210
- ),
211
- );
212
- ajv.addSchema(envelopeSchema, 'baseline-envelope.schema.json');
213
- _ajv = ajv;
214
- return ajv;
190
+ function ajv() {
191
+ if (_ajv === null) {
192
+ _ajv = buildBaselineSchemaAjv();
193
+ }
194
+ return _ajv;
215
195
  }
216
196
 
197
+ /**
198
+ * Look up the pre-registered validator for a per-kind schema. Total over
199
+ * `KNOWN_KINDS` — that list is derived from the registry's own filenames, so
200
+ * every kind reaching here has a compiled schema.
201
+ *
202
+ * @param {string} kind
203
+ * @returns {import('ajv').ValidateFunction}
204
+ */
217
205
  function getValidator(kind) {
218
- if (_validators.has(kind)) return _validators.get(kind);
219
- const ajv = getAjv();
220
- const schema = JSON.parse(
221
- fs.readFileSync(path.join(SCHEMAS_DIR, `${kind}.schema.json`), 'utf8'),
222
- );
223
- const validate = ajv.compile(schema);
224
- _validators.set(kind, validate);
225
- return validate;
206
+ return ajv().getSchema(`${kind}.schema.json`);
226
207
  }
227
208
 
228
209
  /**
@@ -22,6 +22,13 @@
22
22
  // `ref` and `file` are passed as separate argv tokens, never
23
23
  // interpolated into a shell command.
24
24
  //
25
+ // Story #5009 moved the spawn itself, the stdout ceiling and the
26
+ // failure-message shape out to `lib/child-exec.js` — the one child-process
27
+ // surface — so this module now owns only what is genuinely its own: the
28
+ // `(ref, file)` LRU, the exit-code semantics below, and the test seam. The
29
+ // `__setSpawnRunner` seam still injects a mock runner; it is threaded through
30
+ // the shared wrapper as `run` rather than calling `spawnSync` directly.
31
+ //
25
32
  // Exit semantics are intentionally narrow:
26
33
  //
27
34
  // - Exit 0: stdout is the file contents, returned as a UTF-8 string.
@@ -39,7 +46,7 @@
39
46
  // means "path absent at ref". Callers may therefore treat a throw as a
40
47
  // hard failure without having to re-diagnose it.
41
48
 
42
- import { spawnSync } from 'node:child_process';
49
+ import { formatChildFailure, spawnChild } from '../child-exec.js';
43
50
 
44
51
  // ---------------------------------------------------------------------------
45
52
  // LRU cache. Small footprint — even a fan-out across all seven baseline
@@ -53,21 +60,13 @@ import { spawnSync } from 'node:child_process';
53
60
  const DEFAULT_MAX_ENTRIES = 64;
54
61
 
55
62
  /**
56
- * Story #4914 explicit stdout ceiling for every git read in this module.
57
- *
58
- * `child_process.spawnSync` defaults `maxBuffer` to 1 MB. A committed
59
- * baseline legitimately grows past that (a real consumer's crap baseline
60
- * measured 1,178,910 bytes), at which point the child is killed —
61
- * `status: null`, `signal: 'SIGTERM'`, `error.code: 'ENOBUFS'` — and the
62
- * read fails for a reason that has nothing to do with the repository.
63
+ * Injected spawn runner, or `undefined` to use the shared surface's real one.
64
+ * Story #4914's explicit 64 MB ceiling still applies to both git reads below —
65
+ * it is now `child-exec.js`'s `MAX_BUFFER_BYTES`, applied by `spawnChild`.
63
66
  *
64
- * 64 MB is not a new number: it is the bound already used at
65
- * `run-test-profile.js:87`, `audit-baselines/trend.js:61` and
66
- * `audit-baselines/weights.js:65`. This module was the outlier.
67
+ * @type {Function | undefined}
67
68
  */
68
- const MAX_BUFFER_BYTES = 64 * 1024 * 1024;
69
-
70
- let _spawnSync = spawnSync;
69
+ let _spawnRunner;
71
70
  let _cache = new Map();
72
71
  let _maxEntries = DEFAULT_MAX_ENTRIES;
73
72
 
@@ -80,12 +79,12 @@ let _maxEntries = DEFAULT_MAX_ENTRIES;
80
79
  * the contract.
81
80
  *
82
81
  * @param {object} [opts]
83
- * @param {typeof spawnSync} [opts.spawn] - Mock spawnSync.
84
- * @param {number} [opts.maxEntries] - Override LRU capacity.
82
+ * @param {Function} [opts.spawn] - Mock spawnSync.
83
+ * @param {number} [opts.maxEntries] - Override LRU capacity.
85
84
  */
86
85
  export function __setSpawnRunner(opts = {}) {
87
86
  if (opts.spawn) {
88
- _spawnSync = opts.spawn;
87
+ _spawnRunner = opts.spawn;
89
88
  }
90
89
  if (typeof opts.maxEntries === 'number') {
91
90
  _maxEntries = opts.maxEntries;
@@ -98,7 +97,7 @@ export function __setSpawnRunner(opts = {}) {
98
97
  * to keep state from leaking across files.
99
98
  */
100
99
  export function __resetForTests() {
101
- _spawnSync = spawnSync;
100
+ _spawnRunner = undefined;
102
101
  _maxEntries = DEFAULT_MAX_ENTRIES;
103
102
  _cache = new Map();
104
103
  }
@@ -179,13 +178,10 @@ export function readBaseFromGit(ref, file, opts = {}) {
179
178
 
180
179
  const cwd = opts.cwd ?? process.cwd();
181
180
  const spec = `${ref}:${file}`;
182
- const result = _spawnSync('git', ['show', spec], {
181
+ const result = spawnChild('git', ['show', spec], {
182
+ run: _spawnRunner,
183
183
  cwd,
184
- stdio: 'pipe',
185
- encoding: 'utf-8',
186
- shell: false,
187
184
  env: cleanGitEnv(),
188
- maxBuffer: MAX_BUFFER_BYTES,
189
185
  });
190
186
 
191
187
  // `child_process.spawnSync` returns `status: null` when the child died
@@ -206,9 +202,12 @@ export function readBaseFromGit(ref, file, opts = {}) {
206
202
  return null;
207
203
  }
208
204
 
209
- const stderr = (result.stderr || '').toString().trim();
210
205
  throw new Error(
211
- `readBaseFromGit: git show ${spec} failed (status=${status}): ${stderr}`,
206
+ formatChildFailure({
207
+ label: `readBaseFromGit: git show ${spec}`,
208
+ status,
209
+ stderr: result.stderr,
210
+ }),
212
211
  );
213
212
  }
214
213
 
@@ -222,9 +221,10 @@ export function readBaseFromGit(ref, file, opts = {}) {
222
221
  * Restricting the log to `-- <file>` means the returned subjects already
223
222
  * satisfy the "commit whose diff touches the baseline file" half of the
224
223
  * predicate; the caller only has to match the tag substring against each
225
- * subject. Runs through the same `spawnSync` seam as `readBaseFromGit`, so
226
- * it inherits the identical env scrubbing (drop inherited `GIT_*`) and the
227
- * spawn-not-exec security posture (argv tokens, `shell: false`).
224
+ * subject. Runs through the same shared `spawnChild` surface and injected
225
+ * runner as `readBaseFromGit`, so it inherits the identical env scrubbing
226
+ * (drop inherited `GIT_*`), the same stdout ceiling, and the spawn-not-exec
227
+ * security posture (argv tokens, `shell: false`).
228
228
  *
229
229
  * Returns an empty array whenever the range cannot be walked (missing base
230
230
  * ref, git failure, empty range) — the acknowledgment path treats "no
@@ -244,17 +244,10 @@ export function readRangeSubjectsTouchingFile(baseRef, file, opts = {}) {
244
244
  const cwd = opts.cwd ?? process.cwd();
245
245
  let result;
246
246
  try {
247
- result = _spawnSync(
247
+ result = spawnChild(
248
248
  'git',
249
249
  ['log', `${baseRef}..HEAD`, '--format=%s', '--', file],
250
- {
251
- cwd,
252
- stdio: 'pipe',
253
- encoding: 'utf-8',
254
- shell: false,
255
- env: cleanGitEnv(),
256
- maxBuffer: MAX_BUFFER_BYTES,
257
- },
250
+ { run: _spawnRunner, cwd, env: cleanGitEnv() },
258
251
  );
259
252
  } catch {
260
253
  return [];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * _crap-new-method-gate.js — the policy `kinds/crap.js#compareCrap` applies to
3
+ * a method that has no baseline row (Story #5002).
4
+ *
5
+ * Underscore-prefixed like `_shared-metric.js`: this is a helper for the
6
+ * per-kind modules in this directory, not a kind of its own.
7
+ *
8
+ * A leaf — it imports nothing, so `kinds/crap.js` can consume it without
9
+ * adding an edge to anything the comparator does not already reach.
10
+ */
11
+
12
+ /**
13
+ * The set of scanned files in which **every** measured method resolved 0%
14
+ * coverage — the observable signature of a source no test loads at all.
15
+ *
16
+ * Deliberately derived from the scan rows rather than plumbed down from the
17
+ * coverage artifact: the rows are the only thing both the preview gate and the
18
+ * close-validation projection hand the comparator, and "the whole file is at
19
+ * zero" is exactly what an all-zero istanbul entry produces here.
20
+ *
21
+ * A row carrying `coverage: null` is skipped, not counted as a zero — an
22
+ * absent observation is not a measured one, and letting it vote would hand the
23
+ * relief below to a file nothing measured at all.
24
+ *
25
+ * @param {Array<{file: string, coverage?: number|null}>} currentRows
26
+ * @returns {Set<string>}
27
+ */
28
+ export function deriveUncoveredFiles(currentRows) {
29
+ const allZeroByFile = new Map();
30
+ for (const row of currentRows ?? []) {
31
+ if (typeof row?.coverage !== 'number') continue;
32
+ const soFar = allZeroByFile.get(row.file);
33
+ allZeroByFile.set(row.file, (soFar ?? true) && row.coverage === 0);
34
+ }
35
+ const uncovered = new Set();
36
+ for (const [file, allZero] of allZeroByFile) {
37
+ if (allZero) uncovered.add(file);
38
+ }
39
+ return uncovered;
40
+ }
41
+
42
+ /**
43
+ * The score a **new** method is judged against `newMethodCeiling` with.
44
+ *
45
+ * Normally the measured CRAP. In a file whose every method scored 0%, the
46
+ * complexity term `c` alone.
47
+ *
48
+ * **Why.** `crap = c²·(1 − cov)³ + c` collapses to `c² + c` at zero coverage,
49
+ * so the default ceiling of 30 caps a *brand-new* method in an untested file
50
+ * at `c ≈ 5`. For the code that is untestable by construction — argv parsing,
51
+ * `spawn` wiring, a CLI's top-level `main()` — that is not a quality signal
52
+ * but a fragmentation tax: the only way past it is to shatter cohesive wiring
53
+ * into sub-five-branch fragments, which raises the file's method count and its
54
+ * reader's cost while measuring nothing. The coverage term still governs every
55
+ * method in a file the tests *do* reach, and a genuinely sprawling function is
56
+ * still refused — `c` alone must clear the ceiling.
57
+ *
58
+ * This is a **gate** decision, not a scoring one: the persisted baseline row
59
+ * keeps its measured `c² + c`, so the regression arm on the next run compares
60
+ * like with like.
61
+ *
62
+ * @param {{file: string, crap: number, cyclomatic?: number}} row
63
+ * @param {Set<string>} uncoveredFiles
64
+ * @returns {number}
65
+ */
66
+ export function newMethodGateScore(row, uncoveredFiles) {
67
+ if (!uncoveredFiles.has(row.file)) return row.crap;
68
+ return Number.isFinite(row.cyclomatic) ? row.cyclomatic : row.crap;
69
+ }
70
+
71
+ /**
72
+ * Assemble a new-method violation.
73
+ *
74
+ * `gateScore` is written **only** when the ceiling was judged against
75
+ * something other than the measured `crap` — the same write-only-when-
76
+ * non-default idiom `projectRow` uses for its row markers — so the printer can
77
+ * name the number that actually failed instead of one nothing tested.
78
+ *
79
+ * @param {object} row
80
+ * @param {number} ceiling
81
+ * @param {number} gateScore
82
+ * @returns {object}
83
+ */
84
+ export function buildNewViolation(row, ceiling, gateScore) {
85
+ return {
86
+ ...row,
87
+ kind: 'new',
88
+ baseline: null,
89
+ ceiling,
90
+ ...(gateScore === row.crap ? {} : { gateScore }),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Render the measured-vs-gated half of a new-method violation line.
96
+ *
97
+ * @param {{crap: number, gateScore?: number}} v
98
+ * @returns {string}
99
+ */
100
+ export function formatNewViolationMeasure(v) {
101
+ if (v.gateScore === undefined) return `crap=${v.crap.toFixed(2)}`;
102
+ return `complexity=${v.gateScore} (file has no coverage; crap=${v.crap.toFixed(2)})`;
103
+ }