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.
- package/.agents/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-entry-sync.js +469 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/story-body.js +12 -59
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +0 -2
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +42 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +12 -3
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -96,6 +96,7 @@ import {
|
|
|
96
96
|
readPrMergeState as defaultReadPrMergeState,
|
|
97
97
|
} from '../../../single-story/confirm-merge.js';
|
|
98
98
|
import { pollUntil } from '../../../util/poll-loop.js';
|
|
99
|
+
import { applyBehindUpdate } from '../../behind-recovery.js';
|
|
99
100
|
import {
|
|
100
101
|
emitMergeFlipFailed as defaultEmitMergeFlipFailed,
|
|
101
102
|
MERGED_FLIP_FAILED_BLOCK_CLASS,
|
|
@@ -635,7 +636,15 @@ async function blockOnUnlanded({
|
|
|
635
636
|
/**
|
|
636
637
|
* Bring a BEHIND PR up to date, bounded by `updateAttempts`. Best-effort:
|
|
637
638
|
* a failed update is not itself a terminal — the next poll re-reads the
|
|
638
|
-
* real state and lets the normal classification decide
|
|
639
|
+
* real state and lets the normal classification decide, which is why a
|
|
640
|
+
* failed attempt still counts against the wait's tick.
|
|
641
|
+
*
|
|
642
|
+
* The BEHIND / budget / did-it-land decision itself lives in the shared
|
|
643
|
+
* {@link applyBehindUpdate} (Story #5006) — the CI-watch loop in
|
|
644
|
+
* `lib/orchestration/pr-watch.js` runs the same one. This wrapper supplies
|
|
645
|
+
* the merge wait's probe source, its `gh` facade (bounded by
|
|
646
|
+
* {@link withGhTimeout}, so a wedged child cannot strand an unattended
|
|
647
|
+
* async-mode wait), and its operator wording.
|
|
639
648
|
*
|
|
640
649
|
* @returns {Promise<boolean>} whether an update was actually attempted.
|
|
641
650
|
*/
|
|
@@ -648,31 +657,33 @@ async function maybeUpdateBehindPr({
|
|
|
648
657
|
ghTimeoutMs = MERGE_WAIT_GH_TIMEOUT_MS,
|
|
649
658
|
progress,
|
|
650
659
|
}) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
660
|
+
const recovery = await applyBehindUpdate({
|
|
661
|
+
mergeStateStatus: probe.mergeStateStatus,
|
|
662
|
+
updatesUsed,
|
|
663
|
+
maxUpdates: updateAttempts,
|
|
664
|
+
updateBranch: () =>
|
|
665
|
+
withGhTimeout(
|
|
666
|
+
(gh ?? defaultGh).pr.updateBranch(prNumber),
|
|
667
|
+
ghTimeoutMs,
|
|
668
|
+
`gh pr update-branch ${prNumber}`,
|
|
669
|
+
),
|
|
670
|
+
onBudgetSpent: () =>
|
|
671
|
+
progress?.(
|
|
672
|
+
'CONFIRM',
|
|
673
|
+
`⚠️ PR #${prNumber} is BEHIND but the update budget (${updateAttempts}) is spent — not updating again.`,
|
|
674
|
+
),
|
|
675
|
+
onUpdated: () =>
|
|
676
|
+
progress?.(
|
|
677
|
+
'CONFIRM',
|
|
678
|
+
`⏫ PR #${prNumber} was BEHIND its base — updated (attempt ${updatesUsed + 1}/${updateAttempts}).`,
|
|
679
|
+
),
|
|
680
|
+
onUpdateFailed: (detail) =>
|
|
681
|
+
progress?.(
|
|
682
|
+
'CONFIRM',
|
|
683
|
+
`⚠️ gh pr update-branch failed (continuing): ${detail}`,
|
|
684
|
+
),
|
|
685
|
+
});
|
|
686
|
+
return recovery.attempted;
|
|
676
687
|
}
|
|
677
688
|
|
|
678
689
|
/**
|
|
@@ -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
|
+
}
|