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
@@ -1,335 +0,0 @@
1
- /**
2
- * audit-results-graduator.js — Auto-graduate non-blocking audit findings
3
- * from the Epic's unified `verification-results` structured comment into
4
- * routed GitHub follow-up issues. Story #2615 / Epic #2586.
5
- *
6
- * Story #4411 (Epic #4405) unified the former `audit-results` and
7
- * `code-review` structured-comment contracts into one `verification-results`
8
- * contract: this graduator now reads the shared
9
- * {@link VERIFICATION_RESULTS_MARKER} comment rather than the retired
10
- * audit-results structured-comment marker. The lens-aware finding
11
- * parser, the `audit-results::<severity>` + `domain::<lens>` label shape,
12
- * and the audit idempotency marker are unchanged.
13
- *
14
- * As of Story #3845 / Epic #3823 the spawn helper, the path/idempotency
15
- * probes, the `gh issue create` filer, the toggle reader, and the
16
- * route → probe → file walk all live in the shared
17
- * [`graduator-core.js`](./graduator-core.js). This module is the thin
18
- * audit-specific shell: it owns the audit finding parser (lens detection
19
- * plus the 🔵→low / 🟢→suggestion severity mapping), the audit label /
20
- * title / body shape, and the audit idempotency marker. Behaviour is
21
- * identical to the pre-consolidation graduator.
22
- *
23
- * - Read the unified `verification-results` structured comment off the
24
- * Epic ticket via the injected provider (`getTicketComments`).
25
- * - For each non-blocking finding (severity high/medium/low/suggestion
26
- * — i.e. anything that is NOT a 🔴 Critical Blocker), check that
27
- * the cited file still exists in the merged tree.
28
- * - Route by source classification (framework vs consumer) using
29
- * `classifyPathSource`. Cross-repo findings are recorded under
30
- * `skipped: 'cross-repo-deferred'` and the would-be `gh issue
31
- * create` invocation is logged — never shelled out across repos.
32
- * - File a follow-up issue with `gh issue create` carrying:
33
- * - `meta::audit-finding`
34
- * - `meta::framework-gap` or `meta::consumer-improvement`
35
- * - `audit-results::<severity>` (high|medium|low|suggestion)
36
- * - `domain::<lens-name>` (e.g. `domain::audit-security`)
37
- * - Embed an idempotency marker in each body:
38
- * <!-- audit-results-followup: epic-<id>-finding-<idx> -->
39
- * Before filing, probe via `gh search issues "<marker>"` and skip
40
- * findings whose marker is already present.
41
- * - Short-circuit when
42
- * `config.delivery.feedbackLoop.auditResultsAutoFile` is `false` —
43
- * return `{ filed: [], skipped: [{ reason: 'toggle-disabled' }],
44
- * errors: [] }`.
45
- * - NEVER throw. Every failure path is captured in `errors[]`.
46
- */
47
-
48
- import {
49
- fingerprintFinding,
50
- fingerprintFooter,
51
- semanticKeyFooter,
52
- semanticKeyFor,
53
- } from '../findings/route-finding.js';
54
- import {
55
- contentFingerprint,
56
- graduate,
57
- makeIsAutoFileEnabled,
58
- NO_VERIFICATION_RESULTS_COMMENT_REASON,
59
- VERIFICATION_RESULTS_MARKER,
60
- } from './graduator-core.js';
61
-
62
- /**
63
- * Resolve the toggle from the resolved agentrc config. Defaults to `true`
64
- * — the feature is opt-out, not opt-in.
65
- *
66
- * @param {object|undefined|null} config
67
- * @returns {boolean}
68
- */
69
- export const isAutoFileEnabled = makeIsAutoFileEnabled('auditResultsAutoFile');
70
-
71
- /**
72
- * Severity → label mapping. Only non-blocking severities have a route;
73
- * 🔴 Critical Blocker is explicitly filtered out upstream.
74
- */
75
- const SEVERITY_LABEL = Object.freeze({
76
- high: 'audit-results::high',
77
- medium: 'audit-results::medium',
78
- low: 'audit-results::low',
79
- suggestion: 'audit-results::suggestion',
80
- });
81
-
82
- /**
83
- * Map a classification source to its meta routing label.
84
- *
85
- * @param {string} source
86
- * @returns {string}
87
- */
88
- function metaSourceLabel(source) {
89
- return source === 'framework'
90
- ? 'meta::framework-gap'
91
- : 'meta::consumer-improvement';
92
- }
93
-
94
- /**
95
- * Build the **legacy** idempotency marker for a given epicId / finding
96
- * index. Superseded by the content-hash marker
97
- * ({@link buildContentMarker}) at the Story #4415 cutover, but still
98
- * probed for so follow-ups filed before the cutover are recognized and
99
- * not re-filed. An HTML comment so it survives markdown rendering without
100
- * leaking into the visible body; the idempotency probe strips the comment
101
- * delimiters before querying `gh search` (the raw `<!-- … -->` form never
102
- * matches the index — Story #4657).
103
- *
104
- * @param {number} epicId
105
- * @param {number} index — zero-based finding ordinal within the Epic.
106
- * @returns {string}
107
- */
108
- export function buildIdempotencyMarker(epicId, index) {
109
- return `<!-- audit-results-followup: epic-${epicId}-finding-${index} -->`;
110
- }
111
-
112
- /**
113
- * Build the content-hash idempotency marker embedded in freshly filed
114
- * follow-up bodies. Derived from the finding's `lens|path|summary` triple
115
- * so the marker is stable across sibling insert/remove/reorder churn in
116
- * the source `audit-results` comment (Story #4415). An HTML comment so it
117
- * survives markdown rendering without leaking into the visible body; the
118
- * idempotency probe strips the comment delimiters before querying
119
- * `gh search` (the raw `<!-- … -->` form never matches the index —
120
- * Story #4657).
121
- *
122
- * @param {number} epicId
123
- * @param {{ lens?: string, path?: string, summary?: string }} finding
124
- * @returns {string}
125
- */
126
- export function buildContentMarker(epicId, finding) {
127
- const fp = contentFingerprint({
128
- category: finding.lens,
129
- path: finding.path,
130
- title: finding.summary,
131
- });
132
- return `<!-- audit-results-followup: epic-${epicId}-${fp} -->`;
133
- }
134
-
135
- /**
136
- * Project a graduator audit finding onto the canonical identity the shared
137
- * dedup helper (`lib/findings/route-finding.js`) fingerprints over, so a
138
- * close-time graduator filing and a sweep-time `/audit-to-stories` filing
139
- * share ONE identity namespace (Story #4626). The graduator's `lens`
140
- * (`audit-<dimension>`) maps to the identity `area`; its cited `path` is the
141
- * primary file; its `summary` is the title. Historically the graduator only
142
- * stamped its own content-hash `audit-results-followup` marker, disjoint from
143
- * route-finding's `audit-fingerprints` footer — so a `/audit-to-stories`
144
- * sweep could not recognize a graduator-filed issue and would re-file it.
145
- *
146
- * @param {{ lens?: string, path?: string, summary?: string }} finding
147
- * @returns {{ title: string, area: string, primaryFile: string, severity: string, labels: string[] }}
148
- */
149
- export function toCanonicalFinding(finding) {
150
- const lens = typeof finding?.lens === 'string' ? finding.lens : '';
151
- return {
152
- title: typeof finding?.summary === 'string' ? finding.summary : '',
153
- area: lens,
154
- primaryFile: typeof finding?.path === 'string' ? finding.path : '',
155
- severity: '',
156
- labels: lens ? [lens] : [],
157
- };
158
- }
159
-
160
- /**
161
- * Render the canonical `audit-fingerprints` (and location-based
162
- * `audit-semantic-keys`) footer for a graduator finding, computed off the
163
- * shared helper's canonical hash. Stamped into every filed follow-up body so
164
- * the `/audit-to-stories` dedup probe recognizes a graduator-filed issue
165
- * (Story #4626).
166
- *
167
- * @param {{ lens?: string, path?: string, summary?: string }} finding
168
- * @returns {string}
169
- */
170
- export function canonicalFingerprintFooter(finding) {
171
- const canonical = toCanonicalFinding(finding);
172
- const { full } = fingerprintFinding(canonical);
173
- const key = semanticKeyFor(canonical);
174
- const lines = [fingerprintFooter(full)];
175
- if (key) lines.push(semanticKeyFooter(key));
176
- return lines.join('\n');
177
- }
178
-
179
- /**
180
- * Parse the rendered audit-results markdown into a list of findings.
181
- * The format produced by `epic-audit.md` Step 4 groups findings under
182
- * `#### <lens-name>` headings; each finding line begins with a severity
183
- * emoji and embeds the cited path inside backticks. 🔴 critical findings
184
- * are filtered out (they're blocking — the Epic stops on those).
185
- *
186
- * Findings the Phase 4 remediation loop already fixed on-branch are
187
- * rendered under a **"Fixed on-branch"** heading (Story #4399) with a ✅
188
- * prefix so they no longer parse as open findings. As a belt-and-suspenders
189
- * guard the parser also skips every line inside a Fixed-on-branch section
190
- * outright, so a remediated 🟡 Medium never spawns a ghost follow-up issue
191
- * even if its line retains its original severity emoji.
192
- *
193
- * Pure. Exported so the parser can be unit-tested in isolation.
194
- *
195
- * @param {string} body
196
- * @returns {Array<{ severity: 'high'|'medium'|'low'|'suggestion', lens: string, path: string, summary: string, index: number }>}
197
- */
198
- export function parseFindings(body) {
199
- if (typeof body !== 'string' || body.length === 0) return [];
200
- const findings = [];
201
- const lines = body.split(/\r?\n/);
202
- let idx = 0;
203
- let lens = 'unknown';
204
- let inFixedSection = false;
205
- for (const rawLine of lines) {
206
- const trimmed = rawLine.trim();
207
- if (trimmed.length === 0) continue;
208
-
209
- // Any markdown heading resets the Fixed-on-branch guard and, when it
210
- // names a known audit family (`audit-*`), sets the active lens. A
211
- // "Fixed on-branch" heading opens a section whose entries never
212
- // graduate (Story #4399).
213
- const headingMatch = trimmed.match(/^#{2,6}\s+(.+)$/);
214
- if (headingMatch) {
215
- inFixedSection = /fixed on-branch/i.test(headingMatch[1]);
216
- const lensMatch = headingMatch[1].match(/^(audit-[a-z0-9-]+)/i);
217
- if (lensMatch) lens = lensMatch[1];
218
- continue;
219
- }
220
-
221
- if (inFixedSection) continue;
222
-
223
- let severity = null;
224
- if (trimmed.startsWith('🔴')) {
225
- // Critical Blocker — skip; never graduates.
226
- continue;
227
- }
228
- if (trimmed.startsWith('🟠')) severity = 'high';
229
- else if (trimmed.startsWith('🟡')) severity = 'medium';
230
- else if (trimmed.startsWith('🟢')) severity = 'suggestion';
231
- else if (trimmed.startsWith('🔵')) severity = 'low';
232
- else continue;
233
-
234
- const pathMatch = trimmed.match(/`([^`]+)`/);
235
- if (!pathMatch) continue;
236
- const path = pathMatch[1];
237
-
238
- findings.push({
239
- severity,
240
- lens,
241
- path,
242
- summary: trimmed,
243
- index: idx,
244
- });
245
- idx += 1;
246
- }
247
- return findings;
248
- }
249
-
250
- /**
251
- * Auto-graduate non-blocking audit-results findings into routed
252
- * follow-up issues. Never throws. Thin wrapper around the shared
253
- * `graduate()` walk with the audit-specific behaviour bundle.
254
- *
255
- * @param {object} opts
256
- * @param {number} opts.epicId
257
- * @param {object} opts.provider — ticketing provider exposing
258
- * `getTicketComments(ticketId)`.
259
- * @param {object} [opts.config] — resolved agentrc.
260
- * @param {{owner: string, repo: string}} opts.currentRepo — repo the
261
- * listener is running inside; used for the cross-repo guard.
262
- * @param {{owner: string, repo: string}} [opts.frameworkRepo] — where
263
- * framework-tagged findings route. Defaults to `currentRepo`.
264
- * @param {string} [opts.gitRef='HEAD']
265
- * @param {Function} [opts.classifier=classifyPathSource]
266
- * @param {string} [opts.ghPath='gh']
267
- * @param {Function} [opts.spawnImpl]
268
- * @param {string} [opts.cwd]
269
- * @param {{info?: Function, warn?: Function, debug?: Function}} [opts.logger]
270
- * @returns {Promise<{
271
- * filed: Array<{ index: number, severity: string, lens: string, path: string, source: string, repo: string, url: string|null }>,
272
- * skipped: Array<{ index?: number, reason: string, path?: string, severity?: string, lens?: string }>,
273
- * errors: string[],
274
- * }>}
275
- */
276
- export async function graduateAuditResults(opts = {}) {
277
- return graduate({
278
- ...opts,
279
- spec: {
280
- fnName: 'graduateAuditResults',
281
- isAutoFileEnabled,
282
- commentMarker: VERIFICATION_RESULTS_MARKER,
283
- noCommentReason: NO_VERIFICATION_RESULTS_COMMENT_REASON,
284
- parseFindings,
285
- buildContentMarker,
286
- buildLegacyMarker: buildIdempotencyMarker,
287
- crossRepoCommentAttrs: { graduator: 'audit-results' },
288
- decorateRecord: (record, finding) => {
289
- record.lens = finding.lens;
290
- return record;
291
- },
292
- buildCrossRepoLog: ({ finding, routedRepo, source }) => {
293
- const labels = [
294
- 'meta::audit-finding',
295
- metaSourceLabel(source),
296
- SEVERITY_LABEL[finding.severity],
297
- `domain::${finding.lens}`,
298
- ];
299
- return `[audit-results-graduator] cross-repo skip (would file in ${routedRepo.owner}/${routedRepo.repo}): gh issue create --repo ${routedRepo.owner}/${routedRepo.repo} --title "Audit follow-up (${finding.lens}): ${finding.path}" --label "${labels.join(',')}"`;
300
- },
301
- buildFollowUp: ({ finding, source, epicId, idMarker }) => {
302
- const title = `Audit follow-up (${finding.lens}): ${finding.path}`;
303
- const body = [
304
- idMarker,
305
- '',
306
- `Auto-filed from the Epic #${epicId} audit-results pass.`,
307
- '',
308
- `**Lens**: ${finding.lens}`,
309
- `**Severity**: ${finding.severity}`,
310
- `**Source**: ${source}`,
311
- `**Path**: \`${finding.path}\``,
312
- '',
313
- '### Finding',
314
- '',
315
- finding.summary,
316
- '',
317
- `_See Epic #${epicId} for the full audit-results report._`,
318
- '',
319
- // Canonical dedup identity so the `/audit-to-stories` sweep-time
320
- // probe recognizes this close-time filing and never re-files it
321
- // (Story #4626). The content-hash `audit-results-followup` marker
322
- // above stays the graduator's own re-file guard.
323
- canonicalFingerprintFooter(finding),
324
- ].join('\n');
325
- const labels = [
326
- 'meta::audit-finding',
327
- metaSourceLabel(source),
328
- SEVERITY_LABEL[finding.severity],
329
- `domain::${finding.lens}`,
330
- ];
331
- return { title, body, labels };
332
- },
333
- },
334
- });
335
- }
@@ -1,239 +0,0 @@
1
- /**
2
- * lib/mutation/baseline-snapshot.js — Read/write the mutation baseline file
3
- * consumed by the story-close mutation gate (Story #1736, Epic #1720).
4
- *
5
- * The baseline file lives at the path configured by
6
- * `delivery.quality.gates.mutation.baselinePath` (default
7
- * `baselines/mutation.json`) and conforms to:
8
- *
9
- * {
10
- * "generatedAt": "<ISO-8601 timestamp>",
11
- * "tolerancePct": <number, mutation-score percent points>,
12
- * "workspaces": {
13
- * "*": <number, mutation-score percent points 0–100>,
14
- * "<workspace-name>": <number>
15
- * }
16
- * }
17
- *
18
- * The `"*"` catch-all key handles single-workspace consumers; real
19
- * workspace names handle monorepo consumers. Generic applier code reads
20
- * `workspaces[name] ?? workspaces['*']`.
21
- *
22
- * `tolerancePct` carries the per-workspace mutation-score drop tolerance
23
- * in percent points (matches the `delivery.quality.gates.mutation.tolerance`
24
- * resolver default of 0). When a workspace's measured score is below
25
- * `baseline - tolerancePct`, the gate fails.
26
- *
27
- * This module mirrors the dependency-injection style used by
28
- * `lib/baseline-snapshot.js`: the fs surface and the clock are injected so
29
- * unit tests never touch real files. The two writer paths
30
- * (`readBaseline`, `writeBaseline`) are split because the typical caller
31
- * sequence is read-then-merge-then-write — a refresh CLI reads the prior
32
- * baseline to preserve the configured `tolerancePct` if the Stryker run
33
- * does not override it (the former `update-mutation-baseline.js` wrapper
34
- * was retired in #4482; the gate ships dormant).
35
- */
36
-
37
- import fs from 'node:fs';
38
- import path from 'node:path';
39
-
40
- /**
41
- * Default tolerance in percent points. Matches the framework-default
42
- * tolerance value resolved from `delivery.quality.gates.mutation.tolerance`
43
- * (`{ kind: 'absolute', value: 0 }`). A drop of 0% pts means any
44
- * regression below the recorded baseline trips the gate.
45
- */
46
- export const DEFAULT_TOLERANCE_PCT = 0;
47
-
48
- /**
49
- * Default baseline file path, relative to the repo root. Mirrors the
50
- * resolver default in `lib/config/quality.js`.
51
- */
52
- export const DEFAULT_BASELINE_PATH = 'baselines/mutation.json';
53
-
54
- /**
55
- * Read the mutation baseline at `baselinePath`. Returns `null` when the
56
- * file does not exist so the caller can distinguish "no baseline yet"
57
- * (the gate self-skips with an explanatory line) from a parse error.
58
- *
59
- * Throws when the file exists but is malformed (invalid JSON, missing
60
- * required keys, wrong types) — silent corruption must not relax the gate.
61
- *
62
- * @param {string} baselinePath Absolute or cwd-relative path.
63
- * @param {{ cwd?: string, fsImpl?: { existsSync: typeof fs.existsSync, readFileSync: typeof fs.readFileSync } }} [opts]
64
- * @returns {{ generatedAt: string, tolerancePct: number, workspaces: Record<string, number> } | null}
65
- */
66
- export function readBaseline(baselinePath, opts = {}) {
67
- const cwd = opts.cwd ?? process.cwd();
68
- const fsImpl = opts.fsImpl ?? fs;
69
- const abs = path.isAbsolute(baselinePath)
70
- ? baselinePath
71
- : path.resolve(cwd, baselinePath);
72
- if (!fsImpl.existsSync(abs)) return null;
73
- const raw = fsImpl.readFileSync(abs, 'utf8');
74
- let parsed;
75
- try {
76
- parsed = JSON.parse(raw);
77
- } catch (err) {
78
- throw new Error(
79
- `[mutation/baseline-snapshot] failed to parse ${baselinePath}: ${err instanceof Error ? err.message : String(err)}`,
80
- );
81
- }
82
- return validateBaseline(parsed, baselinePath);
83
- }
84
-
85
- /**
86
- * Atomically rewrite the mutation baseline at `baselinePath` from
87
- * `payload`. "Atomic" here means write-temp-then-rename, so a SIGKILL
88
- * mid-write never leaves a half-written baseline file on disk.
89
- *
90
- * The on-disk shape is canonicalised: `workspaces` keys are sorted
91
- * alphabetically (with `"*"` always first) and the file ends with a
92
- * trailing newline. This produces a byte-stable serialisation, which
93
- * lets a refresh caller detect "no change" runs (skip the
94
- * baseline-refresh commit when re-running Stryker produces an identical
95
- * baseline).
96
- *
97
- * @param {string} baselinePath
98
- * @param {{ generatedAt?: string, tolerancePct?: number, workspaces: Record<string, number> }} payload
99
- * @param {{ cwd?: string, fsImpl?: object, clock?: () => Date }} [opts]
100
- * @returns {{ path: string, bytes: string, didChange: boolean }}
101
- */
102
- export function writeBaseline(baselinePath, payload, opts = {}) {
103
- const cwd = opts.cwd ?? process.cwd();
104
- const fsImpl = opts.fsImpl ?? fs;
105
- const clock = opts.clock ?? (() => new Date());
106
- const abs = path.isAbsolute(baselinePath)
107
- ? baselinePath
108
- : path.resolve(cwd, baselinePath);
109
- const generatedAt = payload.generatedAt ?? clock().toISOString();
110
- const tolerancePct = Number.isFinite(payload.tolerancePct)
111
- ? payload.tolerancePct
112
- : DEFAULT_TOLERANCE_PCT;
113
- if (
114
- !payload ||
115
- typeof payload.workspaces !== 'object' ||
116
- payload.workspaces === null
117
- ) {
118
- throw new TypeError(
119
- '[mutation/baseline-snapshot] writeBaseline: payload.workspaces must be an object',
120
- );
121
- }
122
- const workspaces = canonicaliseWorkspaces(payload.workspaces);
123
- const envelope = { generatedAt, tolerancePct, workspaces };
124
- const bytes = `${JSON.stringify(envelope, null, 2)}\n`;
125
- let prior = null;
126
- if (fsImpl.existsSync(abs)) {
127
- try {
128
- prior = fsImpl.readFileSync(abs, 'utf8');
129
- } catch {
130
- prior = null;
131
- }
132
- }
133
- if (prior === bytes) {
134
- return { path: abs, bytes, didChange: false };
135
- }
136
- fsImpl.mkdirSync?.(path.dirname(abs), { recursive: true });
137
- // Atomic write: write to a sibling temp file then rename. `renameSync`
138
- // is atomic on POSIX and best-effort on Windows (which is good enough —
139
- // the previous file is never half-overwritten).
140
- const tmp = `${abs}.tmp-${process.pid}`;
141
- fsImpl.writeFileSync(tmp, bytes);
142
- try {
143
- fsImpl.renameSync?.(tmp, abs);
144
- } catch {
145
- // `fs` shims in tests may not implement renameSync — fall back to a
146
- // direct write + best-effort unlink so the in-memory store still
147
- // converges to the right state.
148
- fsImpl.writeFileSync(abs, bytes);
149
- try {
150
- fsImpl.unlinkSync?.(tmp);
151
- } catch {
152
- /* ignore */
153
- }
154
- }
155
- return { path: abs, bytes, didChange: true };
156
- }
157
-
158
- /**
159
- * Validate that `parsed` matches the baseline envelope contract. Returns
160
- * the parsed object unchanged when it conforms; throws otherwise.
161
- *
162
- * Exposed for unit testing the validator independently of fs I/O.
163
- *
164
- * @param {unknown} parsed
165
- * @param {string} sourceLabel
166
- * @returns {{ generatedAt: string, tolerancePct: number, workspaces: Record<string, number> }}
167
- */
168
- export function validateBaseline(parsed, sourceLabel = '<input>') {
169
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
170
- throw new Error(
171
- `[mutation/baseline-snapshot] ${sourceLabel}: expected an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`,
172
- );
173
- }
174
- const { generatedAt, tolerancePct, workspaces } =
175
- /** @type {Record<string, unknown>} */ (parsed);
176
- if (typeof generatedAt !== 'string' || generatedAt.length === 0) {
177
- throw new Error(
178
- `[mutation/baseline-snapshot] ${sourceLabel}: 'generatedAt' must be a non-empty string`,
179
- );
180
- }
181
- if (
182
- !Number.isFinite(tolerancePct) ||
183
- /** @type {number} */ (tolerancePct) < 0
184
- ) {
185
- throw new Error(
186
- `[mutation/baseline-snapshot] ${sourceLabel}: 'tolerancePct' must be a non-negative number`,
187
- );
188
- }
189
- if (
190
- !workspaces ||
191
- typeof workspaces !== 'object' ||
192
- Array.isArray(workspaces)
193
- ) {
194
- throw new Error(
195
- `[mutation/baseline-snapshot] ${sourceLabel}: 'workspaces' must be an object`,
196
- );
197
- }
198
- for (const [name, score] of Object.entries(workspaces)) {
199
- if (typeof name !== 'string' || name.length === 0) {
200
- throw new Error(
201
- `[mutation/baseline-snapshot] ${sourceLabel}: workspace name must be a non-empty string`,
202
- );
203
- }
204
- if (
205
- !Number.isFinite(score) ||
206
- /** @type {number} */ (score) < 0 ||
207
- /** @type {number} */ (score) > 100
208
- ) {
209
- throw new Error(
210
- `[mutation/baseline-snapshot] ${sourceLabel}: workspace "${name}" score must be a number in [0, 100], got ${JSON.stringify(score)}`,
211
- );
212
- }
213
- }
214
- return /** @type {{ generatedAt: string, tolerancePct: number, workspaces: Record<string, number> }} */ (
215
- parsed
216
- );
217
- }
218
-
219
- /**
220
- * Sort `workspaces` keys deterministically so the on-disk serialisation
221
- * is byte-stable. The `"*"` catch-all key is pinned to the first slot;
222
- * the remaining keys sort alphabetically.
223
- *
224
- * @param {Record<string, number>} workspaces
225
- * @returns {Record<string, number>}
226
- */
227
- function canonicaliseWorkspaces(workspaces) {
228
- const keys = Object.keys(workspaces);
229
- const sorted = keys.slice().sort((a, b) => {
230
- if (a === '*') return -1;
231
- if (b === '*') return 1;
232
- return a.localeCompare(b);
233
- });
234
- const out = /** @type {Record<string, number>} */ ({});
235
- for (const k of sorted) {
236
- out[k] = workspaces[k];
237
- }
238
- return out;
239
- }
@@ -1,119 +0,0 @@
1
- /**
2
- * lib/mutation/config-detector.js — Detect a Stryker config in a repo
3
- * (Story #1736, Task #1754).
4
- *
5
- * The mutation gate is on-by-default but self-skips when Stryker is not
6
- * configured. The detector recognises the canonical Stryker config
7
- * surfaces:
8
- *
9
- * - `stryker.conf.js`
10
- * - `stryker.conf.cjs`
11
- * - `stryker.conf.mjs`
12
- * - `stryker.conf.json`
13
- * - `stryker.config.js` / `.cjs` / `.mjs` / `.json` (Stryker's alternate name)
14
- * - a `"stryker"` key in the repo-root `package.json`
15
- *
16
- * When an explicit `configPath` is supplied (from the gate's
17
- * `strykerConfigPath` block), that path takes precedence — the file is
18
- * checked for existence and returned without re-walking the canonical
19
- * surfaces.
20
- *
21
- * Pure with respect to the injected fs surface; never invokes Stryker
22
- * or any child process.
23
- */
24
-
25
- import fs from 'node:fs';
26
- import path from 'node:path';
27
-
28
- const CANONICAL_FILES = Object.freeze([
29
- 'stryker.conf.js',
30
- 'stryker.conf.cjs',
31
- 'stryker.conf.mjs',
32
- 'stryker.conf.json',
33
- 'stryker.config.js',
34
- 'stryker.config.cjs',
35
- 'stryker.config.mjs',
36
- 'stryker.config.json',
37
- ]);
38
-
39
- /**
40
- * @typedef {Object} DetectionResult
41
- * @property {boolean} found
42
- * @property {'config-file' | 'package-json' | 'explicit' | null} via
43
- * @property {string | null} path Absolute path to the config artifact (or
44
- * the package.json that carries the `stryker` key).
45
- * @property {string} [reason] Populated when `found` is false.
46
- */
47
-
48
- /**
49
- * Detect a Stryker configuration in `cwd`.
50
- *
51
- * @param {{
52
- * cwd?: string,
53
- * configPath?: string | null,
54
- * fsImpl?: { existsSync: typeof fs.existsSync, readFileSync: typeof fs.readFileSync },
55
- * }} [opts]
56
- * @returns {DetectionResult}
57
- */
58
- export function detectStrykerConfig(opts = {}) {
59
- const cwd = opts.cwd ?? process.cwd();
60
- const fsImpl = opts.fsImpl ?? fs;
61
- const explicit = opts.configPath;
62
-
63
- if (typeof explicit === 'string' && explicit.length > 0) {
64
- const abs = path.isAbsolute(explicit)
65
- ? explicit
66
- : path.resolve(cwd, explicit);
67
- if (fsImpl.existsSync(abs)) {
68
- return { found: true, via: 'explicit', path: abs };
69
- }
70
- return {
71
- found: false,
72
- via: null,
73
- path: null,
74
- reason: `explicit strykerConfigPath '${explicit}' does not exist`,
75
- };
76
- }
77
-
78
- for (const filename of CANONICAL_FILES) {
79
- const abs = path.resolve(cwd, filename);
80
- if (fsImpl.existsSync(abs)) {
81
- return { found: true, via: 'config-file', path: abs };
82
- }
83
- }
84
-
85
- const pkgPath = path.resolve(cwd, 'package.json');
86
- if (fsImpl.existsSync(pkgPath)) {
87
- try {
88
- const raw = fsImpl.readFileSync(pkgPath, 'utf8');
89
- const parsed = JSON.parse(raw);
90
- if (
91
- parsed &&
92
- typeof parsed === 'object' &&
93
- parsed.stryker &&
94
- typeof parsed.stryker === 'object'
95
- ) {
96
- return { found: true, via: 'package-json', path: pkgPath };
97
- }
98
- } catch {
99
- // Malformed package.json — fall through to "not found". The repo
100
- // is already broken in other ways; the mutation gate skip is a
101
- // benign symptom, not the cause.
102
- }
103
- }
104
-
105
- return {
106
- found: false,
107
- via: null,
108
- path: null,
109
- reason: 'no Stryker config found',
110
- };
111
- }
112
-
113
- /**
114
- * Exposed for testing — the canonical filename list, frozen.
115
- * @returns {readonly string[]}
116
- */
117
- export function getCanonicalConfigFilenames() {
118
- return CANONICAL_FILES;
119
- }