mandrel 2.26.0 → 2.28.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/docs/execution-reference.md +22 -12
- package/.agents/scripts/lib/baselines/reader.js +52 -38
- package/.agents/scripts/lib/bdd-scenario-budget.js +68 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +49 -13
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +12 -5
- package/docs/CHANGELOG.md +14 -0
- package/package.json +1 -1
|
@@ -96,18 +96,28 @@ over-ceiling envelope or an over-budget Story count.
|
|
|
96
96
|
- **`PLAN_CONTEXT_ENVELOPE_BYTE_CEILING`** (`lib/orchestration/plan-context.js`):
|
|
97
97
|
256 KB (≈64K tokens at the ≈4-chars/token estimate) on the serialized
|
|
98
98
|
envelope `buildPlanContext` assembles, checked at the single choke point
|
|
99
|
-
every mode returns through. A measured seed-mode envelope on this repo
|
|
100
|
-
~120 KB — `docsContext` (~63 KB) and
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
every mode returns through. A measured seed-mode envelope on this repo's
|
|
100
|
+
thin `.feature` corpus is ~120 KB — `docsContext` (~63 KB) and
|
|
101
|
+
`systemPrompts` (~54 KB) are the bulk of the fixed floor, every other field
|
|
102
|
+
under 1 KB. That is **not** representative of every consumer: Story #4977
|
|
103
|
+
measured `bddScenarios` at 118 KB on a consumer with a mature Gherkin
|
|
104
|
+
corpus — larger than `docsContext` and `systemPrompts` combined, leaving
|
|
105
|
+
~5.5% headroom instead of ~2×. `bddScenarios` is now truncated to
|
|
106
|
+
`BDD_SCENARIOS_BYTE_BUDGET` (`lib/bdd-scenario-budget.js`, ≤24 KB,
|
|
107
|
+
reported via `truncated` / `totalScenarios` / `includedScenarios` rather
|
|
108
|
+
than silently dropped) before it reaches the envelope, deliberately a fixed
|
|
109
|
+
framework constant rather than an `.agentrc.json` knob — the same reasoning
|
|
110
|
+
Story #4811 applied when it retired the codebase snapshot. The
|
|
111
|
+
operator-supplied seed remains the one field with no cap and no elision
|
|
112
|
+
path.
|
|
113
|
+
- **On refusal**, the error names the envelope's largest fields and the
|
|
114
|
+
remedy that follows the single largest one (`OVERSIZE_FIELD_REMEDIES` in
|
|
115
|
+
`plan-context.js`) — trim the seed, plan fewer `--tickets` source issues in
|
|
116
|
+
one run, or trim `docsContextFiles`, depending on which field actually blew
|
|
117
|
+
the budget. The seed itself is carried **verbatim** by design — it is the
|
|
118
|
+
operator's request, and summarizing it silently would degrade planning
|
|
119
|
+
quality precisely when the input is richest — so it alone has no elision
|
|
120
|
+
path to fall back on. Raising the ceiling needs a measured justification.
|
|
111
121
|
|
|
112
122
|
### Session-mass capacity (plan-time sizing)
|
|
113
123
|
|
|
@@ -182,6 +182,56 @@ function validate(kind, parsed, sourceHint) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Internal: the ONE narrowing projection every loaded envelope passes through.
|
|
187
|
+
*
|
|
188
|
+
* `load` and `loadFile` reach a validated `parsed` envelope by different routes
|
|
189
|
+
* — one resolves the path from config, the other infers the kind from
|
|
190
|
+
* `$schema` — but the shape they hand back is the same narrow contract, so it
|
|
191
|
+
* is built here rather than written out at each exit.
|
|
192
|
+
*
|
|
193
|
+
* **That single-sourcing is the point, not tidiness.** The projection is an
|
|
194
|
+
* ALLOW-LIST: a field absent from it is silently dropped, and the envelope
|
|
195
|
+
* stamps below are read off the LOADED object by compat axes that fail closed
|
|
196
|
+
* when a stamp reads `undefined`. While the list was duplicated at the two
|
|
197
|
+
* exits, adding a stamp to one and not the other produced a gate that rejected
|
|
198
|
+
* every baseline in the repo for a stamp that was present on disk — which is
|
|
199
|
+
* exactly what happened to `provenanceStamped` between Story #4901 and this
|
|
200
|
+
* fix. One list means a new stamp cannot be half-added.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} kind
|
|
203
|
+
* @param {object} parsed A validated baseline envelope.
|
|
204
|
+
* @returns {{ rollup: object, rows: Array<object>, kernelVersion: string, generatedAt: string }}
|
|
205
|
+
*/
|
|
206
|
+
function shapeEnvelope(kind, parsed) {
|
|
207
|
+
const rows = Array.isArray(parsed.rows)
|
|
208
|
+
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
209
|
+
: [];
|
|
210
|
+
return {
|
|
211
|
+
rollup: parsed.rollup ?? { '*': {} },
|
|
212
|
+
rows,
|
|
213
|
+
kernelVersion: parsed.kernelVersion,
|
|
214
|
+
generatedAt: parsed.generatedAt,
|
|
215
|
+
// Story #4775 — carry the per-kind scoring-semantics stamp through the
|
|
216
|
+
// narrowing. The gate's compat check reads it off the LOADED envelope, so
|
|
217
|
+
// dropping it here would make every baseline look unstamped and fail the
|
|
218
|
+
// whole repo closed on a stamp that is actually present on disk.
|
|
219
|
+
scoringSemantics: parsed.scoringSemantics,
|
|
220
|
+
// Story #4866 — same contract for the transpiler stamp. Dropping it here
|
|
221
|
+
// would leave the ts-transpiler compat axis reachable but blind: it would
|
|
222
|
+
// read `undefined` off every loaded envelope and pass vacuously, which is
|
|
223
|
+
// the exact deadness this Story exists to end.
|
|
224
|
+
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
225
|
+
// Story #4901 — the coordinate-provenance marker, and the stamp that proved
|
|
226
|
+
// the warnings above were not hypothetical. Its `provenance-unstamped` axis
|
|
227
|
+
// keys on `!== true`, so while this line was missing the axis read
|
|
228
|
+
// `undefined` off every envelope and rejected each one with "baseline
|
|
229
|
+
// predates coordinate-provenance stamping" — un-satisfiable, because
|
|
230
|
+
// re-deriving the baseline writes the marker the reader then drops.
|
|
231
|
+
provenanceStamped: parsed.provenanceStamped,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
185
235
|
/**
|
|
186
236
|
* Internal: parse + validate + canonicalise. Used by both `load` and
|
|
187
237
|
* `loadFile`.
|
|
@@ -208,25 +258,7 @@ function readAndShape(kind, absolutePath) {
|
|
|
208
258
|
);
|
|
209
259
|
}
|
|
210
260
|
validate(kind, parsed, absolutePath);
|
|
211
|
-
|
|
212
|
-
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
213
|
-
: [];
|
|
214
|
-
return {
|
|
215
|
-
rollup: parsed.rollup ?? { '*': {} },
|
|
216
|
-
rows,
|
|
217
|
-
kernelVersion: parsed.kernelVersion,
|
|
218
|
-
generatedAt: parsed.generatedAt,
|
|
219
|
-
// Story #4775 — carry the per-kind scoring-semantics stamp through the
|
|
220
|
-
// narrowing. The gate's compat check reads it off the LOADED envelope, so
|
|
221
|
-
// dropping it here would make every baseline look unstamped and fail the
|
|
222
|
-
// whole repo closed on a stamp that is actually present on disk.
|
|
223
|
-
scoringSemantics: parsed.scoringSemantics,
|
|
224
|
-
// Story #4866 — same contract for the transpiler stamp. Dropping it here
|
|
225
|
-
// would leave the ts-transpiler compat axis reachable but blind: it would
|
|
226
|
-
// read `undefined` off every loaded envelope and pass vacuously, which is
|
|
227
|
-
// the exact deadness this Story exists to end.
|
|
228
|
-
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
229
|
-
};
|
|
261
|
+
return shapeEnvelope(kind, parsed);
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
/**
|
|
@@ -306,25 +338,7 @@ export function loadFile(absolutePath, opts = {}) {
|
|
|
306
338
|
);
|
|
307
339
|
}
|
|
308
340
|
validate(kind, parsed, absolutePath);
|
|
309
|
-
|
|
310
|
-
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
311
|
-
: [];
|
|
312
|
-
return {
|
|
313
|
-
rollup: parsed.rollup ?? { '*': {} },
|
|
314
|
-
rows,
|
|
315
|
-
kernelVersion: parsed.kernelVersion,
|
|
316
|
-
generatedAt: parsed.generatedAt,
|
|
317
|
-
// Story #4775 — carry the per-kind scoring-semantics stamp through the
|
|
318
|
-
// narrowing. The gate's compat check reads it off the LOADED envelope, so
|
|
319
|
-
// dropping it here would make every baseline look unstamped and fail the
|
|
320
|
-
// whole repo closed on a stamp that is actually present on disk.
|
|
321
|
-
scoringSemantics: parsed.scoringSemantics,
|
|
322
|
-
// Story #4866 — same contract for the transpiler stamp. Dropping it here
|
|
323
|
-
// would leave the ts-transpiler compat axis reachable but blind: it would
|
|
324
|
-
// read `undefined` off every loaded envelope and pass vacuously, which is
|
|
325
|
-
// the exact deadness this Story exists to end.
|
|
326
|
-
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
327
|
-
};
|
|
341
|
+
return shapeEnvelope(kind, parsed);
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
export const _internals = Object.freeze({
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bdd-scenario-budget.js — envelope byte budget for the `bddScenarios`
|
|
3
|
+
* `/plan` context-envelope field (Story #4977).
|
|
4
|
+
*
|
|
5
|
+
* `bdd-scenario-scanner.js`'s `scanBddScenarios` stays a faithful, uncapped
|
|
6
|
+
* index of the project's `.feature` corpus — that scan is also used
|
|
7
|
+
* directly by `lib/qa/resolve-selection.js`, which needs the full set. The
|
|
8
|
+
* cap belongs at the envelope boundary instead, in its own module: on a
|
|
9
|
+
* consumer with a mature Gherkin corpus, `bddScenarios` grew to 118 KB —
|
|
10
|
+
* larger than `docsContext` and `systemPrompts` combined — consuming
|
|
11
|
+
* nearly the entire `PLAN_CONTEXT_ENVELOPE_BYTE_CEILING` headroom on its
|
|
12
|
+
* own and blocking `/audit-to-stories`' single-plan path entirely.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately a fixed framework constant rather than an `.agentrc.json`
|
|
15
|
+
* knob: Story #4541 retired the one operator-tunable planner-context budget
|
|
16
|
+
* (`planning.context.maxBytes`) because a cap the operator can raise past
|
|
17
|
+
* what the model can read fails silently again, and the same reasoning
|
|
18
|
+
* applies here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Byte budget for the capped `bddScenarios` envelope field. Sized well
|
|
23
|
+
* under `PLAN_CONTEXT_ENVELOPE_BYTE_CEILING` (256 KB) — this is one of
|
|
24
|
+
* several envelope fields, not the whole budget. At the measured ~337
|
|
25
|
+
* bytes/scenario average (Story #4977 evidence, a mature Gherkin corpus),
|
|
26
|
+
* 24 KB holds roughly 70 scenarios before truncating.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately module-private: the only production consumer is
|
|
29
|
+
* {@link capBddScenarios} below via the `opts.byteBudget` default. Tests
|
|
30
|
+
* assert the resulting behavior (truncation, order, fit) rather than
|
|
31
|
+
* importing this value directly, so it carries no public export the
|
|
32
|
+
* `--production` dead-exports ratchet would otherwise flag as test-only.
|
|
33
|
+
*/
|
|
34
|
+
const BDD_SCENARIOS_BYTE_BUDGET = 24_000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Truncate a scenario index to a byte budget, deterministically (scan
|
|
38
|
+
* order — file walk order, then in-file order — never re-sorted), and
|
|
39
|
+
* report what was dropped rather than truncating silently.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<object>} scenarios Full scan output (order preserved).
|
|
42
|
+
* @param {{ byteBudget?: number }} [opts]
|
|
43
|
+
* @returns {{
|
|
44
|
+
* scenarios: Array<object>,
|
|
45
|
+
* totalScenarios: number,
|
|
46
|
+
* includedScenarios: number,
|
|
47
|
+
* truncated: boolean,
|
|
48
|
+
* }}
|
|
49
|
+
*/
|
|
50
|
+
export function capBddScenarios(scenarios, opts = {}) {
|
|
51
|
+
const byteBudget = opts.byteBudget ?? BDD_SCENARIOS_BYTE_BUDGET;
|
|
52
|
+
const list = Array.isArray(scenarios) ? scenarios : [];
|
|
53
|
+
let bytes = 0;
|
|
54
|
+
let cut = list.length;
|
|
55
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
56
|
+
bytes += Buffer.byteLength(JSON.stringify(list[i]), 'utf-8');
|
|
57
|
+
if (bytes > byteBudget) {
|
|
58
|
+
cut = i;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
scenarios: list.slice(0, cut),
|
|
64
|
+
totalScenarios: list.length,
|
|
65
|
+
includedScenarios: cut,
|
|
66
|
+
truncated: cut < list.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -46,15 +46,22 @@ import { buildDecomposerSystemPrompt } from './planning/decomposer-context.js';
|
|
|
46
46
|
* body and ship the raw seed on `seed.content` instead — the budget bounded
|
|
47
47
|
* a field that never left the function.
|
|
48
48
|
*
|
|
49
|
-
* A measured seed-mode envelope on this repo
|
|
50
|
-
* digest-first `docsContext` (~63 KB inline
|
|
51
|
-
* `systemPrompts` (~54 KB); every other field is
|
|
52
|
-
* retired the tier-capped codebase snapshot that
|
|
53
|
-
* (~35 KB skinny here).
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
49
|
+
* A measured seed-mode envelope on this repo (a thin `.feature` corpus) is
|
|
50
|
+
* ~120 KB, dominated by the digest-first `docsContext` (~63 KB inline
|
|
51
|
+
* digest) and the rendered `systemPrompts` (~54 KB); every other field is
|
|
52
|
+
* under 1 KB. Story #4811 retired the tier-capped codebase snapshot that
|
|
53
|
+
* used to sit alongside them (~35 KB skinny here). This measurement is
|
|
54
|
+
* **not** representative of every consumer, though: Story #4977 found
|
|
55
|
+
* `bddScenarios` at 118 KB on a consumer with a mature Gherkin corpus —
|
|
56
|
+
* larger than `docsContext` and `systemPrompts` combined, consuming nearly
|
|
57
|
+
* all of the ceiling's headroom on its own, because the scanner applied no
|
|
58
|
+
* cap. `bddScenarios` is now truncated to `BDD_SCENARIOS_BYTE_BUDGET`
|
|
59
|
+
* (`lib/bdd-scenario-budget.js`, ≤24 KB) before it reaches this envelope,
|
|
60
|
+
* so the seed remains the only field this ceiling leaves genuinely
|
|
61
|
+
* unbounded. 256 KB (~64K tokens at the ≈4-chars/token estimate) leaves
|
|
62
|
+
* roughly 2× headroom over the fixed-floor measurement above while staying
|
|
63
|
+
* well under the session budget. The test suite asserts serialized
|
|
64
|
+
* envelopes stay under this value — raise it only with a measured
|
|
58
65
|
* justification.
|
|
59
66
|
*/
|
|
60
67
|
export const PLAN_CONTEXT_ENVELOPE_BYTE_CEILING = 256_000;
|
|
@@ -62,6 +69,31 @@ export const PLAN_CONTEXT_ENVELOPE_BYTE_CEILING = 256_000;
|
|
|
62
69
|
/** Fields named in the over-ceiling error, to point at what to trim. */
|
|
63
70
|
const OVERSIZE_REPORT_FIELDS = 3;
|
|
64
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Per-field remedy for the over-ceiling refusal, keyed by envelope field
|
|
74
|
+
* name. Story #4977 — the refusal used to hardcode "trim the seed, or plan
|
|
75
|
+
* fewer --tickets" regardless of which field actually blew the budget; on a
|
|
76
|
+
* consumer with a mature Gherkin corpus the dominant field was
|
|
77
|
+
* `bddScenarios` (repo-derived, not seed-derived), and "trim the seed" was a
|
|
78
|
+
* dead lever the operator had no way to act on. The remedy now follows the
|
|
79
|
+
* single largest field.
|
|
80
|
+
*/
|
|
81
|
+
const OVERSIZE_FIELD_REMEDIES = Object.freeze({
|
|
82
|
+
seed: 'Trim the seed text — it is carried verbatim by design and is the one field with no elision path.',
|
|
83
|
+
sourceTickets:
|
|
84
|
+
'Plan fewer --tickets source issues in one run — each source ticket body is carried verbatim.',
|
|
85
|
+
epic: 'Plan fewer --tickets source issues in one run, or re-plan with a shorter Epic body.',
|
|
86
|
+
bddScenarios:
|
|
87
|
+
"The project's .feature corpus is already capped near BDD_SCENARIOS_BYTE_BUDGET (lib/bdd-scenario-budget.js) — if this still dominates, another field is unusually small; check the full field breakdown.",
|
|
88
|
+
docsContext:
|
|
89
|
+
'Trim project.docsContextFiles — docsContext is a digest built from those files.',
|
|
90
|
+
systemPrompts:
|
|
91
|
+
'This field is a fixed framework prompt, not operator content — if it dominates, file a framework-gap issue rather than trying to trim it.',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const DEFAULT_OVERSIZE_REMEDY =
|
|
95
|
+
'Trim the seed, or plan fewer --tickets source issues in one run.';
|
|
96
|
+
|
|
65
97
|
/**
|
|
66
98
|
* Fail closed when an assembled envelope exceeds
|
|
67
99
|
* {@link PLAN_CONTEXT_ENVELOPE_BYTE_CEILING}.
|
|
@@ -97,22 +129,26 @@ function assertPlanContextWithinCeiling(envelope, opts = {}) {
|
|
|
97
129
|
const bytes = Buffer.byteLength(JSON.stringify(envelope) ?? '', 'utf-8');
|
|
98
130
|
if (bytes <= ceiling) return envelope;
|
|
99
131
|
|
|
100
|
-
const
|
|
132
|
+
const sortedFields = Object.entries(envelope)
|
|
101
133
|
.map(([field, value]) => [
|
|
102
134
|
field,
|
|
103
135
|
Buffer.byteLength(JSON.stringify(value) ?? '', 'utf-8'),
|
|
104
136
|
])
|
|
105
|
-
.sort((a, b) => b[1] - a[1])
|
|
137
|
+
.sort((a, b) => b[1] - a[1]);
|
|
138
|
+
|
|
139
|
+
const largest = sortedFields
|
|
106
140
|
.slice(0, OVERSIZE_REPORT_FIELDS)
|
|
107
141
|
.map(([field, size]) => `${field} (${Math.round(size / 1024)} KB)`)
|
|
108
142
|
.join(', ');
|
|
109
143
|
|
|
144
|
+
const topField = sortedFields[0]?.[0];
|
|
145
|
+
const remedy = OVERSIZE_FIELD_REMEDIES[topField] ?? DEFAULT_OVERSIZE_REMEDY;
|
|
146
|
+
|
|
110
147
|
throw new Error(
|
|
111
148
|
`[plan-context] the assembled "${envelope?.mode}" envelope is ` +
|
|
112
149
|
`${Math.round(bytes / 1024)} KB, over the ` +
|
|
113
150
|
`${Math.round(ceiling / 1024)} KB planner-context ceiling. Largest ` +
|
|
114
|
-
`fields: ${largest}.
|
|
115
|
-
'issues in one run. Raising the ceiling needs a measured ' +
|
|
151
|
+
`fields: ${largest}. ${remedy} Raising the ceiling needs a measured ` +
|
|
116
152
|
'justification — see PLAN_CONTEXT_ENVELOPE_BYTE_CEILING.',
|
|
117
153
|
);
|
|
118
154
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolveFeatureRoots,
|
|
13
13
|
verifyBddRunnerPendingTag,
|
|
14
14
|
} from '../../bdd-runner-detect.js';
|
|
15
|
+
import { capBddScenarios } from '../../bdd-scenario-budget.js';
|
|
15
16
|
import { scanBddScenarios } from '../../bdd-scenario-scanner.js';
|
|
16
17
|
import { getPaths, PROJECT_ROOT } from '../../config-resolver.js';
|
|
17
18
|
import { fetchPriorFeedback } from '../../feedback-loop/prior-feedback-fetcher.js';
|
|
@@ -73,18 +74,24 @@ async function buildPlanningDocsContext({ seedIssueId, settings, cwd }) {
|
|
|
73
74
|
/**
|
|
74
75
|
* Story #2637 — index existing BDD scenarios so the Acceptance Engineer step
|
|
75
76
|
* can annotate planned ACs with matches from the project's `.feature` files.
|
|
76
|
-
* Empty
|
|
77
|
-
* best-effort and never throws on filesystem errors.
|
|
77
|
+
* Empty (capped-shape) result when the project has not adopted BDD; the
|
|
78
|
+
* scanner is best-effort and never throws on filesystem errors.
|
|
78
79
|
*
|
|
79
|
-
*
|
|
80
|
+
* Story #4977 — the scan itself stays uncapped (a faithful `.feature`
|
|
81
|
+
* index), but the envelope-bound result is truncated to
|
|
82
|
+
* `BDD_SCENARIOS_BYTE_BUDGET` via `capBddScenarios` so a mature Gherkin
|
|
83
|
+
* corpus cannot alone consume the `/plan` context-envelope ceiling. Callers
|
|
84
|
+
* needing the raw count read `totalScenarios` vs `includedScenarios`.
|
|
85
|
+
*
|
|
86
|
+
* @returns {ReturnType<typeof capBddScenarios>}
|
|
80
87
|
*/
|
|
81
88
|
function scanBddScenariosBestEffort() {
|
|
82
89
|
try {
|
|
83
90
|
const featureRoots = resolveFeatureRoots({ cwd: PROJECT_ROOT });
|
|
84
|
-
return scanBddScenarios({ featureRoots });
|
|
91
|
+
return capBddScenarios(scanBddScenarios({ featureRoots }));
|
|
85
92
|
} catch (err) {
|
|
86
93
|
Logger.warn(`[plan-context] BDD scenario scan skipped: ${err.message}`);
|
|
87
|
-
return [];
|
|
94
|
+
return capBddScenarios([]);
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
97
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,20 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.28.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.27.0...mandrel-v2.28.0) (2026-08-03)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
* uncapped bddScenarios consumes the /plan envelope ceiling; blocks /audit-to-stories' single-plan path entirely ([#4977](https://github.com/dsj1984/mandrel/issues/4977)) ([#4978](https://github.com/dsj1984/mandrel/issues/4978)) ([c8b2731](https://github.com/dsj1984/mandrel/commit/c8b27312e67b138567e4e41789871e3911c7a8cb))
|
|
24
|
+
|
|
25
|
+
## [2.27.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.26.0...mandrel-v2.27.0) (2026-08-03)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
* **baselines:** carry provenanceStamped through the reader's narrowing (refs [#4973](https://github.com/dsj1984/mandrel/issues/4973)) ([#4974](https://github.com/dsj1984/mandrel/issues/4974)) ([4a3795a](https://github.com/dsj1984/mandrel/commit/4a3795a2e3d60748d5b40b0288521af5a7a885ab))
|
|
31
|
+
|
|
18
32
|
## [2.26.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.25.0...mandrel-v2.26.0) (2026-08-03)
|
|
19
33
|
|
|
20
34
|
|
package/package.json
CHANGED