mandrel 2.59.0 → 2.60.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 +11 -9
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +6 -6
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +8 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +4 -5
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -25
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -15
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -28
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +65 -54
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +22 -21
- package/.agents/workflows/mandrel-update.md +36 -21
- package/docs/CHANGELOG.md +35 -0
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +2 -1
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
* the `audit-rules.json` sensitive-path classes the footprint intersects.
|
|
11
11
|
* They ground the authoring template's `changes[]` skeleton and the
|
|
12
12
|
* `/prototype` offer; they route nothing.
|
|
13
|
-
* 2. **Story
|
|
14
|
-
* (`deliver-light`) reads a predicted footprint's
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* 2. **Story risk ({@link deriveStoryShape}).** The light path
|
|
14
|
+
* (`deliver-light`) reads a predicted footprint's **risk** — a migration
|
|
15
|
+
* paired with its consumers, and the `audit-rules.json` sensitive-path
|
|
16
|
+
* classes the footprint intersects — to decide whether a prompt may skip
|
|
17
|
+
* the Story-authoring ceremony. Effort is no longer an axis: Story #4764
|
|
18
|
+
* removed the artifact counts, and Story #5344 removed the declared
|
|
19
|
+
* effort ceilings that replaced them, because a prediction is a
|
|
20
|
+
* declaration and size is enforced against ground truth by the light
|
|
21
|
+
* path's diff backstop.
|
|
20
22
|
* 3. **Dispatch mode ({@link resolveStoryDispatchMode}).** `/mandrel-deliver`
|
|
21
23
|
* answers a different question from the route: may the engine run in the
|
|
22
24
|
* router's own session? Only a **single-Story run** may (Story #4736).
|
|
@@ -25,21 +27,23 @@
|
|
|
25
27
|
* (`--route-downgrade-reason`), the persist-time shape backstop that
|
|
26
28
|
* validated it, the `route::lite` hint label, and the
|
|
27
29
|
* `planning.complexityGate` knobs. Persist no longer routes; every Story
|
|
28
|
-
* lands through the same engine and the same close gates
|
|
29
|
-
* ceilings below are read only where a shape is actually decided on.
|
|
30
|
+
* lands through the same engine and the same close gates.
|
|
30
31
|
*
|
|
31
|
-
* The
|
|
32
|
+
* The risk taxonomy is deliberately the one `review-depth.js` already
|
|
32
33
|
* applies to the landed diff at close (`deriveChangeLevel` over the
|
|
33
|
-
* `audit-rules.json` sensitive-path classes): **predicted
|
|
34
|
-
* actual diff at close** — one taxonomy, two read points.
|
|
35
|
-
* wins: a small change whose footprint intersects a
|
|
36
|
-
* routes `full`, which keeps its
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* light path still produces a Story ticket, still lands via a PR to
|
|
40
|
-
* still runs every repo quality gate, and still honours
|
|
41
|
-
* `rules/security-baseline.md`.
|
|
42
|
-
*
|
|
34
|
+
* `audit-rules.json` sensitive-path classes): **predicted footprint at
|
|
35
|
+
* dispatch, actual diff at close** — one taxonomy, two read points.
|
|
36
|
+
* Sensitivity always wins: a small change whose footprint intersects a
|
|
37
|
+
* sensitive-path class routes `full`, which keeps its deep code review
|
|
38
|
+
* (`review-depth.js`).
|
|
39
|
+
*
|
|
40
|
+
* The light path still produces a Story ticket, still lands via a PR to
|
|
41
|
+
* `main`, still runs every repo quality gate, and still honours
|
|
42
|
+
* `rules/security-baseline.md`. That is a property of
|
|
43
|
+
* `single-story-close.js`, which runs those gates regardless of route — the
|
|
44
|
+
* router cannot and does not switch them off. Story #5366 deleted the frozen
|
|
45
|
+
* `preserves` payload that used to restate it on every decision: nothing read
|
|
46
|
+
* it, and a claim attached to a decision is not the thing that enforces it.
|
|
43
47
|
*
|
|
44
48
|
* @typedef {'lite'|'full'} ComplexityRoute
|
|
45
49
|
*/
|
|
@@ -50,147 +54,32 @@ import { extractChangePaths } from '../story-body/story-body.js';
|
|
|
50
54
|
import { deriveChangeLevel } from './review-depth.js';
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* a
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* So the axes are effort, risk, and uncertainty, and the **prediction** gate
|
|
72
|
-
* they form is deliberately **coarse**: it rejects clearly-epic work only.
|
|
73
|
-
* Real enforcement belongs to the diff-derived backstop, which sees ground
|
|
74
|
-
* truth instead of a declaration
|
|
57
|
+
* ## Why no predicted-effort ceilings live here any more (Story #5344)
|
|
58
|
+
*
|
|
59
|
+
* This module used to export a frozen shape-ceilings constant — declared kinds,
|
|
60
|
+
* a magnitude bucket, an uncertainty bucket, and a deployable span — which the
|
|
61
|
+
* light path judged a prompt's predicted footprint against. Two rounds of
|
|
62
|
+
* evidence retired them. Story #4764 had already removed the artifact counts
|
|
63
|
+
* (`maxChanges`, `maxAcceptance`) because cardinality is the wrong axis in both
|
|
64
|
+
* directions. Story #5313 then demoted the remaining four axes from a gate to a
|
|
65
|
+
* `warnings[]` entry, at which point they decided nothing at all: every one was
|
|
66
|
+
* SELF-DECLARED by the same agent asking to proceed, and an axis a caller sets
|
|
67
|
+
* and no one verifies is not a measurement. Story #5344 deleted them.
|
|
68
|
+
*
|
|
69
|
+
* What survives is what reads something other than the caller's own claim: the
|
|
70
|
+
* two absolute risk rules below (a migration paired with its consumers, and a
|
|
71
|
+
* footprint intersecting a registered sensitive-path class), derived from the
|
|
72
|
+
* predicted PATHS, and the diff-derived backstop that measures the actual
|
|
73
|
+
* change set
|
|
75
74
|
* ({@link module:lib/orchestration/light-suitability.checkLightDiffBackstop}).
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* mechanical edit is one kind at N sites; enumerating
|
|
79
|
-
* more kinds than this is a multi-capability scope.
|
|
80
|
-
* - `maxMagnitude` — coarse magnitude bucket, declared alongside the
|
|
81
|
-
* footprint: `trivial` < `moderate` < `substantial`.
|
|
82
|
-
* - `maxUncertainty` — is the shape determined by the request
|
|
83
|
-
* (`determined`), or does it still need the design
|
|
84
|
-
* decisions `/mandrel-plan` exists to resolve
|
|
85
|
-
* (`needs-design`)?
|
|
86
|
-
* - `maxDeployables` — named deployable roots (`apps/<x>`, `packages/<x>`, …)
|
|
87
|
-
* the footprint spans; more than one is epic by
|
|
88
|
-
* construction.
|
|
89
|
-
*
|
|
90
|
-
* Two rules ride beside the ceilings and are not tunable at all: a footprint
|
|
91
|
-
* pairing a migration with its consumers is epic scope, and a footprint
|
|
92
|
-
* intersecting a sensitive-path class routes `full` however small or mechanical
|
|
93
|
-
* it is — the hard gate, unchanged.
|
|
94
|
-
*
|
|
95
|
-
* Exposed as the `ceilings` field on every {@link deriveStoryShape} decision
|
|
96
|
-
* and exported directly (Story #4740) so the light path's suitability gate
|
|
97
|
-
* ({@link module:lib/orchestration/light-suitability}) judges a prompt's
|
|
98
|
-
* predicted footprint against the **same** axes the plan-time shape backstop
|
|
99
|
-
* applies — one source, so the light entry point and the plan path can never
|
|
100
|
-
* disagree about what work is trivial.
|
|
101
|
-
*/
|
|
102
|
-
export const STORY_SHAPE_CEILINGS = Object.freeze({
|
|
103
|
-
maxChangeKinds: 2,
|
|
104
|
-
maxMagnitude: 'moderate',
|
|
105
|
-
maxUncertainty: 'determined',
|
|
106
|
-
maxDeployables: 1,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
/** Coarse effort buckets, ascending. Anything past `maxMagnitude` routes full. */
|
|
110
|
-
const MAGNITUDE_SCALE = Object.freeze(['trivial', 'moderate', 'substantial']);
|
|
111
|
-
|
|
112
|
-
/** Coarse uncertainty buckets, ascending. */
|
|
113
|
-
const UNCERTAINTY_SCALE = Object.freeze(['determined', 'needs-design']);
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Directory roots whose immediate child is a separately-deployable unit. A
|
|
117
|
-
* footprint spanning two of them is the "multiple deployables" epic signal.
|
|
75
|
+
* The backstop's `LIGHT_DIFF_CEILINGS` are now the only size block on the
|
|
76
|
+
* light path, and they are the only ceilings measured against ground truth.
|
|
118
77
|
*/
|
|
119
|
-
const DEPLOYABLE_ROOTS = Object.freeze([
|
|
120
|
-
'apps',
|
|
121
|
-
'packages',
|
|
122
|
-
'services',
|
|
123
|
-
'functions',
|
|
124
|
-
'workers',
|
|
125
|
-
]);
|
|
126
78
|
|
|
127
79
|
/** Paths that are schema migrations rather than ordinary source. */
|
|
128
80
|
const MIGRATION_PATH_RE =
|
|
129
81
|
/(?:^|\/)(?:migrations?|migrate)(?:\/|$)|\.sql$|(?:^|\/)schema\.(?:prisma|rb)$/i;
|
|
130
82
|
|
|
131
|
-
/**
|
|
132
|
-
* Place a declared bucket on an ordered scale. **Absent** means "not declared"
|
|
133
|
-
* — no signal, so the coarse gate reads the supplied default rather than
|
|
134
|
-
* rejecting. **Present but unrecognized** is a malformed claim, which cannot be
|
|
135
|
-
* verified as small and therefore fails closed to the worst bucket on the
|
|
136
|
-
* scale.
|
|
137
|
-
*
|
|
138
|
-
* @param {unknown} value
|
|
139
|
-
* @param {readonly string[]} scale Ascending buckets.
|
|
140
|
-
* @param {string} whenAbsent Bucket to assume when nothing was declared.
|
|
141
|
-
* @returns {string}
|
|
142
|
-
*/
|
|
143
|
-
function normalizeBucket(value, scale, whenAbsent) {
|
|
144
|
-
if (value === undefined || value === null || value === '') return whenAbsent;
|
|
145
|
-
const key = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
146
|
-
return scale.includes(key) ? key : scale[scale.length - 1];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Resolve the distinct change KINDS in a footprint. An explicit `kinds[]`
|
|
151
|
-
* declaration wins; absent one, each entry's `assumption` is its kind — which
|
|
152
|
-
* is exactly the "N instances of one mechanical edit is one kind at N sites"
|
|
153
|
-
* reading, since N same-assumption entries collapse to one kind.
|
|
154
|
-
*
|
|
155
|
-
* @param {{ changes?: unknown, kinds?: unknown }} args
|
|
156
|
-
* @returns {string[]} Distinct kinds, in order of first appearance.
|
|
157
|
-
*/
|
|
158
|
-
function resolveChangeKinds({ changes, kinds }) {
|
|
159
|
-
const clean = (list) =>
|
|
160
|
-
list
|
|
161
|
-
.filter((k) => typeof k === 'string' && k.trim() !== '')
|
|
162
|
-
.map((k) => k.trim().toLowerCase());
|
|
163
|
-
const declared = clean(Array.isArray(kinds) ? kinds : []);
|
|
164
|
-
if (declared.length > 0) return [...new Set(declared)];
|
|
165
|
-
const derived = (Array.isArray(changes) ? changes : []).map((entry) =>
|
|
166
|
-
entry && typeof entry === 'object' && typeof entry.assumption === 'string'
|
|
167
|
-
? entry.assumption.trim().toLowerCase() || 'unspecified'
|
|
168
|
-
: 'unspecified',
|
|
169
|
-
);
|
|
170
|
-
return [...new Set(derived)];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Named deployable roots a footprint spans (`apps/web`, `packages/core`, …).
|
|
175
|
-
* The repository root itself is deliberately **not** counted: a change to one
|
|
176
|
-
* app plus a root-level README is one deployable, not two.
|
|
177
|
-
*
|
|
178
|
-
* @param {string[]} paths
|
|
179
|
-
* @returns {string[]}
|
|
180
|
-
*/
|
|
181
|
-
function resolveDeployables(paths) {
|
|
182
|
-
const ids = new Set();
|
|
183
|
-
for (const p of paths) {
|
|
184
|
-
const segments = String(p)
|
|
185
|
-
.split('/')
|
|
186
|
-
.filter((s) => s !== '');
|
|
187
|
-
if (segments.length >= 3 && DEPLOYABLE_ROOTS.includes(segments[0])) {
|
|
188
|
-
ids.add(`${segments[0]}/${segments[1]}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return [...ids];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
83
|
/**
|
|
195
84
|
* Does the footprint pair a schema migration with its consumers? A migration
|
|
196
85
|
* plus the code that reads through it is epic scope: the two have to land
|
|
@@ -205,83 +94,52 @@ function spansMigrationAndConsumers(paths) {
|
|
|
205
94
|
}
|
|
206
95
|
|
|
207
96
|
/**
|
|
208
|
-
* Stable machine-readable identifiers for every reason a
|
|
209
|
-
* the `code` field on a {@link deriveStoryShape} decision
|
|
97
|
+
* Stable machine-readable identifiers for every reason a footprint routes
|
|
98
|
+
* `full` — the `code` field on a {@link deriveStoryShape} decision
|
|
99
|
+
* (Story #4815).
|
|
210
100
|
*
|
|
211
101
|
* The prose in `reasons[]` is written for a human reading a gate envelope and
|
|
212
102
|
* is free to be re-worded; a caller that must **branch** on *which* rule
|
|
213
|
-
* objected reads this code instead
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* against ground truth by the diff backstop.
|
|
224
|
-
* - **Absolute rules** — `migration-span`, `sensitive-path`. Risk, not size.
|
|
103
|
+
* objected reads this code instead — keying that decision off reason text
|
|
104
|
+
* would make a copy-edit a routing change.
|
|
105
|
+
*
|
|
106
|
+
* Split two ways since Story #5344 deleted the ceiling rules
|
|
107
|
+
* (`change-kinds`, `magnitude`, `uncertainty`, `deployable-span`), and the
|
|
108
|
+
* grouping is the contract:
|
|
109
|
+
*
|
|
110
|
+
* - **Absolute rules** — `migration-span`, `sensitive-path`. Risk, not size,
|
|
111
|
+
* and derived from the predicted PATHS rather than a self-declared bucket.
|
|
112
|
+
* No re-slicing satisfies one.
|
|
225
113
|
* - **Unknown-footprint rejections** — `no-changes`, `unreadable-changes`,
|
|
226
|
-
* `glob-footprint`, `
|
|
227
|
-
*
|
|
114
|
+
* `glob-footprint`, `classification-unavailable`. Nothing was judged, so
|
|
115
|
+
* there is nothing to appeal. Story #5366 deleted `no-acceptance` with
|
|
116
|
+
* the `--acceptance` flag that was its only source: the flag clamped to a
|
|
117
|
+
* floor of one, so the zero-check behind this code could never fire.
|
|
228
118
|
*
|
|
229
119
|
* A `lite` route carries `code: null`.
|
|
230
120
|
*/
|
|
231
121
|
export const SHAPE_CODES = Object.freeze({
|
|
232
|
-
CHANGE_KINDS: 'change-kinds',
|
|
233
|
-
MAGNITUDE: 'magnitude',
|
|
234
|
-
UNCERTAINTY: 'uncertainty',
|
|
235
|
-
DEPLOYABLE_SPAN: 'deployable-span',
|
|
236
122
|
MIGRATION_SPAN: 'migration-span',
|
|
237
123
|
SENSITIVE_PATH: 'sensitive-path',
|
|
238
124
|
NO_CHANGES: 'no-changes',
|
|
239
125
|
UNREADABLE_CHANGES: 'unreadable-changes',
|
|
240
126
|
GLOB_FOOTPRINT: 'glob-footprint',
|
|
241
|
-
NO_ACCEPTANCE: 'no-acceptance',
|
|
242
127
|
CLASSIFICATION_UNAVAILABLE: 'classification-unavailable',
|
|
243
128
|
});
|
|
244
129
|
|
|
245
130
|
/**
|
|
246
|
-
* Ordered
|
|
247
|
-
* reason for a `full` route.
|
|
248
|
-
*
|
|
131
|
+
* Ordered **absolute risk** rules, evaluated in order; the first hit is the
|
|
132
|
+
* recorded reason for a `full` route. Neither reads a bucket the caller
|
|
133
|
+
* declared about itself — both are derived from the predicted paths, which is
|
|
134
|
+
* exactly why they survived the Story #5344 deletion of the effort ceilings.
|
|
249
135
|
*
|
|
250
136
|
* @type {ReadonlyArray<{
|
|
251
137
|
* code: string,
|
|
252
|
-
* when: (shape: object
|
|
253
|
-
* reason: (shape: object
|
|
138
|
+
* when: (shape: object) => boolean,
|
|
139
|
+
* reason: (shape: object) => string,
|
|
254
140
|
* }>}
|
|
255
141
|
*/
|
|
256
|
-
const
|
|
257
|
-
{
|
|
258
|
-
code: SHAPE_CODES.CHANGE_KINDS,
|
|
259
|
-
when: (s, c) => s.kindCount > c.maxChangeKinds,
|
|
260
|
-
reason: (s, c) =>
|
|
261
|
-
`${s.kindCount} distinct change kinds (${s.changeKinds.join(', ')}) > maxChangeKinds ${c.maxChangeKinds} — an explicit multi-capability enumeration, not one capability; full route`,
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
code: SHAPE_CODES.MAGNITUDE,
|
|
265
|
-
when: (s, c) =>
|
|
266
|
-
MAGNITUDE_SCALE.indexOf(s.magnitude) >
|
|
267
|
-
MAGNITUDE_SCALE.indexOf(c.maxMagnitude),
|
|
268
|
-
reason: (s, c) =>
|
|
269
|
-
`declared magnitude "${s.magnitude}" > maxMagnitude "${c.maxMagnitude}" — a substantial rewrite is effort a single inline pass should not absorb, however few files it touches; full route`,
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
code: SHAPE_CODES.UNCERTAINTY,
|
|
273
|
-
when: (s, c) =>
|
|
274
|
-
UNCERTAINTY_SCALE.indexOf(s.uncertainty) >
|
|
275
|
-
UNCERTAINTY_SCALE.indexOf(c.maxUncertainty),
|
|
276
|
-
reason: (s) =>
|
|
277
|
-
`the shape is not determined by the request (uncertainty "${s.uncertainty}") — the design decisions /mandrel-plan exists to resolve are still open; full route`,
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
code: SHAPE_CODES.DEPLOYABLE_SPAN,
|
|
281
|
-
when: (s, c) => s.deployables.length > c.maxDeployables,
|
|
282
|
-
reason: (s, c) =>
|
|
283
|
-
`footprint spans ${s.deployables.length} deployables (${s.deployables.join(', ')}) > maxDeployables ${c.maxDeployables} — clearly-epic scope; full route`,
|
|
284
|
-
},
|
|
142
|
+
const RISK_RULES = Object.freeze([
|
|
285
143
|
{
|
|
286
144
|
code: SHAPE_CODES.MIGRATION_SPAN,
|
|
287
145
|
when: (s) => s.migrationSpan,
|
|
@@ -292,41 +150,26 @@ const EFFORT_RULES = Object.freeze([
|
|
|
292
150
|
code: SHAPE_CODES.SENSITIVE_PATH,
|
|
293
151
|
when: (s) => s.sensitiveClasses.length > 0,
|
|
294
152
|
reason: (s) =>
|
|
295
|
-
`footprint intersects sensitive-path class(es) ${s.sensitiveClasses.join(', ')} — sensitivity wins over a small
|
|
153
|
+
`footprint intersects sensitive-path class(es) ${s.sensitiveClasses.join(', ')} — sensitivity wins over a small footprint; full route (deep review retained)`,
|
|
296
154
|
},
|
|
297
155
|
]);
|
|
298
156
|
|
|
299
157
|
/**
|
|
300
|
-
* First
|
|
301
|
-
* `null` when it clears them all.
|
|
158
|
+
* First absolute risk rule the footprint violates as a `{ code, reason }`
|
|
159
|
+
* pair, or `null` when it clears them all.
|
|
302
160
|
*
|
|
303
161
|
* @param {object} shape
|
|
304
|
-
* @param {typeof STORY_SHAPE_CEILINGS} ceilings
|
|
305
162
|
* @returns {{ code: string, reason: string }|null}
|
|
306
163
|
*/
|
|
307
|
-
function
|
|
308
|
-
for (const rule of
|
|
309
|
-
if (rule.when(shape
|
|
310
|
-
return { code: rule.code, reason: rule.reason(shape
|
|
164
|
+
function firstRiskViolation(shape) {
|
|
165
|
+
for (const rule of RISK_RULES) {
|
|
166
|
+
if (rule.when(shape)) {
|
|
167
|
+
return { code: rule.code, reason: rule.reason(shape) };
|
|
311
168
|
}
|
|
312
169
|
}
|
|
313
170
|
return null;
|
|
314
171
|
}
|
|
315
172
|
|
|
316
|
-
/**
|
|
317
|
-
* The non-negotiables the ceremony-lite path preserves (Story #4683 AC-2):
|
|
318
|
-
* collapsing ceremony never means dropping the Story ticket, the PR-to-`main`
|
|
319
|
-
* landing, the repo quality gates, or the security baseline. Attached
|
|
320
|
-
* verbatim to every route decision's `preserves` field so a downstream reader
|
|
321
|
-
* (or contract test) can assert the invariants held on either route.
|
|
322
|
-
*/
|
|
323
|
-
const LITE_PATH_INVARIANTS = Object.freeze({
|
|
324
|
-
storyTicket: true,
|
|
325
|
-
prToMain: true,
|
|
326
|
-
repoGates: true,
|
|
327
|
-
securityBaseline: true,
|
|
328
|
-
});
|
|
329
|
-
|
|
330
173
|
/**
|
|
331
174
|
* Count top-level enumerated items (`- `, `* `, `1. `) in a free-form seed —
|
|
332
175
|
* each enumerated line is one predicted artifact.
|
|
@@ -441,143 +284,150 @@ export function buildComplexitySignals({
|
|
|
441
284
|
}
|
|
442
285
|
|
|
443
286
|
/**
|
|
444
|
-
* Assemble
|
|
287
|
+
* Assemble one decision object. Module-level rather than a closure inside
|
|
288
|
+
* {@link deriveStoryShape}, so each rejection family below can build its own
|
|
289
|
+
* verdict through one shape.
|
|
290
|
+
*
|
|
291
|
+
* @param {'lite'|'full'} route
|
|
292
|
+
* @param {string|null} code
|
|
293
|
+
* @param {string} reason
|
|
294
|
+
* @param {object|null} [shape]
|
|
295
|
+
* @returns {object}
|
|
296
|
+
*/
|
|
297
|
+
function decide(route, code, reason, shape = null) {
|
|
298
|
+
return { route, reasons: [reason], code, shape };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Read the declared footprint into path entries, or the rejection that stands
|
|
303
|
+
* in for one when it cannot be read at all — the first of the three rejection
|
|
304
|
+
* families {@link SHAPE_CODES} groups. Nothing has been judged at this point,
|
|
305
|
+
* so neither rejection carries a shape.
|
|
306
|
+
*
|
|
307
|
+
* @param {unknown} changes
|
|
308
|
+
* @returns {{ entries: Array<{ path: string, isGlob?: boolean }>|null, rejection: object|null }}
|
|
309
|
+
*/
|
|
310
|
+
function readFootprintEntries(changes) {
|
|
311
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
312
|
+
return {
|
|
313
|
+
entries: null,
|
|
314
|
+
rejection: decide(
|
|
315
|
+
'full',
|
|
316
|
+
SHAPE_CODES.NO_CHANGES,
|
|
317
|
+
'no changes[] declared — the footprint is unknown, so the work cannot be judged trivial; conservative full route',
|
|
318
|
+
),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
return { entries: extractChangePaths(changes), rejection: null };
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return {
|
|
325
|
+
entries: null,
|
|
326
|
+
rejection: decide(
|
|
327
|
+
'full',
|
|
328
|
+
SHAPE_CODES.UNREADABLE_CHANGES,
|
|
329
|
+
`changes[] could not be read (${err?.message ?? err}) — unknown footprint; conservative full route`,
|
|
330
|
+
),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The one rejection a footprint that WAS read can still earn before any risk
|
|
337
|
+
* rule is reached: an unknowable width (a glob). Returns `null` when the
|
|
338
|
+
* footprint is judgeable.
|
|
339
|
+
*
|
|
340
|
+
* It used to have a sibling — a zero-length acceptance list — which Story
|
|
341
|
+
* #5366 removed along with the `--acceptance` flag that fed it. The flag
|
|
342
|
+
* clamped its own value to a floor of one, so the branch was unreachable from
|
|
343
|
+
* the only caller in the tree.
|
|
344
|
+
*
|
|
345
|
+
* @param {Array<{ isGlob?: boolean }>} entries
|
|
346
|
+
* @param {object} shape
|
|
347
|
+
* @returns {object|null}
|
|
348
|
+
*/
|
|
349
|
+
function unjudgeableFootprintRejection(entries, shape) {
|
|
350
|
+
if (entries.some((e) => e.isGlob)) {
|
|
351
|
+
return decide(
|
|
352
|
+
'full',
|
|
353
|
+
SHAPE_CODES.GLOB_FOOTPRINT,
|
|
354
|
+
'changes[] contains a glob path — unknown footprint width; conservative full route',
|
|
355
|
+
shape,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Assemble the risk shape of a footprint — the evidence
|
|
445
363
|
* {@link deriveStoryShape} decides on and carries on its result.
|
|
446
364
|
*
|
|
447
365
|
* @param {{
|
|
448
|
-
* changes: unknown[],
|
|
449
366
|
* paths: string[],
|
|
450
|
-
* acceptance?: unknown,
|
|
451
|
-
* kinds?: unknown,
|
|
452
|
-
* magnitude?: unknown,
|
|
453
|
-
* uncertainty?: unknown,
|
|
454
367
|
* sensitiveClasses: string[],
|
|
455
368
|
* }} args
|
|
456
369
|
* @returns {{
|
|
457
370
|
* siteCount: number,
|
|
458
|
-
* changeKinds: string[],
|
|
459
|
-
* kindCount: number,
|
|
460
|
-
* magnitude: string,
|
|
461
|
-
* uncertainty: string,
|
|
462
|
-
* acceptanceCount: number,
|
|
463
|
-
* deployables: string[],
|
|
464
371
|
* migrationSpan: boolean,
|
|
465
372
|
* sensitiveClasses: string[],
|
|
466
373
|
* }}
|
|
467
374
|
*/
|
|
468
|
-
function
|
|
469
|
-
changes,
|
|
470
|
-
paths,
|
|
471
|
-
acceptance,
|
|
472
|
-
kinds,
|
|
473
|
-
magnitude,
|
|
474
|
-
uncertainty,
|
|
475
|
-
sensitiveClasses,
|
|
476
|
-
}) {
|
|
477
|
-
const changeKinds = resolveChangeKinds({ changes, kinds });
|
|
375
|
+
function buildRiskShape({ paths, sensitiveClasses }) {
|
|
478
376
|
return {
|
|
479
377
|
siteCount: paths.length,
|
|
480
|
-
changeKinds,
|
|
481
|
-
kindCount: changeKinds.length,
|
|
482
|
-
magnitude: normalizeBucket(magnitude, MAGNITUDE_SCALE, 'moderate'),
|
|
483
|
-
uncertainty: normalizeBucket(uncertainty, UNCERTAINTY_SCALE, 'determined'),
|
|
484
|
-
acceptanceCount: Array.isArray(acceptance) ? acceptance.length : 0,
|
|
485
|
-
deployables: resolveDeployables(paths),
|
|
486
378
|
migrationSpan: spansMigrationAndConsumers(paths),
|
|
487
379
|
sensitiveClasses,
|
|
488
380
|
};
|
|
489
381
|
}
|
|
490
382
|
|
|
491
383
|
/**
|
|
492
|
-
* Derive the complexity route from
|
|
493
|
-
*
|
|
494
|
-
* — the single
|
|
495
|
-
*
|
|
384
|
+
* Derive the complexity route from a footprint's **risk** (Story #4722
|
|
385
|
+
* AC-3/AC-4; re-anchored off artifact cardinality by Story #4764; the declared
|
|
386
|
+
* effort ceilings deleted by Story #5344) — the single function the light
|
|
387
|
+
* path's suitability gate reads, so prediction-time and close-time can never
|
|
388
|
+
* disagree about what is sensitive.
|
|
496
389
|
*
|
|
497
|
-
* `lite` requires
|
|
498
|
-
* {@link STORY_SHAPE_CEILINGS}:
|
|
390
|
+
* `lite` requires:
|
|
499
391
|
*
|
|
500
392
|
* - a declared, parseable, glob-free `changes[]` footprint — width is not
|
|
501
|
-
* counted, but an unknown
|
|
502
|
-
* -
|
|
503
|
-
* judged trivial). The criteria are **not** capped: criterion count is
|
|
504
|
-
* contract detail, not effort;
|
|
505
|
-
* - at most `maxChangeKinds` distinct change kinds, magnitude no worse than
|
|
506
|
-
* `maxMagnitude`, uncertainty no worse than `maxUncertainty`, and at most
|
|
507
|
-
* `maxDeployables` deployable roots — plus no migration-with-consumers
|
|
508
|
-
* span. These are the clearly-epic rejections, and nothing finer: the
|
|
509
|
-
* declared footprint is a guess, so the diff-derived backstop does the real
|
|
510
|
-
* enforcement (see {@link STORY_SHAPE_CEILINGS});
|
|
393
|
+
* counted, but an unknown footprint cannot be classified for risk;
|
|
394
|
+
* - no migration-with-consumers span;
|
|
511
395
|
* - a footprint intersecting **no** sensitive-path class
|
|
512
396
|
* (`deriveChangeLevel`, the taxonomy close applies to the landed diff).
|
|
513
397
|
* Sensitivity always wins (AC-6): a sensitive footprint routes `full`
|
|
514
|
-
* however small or mechanical, which keeps the
|
|
515
|
-
* `
|
|
398
|
+
* however small or mechanical, which keeps the deep code review via
|
|
399
|
+
* `review-depth.js#resolveDepth`. Since Story #5343 it does NOT also buy a
|
|
400
|
+
* fresh acceptance critic — that owner follows the ceremony profile.
|
|
401
|
+
*
|
|
402
|
+
* **Size is not a rule here.** It was, and Story #5344 removed it: every size
|
|
403
|
+
* axis was a bucket the caller declared about its own request, and the light
|
|
404
|
+
* path's diff backstop measures the real change set afterwards. What is left
|
|
405
|
+
* reads the predicted paths, which is evidence.
|
|
516
406
|
*
|
|
517
|
-
* Everything else — an unknown/undeclared footprint
|
|
518
|
-
*
|
|
519
|
-
* `full`. Total: never throws.
|
|
407
|
+
* Everything else — an unknown/undeclared footprint or an unreadable
|
|
408
|
+
* sensitive-path manifest — fails toward `full`. Total: never throws.
|
|
520
409
|
*
|
|
521
410
|
* @param {{
|
|
522
411
|
* changes?: unknown,
|
|
523
|
-
* acceptance?: unknown,
|
|
524
|
-
* kinds?: unknown,
|
|
525
|
-
* magnitude?: unknown,
|
|
526
|
-
* uncertainty?: unknown,
|
|
527
412
|
* injectedRules?: object,
|
|
528
413
|
* selectSensitivePathClassesFn?: Function,
|
|
529
|
-
* }} [args]
|
|
530
|
-
* entry's `assumption` is its kind); `magnitude` and `uncertainty` are the
|
|
531
|
-
* declared coarse buckets.
|
|
414
|
+
* }} [args]
|
|
532
415
|
* @returns {{
|
|
533
416
|
* route: ComplexityRoute,
|
|
534
417
|
* reasons: string[],
|
|
535
418
|
* code: string|null,
|
|
536
|
-
* shape: ReturnType<typeof
|
|
537
|
-
* ceilings: typeof STORY_SHAPE_CEILINGS,
|
|
538
|
-
* preserves: typeof LITE_PATH_INVARIANTS,
|
|
419
|
+
* shape: ReturnType<typeof buildRiskShape>|null,
|
|
539
420
|
* }} `code` is the stable {@link SHAPE_CODES} identifier for the rule that
|
|
540
|
-
* rejected the
|
|
541
|
-
* since `reasons[]` is human prose and free to be re-worded.
|
|
421
|
+
* rejected the footprint (`null` on `lite`) — the field a caller branches
|
|
422
|
+
* on, since `reasons[]` is human prose and free to be re-worded.
|
|
542
423
|
*/
|
|
543
424
|
export function deriveStoryShape({
|
|
544
425
|
changes,
|
|
545
|
-
acceptance,
|
|
546
|
-
kinds,
|
|
547
|
-
magnitude,
|
|
548
|
-
uncertainty,
|
|
549
426
|
injectedRules,
|
|
550
427
|
selectSensitivePathClassesFn,
|
|
551
428
|
} = {}) {
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
const decide = (route, code, reason, shape = null) => ({
|
|
555
|
-
route,
|
|
556
|
-
reasons: [reason],
|
|
557
|
-
code,
|
|
558
|
-
shape,
|
|
559
|
-
ceilings,
|
|
560
|
-
preserves,
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
if (!Array.isArray(changes) || changes.length === 0) {
|
|
564
|
-
return decide(
|
|
565
|
-
'full',
|
|
566
|
-
SHAPE_CODES.NO_CHANGES,
|
|
567
|
-
'no changes[] declared — the footprint is unknown, so the work cannot be judged trivial; conservative full route',
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
let entries;
|
|
572
|
-
try {
|
|
573
|
-
entries = extractChangePaths(changes);
|
|
574
|
-
} catch (err) {
|
|
575
|
-
return decide(
|
|
576
|
-
'full',
|
|
577
|
-
SHAPE_CODES.UNREADABLE_CHANGES,
|
|
578
|
-
`changes[] could not be read (${err?.message ?? err}) — unknown footprint; conservative full route`,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
429
|
+
const { entries, rejection } = readFootprintEntries(changes);
|
|
430
|
+
if (rejection !== null) return rejection;
|
|
581
431
|
|
|
582
432
|
const paths = entries.map((e) => e.path);
|
|
583
433
|
const { level, classes } = deriveChangeLevel({
|
|
@@ -585,34 +435,12 @@ export function deriveStoryShape({
|
|
|
585
435
|
injectedRules,
|
|
586
436
|
selectSensitivePathClassesFn,
|
|
587
437
|
});
|
|
588
|
-
const shape =
|
|
589
|
-
changes,
|
|
590
|
-
paths,
|
|
591
|
-
acceptance,
|
|
592
|
-
kinds,
|
|
593
|
-
magnitude,
|
|
594
|
-
uncertainty,
|
|
595
|
-
sensitiveClasses: classes,
|
|
596
|
-
});
|
|
438
|
+
const shape = buildRiskShape({ paths, sensitiveClasses: classes });
|
|
597
439
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
'full',
|
|
601
|
-
SHAPE_CODES.GLOB_FOOTPRINT,
|
|
602
|
-
'changes[] contains a glob path — unknown footprint width; conservative full route',
|
|
603
|
-
shape,
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
if (shape.acceptanceCount === 0) {
|
|
607
|
-
return decide(
|
|
608
|
-
'full',
|
|
609
|
-
SHAPE_CODES.NO_ACCEPTANCE,
|
|
610
|
-
'no acceptance criteria — the contract cannot be judged trivial; conservative full route',
|
|
611
|
-
shape,
|
|
612
|
-
);
|
|
613
|
-
}
|
|
440
|
+
const unjudgeable = unjudgeableFootprintRejection(entries, shape);
|
|
441
|
+
if (unjudgeable !== null) return unjudgeable;
|
|
614
442
|
|
|
615
|
-
const violation =
|
|
443
|
+
const violation = firstRiskViolation(shape);
|
|
616
444
|
if (violation !== null) {
|
|
617
445
|
return decide('full', violation.code, violation.reason, shape);
|
|
618
446
|
}
|
|
@@ -632,7 +460,7 @@ export function deriveStoryShape({
|
|
|
632
460
|
return decide(
|
|
633
461
|
'lite',
|
|
634
462
|
null,
|
|
635
|
-
`
|
|
463
|
+
`no absolute risk rule fires across ${shape.siteCount} predicted path(s): no migration-with-consumers span, no sensitive-path class — inline-eligible; size is bounded by the diff backstop`,
|
|
636
464
|
shape,
|
|
637
465
|
);
|
|
638
466
|
}
|