Преглед изворни кода

fix(explore): bound how far an oversize cluster member may overshoot (CG-30)

shrinkCluster keeps an oversize cluster's highest-importance member WHOLE on
purpose — an empty file section sends the agent to Read, the outcome explore
exists to prevent. What it lacked was a bound, and "never empty" quietly meant
"never bounded": on the reporting repo one file emitted 22,376 chars against a
9,181-char reservation (2.44x), past both the per-file budget and the spine
ceiling. That overshoot is what collapses `headroom` for every file below it.

The same rule has a second face. When the top member is bigger than the whole
response ceiling, the file does not overshoot — it is dropped entirely at the
renderCeiling check, so the agent gets nothing for a file it named.

renderCluster now takes a ceiling (1.5x what the file may spend — the same
multiple SPINE_CEILING already draws, and never below the cap, so a cluster
that fits is untouched). Past it the member is WINDOWED on whole lines rather
than emitted whole or dropped: leading window plus, on a flow cluster, a window
on the spine's call site. A partial window shorter than 12 lines is dropped
instead — a sliver in the session record forces the next call's dedup to shred
the block around it or re-send it — unless nothing else was emitted, where the
never-empty floor wins.

Measured on the new fixture, pre-fix vs post-fix:
  monthly.ts    12,391 chars on a 3,334 budget (3.7x)  →  4,941 (1.48x)
  quarterly.ts  dropped, no headroom left               →  4,004 delivered

Also: the diagnostic now reports `spendable` (reservation + inherited slack)
alongside `reserved`. Every render bound reads the former, so reporting only
the latter makes an ordinary carry-forward read as a file spending over budget
— and it made the overshoot this issue is about unmeasurable. A windowed file
is now flagged `clipped` too, instead of presenting a window as the whole file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Colby McHenry пре 1 месец
родитељ
комит
765c06aa40

+ 179 - 0
__tests__/explore-oversize-member.test.ts

