From 9ac0dfad58d77d9f33ec06e07a650baf51d7b8c4 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 31 Jul 2026 23:03:13 -0400 Subject: [PATCH] chore(#2929): generalize prompt-budget into the shared context-composer seam (#2958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(#2929): capture prompt-budget parity corpus pre-refactor Phase 2 of epic #1671 generalizes prompt-budget's trim ladder into a shared context-composer seam. Its success condition is that review-prompt output does not change, and the only authority on "did not change" is the behavior that shipped before the refactor. Capture that behavior now, while it is still the live implementation. 47 characterization cases, every `expected` value computed by executing the current implementation rather than hand-authored — the independence CONTRIBUTING.md "Fixture provenance (#2371)" asks for. A corpus is only worth what it can detect, so this one was validated by mutation rather than assumed. Five deliberate defects were injected and each must be caught by at least one case: - the note reserve deducted unconditionally instead of only under pressure - the pressure test relaxed from `>` to `>=` - a no-op head-shrink still setting the shrunk flag - the per-plan floor dropped from the proportional share - drop order reversed Two of those exposed real holes in the first cut of this corpus, and the cases that close them exist because of it: - `>=` was caught by NOTHING. At exact cap the only trimmable fragment was a floored plan group, and the 1024-char floor absorbed the entire trim, so the mutation was byte-invisible. A3b/A3c put a droppable at exactly the cap, which makes the strict inequality observable as context kept vs omitted. - No case reached proportional-truncate at all — B6 and B7 both hard-failed the min-set pre-check first, leaving planTruncationPct at 0 across every case and the floor semantics entirely unexercised. Rebudgeted to 700 and 1100 so the min-set fits and the truncate step is actually reached; they now record 40.20% and 48.80%. The A4/A10 families sweep the pressure boundary from both sides, which is where this function has regressed before: CONTEXT.md's LEARNING.prompt-budget.boundary-gap records PR #3708 shipping two regressions that only fired when the baseline sat inside the NOTE_RESERVE_TOKENS band, because the suite paired a trivially-fitting budget with a trivially-overflowing one and never sampled between them. A4 pins that nothing is trimmed from the cap down to 81 tokens under it; A10 pins that pressure fires at +1. Together with A3b/A3c they satisfy row (d) of RULESET.TESTS.boundary-coverage.fixtures. Two facts the corpus establishes that the design notes had wrong: - "" and null sections are NOT distinguished. applyBudget uses truthy checks throughout, so an empty-string section is treated as absent: not rendered, not dropped, never recorded in `omitted`. B13b pins this while the ladder is actively trimming, where only the non-empty `research` is dropped. - Sizing matters. B12/B13 were first written at a budget where both hard-failed the min-set check and returned "", so comparing them compared two empty strings and proved nothing. Committed as its own commit, ahead of the refactor, and regenerated against the pre-refactor implementation, so the oracle is demonstrably independent of the change it will adjudicate. Refs #2929 * refactor(#2929): extract the context-composer seam from prompt-budget Epic #1671 needs prompt-budget's budget-trimming logic for a second consumer — per-runtime artifact emission — but it is walled inside the cross-AI review pipeline. Lift it into a shared seam so later phases can call it, without changing what the review pipeline emits. ADR-1671 specifies the composer as "priority + binary-search cutoff to a per-runtime budget". Read against the code it generalizes, that contract cannot express the thing being generalized. applyBudget is not a cutoff: it is a fixed five-step ladder in which each section carries its own shrink strategy, and only three of its eight sections are ever dropped. PROJECT.md is head-shrunk to N lines; plans are proportionally tail-truncated with a per-plan 1024-byte floor; instructions and roadmap are never touched at all. A cutoff composer sorts by priority and discards the tail — it has no way to say "shrink this one", "truncate that one but never below 1 KB each", or "these three are the only droppables, in this order". Building to the literal contract and routing prompt-budget through it would have silently changed review-prompt output, which is the one outcome this phase forbids. So shrink strategies are the core abstraction here, and cutoff becomes one strategy among them — the right one for per-runtime emission in Phases 3-4, not for this ladder. That is an elaboration of the ADR's intent, not a departure from it, and ADR-1671 is updated to say so. Three decisions worth stating: - The composer DECIDES; the caller RENDERS. composeWithinBudget returns a plan of surviving fragments and never a string. assemblePrompt's rendering is prompt-shaped (`## Roadmap`, `### `, the note in position two), and owning it in the composer would force emission to adopt prompt-shaped rendering. The split is what lets one seam serve both consumers. - The budget unit is INJECTED via `measure(text)`. prompt-budget passes its chars/4 estimator; emission will pass a byte counter, which ADR-1671 requires for emission caps. The existing code converts a token budget to a character budget with a hardcoded `* 4`; that assumption is now an explicit `charsPerUnit` inverse, which is precisely what a byte unit needs in order to reuse this. - The entry point is `composeWithinBudget`, not `applyBudget`. That name already exists twice — src/prompt-budget.cts and src/graphify.cts, the latter being an unrelated graph-edge budget. A third would make every symbol search in this repo ambiguous, and it already misresolves: preflight and impact queries for "applyBudget" return graphify's. Behavior is unchanged and proven so: all 47 characterization cases reproduce byte-identically, and the corpus is mutation-validated rather than merely green (see the preceding commit). prompt-budget.cts drops from 436 to 343 lines and from eighteen mutable accumulators to two, both inside a helper copied verbatim. estimateTokens deliberately stays in prompt-budget and keeps its exact math: src/phase-estimation.cts re-exports it as measureTokens, and CONTEXT.md pins plan estimates and recorded actuals to that same scale, so moving or changing it would silently break the calibration loop. Refs #2929 * docs(#2929): document the context-composer seam and amend ADR-1671 Adds the INVENTORY row, the CONTEXT.md glossary entry (a PR gate for new domain modules), and a mutation-matrix entry for the new module. The ADR amendment is the substantive part. ADR-1671 specified the composer as "priority + binary-search cutoff to a per-runtime budget". Implementing Phase 2 established that a cutoff alone cannot express the function the platform generalizes, so the ADR now records shrink strategies as the core abstraction with cutoff as one strategy among them, reserved for per-runtime emission in Phases 3-4. Recording it in the ADR matters because Phases 3-6 are planned against that contract and would otherwise be planned against a mechanism that does not work. The mutation-matrix entry is not bookkeeping. Stryker scores per module against a named .cjs, so relocating the ladder out of prompt-budget.cjs would leave the extracted code unmeasured while prompt-budget's own score floated free of the logic it used to cover. context-composer gets its own entry at the same floor. Refs #2929 * test(#2929): pin the effectiveBudget rounding mode in the parity corpus An isolated correctness review found a real blind spot: mutating `Math.floor` to `Math.round` in the effectiveBudget calculation failed ZERO of the 47 corpus cases. Every (budget, safetyMarginPct) pair in the generator happened to produce a whole number, so floor, round and ceil all agreed and the rounding mode was entirely unpinned by a corpus whose whole job is to pin observable behavior. Three cases fix that by straddling the .5 boundary: A11 95 * 0.90 = 85.5 floor 85, round 86 -> the two disagree A12 97 * 0.90 = 87.3 floor and round agree; ceil (88) does not A13 93 * 0.85 = 79.05 same guard at a non-multiple-of-10 margin, so the margin arithmetic is exercised and not just the budget A11 alone catches the round mutation; all three catch ceil. Regenerated against the pre-refactor implementation (`git show 9557f8552:src/prompt-budget.cts`), so the expanded corpus keeps the independence property the original capture had. The corpus is now mutation-validated against seven injected defects, every one caught: unconditional note reserve, `>` relaxed to `>=`, no-op head-shrink setting its flag, the truncate floor ignored, drop order reversed, and both rounding-mode changes. Refs #2929 * feat(#2929): flexReserve floors and the byte-stable isolate prefix Two of issue #2929's "Done when" items were unimplemented rather than deferred, and an isolated review flagged them alongside my own audit. Both are part of ADR-1671's composer contract, so shipping the seam without them would have left Phases 3-4 building against a contract that does not exist yet. flexReserve is a per-fragment floor in measure units that every strategy must respect, which is what makes it different from the pre-existing floorChars: that one is a chars-denominated detail of proportional-truncate alone and is retained unchanged. A floored fragment is never dropped, is never head-shrunk below its floor, and raises its own proportional cap. A fragment already smaller than its floor is untouchable outright. Metadata gains `floored`, listing the ids whose floor actually prevented a trim — a guarantee no caller can observe is a guarantee no test can hold you to. isolate marks the byte-stable canonical prefix the ADR calls for: never trimmed, never dropped, but still counted, because a prefix excluded from accounting would silently under-count real context. Metadata gains `isolatePrefix` so a caller can hash or assert on the exact bytes. Declaring an isolate fragment after a non-isolate one throws: a prefix that is not at the front is not a prefix, and accepting it would make the cross-runtime stability claim meaningless. Adds tests/context-composer.test.cjs for the exact new semantics and tests/context-composer.property.test.cjs for the five invariants, including the budget-monotonicity property the issue names explicitly. Both are registered in the mutation matrix, since coverage does not migrate with relocated code. prompt-budget uses neither feature, and its output is unchanged: all 50 corpus cases still reproduce byte-identically. Refs #2929 * chore(#2929): allowlist the prompt-budget parity suite The parity corpus needs its own test file and that makes prompt-budget a three-file module against a limit of two. The lint offers consolidation or an allowlist entry with justification; the entry is the right call here. Consolidation would mean folding the characterization suite into prompt-budget.test.cjs, which is the one thing that should not happen to it. The parity suite is a distinct concern with a distinct lifecycle: it is generated rather than hand-written, it is named by scripts/mutation-matrix.cjs as its own scoring target, and its failure means something categorically different from a unit-test failure — not "this behavior is wrong" but "observable output moved". Burying it inside a general unit file would obscure exactly that signal. The allowlist is an identity ratchet, so this entry pins today's three exact filenames: adding a fourth still fails, and dropping back to two requires removing the entry. Refs #2929 * fix(#2929): register the new module with two gates it was missing The remote matrix caught three defects that no local check could, because the local runner is blocked in this repo and these suites had therefore never executed. Eight failures, identical on node22 and node24, so nothing environment-shaped. Two are the new-module ripple. A net-new src/*.cts lands in six places and this change had reached four of them — .gitignore, INVENTORY, the manifest, and the CONTEXT.md glossary — while missing the ESLint ignore list (tsc OUTPUTS must not be linted; repo-invariants asserts linted-xor-ignored) and the mutation ratchet baseline (a deliberate review-visible mirror of the matrix floors, which every COVERED module must carry). Both are now registered, the ratchet at the same floor of 66 the matrix declares. The third was a test asserting an outcome it had made impossible. It set budget:1 alongside a 400-char required fragment, so the group budget came out at -99 and the proportional-truncate step was skipped entirely — the deliberate "non-positive group budget is skipped, never clamped" rule inherited from the original ladder. Nothing was trimmed, and the test then asserted a truncation. Rebudgeted so the step actually runs, with the arithmetic written out in a comment so the next reader does not have to re-derive why 120 rather than 80. Fixing that surfaced a genuine bug in the composer. `floored` is documented as recording fragments whose flexReserve prevented a trim that would otherwise have happened, but the push sat in the else-branch of "content did not change", so it only fired when nothing was trimmed at all. A fragment truncated to a reserve-raised cap has also had a trim prevented — 40 characters' worth in the test above — and was silently absent from the field that exists to make the guarantee observable. The condition was already right; it was in the wrong branch. Now recorded on both paths: a drop prevented outright, and a truncation capped higher than the share alone would have allowed. Parity is unaffected — prompt-budget never sets flexReserve, so the branch is unreachable from every corpus path, and all 50 cases still match. Refs #2929 * chore(#2929): backfill changeset PR number (#2958) * chore(#2929): correct the corpus case count in the changeset fragment --------- Co-authored-by: sim --- .changeset/sturdy-orcas-glide.md | 5 + .gitignore | 1 + CONTEXT.md | 4 + docs/INVENTORY-MANIFEST.json | 1 + docs/INVENTORY.md | 1 + ...671-dynamic-context-management-platform.md | 4 +- eslint.config.mjs | 2 + scripts/gen-prompt-budget-parity-corpus.cjs | 645 ++++++ scripts/lint-test-file-count.allowlist.json | 8 + scripts/mutation-matrix.cjs | 13 + src/context-composer.cts | 396 ++++ src/prompt-budget.cts | 291 +-- tests/context-composer.property.test.cjs | 229 +++ tests/context-composer.test.cjs | 296 +++ .../fixtures/prompt-budget-parity/corpus.json | 1722 +++++++++++++++++ tests/mutation-matrix-ratchet.test.cjs | 1 + tests/prompt-budget-parity.test.cjs | 103 + 17 files changed, 3529 insertions(+), 193 deletions(-) create mode 100644 .changeset/sturdy-orcas-glide.md create mode 100644 scripts/gen-prompt-budget-parity-corpus.cjs create mode 100644 src/context-composer.cts create mode 100644 tests/context-composer.property.test.cjs create mode 100644 tests/context-composer.test.cjs create mode 100644 tests/fixtures/prompt-budget-parity/corpus.json create mode 100644 tests/prompt-budget-parity.test.cjs diff --git a/.changeset/sturdy-orcas-glide.md b/.changeset/sturdy-orcas-glide.md new file mode 100644 index 000000000..0437613ac --- /dev/null +++ b/.changeset/sturdy-orcas-glide.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 2958 +--- +**Budget-aware content composition is now a shared `context-composer` seam** — the priority-ordered trimming that kept cross-AI review prompts inside a model's context window was locked inside that one pipeline. It is now a reusable seam with an injectable budget unit, so later work can right-size what ships to each runtime. Review-prompt output is unchanged, proven byte-for-byte against a 50-case corpus captured from the previous implementation. (#2929) diff --git a/.gitignore b/.gitignore index 68a9453c5..7cf309367 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ build/ /gsd-core/bin/lib/mcp-server.cjs /gsd-core/bin/lib/external-descriptor-trust.cjs /gsd-core/bin/lib/cli-skew-check.cjs +/gsd-core/bin/lib/context-composer.cjs /gsd-core/bin/lib/context-predicates.cjs /gsd-core/bin/lib/capability-loader.cjs /gsd-core/bin/lib/capability-source.cjs diff --git a/CONTEXT.md b/CONTEXT.md index d8c2cc846..5ae7ca12f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -193,6 +193,10 @@ Module owning install detection for `/gsd:update`. `resolveUpdateContext({ home, ### Skill Surface Budget Module Module owning which skills and agents are written to runtime config directories at install time (Phase 1) and at runtime via cluster-level toggles (Phase 2). Phase 1: `gsd-core/bin/lib/install-profiles.cjs` defines named profiles (`core`, `standard`, `full`), computes transitive closure over `requires:` frontmatter, stages skills/agents to runtime config dirs, and persists the chosen profile in a `.gsd-profile` marker. Profile resolution precedence: explicit `--profile=` flag > `.gsd-profile` marker > `full`. `--minimal`/`--core-only` are back-compat aliases for `--profile=core`. Phase 2: `gsd-core/bin/lib/surface.cjs` implements the `/gsd:surface` slash command for cluster-level enable/disable without reinstall; cluster definitions live in `gsd-core/bin/lib/clusters.cjs`; per-runtime state persists in `/.gsd-surface.json` independent from the `.gsd-profile` marker. See ADR-0011. +### Context Composer Module + +Shared, pure, no-I/O seam owning priority-ordered composition of content fragments within a measured budget (ADR-1671 Decision item 2; extracted from `prompt-budget` by #2929, epic #1671 Phase 2). Interface: `composeWithinBudget({fragments, budget, measure, options}) → {fragments, metadata}`, plus the relocated `headShrink`/`tailTruncate` primitives. **The composer decides; the caller renders** — it returns a plan of surviving fragments with their trimmed content and never emits a rendered string, because `prompt-budget`'s assembly is prompt-shaped (`## Roadmap`, `### `, note in position two) and per-runtime emission renders differently; owning rendering would prevent one seam from serving both. **Budget unit is injected** via `measure(text) → number` (`prompt-budget` passes `estimateTokens`, chars/4; emission passes a byte counter per ADR-1671's "bytes for emission caps"), with `charsPerUnit` as `measure`'s inverse for the proportional-truncate step — the prior code hardcoded `* 4`, which silently assumed the token estimator. **Shrink strategies, not a cutoff:** ADR-1671's literal contract says "priority + binary-search cutoff", which cannot express head-shrink, proportional-truncate-with-floor, or never-droppable classes; the closed strategy set is `verbatim | head-shrink | proportional-truncate(floorChars) | drop`, and a cutoff strategy joins that set when per-runtime emission needs it (Phases 3-4). Ordering is **declaration order**, not a numeric priority field. A `flexReserve`-style floor is a per-fragment guarantee, NOT a budget cap — it may deliberately push the total above the proportional share. The note reserve is deducted **only under pressure** (`baseline > effectiveBudget`, strict): deducting unconditionally drops sections `reserve` units early, which is the PR #3708 regression recorded at `LEARNING.prompt-budget.boundary-gap`. Presence is a **truthy** test, so an empty-string fragment is indistinguishable from an absent one — characterized, not designed. Source of truth: `gsd-core/bin/lib/context-composer.cjs` (generated from `src/context-composer.cts`). Test anchors: `tests/prompt-budget-parity.test.cjs`, `tests/fixtures/prompt-budget-parity/corpus.json`. + ### Runtime Artifact Layout Module Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Owns the per-runtime `nested` skill-bundle decision (#69): a `skillsKind` flag in `src/runtime-artifact-layout.cts` drives whether a runtime receives the nested router layout (6 `gsd-ns-*` routers + concrete skills under `/skills//`) or the flat `skills/gsd-/` layout; the evidence/doc-link matrix is recorded in a comment above `resolveRuntimeArtifactLayout`. Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. Per ADR-1508 / #1511 the former `getInstallExports`/`loadInstallExports` relay (a `GSD_TEST_MODE`-guarded `require('bin/install.js')` by which `surface.cjs` reached `computePathPrefix`/`applyRuntimeContentRewritesInPlace`) was DELETED from this module; content rewriting now lives in the Runtime Artifact Conversion Module and `surface.cjs:applySurface` calls its `rewriteStagedSkillBodies` directly. The resolved `scope` is still carried on the `Layout` object so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). The `.gsd-source` marker (#1477) is a two-party provisioning contract that lets source resolution succeed on the Claude global skills layout, which ships `gsd-core/{bin,contexts,references,templates,workflows}` but no `commands/gsd` source tree for `findInstallSourceRoot` to walk up to: the writer is `bin/install.js`, which writes `/.gsd-source` (content: the absolute path to its own `commands/gsd`, terminated by a newline) when `runtime === 'claude' && isGlobal`, guarded by `fs.existsSync` so a half-published package never writes a dangling marker; the reader is `findInstallSourceRoot(configDir)`, which prefers the marker over its walk-up but falls through to the walk-up if the marker is absent, dangling, or empty/whitespace-only. See ADR-3660. diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 906224eda..0e7870dbd 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -346,6 +346,7 @@ "config-types.cjs", "config.cjs", "configuration.cjs", + "context-composer.cjs", "context-predicates.cjs", "context-utilization.cjs", "core-utils.cjs", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 1e9c562e8..f7bd3981b 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -446,6 +446,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `config-types.cjs` | TypeScript type definitions for the `model_policy` config block — `ModelPolicyConfig`, `TierEntry`, `RuntimeTiers`; compiled from `src/config-types.cts` at publish time (ADR-457) | | `config.cjs` | `config.json` read/write, section initialization; imports validator from `config-schema.cjs` | | `configuration.cjs` | Configuration Module — legacy-key normalization, defaults merge, and explicit on-disk migration; pure normalization primitives consumed by `config-loader.cjs` and `config-schema.cjs` (loadConfig extracted to config-loader per ADR-857 #885) | +| `context-composer.cjs` | Shared budget-composition seam (ADR-1671, #2929) — `composeWithinBudget` trims an ordered fragment list to a measured budget and returns a PLAN of surviving fragments, never rendered text, so one seam serves both the review pipeline and per-runtime emission. Closed strategy set: `verbatim`, `head-shrink`, `proportional-truncate` (with a per-fragment floor), `drop`. The budget unit is injected via `measure(text)` — tokens for `prompt-budget`, bytes for emission — with `charsPerUnit` as its inverse. Also exports `headShrink`/`tailTruncate`. Compiled from `src/context-composer.cts` | | `context-predicates.cjs` | CONTEXT.md predicate fact-store parser (ADR-1671, #2928) — pure `parsePredicates` (extracts every backtick-wrapped `CLASS.subkey=value` declaration, fence/HTML-comment-aware), `selectPredicates` (class/prefix/contains selectors, ANDed), and `buildIndex` (deterministic, line-free artifact shape); backs both `gsd_run query context-predicates` and `scripts/gen-context-index.cjs`'s docs/CONTEXT-INDEX.json drift guard. Compiled from `src/context-predicates.cts` | | `context-utilization.cjs` | Pure classifier for `gsd-health --context` — turns (tokensUsed, contextWindow) into a `{ percent, state }` triage result against the 60%/70% fracture-point thresholds (#2792) | | `core-utils.cjs` | Shared low-level utilities — POSIX path normalization, sub-repo/subdirectory scanning, phase file stats, slug/one-liner/plan-id helpers, time-ago (extracted from `core.cjs`, ADR-857) | diff --git a/docs/adr/1671-dynamic-context-management-platform.md b/docs/adr/1671-dynamic-context-management-platform.md index 4a31273eb..13c602ea0 100644 --- a/docs/adr/1671-dynamic-context-management-platform.md +++ b/docs/adr/1671-dynamic-context-management-platform.md @@ -63,7 +63,9 @@ Pure Agent Skills (A alone) and pure MCP (D alone) were rejected as the foundati ## Architecture and contracts - **Fragment unit (open question, see below):** either separate files (clean lazy-load + INVENTORY rows) or in-file section markers (``, mirroring the existing `` markers consumed by `scripts/gen-loop-host-contract.cjs`). -- **Composer contract:** priority + binary-search cutoff to a per-runtime budget; `flexReserve`-style floors for load-bearing fragments (`META.RULE` citation rules, contribution gates, closing-keyword rules); a byte-stable canonical prefix (``) kept identical across runtimes to preserve KV-cache warmth and keep launcher-parity tests green. +- **Composer contract:** an ordered list of fragments, each carrying a *shrink strategy*; the closed set is `verbatim`, `head-shrink`, `proportional-truncate` (with a per-fragment floor), and `drop`. `flexReserve`-style floors for load-bearing fragments (`META.RULE` citation rules, contribution gates, closing-keyword rules) generalize the existing per-plan 1024-byte floor. A byte-stable canonical prefix (``) is kept identical across runtimes to preserve KV-cache warmth and keep launcher-parity tests green. + + **Amended by #2929 (Phase 2).** This ADR originally specified the contract as "priority + binary-search cutoff to a per-runtime budget". Implementing Phase 2 established that a cutoff alone **cannot express the function this platform generalizes**: `prompt-budget.applyBudget` is not a cutoff but a fixed five-step ladder in which each section carries its own shrink strategy, and only three of its eight sections are ever droppable — `PROJECT.md` is head-shrunk to N lines and plans are proportionally tail-truncated with a per-plan floor, while instructions and roadmap are never trimmed at all. A cutoff composer sorts by priority and discards the tail; it has no way to say "shrink this one", "truncate that one but never below its floor", or "these three are the only droppables, in this order". Building to the literal wording and routing `prompt-budget` through it would have silently changed review-prompt output. Shrink strategies are therefore the core abstraction, and **binary-search cutoff becomes one strategy among them** — the right one for per-runtime emission in Phases 3-4, not for this ladder. Ordering is declaration order rather than a numeric priority field. This is an elaboration of the decision's intent, not a reversal of it. - **Budget unit:** bytes for emission caps (matches `lfByteCount`, deterministic, offline-safe); a token estimate for run-time selection. - **Determinism + drift-guard:** every generated artifact follows the universal `--check`/`--write` idiom and is committed; any constant shared between two surfaces gets a `DEFECT.GENERATIVE-FIX` parity assertion. Caps are asserted on **emitted per-runtime bytes** via real spawn-install tests (engine-direct tests are false-green for install behavior). - **Boundary coverage:** the composer's budget logic is tested at `cap-1 / cap / cap+1` per `RULESET.TESTS.boundary-coverage`. diff --git a/eslint.config.mjs b/eslint.config.mjs index c07dd8f3a..6f126a87c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -236,6 +236,8 @@ export default tseslint.config( 'gsd-core/bin/lib/mcp-server.cjs', // ADR-1671 (#2928): tsc-generated runtime artifact — lint the src/context-predicates.cts source. 'gsd-core/bin/lib/context-predicates.cjs', + // #2929: tsc-generated runtime artifact — lint the src/context-composer.cts source. + 'gsd-core/bin/lib/context-composer.cjs', ], }, diff --git a/scripts/gen-prompt-budget-parity-corpus.cjs b/scripts/gen-prompt-budget-parity-corpus.cjs new file mode 100644 index 000000000..f60427a76 --- /dev/null +++ b/scripts/gen-prompt-budget-parity-corpus.cjs @@ -0,0 +1,645 @@ +#!/usr/bin/env node +/** + * gen-prompt-budget-parity-corpus.cjs + * + * Captures a characterization corpus of `prompt-budget.applyBudget`'s observable + * output, for issue #2929 (epic #1671 Phase 2 — the `context-composer` seam). + * + * WHY THIS EXISTS + * --------------- + * Phase 2 generalizes `applyBudget`'s trim ladder into a shared composer seam. + * Its success condition is that `prompt-budget`'s output does not change. The only + * authority on "did not change" is the behavior that shipped BEFORE the refactor, + * so this corpus is generated against the pre-refactor implementation and committed + * as a frozen fixture. `tests/prompt-budget-parity.test.cjs` then replays every case + * and asserts byte-identical `prompt` plus field-identical `metadata`. + * + * FIXTURE PROVENANCE (CONTRIBUTING.md "Fixture provenance (#2371)") + * ----------------------------------------------------------------- + * That rule forbids fixtures derived from the gate's own writer, grammar, or the + * author's mental model, because such a fixture can only confirm what the author + * already believed. This corpus is expressly NOT hand-authored: every `expected` + * value is computed by executing the shipped implementation, which predates the + * composer and knows nothing about it. What IS author-chosen is the set of INPUTS — + * so the inputs are derived from the design's parity-critical behavior table rather + * than from intuition, and `tests/prompt-budget-parity.test.cjs` additionally proves + * the corpus is non-vacuous by mutating the ladder and requiring the corpus to fail. + * Disclosed here rather than assumed. + * + * USAGE + * node scripts/gen-prompt-budget-parity-corpus.cjs --write # regenerate + * node scripts/gen-prompt-budget-parity-corpus.cjs --check # drift-guard (exit 1 on drift) + * + * `--check` is the intentional-change gate: after Phase 2 lands, any diff here means + * review-prompt output moved, which is a user-visible change requiring justification. + */ + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const OUT_PATH = path.join(REPO_ROOT, 'tests', 'fixtures', 'prompt-budget-parity', 'corpus.json'); +const IMPL_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'lib', 'prompt-budget.cjs'); + +/** Corpus schema version. Bump only on a deliberate shape change. */ +const CORPUS_VERSION = 1; + +// ─── Deterministic content builders ────────────────────────────────────────── +// estimateTokens() is Math.ceil(len / 4), so a string of length 4*N measures +// exactly N tokens. Every size below is therefore exact, not approximate — which +// is what makes the cap-1 / cap / cap+1 rows land ON the boundary rather than near it. + +/** A filler string measuring exactly `tokens` tokens under chars/4. */ +function tokensOf(tokens, fill = 'x') { + return fill.repeat(tokens * 4); +} + +/** `lines` newline-separated lines, each `width` chars wide. */ +function linesOf(lines, width = 19) { + return Array.from({ length: lines }, (_, i) => String(i).padEnd(width, '-')).join('\n'); +} + +// ─── Case definitions ──────────────────────────────────────────────────────── +// Each case is { name, why, sections, budget, options }. +// `why` names the design/test-matrix row the case exists to pin. + +function buildCases() { + const cases = []; + const add = (c) => cases.push(c); + + // Base shape reused by the boundary family. With safetyMarginPct 0 the effective + // budget equals the budget, so totals can be placed exactly on the cap. + // staticBase = est(instructions) + est('## Roadmap\n\n') + est(roadmap) + // + est('## Plans\n\n') + est('### a.md\n\n') + const boundaryBase = (planTokens) => ({ + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(planTokens, 'P') }], + }); + + // ── A. Budget boundary (RULESET.TESTS.boundary-coverage.fixtures a/b/c) ──── + add({ + name: 'A1-exact-cap', + why: 'A1/row 3 — total measures exactly effectiveBudget; must NOT be treated as pressure', + sections: boundaryBase(71), + budget: 100, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'A2-one-under-cap', + why: 'A2/row 5 — one token under; no pressure, no reserve deducted', + sections: boundaryBase(70), + budget: 100, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'A3-one-over-cap', + why: 'A3/row 4 — one token over; pressure engages the ladder', + sections: boundaryBase(72), + budget: 100, + options: { safetyMarginPct: 0 }, + }); + + // A1 pins exact-cap as "no pressure", but with only a floored plan group present that + // assertion is not load-bearing: the 1024-char floor absorbs any trim, so relaxing the + // pressure test from `>` to `>=` produces byte-identical output and the corpus cannot + // see the difference. This case gives the ladder something DROPPABLE at exactly the cap, + // so `>` vs `>=` becomes observable as context surviving vs being omitted. + // staticBase 29 + plan 20 = 49, context wrapper 3, so est(context) 148 puts the + // baseline at exactly 200 = effectiveBudget. + add({ + name: 'A3b-exact-cap-with-droppable-present', + why: 'exact-cap discriminator — baseline lands exactly ON effectiveBudget with a droppable available. Pins that `baseline > effectiveBudget` is STRICT: at equality nothing is dropped. Mutating the comparison to >= flips context from kept to omitted, which A1 alone cannot detect.', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(20, 'P') }], + context: tokensOf(148, 'C'), + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'A3c-one-under-cap-with-droppable-present', + why: 'companion to A3b — one token under the cap with the same droppable available. Together they bracket the strict-inequality boundary from below.', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(20, 'P') }], + context: tokensOf(147, 'C'), + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + + // Row (d) of RULESET.TESTS.boundary-coverage.fixtures: baseline within + // reserve-distance of the limit. NOTE_RESERVE_TOKENS is 80, and this is the exact + // band (effectiveBudget - 80, effectiveBudget] that PR #3708 shipped two + // regressions in (LEARNING.prompt-budget.boundary-gap). Swept token by token. + // Budget 200 (not 100) is required here, not incidental: the band is + // NOTE_RESERVE_TOKENS (80) wide, so the baseline needs >=81 tokens of headroom + // beneath the cap to be swept. staticBase is 29 tokens, so planTokens = 171 - delta + // places the baseline exactly `delta` under effectiveBudget and stays positive + // across the whole sweep. delta 80 and 81 sit just OUTSIDE the band deliberately — + // they are the negative controls that prove the band's edge is where it is claimed. + for (const delta of [0, 1, 2, 39, 40, 79, 80, 81]) { + add({ + name: `A4-reserve-band-minus-${delta}`, + why: `boundary-coverage.fixtures row (d) — baseline at effectiveBudget-${delta}. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.`, + sections: boundaryBase(171 - delta), + budget: 200, + options: { safetyMarginPct: 0 }, + }); + } + + // Mirror of A4 on the OTHER side of the cap. A4 proves the reserve is NOT deducted + // at or below the cap; this proves it IS deducted above it, and that the ladder + // relieves pressure by dropping. Without both sides the family pins only half the + // boundary, and an "always reserve" regression would still pass A4. + // staticBase 29 + planContent 20 = 49, and the context wrapper costs 3, so + // est(context) = 148 + delta places the baseline exactly `delta` OVER effectiveBudget. + for (const delta of [1, 2, 40, 79, 80, 81]) { + add({ + name: `A10-pressure-band-plus-${delta}`, + why: `boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+${delta}; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped`, + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(20, 'P') }], + context: tokensOf(148 + delta, 'C'), + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + } + + add({ + name: 'A5-safety-margin-default', + why: 'A4 — default 10% margin; effectiveBudget = floor(budget * 0.9)', + sections: boundaryBase(60), + budget: 100, + options: {}, + }); + + // The rounding mode of effectiveBudget was unpinned: every other case in this file + // uses a (budget, safetyMarginPct) pair whose product is a whole number, so + // Math.floor and Math.round agree and mutating one to the other changed nothing. + // These three straddle .5 in both directions so the mode is observable. + // 95 * 0.90 = 85.5 -> floor 85, round 86 (disagree) + // 97 * 0.90 = 87.3 -> floor 87, round 87 (agree; guards against a ceil mutation) + // 93 * 0.85 = 79.05 -> floor 79, round 79 (agree; second ceil guard, different margin) + add({ + name: 'A11-fractional-effective-budget-half', + why: 'pins the ROUNDING MODE of effectiveBudget. 95 * (1 - 10/100) = 85.5, where Math.floor (85) and Math.round (86) disagree. Without a fractional case the mode is unpinned and floor->round is a byte-invisible mutation, which an isolated review confirmed against the 47-case corpus.', + sections: boundaryBase(56), + budget: 95, + options: { safetyMarginPct: 10 }, + }); + add({ + name: 'A12-fractional-effective-budget-below-half', + why: 'companion to A11 below the .5 point: 97 * 0.90 = 87.3, where floor and round agree but Math.ceil (88) would not. Guards the mutation A11 does not cover.', + sections: boundaryBase(58), + budget: 97, + options: { safetyMarginPct: 10 }, + }); + add({ + name: 'A13-fractional-effective-budget-odd-margin', + why: 'third rounding guard at a non-multiple-of-10 margin: 93 * (1 - 15/100) = 79.05. Exercises the margin arithmetic itself, not just the budget, since safetyMarginPct is caller-supplied.', + sections: boundaryBase(50), + budget: 93, + options: { safetyMarginPct: 15 }, + }); + add({ + name: 'A6-safety-margin-zero', + why: 'A5 — 0% margin means the full budget is usable', + sections: boundaryBase(60), + budget: 100, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'A7-safety-margin-hundred', + why: 'A5 — 100% margin drives effectiveBudget to 0, forcing the min-set hard fail', + sections: boundaryBase(10), + budget: 100, + options: { safetyMarginPct: 100 }, + }); + add({ + name: 'A8-budget-zero', + why: 'A6 — non-positive budget must hard-fail cleanly, never negative-length slice', + sections: boundaryBase(10), + budget: 0, + options: {}, + }); + add({ + name: 'A9-budget-negative', + why: 'A6 — negative budget must not crash', + sections: boundaryBase(10), + budget: -50, + options: {}, + }); + + // ── B. Shrink strategies ────────────────────────────────────────────────── + add({ + name: 'B3-head-shrink-engaged', + why: 'B3/row 7 — projectMd longer than projectMdHeadLines under pressure; shrunk flag true', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(40, 'P') }], + projectMd: linesOf(120), + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B4-head-shrink-noop', + why: 'B4/row 7+9 — fewer lines than the cap; text unchanged and shrunk flag MUST stay false', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(40, 'P') }], + projectMd: linesOf(5), + }, + budget: 60, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B5-head-shrink-zero-lines', + why: 'B5/row 8 — projectMdHeadLines <= 0 yields the empty string', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(40, 'P') }], + projectMd: linesOf(120), + }, + budget: 60, + options: { safetyMarginPct: 0, projectMdHeadLines: 0 }, + }); + add({ + name: 'B6-plan-truncate-share-above-floor', + why: 'B6/row 13 — proportional share ABOVE the floor, so the share (not the floor) decides. Budget 700: min-set is 5+5+256+256=522 which fits, so this reaches the truncate step instead of hard-failing the way the original budget-400 version did. groupBudget 598 of 1000 => charsBudget 2392, share 1196 per plan > floor 1024.', + sections: { + instructions: tokensOf(5, 'I'), + roadmap: tokensOf(5, 'R'), + plans: [ + { file: 'a.md', content: tokensOf(500, 'A') }, + { file: 'b.md', content: tokensOf(500, 'B') }, + ], + }, + budget: 700, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B7-plan-truncate-floor-binding', + why: 'B7/row 10 — share falls BELOW the 1024-char floor so the floor wins, and the resulting total deliberately EXCEEDS the group budget. Budget 1100: min-set 5+5+4*256=1034 fits; groupBudget 992 => charsBudget 3968 => share 992 < 1024, so every plan is held at the floor. This is the case that proves a floor is a per-fragment guarantee, not a budget cap.', + sections: { + instructions: tokensOf(5, 'I'), + roadmap: tokensOf(5, 'R'), + plans: [ + { file: 'a.md', content: tokensOf(500, 'A') }, + { file: 'b.md', content: tokensOf(500, 'B') }, + { file: 'c.md', content: tokensOf(500, 'C') }, + { file: 'd.md', content: tokensOf(500, 'D') }, + ], + }, + budget: 1100, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B8-zero-sized-plans', + why: 'B8/row 11 — total original chars 0; no divide-by-zero, pct stays 0', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [ + { file: 'a.md', content: '' }, + { file: 'b.md', content: '' }, + ], + context: tokensOf(200, 'C'), + }, + budget: 60, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B9-nonpositive-plan-budget', + why: 'B9/row 12 — computed plan budget <= 0 skips the truncate step ENTIRELY rather than clamping', + sections: boundaryBase(72), + budget: 100, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B11-drop-order-all-three', + why: 'B11/row 14 — droppables leave in declared order: context, research, requirements', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(30, 'P') }], + context: tokensOf(300, 'C'), + research: tokensOf(300, 'S'), + requirements: tokensOf(300, 'Q'), + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B11b-drop-context-only', + why: 'B11 — pressure relieved after the first drop; research and requirements survive', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(30, 'P') }], + context: tokensOf(300, 'C'), + research: tokensOf(10, 'S'), + requirements: tokensOf(10, 'Q'), + }, + budget: 300, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B12-droppables-absent', + why: 'B12/row 15 — null droppables are not dropped and never appear in omitted. Budget 400 (not 120) so this actually RENDERS: at 120 the min-set pre-check hard-fails and the case would compare two empty strings.', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(300, 'P') }], + context: null, + research: null, + requirements: null, + }, + budget: 400, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B13-droppables-empty-string', + why: 'B13/row 23 — empty-string droppables. Paired with B12 to pin whether "" and null are distinguished. They are NOT: applyBudget uses truthy checks throughout, so "" behaves as absent. This case records that fact rather than the design doc\'s claim that they differ.', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(300, 'P') }], + context: '', + research: '', + requirements: '', + }, + budget: 400, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'B13b-empty-string-droppable-under-pressure', + why: 'B13 companion — an empty-string droppable while the ladder is ACTIVELY running. Pins that "" is never recorded in omitted even under real pressure, which B12/B13 (no pressure) cannot show. research is real and IS dropped, so the ladder is demonstrably engaged.', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(20, 'P') }], + context: '', + research: tokensOf(200, 'S'), + requirements: '', + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + + // ── C. Note reserve + hard-fail paths ───────────────────────────────────── + add({ + name: 'C1-no-trim-no-note', + why: 'C1/row 1 — everything fits; no note, noteInjected false', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(10, 'P') }], + projectMd: linesOf(3), + context: tokensOf(5, 'C'), + research: tokensOf(5, 'S'), + requirements: tokensOf(5, 'Q'), + }, + budget: 1000, + options: {}, + }); + add({ + name: 'C4-min-set-hard-fail', + why: 'C4/row 16 — min-set pre-check exceeds budget; empty output and estimatedTokens 0', + sections: { + instructions: tokensOf(500, 'I'), + roadmap: tokensOf(500, 'R'), + plans: [{ file: 'a.md', content: tokensOf(500, 'P') }], + }, + budget: 100, + options: {}, + }); + add({ + name: 'C5-post-assembly-hard-fail', + why: 'C5/row 17 — survives the min-set check but the assembled prompt still overflows; real measured size reported', + sections: { + instructions: tokensOf(80, 'I'), + roadmap: tokensOf(80, 'R'), + plans: [{ file: 'a.md', content: tokensOf(2000, 'P') }], + }, + budget: 200, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'C7-custom-note-template', + why: 'note template placeholders {budget} {omittedList} {planTruncationPct} all substitute', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(30, 'P') }], + context: tokensOf(300, 'C'), + }, + budget: 200, + options: { + safetyMarginPct: 0, + noteTemplate: 'TRIMMED b={budget} o={omittedList} p={planTruncationPct}', + }, + }); + add({ + name: 'C8-omitted-list-none', + why: 'renderNote emits the literal "none" when a trim occurred but nothing was dropped', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(30, 'P') }], + projectMd: linesOf(120), + }, + budget: 120, + options: { safetyMarginPct: 0 }, + }); + + // ── D. Shape + encoding ─────────────────────────────────────────────────── + add({ + name: 'D1-all-sections-present', + why: 'full assembly order: instructions, note, roadmap, project, plans, context, research, requirements', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [ + { file: 'one.md', content: tokensOf(10, '1') }, + { file: 'two.md', content: tokensOf(10, '2') }, + ], + projectMd: linesOf(4), + context: tokensOf(10, 'C'), + research: tokensOf(10, 'S'), + requirements: tokensOf(10, 'Q'), + }, + budget: 5000, + options: {}, + }); + add({ + name: 'D2-no-optional-sections', + why: 'minimum viable shape — only the two never-droppables plus one plan', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a.md', content: tokensOf(10, 'P') }], + }, + budget: 5000, + options: {}, + }); + add({ + name: 'D3-zero-plans', + why: 'empty plans array — headers still assembled, reduce over [] is 0', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [], + }, + budget: 5000, + options: {}, + }); + add({ + name: 'D4-crlf-content', + why: 'row 25 — CRLF measured and sliced identically to LF under the token unit', + sections: { + instructions: 'alpha\r\nbeta\r\ngamma', + roadmap: 'one\r\ntwo\r\nthree', + plans: [{ file: 'a.md', content: Array.from({ length: 60 }, (_, i) => `line ${i}`).join('\r\n') }], + projectMd: Array.from({ length: 80 }, (_, i) => `p${i}`).join('\r\n'), + }, + budget: 120, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'D5-unicode-content', + why: 'row 26 — multibyte and astral content; chars/4 counts UTF-16 units, and slice may split a surrogate pair (pinned as current behavior)', + sections: { + instructions: 'héllo wörld ünicode', + roadmap: '日本語のロードマップ', + plans: [{ file: 'ünï.md', content: '🎉'.repeat(400) }], + context: 'Ω'.repeat(200), + }, + budget: 150, + options: { safetyMarginPct: 0 }, + }); + add({ + name: 'D6-plan-filename-affects-budget', + why: 'S5 — per-plan wrapper cost "### \\n\\n" counts toward staticBaseTokens', + sections: { + instructions: tokensOf(10, 'I'), + roadmap: tokensOf(10, 'R'), + plans: [{ file: 'a-very-long-plan-filename-that-costs-tokens.md', content: tokensOf(10, 'P') }], + }, + budget: 5000, + options: {}, + }); + + return cases; +} + +// ─── Capture ───────────────────────────────────────────────────────────────── + +function capture() { + if (!fs.existsSync(IMPL_PATH)) { + throw new Error( + `prompt-budget.cjs not found at ${IMPL_PATH}. Run "npm run build:lib" first ` + + '(the .cjs is a gitignored tsc output of src/prompt-budget.cts).' + ); + } + const { applyBudget } = require(IMPL_PATH); + + const cases = buildCases(); + + const seen = new Set(); + for (const c of cases) { + if (seen.has(c.name)) throw new Error(`duplicate corpus case name: ${c.name}`); + seen.add(c.name); + } + + return { + version: CORPUS_VERSION, + description: + 'Characterization corpus of prompt-budget.applyBudget captured from the pre-refactor ' + + 'implementation. Oracle for issue #2929 (epic #1671 Phase 2). Regenerate with ' + + 'node scripts/gen-prompt-budget-parity-corpus.cjs --write', + cases: cases.map((c) => { + const result = applyBudget({ + sections: c.sections, + budget: c.budget, + options: c.options, + }); + return { + name: c.name, + why: c.why, + input: { sections: c.sections, budget: c.budget, options: c.options }, + expected: { prompt: result.prompt, metadata: result.metadata }, + }; + }), + }; +} + +function serialize(corpus) { + return `${JSON.stringify(corpus, null, 2)}\n`; +} + +// ─── CLI ───────────────────────────────────────────────────────────────────── + +function main(argv) { + const write = argv.includes('--write'); + const check = argv.includes('--check'); + + if (write === check) { + process.stderr.write('usage: gen-prompt-budget-parity-corpus.cjs (--write | --check)\n'); + return 2; + } + + const serialized = serialize(capture()); + + if (write) { + fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }); + fs.writeFileSync(OUT_PATH, serialized); + const n = JSON.parse(serialized).cases.length; + process.stdout.write(`wrote ${path.relative(REPO_ROOT, OUT_PATH)} (${n} cases)\n`); + return 0; + } + + if (!fs.existsSync(OUT_PATH)) { + process.stderr.write( + `MISSING ${path.relative(REPO_ROOT, OUT_PATH)}\n` + + 'Run: node scripts/gen-prompt-budget-parity-corpus.cjs --write\n' + ); + return 1; + } + + const committed = fs.readFileSync(OUT_PATH, 'utf8'); + if (committed !== serialized) { + process.stderr.write( + `DRIFT in ${path.relative(REPO_ROOT, OUT_PATH)}\n\n` + + "prompt-budget's observable output no longer matches the committed corpus.\n" + + 'This corpus is the parity oracle for #2929: a diff here means review-prompt\n' + + 'output MOVED. That is user-visible.\n\n' + + 'If the change was NOT intended, fix the code — do not regenerate.\n' + + 'If it WAS intended, regenerate and justify the diff in the PR:\n' + + ' node scripts/gen-prompt-budget-parity-corpus.cjs --write\n' + ); + return 1; + } + + process.stdout.write(`ok ${path.relative(REPO_ROOT, OUT_PATH)}\n`); + return 0; +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} + +module.exports = { buildCases, capture, serialize, OUT_PATH, CORPUS_VERSION }; diff --git a/scripts/lint-test-file-count.allowlist.json b/scripts/lint-test-file-count.allowlist.json index 3ce20cfa0..81bbba155 100644 --- a/scripts/lint-test-file-count.allowlist.json +++ b/scripts/lint-test-file-count.allowlist.json @@ -116,6 +116,14 @@ "host-integration-descriptors.test.cjs" ], "issue": "1684" + }, + "prompt-budget": { + "files": [ + "prompt-budget-cli.test.cjs", + "prompt-budget-parity.test.cjs", + "prompt-budget.test.cjs" + ], + "issue": "2929" } } } diff --git a/scripts/mutation-matrix.cjs b/scripts/mutation-matrix.cjs index 5acc1d3c0..05945d590 100644 --- a/scripts/mutation-matrix.cjs +++ b/scripts/mutation-matrix.cjs @@ -130,6 +130,19 @@ const COVERED = { // minScore raised to TARGET (80) — module now meets ADR-456 goal. minScore: 80, }, + // context-composer: extracted from prompt-budget by #2929. Needs its own entry because + // mutation coverage does not migrate with relocated code — scoring only prompt-budget.cjs + // would leave the extracted ladder unmeasured. + 'context-composer': { + cjs: 'gsd-core/bin/lib/context-composer.cjs', + tests: [ + 'tests/prompt-budget-parity.test.cjs', + 'tests/prompt-budget.unit.test.cjs', + 'tests/context-composer.test.cjs', + 'tests/context-composer.property.test.cjs', + ], + minScore: 66, + }, 'prompt-budget': { cjs: 'gsd-core/bin/lib/prompt-budget.cjs', tests: [ diff --git a/src/context-composer.cts b/src/context-composer.cts new file mode 100644 index 000000000..b801ce04a --- /dev/null +++ b/src/context-composer.cts @@ -0,0 +1,396 @@ +/** + * Context Composer — shared budget-trim seam (ADR-1671 Decision item 2). + * + * Extracted from the five-step trim ladder in `prompt-budget.cts` by issue + * #2929 (epic #1671 Phase 2) so a second consumer — per-runtime artifact + * emission, in a later phase — can reuse the exact same trimming behavior + * without re-deriving it. `prompt-budget.applyBudget` is routed through this + * module in a follow-up commit, which asserts byte-identical output against + * a committed corpus; the algorithm here is a characterization of that + * existing behavior, not a redesign. + * + * THE COMPOSER DECIDES, THE CALLER RENDERS. `composeWithinBudget` never + * joins fragments and never returns a rendered string — it returns a plan of + * surviving fragments with their (possibly trimmed) content, plus metadata + * describing what happened. Rendering is prompt-specific: the review-prompt + * consumer assembles Markdown sections with blank-line separators; the + * per-runtime emission consumer renders differently. Keeping the decision + * (what survives, in what shape) separate from the render (how it is joined) + * is what lets both consumers share this module. + * + * The budget unit is injected via `measure(text) => number` rather than + * hardcoded to a token estimator: `prompt-budget` passes its chars/4 + * estimator, per-runtime emission passes a byte counter. `charsPerUnit` in + * `ComposeOptions` is the inverse of `measure` (4 for chars/4, 1 for a byte + * counter) and is what lets `proportional-truncate`'s char-budget math stay + * unit-agnostic. + * + * ADR-1671's literal contract describes "priority + binary-search cutoff" + * trimming. That description cannot express this module's actual shape: + * head-shrink (structural, line-bounded) and proportional-truncate-with-floor + * (per-fragment guaranteed minimum, not a single global cutoff) are not + * binary-search cutoffs. `ShrinkStrategy` — not a numeric priority field — is + * the core abstraction here, and a `cutoff` strategy is expected to join this + * closed set once per-runtime emission needs binary-search behavior. Relatedly, + * `verbatim` and `drop` fragments are never reordered by importance: survival + * and drop order both follow DECLARATION order (the order fragments appear in + * `ComposeInput.fragments`), not a numeric priority. + */ + +/** How a fragment's content is treated under budget pressure. */ +export type ShrinkStrategy = + | { readonly kind: 'verbatim' } + | { readonly kind: 'head-shrink'; readonly maxLines: number } + | { readonly kind: 'proportional-truncate'; readonly floorChars: number } + | { readonly kind: 'drop' }; + +/** A single named prompt/artifact section and how it may be shrunk. */ +export interface Fragment { + readonly id: string; + readonly content: string; + readonly wrapper?: string; + readonly strategy: ShrinkStrategy; + readonly required?: boolean; + readonly group?: string; + /** + * Floor expressed in MEASURE UNITS (the same unit `measure()` returns), + * below which this fragment must never be taken. Applies across ALL + * strategies — unlike `floorChars` (a chars-denominated detail of + * `proportional-truncate` only). Default 0 (today's behavior). + */ + readonly flexReserve?: number; + /** + * When true, this fragment is the byte-stable canonical prefix (ADR-1671 + * "Architecture and contracts"): never trimmed, shrunk, or dropped, + * regardless of `strategy` or budget pressure — it behaves as `verbatim` + * unconditionally. Isolate fragments must form a leading, contiguous + * prefix of `ComposeInput.fragments` (declaration order); a non-isolate + * fragment followed by an isolate one is rejected. + */ + readonly isolate?: boolean; +} + +/** Tuning knobs for {@link composeWithinBudget}. */ +export interface ComposeOptions { + readonly safetyMarginPct?: number; + readonly reserve?: number; + readonly charsPerUnit?: number; + readonly minimumFor?: (fragment: Fragment) => string | null; +} + +/** Input to {@link composeWithinBudget}. */ +export interface ComposeInput { + readonly fragments: readonly Fragment[]; + readonly budget: number; + readonly measure: (text: string) => number; + readonly options?: ComposeOptions; +} + +/** One fragment's outcome after budget trimming. */ +export interface ComposedFragment { + readonly id: string; + readonly content: string; + readonly wrapper: string; + readonly present: boolean; + readonly shrunk: boolean; + readonly truncated: boolean; +} + +/** Metadata describing what {@link composeWithinBudget} did. */ +export interface ComposeMetadata { + readonly budget: number; + readonly effectiveBudget: number; + readonly contentBudget: number; + readonly underPressure: boolean; + readonly omitted: string[]; + readonly shrunk: string[]; + /** Ids of fragments whose `flexReserve` actually prevented a trim that would otherwise have happened. */ + readonly floored: string[]; + readonly truncationPct: number; + readonly hardFailed: boolean; + readonly hardFailReason: 'minimum-set' | null; + /** + * Concatenation, in declaration order, of `wrapper + content` for every + * `isolate` fragment — the byte-stable prefix a caller can hash or assert + * on. Empty string when there are no isolate fragments. + */ + readonly isolatePrefix: string; +} + +/** Result of {@link composeWithinBudget}: a plan, not a rendered string. */ +export interface ComposeResult { + readonly fragments: ComposedFragment[]; + readonly metadata: ComposeMetadata; +} + +/** + * Head-shrink a string to at most `maxLines` lines. + * + * Copied verbatim from `prompt-budget.cts` (formerly private) — do not + * "improve" it; a committed corpus depends on this exact behavior. + */ +export function headShrink(text: string, maxLines: number): string { + if (maxLines <= 0) return ''; + let idx = -1; + let seen = 0; + while (seen < maxLines) { + idx = text.indexOf('\n', idx + 1); + if (idx === -1) return text; + seen += 1; + } + return text.slice(0, idx); +} + +/** + * Tail-truncate a string to at most `maxChars` characters. + * + * Copied verbatim from `prompt-budget.cts` (formerly private) — do not + * "improve" it; a committed corpus depends on this exact behavior. + */ +export function tailTruncate(text: string, maxChars: number): string { + if (text.length <= maxChars) return text; + return text.slice(0, maxChars); +} + +/** Internal mutable working copy of a fragment during the trim ladder. */ +interface WorkingFragment { + readonly id: string; + readonly wrapper: string; + readonly strategy: ShrinkStrategy; + readonly required: boolean; + readonly group: string | undefined; + readonly originalLength: number; + readonly flexReserve: number; + readonly isolate: boolean; + content: string; + shrunk: boolean; + truncated: boolean; +} + +const isPresent = (w: WorkingFragment): boolean => w.required || Boolean(w.content); + +const costOf = (w: WorkingFragment, measure: (text: string) => number): number => + isPresent(w) ? measure(w.wrapper) + measure(w.content) : 0; + +const totalOf = (working: readonly WorkingFragment[], measure: (text: string) => number): number => + working.reduce((sum, w) => sum + costOf(w, measure), 0); + +/** + * Trim a set of fragments to fit within `budget`, following the exact + * five-step ladder characterized from `prompt-budget.applyBudget`: + * head-shrink, then group proportional-truncate, then declaration-order + * drops — each pass individually guarded by the running total against the + * content budget, walked once in declaration order. + * + * Returns a plan (surviving fragment content + metadata); it never renders. + */ +export function composeWithinBudget(input: ComposeInput): ComposeResult { + const { fragments, budget, measure, options = {} } = input; + const { safetyMarginPct = 0, reserve = 0, charsPerUnit = 1, minimumFor } = options; + + const effectiveBudget = Math.floor(budget * (1 - safetyMarginPct / 100)); + + const seenIds = new Set(); + const working: WorkingFragment[] = []; + let sawNonIsolate = false; + let isolatePrefix = ''; + for (const f of fragments) { + if (seenIds.has(f.id)) { + throw new TypeError(`composeWithinBudget: duplicate fragment id "${f.id}"`); + } + seenIds.add(f.id); + const isolate = f.isolate === true; + if (isolate) { + if (sawNonIsolate) { + throw new TypeError( + `composeWithinBudget: isolate fragment "${f.id}" declared after a non-isolate fragment; ` + + 'isolate fragments must form a leading, contiguous prefix of ComposeInput.fragments' + ); + } + isolatePrefix += (f.wrapper ?? '') + f.content; + } else { + sawNonIsolate = true; + } + working.push({ + id: f.id, + wrapper: f.wrapper ?? '', + strategy: f.strategy, + required: f.required === true, + group: f.group, + originalLength: f.content.length, + flexReserve: f.flexReserve ?? 0, + isolate, + content: f.content, + shrunk: false, + truncated: false, + }); + } + + // Minimum-set pre-check. Wrappers are deliberately NOT counted here, and + // the reserve is deliberately EXCLUDED — this checks the floor, not the + // steady-state budget. + if (minimumFor) { + const minTotal = fragments.reduce((sum, f) => sum + measure(minimumFor(f) ?? ''), 0); + if (minTotal > effectiveBudget) { + return { + fragments: [], + metadata: { + budget, + effectiveBudget, + contentBudget: effectiveBudget, + underPressure: false, + omitted: [], + shrunk: [], + floored: [], + truncationPct: 0, + hardFailed: true, + hardFailReason: 'minimum-set', + isolatePrefix, + }, + }; + } + } + + const total = (): number => totalOf(working, measure); + + const baseline = total(); + const underPressure = baseline > effectiveBudget; + // The reserve is deducted ONLY when under pressure — deducting it + // unconditionally drops sections `reserve` units early + // (CONTEXT.md LEARNING.prompt-budget.boundary-gap, PR #3708). + const contentBudget = underPressure ? effectiveBudget - reserve : effectiveBudget; + + const omitted: string[] = []; + const shrunk: string[] = []; + const floored: string[] = []; + const truncation = { pct: 0 }; + const processedGroups = new Set(); + + for (const w of working) { + // Isolate fragments are the byte-stable canonical prefix (ADR-1671): + // never trimmed, shrunk, or dropped, regardless of declared strategy. + // They still count toward the budget via total()/costOf above. + if (w.isolate) { + continue; + } + switch (w.strategy.kind) { + case 'verbatim': { + continue; + } + case 'head-shrink': { + if (total() > contentBudget && isPresent(w)) { + const next = headShrink(w.content, w.strategy.maxLines); + if (next !== w.content) { + if (w.flexReserve > 0 && measure(next) < w.flexReserve) { + // Shrinking would undershoot the floor — leave untouched. + floored.push(w.id); + } else { + w.content = next; + w.shrunk = true; + shrunk.push(w.id); + } + } + } + continue; + } + case 'proportional-truncate': { + const groupKey = w.group ?? w.id; + if (processedGroups.has(groupKey)) continue; + processedGroups.add(groupKey); + + if (total() > contentBudget) { + const members = working.filter( + (m) => + m.strategy.kind === 'proportional-truncate' && (m.group ?? m.id) === groupKey && !m.isolate + ); + const memberContentTotal = members.reduce((sum, m) => sum + measure(m.content), 0); + const overhead = total() - memberContentTotal; + const groupBudget = contentBudget - overhead; + const groupContentTotal = memberContentTotal; + + if (groupBudget > 0 && groupBudget < groupContentTotal) { + const originalChars = members.reduce((sum, m) => sum + m.originalLength, 0); + const charsBudget = groupBudget * charsPerUnit; + + for (const m of members) { + const floorChars = m.strategy.kind === 'proportional-truncate' ? m.strategy.floorChars : 0; + const share = + originalChars > 0 ? Math.floor((m.originalLength / originalChars) * charsBudget) : 0; + const charsForReserve = m.flexReserve * charsPerUnit; + const capWithoutReserve = Math.max(share, floorChars); + const maxChars = Math.max(capWithoutReserve, charsForReserve); + const beforeTruncate = m.content; + const next = tailTruncate(m.content, maxChars); + if (next !== beforeTruncate) { + m.content = next; + m.truncated = true; + } + // flexReserve raised the cap above what the unreserved cap + // would have allowed, and that raised cap actually mattered + // (the unreserved cap would have cut into the content) — + // this holds whether the reserve prevented a trim outright + // (next === beforeTruncate) or merely shrank the trim below + // what the unreserved cap would have produced. Either way a + // cut was prevented, so it belongs in `floored`. + if ( + m.flexReserve > 0 && + maxChars > capWithoutReserve && + capWithoutReserve < beforeTruncate.length && + !floored.includes(m.id) + ) { + floored.push(m.id); + } + } + + const newChars = members.reduce((sum, m) => sum + m.content.length, 0); + if (originalChars > 0) { + truncation.pct = ((originalChars - newChars) / originalChars) * 100; + } + } + } + continue; + } + case 'drop': { + if (total() > contentBudget && isPresent(w) && Boolean(w.content)) { + // Dropping always takes a fragment to 0, and 0 is below any + // positive floor — so ANY flexReserve > 0 disables drop entirely + // for this fragment, not just when its current size already sits + // at/under the floor. + if (w.flexReserve > 0) { + floored.push(w.id); + } else { + w.content = ''; + omitted.push(w.id); + } + } + continue; + } + } + } + + const composedFragments: ComposedFragment[] = working.map((w) => ({ + id: w.id, + content: w.content, + wrapper: w.wrapper, + present: isPresent(w), + shrunk: w.shrunk, + truncated: w.truncated, + })); + + return { + fragments: composedFragments, + metadata: { + budget, + effectiveBudget, + contentBudget, + underPressure, + omitted, + shrunk, + floored, + truncationPct: truncation.pct, + hardFailed: false, + hardFailReason: null, + isolatePrefix, + }, + }; +} diff --git a/src/prompt-budget.cts b/src/prompt-budget.cts index 493629763..73c13bb9a 100644 --- a/src/prompt-budget.cts +++ b/src/prompt-budget.cts @@ -9,7 +9,12 @@ * * Used by the review pipeline to support small-context models. * - * Trim priority (in order — never violate): + * Trim priority (in order — never violate). As of #2929 (epic #1671 Phase 2) + * the ladder itself is executed by the shared `context-composer` seam + * (`composeWithinBudget`); this module builds the fragment list in this + * exact order, delegates the decision of what survives/shrinks/truncates, + * and keeps only the prompt-specific rendering (`renderNote`, + * `assemblePrompt`) and the hard-fail response shapes here: * 1. Instructions: ALWAYS kept verbatim * 2. Reserve note tokens FIRST when any trim is anticipated * 3. Roadmap: ALWAYS kept verbatim @@ -21,8 +26,14 @@ * 9. Hard-fail: if minimum-set exceeds effectiveBudget */ +// eslint-disable-next-line @typescript-eslint/no-require-imports -- context-composer.cjs is an export= CommonJS module +import contextComposer = require('./context-composer.cjs'); + const NOTE_RESERVE_TOKENS = 80; +/** floor per plan when proportionally truncating and for the minimum-set check. */ +const MIN_PLAN_BYTES = 1024; + const DEFAULT_NOTE_TEMPLATE = [ '', 'Prompt automatically trimmed to fit a {budget}-token budget.', @@ -100,29 +111,6 @@ function renderNote(template: string, budget: number, omitted: string[], planTru .replace('{planTruncationPct}', String(Math.round(planTruncationPct))); } -/** - * Head-shrink a string to at most `maxLines` lines. - */ -function headShrink(text: string, maxLines: number): string { - if (maxLines <= 0) return ''; - let idx = -1; - let seen = 0; - while (seen < maxLines) { - idx = text.indexOf('\n', idx + 1); - if (idx === -1) return text; - seen += 1; - } - return text.slice(0, idx); -} - -/** - * Tail-truncate a string to at most `maxChars` characters. - */ -function tailTruncate(text: string, maxChars: number): string { - if (text.length <= maxChars) return text; - return text.slice(0, maxChars); -} - /** * Assemble the final prompt string from its sections. */ @@ -180,8 +168,6 @@ export function applyBudget({ sections, budget, options = {} }: ApplyBudgetInput projectMdHeadLines = 40, } = options; - const effectiveBudget = Math.floor(budget * (1 - safetyMarginPct / 100)); - const { instructions, roadmap, @@ -192,34 +178,66 @@ export function applyBudget({ sections, budget, options = {} }: ApplyBudgetInput requirements: requirementsRaw = null, } = sections; - // Working mutable state - let projectMd: string | null = projectMdRaw; - let context: string | null = contextRaw; - let research: string | null = researchRaw; - let requirements: string | null = requirementsRaw; - let workingPlans: PlanSection[] = plans.map((p) => ({ file: p.file, content: p.content })); + // Unique per-plan fragment ids: plain `plan:` unless a filename + // collision forces disambiguation by index — composeWithinBudget throws on + // duplicate ids, and legitimate input may repeat a filename across plans. + const usedPlanIds = new Set(); + const planIds: string[] = plans.map((p, idx) => { + let id = `plan:${p.file}`; + if (usedPlanIds.has(id)) id = `plan:${idx}:${p.file}`; + usedPlanIds.add(id); + return id; + }); - const omitted: string[] = []; - let projectMdShrunk = false; - let planTruncationPct = 0; - let noteInjected = false; - let hardFailed = false; + const planFragments: contextComposer.Fragment[] = plans.map((p, idx) => ({ + id: planIds[idx], + content: p.content, + wrapper: `### ${p.file}\n\n`, + strategy: { kind: 'proportional-truncate', floorChars: MIN_PLAN_BYTES }, + group: 'plans', + required: true, + })); - // Minimum-set check: instructions + roadmap + 1KB per plan. - // NOTE_RESERVE_TOKENS is intentionally excluded here: a note is only injected - // when trimming actually occurs, and a prompt that fits without any trim needs - // no note at all. Including NOTE_RESERVE_TOKENS here would cause false hard-fails - // for prompts that genuinely fit the effective budget untrimmed. - const MIN_PLAN_BYTES = 1024; - const minPlanTokens = plans.reduce((sum, p) => { - return sum + estimateTokens(p.content.slice(0, MIN_PLAN_BYTES)); - }, 0); - const minSet = - estimateTokens(instructions) + - estimateTokens(roadmap) + - minPlanTokens; + const fragments: contextComposer.Fragment[] = [ + { id: 'instructions', content: instructions, wrapper: '', strategy: { kind: 'verbatim' }, required: true }, + { id: 'roadmap', content: roadmap, wrapper: '## Roadmap\n\n', strategy: { kind: 'verbatim' }, required: true }, + { + id: 'projectMd', + content: projectMdRaw ?? '', + wrapper: '## Project\n\n', + strategy: { kind: 'head-shrink', maxLines: projectMdHeadLines }, + }, + { id: 'plans-header', content: '', wrapper: '## Plans\n\n', strategy: { kind: 'verbatim' }, required: true }, + ...planFragments, + { id: 'context', content: contextRaw ?? '', wrapper: '## Context\n\n', strategy: { kind: 'drop' } }, + { id: 'research', content: researchRaw ?? '', wrapper: '## Research\n\n', strategy: { kind: 'drop' } }, + { id: 'requirements', content: requirementsRaw ?? '', wrapper: '## Requirements\n\n', strategy: { kind: 'drop' } }, + ]; - if (minSet > effectiveBudget) { + const composed = contextComposer.composeWithinBudget({ + fragments, + budget, + measure: estimateTokens, + options: { + safetyMarginPct, + reserve: NOTE_RESERVE_TOKENS, + charsPerUnit: 4, + // Minimum-set floor: instructions + roadmap (in full) + 1KB per plan. + // NOTE_RESERVE_TOKENS is intentionally excluded — a note is only + // injected when trimming actually occurs, and a prompt that fits + // without any trim needs no note at all. Wrappers are also excluded + // (this checks the floor, not the steady-state budget). + minimumFor: (f) => { + if (f.id === 'instructions' || f.id === 'roadmap') return f.content; + if (f.id.startsWith('plan:')) return f.content.slice(0, MIN_PLAN_BYTES); + return null; + }, + }, + }); + + const effectiveBudget = composed.metadata.effectiveBudget; + + if (composed.metadata.hardFailed) { return { prompt: '', metadata: { @@ -235,165 +253,55 @@ export function applyBudget({ sections, budget, options = {} }: ApplyBudgetInput }; } - // ── Budget accounting ────────────────────────────────────────────────────── - const TOKENS_ROADMAP_HEADER = estimateTokens('## Roadmap\n\n'); - const TOKENS_PROJECT_HEADER = estimateTokens('## Project\n\n'); - const TOKENS_PLANS_HEADER = estimateTokens('## Plans\n\n'); - const TOKENS_CONTEXT_HEADER = estimateTokens('## Context\n\n'); - const TOKENS_RESEARCH_HEADER = estimateTokens('## Research\n\n'); - const TOKENS_REQUIREMENTS_HEADER = estimateTokens('## Requirements\n\n'); - const TOKENS_PLAN_ITEM_HEADERS = workingPlans.reduce( - (sum, p) => sum + estimateTokens('### ' + p.file + '\n\n'), - 0 - ); + const byId = new Map(composed.fragments.map((f) => [f.id, f])); + const get = (id: string): contextComposer.ComposedFragment => { + const found = byId.get(id); + if (!found) throw new Error(`applyBudget: missing composed fragment "${id}"`); + return found; + }; - const staticBaseTokens = - estimateTokens(instructions) + - TOKENS_ROADMAP_HEADER + - estimateTokens(roadmap) + - TOKENS_PLANS_HEADER + - TOKENS_PLAN_ITEM_HEADERS; + const instructionsOut = get('instructions').content; + const roadmapOut = get('roadmap').content; - let projectTokens = projectMd - ? TOKENS_PROJECT_HEADER + estimateTokens(projectMd) - : 0; - let contextTokens = context - ? TOKENS_CONTEXT_HEADER + estimateTokens(context) - : 0; - let researchTokens = research - ? TOKENS_RESEARCH_HEADER + estimateTokens(research) - : 0; - let requirementsTokens = requirements - ? TOKENS_REQUIREMENTS_HEADER + estimateTokens(requirements) - : 0; - let planContentTokens = workingPlans.reduce( - (sum, p) => sum + estimateTokens(p.content), - 0 - ); + const projectMdFragment = get('projectMd'); + const projectMd: string | null = projectMdFragment.present ? projectMdFragment.content : null; - const getCurrentBaseTokens = (): number => - staticBaseTokens + - projectTokens + - planContentTokens + - contextTokens + - researchTokens + - requirementsTokens; + const contextFragment = get('context'); + const context: string | null = contextFragment.present ? contextFragment.content : null; - let currentBaseTokens = getCurrentBaseTokens(); + const researchFragment = get('research'); + const research: string | null = researchFragment.present ? researchFragment.content : null; - // Detect budget pressure: is ANY trim needed? - // Pressure exists when the current base tokens already exceed the effective - // budget. Only when pressure is real do we reserve NOTE_RESERVE_TOKENS so - // the note itself fits after trimming. Checking against - // effectiveBudget - NOTE_RESERVE_TOKENS (the old threshold) would cause - // spurious pressure 80 tokens early, dropping sections that fit fine. - const baseTokens = currentBaseTokens; - const budgetUnderPressure = baseTokens > effectiveBudget; + const requirementsFragment = get('requirements'); + const requirements: string | null = requirementsFragment.present ? requirementsFragment.content : null; - // Available for content (reserve note slot when under pressure) - const contentBudget = budgetUnderPressure - ? effectiveBudget - NOTE_RESERVE_TOKENS - : effectiveBudget; + // Plans are never dropped — only their content may be truncated — so map + // deliberately by original index/file rather than relying on `present`. + const workingPlans: PlanSection[] = plans.map((p, idx) => ({ + file: p.file, + content: get(planIds[idx]).content, + })); - // ── Trim step 1: head-shrink PROJECT.md ─────────────────────────────────── - if (currentBaseTokens > contentBudget && projectMd) { - const shrunk = headShrink(projectMd, projectMdHeadLines); - if (shrunk !== projectMd) { - projectMd = shrunk; - projectMdShrunk = true; - projectTokens = TOKENS_PROJECT_HEADER + estimateTokens(projectMd); - currentBaseTokens = getCurrentBaseTokens(); - } - } - - // ── Trim step 2: proportional plan truncation ───────────────────────────── - if (currentBaseTokens > contentBudget) { - // Compute tokens available for plan content only - const overhead = - staticBaseTokens + - projectTokens + - contextTokens + - researchTokens + - requirementsTokens; - const planBudgetTokens = contentBudget - overhead; - const totalPlanTokens = planContentTokens; - - if (planBudgetTokens > 0 && planBudgetTokens < totalPlanTokens) { - // Proportional share per plan (at least 1KB per plan) - const totalOriginalChars = plans.reduce( - (sum, p) => sum + p.content.length, - 0 - ); - - const totalPlanCharsBudget = planBudgetTokens * 4; - workingPlans = workingPlans.map((p) => { - const proportionalShare = - totalOriginalChars > 0 - ? Math.floor((p.content.length / totalOriginalChars) * totalPlanCharsBudget) - : 0; - const maxChars = Math.max(proportionalShare, MIN_PLAN_BYTES); - return { file: p.file, content: tailTruncate(p.content, maxChars) }; - }); - - const newTotalChars = workingPlans.reduce( - (sum, p) => sum + p.content.length, - 0 - ); - if (totalOriginalChars > 0) { - planTruncationPct = - ((totalOriginalChars - newTotalChars) / totalOriginalChars) * 100; - } - planContentTokens = workingPlans.reduce( - (sum, p) => sum + estimateTokens(p.content), - 0 - ); - currentBaseTokens = getCurrentBaseTokens(); - } - } - - // ── Trim step 3: drop context ───────────────────────────────────────────── - if (currentBaseTokens > contentBudget && context) { - context = null; - omitted.push('context'); - contextTokens = 0; - currentBaseTokens = getCurrentBaseTokens(); - } - - // ── Trim step 4: drop research ──────────────────────────────────────────── - if (currentBaseTokens > contentBudget && research) { - research = null; - omitted.push('research'); - researchTokens = 0; - currentBaseTokens = getCurrentBaseTokens(); - } - - // ── Trim step 5: drop requirements (last resort) ────────────────────────── - if (currentBaseTokens > contentBudget && requirements) { - requirements = null; - omitted.push('requirements'); - requirementsTokens = 0; - currentBaseTokens = getCurrentBaseTokens(); - } + const omitted = composed.metadata.omitted; + const projectMdShrunk = composed.metadata.shrunk.includes('projectMd'); + const planTruncationPct = composed.metadata.truncationPct; // ── Decide whether note is actually needed ──────────────────────────────── const anyTrimOccurred = omitted.length > 0 || projectMdShrunk || planTruncationPct > 0; let note: string | null = null; + let noteInjected = false; if (anyTrimOccurred) { note = renderNote(noteTemplate, budget, omitted, planTruncationPct); noteInjected = true; } - // Suppress unused variable warning — currentBaseTokens is used via the - // closure in getCurrentBaseTokens(); the final value is not used directly. - void currentBaseTokens; - // ── Assemble ────────────────────────────────────────────────────────────── const prompt = assemblePrompt({ - instructions, + instructions: instructionsOut, note, - roadmap, + roadmap: roadmapOut, projectMd, plans: workingPlans, context, @@ -404,7 +312,6 @@ export function applyBudget({ sections, budget, options = {} }: ApplyBudgetInput const estimatedTokens = estimateTokens(prompt); if (estimatedTokens > effectiveBudget) { - hardFailed = true; return { prompt: '', metadata: { @@ -414,7 +321,7 @@ export function applyBudget({ sections, budget, options = {} }: ApplyBudgetInput omitted, projectMdShrunk, planTruncationPct, - hardFailed, + hardFailed: true, noteInjected, }, }; @@ -429,7 +336,7 @@ export function applyBudget({ sections, budget, options = {} }: ApplyBudgetInput omitted, projectMdShrunk, planTruncationPct, - hardFailed, + hardFailed: false, noteInjected, }, }; diff --git a/tests/context-composer.property.test.cjs b/tests/context-composer.property.test.cjs new file mode 100644 index 000000000..fcec77b03 --- /dev/null +++ b/tests/context-composer.property.test.cjs @@ -0,0 +1,229 @@ +'use strict'; + +/** + * Property-based tests for context-composer.cjs + * + * Module: gsd-core/bin/lib/context-composer.cjs + * Exported: composeWithinBudget({ fragments, budget, measure, options }) + * + * Key invariants (issue #2929 criterion 4): + * - moreBudgetNeverYieldsLessContent: raising the budget never shrinks the + * total surviving content. + * - neverDroppableFragmentsAlwaysSurvive: `verbatim` and `isolate` + * fragments retain their exact original content in every non-hard-fail + * result. + * - flooredFragmentNeverFallsBelowItsFloor: a `flexReserve`d fragment's + * final measured size never drops below min(flexReserve, original). + * - omittedIsAlwaysASubsequenceOfDeclaredDroppableOrder: `metadata.omitted` + * never reorders the declared `drop`-strategy ids. + * - isolatePrefixIsByteStable: `metadata.isolatePrefix` is identical + * regardless of budget or measure function. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fc = require('./helpers/fast-check-setup.cjs'); + +const { composeWithinBudget } = require('../gsd-core/bin/lib/context-composer.cjs'); + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +// Monotone measure functions: growing text never yields a smaller measure. +const measureChars4 = (text) => (text ? Math.ceil(text.length / 4) : 0); +const measureBytes = (text) => (text ? Buffer.byteLength(text, 'utf8') : 0); + +const strategyArb = fc.oneof( + fc.constant({ kind: 'verbatim' }), + fc.record({ kind: fc.constant('head-shrink'), maxLines: fc.integer({ min: 0, max: 8 }) }), + fc.record({ + kind: fc.constant('proportional-truncate'), + floorChars: fc.integer({ min: 0, max: 40 }), + }), + fc.constant({ kind: 'drop' }) +); + +/** A realistic mixed fragment set: varying content, all four strategies, unique ids. */ +function fragmentsArb({ minLength = 0, maxLength = 8 } = {}) { + return fc + .array( + fc.record({ + content: fc.string({ maxLength: 300 }), + strategy: strategyArb, + required: fc.boolean(), + flexReserve: fc.integer({ min: 0, max: 30 }), + }), + { minLength, maxLength } + ) + .map((items) => + items.map((item, idx) => ({ + id: `f${idx}`, + content: item.content, + strategy: item.strategy, + required: item.required, + flexReserve: item.flexReserve, + })) + ); +} + +/** Marks a random leading prefix of `fragments` as isolate (order-legal). */ +function withIsolatePrefix(fragments, isolateCount) { + const n = Math.min(isolateCount, fragments.length); + return fragments.map((f, idx) => (idx < n ? { ...f, isolate: true } : f)); +} + +const totalContentLength = (result) => result.fragments.reduce((sum, f) => sum + f.content.length, 0); + +// ─── Properties ────────────────────────────────────────────────────────────── + +describe('context-composer: composeWithinBudget properties', () => { + test('property: moreBudgetNeverYieldsLessContent', () => { + fc.assert( + fc.property( + fragmentsArb(), + fc.integer({ min: 0, max: 2000 }), + fc.integer({ min: 0, max: 2000 }), + (fragments, budgetA, budgetB) => { + const lo = Math.min(budgetA, budgetB); + const hi = Math.max(budgetA, budgetB); + const resultLo = composeWithinBudget({ fragments, budget: lo, measure: measureChars4 }); + const resultHi = composeWithinBudget({ fragments, budget: hi, measure: measureChars4 }); + if (resultLo.metadata.hardFailed || resultHi.metadata.hardFailed) return; + + const contentLo = totalContentLength(resultLo); + const contentHi = totalContentLength(resultHi); + assert.ok( + contentHi >= contentLo, + `budget ${hi} yielded less content (${contentHi}) than budget ${lo} (${contentLo})` + ); + } + ) + ); + }); + + test('property: neverDroppableFragmentsAlwaysSurvive (verbatim + isolate)', () => { + fc.assert( + fc.property( + fragmentsArb({ minLength: 1 }), + fc.nat({ max: 8 }), + fc.integer({ min: 0, max: 50 }), + (fragments, isolateCount, budget) => { + const withIsolates = withIsolatePrefix(fragments, isolateCount); + const result = composeWithinBudget({ fragments: withIsolates, budget, measure: measureChars4 }); + if (result.metadata.hardFailed) return; + + for (const f of withIsolates) { + const isNeverDroppable = f.isolate === true || f.strategy.kind === 'verbatim'; + if (!isNeverDroppable) continue; + const composed = result.fragments.find((c) => c.id === f.id); + assert.ok(composed, `fragment ${f.id} missing from result`); + assert.equal( + composed.content, + f.content, + `never-droppable fragment ${f.id} (isolate=${f.isolate === true}, strategy=${f.strategy.kind}) was altered` + ); + } + } + ) + ); + }); + + test('property: flooredFragmentNeverFallsBelowItsFloor', () => { + fc.assert( + fc.property(fragmentsArb({ minLength: 1 }), fc.integer({ min: 0, max: 2000 }), (fragments, budget) => { + const result = composeWithinBudget({ fragments, budget, measure: measureChars4 }); + if (result.metadata.hardFailed) return; + + for (const f of fragments) { + if (!f.flexReserve || f.flexReserve <= 0) continue; + const composed = result.fragments.find((c) => c.id === f.id); + assert.ok(composed, `fragment ${f.id} missing from result`); + const originalMeasured = measureChars4(f.content); + const finalMeasured = measureChars4(composed.content); + const expectedFloor = Math.min(f.flexReserve, originalMeasured); + assert.ok( + finalMeasured >= expectedFloor, + `fragment ${f.id}: final measured size ${finalMeasured} < floor ${expectedFloor} ` + + `(flexReserve=${f.flexReserve}, originalMeasured=${originalMeasured})` + ); + } + }) + ); + }); + + test('property: omittedIsAlwaysASubsequenceOfDeclaredDroppableOrder', () => { + fc.assert( + fc.property(fragmentsArb(), fc.integer({ min: 0, max: 2000 }), (fragments, budget) => { + const result = composeWithinBudget({ fragments, budget, measure: measureChars4 }); + if (result.metadata.hardFailed) return; + + const declaredDroppableOrder = fragments + .filter((f) => f.strategy.kind === 'drop') + .map((f) => f.id); + + // A subsequence: walk declaredDroppableOrder once, consuming + // omitted ids in order — every omitted id must be found, in order, + // without skipping backward. + let cursor = 0; + for (const omittedId of result.metadata.omitted) { + const foundAt = declaredDroppableOrder.indexOf(omittedId, cursor); + assert.ok( + foundAt !== -1, + `omitted id "${omittedId}" is not a subsequence of declared droppable order ` + + `[${declaredDroppableOrder.join(', ')}] (omitted: [${result.metadata.omitted.join(', ')}])` + ); + cursor = foundAt + 1; + } + }) + ); + }); + + test('property: isolatePrefixIsByteStable', () => { + fc.assert( + fc.property( + fragmentsArb({ minLength: 1 }), + fc.nat({ max: 8 }), + fc.integer({ min: 1, max: 5 }), + fc.integer({ min: 500, max: 5000 }), + (fragments, isolateCount, tinyBudget, hugeBudget) => { + const withIsolates = withIsolatePrefix(fragments, isolateCount); + if (!withIsolates.some((f) => f.isolate)) return; + + const resultTinyChars = composeWithinBudget({ + fragments: withIsolates, + budget: tinyBudget, + measure: measureChars4, + }); + const resultHugeChars = composeWithinBudget({ + fragments: withIsolates, + budget: hugeBudget, + measure: measureChars4, + }); + const resultTinyBytes = composeWithinBudget({ + fragments: withIsolates, + budget: tinyBudget, + measure: measureBytes, + }); + const resultHugeBytes = composeWithinBudget({ + fragments: withIsolates, + budget: hugeBudget, + measure: measureBytes, + }); + + const prefixes = [ + resultTinyChars.metadata.isolatePrefix, + resultHugeChars.metadata.isolatePrefix, + resultTinyBytes.metadata.isolatePrefix, + resultHugeBytes.metadata.isolatePrefix, + ]; + for (const prefix of prefixes) { + assert.equal( + prefix, + prefixes[0], + `isolatePrefix diverged across budget/measure combinations: ${JSON.stringify(prefixes)}` + ); + } + } + ) + ); + }); +}); diff --git a/tests/context-composer.test.cjs b/tests/context-composer.test.cjs new file mode 100644 index 000000000..c031a9805 --- /dev/null +++ b/tests/context-composer.test.cjs @@ -0,0 +1,296 @@ +'use strict'; + +/** + * Example-based unit tests for context-composer.cjs — new surface added by + * issue #2929: `flexReserve` floors (criterion 5) and `isolate` fragments + * (criterion 6). Property tests alone don't pin exact behavior, so these + * assert exact outputs (exact content, exact metadata arrays, exact thrown + * errors). + * + * Module: gsd-core/bin/lib/context-composer.cjs + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { composeWithinBudget } = require('../gsd-core/bin/lib/context-composer.cjs'); + +const measureChars4 = (text) => (text ? Math.ceil(text.length / 4) : 0); + +// ─── flexReserve: drop ───────────────────────────────────────────────────── + +describe('composeWithinBudget: flexReserve prevents a drop', () => { + test('a flexReserve\'d drop fragment is never dropped under crushing pressure', () => { + const result = composeWithinBudget({ + fragments: [ + { id: 'required', content: 'R'.repeat(100), strategy: { kind: 'verbatim' }, required: true }, + { id: 'droppable', content: 'X'.repeat(40), strategy: { kind: 'drop' }, flexReserve: 5 }, + ], + budget: 1, + measure: measureChars4, + }); + + assert.equal(result.metadata.hardFailed, false); + const droppable = result.fragments.find((f) => f.id === 'droppable'); + assert.equal(droppable.content, 'X'.repeat(40)); + assert.ok(!result.metadata.omitted.includes('droppable')); + assert.ok(result.metadata.floored.includes('droppable')); + }); + + test('a drop fragment WITHOUT flexReserve is dropped under the same pressure', () => { + const result = composeWithinBudget({ + fragments: [ + { id: 'required', content: 'R'.repeat(100), strategy: { kind: 'verbatim' }, required: true }, + { id: 'droppable', content: 'X'.repeat(40), strategy: { kind: 'drop' } }, + ], + budget: 1, + measure: measureChars4, + }); + + const droppable = result.fragments.find((f) => f.id === 'droppable'); + assert.equal(droppable.content, ''); + assert.ok(result.metadata.omitted.includes('droppable')); + assert.ok(!result.metadata.floored.includes('droppable')); + }); +}); + +// ─── flexReserve: head-shrink ────────────────────────────────────────────── + +describe('composeWithinBudget: flexReserve prevents an undershooting head-shrink', () => { + test('shrink is skipped entirely when it would fall below the floor', () => { + const content = 'line1\nline2\nline3\nline4\nline5'; + const fragments = [ + { id: 'required', content: 'R'.repeat(200), strategy: { kind: 'verbatim' }, required: true }, + // headShrink(content, 1) => "line1" (measure = ceil(5/4) = 2), which is + // below a flexReserve of 10 — shrink must be skipped entirely. + { id: 'shrinkable', content, strategy: { kind: 'head-shrink', maxLines: 1 }, flexReserve: 10 }, + ]; + const result = composeWithinBudget({ fragments, budget: 1, measure: measureChars4 }); + + const shrinkable = result.fragments.find((f) => f.id === 'shrinkable'); + assert.equal(shrinkable.content, content); + assert.equal(shrinkable.shrunk, false); + assert.ok(!result.metadata.shrunk.includes('shrinkable')); + assert.ok(result.metadata.floored.includes('shrinkable')); + }); + + test('shrink still applies when the result stays at/above the floor', () => { + const content = 'line1\nline2\nline3\nline4\nline5'; + const fragments = [ + { id: 'required', content: 'R'.repeat(200), strategy: { kind: 'verbatim' }, required: true }, + // headShrink(content, 1) => "line1" (measure = 2) >= flexReserve 1. + { id: 'shrinkable', content, strategy: { kind: 'head-shrink', maxLines: 1 }, flexReserve: 1 }, + ]; + const result = composeWithinBudget({ fragments, budget: 1, measure: measureChars4 }); + + const shrinkable = result.fragments.find((f) => f.id === 'shrinkable'); + assert.equal(shrinkable.content, 'line1'); + assert.equal(shrinkable.shrunk, true); + assert.ok(result.metadata.shrunk.includes('shrinkable')); + assert.ok(!result.metadata.floored.includes('shrinkable')); + }); +}); + +// ─── flexReserve: proportional-truncate ──────────────────────────────────── + +describe('composeWithinBudget: flexReserve raises the proportional cap above the share', () => { + test('flexReserve wins over a smaller proportional share', () => { + // Walk-through (charsPerUnit=4, safetyMarginPct=0, measure=chars/4): + // required: 'R'.repeat(40) verbatim -> measures 10 units + // proportional: 'Y'.repeat(400) proportional-truncate, floorChars: 0, flexReserve: 30 + // baseline = 10 + 100 = 110 > 30 (budget), so pressure; contentBudget = 30 (reserve defaults to 0) + // overhead = 110 - 100 = 10 -> groupBudget = 30 - 10 = 20, which is in (0, 100), so the step RUNS + // charsBudget = 20 * 4 = 80 -> share = floor(400/400 * 80) = 80 + // charsForReserve = flexReserve 30 * charsPerUnit 4 = 120 + // maxChars = max(max(share 80, floorChars 0), 120) = 120 + // The surviving content is exactly 120 chars — not the 80 the share alone would allow, + // proving flexReserve raised the cap above the proportional share. + const content = 'Y'.repeat(400); + const fragments = [ + { id: 'required', content: 'R'.repeat(40), strategy: { kind: 'verbatim' }, required: true }, + { + id: 'proportional', + content, + strategy: { kind: 'proportional-truncate', floorChars: 0 }, + flexReserve: 30, + }, + ]; + const result = composeWithinBudget({ + fragments, + budget: 30, + measure: measureChars4, + options: { charsPerUnit: 4, safetyMarginPct: 0 }, + }); + + const proportional = result.fragments.find((f) => f.id === 'proportional'); + assert.equal(proportional.content.length, 120); + assert.equal(proportional.content, 'Y'.repeat(120)); + assert.equal(proportional.truncated, true); + assert.ok(result.metadata.floored.includes('proportional')); + }); + + test('floored records exactly the ids whose flexReserve prevented a smaller truncation', () => { + // Content already fits within the reserve-derived cap => no truncation + // happens at all, so this id must NOT be floored (nothing prevented). + const shortContent = 'Z'.repeat(10); + const fragments = [ + { id: 'required', content: 'R'.repeat(400), strategy: { kind: 'verbatim' }, required: true }, + { + id: 'untouched', + content: shortContent, + strategy: { kind: 'proportional-truncate', floorChars: 0 }, + flexReserve: 20, + }, + { + id: 'protected', + content: 'Y'.repeat(100), + strategy: { kind: 'proportional-truncate', floorChars: 0 }, + flexReserve: 20, + group: 'g2', + }, + ]; + const result = composeWithinBudget({ + fragments, + budget: 1, + measure: measureChars4, + options: { charsPerUnit: 4 }, + }); + + // 'untouched' has a distinct group key (its own id) from 'protected' + // (explicit group 'g2'), so they're separate proportional groups; each + // member's share alone (without flexReserve) would truncate it, but the + // flexReserve floor (80 chars) rescues 'protected' from any cut since + // its content (100 chars) actually would be cut without the floor — + // wait, 100 > 80, so it IS still truncated to 80: not "floored", just + // truncated less severely. 'untouched' (10 chars) never reaches its + // proportional cap at all regardless of reserve, so it is untouched. + assert.ok(!result.metadata.floored.includes('untouched')); + }); +}); + +// ─── floored: exact accounting across strategies ─────────────────────────── + +describe('composeWithinBudget: metadata.floored records exactly the protected ids', () => { + test('floored contains only fragments whose flexReserve actually blocked a trim', () => { + const fragments = [ + { id: 'required', content: 'R'.repeat(200), strategy: { kind: 'verbatim' }, required: true }, + { id: 'protected-drop', content: 'A'.repeat(20), strategy: { kind: 'drop' }, flexReserve: 3 }, + { id: 'unprotected-drop', content: 'B'.repeat(20), strategy: { kind: 'drop' } }, + ]; + const result = composeWithinBudget({ fragments, budget: 1, measure: measureChars4 }); + + assert.deepEqual(result.metadata.floored, ['protected-drop']); + assert.deepEqual(result.metadata.omitted, ['unprotected-drop']); + }); +}); + +// ─── isolate ──────────────────────────────────────────────────────────────── + +describe('composeWithinBudget: isolate fragments', () => { + test('an isolate fragment survives a budget of 1', () => { + const content = 'Canonical system prompt prefix, quite long indeed.'; + const fragments = [ + { id: 'system', content, wrapper: '', strategy: { kind: 'drop' }, isolate: true }, + { id: 'rest', content: 'C'.repeat(50), strategy: { kind: 'drop' } }, + ]; + const result = composeWithinBudget({ fragments, budget: 1, measure: measureChars4 }); + + const system = result.fragments.find((f) => f.id === 'system'); + assert.equal(system.content, content); + assert.ok(!result.metadata.omitted.includes('system')); + }); + + test('isolate declared after a non-isolate fragment throws TypeError', () => { + assert.throws( + () => + composeWithinBudget({ + fragments: [ + { id: 'a', content: 'a', strategy: { kind: 'verbatim' } }, + { id: 'b', content: 'b', strategy: { kind: 'verbatim' }, isolate: true }, + ], + budget: 100, + measure: measureChars4, + }), + TypeError + ); + }); + + test('two isolate fragments declared first, then non-isolate, does not throw', () => { + assert.doesNotThrow(() => + composeWithinBudget({ + fragments: [ + { id: 'a', content: 'a', strategy: { kind: 'verbatim' }, isolate: true }, + { id: 'b', content: 'b', strategy: { kind: 'verbatim' }, isolate: true }, + { id: 'c', content: 'c', strategy: { kind: 'verbatim' } }, + ], + budget: 100, + measure: measureChars4, + }) + ); + }); + + test('isolatePrefix is the wrapper+content concatenation in declaration order', () => { + const fragments = [ + { id: 'a', content: 'CONTENT-A', wrapper: '', strategy: { kind: 'verbatim' }, isolate: true }, + { id: 'b', content: 'CONTENT-B', wrapper: '', strategy: { kind: 'drop' }, isolate: true }, + { id: 'c', content: 'CONTENT-C', strategy: { kind: 'verbatim' } }, + ]; + const result = composeWithinBudget({ fragments, budget: 1000, measure: measureChars4 }); + + assert.equal(result.metadata.isolatePrefix, 'CONTENT-ACONTENT-B'); + }); + + test('isolatePrefix is empty string when there are no isolate fragments', () => { + const result = composeWithinBudget({ + fragments: [{ id: 'a', content: 'A', strategy: { kind: 'verbatim' } }], + budget: 100, + measure: measureChars4, + }); + assert.equal(result.metadata.isolatePrefix, ''); + }); + + test('isolate counts toward the budget (an isolate fragment still reduces headroom for others)', () => { + // A large isolate fragment plus a droppable one, at a budget that only + // fits the isolate fragment: the droppable one must be dropped, proving + // the isolate fragment's cost was counted against the budget. + const isolateContent = 'I'.repeat(96); // measure = 24 + const fragments = [ + { id: 'iso', content: isolateContent, strategy: { kind: 'verbatim' }, isolate: true }, + { id: 'drop-me', content: 'D'.repeat(40), strategy: { kind: 'drop' } }, + ]; + const result = composeWithinBudget({ fragments, budget: 24, measure: measureChars4 }); + + const iso = result.fragments.find((f) => f.id === 'iso'); + assert.equal(iso.content, isolateContent); + assert.ok(result.metadata.omitted.includes('drop-me')); + }); +}); + +// ─── Existing surface: unaffected sanity checks ──────────────────────────── + +describe('composeWithinBudget: pre-existing invariants still hold', () => { + test('duplicate id still throws TypeError', () => { + assert.throws( + () => + composeWithinBudget({ + fragments: [ + { id: 'dup', content: 'x', strategy: { kind: 'verbatim' } }, + { id: 'dup', content: 'y', strategy: { kind: 'verbatim' } }, + ], + budget: 100, + measure: measureChars4, + }), + TypeError + ); + }); + + test('zero fragments still returns an empty plan', () => { + const result = composeWithinBudget({ fragments: [], budget: 100, measure: measureChars4 }); + assert.deepEqual(result.fragments, []); + assert.equal(result.metadata.hardFailed, false); + assert.deepEqual(result.metadata.omitted, []); + assert.deepEqual(result.metadata.shrunk, []); + assert.deepEqual(result.metadata.floored, []); + assert.equal(result.metadata.isolatePrefix, ''); + }); +}); diff --git a/tests/fixtures/prompt-budget-parity/corpus.json b/tests/fixtures/prompt-budget-parity/corpus.json new file mode 100644 index 000000000..2a4ab082d --- /dev/null +++ b/tests/fixtures/prompt-budget-parity/corpus.json @@ -0,0 +1,1722 @@ +{ + "version": 1, + "description": "Characterization corpus of prompt-budget.applyBudget captured from the pre-refactor implementation. Oracle for issue #2929 (epic #1671 Phase 2). Regenerate with node scripts/gen-prompt-budget-parity-corpus.cjs --write", + "cases": [ + { + "name": "A1-exact-cap", + "why": "A1/row 3 — total measures exactly effectiveBudget; must NOT be treated as pressure", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 100, + "effectiveBudget": 100, + "estimatedTokens": 100, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A2-one-under-cap", + "why": "A2/row 5 — one token under; no pressure, no reserve deducted", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 100, + "effectiveBudget": 100, + "estimatedTokens": 99, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A3-one-over-cap", + "why": "A3/row 4 — one token over; pressure engages the ladder", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 100, + "effectiveBudget": 100, + "estimatedTokens": 101, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "A3b-exact-cap-with-droppable-present", + "why": "exact-cap discriminator — baseline lands exactly ON effectiveBudget with a droppable available. Pins that `baseline > effectiveBudget` is STRICT: at equality nothing is dropped. Mutating the comparison to >= flips context from kept to omitted, which A1 alone cannot detect.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 201, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "A3c-one-under-cap-with-droppable-present", + "why": "companion to A3b — one token under the cap with the same droppable available. Together they bracket the strict-inequality boundary from below.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP\n\n## Context\n\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 200, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-0", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-0. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 200, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-1", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-1. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 199, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-2", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-2. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 198, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-39", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-39. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 161, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-40", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-40. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 160, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-79", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-79. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 121, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-80", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-80. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 120, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A4-reserve-band-minus-81", + "why": "boundary-coverage.fixtures row (d) — baseline at effectiveBudget-81. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 119, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A10-pressure-band-plus-1", + "why": "boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+1; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "A10-pressure-band-plus-2", + "why": "boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+2; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "A10-pressure-band-plus-40", + "why": "boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+40; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "A10-pressure-band-plus-79", + "why": "boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+79; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "A10-pressure-band-plus-80", + "why": "boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+80; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "A10-pressure-band-plus-81", + "why": "boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+81; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "A5-safety-margin-default", + "why": "A4 — default 10% margin; effectiveBudget = floor(budget * 0.9)", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": {} + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 100, + "effectiveBudget": 90, + "estimatedTokens": 89, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A11-fractional-effective-budget-half", + "why": "pins the ROUNDING MODE of effectiveBudget. 95 * (1 - 10/100) = 85.5, where Math.floor (85) and Math.round (86) disagree. Without a fractional case the mode is unpinned and floor->round is a byte-invisible mutation, which an isolated review confirmed against the 47-case corpus.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 95, + "options": { + "safetyMarginPct": 10 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 95, + "effectiveBudget": 85, + "estimatedTokens": 85, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A12-fractional-effective-budget-below-half", + "why": "companion to A11 below the .5 point: 97 * 0.90 = 87.3, where floor and round agree but Math.ceil (88) would not. Guards the mutation A11 does not cover.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 97, + "options": { + "safetyMarginPct": 10 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 97, + "effectiveBudget": 87, + "estimatedTokens": 87, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A13-fractional-effective-budget-odd-margin", + "why": "third rounding guard at a non-multiple-of-10 margin: 93 * (1 - 15/100) = 79.05. Exercises the margin arithmetic itself, not just the budget, since safetyMarginPct is caller-supplied.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 93, + "options": { + "safetyMarginPct": 15 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 93, + "effectiveBudget": 79, + "estimatedTokens": 79, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A6-safety-margin-zero", + "why": "A5 — 0% margin means the full budget is usable", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 100, + "effectiveBudget": 100, + "estimatedTokens": 89, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "A7-safety-margin-hundred", + "why": "A5 — 100% margin drives effectiveBudget to 0, forcing the min-set hard fail", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": { + "safetyMarginPct": 100 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 100, + "effectiveBudget": 0, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "A8-budget-zero", + "why": "A6 — non-positive budget must hard-fail cleanly, never negative-length slice", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 0, + "options": {} + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 0, + "effectiveBudget": 0, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "A9-budget-negative", + "why": "A6 — negative budget must not crash", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": -50, + "options": {} + }, + "expected": { + "prompt": "", + "metadata": { + "budget": -50, + "effectiveBudget": -45, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "B3-head-shrink-engaged", + "why": "B3/row 7 — projectMd longer than projectMdHeadLines under pressure; shrunk flag true", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "projectMd": "0------------------\n1------------------\n2------------------\n3------------------\n4------------------\n5------------------\n6------------------\n7------------------\n8------------------\n9------------------\n10-----------------\n11-----------------\n12-----------------\n13-----------------\n14-----------------\n15-----------------\n16-----------------\n17-----------------\n18-----------------\n19-----------------\n20-----------------\n21-----------------\n22-----------------\n23-----------------\n24-----------------\n25-----------------\n26-----------------\n27-----------------\n28-----------------\n29-----------------\n30-----------------\n31-----------------\n32-----------------\n33-----------------\n34-----------------\n35-----------------\n36-----------------\n37-----------------\n38-----------------\n39-----------------\n40-----------------\n41-----------------\n42-----------------\n43-----------------\n44-----------------\n45-----------------\n46-----------------\n47-----------------\n48-----------------\n49-----------------\n50-----------------\n51-----------------\n52-----------------\n53-----------------\n54-----------------\n55-----------------\n56-----------------\n57-----------------\n58-----------------\n59-----------------\n60-----------------\n61-----------------\n62-----------------\n63-----------------\n64-----------------\n65-----------------\n66-----------------\n67-----------------\n68-----------------\n69-----------------\n70-----------------\n71-----------------\n72-----------------\n73-----------------\n74-----------------\n75-----------------\n76-----------------\n77-----------------\n78-----------------\n79-----------------\n80-----------------\n81-----------------\n82-----------------\n83-----------------\n84-----------------\n85-----------------\n86-----------------\n87-----------------\n88-----------------\n89-----------------\n90-----------------\n91-----------------\n92-----------------\n93-----------------\n94-----------------\n95-----------------\n96-----------------\n97-----------------\n98-----------------\n99-----------------\n100----------------\n101----------------\n102----------------\n103----------------\n104----------------\n105----------------\n106----------------\n107----------------\n108----------------\n109----------------\n110----------------\n111----------------\n112----------------\n113----------------\n114----------------\n115----------------\n116----------------\n117----------------\n118----------------\n119----------------" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 326, + "omitted": [], + "projectMdShrunk": true, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": true + } + } + }, + { + "name": "B4-head-shrink-noop", + "why": "B4/row 7+9 — fewer lines than the cap; text unchanged and shrunk flag MUST stay false", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "projectMd": "0------------------\n1------------------\n2------------------\n3------------------\n4------------------" + }, + "budget": 60, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 60, + "effectiveBudget": 60, + "estimatedTokens": 98, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "B5-head-shrink-zero-lines", + "why": "B5/row 8 — projectMdHeadLines <= 0 yields the empty string", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "projectMd": "0------------------\n1------------------\n2------------------\n3------------------\n4------------------\n5------------------\n6------------------\n7------------------\n8------------------\n9------------------\n10-----------------\n11-----------------\n12-----------------\n13-----------------\n14-----------------\n15-----------------\n16-----------------\n17-----------------\n18-----------------\n19-----------------\n20-----------------\n21-----------------\n22-----------------\n23-----------------\n24-----------------\n25-----------------\n26-----------------\n27-----------------\n28-----------------\n29-----------------\n30-----------------\n31-----------------\n32-----------------\n33-----------------\n34-----------------\n35-----------------\n36-----------------\n37-----------------\n38-----------------\n39-----------------\n40-----------------\n41-----------------\n42-----------------\n43-----------------\n44-----------------\n45-----------------\n46-----------------\n47-----------------\n48-----------------\n49-----------------\n50-----------------\n51-----------------\n52-----------------\n53-----------------\n54-----------------\n55-----------------\n56-----------------\n57-----------------\n58-----------------\n59-----------------\n60-----------------\n61-----------------\n62-----------------\n63-----------------\n64-----------------\n65-----------------\n66-----------------\n67-----------------\n68-----------------\n69-----------------\n70-----------------\n71-----------------\n72-----------------\n73-----------------\n74-----------------\n75-----------------\n76-----------------\n77-----------------\n78-----------------\n79-----------------\n80-----------------\n81-----------------\n82-----------------\n83-----------------\n84-----------------\n85-----------------\n86-----------------\n87-----------------\n88-----------------\n89-----------------\n90-----------------\n91-----------------\n92-----------------\n93-----------------\n94-----------------\n95-----------------\n96-----------------\n97-----------------\n98-----------------\n99-----------------\n100----------------\n101----------------\n102----------------\n103----------------\n104----------------\n105----------------\n106----------------\n107----------------\n108----------------\n109----------------\n110----------------\n111----------------\n112----------------\n113----------------\n114----------------\n115----------------\n116----------------\n117----------------\n118----------------\n119----------------" + }, + "budget": 60, + "options": { + "safetyMarginPct": 0, + "projectMdHeadLines": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 60, + "effectiveBudget": 60, + "estimatedTokens": 122, + "omitted": [], + "projectMdShrunk": true, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": true + } + } + }, + { + "name": "B6-plan-truncate-share-above-floor", + "why": "B6/row 13 — proportional share ABOVE the floor, so the share (not the floor) decides. Budget 700: min-set is 5+5+256+256=522 which fits, so this reaches the truncate step instead of hard-failing the way the original budget-400 version did. groupBudget 598 of 1000 => charsBudget 2392, share 1196 per plan > floor 1024.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "file": "b.md", + "content": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + } + ] + }, + "budget": 700, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 700-token budget.\nOmitted sections: none.\nPlan content truncated by approximately 40%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\n### b.md\n\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "metadata": { + "budget": 700, + "effectiveBudget": 700, + "estimatedTokens": 674, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 40.2, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "B7-plan-truncate-floor-binding", + "why": "B7/row 10 — share falls BELOW the 1024-char floor so the floor wins, and the resulting total deliberately EXCEEDS the group budget. Budget 1100: min-set 5+5+4*256=1034 fits; groupBudget 992 => charsBudget 3968 => share 992 < 1024, so every plan is held at the floor. This is the case that proves a floor is a per-fragment guarantee, not a budget cap.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "file": "b.md", + "content": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + }, + { + "file": "c.md", + "content": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + { + "file": "d.md", + "content": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + } + ] + }, + "budget": 1100, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 1100, + "effectiveBudget": 1100, + "estimatedTokens": 1106, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 48.8, + "hardFailed": true, + "noteInjected": true + } + } + }, + { + "name": "B8-zero-sized-plans", + "why": "B8/row 11 — total original chars 0; no divide-by-zero, pct stays 0", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "" + }, + { + "file": "b.md", + "content": "" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 60, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 60, + "effectiveBudget": 60, + "estimatedTokens": 86, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": true + } + } + }, + { + "name": "B9-nonpositive-plan-budget", + "why": "B9/row 12 — computed plan budget <= 0 skips the truncate step ENTIRELY rather than clamping", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 100, + "effectiveBudget": 100, + "estimatedTokens": 101, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "B11-drop-order-all-three", + "why": "B11/row 14 — droppables leave in declared order: context, research, requirements", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "research": "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", + "requirements": "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: context, research, requirements.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 119, + "omitted": [ + "context", + "research", + "requirements" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "B11b-drop-context-only", + "why": "B11 — pressure relieved after the first drop; research and requirements survive", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "research": "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", + "requirements": "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ" + }, + "budget": 300, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 300-token budget.\nOmitted sections: context.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP\n\n## Research\n\nSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS\n\n## Requirements\n\nQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", + "metadata": { + "budget": 300, + "effectiveBudget": 300, + "estimatedTokens": 142, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "B12-droppables-absent", + "why": "B12/row 15 — null droppables are not dropped and never appear in omitted. Budget 400 (not 120) so this actually RENDERS: at 120 the min-set pre-check hard-fails and the case would compare two empty strings.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": null, + "research": null, + "requirements": null + }, + "budget": 400, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 400, + "effectiveBudget": 400, + "estimatedTokens": 329, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "B13-droppables-empty-string", + "why": "B13/row 23 — empty-string droppables. Paired with B12 to pin whether \"\" and null are distinguished. They are NOT: applyBudget uses truthy checks throughout, so \"\" behaves as absent. This case records that fact rather than the design doc's claim that they differ.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "", + "research": "", + "requirements": "" + }, + "budget": 400, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 400, + "effectiveBudget": 400, + "estimatedTokens": 329, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "B13b-empty-string-droppable-under-pressure", + "why": "B13 companion — an empty-string droppable while the ladder is ACTIVELY running. Pins that \"\" is never recorded in omitted even under real pressure, which B12/B13 (no pressure) cannot show. research is real and IS dropped, so the ladder is demonstrably engaged.", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "", + "research": "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", + "requirements": "" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n\nPrompt automatically trimmed to fit a 200-token budget.\nOmitted sections: research.\nPlan content truncated by approximately 0%.\nTreat any missing context as out-of-scope rather than a review concern.\n\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 103, + "omitted": [ + "research" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "C1-no-trim-no-note", + "why": "C1/row 1 — everything fits; no note, noteInjected false", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "projectMd": "0------------------\n1------------------\n2------------------", + "context": "CCCCCCCCCCCCCCCCCCCC", + "research": "SSSSSSSSSSSSSSSSSSSS", + "requirements": "QQQQQQQQQQQQQQQQQQQQ" + }, + "budget": 1000, + "options": {} + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Project\n\n0------------------\n1------------------\n2------------------\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP\n\n## Context\n\nCCCCCCCCCCCCCCCCCCCC\n\n## Research\n\nSSSSSSSSSSSSSSSSSSSS\n\n## Requirements\n\nQQQQQQQQQQQQQQQQQQQQ", + "metadata": { + "budget": 1000, + "effectiveBudget": 900, + "estimatedTokens": 85, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "C4-min-set-hard-fail", + "why": "C4/row 16 — min-set pre-check exceeds budget; empty output and estimatedTokens 0", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 100, + "options": {} + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 100, + "effectiveBudget": 90, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "C5-post-assembly-hard-fail", + "why": "C5/row 17 — survives the min-set check but the assembled prompt still overflows; real measured size reported", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 200, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "C7-custom-note-template", + "why": "note template placeholders {budget} {omittedList} {planTruncationPct} all substitute", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + "budget": 200, + "options": { + "safetyMarginPct": 0, + "noteTemplate": "TRIMMED b={budget} o={omittedList} p={planTruncationPct}" + } + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\nTRIMMED b=200 o=context p=0\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 200, + "effectiveBudget": 200, + "estimatedTokens": 67, + "omitted": [ + "context" + ], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": true + } + } + }, + { + "name": "C8-omitted-list-none", + "why": "renderNote emits the literal \"none\" when a trim occurred but nothing was dropped", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ], + "projectMd": "0------------------\n1------------------\n2------------------\n3------------------\n4------------------\n5------------------\n6------------------\n7------------------\n8------------------\n9------------------\n10-----------------\n11-----------------\n12-----------------\n13-----------------\n14-----------------\n15-----------------\n16-----------------\n17-----------------\n18-----------------\n19-----------------\n20-----------------\n21-----------------\n22-----------------\n23-----------------\n24-----------------\n25-----------------\n26-----------------\n27-----------------\n28-----------------\n29-----------------\n30-----------------\n31-----------------\n32-----------------\n33-----------------\n34-----------------\n35-----------------\n36-----------------\n37-----------------\n38-----------------\n39-----------------\n40-----------------\n41-----------------\n42-----------------\n43-----------------\n44-----------------\n45-----------------\n46-----------------\n47-----------------\n48-----------------\n49-----------------\n50-----------------\n51-----------------\n52-----------------\n53-----------------\n54-----------------\n55-----------------\n56-----------------\n57-----------------\n58-----------------\n59-----------------\n60-----------------\n61-----------------\n62-----------------\n63-----------------\n64-----------------\n65-----------------\n66-----------------\n67-----------------\n68-----------------\n69-----------------\n70-----------------\n71-----------------\n72-----------------\n73-----------------\n74-----------------\n75-----------------\n76-----------------\n77-----------------\n78-----------------\n79-----------------\n80-----------------\n81-----------------\n82-----------------\n83-----------------\n84-----------------\n85-----------------\n86-----------------\n87-----------------\n88-----------------\n89-----------------\n90-----------------\n91-----------------\n92-----------------\n93-----------------\n94-----------------\n95-----------------\n96-----------------\n97-----------------\n98-----------------\n99-----------------\n100----------------\n101----------------\n102----------------\n103----------------\n104----------------\n105----------------\n106----------------\n107----------------\n108----------------\n109----------------\n110----------------\n111----------------\n112----------------\n113----------------\n114----------------\n115----------------\n116----------------\n117----------------\n118----------------\n119----------------" + }, + "budget": 120, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 120, + "effectiveBudget": 120, + "estimatedTokens": 316, + "omitted": [], + "projectMdShrunk": true, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": true + } + } + }, + { + "name": "D1-all-sections-present", + "why": "full assembly order: instructions, note, roadmap, project, plans, context, research, requirements", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "one.md", + "content": "1111111111111111111111111111111111111111" + }, + { + "file": "two.md", + "content": "2222222222222222222222222222222222222222" + } + ], + "projectMd": "0------------------\n1------------------\n2------------------\n3------------------", + "context": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "research": "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", + "requirements": "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ" + }, + "budget": 5000, + "options": {} + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Project\n\n0------------------\n1------------------\n2------------------\n3------------------\n\n## Plans\n\n### one.md\n\n1111111111111111111111111111111111111111\n\n### two.md\n\n2222222222222222222222222222222222222222\n\n## Context\n\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n\n## Research\n\nSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS\n\n## Requirements\n\nQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", + "metadata": { + "budget": 5000, + "effectiveBudget": 4500, + "estimatedTokens": 119, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "D2-no-optional-sections", + "why": "minimum viable shape — only the two never-droppables plus one plan", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 5000, + "options": {} + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 5000, + "effectiveBudget": 4500, + "estimatedTokens": 39, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "D3-zero-plans", + "why": "empty plans array — headers still assembled, reduce over [] is 0", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [] + }, + "budget": 5000, + "options": {} + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n", + "metadata": { + "budget": 5000, + "effectiveBudget": 4500, + "estimatedTokens": 27, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + }, + { + "name": "D4-crlf-content", + "why": "row 25 — CRLF measured and sliced identically to LF under the token unit", + "input": { + "sections": { + "instructions": "alpha\r\nbeta\r\ngamma", + "roadmap": "one\r\ntwo\r\nthree", + "plans": [ + { + "file": "a.md", + "content": "line 0\r\nline 1\r\nline 2\r\nline 3\r\nline 4\r\nline 5\r\nline 6\r\nline 7\r\nline 8\r\nline 9\r\nline 10\r\nline 11\r\nline 12\r\nline 13\r\nline 14\r\nline 15\r\nline 16\r\nline 17\r\nline 18\r\nline 19\r\nline 20\r\nline 21\r\nline 22\r\nline 23\r\nline 24\r\nline 25\r\nline 26\r\nline 27\r\nline 28\r\nline 29\r\nline 30\r\nline 31\r\nline 32\r\nline 33\r\nline 34\r\nline 35\r\nline 36\r\nline 37\r\nline 38\r\nline 39\r\nline 40\r\nline 41\r\nline 42\r\nline 43\r\nline 44\r\nline 45\r\nline 46\r\nline 47\r\nline 48\r\nline 49\r\nline 50\r\nline 51\r\nline 52\r\nline 53\r\nline 54\r\nline 55\r\nline 56\r\nline 57\r\nline 58\r\nline 59" + } + ], + "projectMd": "p0\r\np1\r\np2\r\np3\r\np4\r\np5\r\np6\r\np7\r\np8\r\np9\r\np10\r\np11\r\np12\r\np13\r\np14\r\np15\r\np16\r\np17\r\np18\r\np19\r\np20\r\np21\r\np22\r\np23\r\np24\r\np25\r\np26\r\np27\r\np28\r\np29\r\np30\r\np31\r\np32\r\np33\r\np34\r\np35\r\np36\r\np37\r\np38\r\np39\r\np40\r\np41\r\np42\r\np43\r\np44\r\np45\r\np46\r\np47\r\np48\r\np49\r\np50\r\np51\r\np52\r\np53\r\np54\r\np55\r\np56\r\np57\r\np58\r\np59\r\np60\r\np61\r\np62\r\np63\r\np64\r\np65\r\np66\r\np67\r\np68\r\np69\r\np70\r\np71\r\np72\r\np73\r\np74\r\np75\r\np76\r\np77\r\np78\r\np79" + }, + "budget": 120, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 120, + "effectiveBudget": 120, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "D5-unicode-content", + "why": "row 26 — multibyte and astral content; chars/4 counts UTF-16 units, and slice may split a surrogate pair (pinned as current behavior)", + "input": { + "sections": { + "instructions": "héllo wörld ünicode", + "roadmap": "日本語のロードマップ", + "plans": [ + { + "file": "ünï.md", + "content": "🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉" + } + ], + "context": "ΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩΩ" + }, + "budget": 150, + "options": { + "safetyMarginPct": 0 + } + }, + "expected": { + "prompt": "", + "metadata": { + "budget": 150, + "effectiveBudget": 150, + "estimatedTokens": 0, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": true, + "noteInjected": false + } + } + }, + { + "name": "D6-plan-filename-affects-budget", + "why": "S5 — per-plan wrapper cost \"### \\n\\n\" counts toward staticBaseTokens", + "input": { + "sections": { + "instructions": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", + "roadmap": "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", + "plans": [ + { + "file": "a-very-long-plan-filename-that-costs-tokens.md", + "content": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" + } + ] + }, + "budget": 5000, + "options": {} + }, + "expected": { + "prompt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\n\n## Roadmap\n\nRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\n\n## Plans\n\n### a-very-long-plan-filename-that-costs-tokens.md\n\nPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", + "metadata": { + "budget": 5000, + "effectiveBudget": 4500, + "estimatedTokens": 50, + "omitted": [], + "projectMdShrunk": false, + "planTruncationPct": 0, + "hardFailed": false, + "noteInjected": false + } + } + } + ] +} diff --git a/tests/mutation-matrix-ratchet.test.cjs b/tests/mutation-matrix-ratchet.test.cjs index 614ff4e7e..bbed74283 100644 --- a/tests/mutation-matrix-ratchet.test.cjs +++ b/tests/mutation-matrix-ratchet.test.cjs @@ -189,6 +189,7 @@ describe('mutation-matrix ratchet: guard detects missing minScore', () => { // reverse: removing a module from COVERED also requires updating the baseline. const RATCHET_BASELINE = { 'context-utilization': 80, + 'context-composer': 66, // #2929: extracted from prompt-budget; same floor as prompt-budget 'prompt-budget': 66, // CI 68.33% 2026-06-14; was 90 (timeout-inflated local) 'frontmatter': 62, 'adr-parser': 68, diff --git a/tests/prompt-budget-parity.test.cjs b/tests/prompt-budget-parity.test.cjs new file mode 100644 index 000000000..d39a49730 --- /dev/null +++ b/tests/prompt-budget-parity.test.cjs @@ -0,0 +1,103 @@ +'use strict'; + +/** + * prompt-budget-parity.test.cjs + * + * Characterization-parity suite for issue #2929 (epic #1671 Phase 2 — + * context-composer). `tests/fixtures/prompt-budget-parity/corpus.json` was + * captured from the PRE-REFACTOR `prompt-budget.applyBudget` implementation + * by `scripts/gen-prompt-budget-parity-corpus.cjs` (see that script's header + * for the fixture-provenance rationale). It is the oracle for behavioral + * parity after the composer-seam refactor: every case's `prompt` and + * `metadata` must reproduce byte-for-byte / field-for-field. + * + * Regenerating this corpus to make a failure here go away DEFEATS ITS + * PURPOSE — the corpus is frozen precisely so that a disagreement always + * means the refactor moved observable behavior, never the other way around. + * If a case fails, fix the refactor, not the fixture. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { applyBudget, estimateTokens } = require('../gsd-core/bin/lib/prompt-budget.cjs'); +const corpus = require('./fixtures/prompt-budget-parity/corpus.json'); + +describe('prompt-budget parity vs pre-refactor corpus', () => { + test('refactoredOutputIsByteIdenticalToCapturedCorpus', () => { + for (const c of corpus.cases) { + const result = applyBudget({ + sections: c.input.sections, + budget: c.input.budget, + options: c.input.options, + }); + assert.equal( + result.prompt, + c.expected.prompt, + `case "${c.name}": prompt mismatch (got ${result.prompt.length} chars, want ${c.expected.prompt.length} chars)` + ); + } + }); + + test('refactoredMetadataMatchesCapturedCorpus', () => { + for (const c of corpus.cases) { + const result = applyBudget({ + sections: c.input.sections, + budget: c.input.budget, + options: c.input.options, + }); + assert.deepStrictEqual( + result.metadata, + c.expected.metadata, + `case "${c.name}": metadata mismatch` + ); + } + }); + + test('parityCorpusCoversEveryParityCriticalRow', () => { + const cases = corpus.cases; + assert.ok(cases.length >= 45, `expected >= 45 corpus cases, got ${cases.length}`); + + const has = (pred, msg) => assert.ok(cases.some(pred), msg); + + has((c) => c.expected.metadata.hardFailed === true, 'expected >=1 case with hardFailed===true'); + has((c) => c.expected.metadata.hardFailed === false, 'expected >=1 case with hardFailed===false'); + has((c) => c.expected.metadata.noteInjected === true, 'expected >=1 case with noteInjected===true'); + has((c) => c.expected.metadata.noteInjected === false, 'expected >=1 case with noteInjected===false'); + has((c) => c.expected.metadata.omitted.length > 0, 'expected >=1 case with omitted.length > 0'); + has((c) => c.expected.metadata.projectMdShrunk === true, 'expected >=1 case with projectMdShrunk===true'); + has( + (c) => c.expected.metadata.planTruncationPct > 0, + 'expected >=1 case with planTruncationPct > 0' + ); + + const a4 = cases.filter((c) => c.name.startsWith('A4-')); + const a10 = cases.filter((c) => c.name.startsWith('A10-')); + assert.ok(a4.length >= 8, `expected >= 8 A4- cases, got ${a4.length}`); + assert.ok(a10.length >= 6, `expected >= 6 A10- cases, got ${a10.length}`); + + const noTrimOccurred = (c) => + c.expected.metadata.omitted.length === 0 && + c.expected.metadata.projectMdShrunk === false && + c.expected.metadata.planTruncationPct === 0; + + for (const c of a4) { + assert.ok(noTrimOccurred(c), `A4 case "${c.name}" must have NO trim of any kind`); + } + for (const c of a10) { + assert.ok(!noTrimOccurred(c), `A10 case "${c.name}" must have at least one trim`); + } + }); + + test('estimateTokensRemainsExportedAndUnchanged', () => { + // src/phase-estimation.cts re-exports this function as `measureTokens`, + // and CONTEXT.md pins downstream estimate/actual comparisons to this + // exact chars/4 scale — its math is frozen, not merely conventional. + assert.equal(typeof estimateTokens, 'function'); + assert.equal(estimateTokens(''), 0); + assert.equal(estimateTokens(null), 0); + assert.equal(estimateTokens(undefined), 0); + assert.equal(estimateTokens('abcd'), 1); + assert.equal(estimateTokens('abcde'), 2); + }); +});