mandrel 2.56.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  16. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  17. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  18. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  19. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  20. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  21. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  22. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  23. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  24. package/.agents/scripts/lib/config/explain.js +0 -19
  25. package/.agents/scripts/lib/config/limits.js +18 -78
  26. package/.agents/scripts/lib/config/quality.js +6 -3
  27. package/.agents/scripts/lib/config/runners.js +3 -2
  28. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  29. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  30. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  31. package/.agents/scripts/lib/crap-engine.js +35 -4
  32. package/.agents/scripts/lib/crap-utils.js +17 -1
  33. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  34. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  35. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  36. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  37. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  39. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  40. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  41. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  42. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  43. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  44. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  45. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  46. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  48. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
  49. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  51. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  52. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  53. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  56. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  57. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  58. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  59. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  60. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  61. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  62. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  63. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  64. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  65. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  66. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  67. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  68. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  69. package/.agents/scripts/lib/test-run-credit.js +266 -0
  70. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  71. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  72. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  73. package/.agents/scripts/plan-context.js +7 -9
  74. package/.agents/scripts/plan-critics.js +28 -54
  75. package/.agents/scripts/plan-persist.js +25 -68
  76. package/.agents/scripts/quality-preview.js +51 -0
  77. package/.agents/scripts/run-tests.js +12 -0
  78. package/.agents/scripts/stories-wave-tick.js +23 -45
  79. package/.agents/scripts/test-isolate.js +13 -180
  80. package/.agents/scripts/update-coverage-baseline.js +25 -70
  81. package/.agents/scripts/update-crap-baseline.js +19 -123
  82. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  83. package/.agents/workflows/audit-clean-code.md +4 -3
  84. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  85. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  86. package/.agents/workflows/helpers/code-review.md +2 -3
  87. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  88. package/.agents/workflows/helpers/deliver-light.md +40 -105
  89. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  90. package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
  91. package/.agents/workflows/helpers/deliver-story.md +9 -13
  92. package/.agents/workflows/helpers/plan-reference.md +132 -219
  93. package/.agents/workflows/mandrel-plan.md +27 -40
  94. package/.agents/workflows/memory-consolidate.md +9 -13
  95. package/docs/CHANGELOG.md +23 -0
  96. package/lib/migrations/index.js +4 -0
  97. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  98. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  99. package/package.json +1 -1
  100. package/.agents/scripts/lib/framework-version.js +0 -39
  101. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  102. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  103. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  104. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  105. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  106. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -1,141 +1,24 @@