@@ -0,0 +1,179 @@
+/**
+ * Regression fixture for CG-30 — a cluster's top member may not overshoot the
+ * file's budget without bound.
+ *
+ * `shrinkCluster` keeps the highest-importance member of an oversize cluster
+ * WHOLE, deliberately: an empty file section sends the agent to Read, which is
+ * the outcome explore exists to prevent. What it lacked was a bound. On the
+ * originating repo one file emitted 22,376 chars against a 9,181-char
+ * reservation — 2.44x — past both the per-file budget and the spine ceiling,
+ * because its top member alone was that big. The overshoot is what collapses
+ * `headroom` for every file ranked below it (CG-31), and it has a second face:
+ * a member too big for the whole response ceiling makes the file drop out
+ * entirely rather than render short.
+ *
+ * `__tests__/fixtures/oversize-member-ts/` reproduces both permanently. Three
+ * report builders compete for one envelope, each a single long function far
+ * bigger than any reservation it can earn beside its siblings. Measured against
+ * the pre-fix build, this fixture produced:
+ *
+ *   monthly.ts    12,391 chars emitted on a 3,334 budget  (3.7x)
+ *   quarterly.ts  dropped entirely — no headroom left     (the CG-31 half)
+ *
+ * The gate below is that both are now bounded AND delivered: the bound cuts the
+ * overshoot, and cutting the overshoot is what buys back the starved file.
+ *
+ * Measured against `spendable`, not `reserved`: the render paths bound
+ * themselves by the reservation PLUS whatever slack the files above left on the
+ * table, so a file legitimately spending inherited slack is not an overshoot.
+ */
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import CodeGraph from '../src/index';
+import { ToolHandler } from '../src/mcp/tools';
+import { attributeSourceBytes } from '../src/mcp/explore-diagnostics';
+import type { ExploreDiagnosticReport, ExploreDiagnosticFile } from '../src/mcp/explore-diagnostics';
+
+const FIXTURE_SRC = path.join(__dirname, 'fixtures', 'oversize-member-ts');
+
+/** A symbol bag spanning the three builders — the sibling files compete. */
+const QUERY = 'buildMonthlyReport buildWeeklyReport buildQuarterlyReport formatReportRow persistReport';
+
+/** The giant: one ~24K function, far past the whole-response ceiling. */
+const GIANT = 'src/report/monthly.ts';
+/** Mid-size: one ~11K function — the file the giant's overshoot used to starve. */
+const STARVED = 'src/report/quarterly.ts';
+
+/** The bound: 1.5x, the same multiple the spine ceiling already draws. */
+const OVERSHOOT_FACTOR = 1.5;
+
+describe('CG-30 — an oversize cluster member is bounded, not unbounded', () => {
+  let testDir: string;
+  let cg: CodeGraph;
+  let response: string;
+  let report: ExploreDiagnosticReport;
+  let bytes: Map<string, number>;
+
+  const fileOf = (p: string): ExploreDiagnosticFile => {
+    const rec = report.files.find((f) => f.path === p);
+    if (!rec) throw new Error(`${p} absent from the diagnostic report`);
+    return rec;
+  };
+  /** What the render paths actually bound themselves by. */
+  const budgetOf = (rec: ExploreDiagnosticFile): number => rec.spendable ?? rec.allowance ?? 0;
+
+  beforeAll(async () => {
+    testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg30-'));
+    fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
+    fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
+
+    cg = CodeGraph.initSync(testDir);
+    await cg.indexAll();
+
+    // The per-file budget is only observable through the diagnostic sidecar, and
+    // the whole gate is "emitted vs what the file was allowed to spend".
+    const sidecar = path.join(testDir, 'explore-diag.jsonl');
+    const previous = process.env.CODEGRAPH_EXPLORE_DEBUG;
+    process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
+    try {
+      const handler = new ToolHandler(cg);
+      const result = await handler.execute('codegraph_explore', { query: QUERY });
+      response = result.content?.[0]?.text ?? '';
+    } finally {
+      if (previous === undefined) delete process.env.CODEGRAPH_EXPLORE_DEBUG;
+      else process.env.CODEGRAPH_EXPLORE_DEBUG = previous;
+    }
+    const written = fs.readFileSync(sidecar, 'utf-8').trim().split('\n').filter(Boolean);
+    report = JSON.parse(written[written.length - 1]!) as ExploreDiagnosticReport;
+    bytes = attributeSourceBytes(response);
+  }, 120_000);
+
+  afterAll(() => {
+    if (cg) cg.destroy();
+    if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
+  });
+
+  // ── Fixture shape — if these rot, the gate below means nothing ─────────────
+
+  describe('fixture shape', () => {
+    it('holds single members far bigger than any budget they can earn', () => {
+      for (const file of [GIANT, STARVED]) {
+        const source = fs.readFileSync(path.join(testDir, file), 'utf-8');
+        const top = cg.getNodesInFile(file)
+          .filter((n) => n.kind === 'function')
+          .sort((a, b) => (b.endLine - b.startLine) - (a.endLine - a.startLine))[0];
+        expect(top, `${file} has no function node`).toBeDefined();
+        // One symbol, most of the file — the "top member alone is oversize" shape.
+        expect(top!.endLine - top!.startLine).toBeGreaterThan(180);
+        expect(source.length).toBeGreaterThan(budgetOf(fileOf(file)) * 2);
+      }
+    });
+
+    it('is too long to ship whole, so both render through the cluster path', () => {
+      for (const file of [GIANT, STARVED]) {
+        const lineCount = fs.readFileSync(path.join(testDir, file), 'utf-8').split('\n').length;
+        // Past WHOLE_FILE_MAX_LINES (220 for a non-central file), so the
+        // whole-file paths — grace and buy — cannot claim it.
+        expect(lineCount, file).toBeGreaterThan(220);
+        expect(fileOf(file).render, file).toBe('clusters');
+      }
+    });
+  });
+
+  // ── The gate ──────────────────────────────────────────────────────────────
+
+  describe('bounded overshoot', () => {
+    it('CG-30 GATE: the giant no longer emits a multiple of its budget', () => {
+      const rec = fileOf(GIANT);
+      // Pre-fix this file emitted 12,391 on a 3,334 budget (3.7x).
+      expect(rec.emittedChars).toBeLessThanOrEqual(
+        Math.round(budgetOf(rec) * OVERSHOOT_FACTOR) + 1);
+    });
+
+    it('CG-30 GATE: no clustered file emits past 1.5x what it may spend', () => {
+      const over = report.files
+        .filter((f) => f.render === 'clusters' && budgetOf(f) > 0)
+        .filter((f) => f.emittedChars > Math.round(budgetOf(f) * OVERSHOOT_FACTOR) + 1)
+        .map((f) => `${f.path}: ${f.emittedChars} of ${budgetOf(f)}`);
+      expect(over).toEqual([]);
+    });
+
+    it('CG-31: the file the overshoot used to starve is delivered', () => {
+      // Pre-fix: dropped with skip reason `budget-clusters` — the giant above it
+      // had already spent the headroom this file needed.
+      expect(fileOf(STARVED).skipped).toBeNull();
+      expect(bytes.get(STARVED) ?? 0).toBeGreaterThan(0);
+    });
+
+    it('never emits an empty section — the invariant the old rule protected', () => {
+      for (const rec of report.files) {
+        if (rec.render !== 'clusters') continue;
+        expect(rec.emittedChars, rec.path).toBeGreaterThan(0);
+      }
+      // And the windowed file still leads with the symbol the query named.
+      expect(response).toContain('export function buildMonthlyReport');
+    });
+
+    it('cuts on whole lines — a body is never sliced mid-line', () => {
+      const source = fs.readFileSync(path.join(testDir, GIANT), 'utf-8').split('\n');
+      const numbered = response
+        .split('\n')
+        .map((l) => /^(\d+)\t(.*)$/.exec(l))
+        .filter((m): m is RegExpExecArray => m !== null)
+        .filter((m) => Number(m[1]) >= 1 && Number(m[1]) <= source.length);
+      const matching = numbered.filter((m) => source[Number(m[1]) - 1] === m[2]);
+      // Every line the response numbers for this file is that whole source line.
+      expect(matching.length).toBeGreaterThan(20);
+    });
+
+    it('reports the cut rather than presenting a window as the whole file', () => {
+      expect(fileOf(GIANT).clipped).toBe(true);
+    });
+
+    it('keeps the response inside the hard ceiling', () => {
+      expect(report.envelope.chars).toBeLessThanOrEqual(report.budget.hardCeiling);
+    });
+  });
+});

+ 6 - 0
__tests__/fixtures/oversize-member-ts/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "oversize-member-fixture",
+  "version": "1.0.0",
+  "private": true,
+  "type": "module"
+}

+ 14 - 0
__tests__/fixtures/oversize-member-ts/src/index.ts

@@ -0,0 +1,14 @@
+import { buildMonthlyReport } from './report/monthly';
+import { buildWeeklyReport } from './report/weekly';
+import { buildQuarterlyReport } from './report/quarterly';
+import { formatReportRows } from './report/format';
+import type { Ledger, ReportOptions } from './report/types';
+
+/** Run every report for a ledger and render them. */
+export function runReports(ledger: Ledger, options: ReportOptions): string {
+  return [
+    formatReportRows(buildMonthlyReport(ledger, options)),
+    formatReportRows(buildWeeklyReport(ledger, options)),
+    formatReportRows(buildQuarterlyReport(ledger, options)),
+  ].join('\n\n');
+}

+ 22 - 0
__tests__/fixtures/oversize-member-ts/src/report/format.ts

@@ -0,0 +1,22 @@
+import type { ReportRow } from './types';
+
+/** Format one category total as a report row. */
+export function formatReportRow(category: string, amountCents: number, currency: string): ReportRow {
+  return {
+    category,
+    amount: formatAmount(amountCents),
+    currency,
+  };
+}
+
+/** Render cents as a fixed-point amount. */
+export function formatAmount(amountCents: number): string {
+  const sign = amountCents < 0 ? '-' : '';
+  const abs = Math.abs(amountCents);
+  return `${sign}${Math.floor(abs / 100)}.${String(abs % 100).padStart(2, '0')}`;
+}
+
+/** Render a set of rows as plain text. */
+export function formatReportRows(rows: ReportRow[]): string {
+  return rows.map((row) => `${row.category}\t${row.amount} ${row.currency}`).join('\n');
+}

