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
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* module mechanizes it.
|
|
20
20
|
*
|
|
21
21
|
* Contract (pure where possible — the only side effect is an injectable
|
|
22
|
-
* `git log` read
|
|
22
|
+
* `git log` read):
|
|
23
23
|
*
|
|
24
24
|
* - If `storyTitle` is **already** a parseable Conventional Commit
|
|
25
25
|
* subject, it is preserved verbatim and suffixed with `(#<storyId>)`.
|
|
@@ -28,116 +28,48 @@
|
|
|
28
28
|
* `<type>: <descriptive text> (#<storyId>)`. The `type` is derived
|
|
29
29
|
* from the branch's own (already-conventional) commit subjects when
|
|
30
30
|
* available, falling back to a safe configured default (`chore`).
|
|
31
|
+
* - Either way, a branch (or Story) that declares a breaking change gets
|
|
32
|
+
* the `!` marker and a `BREAKING CHANGE:` footer on the PR body.
|
|
31
33
|
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
34
|
+
* The rules that decide *what* the subject says now live in
|
|
35
|
+
* `conventional-subject.js` — type precedence, acronym-safe casing, and
|
|
36
|
+
* breaking-change collection are pure and unit-tested there. What is left
|
|
37
|
+
* here is the git read those rules consume and the assembly of the two
|
|
38
|
+
* strings `gh pr create` needs.
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
41
|
import { gitSpawn as defaultGitSpawn } from '../../../git-utils.js';
|
|
38
42
|
import { Logger as DefaultLogger } from '../../../Logger.js';
|
|
43
|
+
import {
|
|
44
|
+
collectBreakingNotes,
|
|
45
|
+
isConventionalSubject,
|
|
46
|
+
markBreaking,
|
|
47
|
+
pickDominantType,
|
|
48
|
+
shapeDescription,
|
|
49
|
+
} from './conventional-subject.js';
|
|
39
50
|
|
|
40
51
|
/** Safe default Conventional-Commit type when none can be derived. */
|
|
41
52
|
const DEFAULT_CONVENTIONAL_TYPE = 'chore';
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
55
|
+
* Record separator between whole commit messages in the `git log` read. A NUL
|
|
56
|
+
* cannot occur inside a commit message, so splitting on it is unambiguous —
|
|
57
|
+
* unlike a blank-line or subject-prefix heuristic, which a commit body can
|
|
58
|
+
* forge.
|
|
48
59
|
*/
|
|
49
|
-
const
|
|
50
|
-
'feat',
|
|
51
|
-
'fix',
|
|
52
|
-
'perf',
|
|
53
|
-
'refactor',
|
|
54
|
-
'revert',
|
|
55
|
-
'docs',
|
|
56
|
-
'style',
|
|
57
|
-
'chore',
|
|
58
|
-
'test',
|
|
59
|
-
'build',
|
|
60
|
-
'ci',
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
// Precedence used when a branch carries a mix of conventional types: pick
|
|
64
|
-
// the most release-significant one so the squash subject communicates the
|
|
65
|
-
// branch's headline impact (and release-please bumps appropriately).
|
|
66
|
-
const TYPE_PRECEDENCE = Object.freeze([
|
|
67
|
-
'feat',
|
|
68
|
-
'fix',
|
|
69
|
-
'perf',
|
|
70
|
-
'refactor',
|
|
71
|
-
'revert',
|
|
72
|
-
'docs',
|
|
73
|
-
'style',
|
|
74
|
-
'test',
|
|
75
|
-
'build',
|
|
76
|
-
'ci',
|
|
77
|
-
'chore',
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
|
-
const TYPE_GROUP = CONVENTIONAL_TYPES.join('|');
|
|
81
|
-
// Anchored Conventional-Commit header matcher:
|
|
82
|
-
// <type>(<optional scope>)<optional !>: <non-empty description>
|
|
83
|
-
// Mirrors the shape `@commitlint/config-conventional` enforces (a known
|
|
84
|
-
// type, an optional parenthesised scope, an optional breaking `!`, a
|
|
85
|
-
// colon-space separator, and a non-empty subject). Used for the pure
|
|
86
|
-
// "is this already conventional?" check and to pull the type off a branch
|
|
87
|
-
// commit subject without spawning commitlint per call.
|
|
88
|
-
const CONVENTIONAL_HEADER_RE = new RegExp(
|
|
89
|
-
`^(?:${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?: \\S.*$`,
|
|
90
|
-
);
|
|
91
|
-
const LEADING_TYPE_RE = new RegExp(
|
|
92
|
-
`^(${TYPE_GROUP})(?:\\([^()\\r\\n]+\\))?!?:`,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* True iff `subject` is a parseable Conventional Commit subject under the
|
|
97
|
-
* repo's type vocabulary. Pure.
|
|
98
|
-
*
|
|
99
|
-
* @param {string} subject
|
|
100
|
-
* @returns {boolean}
|
|
101
|
-
*/
|
|
102
|
-
function isConventionalSubject(subject) {
|
|
103
|
-
if (typeof subject !== 'string') return false;
|
|
104
|
-
return CONVENTIONAL_HEADER_RE.test(subject.trim());
|
|
105
|
-
}
|
|
60
|
+
const RECORD_SEP = '\u0000';
|
|
106
61
|
|
|
107
62
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
63
|
+
* Read the branch's own commits (those unique to the Story branch relative to
|
|
64
|
+
* the base branch) as whole messages — subject AND body, because the body is
|
|
65
|
+
* where a `BREAKING CHANGE:` footer lives.
|
|
110
66
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
function parseConventionalType(subject) {
|
|
115
|
-
if (typeof subject !== 'string') return null;
|
|
116
|
-
const match = subject.trim().match(LEADING_TYPE_RE);
|
|
117
|
-
return match ? match[1] : null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Pick the most release-significant type from a list of conventional
|
|
122
|
-
* types, honouring `TYPE_PRECEDENCE`. Returns `null` for an empty list.
|
|
123
|
-
* Pure.
|
|
67
|
+
* Returns `[]` when the read fails, which degrades every downstream rule to
|
|
68
|
+
* its safe default (type `chore`, no breaking marker) rather than throwing a
|
|
69
|
+
* close that is otherwise healthy.
|
|
124
70
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*/
|
|
128
|
-
function pickDominantType(types) {
|
|
129
|
-
const present = new Set(types.filter(Boolean));
|
|
130
|
-
for (const candidate of TYPE_PRECEDENCE) {
|
|
131
|
-
if (present.has(candidate)) return candidate;
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Read the branch's own commit subjects (commits unique to the Story
|
|
138
|
-
* branch relative to the base branch) and derive the dominant
|
|
139
|
-
* Conventional-Commit type. Returns `DEFAULT_CONVENTIONAL_TYPE` when no
|
|
140
|
-
* conventional subject is found or the git read fails.
|
|
71
|
+
* Oldest-first (`--reverse`) is load-bearing: `pickDominantType` breaks a tie
|
|
72
|
+
* on the Story's primary commit, which is the first one authored.
|
|
141
73
|
*
|
|
142
74
|
* @param {{
|
|
143
75
|
* storyBranch: string,
|
|
@@ -146,96 +78,174 @@ function pickDominantType(types) {
|
|
|
146
78
|
* gitSpawn?: typeof defaultGitSpawn,
|
|
147
79
|
* logger?: { warn?: Function },
|
|
148
80
|
* }} args
|
|
149
|
-
* @returns {string}
|
|
81
|
+
* @returns {string[]} Whole commit messages, oldest first.
|
|
150
82
|
*/
|
|
151
|
-
function
|
|
83
|
+
function readBranchCommits({
|
|
152
84
|
storyBranch,
|
|
153
85
|
baseBranch,
|
|
154
86
|
cwd = process.cwd(),
|
|
155
87
|
gitSpawn = defaultGitSpawn,
|
|
156
88
|
logger = DefaultLogger,
|
|
157
89
|
}) {
|
|
90
|
+
if (!storyBranch || !baseBranch) return [];
|
|
91
|
+
const range = `${baseBranch}..${storyBranch}`;
|
|
158
92
|
try {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
93
|
+
const result = gitSpawn(
|
|
94
|
+
cwd,
|
|
95
|
+
'log',
|
|
96
|
+
'--no-merges',
|
|
97
|
+
'--reverse',
|
|
98
|
+
'--format=%B%x00',
|
|
99
|
+
range,
|
|
100
|
+
);
|
|
101
|
+
if (result?.status !== 0) {
|
|
162
102
|
logger?.warn?.(
|
|
163
103
|
`[normalize-pr-title] git log ${range} failed (status=${result?.status ?? 'n/a'}); ` +
|
|
164
|
-
`defaulting type to "${DEFAULT_CONVENTIONAL_TYPE}".`,
|
|
104
|
+
`defaulting type to "${DEFAULT_CONVENTIONAL_TYPE}" and assuming no breaking change.`,
|
|
165
105
|
);
|
|
166
|
-
return
|
|
106
|
+
return [];
|
|
167
107
|
}
|
|
168
|
-
|
|
169
|
-
.split(
|
|
170
|
-
.map((
|
|
171
|
-
.filter(
|
|
172
|
-
return pickDominantType(types) ?? DEFAULT_CONVENTIONAL_TYPE;
|
|
108
|
+
return String(result.stdout ?? '')
|
|
109
|
+
.split(RECORD_SEP)
|
|
110
|
+
.map((message) => message.trim())
|
|
111
|
+
.filter((message) => message.length > 0);
|
|
173
112
|
} catch (err) {
|
|
174
113
|
logger?.warn?.(
|
|
175
|
-
`[normalize-pr-title] could not
|
|
176
|
-
`(defaulting to "${DEFAULT_CONVENTIONAL_TYPE}"): ${err?.message ?? err}`,
|
|
114
|
+
`[normalize-pr-title] could not read branch commits ` +
|
|
115
|
+
`(defaulting to "${DEFAULT_CONVENTIONAL_TYPE}", no breaking change): ${err?.message ?? err}`,
|
|
177
116
|
);
|
|
178
|
-
return
|
|
117
|
+
return [];
|
|
179
118
|
}
|
|
180
119
|
}
|
|
181
120
|
|
|
182
121
|
/**
|
|
183
|
-
* Produce
|
|
122
|
+
* Produce the PR title and the breaking-change notes that belong with it.
|
|
184
123
|
*
|
|
185
124
|
* - Already-conventional `storyTitle` → preserved verbatim + `(#<id>)`.
|
|
186
|
-
* - Otherwise → `<derivedType>: <storyTitle> (#<id>)`.
|
|
187
|
-
* - Empty / missing `storyTitle` → `<derivedType>:
|
|
125
|
+
* - Otherwise → `<derivedType>: <shaped storyTitle> (#<id>)`.
|
|
126
|
+
* - Empty / missing `storyTitle` → `<derivedType>: story #<id>`.
|
|
127
|
+
* - Breaking → `!` inserted before the colon in either shape.
|
|
188
128
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
129
|
+
* `commitMessages` is the branch read (`readBranchCommits`); passing `[]`
|
|
130
|
+
* yields the safe default type and no breaking marker. `storyBody` is the
|
|
131
|
+
* Story issue's body, scanned for a declared `BREAKING CHANGE:` footer.
|
|
191
132
|
*
|
|
192
133
|
* @param {{
|
|
193
134
|
* storyTitle: string,
|
|
194
135
|
* storyId: number|string,
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* cwd?: string,
|
|
198
|
-
* gitSpawn?: typeof defaultGitSpawn,
|
|
199
|
-
* logger?: { warn?: Function },
|
|
136
|
+
* commitMessages?: string[],
|
|
137
|
+
* storyBody?: string,
|
|
200
138
|
* }} args
|
|
139
|
+
* @returns {{ title: string, breaking: boolean, breakingNotes: string[] }}
|
|
140
|
+
*/
|
|
141
|
+
function normalizePrTitle({
|
|
142
|
+
storyTitle,
|
|
143
|
+
storyId,
|
|
144
|
+
commitMessages = [],
|
|
145
|
+
storyBody = '',
|
|
146
|
+
}) {
|
|
147
|
+
const idSuffix = `(#${storyId})`;
|
|
148
|
+
const trimmed = typeof storyTitle === 'string' ? storyTitle.trim() : '';
|
|
149
|
+
const { breaking, notes } = collectBreakingNotes({
|
|
150
|
+
commitMessages,
|
|
151
|
+
storyBody,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Already conventional → preserve verbatim (the maker's own casing and
|
|
155
|
+
// scope survive), append the id reference. Only the breaking marker may be
|
|
156
|
+
// added, and only when it is not already there.
|
|
157
|
+
const subject = isConventionalSubject(trimmed)
|
|
158
|
+
? trimmed
|
|
159
|
+
: synthesizeSubject({ description: trimmed, storyId, commitMessages });
|
|
160
|
+
|
|
161
|
+
const marked = breaking ? markBreaking(subject) : subject;
|
|
162
|
+
return { title: `${marked} ${idSuffix}`, breaking, breakingNotes: notes };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build a conventional subject for a Story whose title is plain prose.
|
|
167
|
+
*
|
|
168
|
+
* @param {{ description: string, storyId: number|string, commitMessages: string[] }} args
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
function synthesizeSubject({ description, storyId, commitMessages }) {
|
|
172
|
+
const subjects = commitMessages.map((message) => message.split('\n')[0]);
|
|
173
|
+
const type = pickDominantType(subjects) ?? DEFAULT_CONVENTIONAL_TYPE;
|
|
174
|
+
const raw = description.length > 0 ? description : `Story #${storyId}`;
|
|
175
|
+
return `${type}: ${shapeDescription(raw)}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build the PR body.
|
|
180
|
+
*
|
|
181
|
+
* The `Closes #<id>` footer is what auto-closes the Story on merge. A
|
|
182
|
+
* `BREAKING CHANGE:` footer goes LAST, as the spec requires, so that a repo
|
|
183
|
+
* configured to use the PR body as the squash-commit message hands
|
|
184
|
+
* release-please a parseable note rather than prose. When the squash body is
|
|
185
|
+
* built from the constituent commit messages instead (GitHub's default, and
|
|
186
|
+
* this repo's setting), the note still reaches `main` through whichever
|
|
187
|
+
* commit carried the footer — and the `!` in the subject carries the signal
|
|
188
|
+
* either way.
|
|
189
|
+
*
|
|
190
|
+
* @param {{ storyId: number|string, breakingNotes?: string[] }} args
|
|
201
191
|
* @returns {string}
|
|
202
192
|
*/
|
|
203
|
-
|
|
193
|
+
function buildPrBody({ storyId, breakingNotes }) {
|
|
194
|
+
const lines = [`Closes #${storyId}`, '', '_Auto-opened by `/deliver`._'];
|
|
195
|
+
if (breakingNotes.length > 0) {
|
|
196
|
+
lines.push('', `BREAKING CHANGE: ${breakingNotes.join(' ')}`);
|
|
197
|
+
}
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Derive the two strings `gh pr create` needs. One `git log` read serves both
|
|
203
|
+
* halves: the commit SUBJECTS decide the type, and the commit BODIES — plus
|
|
204
|
+
* the Story body — decide whether this is a breaking change.
|
|
205
|
+
*
|
|
206
|
+
* A declared break is announced on the progress channel, because a `!` the
|
|
207
|
+
* operator did not expect in the squash subject should be visible while the
|
|
208
|
+
* close is running rather than discovered in the release notes.
|
|
209
|
+
*
|
|
210
|
+
* @param {{ storyTitle: string, storyId: number|string, storyBody?: string,
|
|
211
|
+
* storyBranch: string, baseBranch: string, cwd?: string,
|
|
212
|
+
* gitSpawn?: typeof defaultGitSpawn,
|
|
213
|
+
* progress?: (tag: string, msg: string) => void }} args
|
|
214
|
+
* @returns {{ title: string, body: string, breaking: boolean, breakingNotes: string[] }}
|
|
215
|
+
*/
|
|
216
|
+
export function buildPullRequestFields({
|
|
204
217
|
storyTitle,
|
|
205
218
|
storyId,
|
|
219
|
+
storyBody = '',
|
|
206
220
|
storyBranch,
|
|
207
221
|
baseBranch,
|
|
208
222
|
cwd = process.cwd(),
|
|
209
223
|
gitSpawn = defaultGitSpawn,
|
|
210
|
-
|
|
224
|
+
progress = () => {},
|
|
211
225
|
}) {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
const commitMessages = readBranchCommits({
|
|
227
|
+
storyBranch,
|
|
228
|
+
baseBranch,
|
|
229
|
+
cwd,
|
|
230
|
+
gitSpawn,
|
|
231
|
+
});
|
|
232
|
+
const { title, breaking, breakingNotes } = normalizePrTitle({
|
|
233
|
+
storyTitle,
|
|
234
|
+
storyId,
|
|
235
|
+
commitMessages,
|
|
236
|
+
storyBody,
|
|
237
|
+
});
|
|
238
|
+
if (breaking) {
|
|
239
|
+
progress(
|
|
240
|
+
'PR',
|
|
241
|
+
'⚠️ Breaking change declared — the PR title carries `!` and the body a ' +
|
|
242
|
+
`BREAKING CHANGE footer: ${breakingNotes.join(' ') || '(no note text)'}`,
|
|
243
|
+
);
|
|
218
244
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
baseBranch,
|
|
226
|
-
cwd,
|
|
227
|
-
gitSpawn,
|
|
228
|
-
logger,
|
|
229
|
-
})
|
|
230
|
-
: DEFAULT_CONVENTIONAL_TYPE;
|
|
231
|
-
|
|
232
|
-
// Lowercase the leading character of a synthesized description so the
|
|
233
|
-
// subject satisfies commitlint's `subject-case` rule (matching the
|
|
234
|
-
// `shapeMergeSubject` behaviour). An already-conventional title is left
|
|
235
|
-
// untouched (it was preserved verbatim above). The empty-title fallback
|
|
236
|
-
// uses a lowercased `story #<id>` for the same reason.
|
|
237
|
-
const rawDescription = trimmed.length > 0 ? trimmed : `Story #${storyId}`;
|
|
238
|
-
const description =
|
|
239
|
-
rawDescription.charAt(0).toLowerCase() + rawDescription.slice(1);
|
|
240
|
-
return `${type}: ${description} ${idSuffix}`;
|
|
245
|
+
return {
|
|
246
|
+
title,
|
|
247
|
+
body: buildPrBody({ storyId, breakingNotes }),
|
|
248
|
+
breaking,
|
|
249
|
+
breakingNotes,
|
|
250
|
+
};
|
|
241
251
|
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
parseMergeWatchMode,
|
|
14
|
+
parseOverrideReviewBlock,
|
|
15
|
+
parseSprintArgs,
|
|
16
|
+
} from '../../../cli-args.js';
|
|
13
17
|
import { getDeliveryRouting } from '../../../config/delivery-routing.js';
|
|
14
18
|
import { PROJECT_ROOT } from '../../../project-root.js';
|
|
15
19
|
import { isOperatorMergeReason } from './auto-merge.js';
|
|
@@ -91,8 +95,8 @@ export function resolveWaitForMerge({
|
|
|
91
95
|
* (`waitForMergeExplicit` / `noWaitForMerge`) for the runner to resolve once
|
|
92
96
|
* the config and the arm outcome exist.
|
|
93
97
|
*
|
|
94
|
-
* @param {{ storyIdParam, cwdParam, skipValidationParam, skipSyncParam, noAutoMergeParam, waitForMergeParam, noWaitForMergeParam, maxWaitSecondsParam, mergeWatchModeParam }} raw
|
|
95
|
-
* @returns {{ storyId, cwd, skipValidation, skipSync, noAutoMerge, waitForMergeExplicit, noWaitForMerge, maxWaitSeconds, mergeWatchMode }}
|
|
98
|
+
* @param {{ storyIdParam, cwdParam, skipValidationParam, skipSyncParam, noAutoMergeParam, waitForMergeParam, noWaitForMergeParam, maxWaitSecondsParam, mergeWatchModeParam, overrideReviewBlockParam }} raw
|
|
99
|
+
* @returns {{ storyId, cwd, skipValidation, skipSync, noAutoMerge, waitForMergeExplicit, noWaitForMerge, maxWaitSeconds, mergeWatchMode, overrideReviewBlock }}
|
|
96
100
|
*/
|
|
97
101
|
export function parseCloseOptions({
|
|
98
102
|
storyIdParam,
|
|
@@ -104,6 +108,7 @@ export function parseCloseOptions({
|
|
|
104
108
|
noWaitForMergeParam,
|
|
105
109
|
maxWaitSecondsParam,
|
|
106
110
|
mergeWatchModeParam,
|
|
111
|
+
overrideReviewBlockParam,
|
|
107
112
|
}) {
|
|
108
113
|
// An injecting caller (`storyIdParam` supplied) is not reading argv at all,
|
|
109
114
|
// so there is nothing to parse and `parsed` stays empty. This used to build a
|
|
@@ -148,5 +153,12 @@ export function parseCloseOptions({
|
|
|
148
153
|
? waitForMergeExplicit
|
|
149
154
|
: undefined,
|
|
150
155
|
noWaitForMerge: !!resolveFlag(noWaitForMergeParam, parsed.noWaitForMerge),
|
|
156
|
+
// `undefined` when unsupplied, which is what keeps a review
|
|
157
|
+
// critical blocker blocking. An injecting caller passes the reason string
|
|
158
|
+
// directly; both doors run the same validating parser, so neither can arm
|
|
159
|
+
// a reasonless override.
|
|
160
|
+
overrideReviewBlock: parseOverrideReviewBlock(
|
|
161
|
+
resolveFlag(overrideReviewBlockParam, parsed.overrideReviewBlock),
|
|
162
|
+
),
|
|
151
163
|
};
|
|
152
164
|
}
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
import { gh as defaultGh } from '../../../gh-exec.js';
|
|
45
45
|
import { Logger } from '../../../Logger.js';
|
|
46
46
|
import { computeChangeSet as defaultComputeChangeSet } from '../../change-set.js';
|
|
47
|
-
import {
|
|
47
|
+
import { buildPullRequestFields } from './normalize-pr-title.js';
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Pick the PR this head branch should resolve to from a `gh pr list
|
|
@@ -112,6 +112,7 @@ function probeEmptyDiff({ cwd, baseBranch, storyBranch, computeChangeSet }) {
|
|
|
112
112
|
* cwd: string,
|
|
113
113
|
* storyId: number,
|
|
114
114
|
* storyTitle: string,
|
|
115
|
+
* storyBody?: string,
|
|
115
116
|
* storyBranch: string,
|
|
116
117
|
* baseBranch: string,
|
|
117
118
|
* gh?: ReturnType<typeof import('../../../gh-exec.js').createGh>,
|
|
@@ -124,6 +125,7 @@ export async function ensurePullRequestWith({
|
|
|
124
125
|
cwd: _cwd,
|
|
125
126
|
storyId,
|
|
126
127
|
storyTitle,
|
|
128
|
+
storyBody = '',
|
|
127
129
|
storyBranch,
|
|
128
130
|
baseBranch,
|
|
129
131
|
gh = defaultGh,
|
|
@@ -185,26 +187,19 @@ export async function ensurePullRequestWith({
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
progress('PR', `Opening PR for ${storyBranch} → ${baseBranch}...`);
|
|
188
|
-
// The repo squash-merges and GitHub uses the PR title as the squash
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// (
|
|
192
|
-
|
|
193
|
-
// type derived from the branch's own commit subjects (default `chore`).
|
|
194
|
-
// `gh-exec` spawns `gh` against the current process cwd (the worktree),
|
|
195
|
-
// so the branch-commit read uses the same cwd.
|
|
196
|
-
const title = normalizePrTitle({
|
|
190
|
+
// The repo squash-merges and GitHub uses the PR title as the squash subject
|
|
191
|
+
// on `main`, so both fields are derived rather than typed — see
|
|
192
|
+
// `normalize-pr-title.js`. `gh-exec` spawns `gh` against the current process
|
|
193
|
+
// cwd (the worktree), so the branch read uses the same cwd.
|
|
194
|
+
const { title, body } = buildPullRequestFields({
|
|
197
195
|
storyTitle,
|
|
198
196
|
storyId,
|
|
197
|
+
storyBody,
|
|
199
198
|
storyBranch,
|
|
200
199
|
baseBranch,
|
|
201
200
|
cwd: _cwd ?? process.cwd(),
|
|
201
|
+
progress,
|
|
202
202
|
});
|
|
203
|
-
const body = [
|
|
204
|
-
`Closes #${storyId}`,
|
|
205
|
-
'',
|
|
206
|
-
`_Auto-opened by \`/deliver\`._`,
|
|
207
|
-
].join('\n');
|
|
208
203
|
try {
|
|
209
204
|
const createResult = await gh.pr.create([
|
|
210
205
|
'--base',
|
|
@@ -20,6 +20,11 @@ export async function handleCriticalReviewBlock({
|
|
|
20
20
|
'',
|
|
21
21
|
`The Story-scope review reported **${criticalCount} critical blocker(s)** on ${prUrl}.`,
|
|
22
22
|
'Remediate the posted findings, then re-run `/deliver`.',
|
|
23
|
+
'',
|
|
24
|
+
'If you have reviewed a finding and judged it wrong, re-run close with',
|
|
25
|
+
'`--override-review-block "<reason>"` rather than merging by hand — see',
|
|
26
|
+
'`phases/review-override.js`. The override is recorded on the Story, on the',
|
|
27
|
+
'PR, and as friction telemetry.',
|
|
23
28
|
].join('\n');
|
|
24
29
|
try {
|
|
25
30
|
await upsertStructuredComment(provider, storyId, 'friction', body);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phases/review-override.js — the sanctioned, logged override of a Story-scope
|
|
3
|
+
* code-review critical blocker.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `phases/review-block.js`, which owns the opposite outcome: the
|
|
6
|
+
* blocker that *holds*. Keeping the two in one module meant one file owning
|
|
7
|
+
* both "park this Story" and "ship it anyway", and the override's audit trail is
|
|
8
|
+
* substantial enough — three write surfaces, each independently best-effort —
|
|
9
|
+
* to be its own reason to change.
|
|
10
|
+
*
|
|
11
|
+
* **Why an override exists at all.** A critical review finding halts
|
|
12
|
+
* `single-story-close.js` before auto-merge, and no flag overrode a review
|
|
13
|
+
* verdict: `--skip-validation` bypasses the gate chain and `--no-auto-merge`
|
|
14
|
+
* declines to arm, but neither touches the review. So an operator who had read a
|
|
15
|
+
* finding and judged it wrong could only land by merging the PR by hand, which
|
|
16
|
+
* bypasses the gate with no record anywhere of what was overridden or why. This
|
|
17
|
+
* module does not weaken the gate; it relocates that escape hatch out of an
|
|
18
|
+
* untraceable hand-merge and into a mandatory-reason audit trail.
|
|
19
|
+
*
|
|
20
|
+
* Live provenance: Story #5007 / PR #5022, where a FALSE critical blocker — an
|
|
21
|
+
* MI finding on files `delivery.quality.gates.maintainability.ignoreGlobs`
|
|
22
|
+
* exempts, which the `check-baselines.js` ratchet correctly ignored in the same
|
|
23
|
+
* run — halted a legitimate delivery with a hand-merge as the only way out.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Logger } from '../../../Logger.js';
|
|
27
|
+
import {
|
|
28
|
+
emitRuntimeFriction,
|
|
29
|
+
RUNTIME_FRICTION_CATEGORIES,
|
|
30
|
+
} from '../../../observability/runtime-friction.js';
|
|
31
|
+
import {
|
|
32
|
+
postStructuredComment,
|
|
33
|
+
upsertStructuredComment,
|
|
34
|
+
} from '../../ticketing.js';
|
|
35
|
+
|
|
36
|
+
/** Cap on the reason text copied into the friction signal's `details`. */
|
|
37
|
+
const REASON_SIGNAL_LIMIT = 500;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the audit record posted when an operator overrides a review blocker.
|
|
41
|
+
* Pure.
|
|
42
|
+
*
|
|
43
|
+
* The body restates the count, the reason, and the fact that auto-merge was
|
|
44
|
+
* armed anyway — an override whose trail says only "overridden" is no better
|
|
45
|
+
* than the hand-merge it replaces.
|
|
46
|
+
*
|
|
47
|
+
* Module-local: an implementation detail of
|
|
48
|
+
* {@link handleOverriddenReviewBlock}, whose posted body is observable through
|
|
49
|
+
* that public entry point. Exporting it would add a public symbol no production
|
|
50
|
+
* path reaches — the exact dead-export shape this repo ratchets against.
|
|
51
|
+
*
|
|
52
|
+
* @param {{ prUrl: string, criticalCount: number, reason: string }} args
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function buildReviewOverrideBody({ prUrl, criticalCount, reason }) {
|
|
56
|
+
return [
|
|
57
|
+
'### Code-review blocker overridden by operator',
|
|
58
|
+
'',
|
|
59
|
+
`The Story-scope review reported **${criticalCount} critical blocker(s)** on ${prUrl}.`,
|
|
60
|
+
'The operator reviewed and rejected the finding(s) and authorized delivery',
|
|
61
|
+
'with `--override-review-block`; auto-merge was armed.',
|
|
62
|
+
'',
|
|
63
|
+
'**Recorded reason:**',
|
|
64
|
+
'',
|
|
65
|
+
`> ${reason.split('\n').join('\n> ')}`,
|
|
66
|
+
'',
|
|
67
|
+
'The findings comment on the PR is left in place unchanged — this record',
|
|
68
|
+
'sits beside it rather than resolving it.',
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Post one audit record, swallowing the failure into a warning.
|
|
74
|
+
*
|
|
75
|
+
* Every write here is best-effort by design: an override whose audit trail
|
|
76
|
+
* partly failed must still land the delivery the operator authorized, because
|
|
77
|
+
* the close would otherwise fail for a reason the operator cannot act on. The
|
|
78
|
+
* friction signal is the durable record — it is what makes a rising override
|
|
79
|
+
* count visible to the retro.
|
|
80
|
+
*
|
|
81
|
+
* Module-local: the two call sites below are its only callers.
|
|
82
|
+
*
|
|
83
|
+
* @param {{ post: () => Promise<unknown>, surface: string }} args
|
|
84
|
+
* @returns {Promise<boolean>} true when the record landed.
|
|
85
|
+
*/
|
|
86
|
+
async function postAuditRecord({ post, surface }) {
|
|
87
|
+
try {
|
|
88
|
+
await post();
|
|
89
|
+
return true;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
Logger.warn(
|
|
92
|
+
`[single-story-close] failed to post review-override record on ${surface}: ${err?.message ?? err}`,
|
|
93
|
+
);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Record an operator-authorized override of a critical code-review blocker,
|
|
100
|
+
* then let close continue to the auto-merge phase.
|
|
101
|
+
*
|
|
102
|
+
* Writes to three surfaces: the Story issue via `upsert` so a re-run does not
|
|
103
|
+
* stack duplicates; the PR via `post` so the trail is append-only on the surface
|
|
104
|
+
* a human reviews, and so a reviewer reading the PR need not open the Story to
|
|
105
|
+
* learn the blocker was overridden; and the friction stream.
|
|
106
|
+
*
|
|
107
|
+
* @param {{
|
|
108
|
+
* provider: object,
|
|
109
|
+
* storyId: number,
|
|
110
|
+
* prUrl: string,
|
|
111
|
+
* prNumber: number|null,
|
|
112
|
+
* criticalCount: number,
|
|
113
|
+
* reason: string,
|
|
114
|
+
* config?: object,
|
|
115
|
+
* emitFrictionFn?: typeof emitRuntimeFriction,
|
|
116
|
+
* }} args
|
|
117
|
+
* @returns {Promise<{ overridden: true, reason: string, criticalCount: number }>}
|
|
118
|
+
*/
|
|
119
|
+
export async function handleOverriddenReviewBlock({
|
|
120
|
+
provider,
|
|
121
|
+
storyId,
|
|
122
|
+
prUrl,
|
|
123
|
+
prNumber,
|
|
124
|
+
criticalCount,
|
|
125
|
+
reason,
|
|
126
|
+
config,
|
|
127
|
+
emitFrictionFn = emitRuntimeFriction,
|
|
128
|
+
}) {
|
|
129
|
+
const body = buildReviewOverrideBody({ prUrl, criticalCount, reason });
|
|
130
|
+
Logger.warn(
|
|
131
|
+
`[single-story-close] ⚠️ Story-scope review reported ${criticalCount} critical blocker(s) on ` +
|
|
132
|
+
`PR ${prUrl} — OVERRIDDEN by operator: ${reason}`,
|
|
133
|
+
);
|
|
134
|
+
await postAuditRecord({
|
|
135
|
+
post: () => upsertStructuredComment(provider, storyId, 'friction', body),
|
|
136
|
+
surface: `Story #${storyId}`,
|
|
137
|
+
});
|
|
138
|
+
if (Number.isInteger(prNumber)) {
|
|
139
|
+
await postAuditRecord({
|
|
140
|
+
post: () =>
|
|
141
|
+
postStructuredComment(provider, prNumber, 'notification', body),
|
|
142
|
+
surface: `PR #${prNumber}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await emitFrictionFn({
|
|
146
|
+
storyId,
|
|
147
|
+
category: RUNTIME_FRICTION_CATEGORIES.REVIEW_BLOCK_OVERRIDDEN,
|
|
148
|
+
tool: 'single-story-close',
|
|
149
|
+
details: {
|
|
150
|
+
prUrl,
|
|
151
|
+
criticalCount,
|
|
152
|
+
reason: reason.slice(0, REASON_SIGNAL_LIMIT),
|
|
153
|
+
},
|
|
154
|
+
config,
|
|
155
|
+
});
|
|
156
|
+
return { overridden: true, reason, criticalCount };
|
|
157
|
+
}
|