mandrel 2.31.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 (250) 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/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -393,12 +393,13 @@ async function emitBlockedFriction(ticketId, fromState, newState, opts) {
393
393
  * `agent::done` at story-close (after the merge lands), not when the
394
394
  * last Task commit landed on the still-unmerged Story branch. The
395
395
  * parameter is preserved for callers that still suppress cascade
396
- * explicitly (e.g. batch-transition helpers).
396
+ * explicitly (`single-story-init.js`, and `bulk.js` where the upward
397
+ * recursion is driven by hand instead).
397
398
  *
398
399
  * `ticketSnapshot` (Story #1795 / Epic #1788) is an optional pre-fetched
399
- * ticket object. When the caller already holds the ticket (e.g.
400
- * `batchTransitionTickets`, which loops over a list it just hydrated),
401
- * passing the snapshot eliminates the two `getTicket` round-trips that
400
+ * ticket object. When the caller already holds the ticket e.g. it
401
+ * hydrated the ticket to inspect its labels first — passing the
402
+ * snapshot eliminates the two `getTicket` round-trips that
402
403
  * `transitionTicketState` would otherwise issue — one for the notify
403
404
  * `fromState` lookup and one inside `provider.updateTicket`'s label
404
405
  * merge path. Backwards compatible: when omitted, behaviour is unchanged.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * verify-tier-repair.js — repair-before-judging for the `verify[]` tier suffix
3
+ * (Story #5005).
4
+ *
5
+ * The tier suffix (`… (unit)`) is a deterministic, mechanically-derivable
6
+ * formality. `task-body-validator.js` already computed the corrected entry in
7
+ * order to print it as a "Suggested fix" — then rejected the plan anyway and
8
+ * charged the author a full re-drafting round to paste that exact string back.
9
+ * This module applies the inference the validator already trusts, so the hard
10
+ * error is reserved for the entries only the author can resolve.
11
+ *
12
+ * It lives beside the validator rather than inside it because the validator's
13
+ * job is to *judge*: mixing a mutating repair pass into a module of pure
14
+ * collectors muddies both. `persist-helpers.js#validateTickets` calls this
15
+ * first, then `validateTaskBodies`.
16
+ *
17
+ * @module lib/orchestration/verify-tier-repair
18
+ */
19
+
20
+ import { suggestVerifyFix } from '../story-body/body-format-lints.js';
21
+ import {
22
+ parse as parseStoryBody,
23
+ serialize as serializeStoryBody,
24
+ } from '../story-body/story-body.js';
25
+ import { VERIFY_TIER_VALUES } from './task-body-validator.js';
26
+
27
+ /** A parenthesised tier drawn from the canonical vocabulary, at end of entry. */
28
+ const VERIFY_TIER_RE = new RegExp(
29
+ `\\((?:${VERIFY_TIER_VALUES.join('|')})\\)\\s*$`,
30
+ );
31
+
32
+ /**
33
+ * Rewrite one `verify[]` list, appending the inferable testing tier to any
34
+ * entry missing one. Returns `null` when nothing changed, so callers can keep
35
+ * an already-compliant list byte-identical (and skip re-serializing the body
36
+ * it came from).
37
+ *
38
+ * @param {unknown} rawVerify
39
+ * @returns {string[]|null} The corrected list, or `null` when no fix applied.
40
+ */
41
+ function repairVerifyList(rawVerify) {
42
+ if (!Array.isArray(rawVerify)) return null;
43
+ let changed = false;
44
+ const next = rawVerify.map((entry) => {
45
+ if (typeof entry !== 'string') return entry;
46
+ if (entry.startsWith('manual:') || VERIFY_TIER_RE.test(entry)) return entry;
47
+ const fix = suggestVerifyFix(entry);
48
+ if (fix === null) return entry;
49
+ changed = true;
50
+ return fix;
51
+ });
52
+ return changed ? next : null;
53
+ }
54
+
55
+ /**
56
+ * Repair the `## Verify` section of a serialized (string) body, in place on
57
+ * the ticket. Only re-serializes when a fix actually applied, so a compliant
58
+ * body is never round-tripped through `parse → serialize`.
59
+ *
60
+ * Leaving the body untouched while the top level was repaired would be worse
61
+ * than not repairing at all: `syncContractFieldFromTopLevel` fails closed on a
62
+ * body section that disagrees with its top-level array, so both sides must be
63
+ * repaired by the same rule or neither.
64
+ *
65
+ * @param {object} ticket
66
+ */
67
+ function repairStringBodyVerify(ticket) {
68
+ let parsed;
69
+ try {
70
+ parsed = parseStoryBody(ticket.body).body;
71
+ } catch {
72
+ // An unparseable body is already a hard error in `validateTaskBodyShape`;
73
+ // there is nothing to repair and nothing to report twice.
74
+ return;
75
+ }
76
+ const repaired = repairVerifyList(parsed.verify);
77
+ if (repaired === null) return;
78
+ ticket.body = serializeStoryBody({ ...parsed, verify: repaired });
79
+ }
80
+
81
+ /**
82
+ * Auto-append the testing tier to `verify[]` entries that omit one, wherever
83
+ * `suggestVerifyFix` can infer it from the command. An entry whose tier cannot
84
+ * be inferred is left untouched and still hard-errors downstream.
85
+ *
86
+ * Mutates `tickets` in place (the persist pipeline threads this same array on
87
+ * to assembly) and returns it. Already-compliant tickets are untouched, so
88
+ * their persisted output stays byte-identical. Total — a non-array argument
89
+ * and non-Story tickets are no-ops.
90
+ *
91
+ * @param {object[]} tickets
92
+ * @returns {object[]} `tickets`
93
+ */
94
+ export function normalizeVerifyTiers(tickets) {
95
+ for (const ticket of Array.isArray(tickets) ? tickets : []) {
96
+ if (!ticket || ticket.type !== 'story' || ticket.body == null) continue;
97
+ const topLevel = repairVerifyList(ticket.verify);
98
+ if (topLevel !== null) ticket.verify = topLevel;
99
+ if (typeof ticket.body === 'string') {
100
+ repairStringBodyVerify(ticket);
101
+ continue;
102
+ }
103
+ const bodyLevel = repairVerifyList(ticket.body.verify);
104
+ if (bodyLevel !== null) ticket.body.verify = bodyLevel;
105
+ }
106
+ return tickets;
107
+ }
@@ -34,17 +34,12 @@
34
34
  // operator-facing string explaining the verdict (always populated,
35
35
  // including for present tiers).
36
36
  //
37
- // acceptanceMatrix(criteria) -> {
38
- // tiers: TIERS,
39
- // rows: [{ id, label, verdict }, ...],
40
- // }
41
- //
42
- // Maps each acceptance criterion to its per-tier `coverageVerdict`, giving
43
- // the AC × test-tier matrix the markdown report (lib/qa/coverage-report.js)
44
- // renders.
37
+ // Story #5008 removed the `acceptanceMatrix` feeder and the markdown report it
38
+ // fed: the QA workflows read the per-tier verdict and author the missing-test
39
+ // prose directly, so the AC x tier matrix was a round-trip with no reader.
45
40
 
46
- /** The three test tiers, in pyramid order (base top). */
47
- export const TIERS = Object.freeze(['unit', 'contract', 'acceptance']);
41
+ /** The three test tiers, in pyramid order (base -> top). */
42
+ const TIERS = Object.freeze(['unit', 'contract', 'acceptance']);
48
43
 
49
44
  const PRESENT = 'present';
50
45
  const ABSENT = 'absent';
@@ -217,80 +212,3 @@ export function coverageVerdict(surface = {}) {
217
212
 
218
213
  return verdict;
219
214
  }
220
-
221
- /**
222
- * Normalize a single criterion descriptor into `{id, label, surface}`. A
223
- * descriptor may carry `id`, `label` (falls back to id), and either a nested
224
- * `surface` or a flat `{symbol, tests}` shape.
225
- */
226
- function normalizeOne(entry, index) {
227
- const e = entry && typeof entry === 'object' ? entry : {};
228
- const id =
229
- typeof e.id === 'string' && e.id.trim() !== ''
230
- ? e.id.trim()
231
- : `AC-${index + 1}`;
232
- const label =
233
- typeof e.label === 'string' && e.label.trim() !== '' ? e.label.trim() : id;
234
-
235
- let surface;
236
- if (e.surface && typeof e.surface === 'object') {
237
- surface = e.surface;
238
- } else if (Array.isArray(e.tests) || typeof e.symbol === 'string') {
239
- surface = { symbol: e.symbol, tests: e.tests };
240
- } else {
241
- surface = {};
242
- }
243
-
244
- return { id, label, surface };
245
- }
246
-
247
- /**
248
- * Normalize the criteria input for {@link acceptanceMatrix}. Accepts either an
249
- * array of criterion descriptors or a plain object keyed by criterion id whose
250
- * values are surfaces (or `{surface}` wrappers). Returns a normalized array of
251
- * `{id, label, surface}`.
252
- *
253
- * @param {Array<object>|object} criteria
254
- * @returns {Array<{id:string,label:string,surface:object}>}
255
- */
256
- function normalizeCriteria(criteria) {
257
- if (Array.isArray(criteria)) {
258
- return criteria.map((entry, index) => normalizeOne(entry, index));
259
- }
260
- if (criteria && typeof criteria === 'object') {
261
- return Object.entries(criteria).map(([id, value], index) =>
262
- normalizeOne(
263
- value && typeof value === 'object' && !Array.isArray(value)
264
- ? { id, ...value }
265
- : { id, surface: { tests: value } },
266
- index,
267
- ),
268
- );
269
- }
270
- throw new TypeError(
271
- 'acceptanceMatrix: criteria must be an array or an object',
272
- );
273
- }
274
-
275
- /**
276
- * Build the AC × test-tier matrix: for each acceptance criterion, the per-tier
277
- * {@link coverageVerdict}. This is the structured shape the markdown report
278
- * (`lib/qa/coverage-report.js`) renders into a table.
279
- *
280
- * @param {Array<{id?:string,label?:string,surface?:object,symbol?:string,tests?:Array}>|Record<string,object>} criteria
281
- * Acceptance criteria, either as an array of descriptors or an object keyed
282
- * by criterion id. Each descriptor names a surface (nested `surface`, or a
283
- * flat `{symbol, tests}`).
284
- * @returns {{tiers: ReadonlyArray<string>,
285
- * rows: Array<{id:string,label:string,
286
- * verdict:ReturnType<typeof coverageVerdict>}>}}
287
- */
288
- export function acceptanceMatrix(criteria) {
289
- const normalized = normalizeCriteria(criteria);
290
- const rows = normalized.map(({ id, label, surface }) => ({
291
- id,
292
- label,
293
- verdict: coverageVerdict(surface),
294
- }));
295
- return { tiers: TIERS, rows };
296
- }
@@ -2,7 +2,7 @@
2
2
  * detectors/common.js — shared helpers for the signals layer.
3
3
  *
4
4
  * Hoisted out of the detector modules (retry, rework) plus
5
- * `signals/read.js` and `signals/schema.js`, all of which shipped
5
+ * the since-deleted `signals/read.js`, and `signals/schema.js`, all of which shipped
6
6
  * byte-equivalent copies of these predicates. See Story #2464.
7
7
  */
8
8
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `lib/signals/` barrel.
3
3
  *
4
- * Consumers do `import { read, schema, buildSpanTree, appendSignal } from './lib/signals/index.js'`
4
+ * Consumers do `import { schema, appendSignal } from './lib/signals/index.js'`
5
5
  * (or `import * as signals from './lib/signals/index.js'`) so a future
6
6
  * shape migration only has to touch this file.
7
7
  *
@@ -9,18 +9,20 @@
9
9
  * - Epic #1181 / Story #1438 / Task #1459 — initial barrel with `read`
10
10
  * + `schema`; `buildSpanTree` was a throwing placeholder.
11
11
  * - Epic #1181 / Story #1440 / Task #1461 — placeholder replaced by
12
- * the real export from `./span-tree.js`.
12
+ * the real span-tree builder.
13
13
  * - Story #1476 — `appendSignal` / `appendTrace` re-exported here
14
14
  * via `./write.js` so the gate scripts (and any
15
15
  * new code) converge on `lib/signals/` instead of importing the
16
16
  * writer directly from `lib/observability/`.
17
+ * - Story #5003 — the reader half (`read`, `buildSpanTree`) went with the
18
+ * debug viewer that consumed it: it walked a `run-<id>/` layout no v2
19
+ * writer populates. `appendTrace` went with the trace hook, its only
20
+ * producer. What remains is the live write surface.
17
21
  *
18
22
  * @module lib/signals
19
23
  */
20
24
 
21
- import { read } from './read.js';
22
25
  import * as schema from './schema.js';
23
- import { buildSpanTree } from './span-tree.js';
24
- import { appendSignal, appendTrace, forEachLine } from './write.js';
26
+ import { appendSignal, forEachLine } from './write.js';
25
27
 
26
- export { appendSignal, appendTrace, buildSpanTree, forEachLine, read, schema };
28
+ export { appendSignal, forEachLine, schema };
@@ -2,10 +2,10 @@
2
2
  * Signals schema — single source of truth for event-kind constants and
3
3
  * shape guards (Epic #1181 / Story #1438 / Task #1458).
4
4
  *
5
- * Both writers (`lib/observability/signals-writer.js` callers) and
6
- * readers (`lib/signals/read.js`, the retro's gather-signals phase)
7
- * import their field names from this module so the on-disk NDJSON shape
8
- * stays under one schema.
5
+ * Both writers (`lib/observability/signals-writer.js` callers) and readers
6
+ * (its `forEachLine` / `forEachSignalStreamLine` walkers, and the retro's
7
+ * gather-signals phase) import their field names from this module so the
8
+ * on-disk NDJSON shape stays under one schema.
9
9
  *
10
10
  * Pure module: importing has **no I/O side effects**. The exported
11
11
  * `EVENT_KINDS` object is `Object.freeze`'d so consumers can use it as a
@@ -14,30 +14,25 @@
14
14
  * ## Event-kind enumeration
15
15
  *
16
16
  * The set below is the audited union of `kind:` literals emitted via
17
- * `signals-writer.appendSignal` and `signals-writer.appendTrace` across
18
- * the codebase as of Epic #1181 (audit-snapshot 2026-05-11):
17
+ * `signals-writer.appendSignal` across the codebase as of Epic #1181
18
+ * (audit-snapshot 2026-05-11):
19
19
  *
20
20
  * Signals-writer `appendSignal` call sites:
21
21
  * - `friction` — quality-gate / runtime friction (diagnose-friction.js,
22
22
  * lib/gates/friction.js)
23
23
  * - `acceptance-eval` — acceptance-eval.js per-criterion terminus signal.
24
- * - `wave-start` / `wave-complete` — wave-window anchors; `wave-start` also
25
- * anchors span-tree Story spans
26
- * - `wave-end` — span-tree pairing anchor (retained for span-tree)
24
+ * - `wave-start` / `wave-complete` — wave-window anchors.
25
+ * - `wave-end` — wave-window pairing anchor.
27
26
  * - `state-transition` — notification-derived window anchor.
28
27
  *
29
28
  * The `dispatched` kind was deleted in the Epic #4406 signal-contract
30
29
  * cutover — it had no live emitter and no consumer.
31
30
  *
32
- * Signals-writer `appendTrace` call sites (traces.ndjson sibling, but
33
- * sharing the same envelope shape `tool-trace-hook.js`):
34
- * - `trace` per-tool-call timing record
35
- *
36
- * Detector-emitted / aggregator-consumed kinds (`rework`, `retry` are
37
- * emitted by `lib/signals/detectors/*`; `hotspot`, `churn`, `idle` remain
38
- * pinned in the enum for the aggregator's kind-count rollup even though no
39
- * live detector emits them):
40
- * - `hotspot`, `rework`, `churn`, `idle`, `retry`
31
+ * Producerless kinds, pinned in the enum so a re-introduction needs no schema
32
+ * bump. Story #5003 deleted the last emitters of `trace` (the tool-trace hook
33
+ * and its `appendTrace` writer limb) and of `rework` / `retry` (the detector
34
+ * pair the hook fed); `hotspot`, `churn`, `idle` never had one:
35
+ * - `trace`, `hotspot`, `rework`, `churn`, `idle`, `retry`
41
36
  *
42
37
  * ## Common envelope (canonical — Epic #4406 / Story #4413)
43
38
  *
@@ -68,18 +63,18 @@ import { isPositiveInt } from './detectors/common.js';
68
63
 
69
64
  /**
70
65
  * Frozen enumeration of every event kind currently emitted by
71
- * `signals-writer.appendSignal` / `appendTrace`, plus the
72
- * aggregator-consumed kinds whose detectors are future Stories.
66
+ * `signals-writer.appendSignal`, plus the producerless kinds a future Story
67
+ * may re-introduce an emitter for.
73
68
  *
74
69
  * Field names are kebab-cased to match the on-disk literals.
75
70
  */
76
71
  export const EVENT_KINDS = Object.freeze({
77
72
  FRICTION: 'friction',
78
73
  TRACE: 'trace',
79
- // Wave-window forensics signals: `wave-start` / `wave-end` anchor the
80
- // span-tree's Story spans. Story #4545 deleted the `waveParallelism`
81
- // consumer (perf-aggregator) with the execution-analysis surface; these
82
- // kinds stay as the span-tree's anchors.
74
+ // Wave-window forensics signals. Story #4545 deleted the `waveParallelism`
75
+ // consumer (perf-aggregator) with the execution-analysis surface, and
76
+ // Story #5003 deleted the span-tree that paired them; the kinds stay
77
+ // pinned so a re-introduced window reader needs no schema bump.
83
78
  WAVE_START: 'wave-start',
84
79
  WAVE_END: 'wave-end',
85
80
  WAVE_COMPLETE: 'wave-complete',
@@ -156,7 +151,7 @@ function isTimestamp(v) {
156
151
  * recognised `kind`. Canonical keys only — the legacy `timestamp` / `epic`
157
152
  * aliases were deleted in the Epic #4406 cutover.
158
153
  *
159
- * Returns true when the envelope is well-formed. Used by `lib/signals/read`
154
+ * Returns true when the envelope is well-formed. Used by the stream readers
160
155
  * to discard malformed lines before yielding them to the consumer.
161
156
  *
162
157
  * @param {unknown} evt
@@ -1,18 +1,18 @@
1
1
  /**
2
- * `lib/signals/write.js` — thin re-export of the signals/traces writer.
2
+ * `lib/signals/write.js` — thin re-export of the signals writer.
3
3
  *
4
4
  * The real implementation lives at `../observability/signals-writer.js`
5
5
  * (Epic #1030 / Story #1041). This module exists so callers can converge
6
- * on `lib/signals/` for both reads and writes the reader barrel landed
7
- * in Epic #1181 / Story #1438, but the writer surface was still split
8
- * across `lib/observability/`. New code should `import { appendSignal }
9
- * from './lib/signals/index.js'` (or `'./lib/signals/write.js'` for a
10
- * narrower import); legacy direct imports from `lib/observability/` keep
11
- * working unchanged.
6
+ * on `lib/signals/` for the write surface. New code should
7
+ * `import { appendSignal } from './lib/signals/index.js'` (or
8
+ * `'./lib/signals/write.js'` for a narrower import); legacy direct imports
9
+ * from `lib/observability/` keep working unchanged.
10
+ *
11
+ * `appendTrace` was removed in Story #5003 together with the tool-trace
12
+ * hook — its only producer — and the viewer that was its only reader.
12
13
  */
13
14
 
14
15
  export {
15
16
  appendSignal,
16
- appendTrace,
17
17
  forEachLine,
18
18
  } from '../observability/signals-writer.js';
@@ -21,7 +21,6 @@
21
21
  * wide: { reason } | null,// declared-wide footprint (optional)
22
22
  * reason_to_exist: string | null, // one-sentence cohesion reason (optional)
23
23
  * depends_on: string[], // blocker story slugs or #ids
24
- * estimated_test_files: number | null, // absent → null (informational)
25
24
  * }
26
25
  * ```
27
26
  *
@@ -79,7 +78,6 @@ import { suggestPathEntryFix } from './body-format-lints.js';
79
78
  * @property {{ reason: string }|null} wide - Declared-wide footprint (reason), or null.
80
79
  * @property {string|null} reason_to_exist - One-sentence cohesion reason ("why this Story exists"), or null.
81
80
  * @property {string[]} depends_on - Blocking story slugs / issue refs.
82
- * @property {number|null} estimated_test_files - Test surface count or null.
83
81
  * @property {string|null} mandrel_version - Framework version stamped at authoring, or null.
84
82
  * @property {string|null} authored_at - Authoring date (YYYY-MM-DD) stamped at authoring, or null.
85
83
  */
@@ -334,7 +332,7 @@ const META_BLOCK_RE = /<!--\s*meta:\s*([\s\S]*?)\s*-->/;
334
332
  const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
335
333
 
336
334
  /**
337
- * Extract the `wide` / `estimated_test_files` fields from the trailing
335
+ * Extract the `wide` / `reason_to_exist` fields from the trailing
338
336
  * `<!-- meta: {...} -->` comment block written by {@link serialize}. Returns
339
337
  * canonical-shaped values (null when absent or malformed) so the parser
340
338
  * round-trips the meta block faithfully.
@@ -350,13 +348,12 @@ const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
350
348
  * rather than dropping or re-deriving it.
351
349
  *
352
350
  * @param {string} markdown
353
- * @returns {{ wide: { reason: string }|null, reason_to_exist: string|null, estimated_test_files: number|null, mandrel_version: string|null, authored_at: string|null }}
351
+ * @returns {{ wide: { reason: string }|null, reason_to_exist: string|null, mandrel_version: string|null, authored_at: string|null }}
354
352
  */
355
353
  function extractMeta(markdown) {
356
354
  const result = {
357
355
  wide: null,
358
356
  reason_to_exist: null,
359
- estimated_test_files: null,
360
357
  mandrel_version: null,
361
358
  authored_at: null,
362
359
  };
@@ -377,9 +374,6 @@ function extractMeta(markdown) {
377
374
 
378
375
  result.wide = normalizeWide(parsed.wide);
379
376
  result.reason_to_exist = normalizeReasonToExist(parsed.reason_to_exist);
380
- if (typeof parsed.estimated_test_files === 'number') {
381
- result.estimated_test_files = parsed.estimated_test_files;
382
- }
383
377
  if (
384
378
  typeof parsed.mandrel_version === 'string' &&
385
379
  parsed.mandrel_version.trim()
@@ -579,7 +573,6 @@ function isMachineMarkerLine(line) {
579
573
  function parseUnstructuredBody(input, preamble, footer) {
580
574
  const warnings = [
581
575
  'unstructured-body: no structured sections found; returning minimal body from preamble text.',
582
- 'test-surface-unestimated: estimated_test_files not present.',
583
576
  ];
584
577
  const body = {
585
578
  goal: preamble || input.trim(),
@@ -593,7 +586,6 @@ function parseUnstructuredBody(input, preamble, footer) {
593
586
  wide: null,
594
587
  reason_to_exist: null,
595
588
  depends_on: extractBlockedBy(footer),
596
- estimated_test_files: null,
597
589
  mandrel_version: null,
598
590
  authored_at: null,
599
591
  };
@@ -688,11 +680,6 @@ function parseTextListSection(lines) {
688
680
  * `result.warnings` to detect legacy path entries that should be
689
681
  * migrated.
690
682
  *
691
- * Informational finding emitted on `result.warnings`:
692
- * - `test-surface-unestimated` — when `estimated_test_files` is absent
693
- * from both a structured body and the markdown. Callers that care about
694
- * test-surface coverage SHOULD surface this to the operator.
695
- *
696
683
  * @param {string|object} input - Markdown string or already-structured body object.
697
684
  * @returns {ParseResult}
698
685
  * @throws {StoryBodyParseError} When the body is structurally unrecoverable.
@@ -765,20 +752,16 @@ export function parse(input) {
765
752
  const non_goals = parseTextListSection(sections.get('non_goals') ?? []);
766
753
  const dependsOn = extractBlockedBy(footer);
767
754
 
768
- // --- Recover wide / estimated_test_files from the meta block ---
755
+ // --- Recover wide / reason_to_exist / provenance from the meta block ---
769
756
  // serialize() writes these into a trailing `<!-- meta: {...} -->` comment
770
757
  // so round-trips preserve them. Absent meta block → canonical null defaults.
758
+ // Unknown keys are ignored, so a body carrying a retired meta field parses
759
+ // clean and simply drops it.
771
760
  const meta = extractMeta(input);
772
- const estimated_test_files = meta.estimated_test_files;
773
761
  const wide = meta.wide;
774
762
  const reason_to_exist = meta.reason_to_exist;
775
763
  const mandrel_version = meta.mandrel_version;
776
764
  const authored_at = meta.authored_at;
777
- if (estimated_test_files === null) {
778
- warnings.push(
779
- 'test-surface-unestimated: estimated_test_files not present.',
780
- );
781
- }
782
765
 
783
766
  const body = {
784
767
  goal,
@@ -792,7 +775,6 @@ export function parse(input) {
792
775
  wide,
793
776
  reason_to_exist,
794
777
  depends_on: dependsOn,
795
- estimated_test_files,
796
778
  mandrel_version,
797
779
  authored_at,
798
780
  };
@@ -834,13 +816,6 @@ function parseStructuredObject(obj) {
834
816
  body[name] = STRUCTURED_FIELD_NORMALIZERS[kind](obj[name], warnings);
835
817
  }
836
818
 
837
- // estimated_test_files is the one warning-coupled scalar: absent (== null)
838
- // warns; a non-number, non-null value stays null silently.
839
- body.estimated_test_files = normalizeEstimatedTestFiles(
840
- obj.estimated_test_files,
841
- warnings,
842
- );
843
-
844
819
  // Provenance stamp (preserved verbatim; never re-derived here).
845
820
  body.mandrel_version = normalizeProvenanceString(obj.mandrel_version);
846
821
  body.authored_at = normalizeProvenanceString(obj.authored_at);
@@ -917,25 +892,6 @@ const STRUCTURED_FIELD_NORMALIZERS = {
917
892
  reasonToExist: (raw) => normalizeReasonToExist(raw),
918
893
  };
919
894
 
920
- /**
921
- * Normalize `estimated_test_files`: a number passes through; an absent
922
- * (`null`/`undefined`) value warns `test-surface-unestimated`; any other
923
- * value stays `null` silently.
924
- *
925
- * @param {unknown} raw
926
- * @param {string[]} warnings
927
- * @returns {number|null}
928
- */
929
- function normalizeEstimatedTestFiles(raw, warnings) {
930
- if (typeof raw === 'number') return raw;
931
- if (raw == null) {
932
- warnings.push(
933
- 'test-surface-unestimated: estimated_test_files not present.',
934
- );
935
- }
936
- return null;
937
- }
938
-
939
895
  /**
940
896
  * Normalize a provenance stamp field (`mandrel_version` / `authored_at`) to
941
897
  * a non-empty trimmed string or `null`.
@@ -1066,11 +1022,11 @@ const SERIALIZE_SECTIONS = [
1066
1022
 
1067
1023
  /**
1068
1024
  * Build the trailing `<!-- meta: {...} -->` block carrying the fields that
1069
- * have no human-readable section (`wide`, `reason_to_exist`,
1070
- * `estimated_test_files`). Returns the empty string when no meta field is
1071
- * present so {@link serialize} appends nothing.
1025
+ * have no human-readable section (`wide`, `reason_to_exist`). Returns the
1026
+ * empty string when no meta field is present so {@link serialize} appends
1027
+ * nothing.
1072
1028
  *
1073
- * Key insertion order (`wide` → `reason_to_exist` → `estimated_test_files` →
1029
+ * Key insertion order (`wide` → `reason_to_exist` →
1074
1030
  * `mandrel_version` → `authored_at`) is load-bearing: it fixes the serialized
1075
1031
  * JSON byte sequence the parser's meta round-trip and the unit suite assert
1076
1032
  * against. The provenance stamp keys are appended **last** so every
@@ -1089,9 +1045,6 @@ function serializeMetaBlock(body) {
1089
1045
  if (reasonToExist !== null) {
1090
1046
  metaFields.reason_to_exist = reasonToExist;
1091
1047
  }
1092
- if (typeof body.estimated_test_files === 'number') {
1093
- metaFields.estimated_test_files = body.estimated_test_files;
1094
- }
1095
1048
  if (typeof body.mandrel_version === 'string' && body.mandrel_version.trim()) {
1096
1049
  metaFields.mandrel_version = body.mandrel_version.trim();
1097
1050
  }
@@ -1153,9 +1106,9 @@ function serializeFooter(body, opts) {
1153
1106
  * `## Goal`, `## Slicing`, `## Spec`, `## Changes`, `## Acceptance`,
1154
1107
  * `## Verify`, `## References`, `## Non-Goals` (each omitted when empty).
1155
1108
  *
1156
- * `wide`, `reason_to_exist`, and `estimated_test_files` are emitted as a
1157
- * fenced `<!-- meta -->` comment block so round-trips preserve them without
1158
- * polluting the human-readable body.
1109
+ * `wide` and `reason_to_exist` are emitted as a fenced `<!-- meta -->`
1110
+ * comment block so round-trips preserve them without polluting the
1111
+ * human-readable body.
1159
1112
  *
1160
1113
  * @param {StoryBody} body
1161
1114
  * @param {SerializeOptions} [opts]
@@ -31,7 +31,7 @@
31
31
  * ## Keep-class
32
32
  *
33
33
  * `signals.ndjson` is the artifact whose value *starts* when the run ends —
34
- * `signals-view`, `acceptance-eval`, and the loop-health check all read it
34
+ * `acceptance-eval` and the loop-health check both read it
35
35
  * long after the Story merged. It is excluded twice over: it is not in the
36
36
  * evidence basename allowlist, and {@link KEEP_BASENAMES} is re-checked at
37
37
  * the deletion site. Defence in depth is warranted for the one file whose