+ 509 - 0
__tests__/fixtures/oversize-member-ts/src/report/monthly.ts

@@ -0,0 +1,509 @@
+import { formatReportRow } from './format';
+import { persistReport } from './store';
+import type { Ledger, ReportOptions, ReportRow } from './types';
+
+/**
+ * Build the monthly report for one ledger.
+ *
+ * Every expense category is accrued in its own block so the finance team can
+ * read the month end-to-end in one place; the shape is deliberately flat.
+ */
+export function buildMonthlyReport(ledger: Ledger, options: ReportOptions): ReportRow[] {
+  const rows: ReportRow[] = [];
+  const totals = new Map<string, number>();
+
+  // 1. payroll — accrue the payroll component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'payroll');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('payroll', adjusted, options.currency));
+      totals.set('payroll', adjusted);
+    }
+  }
+
+  // 2. benefits — accrue the benefits component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'benefits');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('benefits', adjusted, options.currency));
+      totals.set('benefits', adjusted);
+    }
+  }
+
+  // 3. travel — accrue the travel component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'travel');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('travel', adjusted, options.currency));
+      totals.set('travel', adjusted);
+    }
+  }
+
+  // 4. equipment — accrue the equipment component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'equipment');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('equipment', adjusted, options.currency));
+      totals.set('equipment', adjusted);
+    }
+  }
+
+  // 5. software — accrue the software component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'software');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('software', adjusted, options.currency));
+      totals.set('software', adjusted);
+    }
+  }
+
+  // 6. contractors — accrue the contractors component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'contractors');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('contractors', adjusted, options.currency));
+      totals.set('contractors', adjusted);
+    }
+  }
+
+  // 7. marketing — accrue the marketing component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'marketing');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('marketing', adjusted, options.currency));
+      totals.set('marketing', adjusted);
+    }
+  }
+
+  // 8. training — accrue the training component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'training');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('training', adjusted, options.currency));
+      totals.set('training', adjusted);
+    }
+  }
+
+  // 9. utilities — accrue the utilities component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'utilities');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('utilities', adjusted, options.currency));
+      totals.set('utilities', adjusted);
+    }
+  }
+
+  // 10. rent — accrue the rent component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'rent');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('rent', adjusted, options.currency));
+      totals.set('rent', adjusted);
+    }
+  }
+
+  // 11. insurance — accrue the insurance component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'insurance');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('insurance', adjusted, options.currency));
+      totals.set('insurance', adjusted);
+    }
+  }
+
+  // 12. legal — accrue the legal component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'legal');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('legal', adjusted, options.currency));
+      totals.set('legal', adjusted);
+    }
+  }
+
+  // 13. shipping — accrue the shipping component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'shipping');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('shipping', adjusted, options.currency));
+      totals.set('shipping', adjusted);
+    }
+  }
+
+  // 14. hosting — accrue the hosting component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'hosting');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('hosting', adjusted, options.currency));
+      totals.set('hosting', adjusted);
+    }
+  }
+
+  // 15. support — accrue the support component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'support');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('support', adjusted, options.currency));
+      totals.set('support', adjusted);
+    }
+  }
+
+  // 16. recruiting — accrue the recruiting component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'recruiting');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('recruiting', adjusted, options.currency));
+      totals.set('recruiting', adjusted);
+    }
+  }
+
+  // 17. licenses — accrue the licenses component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'licenses');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('licenses', adjusted, options.currency));
+      totals.set('licenses', adjusted);
+    }
+  }
+
+  // 18. taxes — accrue the taxes component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'taxes');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('taxes', adjusted, options.currency));
+      totals.set('taxes', adjusted);
+    }
+  }
+
+  // 19. refunds — accrue the refunds component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'refunds');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('refunds', adjusted, options.currency));
+      totals.set('refunds', adjusted);
+    }
+  }
+
+  // 20. discounts — accrue the discounts component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'discounts');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('discounts', adjusted, options.currency));
+      totals.set('discounts', adjusted);
+    }
+  }
+
+  // 21. interest — accrue the interest component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'interest');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('interest', adjusted, options.currency));
+      totals.set('interest', adjusted);
+    }
+  }
+
+  // 22. depreciation — accrue the depreciation component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'depreciation');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('depreciation', adjusted, options.currency));
+      totals.set('depreciation', adjusted);
+    }
+  }
+
+  // 23. maintenance — accrue the maintenance component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'maintenance');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('maintenance', adjusted, options.currency));
+      totals.set('maintenance', adjusted);
+    }
+  }
+
+  // 24. subscriptions — accrue the subscriptions component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'subscriptions');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('subscriptions', adjusted, options.currency));
+      totals.set('subscriptions', adjusted);
+    }
+  }
+
+  // 25. hardware — accrue the hardware component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'hardware');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('hardware', adjusted, options.currency));
+      totals.set('hardware', adjusted);
+    }
+  }
+
+  // 26. catering — accrue the catering component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'catering');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('catering', adjusted, options.currency));
+      totals.set('catering', adjusted);
+    }
+  }
+
+  // 27. conferences — accrue the conferences component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'conferences');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('conferences', adjusted, options.currency));
+      totals.set('conferences', adjusted);
+    }
+  }
+
+  // 28. advertising — accrue the advertising component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'advertising');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('advertising', adjusted, options.currency));
+      totals.set('advertising', adjusted);
+    }
+  }
+
+  // 29. research — accrue the research component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'research');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('research', adjusted, options.currency));
+      totals.set('research', adjusted);
+    }
+  }
+
+  // 30. logistics — accrue the logistics component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'logistics');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('logistics', adjusted, options.currency));
+      totals.set('logistics', adjusted);
+    }
+  }
+
+  // 31. warranty — accrue the warranty component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'warranty');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('warranty', adjusted, options.currency));
+      totals.set('warranty', adjusted);
+    }
+  }
+
+  // 32. penalties — accrue the penalties component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'penalties');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('penalties', adjusted, options.currency));
+      totals.set('penalties', adjusted);
+    }
+  }
+
+  // 33. bonuses — accrue the bonuses component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'bonuses');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('bonuses', adjusted, options.currency));
+      totals.set('bonuses', adjusted);
+    }
+  }
+
+  // 34. commissions — accrue the commissions component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'commissions');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('commissions', adjusted, options.currency));
+      totals.set('commissions', adjusted);
+    }
+  }
+
+  // 35. relocation — accrue the relocation component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'relocation');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('relocation', adjusted, options.currency));
+      totals.set('relocation', adjusted);
+    }
+  }
+
+  // 36. tooling — accrue the tooling component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'tooling');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('tooling', adjusted, options.currency));
+      totals.set('tooling', adjusted);
+    }
+  }
+
+  // 37. audit — accrue the audit component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'audit');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('audit', adjusted, options.currency));
+      totals.set('audit', adjusted);
+    }
+  }
+
+  // 38. compliance — accrue the compliance component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'compliance');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('compliance', adjusted, options.currency));
+      totals.set('compliance', adjusted);
+    }
+  }
+
+  // 39. storage — accrue the storage component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'storage');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('storage', adjusted, options.currency));
+      totals.set('storage', adjusted);
+    }
+  }
+
+  // 40. bandwidth — accrue the bandwidth component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'bandwidth');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('bandwidth', adjusted, options.currency));
+      totals.set('bandwidth', adjusted);
+    }
+  }
+
+  const grandTotal = [...totals.values()].reduce((sum, value) => sum + value, 0);
+  rows.push(formatReportRow('total', grandTotal, options.currency));
+  persistReport(ledger.periodId, rows);
+  return rows;
+}
+
+/** Header line for a rendered monthly report. */
+export function monthlyReportHeader(ledger: Ledger, options: ReportOptions): string {
+  return `Monthly report ${ledger.periodId} (${options.currency})`;
+}
+
+/** Footer line for a rendered monthly report. */
+export function monthlyReportFooter(rows: ReportRow[]): string {
+  return `${rows.length} categories reported`;
+}