1
1
  /**
2
- * Story sizing model — v2 model-capacity split advisory.
2
+ * Story authoring guidance — the two prose constants the story-author prompt
3
+ * and the `core/scope-triage` skill both cite, stated once so the surfaces
4
+ * cannot drift.
3
5
  *
4
- * Per-Story file/AC ceilings (`DEFAULT_TASK_SIZING.softFiles` /
5
- * `hardFiles` / `softAcceptanceCount`) are gone. A Story is sized by
6
- * **cohesion first**; the numeric backstop only asks whether the authored
7
- * Story ticket itself is pathologically verbose. See `docs/roadmap.md`
8
- * § v2.0.0 design decision 2.
9
- *
10
- * Co-located with `ticket-validator.js`, but kept as its own module so the
11
- * validator's primary file stays under the maintainability ceiling. The
12
- * validator imports `computeSizingFindings` and `renderHardFindingError` and
13
- * stitches the result onto its return value as `findings` / `errors`.
14
- *
15
- * `DEFAULT_MODEL_CAPACITY` is the **single source of truth** for the capacity
16
- * thresholds. The decomposer prompt template
17
- * (`.agents/scripts/lib/templates/decomposer-prompts.js`) generates its
18
- * threshold sentence from this constant rather than restating divergent
19
- * numbers, so the two surfaces cannot drift.
20
- *
21
- * Capacity model:
22
- * - Plan-time **session mass** = authored tokens only (`estimateTokens` of
23
- * the Story's goal/reason/acceptance/verify/slicing/spec/change paths).
24
- * - Soft / hard ceilings are **absolute** authored-token counts (no
25
- * `maxTokenBudget` denominator — that envelope was retired).
26
- * - The optional `wide` declaration lifts the hard session-mass rejection.
27
- * - Cohesion remains the primary heuristic; capacity is a pathological-
28
- * verbosity backstop only (not an operator-tunable agentrc knob).
29
- */
30
-
31
- import { parse as parseStoryBody } from '../story-body/story-body.js';
32
- import { estimateTokens } from './spec-spill.js';
33
-
34
- /**
35
- * Normalize a Story's `body` to the structured object the sizing layers
36
- * score, mirroring `validateAcFreshness` / `collectStoryAssumptionEntries`
37
- * (Story #3302) and `resolveStructuredBody` in `task-body-validator.js`.
38
- *
39
- * @param {object} story
40
- * @returns {object|null}
41
- */
42
- function resolveStoryBody(story) {
43
- const body = story?.body;
44
- if (typeof body === 'string') {
45
- if (body.trim().length === 0) return null;
46
- try {
47
- return parseStoryBody(body).body;
48
- } catch {
49
- return null;
50
- }
51
- }
52
- if (body !== null && typeof body === 'object') return body;
53
- return null;
54
- }
55
-
56
- /**
57
- * Resolve the acceptance-criteria array for a Story, preferring the
58
- * authoritative top-level `story.acceptance` over the structured body's
59
- * `acceptance`.
60
- *
61
- * @param {object} story
62
- * @returns {unknown[]}
63
- */
64
- function resolveAcceptance(story) {
65
- if (Array.isArray(story?.acceptance)) return story.acceptance;
66
- const body = resolveStoryBody(story);
67
- return Array.isArray(body?.acceptance) ? body.acceptance : [];
68
- }
69
-
70
- /**
71
- * Resolve the verify-commands array for a Story, preferring the top-level
72
- * `story.verify` over the structured body's `verify`.
73
- *
74
- * @param {object} story
75
- * @returns {unknown[]}
76
- */
77
- function resolveVerify(story) {
78
- if (Array.isArray(story?.verify)) return story.verify;
79
- const body = resolveStoryBody(story);
80
- return Array.isArray(body?.verify) ? body.verify : [];
81
- }
82
-
83
- /**
84
- * Resolve the `depends_on` edge list for a Story.
85
- *
86
- * @param {object} story
87
- * @returns {string[]}
6
+ * Story #5312 deleted the numeric sizing model that used to live beside
7
+ * them: `DEFAULT_MODEL_CAPACITY` with its soft / hard session-mass ceilings,
8
+ * the `wide` declaration that lifted the hard one, the `merge-candidate`,
9
+ * `unanchored-constant` and `missing-reason-to-exist` findings, and the
10
+ * `estimateStorySessionMass` estimator they read. None of those ceilings
11
+ * fired on real work — the hard ceiling never rejected an accepted Story, and
12
+ * the soft ones duplicated a judgment the authoring model already makes — so
13
+ * a Story is now as large and as loosely prescribed as the work needs. What
14
+ * survives here is the prose that says *how* to think about a slice, not a
15
+ * number that scores it.
88
16
  */
