|
|
@@ -111,6 +111,17 @@ async function explore(project: Project, query: string) {
|
|
|
score: (file: string) => fileOf(file)?.score ?? 0,
|
|
|
/** Chars of source the allocator RESERVED for it, before anything rendered. */
|
|
|
allowance: (file: string) => fileOf(file)?.allowance ?? 0,
|
|
|
+ /** Which render path the loop took: `whole`, `clusters`, `focused`, `skeleton`. */
|
|
|
+ render: (file: string) => fileOf(file)?.render ?? null,
|
|
|
+ /**
|
|
|
+ * Rank the ranking pass gave it (1 = the file the response leads with, and
|
|
|
+ * the first the render loop reaches). Read off the record rather than from
|
|
|
+ * the position in `report.files`, which the report re-sorts by delivered
|
|
|
+ * bytes for legibility.
|
|
|
+ */
|
|
|
+ rank: (file: string) => fileOf(file)?.rank ?? -1,
|
|
|
+ /** Total source bytes delivered across every rendered file. */
|
|
|
+ sourceTotal: () => [...bytes.values()].reduce((sum, n) => sum + n, 0),
|
|
|
/** Fraction of the WHOLE response this file's source occupies. */
|
|
|
share: (file: string) => (bytes.get(file) ?? 0) / (text.length || 1),
|
|
|
shareUnder: (prefix: string) => {
|
|
|
@@ -412,6 +423,412 @@ export function log(message: string): void {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+// ── 1b. CG-21: a reservation below the file's size must not lose its bytes ──
|
|
|
+
|
|
|
+/**
|
|
|
+ * The shape CG-15's agent A/B found in the wild, and the one thing the suite
|
|
|
+ * above could not catch: a file whose reservation lands BELOW its own size.
|
|
|
+ *
|
|
|
+ * Express, `lib/utils.js` (5,293 B), the top-ranked file for
|
|
|
+ * "res.send Content-Type ETag generateETag setETag":
|
|
|
+ *
|
|
|
+ * | | baseline | CG-12 |
|
|
|
+ * |---|---|---|
|
|
|
+ * | delivered | 6,380 (46.1%) whole | **583 (7.7%) cluster stub** |
|
|
|
+ * | source envelope (13,000 budget) | 13,849 | **9,241** |
|
|
|
+ *
|
|
|
+ * It was reserved 3,870 and spent 583. The whole-file grace bound
|
|
|
+ * (`allowance + min(800, allowance * 0.15)` = 4,450) sits just under the file,
|
|
|
+ * so the whole-file render is declined; the fallback cluster render has three
|
|
|
+ * matched symbols to work with and emits a stub. The other 3,287 chars were
|
|
|
+ * neither delivered nor redistributed — **the pool shrank by a third against an
|
|
|
+ * unchanged budget**, and the agent Read the file back four times.
|
|
|
+ *
|
|
|
+ * Everything about that is invisible to the fixtures above, and to the payroll
|
|
|
+ * one: both SATURATE (`[over budget] [TRUNCATED]`, 23,599 of a 23,600 pool),
|
|
|
+ * so there is no unspent reservation to lose. This fixture is built to sit in
|
|
|
+ * the gap instead — a mid-sized top-ranked file with a THIN matched-symbol set,
|
|
|
+ * sized just above its reservation — which is the combination that has to hold
|
|
|
+ * for the defect to reproduce, and is why it shipped.
|
|
|
+ *
|
|
|
+ * The `fixture shape` block below is load-bearing, not scaffolding: every gate
|
|
|
+ * here passes vacuously if the target ever drifts small enough for the grace
|
|
|
+ * bound to cover it, so the window `0.6 × size <= reservation < size` is
|
|
|
+ * asserted directly.
|
|
|
+ */
|
|
|
+describe('CG-21 — a reservation under the file size still buys the file', () => {
|
|
|
+ // Names two symbols that live in ONE mid-sized file (the named-seed tier is
|
|
|
+ // what puts it at rank 0) while the rest of the terms pull in its peers, so
|
|
|
+ // the proportional split hands the target well under its own size.
|
|
|
+ const QUERY = 'generateEtag compileEtag send response body';
|
|
|
+ const TARGET = 'src/http/etag.ts';
|
|
|
+ const RESPONSE = 'src/http/response.ts';
|
|
|
+ const APPLICATION = 'src/http/application.ts';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Bulk for the target: real, extractable symbols that match NOTHING in the
|
|
|
+ * query. They make the file BIG without making it more relevant — which is
|
|
|
+ * precisely how a file ends up reserved less than it is worth in bytes. Kept
|
|
|
+ * dense (4 lines each) so the file stays well inside `WHOLE_FILE_MAX_LINES`
|
|
|
+ * and the byte bound is the only thing that can decline the whole render.
|
|
|
+ */
|
|
|
+ const inertFiller = (n: number) => `
|
|
|
+export function normalizeLedgerRow${n}(row: string[], fallback: string, separator: string): string[] {
|
|
|
+ const trimmed = row.map((cell) => cell.trim()).filter((cell) => cell.length > 0 && cell !== separator);
|
|
|
+ return trimmed.length > 0 ? trimmed : [fallback, separator, String(trimmed.length), 'ledger-row-${n}'];
|
|
|
+}
|
|
|
+`;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The matched-symbol set, deliberately THIN and small. This is the second
|
|
|
+ * half of the shape: with only these two tiny functions to cluster around,
|
|
|
+ * the fallback render emits a few hundred chars and abandons the rest of the
|
|
|
+ * reservation. A file with a fat matched set would spend its allowance the
|
|
|
+ * ordinary way and never expose the bug.
|
|
|
+ */
|
|
|
+ const TARGET_SOURCE = `
|
|
|
+/** ETag helpers. */
|
|
|
+export function generateEtag(body: string): string {
|
|
|
+ return '"' + body.length.toString(16) + '"';
|
|
|
+}
|
|
|
+
|
|
|
+export function compileEtag(setting: string): (body: string) => string {
|
|
|
+ return setting === 'strong' ? generateEtag : (body: string) => 'W/' + generateEtag(body);
|
|
|
+}
|
|
|
+${Array.from({ length: 27 }, (_, i) => inertFiller(i + 1)).join('')}`;
|
|
|
+
|
|
|
+ const responseMethod = (name: string) => `
|
|
|
+ public ${name}(body: string): string {
|
|
|
+ const etag = compileEtag(this.etagSetting)(body);
|
|
|
+ this.headers.set('etag', etag);
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+ const RESPONSE_SOURCE = `
|
|
|
+import { compileEtag } from './etag';
|
|
|
+
|
|
|
+/** The response object: sends a body and negotiates its representation. */
|
|
|
+export class ServerResponse {
|
|
|
+ private headers = new Map<string, string>();
|
|
|
+ private etagSetting = 'strong';
|
|
|
+${[
|
|
|
+ 'send',
|
|
|
+ 'sendBody',
|
|
|
+ 'sendResponse',
|
|
|
+ 'writeBody',
|
|
|
+ 'endResponse',
|
|
|
+ 'json',
|
|
|
+ 'setResponseBody',
|
|
|
+ 'flushResponseBody',
|
|
|
+].map(responseMethod).join('')}
|
|
|
+}
|
|
|
+`;
|
|
|
+
|
|
|
+ let project: Project;
|
|
|
+ let run: Awaited<ReturnType<typeof explore>>;
|
|
|
+ let targetSize = 0;
|
|
|
+
|
|
|
+ beforeAll(async () => {
|
|
|
+ project = await buildProject('codegraph-alloc-cg21-', {
|
|
|
+ [TARGET]: TARGET_SOURCE,
|
|
|
+ [RESPONSE]: RESPONSE_SOURCE,
|
|
|
+ [APPLICATION]: `
|
|
|
+import { ServerResponse } from './response';
|
|
|
+
|
|
|
+/** The application: routes a request and hands the response its body. */
|
|
|
+export class Application {
|
|
|
+ private routes = new Map<string, (res: ServerResponse) => string>();
|
|
|
+
|
|
|
+ public handleRequest(path: string, res: ServerResponse, body: string): string {
|
|
|
+ const route = this.routes.get(path);
|
|
|
+ return route ? route(res) : res.send(body);
|
|
|
+ }
|
|
|
+
|
|
|
+ public registerResponseRoute(path: string, handler: (res: ServerResponse) => string): void {
|
|
|
+ this.routes.set(path, handler);
|
|
|
+ }
|
|
|
+}
|
|
|
+`,
|
|
|
+ 'src/http/request.ts': `
|
|
|
+/** The request object: carries the inbound body. */
|
|
|
+export class ServerRequest {
|
|
|
+ public constructor(public readonly body: string) {}
|
|
|
+
|
|
|
+ public freshResponseBody(): string {
|
|
|
+ return this.body.trim();
|
|
|
+ }
|
|
|
+}
|
|
|
+`,
|
|
|
+ 'src/util/logger.ts': `
|
|
|
+export function log(message: string): void {
|
|
|
+ console.log(message);
|
|
|
+}
|
|
|
+`,
|
|
|
+ });
|
|
|
+ targetSize = fs.readFileSync(path.join(project.dir, TARGET), 'utf-8').length;
|
|
|
+ run = await explore(project, QUERY);
|
|
|
+ }, 120_000);
|
|
|
+
|
|
|
+ afterAll(() => destroyProject(project));
|
|
|
+
|
|
|
+ describe('fixture shape', () => {
|
|
|
+ it('ranks the target first, on a matched set of only two symbols', () => {
|
|
|
+ // Rank 0 is what makes the loss expensive: this is the file the response
|
|
|
+ // leads with, and the one the agent Reads back when it arrives as a stub.
|
|
|
+ expect(run.rank(TARGET)).toBe(1);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('sizes the target ABOVE its reservation but inside the buy window', () => {
|
|
|
+ // The whole assertion set below is vacuous outside this window, so it is
|
|
|
+ // pinned here rather than assumed:
|
|
|
+ // reservation >= size → the grace bound already covers it, and the
|
|
|
+ // buy rule is never consulted (express's other
|
|
|
+ // three queries look like this).
|
|
|
+ // reservation < 0.6×size → the shortfall is real, clustering is the
|
|
|
+ // right answer, and the carry-forward — not the
|
|
|
+ // buy rule — is what conserves the bytes.
|
|
|
+ const reserved = run.allowance(TARGET);
|
|
|
+ expect(reserved).toBeGreaterThan(0);
|
|
|
+ expect(reserved).toBeLessThan(targetSize);
|
|
|
+ expect(reserved / targetSize).toBeGreaterThanOrEqual(EXPLORE_ALLOCATION.WHOLE_FILE_BUY_FRACTION);
|
|
|
+ // ...and specifically OUTSIDE the grace bound, which is the pre-CG-21
|
|
|
+ // rule. If grace alone could carry it, this fixture proves nothing.
|
|
|
+ const graceBound = reserved + Math.min(
|
|
|
+ EXPLORE_ALLOCATION.WHOLE_FILE_GRACE_MAX,
|
|
|
+ Math.round(reserved * EXPLORE_ALLOCATION.WHOLE_FILE_GRACE_FRACTION),
|
|
|
+ );
|
|
|
+ expect(targetSize).toBeGreaterThan(graceBound);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('keeps the target inside the whole-file LINE bound, so only bytes can gate it', () => {
|
|
|
+ // `WHOLE_FILE_MAX_LINES` (220 for a non-central file) is a separate gate
|
|
|
+ // that also declines a whole render. If the fixture ever crossed it the
|
|
|
+ // suite would go red for the wrong reason — and, worse, a genuine
|
|
|
+ // regression in the BYTE bound would be masked by it.
|
|
|
+ const lines = fs.readFileSync(path.join(project.dir, TARGET), 'utf-8').split('\n').length;
|
|
|
+ expect(lines).toBeLessThanOrEqual(220);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('the reservation is spent', () => {
|
|
|
+ it('delivers the target WHOLE rather than as a cluster stub', () => {
|
|
|
+ // The headline. Pre-CG-21 this file rendered `clusters` and emitted a few
|
|
|
+ // hundred chars against a multi-thousand-char reservation.
|
|
|
+ expect(run.render(TARGET)).toBe('whole');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('spends more than the reservation, not a fraction of it', () => {
|
|
|
+ // Stated as bytes so it bites independently of the render-mode label: a
|
|
|
+ // build that renamed the whole path but still emitted a stub fails here.
|
|
|
+ // Express: 583 delivered against 3,870 reserved.
|
|
|
+ const delivered = run.bytes.get(TARGET) ?? 0;
|
|
|
+ expect(delivered).toBeGreaterThanOrEqual(targetSize);
|
|
|
+ expect(delivered).toBeGreaterThan(run.allowance(TARGET));
|
|
|
+ });
|
|
|
+
|
|
|
+ it('leaves no rendered file both under its reservation and short of content', () => {
|
|
|
+ // The defect stated as an invariant, which is what makes it general rather
|
|
|
+ // than a re-assertion of the case above: a rendered file either SPENDS what
|
|
|
+ // it was promised, or it ran out of file. Express's `lib/utils.js` did
|
|
|
+ // neither — 583 delivered, 3,870 promised, 5,293 bytes of file sitting
|
|
|
+ // there — and the difference was dropped rather than redistributed, which
|
|
|
+ // is why the source envelope fell 13,849 → 9,241 on an unchanged budget.
|
|
|
+ //
|
|
|
+ // `response.ts` is the case the naive "spend the whole pool" version of
|
|
|
+ // this test gets wrong: it delivers 1,635 of a 5,292 reservation and that
|
|
|
+ // is CORRECT — the file is only 1,635 bytes. A pool cannot be spent past
|
|
|
+ // the content that exists to fill it.
|
|
|
+ for (const f of run.report.files) {
|
|
|
+ if (!f.render || (f.emittedChars ?? 0) === 0) continue;
|
|
|
+ const size = fs.readFileSync(path.join(project.dir, f.path), 'utf-8').length;
|
|
|
+ expect(f.emittedChars, `${f.path} spent its reservation or ran out of file`)
|
|
|
+ .toBeGreaterThanOrEqual(Math.min(f.allowance ?? 0, size));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('holds the hard ceiling while doing it', () => {
|
|
|
+ // The buy rule spends MORE than the reservation, so the bound that stops
|
|
|
+ // it running away has to be re-proved here and not inherited: the
|
|
|
+ // overshoot pool is finite, and the 25K inline cap is absolute — past it
|
|
|
+ // the host writes the result to a file the agent Reads back.
|
|
|
+ const budget = getExploreOutputBudget(project.cg.getFiles().length);
|
|
|
+ const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), INLINE_CAP);
|
|
|
+ expect(run.text.length).toBeLessThanOrEqual(hardCeiling);
|
|
|
+ expect(run.text.length).toBeLessThan(INLINE_CAP);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('still serves the peers — concentration, not a single-file response', () => {
|
|
|
+ // The over-correction control for this fixture. Buying the target whole
|
|
|
+ // must not eat the files below it: that is the trade the shared overshoot
|
|
|
+ // pool refuses (it dropped `payslip_builder.go` when funding was per-file).
|
|
|
+ const peers = [RESPONSE, APPLICATION].filter((f) => (run.bytes.get(f) ?? 0) > 0);
|
|
|
+ expect(peers.length).toBeGreaterThan(0);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * The other half of CG-21, and the half the whole-file buy rule cannot reach.
|
|
|
+ *
|
|
|
+ * Buying the file whole only helps when the reservation has already covered
|
|
|
+ * most of it. Below that the shortfall is real — the file is several times its
|
|
|
+ * reservation, and clustering IS the right render — but the bytes it cannot
|
|
|
+ * spend still must not evaporate. Express, query "compileETag req.fresh":
|
|
|
+ * `lib/utils.js` was reserved 3,809 and spent 791; the 3,018 chars it left had
|
|
|
+ * to reach `lib/response.js` below it, which delivered 4,650 on a 1,895
|
|
|
+ * reservation.
|
|
|
+ *
|
|
|
+ * So this fixture is deliberately the INVERSE of the one above: the leading
|
|
|
+ * file is far too big for the buy rule to fire, and the assertion is on the
|
|
|
+ * file BELOW it. Without this, `allowance = reserved` — the whole carry-forward
|
|
|
+ * deleted — passes every other test in this file.
|
|
|
+ */
|
|
|
+describe('CG-21 — an unspendable reservation flows to the next file down', () => {
|
|
|
+ // Names three tiny callables that all live in the SPRAWL file — the named-seed
|
|
|
+ // tier is what puts a file with almost no matched content at rank 1 — plus one
|
|
|
+ // term the absorber's methods carry, so it ranks second rather than cliffing.
|
|
|
+ const QUERY = 'renderStaticScene renderInteractiveScene renderNewElementScene paintSceneLayer';
|
|
|
+ // Rank 1: a huge file the query names two symbols in. Its reservation cannot
|
|
|
+ // approach its size, so it clusters — and clusters thinly, because those two
|
|
|
+ // symbols are all it matched.
|
|
|
+ const SPRAWL = 'src/scene/sprawl.ts';
|
|
|
+ // Rank 2: dense with matched symbols and bigger than any share it can be
|
|
|
+ // reserved, so it will absorb whatever the file above it leaves.
|
|
|
+ const ABSORBER = 'src/render/absorber.ts';
|
|
|
+
|
|
|
+ const inertBulk = (n: number) => `
|
|
|
+export function reconcileLedgerEntry${n}(rows: string[], fallback: string, separator: string): string[] {
|
|
|
+ const trimmed = rows.map((cell) => cell.trim()).filter((cell) => cell.length > 0 && cell !== separator);
|
|
|
+ return trimmed.length > 0 ? trimmed : [fallback, separator, String(trimmed.length), 'entry-${n}'];
|
|
|
+}
|
|
|
+`;
|
|
|
+
|
|
|
+ // Long ENOUGH, in lines, that the absorber cannot ship whole (220 lines is the
|
|
|
+ // other whole-file gate). That matters: a file that renders whole ignores the
|
|
|
+ // per-file budget entirely, and this fixture is about a budget being spent.
|
|
|
+ const matchedPaint = (n: number) => `
|
|
|
+ public paintSceneLayer${n}(canvas: string, scene: string, element: string): string {
|
|
|
+ const appState = this.appState.get('layer${n}') ?? scene;
|
|
|
+ const painted = canvas + '|' + appState + '|' + element;
|
|
|
+ const stamped = painted + '|layer-${n}';
|
|
|
+ const merged = stamped + '|' + scene + '|' + element;
|
|
|
+ const settled = merged.split('|').filter((part) => part.length > 0).join('|');
|
|
|
+ this.appState.set('layer${n}', settled);
|
|
|
+ if (settled.length === 0) {
|
|
|
+ return this.paint(scene, scene);
|
|
|
+ }
|
|
|
+ return this.paint(settled, scene);
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+ let project: Project;
|
|
|
+ let run: Awaited<ReturnType<typeof explore>>;
|
|
|
+
|
|
|
+ beforeAll(async () => {
|
|
|
+ project = await buildProject('codegraph-alloc-cg21-carry-', {
|
|
|
+ [SPRAWL]: `
|
|
|
+import { Absorber } from '../render/absorber';
|
|
|
+
|
|
|
+/** Scene sprawl: three one-line answers buried in a very large file. */
|
|
|
+export function renderStaticScene(scene: string): string {
|
|
|
+ return new Absorber().paint(scene, scene);
|
|
|
+}
|
|
|
+
|
|
|
+export function renderInteractiveScene(scene: string): string {
|
|
|
+ return new Absorber().paint(scene, scene + ':interactive');
|
|
|
+}
|
|
|
+
|
|
|
+export function renderNewElementScene(scene: string): string {
|
|
|
+ return new Absorber().paint(scene, scene + ':new-element');
|
|
|
+}
|
|
|
+${Array.from({ length: 90 }, (_, i) => inertBulk(i + 1)).join('')}`,
|
|
|
+ [ABSORBER]: `
|
|
|
+/** The renderer: many matched paint passes, all of them wanted. */
|
|
|
+export class Absorber {
|
|
|
+ private appState = new Map<string, string>();
|
|
|
+
|
|
|
+ public paint(element: string, scene: string): string {
|
|
|
+ return element + '|' + scene;
|
|
|
+ }
|
|
|
+${Array.from({ length: 20 }, (_, i) => matchedPaint(i + 1)).join('')}
|
|
|
+}
|
|
|
+${/* Inert tail: pushes the absorber FAR past its reservation so the whole-file
|
|
|
+ buy rule cannot fire on it either. Without this the absorber ships whole
|
|
|
+ and the fixture measures the buy rule a second time instead of the
|
|
|
+ carry-forward — which is exactly how it read on the first attempt. */
|
|
|
+ Array.from({ length: 40 }, (_, i) => inertBulk(100 + i)).join('')}`,
|
|
|
+ 'src/util/logger.ts': `
|
|
|
+export function log(message: string): void {
|
|
|
+ console.log(message);
|
|
|
+}
|
|
|
+`,
|
|
|
+ });
|
|
|
+ run = await explore(project, QUERY);
|
|
|
+ }, 120_000);
|
|
|
+
|
|
|
+ afterAll(() => destroyProject(project));
|
|
|
+
|
|
|
+ it('leaves the leading file unable to spend its reservation', () => {
|
|
|
+ // The precondition. If the sprawl file ever spends its share, there is no
|
|
|
+ // slack, and the assertion below passes for no reason at all.
|
|
|
+ const spent = run.bytes.get(SPRAWL) ?? 0;
|
|
|
+ expect(spent).toBeGreaterThan(0);
|
|
|
+ expect(spent).toBeLessThan(run.allowance(SPRAWL));
|
|
|
+ // ...and it is out of reach of the buy rule, so this is genuinely the
|
|
|
+ // carry-forward's case and not a second test of the fixture above.
|
|
|
+ const size = fs.readFileSync(path.join(project.dir, SPRAWL), 'utf-8').length;
|
|
|
+ expect(run.allowance(SPRAWL) / size).toBeLessThan(EXPLORE_ALLOCATION.WHOLE_FILE_BUY_FRACTION);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('hands the shortfall to the file below, which spends past its own reservation', () => {
|
|
|
+ // The lever. Measured both ways on this fixture: with the carry-forward the
|
|
|
+ // absorber delivers 9,297 against a 7,455 reservation; with
|
|
|
+ // `allowance = reserved` it delivers 7,479 — its reservation and nothing
|
|
|
+ // more, while the sprawl file's 4,408 unspent chars are dropped.
|
|
|
+ //
|
|
|
+ // The 1.1 margin is not padding. A cluster section can land a few chars over
|
|
|
+ // the budget it was selected against (whole symbol ranges, never sliced
|
|
|
+ // mid-method), so "delivered > reserved" alone is true by ~24 chars even on
|
|
|
+ // the mutated build — a test that passes on the defect.
|
|
|
+ const delivered = run.bytes.get(ABSORBER) ?? 0;
|
|
|
+ expect(delivered).toBeGreaterThan(Math.round(run.allowance(ABSORBER) * 1.1));
|
|
|
+ });
|
|
|
+
|
|
|
+ it('keeps the shortfall in the envelope instead of dropping it', () => {
|
|
|
+ // The same lever read off the response as a whole, which is the form the
|
|
|
+ // user actually feels: express's source envelope fell 13,849 → 9,241 on an
|
|
|
+ // unchanged 13,000 budget because nothing picked up what `lib/utils.js`
|
|
|
+ // could not spend. Here: 10,033 delivered with the carry-forward, 8,215
|
|
|
+ // without.
|
|
|
+ //
|
|
|
+ // Stated against what a no-carry build could produce — the leader's actual
|
|
|
+ // spend plus the absorber's own reservation — so it stays a statement about
|
|
|
+ // the mechanism rather than a hard-coded byte count.
|
|
|
+ const noCarryCeiling = (run.bytes.get(SPRAWL) ?? 0) + Math.round(run.allowance(ABSORBER) * 1.05);
|
|
|
+ expect(run.sourceTotal()).toBeGreaterThan(noCarryCeiling);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('bounds the borrowing — slack concentrates, it does not consume', () => {
|
|
|
+ // Carried slack is clamped to `MAX_SHARE` of the envelope, so an
|
|
|
+ // under-spending leader cannot hand the file below it the whole response.
|
|
|
+ // The bound is stated WITH the spine allowance (`SPINE_CEILING`, 1.5x)
|
|
|
+ // folded in: a flow-path cluster is deliberately allowed past the per-file
|
|
|
+ // share, and that predates CG-21 — writing the tighter bound here would
|
|
|
+ // make this test fail on a build with no defect in it.
|
|
|
+ const budget = getExploreOutputBudget(project.cg.getFiles().length);
|
|
|
+ const clamp = Math.max(
|
|
|
+ run.allowance(ABSORBER),
|
|
|
+ Math.round(budget.maxOutputChars * EXPLORE_ALLOCATION.MAX_SHARE),
|
|
|
+ );
|
|
|
+ expect(run.bytes.get(ABSORBER) ?? 0).toBeLessThanOrEqual(Math.round(clamp * 1.5));
|
|
|
+ // The anti-starvation half, and the one that would actually bite: the file
|
|
|
+ // that lent the slack still gets rendered.
|
|
|
+ expect(run.bytes.get(SPRAWL) ?? 0).toBeGreaterThan(0);
|
|
|
+ expect(run.text.length).toBeLessThan(INLINE_CAP);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
// ── 2. Degenerate and diffuse result sets ───────────────────────────────────
|
|
|
|
|
|
describe('allocation on degenerate result sets', () => {
|