+ 235 - 0
__tests__/fixtures/oversize-member-ts/src/report/quarterly.ts

@@ -0,0 +1,235 @@
+import { formatReportRow } from './format';
+import { persistReport } from './store';
+import type { Ledger, ReportOptions, ReportRow } from './types';
+
+/** Build the quarterly report for one ledger. */
+export function buildQuarterlyReport(ledger: Ledger, options: ReportOptions): ReportRow[] {
+  const rows: ReportRow[] = [];
+  const totals = new Map<string, number>();
+
+  // 1. insurance — accrue the insurance component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'insurance');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('insurance', adjusted, options.currency));
+      totals.set('insurance', adjusted);
+    }
+  }
+
+  // 2. legal — accrue the legal component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'legal');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('legal', adjusted, options.currency));
+      totals.set('legal', adjusted);
+    }
+  }
+
+  // 3. shipping — accrue the shipping component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'shipping');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('shipping', adjusted, options.currency));
+      totals.set('shipping', adjusted);
+    }
+  }
+
+  // 4. hosting — accrue the hosting component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'hosting');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('hosting', adjusted, options.currency));
+      totals.set('hosting', adjusted);
+    }
+  }
+
+  // 5. support — accrue the support component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'support');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('support', adjusted, options.currency));
+      totals.set('support', adjusted);
+    }
+  }
+
+  // 6. recruiting — accrue the recruiting component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'recruiting');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('recruiting', adjusted, options.currency));
+      totals.set('recruiting', adjusted);
+    }
+  }
+
+  // 7. licenses — accrue the licenses component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'licenses');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('licenses', adjusted, options.currency));
+      totals.set('licenses', adjusted);
+    }
+  }
+
+  // 8. taxes — accrue the taxes component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'taxes');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('taxes', adjusted, options.currency));
+      totals.set('taxes', adjusted);
+    }
+  }
+
+  // 9. refunds — accrue the refunds component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'refunds');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('refunds', adjusted, options.currency));
+      totals.set('refunds', adjusted);
+    }
+  }
+
+  // 10. discounts — accrue the discounts component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'discounts');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('discounts', adjusted, options.currency));
+      totals.set('discounts', adjusted);
+    }
+  }
+
+  // 11. interest — accrue the interest component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'interest');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('interest', adjusted, options.currency));
+      totals.set('interest', adjusted);
+    }
+  }
+
+  // 12. depreciation — accrue the depreciation component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'depreciation');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('depreciation', adjusted, options.currency));
+      totals.set('depreciation', adjusted);
+    }
+  }
+
+  // 13. maintenance — accrue the maintenance component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'maintenance');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('maintenance', adjusted, options.currency));
+      totals.set('maintenance', adjusted);
+    }
+  }
+
+  // 14. subscriptions — accrue the subscriptions component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'subscriptions');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('subscriptions', adjusted, options.currency));
+      totals.set('subscriptions', adjusted);
+    }
+  }
+
+  // 15. hardware — accrue the hardware component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'hardware');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('hardware', adjusted, options.currency));
+      totals.set('hardware', adjusted);
+    }
+  }
+
+  // 16. catering — accrue the catering component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'catering');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('catering', adjusted, options.currency));
+      totals.set('catering', adjusted);
+    }
+  }
+
+  // 17. conferences — accrue the conferences component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'conferences');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('conferences', adjusted, options.currency));
+      totals.set('conferences', adjusted);
+    }
+  }
+
+  // 18. advertising — accrue the advertising component of the month.
+  {
+    const bucket = ledger.entries.filter((entry) => entry.category === 'advertising');
+    const gross = bucket.reduce((sum, entry) => sum + entry.amountCents, 0);
+    const pending = bucket.filter((entry) => entry.pending).reduce((s, e) => s + e.amountCents, 0);
+    const adjusted = options.includePending ? gross : gross - pending;
+    if (adjusted !== 0 || options.includeEmptyCategories) {
+      rows.push(formatReportRow('advertising', adjusted, options.currency));
+      totals.set('advertising', adjusted);
+    }
+  }
+
+  const grandTotal = [...totals.values()].reduce((sum, value) => sum + value, 0);
+  rows.push(formatReportRow('total', grandTotal, options.currency));
+  persistReport(ledger.periodId, rows);
+  return rows;
+}
+
+/** Header line for a rendered quarterly report. */
+export function buildQuarterlyReportHeader(ledger: Ledger, options: ReportOptions): string {
+  return `quarterly report ${ledger.periodId} (${options.currency})`;
+}