89
- function resolveDependsOn(story) {
90
- const raw = Array.isArray(story?.depends_on)
91
- ? story.depends_on
92
- : (resolveStoryBody(story)?.depends_on ?? []);
93
- return Array.isArray(raw)
94
- ? raw.filter((d) => typeof d === 'string' && d.trim().length > 0)
95
- : [];
96
- }
97
-
98
- /**
99
- * Framework capacity constant — not operator-tunable via `.agentrc.json`
100
- * (same collapse pattern as `maxTickets` / Story #4163).
101
- *
102
- * Absolute authored-token ceilings (no delivery-envelope denominator):
103
- *
104
- * - Soft 30_000: cohesion check when the ticket itself is already a
105
- * substantial document. Normal capability Stories stay silent.
106
- * - Hard 75_000: reject Spec novels unless `wide`.
107
- * - Merge candidate 1_500: thin `depends_on` fragment (soft only).
108
- *
109
- * Cohesion / split policy / conflict advisories remain the primary sizing
110
- * signal; these ceilings catch pathological ticket verbosity only.
111
- */
112
- export const DEFAULT_MODEL_CAPACITY = Object.freeze({
113
- softSessionTokens: 30000,
114
- hardSessionTokens: 75000,
115
- mergeCandidateMaxSessionTokens: 1500,
116
- });
117
-
118
- /**
119
- * Resolve capacity ceilings, shallow-merging an optional override bag
120
- * (tests / programmatic only). Pure; used by the validator and the
121
- * decomposer prompt.
122
- *
123
- * @param {object} [capacity]
124
- * @returns {{ softSessionTokens: number, hardSessionTokens: number, mergeCandidateMaxSessionTokens: number }}
125
- */
126
- export function resolveCapacityCeilings(capacity = DEFAULT_MODEL_CAPACITY) {
127
- const merged = { ...DEFAULT_MODEL_CAPACITY, ...(capacity ?? {}) };
128
- return {
129
- softSessionTokens: merged.softSessionTokens,
130
- hardSessionTokens: merged.hardSessionTokens,
131
- mergeCandidateMaxSessionTokens: merged.mergeCandidateMaxSessionTokens,
132
- };
133
- }
134
17
 
135
18
  /**
136
19
  * `DELIVERABLE_GRANULARITY_GUIDANCE` is the **single source of truth** for the
137
20
  * deliverable-granularity definition of a Story (Story #3777). It is stated
138
- * ONCE here and consumed by BOTH the decomposer prompt template and the
21
+ * ONCE here and consumed by BOTH the story-author prompt template and the
139
22
  * authoring SKILL.
140
23
  */
