diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 6c92db68410..39b7396d125 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -163,7 +163,7 @@ "_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).", "_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts ( acc.validationWarnings.add(w)); } } @@ -972,6 +977,11 @@ async function runStackedCompressionAsync( mergeStackStep(acc, "hard-budget", hbResult); currentBody = hbResult.body; compressed = true; + } else { + // No unit could be dropped (e.g. every unit is preserve-guarded): surface the + // unreachable-budget validationWarnings instead of dropping them silently (#17 fix #3). + // mergeStackStep is gated on `compressed`, so propagate the warnings here directly. + hbResult.stats?.validationWarnings?.forEach((w) => acc.validationWarnings.add(w)); } } diff --git a/tests/unit/compression/hard-budget.test.ts b/tests/unit/compression/hard-budget.test.ts index 7370e2009d1..686cc4c1ea7 100644 --- a/tests/unit/compression/hard-budget.test.ts +++ b/tests/unit/compression/hard-budget.test.ts @@ -30,9 +30,7 @@ const PROSE = [ function makeBody(content: string) { return { - messages: [ - { role: "user", content }, - ], + messages: [{ role: "user", content }], }; } @@ -46,10 +44,7 @@ test("targetTokens: cuts body to ≤ targetTokens", () => { const msgs = result.body.messages as Array<{ content: string }>; const outTokens = countTextTokens(msgs.map((m) => m.content).join(" ")); - assert.ok( - outTokens <= 200, - `Output tokens ${outTokens} exceed targetTokens 200` - ); + assert.ok(outTokens <= 200, `Output tokens ${outTokens} exceed targetTokens 200`); }); test("targetTokens: preserves highest-saliency sentences", () => { @@ -234,10 +229,7 @@ test("review#2: aggregate target keeps TOTAL ≤ target across multiple messages const msgs = result.body.messages as Array<{ content: string }>; const total = msgs.reduce((s, m) => s + countTextTokens(m.content), 0); assert.ok(result.compressed, "should be compressed"); - assert.ok( - total <= target, - `aggregate TOTAL ${total} must stay ≤ target ${target}` - ); + assert.ok(total <= target, `aggregate TOTAL ${total} must stay ≤ target ${target}`); }); // --- Review fix #3: signal a warning when target is impossible --- @@ -325,10 +317,7 @@ test("integration: applyStackedCompression with config.targetTokens cuts at end const result = applyStackedCompression(body, config.stackedPipeline, { config }); const msgs = result.body.messages as Array<{ content: string }>; const outTokens = countTextTokens(msgs.map((m) => m.content).join(" ")); - assert.ok( - outTokens <= 150, - `Integration: output tokens ${outTokens} exceed targetTokens 150` - ); + assert.ok(outTokens <= 150, `Integration: output tokens ${outTokens} exceed targetTokens 150`); assert.ok(result.compressed, "Integration: result should be compressed"); const techniques = result.stats?.techniquesUsed ?? []; @@ -337,3 +326,34 @@ test("integration: applyStackedCompression with config.targetTokens cuts at end `Integration: techniquesUsed should include 'hard-budget', got: ${techniques}` ); }); + +test("review#3: unreachable-budget warning propagates through applyStackedCompression", () => { + // Every line is preserve-guarded (numbers) => hard-budget drops nothing => hbResult.compressed + // is false. The seam still surfaces the warning instead of swallowing it (the gate is on + // `compressed`, so the warning must be merged on the else branch). + const lines = ["Value 11111", "Value 22222", "Value 33333", "Value 44444", "Value 55555"].join( + "\n" + ); + const body = makeBody(lines); + const totalTokens = countTextTokens(lines); + const config = { + enabled: true, + defaultMode: "stacked" as const, + autoTriggerMode: "lite" as const, + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + compressionComboId: null, + stackedPipeline: [{ engine: "caveman" as const, intensity: "lite" as const }], + engines: {}, + activeComboId: null, + targetTokens: Math.floor(totalTokens * 0.3), + }; + const result = applyStackedCompression(body, config.stackedPipeline, { config }); + const warnings = result.stats?.validationWarnings ?? []; + assert.ok( + warnings.some((w) => w.includes("hard-budget") && w.includes("could not reach target")), + `warning must propagate through the stacked seam, got: ${JSON.stringify(warnings)}` + ); +});