+ 18 - 0
__tests__/fixtures/oversize-member-ts/src/report/store.ts

@@ -0,0 +1,18 @@
+import type { ReportRow } from './types';
+
+const saved = new Map<string, ReportRow[]>();
+
+/** Persist a built report for a period. */
+export function persistReport(periodId: string, rows: ReportRow[]): void {
+  saved.set(periodId, rows);
+}
+
+/** Read back a persisted report. */
+export function loadReport(periodId: string): ReportRow[] {
+  return saved.get(periodId) ?? [];
+}
+
+/** Drop a persisted report. */
+export function clearReport(periodId: string): void {
+  saved.delete(periodId);
+}

+ 28 - 0
__tests__/fixtures/oversize-member-ts/src/report/types.ts

@@ -0,0 +1,28 @@
+/** One posted ledger entry. */
+export interface LedgerEntry {
+  id: string;
+  category: string;
+  amountCents: number;
+  pending: boolean;
+  postedAt: string;
+}
+
+/** A period's ledger. */
+export interface Ledger {
+  periodId: string;
+  entries: LedgerEntry[];
+}
+
+/** How a report should be built. */
+export interface ReportOptions {
+  currency: string;
+  includePending: boolean;
+  includeEmptyCategories: boolean;
+}
+
+/** One rendered report line. */
+export interface ReportRow {
+  category: string;
+  amount: string;
+  currency: string;
+}

+ 372 - 0
__tests__/fixtures/oversize-member-ts/src/report/weekly.ts

@@ -0,0 +1,372 @@
+import { formatReportRow } from './format';
+import { persistReport } from './store';
+import type { Ledger, ReportOptions, ReportRow } from './types';
+
+/** Total the posted entries in one category. */
+function sumOf(ledger: Ledger, category: string): number {
+  return ledger.entries
+    .filter((entry) => entry.category === category && !entry.pending)
+    .reduce((sum, entry) => sum + entry.amountCents, 0);
+}
+
+/** Total the still-pending entries in one category. */
+function pendingOf(ledger: Ledger, category: string): number {
+  return ledger.entries
+    .filter((entry) => entry.category === category && entry.pending)
+    .reduce((sum, entry) => sum + entry.amountCents, 0);
+}
+
+/** Build the weekly report for one ledger. */
+export function buildWeeklyReport(ledger: Ledger, options: ReportOptions): ReportRow[] {
+  const rows: ReportRow[] = [];
+  const totals = new Map<string, number>();
+
+  // 1. payroll
+  {
+    const gross = sumOf(ledger, 'payroll');
+    const held = pendingOf(ledger, 'payroll');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('payroll', net, options.currency));
+      totals.set('payroll', net);
+    }
+  }
+
+  // 2. benefits
+  {
+    const gross = sumOf(ledger, 'benefits');
+    const held = pendingOf(ledger, 'benefits');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('benefits', net, options.currency));
+      totals.set('benefits', net);
+    }
+  }
+
+  // 3. travel
+  {
+    const gross = sumOf(ledger, 'travel');
+    const held = pendingOf(ledger, 'travel');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('travel', net, options.currency));
+      totals.set('travel', net);
+    }
+  }
+
+  // 4. equipment
+  {
+    const gross = sumOf(ledger, 'equipment');
+    const held = pendingOf(ledger, 'equipment');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('equipment', net, options.currency));
+      totals.set('equipment', net);
+    }
+  }
+
+  // 5. software
+  {
+    const gross = sumOf(ledger, 'software');
+    const held = pendingOf(ledger, 'software');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('software', net, options.currency));
+      totals.set('software', net);
+    }
+  }
+
+  // 6. contractors
+  {
+    const gross = sumOf(ledger, 'contractors');
+    const held = pendingOf(ledger, 'contractors');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('contractors', net, options.currency));
+      totals.set('contractors', net);
+    }
+  }
+
+  // 7. marketing
+  {
+    const gross = sumOf(ledger, 'marketing');
+    const held = pendingOf(ledger, 'marketing');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('marketing', net, options.currency));
+      totals.set('marketing', net);
+    }
+  }
+
+  // 8. training
+  {
+    const gross = sumOf(ledger, 'training');
+    const held = pendingOf(ledger, 'training');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('training', net, options.currency));
+      totals.set('training', net);
+    }
+  }
+
+  // 9. utilities
+  {
+    const gross = sumOf(ledger, 'utilities');
+    const held = pendingOf(ledger, 'utilities');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('utilities', net, options.currency));
+      totals.set('utilities', net);
+    }
+  }
+
+  // 10. rent
+  {
+    const gross = sumOf(ledger, 'rent');
+    const held = pendingOf(ledger, 'rent');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('rent', net, options.currency));
+      totals.set('rent', net);
+    }
+  }
+
+  // 11. insurance
+  {
+    const gross = sumOf(ledger, 'insurance');
+    const held = pendingOf(ledger, 'insurance');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('insurance', net, options.currency));
+      totals.set('insurance', net);
+    }
+  }
+
+  // 12. legal
+  {
+    const gross = sumOf(ledger, 'legal');
+    const held = pendingOf(ledger, 'legal');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('legal', net, options.currency));
+      totals.set('legal', net);
+    }
+  }
+
+  // 13. shipping
+  {
+    const gross = sumOf(ledger, 'shipping');
+    const held = pendingOf(ledger, 'shipping');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('shipping', net, options.currency));
+      totals.set('shipping', net);
+    }
+  }
+
+  // 14. hosting
+  {
+    const gross = sumOf(ledger, 'hosting');
+    const held = pendingOf(ledger, 'hosting');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('hosting', net, options.currency));
+      totals.set('hosting', net);
+    }
+  }
+
+  // 15. support
+  {
+    const gross = sumOf(ledger, 'support');
+    const held = pendingOf(ledger, 'support');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('support', net, options.currency));
+      totals.set('support', net);
+    }
+  }
+
+  // 16. recruiting
+  {
+    const gross = sumOf(ledger, 'recruiting');
+    const held = pendingOf(ledger, 'recruiting');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('recruiting', net, options.currency));
+      totals.set('recruiting', net);
+    }
+  }
+
+  // 17. licenses
+  {
+    const gross = sumOf(ledger, 'licenses');
+    const held = pendingOf(ledger, 'licenses');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('licenses', net, options.currency));
+      totals.set('licenses', net);
+    }
+  }
+
+  // 18. taxes
+  {
+    const gross = sumOf(ledger, 'taxes');
+    const held = pendingOf(ledger, 'taxes');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('taxes', net, options.currency));
+      totals.set('taxes', net);
+    }
+  }
+
+  // 19. refunds
+  {
+    const gross = sumOf(ledger, 'refunds');
+    const held = pendingOf(ledger, 'refunds');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('refunds', net, options.currency));
+      totals.set('refunds', net);
+    }
+  }
+
+  // 20. discounts
+  {
+    const gross = sumOf(ledger, 'discounts');
+    const held = pendingOf(ledger, 'discounts');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('discounts', net, options.currency));
+      totals.set('discounts', net);
+    }
+  }
+
+  // 21. interest
+  {
+    const gross = sumOf(ledger, 'interest');
+    const held = pendingOf(ledger, 'interest');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('interest', net, options.currency));
+      totals.set('interest', net);
+    }
+  }
+
+  // 22. depreciation
+  {
+    const gross = sumOf(ledger, 'depreciation');
+    const held = pendingOf(ledger, 'depreciation');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('depreciation', net, options.currency));
+      totals.set('depreciation', net);
+    }
+  }
+
+  // 23. maintenance
+  {
+    const gross = sumOf(ledger, 'maintenance');
+    const held = pendingOf(ledger, 'maintenance');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('maintenance', net, options.currency));
+      totals.set('maintenance', net);
+    }
+  }
+
+  // 24. subscriptions
+  {
+    const gross = sumOf(ledger, 'subscriptions');
+    const held = pendingOf(ledger, 'subscriptions');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('subscriptions', net, options.currency));
+      totals.set('subscriptions', net);
+    }
+  }
+
+  // 25. hardware
+  {
+    const gross = sumOf(ledger, 'hardware');
+    const held = pendingOf(ledger, 'hardware');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('hardware', net, options.currency));
+      totals.set('hardware', net);
+    }
+  }
+
+  // 26. catering
+  {
+    const gross = sumOf(ledger, 'catering');
+    const held = pendingOf(ledger, 'catering');
+    const net = options.includePending
+      ? gross
+      : gross - held;
+    if (net !== 0) {
+      rows.push(formatReportRow('catering', net, options.currency));
+      totals.set('catering', net);
+    }
+  }
+
+  const grandTotal = [...totals.values()]
+    .reduce((sum, value) => sum + value, 0);
+  rows.push(formatReportRow('total', grandTotal, options.currency));
+  persistReport(ledger.periodId, rows);
+  return rows;
+}
+
+/** Header line for a rendered weekly report. */
+export function buildWeeklyReportHeader(ledger: Ledger, options: ReportOptions): string {
+  return `weekly report ${ledger.periodId} (${options.currency})`;
+}