141
24
  export const DELIVERABLE_GRANULARITY_GUIDANCE = Object.freeze({
@@ -151,293 +34,17 @@ export const DELIVERABLE_GRANULARITY_GUIDANCE = Object.freeze({
151
34
  * `AUTHORING_ALTITUDE_GUIDANCE` is the **single source of truth** for the
152
35
  * binding-vs-advisory authoring altitude (Epic #4131 F8) and the New-File
153
36
  * Contract (Story #4272).
37
+ *
38
+ * Story #5312 demoted the footprint probes behind the advisory caveat to
39
+ * dry-run warnings: a `creates` / `refactors-existing` mismatch, or a goal or
40
+ * acceptance path absent at base, is now reported and the persist proceeds.
41
+ * Only a `deletes` naming a path absent at base is still refused.
154
42
  */
155
43
  export const AUTHORING_ALTITUDE_GUIDANCE = Object.freeze({
156
44
  altitude:
157
45
  '**Binding contract vs advisory sketch.** `acceptance[]` and `verify[]` are the Story\'s **binding contract** — the executor MUST satisfy them exactly, and they are the only definition of "done." `changes[]` and `references[]` are an **advisory implementation sketch**: your best prediction of the file footprint, which the executor MAY revise when the real codebase diverges from the sketch. Author `acceptance[]` / `verify[]` to assert the **outcome** independent of any one file layout — never pin an incidental implementation detail (an internal helper name, a private file path) into an acceptance item that the advisory `changes[]` is free to reshape; assert the observable behaviour instead.',
158
46
  advisoryCaveat:
159
- "**Advisory does not mean unvalidated.** `changes[]` paths still pass the base-branch file-assumption probes (a `creates` against an existing path still fails), the New-File Contract still holds, and the executor's latitude to revise the approach never licenses skipping `acceptance[]` / `verify[]` or relaxing any `rules/security-baseline.md` MUST.",
47
+ "**Advisory does not mean unchecked.** `changes[]` paths are still probed against the base branch: a `creates` on an existing path or a `refactors-existing` on an absent one is reported as a dry-run warning, and a `deletes` naming a path absent at base is refused. The executor's latitude to revise the approach never licenses skipping `acceptance[]` / `verify[]` or relaxing any `rules/security-baseline.md` MUST.",
160
48
  newFileContract:
161
- '**New-File Contract.** Any path named in a Story\'s `goal`, `acceptance`, or `verify` that does NOT already exist on `main` MUST also appear in that Story\'s `changes[]` with `assumption: "creates"`; otherwise the freshness validator rejects the decompose — even when the Story is the one authoring the file.',
49
+ '**New-File Contract.** Any path named in a Story\'s `goal`, `acceptance`, or `verify` that does NOT already exist on `main` should also appear in that Story\'s `changes[]` with `assumption: "creates"`; the dry-run warns on any such path it cannot find at base — even when the Story is the one authoring the file.',
162
50
  });
163
-
164
- const UNANCHORED_CONSTANT_PATTERNS = Object.freeze([
165
- /\bretention window\b/i,
166
- /\brate[ -]limit\b/i,
167
- /\btimeout\b/i,
168
- /\bolder than\b/i,
169
- /\bwithin\b/i,
170
- /\bmax\b[^.]*\bper\b/i,
171
- /\bquota\b/i,
172
- ]);
173
-
174
- const CONCRETE_VALUE_RE = /\d/;
175
-
176
- function makeUnanchoredConstant(slug, criterion) {
177
- return {
178
- kind: 'unanchored-constant',
179
- severity: 'soft',
180
- ticketSlug: slug,
181
- criterion,
182
- message: `Acceptance criterion references a configuration constant without a concrete value: "${criterion}". Specify the value inline (e.g. "90 days", "5 req/s", "30 minutes") so the implementing agent doesn't have to read the Tech Spec or guess.`,
183
- };
184
- }
185
-
186
- function computeUnanchoredConstantFindings(story) {
187
- const out = [];
188
- const acceptance = resolveAcceptance(story);
189
- for (const item of acceptance) {
190
- const criterion = String(item ?? '');
191
- if (CONCRETE_VALUE_RE.test(criterion)) continue;
192
- const matches = UNANCHORED_CONSTANT_PATTERNS.some((re) =>
193
- re.test(criterion),
194
- );
195
- if (matches) {
196
- out.push(makeUnanchoredConstant(story.slug, criterion));
197
- }
198
- }
199
- return out;
200
- }
201
-
202
- function makeMissingReasonToExist(slug) {
203
- return {
204
- kind: 'missing-reason-to-exist',
205
- severity: 'soft',
206
- ticketSlug: slug,
207
- message:
208
- 'Story body carries no non-empty `reason_to_exist`. State the single coherent reason this Story exists in one sentence (the machine-checkable form of "one Story = one coherent change with one reason to exist"), encoded as the `reason_to_exist` field of the body meta comment.',
209
- };
210
- }
211
-
212
- function computeMissingReasonToExistFinding(story) {
213
- const body = resolveStoryBody(story);
214
- const reason = body?.reason_to_exist;
215
- const hasReason = typeof reason === 'string' && reason.trim().length > 0;
216
- return hasReason ? [] : [makeMissingReasonToExist(story.slug)];
217
- }
218
-
219
- function makeMergeCandidate(slug, sessionMass, dependsOn) {
220
- const siblings = dependsOn.map((d) => `"${d}"`).join(', ');
221
- return {
222
- kind: 'merge-candidate',
223
- severity: 'soft',
224
- ticketSlug: slug,
225
- sessionMass,
226
- dependsOn,
227
- message: `Story "${slug}" is a thin dependent slice (estimated session mass ${sessionMass} tokens) that depends on sibling(s) ${siblings}. A Story whose only role is to feed one sibling is not its own unit of work — consider merging it into the consumer (single-consumer merge rule) rather than shipping it as a separate slice.`,
228
- };
229
- }
230
-
231
- /**
232
- * Returns true when a `changes[]` entry is a glob pattern.
233
- */
234
- function isGlobBullet(bullet) {
235
- const s = bullet?.path;
236
- return typeof s === 'string' && s.includes('*');
237
- }
238
-
239
- /**
240
- * Extract the path from a single object-form `changes` entry.
241
- */
242
- function extractChangeBulletPath(bullet) {
243
- const s = bullet?.path ?? null;
244
- if (!s) return null;
245
- const colonIdx = s.indexOf(':');
246
- if (colonIdx <= 0) return /[\\/.]/.test(s) ? s.trim() : null;
247
- const head = s.slice(0, colonIdx).trim();
248
- return /[\\/.]/.test(head) ? head : null;
249
- }
250
-
251
- /**
252
- * Analyse the `changes[]` array and return fileCount / hasGlobs / path texts.
253
- */
254
- function analyseChanges(changes) {
255
- const paths = new Set();
256
- const pathTexts = [];
257
- let hasGlobs = false;
258
- for (const bullet of changes) {
259
- if (isGlobBullet(bullet)) {
260
- hasGlobs = true;
261
- const globText = bullet?.path ?? '';
262
- if (globText) pathTexts.push(String(globText));
263
- continue;
264
- }
265
- const path = extractChangeBulletPath(bullet);
266
- if (path) {
267
- paths.add(path);
268
- pathTexts.push(path);
269
- }
270
- }
271
- return { fileCount: paths.size, hasGlobs, pathTexts };
272
- }
273
-
274
- /**
275
- * Estimate the plan-time session mass (tokens) for a Story.
276
- *
277
- * Session mass is **authored prose only** — `estimateTokens` of the Story's
278
- * goal / reason / acceptance / verify / slicing / spec / change-path text.
279
- * File count and AC count do not add delivery-cost proxies (those recreated
280
- * the retired count ceilings). Glob entries contribute their text but do not
281
- * imply known width.
282
- *
283
- * @param {object} story
284
- * @param {object} [_capacity] Unused; retained so call sites that still pass
285
- * a capacity bag keep working. Mass no longer depends on capacity knobs.
286
- * @returns {{ sessionMass: number, authoredTokens: number, acceptanceCount: number, fileCount: number, hasGlobs: boolean, changesAnalysis: object }}
287
- */
288
- export function estimateStorySessionMass(
289
- story,
290
- _capacity = DEFAULT_MODEL_CAPACITY,
291
- ) {
292
- const body = resolveStoryBody(story);
293
- const acceptance = resolveAcceptance(story);
294
- const verify = resolveVerify(story);
295
- const changes = Array.isArray(body?.changes) ? body.changes : [];
296
- const changesAnalysis = analyseChanges(changes);
297
-
298
- const authoredParts = [
299
- body?.goal,
300
- body?.reason_to_exist,
301
- typeof body?.spec === 'string' ? body.spec : '',
302
- ...acceptance.map((a) => String(a ?? '')),
303
- ...verify.map((v) => String(v ?? '')),
304
- typeof body?.slicing === 'string' ? body.slicing : '',
305
- ...changesAnalysis.pathTexts,
306
- ].filter((p) => typeof p === 'string' && p.length > 0);
307
-
308
- const authoredTokens = estimateTokens(authoredParts.join('\n'));
309
-
310
- return {
311
- sessionMass: authoredTokens,
312
- authoredTokens,
313
- acceptanceCount: acceptance.length,
314
- fileCount: changesAnalysis.fileCount,
315
- hasGlobs: changesAnalysis.hasGlobs,
316
- changesAnalysis,
317
- };
318
- }
319
-
320
- function makeOversized(slug, observed, ceiling) {
321
- return {
322
- kind: 'oversized-task',
323
- severity: 'hard',
324
- ticketSlug: slug,
325
- field: 'sessionMass',
326
- observed,
327
- ceiling,
328
- };
329
- }
330
-
331
- function makeSoftSessionPressure(slug, observed, soft) {
332
- return {
333
- kind: 'soft-session-pressure',
334
- severity: 'soft',
335
- ticketSlug: slug,
336
- field: 'sessionMass',
337
- observed,
338
- soft,
339
- };
340
- }
341
-
342
- /**
343
- * Returns true when the Story declares itself `wide` with a non-empty reason.
344
- * A `wide` declaration lifts the hard session-mass rejection.
345
- */
346
- function isDeclaredWide(wide) {
347
- return (
348
- wide !== null &&
349
- typeof wide === 'object' &&
350
- typeof wide.reason === 'string' &&
351
- wide.reason.trim().length > 0
352
- );
353
- }
354
-
355
- function computeMergeCandidateFinding(story, mass, ceilings) {
356
- if (mass.hasGlobs) return [];
357
- const dependsOn = resolveDependsOn(story);
358
- if (dependsOn.length === 0) return [];
359
- if (mass.sessionMass > ceilings.mergeCandidateMaxSessionTokens) return [];
360
- return [makeMergeCandidate(story.slug, mass.sessionMass, dependsOn)];
361
- }
362
-
363
- /**
364
- * Compute the hard + soft capacity findings for a single Story.
365
- */
366
- function computeStorySizingFindings(story, capacity, ceilings) {
367
- const out = [];
368
- const body = resolveStoryBody(story);
369
- const declaredWide = isDeclaredWide(body?.wide ?? null);
370
- const mass = estimateStorySessionMass(story, capacity);
371
-
372
- out.push(...computeUnanchoredConstantFindings(story));
373
- out.push(...computeMissingReasonToExistFinding(story));
374
- out.push(...computeMergeCandidateFinding(story, mass, ceilings));
375
-
376
- // Glob entries mark the change footprint as unknown-width. A non-wide Story
377
- // carrying globs still gets an informational nudge to declare `wide` when
378
- // the authored mass alone does not already trip a capacity finding.
379
- if (mass.hasGlobs && !declaredWide) {
380
- out.push({
381
- kind: 'wide-undeclared',
382
- severity: 'soft',
383
- ticketSlug: story.slug,
384
- reason: 'glob-changes',
385
- });
386
- }
387
-
388
- const { softSessionTokens, hardSessionTokens } = ceilings;
389
-
390
- if (mass.sessionMass > hardSessionTokens && !declaredWide) {
391
- out.push(makeOversized(story.slug, mass.sessionMass, hardSessionTokens));
392
- } else if (mass.sessionMass > softSessionTokens) {
393
- if (declaredWide) {
394
- out.push(
395
- makeSoftSessionPressure(
396
- story.slug,
397
- mass.sessionMass,
398
- softSessionTokens,
399
- ),
400
- );
401
- } else {
402
- out.push({
403
- kind: 'wide-undeclared',
404
- severity: 'soft',
405
- ticketSlug: story.slug,
406
- sessionMass: mass.sessionMass,
407
- softSessionTokens,
408
- });
409
- }
410
- }
411
-
412
- return out;
413
- }
414
-
415
- /**
416
- * Compute the full structured findings array for a normalized ticket
417
- * hierarchy.
418
- *
419
- * @param {{ stories: object[], capacity?: object }} input
420
- * @returns {object[]}
421
- */
422
- export function computeSizingFindings({ stories, capacity }) {
423
- const merged = {
424
- ...DEFAULT_MODEL_CAPACITY,
425
- ...(capacity ?? {}),
426
- };
427
- const ceilings = resolveCapacityCeilings(merged);
428
- const findings = [];
429
- for (const story of stories ?? []) {
430
- findings.push(...computeStorySizingFindings(story, merged, ceilings));
431
- }
432
- return findings;
433
- }
434
-
435
- /**
436
- * Render a structured hard finding as a human-readable error message.
437
- */
438
- export function renderHardFindingError(finding) {
439
- if (finding.kind === 'oversized-task') {
440
- return `Story "${finding.ticketSlug}" exceeds the session-capacity ceiling: observed ${finding.observed} estimated tokens, max ${finding.ceiling}. Size the Story to what one guarded session can deliver and self-verify, or declare \`wide\` with a one-line reason.`;
441
- }
442
- return `Story "${finding.ticketSlug}" tripped hard finding ${finding.kind}.`;
443
- }