mandrel 2.14.0 → 2.16.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/audit-checklists/navigability.md +1 -1
- package/.agents/docs/workflows.md +4 -4
- package/.agents/scripts/acceptance-eval.js +18 -1
- package/.agents/scripts/agents-bootstrap-github.js +22 -1
- package/.agents/scripts/apply-quality-bootstrap.js +6 -0
- package/.agents/scripts/audit-labels-bootstrap.js +15 -1
- package/.agents/scripts/audit-to-stories.js +26 -1
- package/.agents/scripts/boot-sweep.js +4 -1
- package/.agents/scripts/bootstrap.js +1 -0
- package/.agents/scripts/check-arch-cycles.js +20 -0
- package/.agents/scripts/check-baselines.js +8 -2
- package/.agents/scripts/check-context-budget.js +40 -5
- package/.agents/scripts/check-dead-exports.js +21 -0
- package/.agents/scripts/check-doc-links.js +12 -1
- package/.agents/scripts/check-lifecycle-doc-drift.js +9 -0
- package/.agents/scripts/check-workflow-citations.js +332 -0
- package/.agents/scripts/deliver-light.js +32 -3
- package/.agents/scripts/deliver-recover.js +4 -1
- package/.agents/scripts/diagnose-friction.js +17 -1
- package/.agents/scripts/diagnose.js +20 -14
- package/.agents/scripts/drain-pending-cleanup.js +20 -1
- package/.agents/scripts/evidence-gate.js +20 -1
- package/.agents/scripts/generate-config-docs.js +14 -1
- package/.agents/scripts/generate-lifecycle-docs.js +14 -1
- package/.agents/scripts/generate-workflows-doc.js +14 -1
- package/.agents/scripts/git-cleanup.js +32 -1
- package/.agents/scripts/lib/cli-usage.js +174 -0
- package/.agents/scripts/lib/cli-utils.js +12 -0
- package/.agents/scripts/lib/close-validation/process.js +61 -15
- package/.agents/scripts/lib/doc-tiers.js +53 -10
- package/.agents/scripts/lib/orchestration/complexity-gate.js +307 -89
- package/.agents/scripts/lib/orchestration/light-suitability.js +31 -9
- package/.agents/scripts/lib/orchestration/plan-context.js +205 -20
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +87 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +24 -15
- package/.agents/scripts/lib/workflow-closure.js +431 -0
- package/.agents/scripts/mandrel-update-preflight.js +9 -0
- package/.agents/scripts/nav-registry-diff.js +13 -0
- package/.agents/scripts/plan-context.js +21 -2
- package/.agents/scripts/plan-critics.js +10 -0
- package/.agents/scripts/plan-persist.js +33 -1
- package/.agents/scripts/plan-run-epilogue.js +12 -1
- package/.agents/scripts/quality-preview.js +17 -1
- package/.agents/scripts/resolve-doc-tiers.js +13 -0
- package/.agents/scripts/resolve-stories.js +1 -0
- package/.agents/scripts/resync-status-column.js +4 -1
- package/.agents/scripts/signals-view.js +11 -0
- package/.agents/scripts/single-story-close.js +24 -0
- package/.agents/scripts/single-story-confirm-merge.js +16 -0
- package/.agents/scripts/single-story-init.js +21 -1
- package/.agents/scripts/stories-wave-tick.js +1 -0
- package/.agents/scripts/sync-agentrc.js +16 -4
- package/.agents/scripts/update-ticket-state.js +23 -2
- package/.agents/workflows/audit-navigability.md +2 -2
- package/.agents/workflows/audit-to-stories.md +1 -1
- package/.agents/workflows/deliver.md +80 -81
- package/.agents/workflows/git-cleanup.md +9 -14
- package/.agents/workflows/helpers/acceptance-self-eval.md +14 -13
- package/.agents/workflows/helpers/audit-lens-core.md +2 -2
- package/.agents/workflows/helpers/code-review.md +11 -11
- package/.agents/workflows/helpers/deliver-digest.md +7 -8
- package/.agents/workflows/helpers/deliver-light.md +222 -0
- package/.agents/workflows/helpers/deliver-reference.md +46 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +55 -63
- package/.agents/workflows/helpers/deliver-story.md +22 -22
- package/.agents/workflows/helpers/mandrel-sync-config.md +2 -2
- package/.agents/workflows/helpers/parallel-tooling.md +1 -2
- package/.agents/workflows/helpers/plan-reference.md +96 -17
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -4
- package/.agents/workflows/mandrel-update.md +6 -6
- package/.agents/workflows/plan.md +90 -85
- package/.agents/workflows/prototype.md +104 -0
- package/docs/CHANGELOG.md +32 -0
- package/package.json +2 -1
- package/.agents/workflows/deliver-light.md +0 -148
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
* reason is `full`.
|
|
25
25
|
* 3. **Deterministic backstop at persist.** After authoring, the work has
|
|
26
26
|
* measurable shape: {@link deriveStoryShape} reads the Story's own
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* A `lite` claim whose
|
|
30
|
-
* `full`** (`run-plan-persist.js`).
|
|
27
|
+
* effort and risk — distinct change kinds, declared magnitude,
|
|
28
|
+
* uncertainty, deployable/migration span, and sensitive-path classes —
|
|
29
|
+
* against {@link STORY_SHAPE_CEILINGS}. A `lite` claim whose work exceeds
|
|
30
|
+
* them **fails closed to `full`** (`run-plan-persist.js`). Artifact
|
|
31
|
+
* cardinality is deliberately not an axis (Story #4764).
|
|
31
32
|
* 4. **Deliver re-derives.** `/deliver` computes the route from the fetched
|
|
32
33
|
* Story body via the **same** shape function at dispatch
|
|
33
34
|
* ({@link resolveStoryDispatchMode}) and honors it: a lite-shaped Story
|
|
@@ -104,35 +105,221 @@ const DEFAULT_COMPLEXITY_GATE = Object.freeze({
|
|
|
104
105
|
export const LITE_ROUTE_LABEL = 'route::lite';
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
|
-
*
|
|
108
|
+
* Effort/risk ceilings a Story's work must fit for the `lite` route
|
|
108
109
|
* ({@link deriveStoryShape}). Framework constants, not operator knobs — a
|
|
109
|
-
* ceiling an operator can widen past what the inline path can safely absorb
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
110
|
+
* ceiling an operator can widen past what the inline path can safely absorb is
|
|
111
|
+
* a ceiling that fails silently.
|
|
112
|
+
*
|
|
113
|
+
* ## Effort and risk, never artifact cardinality (Story #4764)
|
|
114
|
+
*
|
|
115
|
+
* These ceilings used to count the declared footprint (`maxChanges: 2`,
|
|
116
|
+
* `maxAcceptance: 3`, `maxNonCreateChanges: 1`). Cardinality is the wrong axis
|
|
117
|
+
* in both directions: three identical one-line edits across three files is
|
|
118
|
+
* trivial work with a high count, while a 200-line rewrite of one module is a
|
|
119
|
+
* single change. And the count was read off a footprint the model **declares
|
|
120
|
+
* before doing the work** — a guess, and a gameable one — so counting it
|
|
121
|
+
* rejected genuinely small work (mandrel-bench's hello-world scenario is a
|
|
122
|
+
* server create plus a `package.json` edit plus a test create, structurally
|
|
123
|
+
* over the old ceilings) while admitting whatever an optimistic declaration
|
|
124
|
+
* under-counted.
|
|
125
|
+
*
|
|
126
|
+
* So the axes are effort, risk, and uncertainty, and the **prediction** gate
|
|
127
|
+
* they form is deliberately **coarse**: it rejects clearly-epic work only.
|
|
128
|
+
* Real enforcement belongs to the diff-derived backstop, which sees ground
|
|
129
|
+
* truth instead of a declaration
|
|
130
|
+
* ({@link module:lib/orchestration/light-suitability.checkLightDiffBackstop}).
|
|
131
|
+
*
|
|
132
|
+
* - `maxChangeKinds` — distinct change KINDS, not files. N instances of one
|
|
133
|
+
* mechanical edit is one kind at N sites; enumerating
|
|
134
|
+
* more kinds than this is a multi-capability scope.
|
|
135
|
+
* - `maxMagnitude` — coarse magnitude bucket, declared alongside the
|
|
136
|
+
* footprint: `trivial` < `moderate` < `substantial`.
|
|
137
|
+
* - `maxUncertainty` — is the shape determined by the request
|
|
138
|
+
* (`determined`), or does it still need the design
|
|
139
|
+
* decisions `/plan` exists to resolve
|
|
140
|
+
* (`needs-design`)?
|
|
141
|
+
* - `maxDeployables` — named deployable roots (`apps/<x>`, `packages/<x>`, …)
|
|
142
|
+
* the footprint spans; more than one is epic by
|
|
143
|
+
* construction.
|
|
144
|
+
*
|
|
145
|
+
* Two rules ride beside the ceilings and are not tunable at all: a footprint
|
|
146
|
+
* pairing a migration with its consumers is epic scope, and a footprint
|
|
147
|
+
* intersecting a sensitive-path class routes `full` however small or mechanical
|
|
148
|
+
* it is — the hard gate, unchanged.
|
|
122
149
|
*
|
|
123
150
|
* Exposed as the `ceilings` field on every {@link deriveStoryShape} decision
|
|
124
|
-
* and exported directly (Story #4740) so the
|
|
151
|
+
* and exported directly (Story #4740) so the light path's suitability gate
|
|
125
152
|
* ({@link module:lib/orchestration/light-suitability}) judges a prompt's
|
|
126
|
-
* predicted footprint against the **same**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
153
|
+
* predicted footprint against the **same** axes the plan-time shape backstop
|
|
154
|
+
* applies — one source, so the light entry point and the plan path can never
|
|
155
|
+
* disagree about what work is trivial.
|
|
129
156
|
*/
|
|
130
157
|
export const STORY_SHAPE_CEILINGS = Object.freeze({
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
maxChangeKinds: 2,
|
|
159
|
+
maxMagnitude: 'moderate',
|
|
160
|
+
maxUncertainty: 'determined',
|
|
161
|
+
maxDeployables: 1,
|
|
134
162
|
});
|
|
135
163
|
|
|
164
|
+
/** Coarse effort buckets, ascending. Anything past `maxMagnitude` routes full. */
|
|
165
|
+
const MAGNITUDE_SCALE = Object.freeze(['trivial', 'moderate', 'substantial']);
|
|
166
|
+
|
|
167
|
+
/** Coarse uncertainty buckets, ascending. */
|
|
168
|
+
const UNCERTAINTY_SCALE = Object.freeze(['determined', 'needs-design']);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Directory roots whose immediate child is a separately-deployable unit. A
|
|
172
|
+
* footprint spanning two of them is the "multiple deployables" epic signal.
|
|
173
|
+
*/
|
|
174
|
+
const DEPLOYABLE_ROOTS = Object.freeze([
|
|
175
|
+
'apps',
|
|
176
|
+
'packages',
|
|
177
|
+
'services',
|
|
178
|
+
'functions',
|
|
179
|
+
'workers',
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
/** Paths that are schema migrations rather than ordinary source. */
|
|
183
|
+
const MIGRATION_PATH_RE =
|
|
184
|
+
/(?:^|\/)(?:migrations?|migrate)(?:\/|$)|\.sql$|(?:^|\/)schema\.(?:prisma|rb)$/i;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Place a declared bucket on an ordered scale. **Absent** means "not declared"
|
|
188
|
+
* — no signal, so the coarse gate reads the supplied default rather than
|
|
189
|
+
* rejecting. **Present but unrecognized** is a malformed claim, which cannot be
|
|
190
|
+
* verified as small and therefore fails closed to the worst bucket on the
|
|
191
|
+
* scale.
|
|
192
|
+
*
|
|
193
|
+
* @param {unknown} value
|
|
194
|
+
* @param {readonly string[]} scale Ascending buckets.
|
|
195
|
+
* @param {string} whenAbsent Bucket to assume when nothing was declared.
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function normalizeBucket(value, scale, whenAbsent) {
|
|
199
|
+
if (value === undefined || value === null || value === '') return whenAbsent;
|
|
200
|
+
const key = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
201
|
+
return scale.includes(key) ? key : scale[scale.length - 1];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve the distinct change KINDS in a footprint. An explicit `kinds[]`
|
|
206
|
+
* declaration wins; absent one, each entry's `assumption` is its kind — which
|
|
207
|
+
* is exactly the "N instances of one mechanical edit is one kind at N sites"
|
|
208
|
+
* reading, since N same-assumption entries collapse to one kind.
|
|
209
|
+
*
|
|
210
|
+
* @param {{ changes?: unknown, kinds?: unknown }} args
|
|
211
|
+
* @returns {string[]} Distinct kinds, in order of first appearance.
|
|
212
|
+
*/
|
|
213
|
+
function resolveChangeKinds({ changes, kinds }) {
|
|
214
|
+
const clean = (list) =>
|
|
215
|
+
list
|
|
216
|
+
.filter((k) => typeof k === 'string' && k.trim() !== '')
|
|
217
|
+
.map((k) => k.trim().toLowerCase());
|
|
218
|
+
const declared = clean(Array.isArray(kinds) ? kinds : []);
|
|
219
|
+
if (declared.length > 0) return [...new Set(declared)];
|
|
220
|
+
const derived = (Array.isArray(changes) ? changes : []).map((entry) =>
|
|
221
|
+
entry && typeof entry === 'object' && typeof entry.assumption === 'string'
|
|
222
|
+
? entry.assumption.trim().toLowerCase() || 'unspecified'
|
|
223
|
+
: 'unspecified',
|
|
224
|
+
);
|
|
225
|
+
return [...new Set(derived)];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Named deployable roots a footprint spans (`apps/web`, `packages/core`, …).
|
|
230
|
+
* The repository root itself is deliberately **not** counted: a change to one
|
|
231
|
+
* app plus a root-level README is one deployable, not two.
|
|
232
|
+
*
|
|
233
|
+
* @param {string[]} paths
|
|
234
|
+
* @returns {string[]}
|
|
235
|
+
*/
|
|
236
|
+
function resolveDeployables(paths) {
|
|
237
|
+
const ids = new Set();
|
|
238
|
+
for (const p of paths) {
|
|
239
|
+
const segments = String(p)
|
|
240
|
+
.split('/')
|
|
241
|
+
.filter((s) => s !== '');
|
|
242
|
+
if (segments.length >= 3 && DEPLOYABLE_ROOTS.includes(segments[0])) {
|
|
243
|
+
ids.add(`${segments[0]}/${segments[1]}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return [...ids];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Does the footprint pair a schema migration with its consumers? A migration
|
|
251
|
+
* plus the code that reads through it is epic scope: the two have to land
|
|
252
|
+
* together and the ordering is the design work.
|
|
253
|
+
*
|
|
254
|
+
* @param {string[]} paths
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
function spansMigrationAndConsumers(paths) {
|
|
258
|
+
const migrations = paths.filter((p) => MIGRATION_PATH_RE.test(String(p)));
|
|
259
|
+
return migrations.length > 0 && migrations.length < paths.length;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Ordered effort/risk rules, evaluated in order; the first hit is the recorded
|
|
264
|
+
* reason for a `full` route. Every rule names an effort, risk, or uncertainty
|
|
265
|
+
* property of the work — none counts artifacts.
|
|
266
|
+
*
|
|
267
|
+
* @type {ReadonlyArray<{
|
|
268
|
+
* when: (shape: object, ceilings: typeof STORY_SHAPE_CEILINGS) => boolean,
|
|
269
|
+
* reason: (shape: object, ceilings: typeof STORY_SHAPE_CEILINGS) => string,
|
|
270
|
+
* }>}
|
|
271
|
+
*/
|
|
272
|
+
const EFFORT_RULES = Object.freeze([
|
|
273
|
+
{
|
|
274
|
+
when: (s, c) => s.kindCount > c.maxChangeKinds,
|
|
275
|
+
reason: (s, c) =>
|
|
276
|
+
`${s.kindCount} distinct change kinds (${s.changeKinds.join(', ')}) > maxChangeKinds ${c.maxChangeKinds} — an explicit multi-capability enumeration, not one capability; full route`,
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
when: (s, c) =>
|
|
280
|
+
MAGNITUDE_SCALE.indexOf(s.magnitude) >
|
|
281
|
+
MAGNITUDE_SCALE.indexOf(c.maxMagnitude),
|
|
282
|
+
reason: (s, c) =>
|
|
283
|
+
`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`,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
when: (s, c) =>
|
|
287
|
+
UNCERTAINTY_SCALE.indexOf(s.uncertainty) >
|
|
288
|
+
UNCERTAINTY_SCALE.indexOf(c.maxUncertainty),
|
|
289
|
+
reason: (s) =>
|
|
290
|
+
`the shape is not determined by the request (uncertainty "${s.uncertainty}") — the design decisions /plan exists to resolve are still open; full route`,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
when: (s, c) => s.deployables.length > c.maxDeployables,
|
|
294
|
+
reason: (s, c) =>
|
|
295
|
+
`footprint spans ${s.deployables.length} deployables (${s.deployables.join(', ')}) > maxDeployables ${c.maxDeployables} — clearly-epic scope; full route`,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
when: (s) => s.migrationSpan,
|
|
299
|
+
reason: () =>
|
|
300
|
+
'footprint pairs a migration with its consumers — clearly-epic scope; full route',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
when: (s) => s.sensitiveClasses.length > 0,
|
|
304
|
+
reason: (s) =>
|
|
305
|
+
`footprint intersects sensitive-path class(es) ${s.sensitiveClasses.join(', ')} — sensitivity wins over a small shape; full route (fresh acceptance critic retained)`,
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* First effort/risk rule the shape violates, or `null` when it clears them all.
|
|
311
|
+
*
|
|
312
|
+
* @param {object} shape
|
|
313
|
+
* @param {typeof STORY_SHAPE_CEILINGS} ceilings
|
|
314
|
+
* @returns {string|null}
|
|
315
|
+
*/
|
|
316
|
+
function firstEffortViolation(shape, ceilings) {
|
|
317
|
+
for (const rule of EFFORT_RULES) {
|
|
318
|
+
if (rule.when(shape, ceilings)) return rule.reason(shape, ceilings);
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
136
323
|
/**
|
|
137
324
|
* The non-negotiables the ceremony-lite path preserves (Story #4683 AC-2):
|
|
138
325
|
* collapsing ceremony never means dropping the Story ticket, the PR-to-`main`
|
|
@@ -376,44 +563,98 @@ export function resolvePlannerRouteVerdict({ reason } = {}) {
|
|
|
376
563
|
}
|
|
377
564
|
|
|
378
565
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
566
|
+
* Assemble the effort/risk shape of a footprint — the evidence
|
|
567
|
+
* {@link deriveStoryShape} decides on and carries on its result.
|
|
568
|
+
*
|
|
569
|
+
* @param {{
|
|
570
|
+
* changes: unknown[],
|
|
571
|
+
* paths: string[],
|
|
572
|
+
* acceptance?: unknown,
|
|
573
|
+
* kinds?: unknown,
|
|
574
|
+
* magnitude?: unknown,
|
|
575
|
+
* uncertainty?: unknown,
|
|
576
|
+
* sensitiveClasses: string[],
|
|
577
|
+
* }} args
|
|
578
|
+
* @returns {{
|
|
579
|
+
* siteCount: number,
|
|
580
|
+
* changeKinds: string[],
|
|
581
|
+
* kindCount: number,
|
|
582
|
+
* magnitude: string,
|
|
583
|
+
* uncertainty: string,
|
|
584
|
+
* acceptanceCount: number,
|
|
585
|
+
* deployables: string[],
|
|
586
|
+
* migrationSpan: boolean,
|
|
587
|
+
* sensitiveClasses: string[],
|
|
588
|
+
* }}
|
|
589
|
+
*/
|
|
590
|
+
function buildEffortShape({
|
|
591
|
+
changes,
|
|
592
|
+
paths,
|
|
593
|
+
acceptance,
|
|
594
|
+
kinds,
|
|
595
|
+
magnitude,
|
|
596
|
+
uncertainty,
|
|
597
|
+
sensitiveClasses,
|
|
598
|
+
}) {
|
|
599
|
+
const changeKinds = resolveChangeKinds({ changes, kinds });
|
|
600
|
+
return {
|
|
601
|
+
siteCount: paths.length,
|
|
602
|
+
changeKinds,
|
|
603
|
+
kindCount: changeKinds.length,
|
|
604
|
+
magnitude: normalizeBucket(magnitude, MAGNITUDE_SCALE, 'moderate'),
|
|
605
|
+
uncertainty: normalizeBucket(uncertainty, UNCERTAINTY_SCALE, 'determined'),
|
|
606
|
+
acceptanceCount: Array.isArray(acceptance) ? acceptance.length : 0,
|
|
607
|
+
deployables: resolveDeployables(paths),
|
|
608
|
+
migrationSpan: spansMigrationAndConsumers(paths),
|
|
609
|
+
sensitiveClasses,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Derive the complexity route from an authored Story's **effort and risk**
|
|
615
|
+
* (Story #4722 AC-3/AC-4; re-anchored off artifact cardinality by Story #4764)
|
|
616
|
+
* — the single shape function persist's backstop and `/deliver`'s dispatch
|
|
617
|
+
* derivation both read, so the two can never disagree about the same body.
|
|
383
618
|
*
|
|
384
619
|
* `lite` requires **every** signal to agree, against
|
|
385
620
|
* {@link STORY_SHAPE_CEILINGS}:
|
|
386
621
|
*
|
|
387
|
-
* - a declared, parseable, glob-free `changes[]` footprint
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
622
|
+
* - a declared, parseable, glob-free `changes[]` footprint — width is not
|
|
623
|
+
* counted, but an unknown width cannot be judged;
|
|
624
|
+
* - at least one acceptance criterion (a Story with no contract cannot be
|
|
625
|
+
* judged trivial). The criteria are **not** capped: criterion count is
|
|
626
|
+
* contract detail, not effort;
|
|
627
|
+
* - at most `maxChangeKinds` distinct change kinds, magnitude no worse than
|
|
628
|
+
* `maxMagnitude`, uncertainty no worse than `maxUncertainty`, and at most
|
|
629
|
+
* `maxDeployables` deployable roots — plus no migration-with-consumers
|
|
630
|
+
* span. These are the clearly-epic rejections, and nothing finer: the
|
|
631
|
+
* declared footprint is a guess, so the diff-derived backstop does the real
|
|
632
|
+
* enforcement (see {@link STORY_SHAPE_CEILINGS});
|
|
392
633
|
* - a footprint intersecting **no** sensitive-path class
|
|
393
634
|
* (`deriveChangeLevel`, the taxonomy close applies to the landed diff).
|
|
394
|
-
* Sensitivity always wins (AC-6): a sensitive footprint routes `full
|
|
395
|
-
* which keeps the fresh acceptance critic via
|
|
635
|
+
* Sensitivity always wins (AC-6): a sensitive footprint routes `full`
|
|
636
|
+
* however small or mechanical, which keeps the fresh acceptance critic via
|
|
637
|
+
* `ceremony-routing.js`.
|
|
396
638
|
*
|
|
397
|
-
* Everything else —
|
|
398
|
-
* unreadable sensitive-path manifest — fails toward
|
|
399
|
-
* throws.
|
|
639
|
+
* Everything else — an unknown/undeclared footprint, a malformed magnitude or
|
|
640
|
+
* uncertainty claim, or an unreadable sensitive-path manifest — fails toward
|
|
641
|
+
* `full`. Total: never throws.
|
|
400
642
|
*
|
|
401
643
|
* @param {{
|
|
402
644
|
* changes?: unknown,
|
|
403
645
|
* acceptance?: unknown,
|
|
646
|
+
* kinds?: unknown,
|
|
647
|
+
* magnitude?: unknown,
|
|
648
|
+
* uncertainty?: unknown,
|
|
404
649
|
* injectedRules?: object,
|
|
405
650
|
* selectSensitivePathClassesFn?: Function,
|
|
406
|
-
* }} [args]
|
|
651
|
+
* }} [args] `kinds` declares the distinct change kinds explicitly (absent, each
|
|
652
|
+
* entry's `assumption` is its kind); `magnitude` and `uncertainty` are the
|
|
653
|
+
* declared coarse buckets.
|
|
407
654
|
* @returns {{
|
|
408
655
|
* route: ComplexityRoute,
|
|
409
656
|
* reasons: string[],
|
|
410
|
-
* shape:
|
|
411
|
-
* changeCount: number,
|
|
412
|
-
* acceptanceCount: number,
|
|
413
|
-
* createCount: number,
|
|
414
|
-
* nonCreateCount: number,
|
|
415
|
-
* sensitiveClasses: string[],
|
|
416
|
-
* }|null,
|
|
657
|
+
* shape: ReturnType<typeof buildEffortShape>|null,
|
|
417
658
|
* ceilings: typeof STORY_SHAPE_CEILINGS,
|
|
418
659
|
* preserves: typeof LITE_PATH_INVARIANTS,
|
|
419
660
|
* }}
|
|
@@ -421,6 +662,9 @@ export function resolvePlannerRouteVerdict({ reason } = {}) {
|
|
|
421
662
|
export function deriveStoryShape({
|
|
422
663
|
changes,
|
|
423
664
|
acceptance,
|
|
665
|
+
kinds,
|
|
666
|
+
magnitude,
|
|
667
|
+
uncertainty,
|
|
424
668
|
injectedRules,
|
|
425
669
|
selectSensitivePathClassesFn,
|
|
426
670
|
} = {}) {
|
|
@@ -437,7 +681,7 @@ export function deriveStoryShape({
|
|
|
437
681
|
if (!Array.isArray(changes) || changes.length === 0) {
|
|
438
682
|
return decide(
|
|
439
683
|
'full',
|
|
440
|
-
'no changes[] declared — the footprint is unknown, so the
|
|
684
|
+
'no changes[] declared — the footprint is unknown, so the work cannot be judged trivial; conservative full route',
|
|
441
685
|
);
|
|
442
686
|
}
|
|
443
687
|
|
|
@@ -451,23 +695,21 @@ export function deriveStoryShape({
|
|
|
451
695
|
);
|
|
452
696
|
}
|
|
453
697
|
|
|
454
|
-
const
|
|
455
|
-
const nonCreateCount = changes.filter(
|
|
456
|
-
(entry) =>
|
|
457
|
-
!(entry && typeof entry === 'object' && entry.assumption === 'creates'),
|
|
458
|
-
).length;
|
|
698
|
+
const paths = entries.map((e) => e.path);
|
|
459
699
|
const { level, classes } = deriveChangeLevel({
|
|
460
|
-
changedFiles:
|
|
700
|
+
changedFiles: paths,
|
|
461
701
|
injectedRules,
|
|
462
702
|
selectSensitivePathClassesFn,
|
|
463
703
|
});
|
|
464
|
-
const shape = {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
704
|
+
const shape = buildEffortShape({
|
|
705
|
+
changes,
|
|
706
|
+
paths,
|
|
707
|
+
acceptance,
|
|
708
|
+
kinds,
|
|
709
|
+
magnitude,
|
|
710
|
+
uncertainty,
|
|
469
711
|
sensitiveClasses: classes,
|
|
470
|
-
};
|
|
712
|
+
});
|
|
471
713
|
|
|
472
714
|
if (entries.some((e) => e.isGlob)) {
|
|
473
715
|
return decide(
|
|
@@ -476,13 +718,6 @@ export function deriveStoryShape({
|
|
|
476
718
|
shape,
|
|
477
719
|
);
|
|
478
720
|
}
|
|
479
|
-
if (shape.changeCount > ceilings.maxChanges) {
|
|
480
|
-
return decide(
|
|
481
|
-
'full',
|
|
482
|
-
`changes[] declares ${shape.changeCount} entries (> maxChanges ${ceilings.maxChanges}) — not a trivial footprint; full route`,
|
|
483
|
-
shape,
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
721
|
if (shape.acceptanceCount === 0) {
|
|
487
722
|
return decide(
|
|
488
723
|
'full',
|
|
@@ -490,27 +725,10 @@ export function deriveStoryShape({
|
|
|
490
725
|
shape,
|
|
491
726
|
);
|
|
492
727
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
shape,
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
if (shape.nonCreateCount > ceilings.maxNonCreateChanges) {
|
|
501
|
-
return decide(
|
|
502
|
-
'full',
|
|
503
|
-
`${shape.nonCreateCount} non-create change(s) (> maxNonCreateChanges ${ceilings.maxNonCreateChanges}) — a mostly-refactoring mix is not a trivial additive scope; full route`,
|
|
504
|
-
shape,
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
if (shape.sensitiveClasses.length > 0) {
|
|
508
|
-
return decide(
|
|
509
|
-
'full',
|
|
510
|
-
`footprint intersects sensitive-path class(es) ${shape.sensitiveClasses.join(', ')} — sensitivity wins over a small shape; full route (fresh acceptance critic retained)`,
|
|
511
|
-
shape,
|
|
512
|
-
);
|
|
513
|
-
}
|
|
728
|
+
|
|
729
|
+
const violation = firstEffortViolation(shape, ceilings);
|
|
730
|
+
if (violation !== null) return decide('full', violation, shape);
|
|
731
|
+
|
|
514
732
|
if (level !== 'low') {
|
|
515
733
|
// `deriveChangeLevel` degraded to its null fail-safe (unreadable
|
|
516
734
|
// manifest / failed selector): there is no evidence the footprint is
|
|
@@ -524,7 +742,7 @@ export function deriveStoryShape({
|
|
|
524
742
|
|
|
525
743
|
return decide(
|
|
526
744
|
'lite',
|
|
527
|
-
`trivial shape: ${shape.
|
|
745
|
+
`trivial shape: ${shape.kindCount} change kind(s) (${shape.changeKinds.join(', ')}) ≤ ${ceilings.maxChangeKinds} across ${shape.siteCount} site(s), magnitude ${shape.magnitude} ≤ ${ceilings.maxMagnitude}, shape ${shape.uncertainty}, no epic-scope span, no sensitive-path class — inline-eligible; non-negotiables preserved`,
|
|
528
746
|
shape,
|
|
529
747
|
);
|
|
530
748
|
}
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
* {@link module:lib/orchestration/complexity-gate.STORY_SHAPE_CEILINGS})
|
|
23
23
|
* **and** a ledgered model verdict carrying a recorded reason
|
|
24
24
|
* ({@link resolveLedgeredVerdict}). Both must agree on `lite`; either
|
|
25
|
-
* falling short fails closed to `full`.
|
|
25
|
+
* falling short fails closed to `full`. The shape axes are effort and
|
|
26
|
+
* risk — distinct change kinds, a coarse magnitude bucket, uncertainty,
|
|
27
|
+
* and epic-scope span — never artifact counts (Story #4764), so this gate
|
|
28
|
+
* is deliberately **coarse**: it rejects clearly-epic work, and invariant
|
|
29
|
+
* 3 below does the real enforcement against ground truth.
|
|
26
30
|
* 2. **Over-scope stops, never silently proceeds ({@link
|
|
27
31
|
* resolveLightGateOutcome}).** An over-ceiling prompt does **not**
|
|
28
32
|
* hard-fail — it STOPS and asks the operator to escalate to `/plan` or
|
|
@@ -49,12 +53,13 @@ import { deriveChangeLevel } from './review-depth.js';
|
|
|
49
53
|
|
|
50
54
|
/**
|
|
51
55
|
* File-count ceiling for the **actual landed** change set the diff backstop
|
|
52
|
-
* ({@link checkLightDiffBackstop}) enforces.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* ({@link checkLightDiffBackstop}) enforces. This is the light path's **only**
|
|
57
|
+
* cardinality ceiling, and deliberately so (Story #4764): the predicted
|
|
58
|
+
* footprint is a declaration — a guess, and a gameable one — so the gate that
|
|
59
|
+
* counts must be the one reading ground truth. A genuinely-light change stays
|
|
60
|
+
* small; conservative by construction, since a ceiling an operator could widen
|
|
61
|
+
* past what a single session safely absorbs is a ceiling that fails silently.
|
|
62
|
+
* A framework constant, not a knob.
|
|
58
63
|
*/
|
|
59
64
|
export const LIGHT_DIFF_CEILINGS = Object.freeze({
|
|
60
65
|
maxFiles: 4,
|
|
@@ -122,16 +127,27 @@ export function resolveLedgeredVerdict({ route, reason } = {}) {
|
|
|
122
127
|
|
|
123
128
|
/**
|
|
124
129
|
* Judge whether an operator prompt's predicted footprint is suitable for the
|
|
125
|
-
* light path. The deterministic
|
|
126
|
-
* must **both** agree on `lite`; anything else —
|
|
130
|
+
* light path. The deterministic effort/risk derivation and the ledgered model
|
|
131
|
+
* verdict must **both** agree on `lite`; anything else — clearly-epic work, a
|
|
127
132
|
* sensitive-path footprint, an unledgered verdict — resolves to `full` (the
|
|
128
133
|
* conservative default that routes the operator to `/plan`).
|
|
129
134
|
*
|
|
135
|
+
* The predicted axes are declared by the caller: `predictedKinds` (the distinct
|
|
136
|
+
* kinds of change; absent, each entry's `assumption` is its kind, so N
|
|
137
|
+
* instances of one mechanical edit count once), `predictedMagnitude`
|
|
138
|
+
* (`trivial` | `moderate` | `substantial`), and `predictedUncertainty`
|
|
139
|
+
* (`determined` | `needs-design`). A malformed bucket fails closed; an absent
|
|
140
|
+
* one carries no signal, because a marginal footprint must not be rejected on
|
|
141
|
+
* counts the diff backstop is the right place to enforce.
|
|
142
|
+
*
|
|
130
143
|
* Pure and total: never throws, never mutates its inputs.
|
|
131
144
|
*
|
|
132
145
|
* @param {{
|
|
133
146
|
* predictedChanges?: unknown,
|
|
134
147
|
* predictedAcceptance?: unknown,
|
|
148
|
+
* predictedKinds?: unknown,
|
|
149
|
+
* predictedMagnitude?: unknown,
|
|
150
|
+
* predictedUncertainty?: unknown,
|
|
135
151
|
* verdict?: { route?: unknown, reason?: unknown },
|
|
136
152
|
* injectedRules?: object,
|
|
137
153
|
* selectSensitivePathClassesFn?: Function,
|
|
@@ -148,6 +164,9 @@ export function resolveLedgeredVerdict({ route, reason } = {}) {
|
|
|
148
164
|
export function deriveLightSuitability({
|
|
149
165
|
predictedChanges,
|
|
150
166
|
predictedAcceptance,
|
|
167
|
+
predictedKinds,
|
|
168
|
+
predictedMagnitude,
|
|
169
|
+
predictedUncertainty,
|
|
151
170
|
verdict,
|
|
152
171
|
injectedRules,
|
|
153
172
|
selectSensitivePathClassesFn,
|
|
@@ -156,6 +175,9 @@ export function deriveLightSuitability({
|
|
|
156
175
|
const shape = deriveStoryShape({
|
|
157
176
|
changes: predictedChanges,
|
|
158
177
|
acceptance: predictedAcceptance,
|
|
178
|
+
kinds: predictedKinds,
|
|
179
|
+
magnitude: predictedMagnitude,
|
|
180
|
+
uncertainty: predictedUncertainty,
|
|
159
181
|
injectedRules,
|
|
160
182
|
selectSensitivePathClassesFn,
|
|
161
183
|
});
|