+ 27 - 1
src/mcp/explore-diagnostics.ts

@@ -92,6 +92,15 @@ interface FileRecord extends ExploreCandidateMeta {
    * means an oversize first cluster or the whole-file grace overshot.
    */
   allowance: number | null;
+  /**
+   * What the file could actually SPEND: its reservation plus the slack the
+   * files above it left on the table (bounded by MAX_SHARE). Every render bound
+   * reads this, not `allowance`, so it — not the reservation — is what an
+   * overshoot is measured against. `null` until the render loop reaches the
+   * file. Reporting only `allowance` makes an ordinary carry-forward look like
+   * a file spending over its reservation.
+   */
+  spendable: number | null;
   render?: ExploreRenderMode;
   /**
    * Source chars this call did NOT re-send because an earlier call in the
@@ -139,6 +148,8 @@ interface BudgetShape {
 export interface ExploreDiagnosticFile extends ExploreCandidateMeta {
   path: string;
   allowance: number | null;
+  /** Reservation + inherited slack — the bound the render paths actually use. */
+  spendable: number | null;
   render: ExploreRenderMode | null;
   skipped: ExploreSkipReason | null;
   clipped: boolean;
@@ -362,7 +373,7 @@ export class ExploreDiagnostics {
   /** Record one ranked candidate's scoring inputs, in final sort order. */
   noteCandidate(path: string, meta: ExploreCandidateMeta): void {
     this.files.set(path, {
-      path, ...meta, allowance: null,
+      path, ...meta, allowance: null, spendable: null,
       dedupSavedChars: 0, dedupCovered: [],
       emittedChars: 0, finalChars: 0, share: 0, allocatedShare: 0, clipped: false,
     });
@@ -391,6 +402,15 @@ export class ExploreDiagnostics {
     }
   }
 
+  /**
+   * What the render loop will let this file spend — reservation plus inherited
+   * slack. Called once per file, before any of its render paths run.
+   */
+  recordSpendable(path: string, chars: number): void {
+    const rec = this.files.get(path);
+    if (rec) rec.spendable = chars;
+  }
+
   /** A candidate rendered source into the response. */
   recordRender(path: string, render: ExploreRenderMode, sourceChars: number, clipped: boolean): void {
     const rec = this.files.get(path);
@@ -538,6 +558,7 @@ export class ExploreDiagnostics {
           penalty: round6(r.penalty),
           kinds: r.kinds,
           allowance: r.allowance,
+          spendable: r.spendable,
           render: r.render ?? null,
           skipped: r.skipped ?? null,
           clipped: r.clipped,
@@ -704,6 +725,11 @@ export function renderTable(report: ExploreDiagnosticReport): string {
         f.path,
       );
       out.push('        kinds: ' + (f.kinds || '-'));
+      // Only when it differs: a file that spent over `reserved` but inside
+      // `spendable` took inherited slack, not a budget bug.
+      if (f.spendable !== null && f.allowance !== null && f.spendable !== f.allowance) {
+        out.push(`        spendable: ${num(f.spendable)} (reservation + inherited slack)`);
+      }
       if (f.dedupSavedChars > 0) {
         const spans = f.dedupCovered.slice(0, 6).map(([a, b]) => (a === b ? `${a}` : `${a}-${b}`)).join(',');
         const more = f.dedupCovered.length > 6 ? `,+${f.dedupCovered.length - 6}` : '';

+ 150 - 9
src/mcp/tools.ts

@@ -4093,6 +4093,7 @@ export class ToolHandler {
         Math.max(reserved, Math.round(budget.maxOutputChars * EXPLORE_ALLOCATION.MAX_SHARE)),
       );
       reservedSoFar += reserved;
+      diag?.recordSpendable(filePath, allowance);
       const absPath = validatePathWithinRoot(projectRoot, filePath);
       if (!absPath || !existsSync(absPath)) {
         diag?.recordSkip(filePath, 'unreadable');
@@ -4731,7 +4732,9 @@ export class ToolHandler {
         for (const r of byImportance) {
           const sz = sizeOf(r) + GAP_MARKER.length;
           // Always keep the most important range, even if it alone is oversize —
-          // an empty section sends the agent to Read, which costs far more.
+          // an empty section sends the agent to Read, which costs far more. How
+          // far it may overshoot is bounded by the caller's ceiling (CG-30), which
+          // windows a runaway member instead of dropping it.
           if (keep.length > 0 && kept + sz > cap) continue;
           keep.push(r);
           kept += sz;
@@ -4749,6 +4752,117 @@ export class ToolHandler {
         return merged.flatMap((m) => buildSection(m));
       };
 
+      /**
+       * Bounded overshoot for one cluster's render (CG-30).
+       *
+       * `shrinkCluster` keeps the highest-importance member whole even when that
+       * member alone is oversize — an empty file section sends the agent to Read,
+       * which is exactly what explore exists to prevent. But "never empty" is not
+       * "any size": with nothing bounding it, one 22K member rendered against a
+       * 9K reservation (2.4x), which collapses the headroom every file ranked
+       * below it draws from. Past the ceiling the member is WINDOWED rather than
+       * dropped — a leading window (signature + head of the body), plus a window
+       * on the spine's call site when the head misses it, since on a flow cluster
+       * the call path IS the answer.
+       */
+      const MIN_WINDOW_LINES = 12;
+      /** Rendered cost of one source line, line numbering included. */
+      const lineCost = (ln: number): number =>
+        (fileLines[ln - 1] ?? '').length + 1 + (withLineNumbers ? String(ln).length + 1 : 0);
+      /**
+       * Longest prefix of `r` that fits `room`. `minLines` is the never-empty
+       * floor — it may overrun `room`, so it is only ever asked for when nothing
+       * else has been emitted and the alternative is an empty section.
+       */
+      const headWindowOf = (
+        r: ExploreLineRange, room: number, minLines = 0,
+      ): ExploreLineRange | null => {
+        let end = r.start - 1;
+        let chars = 0;
+        for (let ln = r.start; ln <= r.end; ln++) {
+          const cost = lineCost(ln);
+          if (chars + cost > room && end - r.start + 1 >= minLines) break;
+          chars += cost;
+          end = ln;
+        }
+        return end >= r.start ? { start: r.start, end } : null;
+      };
+      /** Widest window around `line` inside [lo, hi] that fits `room`. */
+      const centeredWindowOf = (
+        line: number, lo: number, hi: number, room: number,
+      ): ExploreLineRange | null => {
+        if (line < lo || line > hi) return null;
+        let start = line, end = line, chars = lineCost(line);
+        for (let grown = true; grown;) {
+          grown = false;
+          if (end + 1 <= hi && chars + lineCost(end + 1) <= room) { end += 1; chars += lineCost(end); grown = true; }
+          if (start - 1 >= lo && chars + lineCost(start - 1) <= room) { start -= 1; chars += lineCost(start); grown = true; }
+        }
+        return { start, end };
+      };
+      /**
+       * Reduce rendered parts to fit `ceiling`, never to nothing. Whole parts are
+       * kept while they fit; the first part that overruns is cut to a leading
+       * window on whole lines (a body is never cut mid-line), and everything past
+       * it is dropped. The GAP_MARKER between surviving parts — and the line-number
+       * jump — is what tells the agent the cut happened.
+       *
+       * A partial window shorter than MIN_WINDOW_LINES is not worth emitting, and
+       * emitting one is actively harmful: the session record then claims a 4-line
+       * sliver, and the NEXT call's dedup has to either shred a whole block around
+       * it or re-send it. Below that floor the part is simply dropped — unless
+       * nothing has been emitted at all, where the floor wins over the ceiling
+       * because an empty section is the one outcome worse than an oversize one.
+       */
+      const windowToCeiling = (
+        parts: ReadonlyArray<SectionPart>,
+        ceiling: number,
+        focusLine?: number,
+      ): SectionPart[] => {
+        const emit: ExploreLineRange[] = [];
+        const inParts = (line: number) =>
+          parts.some((p) => line >= p.range.start && line <= p.range.end);
+        const needFocus = typeof focusLine === 'number' && focusLine > 0 && inParts(focusLine);
+        // Hold room back for the call site so the head window can't eat all of it.
+        const headRoom = needFocus ? Math.floor(ceiling * 0.6) : ceiling;
+        let used = 0;
+        for (const p of parts) {
+          const join = emit.length > 0 ? GAP_MARKER.length : 0;
+          if (used + join + p.text.length <= headRoom) {
+            emit.push(p.range);
+            used += join + p.text.length;
+            continue;
+          }
+          const first = emit.length === 0;
+          const win = headWindowOf(
+            p.range, Math.max(0, headRoom - used - join), first ? MIN_WINDOW_LINES : 0);
+          if (win && (first || win.end - win.start + 1 >= MIN_WINDOW_LINES)) {
+            emit.push(win);
+            used += join + renderSpan(win).length;
+          }
+          break;
+        }
+        const last = emit[emit.length - 1];
+        if (needFocus && (!last || focusLine! > last.end)) {
+          const host = parts.find((p) => focusLine! >= p.range.start && focusLine! <= p.range.end)!;
+          const lo = Math.max(host.range.start, focusLine! - SPINE_WINDOW, last ? last.end + 1 : 0);
+          const hi = Math.min(host.range.end, focusLine! + SPINE_WINDOW);
+          const win = centeredWindowOf(
+            focusLine!, lo, hi, Math.max(0, ceiling - used - GAP_MARKER.length));
+          // Same sliver floor as the head window — a two-line peek at the call
+          // site teaches the next call's dedup to shred the block around it.
+          if (win && win.end - win.start + 1 >= MIN_WINDOW_LINES) emit.push(win);
+        }
+        // Never empty: a section with no source sends the agent to Read.
+        if (emit.length === 0 && parts.length > 0) {
+          const first = headWindowOf(parts[0]!.range, ceiling, MIN_WINDOW_LINES);
+          if (first) emit.push(first);
+        }
+        return emit
+          .sort((a, b) => a.start - b.start)
+          .map((r) => ({ range: r, text: renderSpan(r) }));
+      };
+
       /**
        * One cluster's final parts: built, shrunk if it overruns `cap`, then
        * passed through the session history (CG-18).
@@ -4761,15 +4875,33 @@ export class ToolHandler {
       const renderCluster = (
         c: ExploreCluster,
         cap: number,
+        /**
+         * Hard bound on the rendered result (CG-30). `cap` is what selection asks
+         * for; this is how far a single oversize member is allowed to overshoot it
+         * before being windowed. Always >= `cap`, so a cluster that already fits is
+         * never touched.
+         */
+        ceiling: number = Infinity,
       ): { parts: SectionPart[]; covered: ExploreLineRange[]; shrunk: boolean } => {
         const base = dedupeSpans(buildSection(c));
+        const bound = (
+          r: { parts: SectionPart[]; covered: ExploreLineRange[]; shrunk: boolean },
+        ) => {
+          if (!Number.isFinite(ceiling) || sectionText(r.parts).length <= ceiling) return r;
+          // Windows are subsets of spans dedupeSpans already cleared, so the record
+          // still only ever claims source that was actually sent.
+          const parts = windowToCeiling(r.parts, ceiling, c.spineCallLine);
+          return { parts, covered: r.covered, shrunk: true };
+        };
         if (sectionText(base.parts).length <= cap) {
           return { parts: base.parts, covered: base.covered, shrunk: false };
         }
         const shrunk = shrinkCluster(c, cap);
-        if (shrunk === null) return { parts: base.parts, covered: base.covered, shrunk: false };
+        if (shrunk === null) {
+          return bound({ parts: base.parts, covered: base.covered, shrunk: false });
+        }
         const dd = dedupeSpans(shrunk);
-        return { parts: dd.parts, covered: dd.covered, shrunk: true };
+        return bound({ parts: dd.parts, covered: dd.covered, shrunk: true });
       };
 
       // Rank clusters for inclusion under the per-file cap. Entry-point
@@ -4830,7 +4962,13 @@ export class ToolHandler {
         // clusters are never shrunk — they either fit or wait for another call.
         const first = chosenIndices.size === 0;
         const cap = rc.c.hasSpine ? SPINE_CEILING : fileBudget;
-        const section = renderCluster(rc.c, first ? cap : Infinity);
+        // CG-30: shrinking keeps the top member whole however big it is, so bound
+        // how far that member may overshoot — the same 1.5x-of-reservation bound
+        // SPINE_CEILING already draws, never below `cap` (a cluster that fits its
+        // cap is never windowed). A spine cluster's cap already IS that bound, so
+        // this holds it to it rather than letting the member rule walk past it.
+        const ceiling = Math.max(cap, SPINE_CEILING);
+        const section = renderCluster(rc.c, first ? cap : Infinity, first ? ceiling : Infinity);
         const text = sectionText(section.parts);
         const sectionLen = text.length + (!first && text.length > 0 ? GAP_MARKER.length : 0);
         if (first) {
@@ -4872,10 +5010,11 @@ export class ToolHandler {
 
       // A chosen cluster is a COMPLETE method-range — we never cut through a body,
       // and a shrunk cluster drops WHOLE members for the same reason. An oversize
-      // single MEMBER (one long monolithic function) still renders in full: half a
-      // method is useless (the agent just Reads the rest for the other half), which
-      // is the very fallback explore exists to prevent. A pathological file is
-      // bounded by the cluster SELECTION above + the total hard ceiling.
+      // single MEMBER (one long monolithic function) is kept whole for as long as
+      // it fits the bounded overshoot (half a method is useless — the agent just
+      // Reads the rest, the fallback explore exists to prevent); past that bound it
+      // is WINDOWED on whole lines rather than dropped (CG-30), so a god-method
+      // can neither be silently lost nor spend the response's whole envelope.
       if (chosenIndices.size < clusters.length || anyClusterShrunk) {
         anyFileTrimmed = true;
       }
@@ -4928,7 +5067,9 @@ export class ToolHandler {
         covered: mergeRanges(coveredRanges),
         overhead: 200,
         mode: 'clusters',
-        clipped: chosenIndices.size < clusters.length,
+        // Windowing an oversize member elides source too — reporting it as
+        // unclipped would hide exactly the cut the diagnostic exists to show.
+        clipped: chosenIndices.size < clusters.length || anyClusterShrunk,
         fullBody: sectionText(fullClusterParts),
         fullRanges: fullClusterParts.map((p) => p.range),
       });