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,376 @@
1
+ /**
2
+ * conventional-subject.js — the pure Conventional-Commit rules behind the
3
+ * squash-merge subject a standalone Story lands on `main`.
4
+ *
5
+ * Split out of `normalize-pr-title.js` so the rules that decide
6
+ * what the release notes say are testable without a git read. The three rules
7
+ * here each closed a defect observed live on `main`:
8
+ *
9
+ * 1. **Type precedence follows release impact.** A multi-commit Story used
10
+ * to resolve its type off a hand-ordered list in which `docs` outranked
11
+ * `chore` for no stated reason. Story #5004 (`chore(gates)`, `docs(ci)`,
12
+ * `chore(baselines)`) therefore landed as
13
+ * `docs: check/CI gate sweep …` — a change that deleted five
14
+ * `lib/checks` modules, two CLIs and a dependency, filed under
15
+ * documentation. `TYPE_RANK` now mirrors `changelog-sections` in
16
+ * `release-please-config.json`: rank IS how visible the type is in the
17
+ * release notes. Types that render identically (the six `hidden: true`
18
+ * ones) share a rank, because there is no honest ordering between them —
19
+ * those ties break on how much of the branch carries the type, then on
20
+ * the Story's primary (oldest) commit.
21
+ *
22
+ * 2. **Casing leaves acronyms alone.** The old synthesizer lowercased the
23
+ * first character unconditionally to satisfy commitlint's `subject-case`
24
+ * rule, which turned Story #5002's "CRAP surface diet" into
25
+ * `refactor: cRAP surface diet`. `shapeDescription` lowercases only when
26
+ * the leading word is not an all-caps token, so CRAP / CI / QA / API
27
+ * survive. That is a deliberate, narrow trade: commitlint's
28
+ * `subject-case` treats ANY leading capital as sentence-case and would
29
+ * flag `CRAP surface diet` — but commitlint never runs on a PR title or
30
+ * a GitHub squash subject (see `rules/git-conventions.md` §
31
+ * Conventional Commits), so the rule it would fail is not a gate this
32
+ * subject passes through, while a mangled acronym is permanent in the
33
+ * changelog.
34
+ *
35
+ * 3. **A breaking change survives the squash.** release-please only sees a
36
+ * breaking change through a `!` in the subject or a `BREAKING CHANGE:`
37
+ * footer. Story #5004 removed `project.commands.lintBaseline` from a
38
+ * schema block that is `additionalProperties: false` — a hard consumer
39
+ * break — described in prose in the commit body and therefore invisible
40
+ * to the parser. `collectBreakingNotes` reads the footer out of any
41
+ * constituent commit (or a Story-body declaration) and `markBreaking`
42
+ * puts the `!` where the parser looks.
43
+ */
44
+
45
+ /**
46
+ * The Conventional-Commit types Mandrel accepts. Mirrors
47
+ * `commitlint.config.js` → `type-enum` and `release-please-config.json` →
48
+ * `changelog-sections`. Kept in sync by hand (single hard-cutover, no
49
+ * shim) — adding a type means touching all three.
50
+ */
51
+ const CONVENTIONAL_TYPES = Object.freeze([
52
+ 'feat',
53
+ 'fix',
54
+ 'perf',
55
+ 'refactor',
56
+ 'revert',
57
+ 'docs',
58
+ 'style',
59
+ 'chore',
60
+ 'test',
61
+ 'build',
62
+ 'ci',
63
+ ]);
64
+
65
+ /**
66
+ * Release-impact rank per type — LOWER wins. Derived from
67
+ * `changelog-sections` in `release-please-config.json`: the five types that
68
+ * render a visible section are ordered by how much a reader needs to see
69
+ * them, and the six `hidden: true` types share the bottom rank because the
70
+ * release notes draw no distinction between them.
71
+ *
72
+ * @type {Readonly<Record<string, number>>}
73
+ */
74
+ const TYPE_RANK = Object.freeze({
75
+ feat: 0, // "Added"
76
+ fix: 1, // "Fixed"
77
+ perf: 2, // "Performance"
78
+ revert: 3, // "Reverted"
79
+ refactor: 4, // "Changed"
80
+ docs: 5, // hidden
81
+ style: 5, // hidden
82
+ chore: 5, // hidden
83
+ test: 5, // hidden
84
+ build: 5, // hidden
85
+ ci: 5, // hidden
86
+ });
87
+
88
+ /** Rank for a type absent from `TYPE_RANK` — always loses. */
89
+ const UNRANKED = Number.MAX_SAFE_INTEGER;
90
+
91
+ const TYPE_GROUP = CONVENTIONAL_TYPES.join('|');
92
+
93
+ // Anchored Conventional-Commit header matcher:
94
+ // <type>(<optional scope>)<optional !>: <non-empty description>
95
+ // Mirrors the shape `@commitlint/config-conventional` enforces (a known
96
+ // type, an optional parenthesised scope, an optional breaking `!`, a
97
+ // colon-space separator, and a non-empty subject). Used for the pure
98
+ // "is this already conventional?" check and to pull the type off a branch
99
+ // commit subject without spawning commitlint per call.
100
+ const CONVENTIONAL_HEADER_RE = new RegExp(
101
+ `^(?:${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?: \\S.*$`,
102
+ );
103
+ const LEADING_TYPE_RE = new RegExp(
104
+ `^(${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?(!?):`,
105
+ );
106
+ // Splits a conventional header into `<type><scope?>`, `<!?>`, `<description>`
107
+ // so the breaking marker can be inserted at the one position the parser reads.
108
+ const HEADER_PARTS_RE = new RegExp(
109
+ `^((?:${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?)(!?): (.*)$`,
110
+ );
111
+
112
+ /**
113
+ * The Conventional-Commits breaking footer, in both spellings the spec
114
+ * defines (`BREAKING CHANGE:` and the hyphenated `BREAKING-CHANGE:`). Case is
115
+ * significant — the spec requires uppercase, and so does
116
+ * `conventional-commits-parser`'s default `noteKeywords`, so matching
117
+ * case-insensitively here would announce breaks release-please will not.
118
+ */
119
+ const BREAKING_FOOTER_RE = /^BREAKING[ -]CHANGE:[ \t]*(.*)$/;
120
+
121
+ /** A git-trailer-shaped line (`Some-Token: value`) — ends a footer's text. */
122
+ const TRAILER_RE = /^[A-Za-z][A-Za-z-]*:[ \t]/;
123
+ /** An opening or closing markdown code fence: ``` or ~~~, optionally indented. */
124
+ const FENCE_RE = /^\s{0,3}(`{3,}|~{3,})/;
125
+
126
+ /**
127
+ * True iff `subject` is a parseable Conventional Commit subject under the
128
+ * repo's type vocabulary. Pure.
129
+ *
130
+ * @param {string} subject
131
+ * @returns {boolean}
132
+ */
133
+ export function isConventionalSubject(subject) {
134
+ if (typeof subject !== 'string') return false;
135
+ return CONVENTIONAL_HEADER_RE.test(subject.trim());
136
+ }
137
+
138
+ /**
139
+ * Extract the Conventional-Commit `type` from a single commit subject, or
140
+ * `null` when the subject is not conventional. Pure.
141
+ *
142
+ * @param {string} subject
143
+ * @returns {string|null}
144
+ */
145
+ function parseConventionalType(subject) {
146
+ if (typeof subject !== 'string') return null;
147
+ const match = subject.trim().match(LEADING_TYPE_RE);
148
+ return match ? match[1] : null;
149
+ }
150
+
151
+ /**
152
+ * Order two type candidates: release impact first, then how much of the
153
+ * branch carries the type, then the earliest commit that used it. The second
154
+ * and third keys only ever decide a tie inside the hidden tier — every
155
+ * visible type holds its rank alone.
156
+ *
157
+ * @param {{rank: number, count: number, firstIndex: number}} a
158
+ * @param {{rank: number, count: number, firstIndex: number}} b
159
+ * @returns {number}
160
+ */
161
+ function compareTypeCandidates(a, b) {
162
+ if (a.rank !== b.rank) return a.rank - b.rank;
163
+ if (a.count !== b.count) return b.count - a.count;
164
+ return a.firstIndex - b.firstIndex;
165
+ }
166
+
167
+ /**
168
+ * Pick the type the squash subject should carry from the branch's own commit
169
+ * subjects. Returns `null` when no subject is conventional.
170
+ *
171
+ * `subjects` MUST be oldest-first: the final tie-break reads index 0 as the
172
+ * Story's primary commit, the one whose type the operator chose before any
173
+ * fixup or baseline-refresh commit piled on.
174
+ *
175
+ * @param {string[]} subjects Commit subjects, oldest first.
176
+ * @returns {string|null}
177
+ */
178
+ export function pickDominantType(subjects) {
179
+ /** @type {Map<string, {type: string, rank: number, count: number, firstIndex: number}>} */
180
+ const candidates = new Map();
181
+ const list = Array.isArray(subjects) ? subjects : [];
182
+ list.forEach((subject, index) => {
183
+ const type = parseConventionalType(subject);
184
+ if (!type) return;
185
+ const seen = candidates.get(type);
186
+ if (seen) {
187
+ seen.count += 1;
188
+ return;
189
+ }
190
+ candidates.set(type, {
191
+ type,
192
+ rank: TYPE_RANK[type] ?? UNRANKED,
193
+ count: 1,
194
+ firstIndex: index,
195
+ });
196
+ });
197
+ if (candidates.size === 0) return null;
198
+ return [...candidates.values()].sort(compareTypeCandidates)[0].type;
199
+ }
200
+
201
+ /**
202
+ * True when the leading word of `text` is an all-caps token — an acronym the
203
+ * synthesizer must not touch. Requires two or more letters so a stray leading
204
+ * "A" or "I" still lowercases; punctuation and digits are ignored so
205
+ * `CRAP:`, `CI/CD` and `API-surface` all read as acronyms while `A11y` does
206
+ * not.
207
+ *
208
+ * @param {string} text
209
+ * @returns {boolean}
210
+ */
211
+ function leadsWithAcronym(text) {
212
+ const [word = ''] = text.split(/\s+/, 1);
213
+ const letters = word.replace(/[^A-Za-z]/g, '');
214
+ return letters.length >= 2 && letters === letters.toUpperCase();
215
+ }
216
+
217
+ /**
218
+ * Shape a human Story title into the description half of a synthesized
219
+ * Conventional-Commit subject: lowercase the first character so the subject
220
+ * reads as a sentence fragment, EXCEPT when the leading word is an acronym.
221
+ * Pure.
222
+ *
223
+ * @param {string} text
224
+ * @returns {string}
225
+ */
226
+ export function shapeDescription(text) {
227
+ const trimmed = typeof text === 'string' ? text.trim() : '';
228
+ if (trimmed.length === 0) return trimmed;
229
+ if (leadsWithAcronym(trimmed)) return trimmed;
230
+ return trimmed.charAt(0).toLowerCase() + trimmed.slice(1);
231
+ }
232
+
233
+ /**
234
+ * Insert the breaking-change `!` into a conventional subject, at the one
235
+ * position `conventional-commits-parser` reads it: immediately before the
236
+ * colon, after any scope. A subject that already carries `!`, or that is not
237
+ * conventional, is returned unchanged. Pure.
238
+ *
239
+ * @param {string} subject
240
+ * @returns {string}
241
+ */
242
+ export function markBreaking(subject) {
243
+ const parts = String(subject ?? '').match(HEADER_PARTS_RE);
244
+ if (!parts) return subject;
245
+ const [, prefix, bang, description] = parts;
246
+ if (bang === '!') return subject;
247
+ return `${prefix}!: ${description}`;
248
+ }
249
+
250
+ /**
251
+ * Pull the text of one `BREAKING CHANGE:` footer out of `lines`, starting at
252
+ * the footer line itself. The note runs to the end of its paragraph: a blank
253
+ * line, another trailer, or the end of the message closes it.
254
+ *
255
+ * @param {string[]} lines
256
+ * @param {number} start Index of the matched footer line.
257
+ * @param {string} head The footer line's own text (may be empty).
258
+ * @returns {{ note: string, next: number }}
259
+ */
260
+ function readFooterNote(lines, start, head) {
261
+ const collected = head.trim().length > 0 ? [head.trim()] : [];
262
+ let cursor = start + 1;
263
+ while (cursor < lines.length) {
264
+ const line = lines[cursor];
265
+ if (line.trim().length === 0) break;
266
+ if (BREAKING_FOOTER_RE.test(line) || TRAILER_RE.test(line)) break;
267
+ collected.push(line.trim());
268
+ cursor += 1;
269
+ }
270
+ return { note: collected.join(' ').trim(), next: cursor };
271
+ }
272
+
273
+ /**
274
+ * Scan one commit message (or the Story body) for breaking-change evidence.
275
+ *
276
+ * @param {string} text
277
+ * @param {boolean} readHeaderBang Whether line 0 is a commit header whose `!`
278
+ * counts. False for the Story body, which has no header.
279
+ * @returns {{ breaking: boolean, notes: string[], subject: string|null }}
280
+ */
281
+ /**
282
+ * Blank every line inside a markdown code fence, keeping line indices intact.
283
+ *
284
+ * A Story `## Spec` that documents this very contract quotes the footer in a
285
+ * fence; reading that as a declaration ships a `<type>!:` subject and a release
286
+ * note for a break nobody made. Fenced spans are the only place the anchored
287
+ * footer regex can fire on non-footer text — an indented block or a blockquote
288
+ * already fails the `^` anchor. Blanking rather than dropping means a fence
289
+ * also closes an open footer note, which is what a paragraph break would do.
290
+ *
291
+ * @param {string[]} lines
292
+ * @returns {string[]}
293
+ */
294
+ function blankFencedLines(lines) {
295
+ let fence = null;
296
+ return lines.map((line) => {
297
+ const marker = line.match(FENCE_RE)?.[1][0];
298
+ if (!marker) return fence === null ? line : '';
299
+ if (fence === null) fence = marker;
300
+ else if (fence === marker) fence = null;
301
+ return '';
302
+ });
303
+ }
304
+
305
+ function scanForBreaking(text, readHeaderBang) {
306
+ const lines = blankFencedLines(String(text ?? '').split('\n'));
307
+ const notes = [];
308
+ let breaking = false;
309
+ let subject = null;
310
+
311
+ if (readHeaderBang) {
312
+ const header = lines[0]?.trim() ?? '';
313
+ const parts = header.match(HEADER_PARTS_RE);
314
+ if (parts?.[2] === '!') {
315
+ breaking = true;
316
+ subject = parts[3].trim();
317
+ }
318
+ }
319
+
320
+ let cursor = 0;
321
+ while (cursor < lines.length) {
322
+ const match = lines[cursor].match(BREAKING_FOOTER_RE);
323
+ if (!match) {
324
+ cursor += 1;
325
+ continue;
326
+ }
327
+ breaking = true;
328
+ const { note, next } = readFooterNote(lines, cursor, match[1]);
329
+ if (note.length > 0) notes.push(note);
330
+ cursor = Math.max(next, cursor + 1);
331
+ }
332
+
333
+ return { breaking, notes, subject };
334
+ }
335
+
336
+ /**
337
+ * Collect the branch's breaking-change declarations.
338
+ *
339
+ * Two sources, both honoured:
340
+ *
341
+ * - **Any constituent commit** — a `BREAKING CHANGE:` / `BREAKING-CHANGE:`
342
+ * footer, or a `!` in the header. This is the path a maker takes while
343
+ * writing the commit that does the breaking.
344
+ * - **The Story body** — the same footer, written as its own line anywhere
345
+ * in the Story issue (the `## Spec` block is the natural home). This is
346
+ * the declarative path: `/plan` can state the break up front and close
347
+ * propagates it even when no individual commit remembered the footer.
348
+ * Only the footer form counts; prose describing a break does not, because
349
+ * a keyword-free sentence is exactly what release-please cannot parse.
350
+ *
351
+ * When something is breaking but no footer supplied text, the `!` commit's
352
+ * own description becomes the note — the fallback Conventional Commits
353
+ * itself prescribes for a `!` header with no footer.
354
+ *
355
+ * @param {{ commitMessages?: string[], storyBody?: string }} args
356
+ * @returns {{ breaking: boolean, notes: string[] }}
357
+ */
358
+ export function collectBreakingNotes({ commitMessages = [], storyBody = '' }) {
359
+ const notes = [];
360
+ let breaking = false;
361
+ let bangSubject = null;
362
+
363
+ for (const message of Array.isArray(commitMessages) ? commitMessages : []) {
364
+ const scan = scanForBreaking(message, true);
365
+ breaking = breaking || scan.breaking;
366
+ notes.push(...scan.notes);
367
+ bangSubject ??= scan.subject;
368
+ }
369
+
370
+ const bodyScan = scanForBreaking(storyBody, false);
371
+ breaking = breaking || bodyScan.breaking;
372
+ notes.push(...bodyScan.notes);
373
+
374
+ if (breaking && notes.length === 0 && bangSubject) notes.push(bangSubject);
375
+ return { breaking, notes: [...new Set(notes)] };
376
+ }