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.
- package/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +266 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +41 -57
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -1,141 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Story
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
}
|