mandrel 2.31.0 → 2.33.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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -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 +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  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 +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  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 +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  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 +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -0,0 +1,508 @@
1
+ /**
2
+ * knip-entry-sync.js — derive which top-level `.agents/scripts/*.js` CLIs are
3
+ * actually invoked, and diff that set against the `entry` array of whatever
4
+ * configuration knip itself would load (Story #5039; #5026 could only read a
5
+ * literal `knip.json`).
6
+ *
7
+ * Why this exists (Story #5001 → the #5012 near-miss):
8
+ *
9
+ * #5001 replaced the blanket `.agents/scripts/*.js!` entry glob with an
10
+ * explicit list, precisely so that a top-level CLI **nothing invokes** stops
11
+ * being declared live by the glob and surfaces as dead. It simultaneously
12
+ * promoted knip's `files` rule to `error`, so whole-file death now produces
13
+ * `{ file, symbol: '*' }` baseline rows.
14
+ *
15
+ * That pairing has a silent failure mode. A CLI added *after* #5001 is not in
16
+ * the hand-written list, so knip reads it as unreachable and emits a whole-file
17
+ * row for it — plus transitive rows for every lib module only that CLI imports.
18
+ * During #5012's delivery this produced 5 false rows (`check-baseline-scope.js`,
19
+ * `prune-baseline-orphans.js`, and their three `lib/baselines/*` modules). The
20
+ * natural remedy — accept the ratchet's diff — would have permanently recorded
21
+ * live, working operator CLIs as expected-dead, blinding the ratchet to their
22
+ * real death later. It was caught only because a base-sync conflict forced a
23
+ * manual read of the diff.
24
+ *
25
+ * The fix is to stop depending on a human remembering to edit the config.
26
+ * #5001's own wording already names the derivation rule — entries are derived
27
+ * "from what package.json scripts, husky hooks, .github/workflows, and the
28
+ * .agents workflow/skill markdown actually invoke". This module mechanizes
29
+ * exactly that rule and asserts it in both directions:
30
+ *
31
+ * missing — invoked by a real caller but absent from `entry`. This is the
32
+ * #5012 bug: knip will call it dead and the ratchet will offer to record
33
+ * the lie.
34
+ * stale — listed in `entry` but invoked by nothing. This is the blind spot
35
+ * #5001 closed: a declared entry point is immortal, so its death can never
36
+ * surface.
37
+ *
38
+ * Deliberately NOT a filesystem-derived list. Restoring `.agents/scripts/*.js!`
39
+ * — or regenerating the array from `readdirSync` — would make every file live
40
+ * by construction and undo #5001's AC-2. Liveness here means *invoked*, not
41
+ * *present*.
42
+ *
43
+ * Deliberately NOT conferred by documentation prose. `.agents/docs/**` and
44
+ * `docs/**` describe operator-run commands; a paragraph naming a CLI is not a
45
+ * caller. Three CLIs are named only in prose today
46
+ * (`check-baseline-drift.js`, `post-structured-comment.js`,
47
+ * `provision-git-hooks.js`) and #5001 recorded all of them as whole-file dead
48
+ * rows on purpose. Counting prose would silently resurrect them.
49
+ *
50
+ * A CLI that is only *imported* by another script (`cleanup-repo-test-temp.js`,
51
+ * imported by `run-tests.js` and `run-coverage.js`) is correctly neither: it is
52
+ * reachable through the module graph from a real entry, so knip already sees it
53
+ * and it must not be declared an entry point of its own.
54
+ */
55
+
56
+ import fs from 'node:fs';
57
+ import path from 'node:path';
58
+ import {
59
+ resolveKnipEntryPatterns,
60
+ __testing as resolverTesting,
61
+ } from './knip-config-resolver.js';
62
+
63
+ // Only `resolveEntrySync` and `renderEntrySyncReport` are public — they are
64
+ // what `check-knip-entries.js` calls. Everything else is module-private and
65
+ // reached by the suite through the `__testing` bundle at the foot of this file,
66
+ // the same seam `source-classifier.js` and five sibling modules use. Exporting
67
+ // the internals individually would put seven test-only symbols on the
68
+ // production dead-export baseline and make private helpers look like API.
69
+
70
+ /** Top-level CLI directory, repo-relative. */
71
+ const SCRIPTS_DIR = path.join('.agents', 'scripts');
72
+
73
+ /**
74
+ * Executable surfaces — a mention of a CLI here is an instruction to run it.
75
+ *
76
+ * `dirs` entries are walked recursively and filtered by `test`. `files` are
77
+ * read directly. Documentation trees are absent by design (see module header).
78
+ *
79
+ * @type {ReadonlyArray<{ kind: 'file' | 'dir', at: string, test?: RegExp }>}
80
+ */
81
+ const INVOCATION_SURFACES = Object.freeze([
82
+ { kind: 'file', at: 'package.json' },
83
+ { kind: 'dir', at: '.husky', test: /^(?!.*[/\\]_[/\\]).*$/ },
84
+ { kind: 'dir', at: path.join('.github', 'workflows'), test: /\.ya?ml$/ },
85
+ { kind: 'dir', at: path.join('.agents', 'workflows'), test: /\.md$/ },
86
+ { kind: 'dir', at: path.join('.agents', 'skills'), test: /\.md$/ },
87
+ { kind: 'dir', at: path.join('.agents', 'agents'), test: /\.md$/ },
88
+ { kind: 'dir', at: path.join('.agents', 'rules'), test: /\.md$/ },
89
+ { kind: 'dir', at: SCRIPTS_DIR, test: /\.js$/ },
90
+ ]);
91
+
92
+ /**
93
+ * Strip line and block comments from JavaScript source, preserving string and
94
+ * template literals so a `'https://…'` or a `` `${x}//y` `` is not mangled.
95
+ *
96
+ * Comments are stripped before scanning `.js` surfaces so a JSDoc paragraph
97
+ * that merely *names* a CLI ("superseded by `node .agents/scripts/foo.js`")
98
+ * cannot confer liveness on it. Several such mentions exist today; every one
99
+ * of them is prose about a script's internals, not a call.
100
+ *
101
+ * Replaces comment bodies with equivalent whitespace rather than deleting
102
+ * them, so byte offsets and line numbers survive for any future caller that
103
+ * wants to report a position.
104
+ *
105
+ * @param {string} source
106
+ * @returns {string}
107
+ */
108
+ function stripJsComments(source) {
109
+ const text = String(source ?? '');
110
+ let out = '';
111
+ let i = 0;
112
+ const blank = (s) => s.replace(/[^\n]/g, ' ');
113
+ while (i < text.length) {
114
+ const two = text.slice(i, i + 2);
115
+ if (two === '//') {
116
+ const end = text.indexOf('\n', i);
117
+ const stop = end === -1 ? text.length : end;
118
+ out += blank(text.slice(i, stop));
119
+ i = stop;
120
+ } else if (two === '/*') {
121
+ const end = text.indexOf('*/', i + 2);
122
+ const stop = end === -1 ? text.length : end + 2;
123
+ out += blank(text.slice(i, stop));
124
+ i = stop;
125
+ } else {
126
+ const ch = text[i];
127
+ if (ch === "'" || ch === '"' || ch === '`') {
128
+ let j = i + 1;
129
+ while (j < text.length) {
130
+ if (text[j] === '\\') {
131
+ j += 2;
132
+ continue;
133
+ }
134
+ if (text[j] === ch) break;
135
+ j += 1;
136
+ }
137
+ const stop = Math.min(j + 1, text.length);
138
+ out += text.slice(i, stop);
139
+ i = stop;
140
+ } else {
141
+ out += ch;
142
+ i += 1;
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * Recursively list files under `dir`, skipping `node_modules` and `.git`.
151
+ *
152
+ * @param {string} dir absolute path
153
+ * @param {typeof fs} fsImpl
154
+ * @param {string[]} [acc]
155
+ * @returns {string[]} absolute file paths
156
+ */
157
+ function walk(dir, fsImpl, acc = []) {
158
+ let entries;
159
+ try {
160
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
161
+ } catch {
162
+ return acc;
163
+ }
164
+ for (const entry of entries) {
165
+ const full = path.join(dir, entry.name);
166
+ if (entry.isDirectory()) {
167
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
168
+ walk(full, fsImpl, acc);
169
+ } else {
170
+ acc.push(full);
171
+ }
172
+ }
173
+ return acc;
174
+ }
175
+
176
+ /**
177
+ * Read every executable surface under `repoRoot`.
178
+ *
179
+ * `.js` surfaces arrive comment-stripped; every other surface arrives verbatim.
180
+ * Test files are excluded: a test importing or naming a CLI does not make it
181
+ * invoked, which is the same test-only-importer discount `--production` applies
182
+ * to the dead-exports ratchet itself.
183
+ *
184
+ * @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
185
+ * @returns {Array<{ path: string, text: string }>} repo-relative path + content
186
+ */
187
+ function collectInvocationSurfaces({ repoRoot, fsImpl = fs }) {
188
+ const surfaces = [];
189
+ const push = (absolute) => {
190
+ const rel = path.relative(repoRoot, absolute);
191
+ if (rel.includes('__tests__') || /\.test\.[cm]?js$/.test(rel)) return;
192
+ let text;
193
+ try {
194
+ text = fsImpl.readFileSync(absolute, 'utf8');
195
+ } catch {
196
+ return;
197
+ }
198
+ surfaces.push({
199
+ path: rel.split(path.sep).join('/'),
200
+ text: rel.endsWith('.js') ? stripJsComments(text) : text,
201
+ });
202
+ };
203
+
204
+ for (const surface of INVOCATION_SURFACES) {
205
+ const absolute = path.join(repoRoot, surface.at);
206
+ if (surface.kind === 'file') {
207
+ push(absolute);
208
+ continue;
209
+ }
210
+ for (const file of walk(absolute, fsImpl)) {
211
+ const rel = path.relative(repoRoot, file);
212
+ if (surface.test && !surface.test.test(rel)) continue;
213
+ push(file);
214
+ }
215
+ }
216
+ return surfaces;
217
+ }
218
+
219
+ /**
220
+ * Build the two recognizers for one CLI basename.
221
+ *
222
+ * `pathLiteral` — the CLI named by its repo path (`.agents/scripts/foo.js`),
223
+ * in either separator style. This is how package.json scripts, husky hooks,
224
+ * workflow `run:` blocks and workflow markdown spell an invocation.
225
+ *
226
+ * `joinedSpawn` — the CLI named by bare basename as the tail of a
227
+ * `path.join(…, 'scripts', 'foo.js')` construction, which is how
228
+ * `lib/bootstrap/project-bootstrap.js` spawns `check-windows-git-perf.js`
229
+ * into a consumer checkout. Requiring the `'scripts'` sibling argument is
230
+ * what separates a real spawn from the several *data* inventories that list
231
+ * the same basenames as plain array elements (`source-classifier.js`'s
232
+ * `FRAMEWORK_SCRIPT_BASENAMES`, `check-lifecycle-lint.js`'s allowlist) —
233
+ * those are catalogues of names, not calls.
234
+ *
235
+ * @param {string} cli basename, e.g. `check-dead-exports.js`
236
+ * @returns {{ pathLiteral: RegExp, joinedSpawn: RegExp }}
237
+ */
238
+ function buildInvocationPatterns(cli) {
239
+ const escaped = cli.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
240
+ return {
241
+ pathLiteral: new RegExp(`\\.agents[/\\\\]scripts[/\\\\]${escaped}`),
242
+ joinedSpawn: new RegExp(
243
+ `['"\`]scripts['"\`]\\s*,\\s*['"\`]${escaped}['"\`]`,
244
+ ),
245
+ };
246
+ }
247
+
248
+ /**
249
+ * List the top-level CLI basenames actually present in `.agents/scripts/`.
250
+ *
251
+ * @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
252
+ * @returns {string[]} sorted basenames
253
+ */
254
+ function listTopLevelClis({ repoRoot, fsImpl = fs }) {
255
+ let entries;
256
+ try {
257
+ entries = fsImpl.readdirSync(path.join(repoRoot, SCRIPTS_DIR), {
258
+ withFileTypes: true,
259
+ });
260
+ } catch {
261
+ return [];
262
+ }
263
+ return entries
264
+ .filter((e) => e.isFile() && e.name.endsWith('.js'))
265
+ .map((e) => e.name)
266
+ .sort();
267
+ }
268
+
269
+ /**
270
+ * Read the `.agents/scripts/*.js` basenames declared as knip entry points.
271
+ *
272
+ * Resolution goes through knip's own loader — see
273
+ * `knip-config-resolver.js` for why, and for the skipped / error / resolved
274
+ * contract this forwards. All this adds is the projection onto top-level CLI
275
+ * basenames: glob-bearing patterns are dropped, so what remains is the explicit
276
+ * enumeration #5001 made authoritative.
277
+ *
278
+ * The trailing `!` is knip's production-mode marker and is load-bearing, not
279
+ * decoration: in a `--production` run knip keeps only the suffixed entries and
280
+ * *negates* the rest (`WorkspaceWorker`'s `hasProductionSuffix` /
281
+ * `hasNoProductionSuffix` split). So an entry written without it leaves the CLI
282
+ * unreachable in exactly the pass that emits whole-file rows — the #5012 shape
283
+ * this gate exists to catch. Reading it as satisfied would point the operator
284
+ * away from the cause, so unsuffixed entries are reported as their own
285
+ * divergence rather than silently normalized. `createOptions` preserves the
286
+ * suffix, so the marker survives resolution.
287
+ *
288
+ * @param {{ repoRoot: string, loadKnipSession?: () => Promise<object> }} opts
289
+ * @returns {Promise<{
290
+ * entries: string[],
291
+ * unsuffixed: string[],
292
+ * configFilePath: string | null,
293
+ * skipped: string | null,
294
+ * error: string | null,
295
+ * }>}
296
+ */
297
+ async function readKnipEntries({ repoRoot, loadKnipSession }) {
298
+ const { patterns, configFilePath, skipped, error } =
299
+ await resolveKnipEntryPatterns({ repoRoot, loadKnipSession });
300
+ if (skipped || error) {
301
+ return { entries: [], unsuffixed: [], configFilePath, skipped, error };
302
+ }
303
+
304
+ const prefix = '.agents/scripts/';
305
+ const declared = patterns
306
+ .filter((e) => e.startsWith(prefix) && !e.includes('*'))
307
+ .map((e) => ({
308
+ cli: e.slice(prefix.length).replace(/!$/, ''),
309
+ suffixed: e.endsWith('!'),
310
+ }))
311
+ .filter((e) => !e.cli.includes('/'));
312
+
313
+ return {
314
+ entries: [...new Set(declared.map((e) => e.cli))].sort(),
315
+ unsuffixed: [
316
+ ...new Set(declared.filter((e) => !e.suffixed).map((e) => e.cli)),
317
+ ].sort(),
318
+ configFilePath,
319
+ skipped: null,
320
+ error: null,
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Resolve the full entry-sync report for a repository.
326
+ *
327
+ * Async because knip's config loader is: a `knip.config.ts` has to be
328
+ * *evaluated*, not read. `EntrySync` names entry-list synchronization, not
329
+ * synchronous I/O, so the name still describes what this returns.
330
+ *
331
+ * @param {{
332
+ * repoRoot: string,
333
+ * fsImpl?: typeof fs,
334
+ * loadKnipSession?: () => Promise<object>,
335
+ * }} opts
336
+ * @returns {Promise<{
337
+ * error: string | null,
338
+ * skipped: string | null,
339
+ * configFilePath: string | null,
340
+ * clis: string[],
341
+ * declared: string[],
342
+ * missing: Array<{ cli: string, invokers: string[] }>,
343
+ * stale: string[],
344
+ * phantom: string[],
345
+ * unsuffixed: string[],
346
+ * }>}
347
+ * `skipped` — nothing to check; the caller passes rather than failing.
348
+ * `missing` — invoked but not declared (the #5012 bug).
349
+ * `stale` — declared but invoked by nothing.
350
+ * `phantom` — declared but no such file on disk (a rename left the list behind).
351
+ * `unsuffixed` — declared without the `!` production marker, which knip's
352
+ * production pass negates rather than honours (the #5012 bug wearing a
353
+ * declared entry).
354
+ */
355
+ export async function resolveEntrySync({
356
+ repoRoot,
357
+ fsImpl = fs,
358
+ loadKnipSession,
359
+ }) {
360
+ const {
361
+ entries: declared,
362
+ unsuffixed,
363
+ configFilePath,
364
+ skipped,
365
+ error,
366
+ } = await readKnipEntries({ repoRoot, loadKnipSession });
367
+ const empty = {
368
+ skipped: null,
369
+ configFilePath,
370
+ clis: [],
371
+ declared,
372
+ missing: [],
373
+ stale: [],
374
+ phantom: [],
375
+ unsuffixed: [],
376
+ };
377
+ if (skipped) return { error: null, ...empty, skipped };
378
+ if (error) return { error, ...empty };
379
+
380
+ const clis = listTopLevelClis({ repoRoot, fsImpl });
381
+ if (clis.length === 0) {
382
+ return { error: `no CLIs found under ${SCRIPTS_DIR}`, ...empty };
383
+ }
384
+ const surfaces = collectInvocationSurfaces({ repoRoot, fsImpl });
385
+ const declaredSet = new Set(declared);
386
+
387
+ const missing = [];
388
+ const stale = [];
389
+ for (const cli of clis) {
390
+ const self = `${SCRIPTS_DIR.split(path.sep).join('/')}/${cli}`;
391
+ const { pathLiteral, joinedSpawn } = buildInvocationPatterns(cli);
392
+ const invokers = surfaces
393
+ .filter(
394
+ (s) =>
395
+ s.path !== self &&
396
+ (pathLiteral.test(s.text) ||
397
+ (s.path.endsWith('.js') && joinedSpawn.test(s.text))),
398
+ )
399
+ .map((s) => s.path);
400
+ const isDeclared = declaredSet.has(cli);
401
+ if (invokers.length > 0 && !isDeclared) missing.push({ cli, invokers });
402
+ if (invokers.length === 0 && isDeclared) stale.push(cli);
403
+ }
404
+
405
+ const onDisk = new Set(clis);
406
+ const phantom = declared.filter((cli) => !onDisk.has(cli));
407
+
408
+ return {
409
+ error: null,
410
+ skipped: null,
411
+ configFilePath,
412
+ clis,
413
+ declared,
414
+ missing,
415
+ stale,
416
+ phantom,
417
+ unsuffixed,
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Total divergences across every direction. Single source of the gate's
423
+ * pass/fail arithmetic so a newly added direction cannot be counted in the
424
+ * report but missed by the exit code.
425
+ *
426
+ * @param {ReturnType<typeof resolveEntrySync>} report
427
+ * @returns {number}
428
+ */
429
+ export function countDivergences(report) {
430
+ return (
431
+ report.missing.length +
432
+ report.stale.length +
433
+ report.phantom.length +
434
+ report.unsuffixed.length
435
+ );
436
+ }
437
+
438
+ /**
439
+ * Render the human-readable report. Each divergence names its own remedy —
440
+ * the whole point of the gate is that the *wrong* remedy (accepting the
441
+ * dead-exports ratchet's diff) is the tempting one.
442
+ *
443
+ * @param {ReturnType<typeof resolveEntrySync>} report
444
+ * @returns {string}
445
+ */
446
+ export function renderEntrySyncReport(report) {
447
+ const lines = [];
448
+ for (const { cli, invokers } of report.missing) {
449
+ lines.push(
450
+ `+ ${cli} — invoked by ${invokers.slice(0, 3).join(', ')}${
451
+ invokers.length > 3 ? ` (+${invokers.length - 3} more)` : ''
452
+ }`,
453
+ );
454
+ lines.push(
455
+ ` add ".agents/scripts/${cli}!" to knip.json "entry" — until you do, knip`,
456
+ );
457
+ lines.push(
458
+ ' reads it as unreachable and check-dead-exports will offer a false whole-file row.',
459
+ );
460
+ }
461
+ for (const cli of report.stale) {
462
+ lines.push(
463
+ `- ${cli} — declared in knip.json "entry" but nothing invokes it`,
464
+ );
465
+ lines.push(
466
+ ' remove the entry so its death can surface, or wire up the caller.',
467
+ );
468
+ }
469
+ for (const cli of report.phantom) {
470
+ lines.push(`? ${cli} — declared in knip.json "entry" but not on disk`);
471
+ lines.push(' drop the stale path (the file was renamed or removed).');
472
+ }
473
+ for (const cli of report.unsuffixed) {
474
+ lines.push(`! ${cli} — declared in knip.json "entry" without a "!" suffix`);
475
+ lines.push(
476
+ ` write it as ".agents/scripts/${cli}!" — the production pass negates an`,
477
+ );
478
+ lines.push(
479
+ ' unsuffixed entry, so knip still reads the CLI as unreachable.',
480
+ );
481
+ }
482
+ const total = countDivergences(report);
483
+ lines.push(
484
+ `[knip-entries] clis=${report.clis.length} declared=${report.declared.length} ` +
485
+ `missing=${report.missing.length} stale=${report.stale.length} ` +
486
+ `phantom=${report.phantom.length} unsuffixed=${report.unsuffixed.length} ` +
487
+ `${total > 0 ? '(gate fail)' : '(ok)'}`,
488
+ );
489
+ return lines.join('\n');
490
+ }
491
+
492
+ /**
493
+ * Test-only seam. Not API: these helpers are exercised directly by
494
+ * `tests/check-knip-entries.test.js` so the recognizers can be pinned
495
+ * independently of a full fixture tree.
496
+ */
497
+ export const __testing = Object.freeze({
498
+ buildInvocationPatterns,
499
+ // Re-exported from `knip-config-resolver.js` so the suite reaches the whole
500
+ // gate through one seam rather than importing two modules to test one gate.
501
+ collectEntryPatterns: resolverTesting.collectEntryPatterns,
502
+ collectInvocationSurfaces,
503
+ INVOCATION_SURFACES,
504
+ listTopLevelClis,
505
+ readKnipEntries,
506
+ SCRIPTS_DIR,
507
+ stripJsComments,
508
+ });
@@ -74,7 +74,7 @@ export const MAX_LEDGER_BYTES = 1024 * 1024;
74
74
 
75
75
  /**
76
76
  * Resolve the ledger path for an Epic (or the standalone stream when
77
- * `epicId` is `null` — the `story-plan.js` / Epic-less healthcheck case).
77
+ * `epicId` is `null` — the v2 Epic-less plan/healthcheck case).
78
78
  *
79
79
  * @param {number|null} epicId
80
80
  * @param {object} [config] Resolved config (threads `project.paths.tempRoot`).
@@ -83,6 +83,16 @@ export const RUNTIME_FRICTION_CATEGORIES = Object.freeze({
83
83
  * ceilings recalibratable from recorded evidence.
84
84
  */
85
85
  LIGHT_SCOPE_REJECTED: 'light-scope-rejected',
86
+ /**
87
+ * An operator overrode a Story-scope code-review critical blocker with
88
+ * `--override-review-block <reason>`. Deliberately its own
89
+ * bucket: this is the one signal that says a gate's verdict was rejected by
90
+ * a human who then shipped anyway, so a rising `occurrences` count is direct
91
+ * evidence the gate is miscalibrated rather than the code being bad. It is
92
+ * *not* `TOOL_DEGRADED` — the tool ran fine and produced a verdict; what
93
+ * failed was the verdict's usefulness.
94
+ */
95
+ REVIEW_BLOCK_OVERRIDDEN: 'review-block-overridden',
86
96
  });
87
97
 
88
98
  /** Cap on free-form reason text copied into a signal's `details`. */
@@ -17,22 +17,20 @@
17
17
  * document (no hand-rolled drift). `strict: false` matches the repo's
18
18
  * other draft-07 validators (see `tests/schemas/signal-schemas.test.js`).
19
19
  *
20
- * A per-Epic reject tally is persisted under the Epic temp tree
21
- * (`temp/run-<eid>/signal-rejects.json`) so a cross-process reader (the
22
- * loop-health check, a follow-on Story) can surface how many records were
23
- * dropped. The tally is read-modify-written best-effort; a lost increment
24
- * under a write race is acceptable for a diagnostic counter.
20
+ * Story #5003 deleted the persisted per-Epic reject tally
21
+ * (`temp/run-<eid>/signal-rejects.json`) and its `readSignalRejectCount`
22
+ * reader: the tally was keyed on a positive `epicId`, and v2 Stories are
23
+ * standalone, so nothing ever wrote a row and nothing could ever read one.
24
+ * Write-time validation itself is untouched.
25
25
  */
26
26
 
27
27
  import { readFileSync } from 'node:fs';
28
- import fs from 'node:fs/promises';
29
28
  import path from 'node:path';
30
29
  import { fileURLToPath } from 'node:url';
31
30
 
32
31
  import Ajv from 'ajv';
33
32
  import addFormats from 'ajv-formats';
34
33
 
35
- import { runArtifactPath } from '../config/temp-paths.js';
36
34
  import { Logger } from '../Logger.js';
37
35
 
38
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -45,8 +43,6 @@ const SCHEMA_PATH = path.resolve(
45
43
  'signal-event.schema.json',
46
44
  );
47
45
 
48
- const REJECT_TALLY_BASENAME = 'signal-rejects.json';
49
-
50
46
  /**
51
47
  * Compile the signal-event schema once. Returns `null` when the schema
52
48
  * cannot be read or compiled — the caller then treats every record as
@@ -126,79 +122,3 @@ export function validateSignal(record) {
126
122
  const message = _validate.errors?.[0]?.message ?? 'schema validation failed';
127
123
  return { valid: false, violatingField: field, message };
128
124
  }
129
-
130
- /**
131
- * Increment the per-Epic reject tally persisted under the Epic temp tree.
132
- * Best-effort and never throws: a missing/corrupt tally file resets to a
133
- * count of 1. When `epicId` is not a positive integer (e.g. a standalone
134
- * friction record with `epicId: null`) the tally write is skipped — there
135
- * is no Epic temp tree to anchor it to.
136
- *
137
- * @param {{ epicId: number|null|undefined, config?: object, field?: string|null }} args
138
- * @returns {Promise<number|null>} the new count, or null when skipped/failed.
139
- */
140
- export async function recordSignalReject({ epicId, config, field }) {
141
- if (!Number.isInteger(epicId) || epicId <= 0) return null;
142
- let target;
143
- try {
144
- target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
145
- } catch {
146
- return null;
147
- }
148
- try {
149
- let prior = { count: 0, lastField: null };
150
- try {
151
- const raw = await fs.readFile(target, 'utf8');
152
- const parsed = JSON.parse(raw);
153
- if (
154
- parsed &&
155
- typeof parsed === 'object' &&
156
- Number.isFinite(parsed.count)
157
- ) {
158
- prior = { count: parsed.count, lastField: parsed.lastField ?? null };
159
- }
160
- } catch {
161
- // Missing or corrupt file → start a fresh tally.
162
- }
163
- const next = {
164
- count: prior.count + 1,
165
- lastField: field ?? prior.lastField ?? null,
166
- updatedAt: new Date().toISOString(),
167
- };
168
- await fs.mkdir(path.dirname(target), { recursive: true });
169
- await fs.writeFile(target, `${JSON.stringify(next)}\n`, 'utf8');
170
- return next.count;
171
- } catch (err) {
172
- Logger.warn(
173
- `signal-validator: failed to persist reject tally for epic-${epicId}: ${
174
- err instanceof Error ? err.message : String(err)
175
- }`,
176
- );
177
- return null;
178
- }
179
- }
180
-
181
- /**
182
- * Read the current per-Epic reject count. Cross-process readable
183
- * counterpart to {@link recordSignalReject}. Returns 0 when the tally
184
- * file is absent or unreadable.
185
- *
186
- * @param {{ epicId: number, config?: object }} args
187
- * @returns {Promise<number>}
188
- */
189
- export async function readSignalRejectCount({ epicId, config }) {
190
- if (!Number.isInteger(epicId) || epicId <= 0) return 0;
191
- let target;
192
- try {
193
- target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
194
- } catch {
195
- return 0;
196
- }
197
- try {
198
- const raw = await fs.readFile(target, 'utf8');
199
- const parsed = JSON.parse(raw);
200
- return parsed && Number.isFinite(parsed.count) ? parsed.count : 0;
201
- } catch {
202
- return 0;
203
- }
204
- }