-**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro**
+**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and GitHub Copilot**
[Report Bug](https://github.com/colbymchenry/codegraph/issues) · [Request Feature](https://github.com/colbymchenry/codegraph/issues)
diff --git a/__tests__/c-fnptr-synthesizer.test.ts b/__tests__/c-fnptr-synthesizer.test.ts
index 202c9638f..bba9fcadf 100644
--- a/__tests__/c-fnptr-synthesizer.test.ts
+++ b/__tests__/c-fnptr-synthesizer.test.ts
@@ -86,6 +86,45 @@ int dispatch(struct ops o) { return o.handler(); }
expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
});
+ it('bridges function-pointer fields declared in a union', async () => {
+ write('union-ops.c', `
+union ops { int (*handler)(void); };
+static int on_open(void) { return 1; }
+static union ops the_ops = { .handler = on_open };
+
+int dispatch(union ops o) { return o.handler(); }
+`);
+ const edges = await load();
+ expect(has(edges, 'dispatch', 'on_open')).toBe(true);
+ expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
+ });
+
+ it('bridges an inline union table whose entries are macro-built', async () => {
+ write('inline-union.c', `
+#define SLOT(fn) { fn }
+static int on_open(void) { return 1; }
+static union inline_ops { int (*handler)(void); } ops[] = { SLOT(on_open) };
+
+int dispatch(union inline_ops o) { return o.handler(); }
+`);
+ const edges = await load();
+ expect(has(edges, 'dispatch', 'on_open')).toBe(true);
+ });
+
+ it('bridges a union table declared through an object-macro type alias', async () => {
+ write('alias-union.c', `
+#define OPS_TYPE union ops
+#define SLOT(fn) { fn }
+union ops { int (*handler)(void); };
+static int on_open(void) { return 1; }
+static OPS_TYPE ops[] = { SLOT(on_open) };
+
+int dispatch(union ops o) { return o.handler(); }
+`);
+ const edges = await load();
+ expect(has(edges, 'dispatch', 'on_open')).toBe(true);
+ });
+
it('bridges the typedef-field + field←field double-hop (the hook_demo.c shape)', async () => {
write('hook.c', `
typedef void (*hook_func)(void);
diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts
index 52dae1fe8..c46c612d7 100644
--- a/__tests__/context.test.ts
+++ b/__tests__/context.test.ts
@@ -135,10 +135,16 @@ export function validateEmail(email: string): boolean {
`
);
+ fs.writeFileSync(
+ path.join(srcDir, 'callback_ops.c'),
+ `union CallbackOps { int (*run)(int); };
+`
+ );
+
// Initialize CodeGraph
cg = CodeGraph.initSync(testDir, {
config: {
- include: ['**/*.ts'],
+ include: ['**/*.ts', '**/*.c'],
exclude: [],
},
});
@@ -194,6 +200,15 @@ export function validateEmail(email: string): boolean {
).toBe(true);
});
+ it('includes union definitions in the default context search', async () => {
+ const result = await cg.findRelevantContext('CallbackOps');
+ const union = [...result.nodes.values()].find(
+ (node) => node.kind === 'union' && node.name === 'CallbackOps'
+ );
+
+ expect(union).toBeDefined();
+ });
+
it('should include edges in the result', async () => {
const result = await cg.findRelevantContext('checkout', {
traversalDepth: 2,
diff --git a/__tests__/explore-cluster-starvation.test.ts b/__tests__/explore-cluster-starvation.test.ts
new file mode 100644
index 000000000..003534829
--- /dev/null
+++ b/__tests__/explore-cluster-starvation.test.ts
@@ -0,0 +1,170 @@
+/**
+ * Regression gate for CLUSTER-LEVEL STARVATION inside one file (task CG-36).
+ *
+ * A file's ranked clusters used to be all-or-nothing past the first one: the
+ * top-ranked cluster was taken (shrunk to fit if it had to be), and every
+ * cluster below it was rendered whole and then either fit the remainder or was
+ * dropped entirely. On a file whose top-ranked cluster is TRIVIAL that discards
+ * the answer — django's `db/models/sql/query.py` kept a 22-line glue cluster and
+ * dropped the 624-line `Query` body beneath it, spending 1,923 of a 7,947
+ * reservation, and okhttp's `RealInterceptorChain.kt` did the same behind its
+ * import header.
+ *
+ * What makes it hard to see is that the response stays FULL: the unspent
+ * reservation carries forward exactly as designed, so a lower-scoring file takes
+ * the bytes and every envelope-share measure still looks healthy. The gate is
+ * therefore per-file spend, not share.
+ *
+ * Two fixtures, pulling in opposite directions — read them together:
+ *
+ * - `starved-cluster-ts` is the defect. Its answer-bearing cluster must be
+ * SHRUNK into whatever the trivial cluster left, not dropped.
+ * - `dense-header-ts` is the Session.swift shape that cluster ranking puts
+ * importance ahead of density FOR. Its query's methods sit ~200 lines under
+ * a dense property list, and they must keep winning the budget. Any future
+ * rework of selection or shrinking has to satisfy both.
+ */
+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 type { ExploreDiagnosticReport } from '../src/mcp/explore-diagnostics';
+
+interface Run {
+ dir: string;
+ cg: CodeGraph;
+ response: string;
+ report: ExploreDiagnosticReport;
+}
+
+/** Copy a fixture tree to a temp dir, index it, and run one explore call. */
+async function runFixture(fixture: string, query: string): Promise
{
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg36-'));
+ fs.cpSync(path.join(__dirname, 'fixtures', fixture), dir, { recursive: true });
+ fs.rmSync(path.join(dir, '.codegraph'), { recursive: true, force: true });
+
+ const cg = CodeGraph.initSync(dir);
+ await cg.indexAll();
+
+ const sidecar = path.join(dir, 'explore-diag.jsonl');
+ const previous = process.env.CODEGRAPH_EXPLORE_DEBUG;
+ process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
+ let response: string;
+ try {
+ response = (await new ToolHandler(cg).execute('codegraph_explore', { query }))
+ .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);
+ return { dir, cg, response, report: JSON.parse(written[written.length - 1]!) };
+}
+
+function teardown(run: Run | undefined): void {
+ if (!run) return;
+ run.cg.destroy();
+ if (fs.existsSync(run.dir)) fs.rmSync(run.dir, { recursive: true, force: true });
+}
+
+describe('CG-36 — a trivial cluster must not starve the answer-bearing one', () => {
+ const TARGET = 'src/pipeline/chain.ts';
+ const QUERY = 'how does a request travel from sendRequest to the socket';
+ let run: Run;
+ let target: ExploreDiagnosticReport['files'][number];
+
+ beforeAll(async () => {
+ run = await runFixture('starved-cluster-ts', QUERY);
+ target = run.report.files.find((f) => f.path === TARGET)!;
+ }, 120_000);
+
+ afterAll(() => teardown(run));
+
+ describe('fixture shape — if this rots, the gate below means nothing', () => {
+ it('renders through the cluster path, with the answer past the trivial helper', () => {
+ expect(target, `${TARGET} is not among the ranked candidates`).toBeDefined();
+ expect(target.render).toBe('clusters');
+ // The helper the entry point calls directly, and the class it does not.
+ const nodes = run.cg.getNodesInFile(TARGET);
+ const helper = nodes.find((n) => n.name === 'describeChain')!;
+ const proceed = nodes.find((n) => n.name === 'proceed')!;
+ expect(helper).toBeDefined();
+ expect(proceed).toBeDefined();
+ // Far enough apart to cluster separately at any gap threshold we ship.
+ expect(proceed.startLine - helper.endLine).toBeGreaterThan(20);
+ });
+
+ it('reserves the file the largest share, so an unspent share is a defect', () => {
+ expect(target.allowance ?? 0).toBeGreaterThan(4000);
+ const others = run.report.files.filter((f) => f.path !== TARGET);
+ for (const f of others) expect(f.allowance ?? 0).toBeLessThan(target.allowance!);
+ });
+ });
+
+ describe('the gate', () => {
+ it('spends most of the reservation it was given', () => {
+ // 28.8% on the CG-24 epic tip, 131% (its reservation plus carry-forward
+ // slack it can now actually use) with the fix. The bar is deliberately
+ // well below both so ordinary budget movement does not fail the suite.
+ expect(target.finalChars / target.allowance!).toBeGreaterThan(0.6);
+ });
+
+ it('delivers the flow the query asked about, not just the helper beside it', () => {
+ // Both ends of the in-file flow, in the cluster that used to be dropped.
+ expect(run.response).toContain('async proceed(request: PipelineRequest)');
+ expect(run.response).toContain('private async writeAndRead(request: PipelineRequest)');
+ });
+
+ it('keeps the response inside the hard ceiling', () => {
+ expect(run.report.envelope.chars).toBeLessThanOrEqual(run.report.budget.hardCeiling);
+ });
+ });
+});
+
+describe('CG-36 — a dense declaration block must not bury the query\'s methods', () => {
+ const TARGET = 'src/net/session.ts';
+ const QUERY = 'how does perform create a URLRequest and start the task';
+ let run: Run;
+ let target: ExploreDiagnosticReport['files'][number];
+
+ beforeAll(async () => {
+ run = await runFixture('dense-header-ts', QUERY);
+ target = run.report.files.find((f) => f.path === TARGET)!;
+ }, 120_000);
+
+ afterAll(() => teardown(run));
+
+ describe('fixture shape — if this rots, the gate below means nothing', () => {
+ it('has a dense low-importance header and the named methods far below it', () => {
+ expect(target, `${TARGET} is not among the ranked candidates`).toBeDefined();
+ expect(target.render).toBe('clusters');
+ const nodes = run.cg.getNodesInFile(TARGET);
+ const perform = nodes.find((n) => n.name === 'perform')!;
+ expect(perform).toBeDefined();
+ // The header block: many adjacent declarations above the first named
+ // method, which is what makes it the densest region of the file.
+ const above = nodes.filter((n) => n.endLine < perform.startLine
+ && (n.kind === 'property' || n.kind === 'field' || n.kind === 'method'));
+ expect(above.length).toBeGreaterThan(20);
+ expect(perform.startLine).toBeGreaterThan(150);
+ });
+ });
+
+ describe('the gate', () => {
+ it('delivers all three methods the query named', () => {
+ expect(run.response).toContain('async perform(url: string, method: string');
+ expect(run.response).toContain('didCreateURLRequest(request: URLRequest)');
+ expect(run.response).toContain('task(request: URLRequest, identifier: number)');
+ });
+
+ it('spends the file\'s reservation on them', () => {
+ expect(target.finalChars / target.allowance!).toBeGreaterThan(0.6);
+ });
+
+ it('keeps the response inside the hard ceiling', () => {
+ expect(run.report.envelope.chars).toBeLessThanOrEqual(run.report.budget.hardCeiling);
+ });
+ });
+});
diff --git a/__tests__/explore-declaration-only.test.ts b/__tests__/explore-declaration-only.test.ts
new file mode 100644
index 000000000..004711f7d
--- /dev/null
+++ b/__tests__/explore-declaration-only.test.ts
@@ -0,0 +1,207 @@
+/**
+ * Regression gate for DECLARATION-ONLY files in explore ranking (task CG-28).
+ *
+ * A file that holds nothing but type declarations — an ambient `.d.ts`, vendored
+ * typings, a `types.ts` of pure interfaces — cannot answer a FLOW question: no
+ * bodies, no call edges, no behaviour. But the identifiers it declares are
+ * exactly the generic ones a prose question uses (`Body`, `Message`,
+ * `ImageMetadata`, `ReadableStream`), so on term overlap it out-scored the
+ * implementation and took the envelope. Measured on this fixture before the fix:
+ * rank #1 and 51% of delivered source on a prose flow query.
+ *
+ * CG-25 already covers the file that STARTED this — a Wrangler
+ * `worker-configuration.d.ts`, which announces itself with a generated banner.
+ * `docs/benchmarks/explore-declaration-only-cg28.md` has that measurement; the
+ * banner alone is worth 15–46 points of envelope share. What it does not cover
+ * is a declaration file with no banner at all, which is what this fixture's
+ * `platform-shims.d.ts` is, and what the damping in `rankPenalty` addresses.
+ *
+ * Two claims, and BOTH have to hold — the counter-case is why the penalty is
+ * guarded rather than flat:
+ *
+ * 1. a prose flow query must not let a declaration-only file outrank the
+ * implementation files that answer it;
+ * 2. a query genuinely ABOUT a declared type must still reach the declaration
+ * at full weight.
+ *
+ * The suppression the issue explicitly forbids is also pinned: a damped file is
+ * still a candidate and still named in the response, so one follow-up explore
+ * fetches it.
+ */
+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 type { ExploreDiagnosticReport, ExploreDiagnosticFile } from '../src/mcp/explore-diagnostics';
+
+const FIXTURE_SRC = path.join(__dirname, 'fixtures', 'ambient-decls-ts');
+
+/** Declaration-only, hand-written, NO generated banner — the surviving gap. */
+const HANDWRITTEN_DECL = 'types/platform-shims.d.ts';
+/** Declaration-only WITH a Wrangler banner — the CG-25 control in the same run. */
+const GENERATED_DECL = 'types/worker-configuration.d.ts';
+/** Declaration-only but IMPORTED by the storage layer — must never be damped. */
+const SHARED_TYPES = 'src/storage/types.ts';
+
+/** Prose, naming no symbol — the query shape that let the original file in. */
+const FLOW_QUERY =
+ 'how does an upload request stream the file body to storage and record image metadata';
+/** Prose that DOES name a declared type — the counter-case. */
+const TYPE_QUERY = 'what does the UploadStorage interface declare for putting an object';
+
+describe('CG-28 — a declaration-only file does not outrank implementation on a flow query', () => {
+ let testDir: string;
+ let cg: CodeGraph;
+ let sidecar: string;
+
+ /** One explore call; returns its diagnostic report plus the response text. */
+ const explore = async (query: string): Promise<{ report: ExploreDiagnosticReport; text: string }> => {
+ fs.rmSync(sidecar, { force: true });
+ const previous = process.env.CODEGRAPH_EXPLORE_DEBUG;
+ process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
+ let text: string;
+ try {
+ text = (await new ToolHandler(cg).execute('codegraph_explore', { query })).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);
+ return { report: JSON.parse(written[written.length - 1]!) as ExploreDiagnosticReport, text };
+ };
+
+ const fileOf = (report: ExploreDiagnosticReport, p: string): ExploreDiagnosticFile | undefined =>
+ report.files.find((f) => f.path === p);
+
+ let flow: { report: ExploreDiagnosticReport; text: string };
+ let typed: { report: ExploreDiagnosticReport; text: string };
+
+ beforeAll(async () => {
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg28-'));
+ fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
+ fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
+ sidecar = path.join(testDir, 'explore-diag.jsonl');
+
+ cg = CodeGraph.initSync(testDir);
+ await cg.indexAll();
+
+ flow = await explore(FLOW_QUERY);
+ typed = await explore(TYPE_QUERY);
+ }, 120_000);
+
+ afterAll(() => {
+ if (cg) cg.destroy();
+ if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
+ });
+
+ describe('fixture shape — if this rots, the gate below means nothing', () => {
+ it('holds two declaration-only files that differ only in the banner', () => {
+ for (const p of [HANDWRITTEN_DECL, GENERATED_DECL]) {
+ const nodes = cg.getNodesInFile(p).filter((n) => n.kind !== 'file' && n.kind !== 'import');
+ expect(nodes.length, `${p} declares nothing`).toBeGreaterThan(10);
+ // Every symbol type-level, nothing with a body — the structural test the
+ // penalty keys on. A `function`/`class` creeping in would silently exempt
+ // the file and make every assertion below vacuous.
+ expect(nodes.every((n) => n.kind === 'interface' || n.kind === 'type_alias'), `${p} has a non-type symbol`).toBe(true);
+ }
+ // Only one of them announces itself, so the CG-25 penalty is the ONLY
+ // difference between the two — that is what makes them comparable.
+ expect(cg.getFile(GENERATED_DECL)?.generated).toBe(true);
+ expect(cg.getFile(HANDWRITTEN_DECL)?.generated).toBeFalsy();
+ });
+
+ it('holds a pure-type module the code IMPORTS, as the safety control', () => {
+ // Identical to the ambient files on kinds and bodies; different only in
+ // that the storage layer is typed by it. This is the shape the penalty
+ // must NOT catch — a `types.ts` the codebase depends on is part of the
+ // structure of any answer about that code.
+ const nodes = cg.getNodesInFile(SHARED_TYPES).filter((n) => n.kind !== 'file' && n.kind !== 'import');
+ expect(nodes.length).toBeGreaterThan(0);
+ expect(nodes.every((n) => n.kind === 'interface' || n.kind === 'type_alias')).toBe(true);
+ expect(cg.getFile(SHARED_TYPES)?.generated).toBeFalsy();
+ });
+
+ it('holds implementation files that DO answer the flow question', () => {
+ for (const p of ['src/routes/upload.ts', 'src/storage/stream.ts', 'src/storage/metadata.ts']) {
+ expect(cg.getNodesInFile(p).some((n) => n.kind === 'function'), `${p} has no functions`).toBe(true);
+ }
+ });
+ });
+
+ describe('the gate — a prose flow query', () => {
+ it('damps the un-bannered declaration file rather than letting it rank free', () => {
+ const rec = fileOf(flow.report, HANDWRITTEN_DECL);
+ expect(rec, 'the declaration file is not even a candidate — fixture drifted').toBeDefined();
+ expect(rec!.ambientDeclaration).toBe(true);
+ expect(rec!.penalty).toBeLessThan(1);
+ });
+
+ it('does not let it outrank the implementation files', () => {
+ const decl = fileOf(flow.report, HANDWRITTEN_DECL)!;
+ const impl = flow.report.files.filter((f) => f.path.startsWith('src/') && f.finalChars > 0);
+ expect(impl.length, 'no implementation file delivered anything').toBeGreaterThanOrEqual(2);
+ // Measured before the fix: the declaration file was rank #1 with score 53
+ // against the best implementation file's 34. The bar is that at least one
+ // implementation file now ranks above it — ordinary budget movement must
+ // not fail the suite, but the inversion coming back must.
+ expect(impl.some((f) => f.rank < decl.rank), 'declaration file still ranks first').toBe(true);
+ });
+
+ it('still names it in the response, so one follow-up call fetches it', () => {
+ // The issue forbids suppression: a damped file must remain reachable.
+ expect(flow.text).toContain(HANDWRITTEN_DECL);
+ });
+
+ it('leaves the implementation files at full weight', () => {
+ for (const f of flow.report.files.filter((x) => x.path.startsWith('src/'))) {
+ expect(f.ambientDeclaration, `${f.path} was misread as an ambient declaration`).toBe(false);
+ expect(f.penalty).toBe(1);
+ }
+ });
+
+ it('does not damp a pure-type module the codebase imports', () => {
+ // The condition that keeps this narrow enough to be safe. Without it the
+ // same rule demotes `displacement-ts`'s pipeline `types.ts` — pure
+ // interfaces, but 13 inbound imports — and breaks the CG-31 gate.
+ const rec = flow.report.files.find((f) => f.path === SHARED_TYPES);
+ if (rec) {
+ expect(rec.ambientDeclaration, `${SHARED_TYPES} was flagged ambient`).toBe(false);
+ expect(rec.penalty).toBe(1);
+ }
+ // Independent of whether this query ranked it: the predicate itself must
+ // separate the two shapes.
+ const isAmbient = cg.ambientDeclarationFilePredicate([SHARED_TYPES, HANDWRITTEN_DECL]);
+ expect(isAmbient(SHARED_TYPES)).toBe(false);
+ expect(isAmbient(HANDWRITTEN_DECL)).toBe(true);
+ });
+ });
+
+ describe('the counter-case — a query that NAMES a declared type', () => {
+ it('reaches the declaration at full weight, undamped', () => {
+ const rec = fileOf(typed.report, HANDWRITTEN_DECL);
+ expect(rec, 'the named type\'s file is not a candidate').toBeDefined();
+ expect(rec!.ambientDeclaration).toBe(true);
+ // Detected as declaration-only, but EXEMPT — the query asked for it.
+ expect(rec!.penalty).toBe(1);
+ });
+
+ it('ranks it first and delivers its source', () => {
+ const rec = fileOf(typed.report, HANDWRITTEN_DECL)!;
+ expect(rec.rank).toBe(1);
+ expect(rec.finalChars).toBeGreaterThan(0);
+ });
+ });
+
+ describe('the two penalties do not stack', () => {
+ it('charges a generated declaration file once, at the stronger rate', () => {
+ // A file that is BOTH generated and declaration-only has ONE property two
+ // signals happen to see. Penalising twice (0.3 * 0.5 = 0.15) is how a file
+ // gets cliffed out of answers where it is genuinely relevant.
+ const rec = flow.report.files.find((f) => f.generated && f.ambientDeclaration);
+ if (!rec) return; // not a candidate for this query — nothing to assert
+ expect(rec.penalty).toBeGreaterThanOrEqual(0.3);
+ });
+ });
+});
diff --git a/__tests__/explore-displacement-guard.test.ts b/__tests__/explore-displacement-guard.test.ts
new file mode 100644
index 000000000..716bc9763
--- /dev/null
+++ b/__tests__/explore-displacement-guard.test.ts
@@ -0,0 +1,245 @@
+/**
+ * Regression fixture for CG-31 — a clustered render may not spend a reservation
+ * still owed to a file the loop has not reached.
+ *
+ * The allocator hands every admitted file a reservation (CG-12), and the render
+ * loop then walks the files in rank order. Carry-forward slack lets a file spend
+ * what the files ABOVE it left on the table, which is right; what was missing is
+ * the other half — nothing was held back for the files BELOW it. The whole-file
+ * BUY arm has always refused that trade (`owedBelow`, `tools.ts`); the cluster
+ * path had no equivalent, so `fileBudget`/`SPINE_CEILING` read what was left
+ * before the hard ceiling rather than what was still promised, and the first
+ * oversize file could take the response.
+ *
+ * `__tests__/fixtures/displacement-ts/` reproduces it. Four pipeline stages
+ * compete for one envelope; the first, `ingest.ts`, is a single ~20K function —
+ * one cluster member far bigger than any reservation it can earn — so it takes
+ * the bounded overshoot CG-30 left it. The fixture is padded to >500 indexed
+ * files on purpose: the displacement only exists on the 24K tier, where the
+ * reservations plus the response preamble genuinely saturate the hard ceiling.
+ *
+ * Measured against the pre-fix build (CG-30 landed, CG-31 not):
+ *
+ * ingest.ts 9,301 chars emitted on a 6,289 spendable — then dropped whole
+ * by the final ceiling, so it cost the response and delivered 0
+ * types.ts skipped `budget-whole-file`
+ * sink.ts skipped `budget-whole-file`
+ * delivered 3 of 6 admitted files, 14,908-char envelope
+ *
+ * With the guard: 6 of 6, 22,066-char envelope, and `ingest.ts` bounded to the
+ * 4,913 that were actually still free.
+ */
+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', 'displacement-ts');
+
+/**
+ * Padding modules, written into the temp copy rather than checked in. The
+ * output tier is chosen by INDEXED FILE COUNT, and the displacement this test
+ * pins only exists at >=500 files (24K envelope against a 24.4K render ceiling
+ * that also has to hold the response preamble). Below that the ceiling has
+ * enough slack to absorb an overshoot and the bug is invisible.
+ */
+const FILLER_FILES = 520;
+
+/** A symbol bag spanning all four stages — they compete for one envelope. */
+const QUERY = 'ingestRecords normalizeRecords enrichRecords publishRecords';
+/** One symbol, one file — the concentration case the guard must not flatten. */
+const PRECISE_QUERY = 'ingestRecords';
+
+/** The giant: one ~20K function, the file that used to take the response. */
+const GIANT = 'src/pipeline/ingest.ts';
+/** Ranked below the giant and dropped by it pre-fix. */
+const STARVED = ['src/pipeline/types.ts', 'src/pipeline/sink.ts'];
+
+interface Probe {
+ response: string;
+ report: ExploreDiagnosticReport;
+ bytes: Map;
+}
+
+describe('CG-31 — the cluster path holds back what is still owed below it', () => {
+ let testDir: string;
+ let cg: CodeGraph;
+ let spread: Probe;
+ let precise: Probe;
+
+ const fileOf = (probe: Probe, p: string): ExploreDiagnosticFile => {
+ const rec = probe.report.files.find((f) => f.path === p);
+ if (!rec) throw new Error(`${p} absent from the diagnostic report`);
+ return rec;
+ };
+ /** Admitted = the allocator reserved bytes for it. */
+ const admitted = (probe: Probe): ExploreDiagnosticFile[] =>
+ probe.report.files.filter((f) => (f.allowance ?? 0) > 0);
+
+ beforeAll(async () => {
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg31-'));
+ fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
+ fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
+
+ const filler = path.join(testDir, 'src', 'generated');
+ fs.mkdirSync(filler, { recursive: true });
+ for (let i = 0; i < FILLER_FILES; i++) {
+ // Deterministic, unrelated to the query — these pad the file count, they
+ // must never rank.
+ fs.writeFileSync(
+ path.join(filler, `unit${i}.ts`),
+ `export const seed${i} = ${i};\n`
+ + `export function widget${i}(n: number): number {\n return n * ${i + 1} + seed${i};\n}\n`,
+ );
+ }
+
+ cg = CodeGraph.initSync(testDir);
+ await cg.indexAll();
+
+ // The per-file bounds are only observable through the diagnostic sidecar.
+ const sidecar = path.join(testDir, 'explore-diag.jsonl');
+ const previous = process.env.CODEGRAPH_EXPLORE_DEBUG;
+ process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
+ const run = async (handler: ToolHandler, query: string): Promise => {
+ const result = await handler.execute('codegraph_explore', { query });
+ const response = result.content?.[0]?.text ?? '';
+ const written = fs.readFileSync(sidecar, 'utf-8').trim().split('\n').filter(Boolean);
+ return {
+ response,
+ report: JSON.parse(written[written.length - 1]!) as ExploreDiagnosticReport,
+ bytes: attributeSourceBytes(response),
+ };
+ };
+ try {
+ const handler = new ToolHandler(cg);
+ spread = await run(handler, QUERY);
+ precise = await run(handler, PRECISE_QUERY);
+ } finally {
+ if (previous === undefined) delete process.env.CODEGRAPH_EXPLORE_DEBUG;
+ else process.env.CODEGRAPH_EXPLORE_DEBUG = previous;
+ }
+ }, 180_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('sits on the 24K tier, where the reservations saturate the ceiling', () => {
+ expect(cg.getStats().fileCount).toBeGreaterThanOrEqual(500);
+ expect(spread.report.budget.maxOutputChars).toBe(24000);
+ });
+
+ it('admits every stage file, so there is something to displace', () => {
+ const paths = admitted(spread).map((f) => f.path);
+ expect(paths).toContain(GIANT);
+ for (const p of STARVED) expect(paths).toContain(p);
+ expect(paths.length).toBeGreaterThanOrEqual(5);
+ });
+
+ it('renders the giant through the CLUSTER path, over its reservation', () => {
+ const rec = fileOf(spread, GIANT);
+ expect(rec.render).toBe('clusters');
+ // One member bigger than anything it can earn beside its siblings — the
+ // shape that makes the bounded overshoot fire at all.
+ const source = fs.readFileSync(path.join(testDir, GIANT), 'utf-8');
+ expect(source.length).toBeGreaterThan((rec.spendable ?? 0) * 2);
+ // And the guard actually bit — a vacuous pass here would hide a
+ // regression. Measured against the bounded overshoot a cluster's top
+ // member may otherwise take (1.5x, CG-30), which is what it refused.
+ expect(rec.funded).not.toBeNull();
+ expect(rec.funded!).toBeLessThan(Math.round(rec.spendable! * 1.5));
+ });
+ });
+
+ // ── The gate ──────────────────────────────────────────────────────────────
+
+ describe('displacement refusal', () => {
+ it('CG-31 GATE: no clustered file emits past what was still free to spend', () => {
+ for (const probe of [spread, precise]) {
+ const over = probe.report.files
+ .filter((f) => f.render === 'clusters' && f.funded !== null)
+ // +1 for the render loop's own rounding on the windowed cut.
+ .filter((f) => f.emittedChars > f.funded! + 1)
+ .map((f) => `${f.path}: ${f.emittedChars} of ${f.funded}`);
+ expect(over).toEqual([]);
+ }
+ });
+
+ it('CG-31 GATE: every admitted file below the top one is delivered', () => {
+ // Pre-fix: 3 of 6 — `ingest.ts` overshot, was itself cut by the final
+ // ceiling, and took `types.ts` + `sink.ts` down with it.
+ for (const rec of admitted(spread)) {
+ expect(rec.skipped, `${rec.path} skipped`).toBeNull();
+ expect(spread.bytes.get(rec.path) ?? 0, `${rec.path} bytes`).toBeGreaterThan(0);
+ }
+ for (const p of STARVED) expect(spread.bytes.get(p) ?? 0).toBeGreaterThan(0);
+ });
+
+ it('the guard is symmetric — it is about ORDER, not rank', () => {
+ // Nothing here protects rank #1 specifically: the LAST admitted file, the
+ // only one with no reservation owed below it, is delivered too.
+ const files = admitted(spread);
+ const last = files[files.length - 1]!;
+ expect(last.skipped).toBeNull();
+ expect(spread.bytes.get(last.path) ?? 0).toBeGreaterThan(0);
+ // And the last file is never itself cut by the guard — nothing is owed
+ // below it, so `funded` may not sit under its own reservation.
+ expect(last.funded!).toBeGreaterThanOrEqual(Math.min(last.allowance!, last.emittedChars));
+ });
+
+ it('a kept promise is not a displacement — no file is cut below its reservation', () => {
+ for (const probe of [spread, precise]) {
+ for (const rec of admitted(probe)) {
+ if (rec.funded === null) continue;
+ expect(rec.funded, rec.path).toBeGreaterThanOrEqual(
+ Math.min(rec.allowance!, rec.emittedChars));
+ }
+ }
+ });
+
+ it('nothing is lost to the hard ceiling — the epilogue is cut before a section', () => {
+ // A section thrown away by the final truncation is the same starvation
+ // arriving after the guard has done its work: the bytes were held back
+ // for that file and then nobody received them.
+ for (const probe of [spread, precise]) {
+ expect(probe.report.files.filter((f) => f.render === 'dropped')).toEqual([]);
+ }
+ });
+
+ it('keeps the response inside the hard ceiling', () => {
+ for (const probe of [spread, precise]) {
+ expect(probe.report.envelope.chars).toBeLessThanOrEqual(probe.report.budget.hardCeiling);
+ }
+ });
+ });
+
+ // ── The thing the guard must NOT become ───────────────────────────────────
+
+ describe('concentration survives', () => {
+ it('a precise symbol query still puts the most source in the named file', () => {
+ const mine = precise.bytes.get(GIANT) ?? 0;
+ const others = [...precise.bytes.entries()].filter(([p]) => p !== GIANT);
+ expect(mine).toBeGreaterThan(0);
+ for (const [p, n] of others) {
+ expect(mine, `${GIANT} vs ${p}`).toBeGreaterThan(n);
+ }
+ // Not a forced even split: the named file takes a clear plurality.
+ const total = [...precise.bytes.values()].reduce((s, n) => s + n, 0);
+ expect(mine / total).toBeGreaterThan(1 / precise.bytes.size);
+ });
+
+ it('the named file still outspends what it would get from an even split', () => {
+ const rec = fileOf(precise, GIANT);
+ const even = precise.report.budget.maxOutputChars / admitted(precise).length;
+ expect(rec.emittedChars).toBeGreaterThan(even);
+ });
+ });
+});
diff --git a/__tests__/explore-factory-closure.test.ts b/__tests__/explore-factory-closure.test.ts
new file mode 100644
index 000000000..d0b083839
--- /dev/null
+++ b/__tests__/explore-factory-closure.test.ts
@@ -0,0 +1,157 @@
+/**
+ * Regression gate for the FACTORY-CLOSURE file shape (task CG-27).
+ *
+ * A `createFoo()` that returns an object of closures spans almost all of its
+ * file, so its indexed range is an ENVELOPE around every symbol the query
+ * actually wants. Svelte 5 rune stores, React custom-hook modules, IIFE
+ * module-pattern JS and Zustand's `create((set, get) => ({ … }))` are all
+ * written this way, so it is a shape rather than a one-repo quirk.
+ *
+ * CG-27 asked whether the >50%-of-file envelope drop — which fires for `class`,
+ * `struct`, `interface` and friends but not for `function`/`method` — should be
+ * extended to cover it. **Measured, it should not**, and the issue was closed as
+ * obsolete: `docs/benchmarks/explore-factory-closure-cg27.md` has the numbers.
+ * Two independent mechanisms already absorb the shape:
+ *
+ * - `shrinkCluster` orders members by (importance desc, SIZE ASC) and refuses
+ * any member that overruns the cap once something is kept, so a file-spanning
+ * member is only ever selected when it is the sole member of the top
+ * importance tier;
+ * - when it IS selected, CG-30 windows it on whole lines rather than emitting
+ * it whole, so the file still delivers bounded, readable source.
+ *
+ * Dropping the range instead SPLITS the file into several clusters, and only the
+ * first-chosen cluster may be shrunk — measured, a trivial 7-line cluster won the
+ * density tiebreak and the answer-bearing cluster was dropped whole, taking the
+ * rank-#1 file from 7,539 chars and 7 of 11 inner definitions to 397 and none.
+ *
+ * So this file pins the OUTCOME, not the mechanism: whatever future work does to
+ * clustering, a factory-closure file must keep delivering the closures inside it
+ * — that is what stops the agent Reading the file back.
+ */
+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 type { ExploreDiagnosticReport } from '../src/mcp/explore-diagnostics';
+
+const FIXTURE_SRC = path.join(__dirname, 'fixtures', 'factory-closure-ts');
+
+/** The factory file, and the closure factory whose body is nearly all of it. */
+const TARGET = 'src/stores/dashboard-store.ts';
+const FACTORY = 'createDashboardStore';
+/** Prose the way a newcomer asks it, naming two of the closures inside. */
+const QUERY = 'how does the dashboard store refresh its metrics and apply a filter';
+
+describe('CG-27 — a factory-closure file delivers the closures inside it', () => {
+ let testDir: string;
+ let cg: CodeGraph;
+ let response: string;
+ let report: ExploreDiagnosticReport;
+ /** Source lines of TARGET the response actually carried. */
+ let delivered: Set;
+ let sourceLines: string[];
+
+ beforeAll(async () => {
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg27-'));
+ fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
+ fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
+
+ cg = CodeGraph.initSync(testDir);
+ await cg.indexAll();
+
+ const sidecar = path.join(testDir, 'explore-diag.jsonl');
+ const previous = process.env.CODEGRAPH_EXPLORE_DEBUG;
+ process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
+ try {
+ response = (await new ToolHandler(cg).execute('codegraph_explore', { query: QUERY }))
+ .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;
+
+ // A line counts as delivered only when the response numbers it AND the text
+ // matches that source line — a line number quoted in prose must not count.
+ sourceLines = fs.readFileSync(path.join(testDir, TARGET), 'utf-8').split('\n');
+ delivered = new Set();
+ for (const line of response.split('\n')) {
+ const m = /^(\d+)\t(.*)$/.exec(line);
+ if (!m) continue;
+ const n = Number(m[1]);
+ if (n >= 1 && n <= sourceLines.length && sourceLines[n - 1] === m[2]) delivered.add(n);
+ }
+ }, 120_000);
+
+ afterAll(() => {
+ if (cg) cg.destroy();
+ if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
+ });
+
+ /** The closures defined inside the factory, straight from the index. */
+ const innerClosures = () => {
+ const nodes = cg.getNodesInFile(TARGET);
+ const factory = nodes.find((n) => n.name === FACTORY)!;
+ return nodes.filter((n) => (n.kind === 'function' || n.kind === 'method')
+ && n.name !== FACTORY
+ && n.startLine > factory.startLine && n.endLine <= factory.endLine);
+ };
+
+ describe('fixture shape — if this rots, the gate below means nothing', () => {
+ it('holds one symbol spanning most of the file, with closures inside it', () => {
+ const factory = cg.getNodesInFile(TARGET).find((n) => n.name === FACTORY);
+ expect(factory, `${TARGET} has no ${FACTORY} node`).toBeDefined();
+ // The envelope condition the >50% drop tests for — and `function`, the kind
+ // that drop does not cover.
+ expect(factory!.kind).toBe('function');
+ expect(factory!.endLine - factory!.startLine + 1)
+ .toBeGreaterThan(sourceLines.length * 0.5);
+ expect(innerClosures().length).toBeGreaterThanOrEqual(8);
+ });
+
+ it('is too long to ship whole, so it renders through the cluster path', () => {
+ // Past WHOLE_FILE_MAX_LINES (220 for a non-central file): the whole-file
+ // grace and buy arms cannot claim it, so the envelope actually matters.
+ expect(sourceLines.length).toBeGreaterThan(220);
+ expect(report.files.find((f) => f.path === TARGET)?.render).toBe('clusters');
+ });
+ });
+
+ describe('the gate', () => {
+ it('delivers the closures the query named, not just the factory head', () => {
+ const inner = innerClosures();
+ for (const name of ['refreshMetrics', 'applyFilter']) {
+ const node = inner.find((n) => n.name === name)!;
+ expect(node, `${name} is not an inner closure any more`).toBeDefined();
+ expect(delivered.has(node.startLine), `${name} definition line not delivered`).toBe(true);
+ }
+ });
+
+ it('delivers most of the closures, spread across the file', () => {
+ const inner = innerClosures();
+ const hit = inner.filter((n) => delivered.has(n.startLine));
+ // Measured on the `feature/CG-24` tip: 7 of 11. The bar is half, so ordinary
+ // budget movement does not fail the suite, but losing the closures does.
+ expect(hit.length).toBeGreaterThanOrEqual(Math.ceil(inner.length / 2));
+ // Not one contiguous head window off the top of the factory: the whole
+ // point is that selection reaches symbols deep in the body.
+ const last = inner[inner.length - 1]!;
+ const deepest = Math.max(...hit.map((n) => n.startLine));
+ expect(deepest).toBeGreaterThan((last.startLine + inner[0]!.startLine) / 2);
+ });
+
+ it('never renders an empty section for the file', () => {
+ const rec = report.files.find((f) => f.path === TARGET)!;
+ expect(rec.emittedChars).toBeGreaterThan(0);
+ expect(delivered.size).toBeGreaterThan(20);
+ });
+
+ it('keeps the response inside the hard ceiling', () => {
+ expect(report.envelope.chars).toBeLessThanOrEqual(report.budget.hardCeiling);
+ });
+ });
+});
diff --git a/__tests__/explore-named-symbol-render.test.ts b/__tests__/explore-named-symbol-render.test.ts
new file mode 100644
index 000000000..41b930692
--- /dev/null
+++ b/__tests__/explore-named-symbol-render.test.ts
@@ -0,0 +1,199 @@
+/**
+ * Standing gate for THE GUARANTEE (task CG-38): if the agent names a symbol and
+ * that symbol's file is admitted to the response, the symbol's DEFINITION renders.
+ *
+ * This is the measurement the CG-24 epic never had. Its probes all score the
+ * response in aggregate — envelope share, per-file spend, source totals, file
+ * counts — and every one of them is green on a response that returns 25K of
+ * source from the right file and still omits the function the agent asked for by
+ * name. That is what CG-38 was: on a 1,414-line Svelte store, `queueMessage`
+ * (L1087) and `flushQueuedMessages` (L1102) never rendered even though their file
+ * won rank #1 with 67% of the envelope; the agent got the same-stem
+ * `QueuedMessage` INTERFACE at L70 and had to Read the file to find the
+ * functions. Longstanding, not an epic regression — the controlled bisect (index
+ * held fixed, engine varied across every epic merge point) found it at every
+ * build including pre-epic.
+ *
+ * Two independent causes, and the fixture below fails on either:
+ *
+ * 1. `buildFlowFromNamedSymbols` returned EMPTY — throwing away the NAMED-SYMBOL
+ * IDENTITY along with the narrative — whenever the named symbols happened not
+ * to form a call chain. Two sibling closures in one factory produce no chain,
+ * no synthesized hop and no dispatch boundary, so both defs lost the
+ * importance-9 rank that the named-def injection exists to give them.
+ * 2. The ceiling trim cut in SOURCE ORDER, so whatever survived the shrink at
+ * the END of a large file was always the first thing dropped.
+ *
+ * The fixture mirrors the reported file's geometry deliberately: a decoy
+ * same-stem interface at L70, a factory closure at L104 spanning ~92% of the file
+ * (so every symbol merges into ONE cluster), the target functions past L1000, and
+ * a 2,500-line generated `.d.ts` for the ranker to penalise.
+ */
+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';
+
+const FIXTURE = 'tail-render-ts';
+const TARGET = 'src/lib/session-store.ts';
+
+let dir: string;
+let cg: CodeGraph;
+
+/** Every `\t` line number the response actually sent. */
+function renderedLines(response: string): Set {
+ const out = new Set();
+ for (const m of response.matchAll(/^(\d+)\t/gm)) out.add(Number(m[1]));
+ return out;
+}
+
+async function explore(query: string): Promise {
+ const res = await new ToolHandler(cg).execute('codegraph_explore', { query });
+ return res.content?.[0]?.text ?? '';
+}
+
+function defLineOf(name: string): number {
+ const node = cg.getNodesByName(name).find((n) => n.filePath === TARGET && n.startLine > 0);
+ expect(node, `${name} is not indexed in ${TARGET}`).toBeDefined();
+ return node!.startLine;
+}
+
+beforeAll(async () => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg38-'));
+ fs.cpSync(path.join(__dirname, 'fixtures', FIXTURE), dir, { recursive: true });
+ fs.rmSync(path.join(dir, '.codegraph'), { recursive: true, force: true });
+ cg = CodeGraph.initSync(dir);
+ await cg.indexAll();
+}, 180_000);
+
+afterAll(() => {
+ cg?.destroy();
+ if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
+});
+
+describe('CG-38 fixture shape — if this rots, the gate below means nothing', () => {
+ it('puts the target functions past L1000 of a ~1,400-line file', () => {
+ const lines = fs.readFileSync(path.join(dir, TARGET), 'utf-8').split('\n');
+ expect(lines.length).toBeGreaterThan(1300);
+ expect(defLineOf('queueMessage')).toBeGreaterThan(1000);
+ expect(defLineOf('flushQueuedMessages')).toBeGreaterThan(1000);
+ });
+
+ it('wraps them in a closure spanning most of the file, so they all cluster as one', () => {
+ const lines = fs.readFileSync(path.join(dir, TARGET), 'utf-8').split('\n');
+ const factory = cg.getNodesByName('createSessionStore')
+ .find((n) => n.filePath === TARGET)!;
+ expect(factory).toBeDefined();
+ expect(factory.endLine - factory.startLine + 1).toBeGreaterThan(lines.length * 0.5);
+ });
+
+ it('carries the same-stem decoy near the top', () => {
+ const decoy = cg.getNodesByName('QueuedMessage').find((n) => n.filePath === TARGET)!;
+ expect(decoy).toBeDefined();
+ expect(decoy.kind).toBe('interface');
+ expect(decoy.startLine).toBeLessThan(100);
+ });
+
+ it('carries a generated declaration file for the ranker to penalise', () => {
+ const dts = path.join(dir, 'types/worker-configuration.d.ts');
+ expect(fs.existsSync(dts)).toBe(true);
+ expect(fs.readFileSync(dts, 'utf-8').split('\n').length).toBeGreaterThan(2000);
+ });
+
+ it('neither target calls the other — that absence is what produced no flow', () => {
+ const queue = cg.getNodesByName('queueMessage').find((n) => n.filePath === TARGET)!;
+ const flush = cg.getNodesByName('flushQueuedMessages').find((n) => n.filePath === TARGET)!;
+ const between = [...cg.getCallees(queue.id), ...cg.getCallees(flush.id)]
+ .filter(({ node }) => node.id === queue.id || node.id === flush.id);
+ expect(between).toHaveLength(0);
+ });
+});
+
+describe('CG-38 — an agent-named symbol renders its definition', () => {
+ /**
+ * Both reported query shapes. They fail for different reasons — the symbol bag
+ * never built a flow at all, the prose question built one and then lost the
+ * tail to the ceiling trim — so a fix for one does not imply the other.
+ */
+ const CASES: Array<{ shape: string; query: string; symbols: string[] }> = [
+ {
+ shape: 'symbol bag',
+ query: 'queueMessage flushQueuedMessages',
+ symbols: ['queueMessage', 'flushQueuedMessages'],
+ },
+ {
+ shape: 'prose question',
+ query: 'how does queueMessage hand its entries to flushQueuedMessages',
+ symbols: ['queueMessage', 'flushQueuedMessages'],
+ },
+ {
+ shape: 'three siblings, with the decoy interface competing',
+ query: 'explain queueMessage, removeQueuedMessage and flushQueuedMessages',
+ symbols: ['queueMessage', 'removeQueuedMessage', 'flushQueuedMessages'],
+ },
+ ];
+
+ for (const { shape, query, symbols } of CASES) {
+ it(`renders every named definition — ${shape}`, async () => {
+ const response = await explore(query);
+ const lines = renderedLines(response);
+ for (const name of symbols) {
+ const line = defLineOf(name);
+ // The NAME alone proves nothing: it appears in the section header's
+ // symbol list and at call sites whether or not the body was sent. Only
+ // the definition LINE being among the rendered lines counts.
+ expect(lines.has(line), `${name} (${TARGET}:${line}) did not render for "${query}"`)
+ .toBe(true);
+ }
+ }, 120_000);
+ }
+
+ it('never steers the agent to Read', async () => {
+ const response = await explore('queueMessage flushQueuedMessages');
+ expect(response).not.toMatch(/\buse Read\b|\bRead the file\b/i);
+ }, 120_000);
+});
+
+describe('CG-38 — a penalty on one file cannot shrink an unrelated file\'s render', () => {
+ /**
+ * The issue's sharpest lead: on an index where the generated `.d.ts` was NOT
+ * flagged, the target file rendered ~581 lines including both symbols; on an
+ * index where it WAS flagged, the same engine rendered 12. `rankPenalty` scales
+ * `fileGraphScore`, which moves the relevance gate (6% of max) and so reshuffles
+ * the admitted set — a demotion of one file must not cost an unrelated
+ * top-ranked file its source.
+ *
+ * Flipping `files.generated` on that one row holds the INDEX constant and
+ * attributes any delta to the ranker alone (the CG-25 method).
+ */
+ const DTS = 'types/worker-configuration.d.ts';
+ const QUERY = 'queueMessage flushQueuedMessages';
+
+ it('renders the same named definitions with the .d.ts flagged and unflagged', async () => {
+ const setGenerated = (value: number) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const db = (cg as any).db?.getDatabase?.() ?? (cg as any).db?.db;
+ db.prepare('UPDATE files SET generated = ? WHERE path = ?').run(value, DTS);
+ };
+ const linesFor = async () => renderedLines(await explore(QUERY));
+
+ const flagged = await linesFor();
+ setGenerated(0);
+ try {
+ const unflagged = await linesFor();
+ for (const name of ['queueMessage', 'flushQueuedMessages']) {
+ const line = defLineOf(name);
+ expect(flagged.has(line), `${name} missing with the .d.ts FLAGGED`).toBe(true);
+ expect(unflagged.has(line), `${name} missing with the .d.ts UNFLAGGED`).toBe(true);
+ }
+ // The guarantee is about the named defs, not byte equality — the penalty is
+ // supposed to move bytes around. What it must never do is cost the
+ // top-ranked file the source the agent asked for.
+ expect(unflagged.size).toBeGreaterThan(0);
+ } finally {
+ setGenerated(1);
+ }
+ }, 180_000);
+});
diff --git a/__tests__/explore-oversize-member.test.ts b/__tests__/explore-oversize-member.test.ts
new file mode 100644
index 000000000..019d1ff9a
--- /dev/null
+++ b/__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;
+
+ 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);
+ });
+ });
+});
diff --git a/__tests__/explore-reservation-invariant.test.ts b/__tests__/explore-reservation-invariant.test.ts
new file mode 100644
index 000000000..4b23df392
--- /dev/null
+++ b/__tests__/explore-reservation-invariant.test.ts
@@ -0,0 +1,265 @@
+/**
+ * Regression fixture for CG-26 — the end-to-end reservation invariant.
+ *
+ * Every admitted file receives at least its reservation before any file draws
+ * on carry-forward slack.
+ *
+ * CG-30 bounded how far an oversize cluster member may overshoot and CG-31 gave
+ * the cluster path a displacement guard. This pins the invariant they jointly
+ * satisfy across EVERY render path — cluster, whole-file grace, whole-file BUY —
+ * and in BOTH directions: the top-ranked file when the files below it overspend,
+ * and an admitted lower-ranked file when the top one does.
+ *
+ * Two things CG-26 fixed are pinned here because nothing else can see them:
+ *
+ * - The whole-file arms were fit-tested against raw room before the ceiling,
+ * never against what was still owed below. A grace-sized file could take a
+ * pending file's reservation on its way to the ceiling; okhttp's
+ * `CallServerInterceptor.kt` shipped 8,499 chars on a 5,964 funded ceiling
+ * and the rank-6 file below it delivered nothing.
+ * - Every section was charged a flat 200 chars of overhead while a real header
+ * runs 300–500. The loop believed it had room it did not have (okhttp
+ * rendered 26,601 chars against a 24,400 ceiling), so the final truncation
+ * threw a fully-rendered section away — the same starvation, arriving after
+ * the guard had done its work.
+ *
+ * Shares the `displacement-ts` fixture: four pipeline stages competing for one
+ * envelope, the first a single ~20K function, padded past 500 indexed files so
+ * the response sits on the 24K tier where reservations genuinely saturate the
+ * ceiling.
+ */
+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', 'displacement-ts');
+const FILLER_FILES = 520;
+
+/** The giant: one ~20K function. Ranks #1 under the spread query. */
+const GIANT = 'src/pipeline/ingest.ts';
+
+/**
+ * Three shapes, so the invariant is tested from both sides:
+ * spread — every stage named; the giant ranks #1 and overspends downwards.
+ * tail — the stages BELOW the giant named; something small ranks #1 while
+ * the giant competes from underneath. This is the direction CG-31's
+ * fixture could not reach.
+ * precise — one symbol. The concentration case the guard must not flatten.
+ */
+const QUERIES = {
+ spread: 'ingestRecords normalizeRecords enrichRecords publishRecords',
+ tail: 'publishRecords sinkRecord PipelineRecord ingestRecords',
+ precise: 'ingestRecords',
+} as const;
+type Shape = keyof typeof QUERIES;
+
+interface Probe {
+ response: string;
+ report: ExploreDiagnosticReport;
+ bytes: Map;
+}
+
+describe('CG-26 — no admitted file is starved, on any render path', () => {
+ let testDir: string;
+ let cg: CodeGraph;
+ const probes = {} as Record;
+
+ /** Admitted = the allocator reserved bytes for it. */
+ const admitted = (probe: Probe): ExploreDiagnosticFile[] =>
+ probe.report.files.filter((f) => (f.allowance ?? 0) > 0);
+ const all = (): Probe[] => Object.values(probes);
+
+ beforeAll(async () => {
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg26-'));
+ fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
+ fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
+
+ const filler = path.join(testDir, 'src', 'generated');
+ fs.mkdirSync(filler, { recursive: true });
+ for (let i = 0; i < FILLER_FILES; i++) {
+ fs.writeFileSync(
+ path.join(filler, `unit${i}.ts`),
+ `export const seed${i} = ${i};\n`
+ + `export function widget${i}(n: number): number {\n return n * ${i + 1} + seed${i};\n}\n`,
+ );
+ }
+
+ cg = CodeGraph.initSync(testDir);
+ await cg.indexAll();
+
+ 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);
+ for (const [shape, query] of Object.entries(QUERIES) as [Shape, string][]) {
+ const result = await handler.execute('codegraph_explore', { query });
+ const response = result.content?.[0]?.text ?? '';
+ const written = fs.readFileSync(sidecar, 'utf-8').trim().split('\n').filter(Boolean);
+ probes[shape] = {
+ response,
+ report: JSON.parse(written[written.length - 1]!) as ExploreDiagnosticReport,
+ bytes: attributeSourceBytes(response),
+ };
+ }
+ } finally {
+ if (previous === undefined) delete process.env.CODEGRAPH_EXPLORE_DEBUG;
+ else process.env.CODEGRAPH_EXPLORE_DEBUG = previous;
+ }
+ }, 180_000);
+
+ afterAll(() => {
+ if (cg) cg.destroy();
+ if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
+ });
+
+ // ── Fixture shape — if these rot, the gates below mean nothing ─────────────
+
+ describe('fixture shape', () => {
+ it('sits on the 24K tier, where the reservations saturate the ceiling', () => {
+ expect(cg.getStats().fileCount).toBeGreaterThanOrEqual(500);
+ for (const probe of all()) expect(probe.report.budget.maxOutputChars).toBe(24000);
+ });
+
+ it('exercises both directions — the giant ranks #1 in one shape and lower in another', () => {
+ // Which shape puts it where is the ranker's business and may move; that
+ // it lands on BOTH sides across the three is what makes the gates below
+ // test the invariant rather than one arrangement of it.
+ const ranks = all().map((p) => p.report.files.find((f) => f.path === GIANT)?.rank ?? -1);
+ expect(ranks).toContain(1);
+ expect(ranks.some((r) => r > 1)).toBe(true);
+ });
+
+ it('exercises both render paths — something ships whole, something clusters', () => {
+ const modes = new Set(all().flatMap((p) => p.report.files.map((f) => f.render)));
+ expect(modes).toContain('clusters');
+ expect(modes).toContain('whole');
+ });
+ });
+
+ // ── The invariant ─────────────────────────────────────────────────────────
+
+ describe('the reservation invariant', () => {
+ it('CG-26 GATE: no file on ANY render path emits past what was still free', () => {
+ // CG-31 pinned this for `clusters` only. The whole-file arms were fit-
+ // tested against `renderCeiling - totalChars`, which is everyone's room,
+ // not this file's — so a whole render could spend a reservation the loop
+ // had already promised further down.
+ for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
+ const over = probe.report.files
+ .filter((f) => f.render !== null && f.render !== 'dropped' && f.funded !== null)
+ // +1 for the render loop's own rounding on a windowed cut.
+ .filter((f) => f.emittedChars > f.funded! + 1)
+ .map((f) => `${shape}/${f.path}: ${f.emittedChars} emitted of ${f.funded} funded (${f.render})`);
+ expect(over).toEqual([]);
+ }
+ });
+
+ it('CG-26 GATE: every admitted file is delivered, whatever its rank', () => {
+ for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
+ for (const rec of admitted(probe)) {
+ expect(rec.skipped, `${shape}/${rec.path} skipped`).toBeNull();
+ expect(probe.bytes.get(rec.path) ?? 0, `${shape}/${rec.path} bytes`).toBeGreaterThan(0);
+ }
+ }
+ });
+
+ it('CG-26 GATE: the rank-#1 file gets its reservation even when a file below overspends', () => {
+ // The direction CG-31's fixture could not reach: under `tail` the giant
+ // ranks below a small file and draws far past its own reservation from
+ // carry-forward slack. Rank #1 must still receive what it was promised
+ // (or its whole file, if that is less).
+ for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
+ const top = admitted(probe).sort((a, b) => a.rank - b.rank)[0];
+ if (!top) continue;
+ const onDisk = fs.statSync(path.join(testDir, top.path)).size;
+ expect(probe.bytes.get(top.path) ?? 0, `${shape}/${top.path}`)
+ .toBeGreaterThanOrEqual(Math.min(top.allowance!, onDisk) * 0.9);
+ }
+ });
+
+ it('and the gate above is not vacuous — a lower-ranked file does overspend', () => {
+ const overspenders = (probe: Probe) => admitted(probe)
+ .filter((f) => f.rank > 1 && f.emittedChars > f.allowance!);
+ expect(overspenders(probes.tail).length).toBeGreaterThan(0);
+ });
+ });
+
+ // ── What the ceiling must no longer do ────────────────────────────────────
+
+ describe('the hard ceiling never throws a rendered section away', () => {
+ it('the render loop spends what it counts — nothing is allocated past the ceiling', () => {
+ // Sections used to be charged a flat 200 chars against a header that runs
+ // 300–500, so the loop over-filled and the final truncation dropped whole
+ // sections. `allocatedChars` is the pre-truncation length: it staying
+ // under the ceiling IS the accounting being exact.
+ for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
+ expect(probe.report.envelope.allocatedChars, shape)
+ .toBeLessThanOrEqual(probe.report.budget.hardCeiling);
+ expect(probe.report.envelope.truncated, shape).toBe(false);
+ }
+ });
+
+ it('no file is rendered and then dropped', () => {
+ for (const probe of all()) {
+ expect(probe.report.files.filter((f) => f.render === 'dropped')).toEqual([]);
+ }
+ });
+
+ it('keeps the response inside the hard ceiling', () => {
+ for (const probe of all()) {
+ expect(probe.report.envelope.chars).toBeLessThanOrEqual(probe.report.budget.hardCeiling);
+ }
+ });
+ });
+
+ // ── The epilogue is budgeted, not discarded ───────────────────────────────
+
+ describe('the epilogue the loop budgeted for is the epilogue it emits', () => {
+ it('a response that withheld files still says so, and says to explore not Read', () => {
+ // The flat 600-char margin was neither the epilogue's size nor a bound on
+ // it, so a saturated response shipped with no pointer list and no
+ // reminders at all. Whatever else is traded away, the agent must be told
+ // an uncovered area exists and that another explore reaches it.
+ for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
+ const withheld = probe.report.files.some(
+ (f) => f.render === null || (probe.bytes.get(f.path) ?? 0) === 0);
+ if (!withheld) continue;
+ expect(
+ /Not shown above|omitted for size|codegraph_explore/.test(probe.response),
+ `${shape} withheld files without saying where to look`,
+ ).toBe(true);
+ }
+ });
+
+ it('never steers the agent to Read', () => {
+ for (const probe of all()) {
+ expect(/use (the )?Read|fall back to Read(?!ing those files)/i.test(probe.response)).toBe(false);
+ }
+ });
+ });
+
+ // ── The thing the invariant must NOT become ───────────────────────────────
+
+ describe('concentration survives', () => {
+ it('a precise symbol query still puts the most source in the named file', () => {
+ const mine = probes.precise.bytes.get(GIANT) ?? 0;
+ expect(mine).toBeGreaterThan(0);
+ for (const [p, n] of probes.precise.bytes) {
+ if (p === GIANT) continue;
+ expect(mine, `${GIANT} vs ${p}`).toBeGreaterThan(n);
+ }
+ });
+
+ it('is not an even split — the named file outspends its equal share', () => {
+ const rec = probes.precise.report.files.find((f) => f.path === GIANT)!;
+ const even = probes.precise.report.budget.maxOutputChars / admitted(probes.precise).length;
+ expect(rec.emittedChars).toBeGreaterThan(even);
+ });
+ });
+});
diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts
index 320f99aa5..cfb198c86 100644
--- a/__tests__/extraction.test.ts
+++ b/__tests__/extraction.test.ts
@@ -1266,6 +1266,51 @@ static REGISTRY: Lazy = Lazy::new(|| build_cfg());
expect(owner('compute_len')).toBe('variable:LEN');
expect(owner('build_cfg')).toBe('variable:REGISTRY');
});
+
+ it('should extract union declarations and their impl edges', () => {
+ const code = `
+pub union Reg {
+ pub raw: u32,
+ pub halves: [u16; 2],
+}
+
+pub trait Describe {
+ fn describe(&self) -> u32;
+}
+
+impl Describe for Reg {
+ fn describe(&self) -> u32 {
+ unsafe { self.raw }
+ }
+}
+`;
+ const result = extractFromSource('reg.rs', code);
+
+ // A union is a first-class type definition, not an alias — it must be a
+ // node, or the impl below has no source endpoint to hang off.
+ const reg = result.nodes.find((n) => n.name === 'Reg');
+ expect(reg).toBeDefined();
+ expect(reg?.kind).toBe('union');
+
+ const implRef = result.unresolvedReferences.find(
+ (r) => r.referenceKind === 'implements' && r.referenceName === 'Describe'
+ );
+ expect(implRef).toBeDefined();
+ expect(implRef?.fromNodeId).toBe(reg?.id);
+
+ // The impl's method attaches to the union, not to the file — without a Reg
+ // node it was an orphan whose qualifiedName pointed at a type that did not
+ // exist in the graph.
+ const implMethod = result.nodes.find(
+ (n) => n.kind === 'method' && n.qualifiedName?.includes('Reg')
+ );
+ expect(implMethod).toBeDefined();
+ expect(
+ result.edges.some(
+ (e) => e.kind === 'contains' && e.source === reg?.id && e.target === implMethod?.id
+ )
+ ).toBe(true);
+ });
});
describe('Java Extraction', () => {
@@ -5879,6 +5924,71 @@ std::string use() {
});
});
+describe('C/C++ union declarations', () => {
+ it('extracts a named union as a type node, but not a forward declaration', () => {
+ const code = `
+union packet_hdr {
+ unsigned int raw;
+ unsigned short port;
+};
+
+/* forward declaration — not a definition */
+union opaque_hdr;
+
+static unsigned int hdr_raw(union packet_hdr *h) { return h->raw; }
+`;
+ const result = extractFromSource('packet.c', code);
+
+ const hdr = result.nodes.find((n) => n.name === 'packet_hdr');
+ expect(hdr).toBeDefined();
+ expect(hdr?.kind).toBe('union');
+
+ // Same rule as `struct Foo;`: bodiless is a forward declaration, so it must
+ // not mint a phantom node beside the real definition.
+ expect(result.nodes.some((n) => n.name === 'opaque_hdr')).toBe(false);
+
+ // Exactly one node for the type — the definition — so a call site or a
+ // `union packet_hdr *` parameter has a single resolution target.
+ expect(result.nodes.filter((n) => n.name === 'packet_hdr')).toHaveLength(1);
+ });
+
+ it('gives a typedef union the typedef name, not a second node', () => {
+ const code = `
+typedef union {
+ unsigned int u;
+ float f;
+} word_t;
+`;
+ const result = extractFromSource('word.c', code);
+
+ const word = result.nodes.find((n) => n.name === 'word_t');
+ expect(word?.kind).toBe('union');
+ // Resolved through the typedef the same way `typedef struct { … } X;` is,
+ // so the anonymous union body does not become its own node.
+ expect(result.nodes.some((n) => n.name === '')).toBe(false);
+ });
+
+ it('extracts a C++ union with member functions', () => {
+ const code = `
+union Value {
+ int i;
+ double d;
+ int as_int() const { return i; }
+};
+`;
+ const result = extractFromSource('value.cpp', code);
+
+ const value = result.nodes.find((n) => n.name === 'Value');
+ expect(value?.kind).toBe('union');
+
+ const asInt = result.nodes.find((n) => n.name === 'as_int');
+ expect(asInt).toBeDefined();
+ expect(
+ result.edges.some((e) => e.kind === 'contains' && e.source === value?.id && e.target === asInt?.id)
+ ).toBe(true);
+ });
+});
+
describe('Dart mixins and type references', () => {
let tempDir: string;
let cg: CodeGraph;
@@ -8602,6 +8712,22 @@ void helperFunction(int count) {
expect(imports).toContain('MyClass.h');
});
+ it('extracts union declarations as first-class union nodes', () => {
+ const code = `
+typedef union {
+ unsigned int raw;
+ float value;
+} NumberBits;
+
+union opaque_bits;
+`;
+ const result = extractFromSource('NumberBits.m', code);
+
+ const numberBits = result.nodes.find((n) => n.name === 'NumberBits');
+ expect(numberBits?.kind).toBe('union');
+ expect(result.nodes.some((n) => n.name === 'opaque_bits')).toBe(false);
+ });
+
it('should record inheritance and protocol conformance', () => {
const result = extractFromSource('App.m', sample);
const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
diff --git a/__tests__/fixtures/ambient-decls-ts/package.json b/__tests__/fixtures/ambient-decls-ts/package.json
new file mode 100644
index 000000000..90cf337a1
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "ambient-decls-ts-fixture",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "description": "CG-28 fixture — declaration-only files competing with implementation for one explore envelope."
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/lib/bucket.ts b/__tests__/fixtures/ambient-decls-ts/src/lib/bucket.ts
new file mode 100644
index 000000000..15304c64b
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/lib/bucket.ts
@@ -0,0 +1,46 @@
+export interface BucketObject {
+ key: string;
+ body: ReadableStream;
+ size: number;
+}
+
+export interface Bucket {
+ put(
+ key: string,
+ value: ReadableStream,
+ options?: { httpMetadata?: { contentType?: string } },
+ ): Promise;
+ get(key: string): Promise;
+}
+
+export interface MetadataStore {
+ put(id: string, value: string): Promise;
+ get(id: string): Promise;
+}
+
+const objects = new Map();
+const rows = new Map();
+
+/** The object-storage binding. */
+export function openBucket(): Bucket {
+ return {
+ async put(key, value) {
+ objects.set(key, { key, body: value, size: 0 });
+ },
+ async get(key) {
+ return objects.get(key) ?? null;
+ },
+ };
+}
+
+/** The metadata key-value binding. */
+export function openMetadataStore(): MetadataStore {
+ return {
+ async put(id, value) {
+ rows.set(id, value);
+ },
+ async get(id) {
+ return rows.get(id) ?? null;
+ },
+ };
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/lib/queue.ts b/__tests__/fixtures/ambient-decls-ts/src/lib/queue.ts
new file mode 100644
index 000000000..9d804bdda
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/lib/queue.ts
@@ -0,0 +1,37 @@
+export interface UploadMessageBody {
+ key: string;
+ metadataId: string;
+ contentType: string;
+}
+
+/**
+ * Publish the follow-up message for a stored upload. Batched so a burst of
+ * uploads does not open one producer call per object.
+ */
+export async function enqueueUploadMessage(body: UploadMessageBody): Promise {
+ const queue = openUploadQueue();
+ await queue.send(body, { contentType: 'json' });
+}
+
+/** Consumer side: process a batch of upload messages. */
+export async function consumeUploadBatch(messages: UploadMessageBody[]): Promise {
+ let handled = 0;
+ for (const message of messages) {
+ if (!message.key) continue;
+ handled += 1;
+ }
+ return handled;
+}
+
+interface UploadQueue {
+ send(body: UploadMessageBody, options: { contentType: string }): Promise;
+}
+
+/** The binding lookup, isolated so tests can swap it. */
+export function openUploadQueue(): UploadQueue {
+ return {
+ async send() {
+ /* binding provided by the runtime */
+ },
+ };
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/lib/request.ts b/__tests__/fixtures/ambient-decls-ts/src/lib/request.ts
new file mode 100644
index 000000000..52fcc2ea8
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/lib/request.ts
@@ -0,0 +1,44 @@
+export interface ParsedUpload {
+ ok: true;
+ key: string;
+ body: ReadableStream;
+ contentType: string;
+ width: number;
+ height: number;
+ format: string;
+}
+
+export interface ParseFailure {
+ ok: false;
+ error: string;
+}
+
+/**
+ * Pull the object key, declared dimensions and the raw body stream off an
+ * upload request. Never buffers the body — the stream is handed straight to
+ * the storage layer.
+ */
+export async function parseUploadRequest(
+ request: Request,
+): Promise {
+ const url = new URL(request.url);
+ const key = url.searchParams.get('key');
+ if (!key) return { ok: false, error: 'missing key' };
+ if (!request.body) return { ok: false, error: 'missing body' };
+
+ return {
+ ok: true,
+ key,
+ body: request.body as ReadableStream,
+ contentType: request.headers.get('content-type') ?? 'application/octet-stream',
+ width: numberParam(url, 'width'),
+ height: numberParam(url, 'height'),
+ format: url.searchParams.get('format') ?? 'jpeg',
+ };
+}
+
+function numberParam(url: URL, name: string): number {
+ const raw = url.searchParams.get(name);
+ const parsed = raw ? Number.parseInt(raw, 10) : 0;
+ return Number.isFinite(parsed) ? parsed : 0;
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/routes/upload.ts b/__tests__/fixtures/ambient-decls-ts/src/routes/upload.ts
new file mode 100644
index 000000000..15604934c
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/routes/upload.ts
@@ -0,0 +1,66 @@
+import { streamBodyToStorage } from '../storage/stream.js';
+import { recordImageMetadata } from '../storage/metadata.js';
+import { enqueueUploadMessage } from '../lib/queue.js';
+import { parseUploadRequest } from '../lib/request.js';
+
+export interface UploadResult {
+ key: string;
+ bytes: number;
+ contentType: string;
+}
+
+/**
+ * Entry point for an upload request: parse it, stream the body into object
+ * storage, record the image metadata, then queue the follow-up work.
+ */
+export async function handleUploadRequest(request: Request): Promise {
+ const parsed = await parseUploadRequest(request);
+ if (!parsed.ok) {
+ return new Response(JSON.stringify({ error: parsed.error }), { status: 400 });
+ }
+
+ const stored = await streamBodyToStorage(parsed.body, parsed.key, parsed.contentType);
+ const metadata = await recordImageMetadata(stored.key, {
+ width: parsed.width,
+ height: parsed.height,
+ format: parsed.format,
+ bytes: stored.bytes,
+ });
+
+ await enqueueUploadMessage({
+ key: stored.key,
+ metadataId: metadata.id,
+ contentType: stored.contentType,
+ });
+
+ return new Response(JSON.stringify(summarizeUpload(stored, metadata.id)), {
+ status: 201,
+ headers: { 'content-type': 'application/json' },
+ });
+}
+
+/** Shape the client sees back after a successful upload. */
+export function summarizeUpload(stored: UploadResult, metadataId: string) {
+ return {
+ key: stored.key,
+ bytes: stored.bytes,
+ contentType: stored.contentType,
+ metadataId,
+ };
+}
+
+/** Reject uploads whose declared size exceeds the per-account ceiling. */
+export function isWithinUploadLimit(bytes: number, limit: number): boolean {
+ if (!Number.isFinite(bytes) || bytes < 0) return false;
+ return bytes <= limit;
+}
+
+/** Delete-side counterpart, kept here so the route module is not a one-liner. */
+export async function handleDeleteRequest(request: Request, key: string): Promise {
+ const parsed = await parseUploadRequest(request);
+ if (!parsed.ok) {
+ return new Response(JSON.stringify({ error: parsed.error }), { status: 400 });
+ }
+ await enqueueUploadMessage({ key, metadataId: '', contentType: 'application/x-delete' });
+ return new Response(null, { status: 204 });
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/storage/metadata.ts b/__tests__/fixtures/ambient-decls-ts/src/storage/metadata.ts
new file mode 100644
index 000000000..161b0f06a
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/storage/metadata.ts
@@ -0,0 +1,54 @@
+import { openMetadataStore } from '../lib/bucket.js';
+
+export interface ImageMetadataInput {
+ width: number;
+ height: number;
+ format: string;
+ bytes: number;
+}
+
+export interface ImageMetadataRecord extends ImageMetadataInput {
+ id: string;
+ key: string;
+ recordedAt: number;
+}
+
+/**
+ * Record the image metadata for a stored object. Writes go to the metadata
+ * store keyed by object key; the returned record carries the id the queue
+ * message references.
+ */
+export async function recordImageMetadata(
+ key: string,
+ input: ImageMetadataInput,
+): Promise {
+ const store = openMetadataStore();
+ const record: ImageMetadataRecord = {
+ ...input,
+ id: metadataIdFor(key, input),
+ key,
+ recordedAt: 0,
+ };
+ await store.put(record.id, JSON.stringify(record));
+ return record;
+}
+
+/** Deterministic id so a retried upload records the same metadata row. */
+export function metadataIdFor(key: string, input: ImageMetadataInput): string {
+ return `${key}:${input.format}:${input.width}x${input.height}`;
+}
+
+/** Read a metadata record back for the download and listing paths. */
+export async function loadImageMetadata(id: string): Promise {
+ const store = openMetadataStore();
+ const raw = await store.get(id);
+ return raw ? (JSON.parse(raw) as ImageMetadataRecord) : null;
+}
+
+/** Normalize a client-declared format string to the canonical set. */
+export function normalizeFormat(format: string): string {
+ const lowered = format.trim().toLowerCase();
+ if (lowered === 'jpg') return 'jpeg';
+ if (lowered === 'tif') return 'tiff';
+ return lowered;
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/storage/stream.ts b/__tests__/fixtures/ambient-decls-ts/src/storage/stream.ts
new file mode 100644
index 000000000..76c784d82
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/storage/stream.ts
@@ -0,0 +1,86 @@
+import { openBucket } from '../lib/bucket.js';
+import type { StorageFailure, UploadTelemetry } from './types.js';
+
+export interface StoredObject {
+ key: string;
+ bytes: number;
+ contentType: string;
+}
+
+/**
+ * Stream a request body into object storage without buffering it in memory.
+ * The body is piped through a counting transform so the byte total is known
+ * by the time the put resolves.
+ */
+export async function streamBodyToStorage(
+ body: ReadableStream,
+ key: string,
+ contentType: string,
+): Promise {
+ const bucket = openBucket();
+ const counter = createByteCounter();
+ const piped = body.pipeThrough(counter.transform, { preventClose: false });
+
+ await bucket.put(key, piped, { httpMetadata: { contentType } });
+
+ return { key, bytes: counter.total(), contentType };
+}
+
+/**
+ * A transform stream that counts the bytes flowing through it. Separated from
+ * the pipe above so the byte total can be read after the stream settles.
+ */
+export function createByteCounter() {
+ let total = 0;
+ const transform = new TransformStream({
+ transform(chunk, controller) {
+ total += chunk.byteLength;
+ controller.enqueue(chunk);
+ },
+ });
+ return { transform, total: () => total };
+}
+
+/**
+ * Read a stored object back out of the bucket as a stream, for the download
+ * path. Mirrors the upload side so both directions live in one module.
+ */
+export async function readObjectStream(key: string): Promise | null> {
+ const bucket = openBucket();
+ const object = await bucket.get(key);
+ if (!object) return null;
+ return object.body;
+}
+
+/** Timing/retry record for one stored object, handed to the metrics sink. */
+export function telemetryFor(stored: StoredObject, durationMs: number): UploadTelemetry {
+ return { key: stored.key, bytes: stored.bytes, durationMs, retries: 0 };
+}
+
+/** Describe a failed stage so the caller can report it without re-deriving it. */
+export function storageFailure(
+ key: string,
+ stage: StorageFailure['stage'],
+ message: string,
+): StorageFailure {
+ return { key, stage, message };
+}
+
+/** Cap a stream at `limit` bytes, erroring out rather than storing an overrun. */
+export function limitStream(
+ source: ReadableStream,
+ limit: number,
+): ReadableStream {
+ let seen = 0;
+ const guard = new TransformStream({
+ transform(chunk, controller) {
+ seen += chunk.byteLength;
+ if (seen > limit) {
+ controller.error(new Error(`upload exceeded ${limit} bytes`));
+ return;
+ }
+ controller.enqueue(chunk);
+ },
+ });
+ return source.pipeThrough(guard);
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/src/storage/types.ts b/__tests__/fixtures/ambient-decls-ts/src/storage/types.ts
new file mode 100644
index 000000000..53185a21e
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/src/storage/types.ts
@@ -0,0 +1,18 @@
+/**
+ * Shared shapes for the storage layer. Declaration-only like the ambient files
+ * under `types/` — but the modules that answer a flow question are typed BY it,
+ * so it is part of that answer's structure rather than a global shim.
+ */
+
+export interface UploadTelemetry {
+ key: string;
+ bytes: number;
+ durationMs: number;
+ retries: number;
+}
+
+export interface StorageFailure {
+ key: string;
+ stage: 'parse' | 'stream' | 'metadata' | 'queue';
+ message: string;
+}
diff --git a/__tests__/fixtures/ambient-decls-ts/types/platform-shims.d.ts b/__tests__/fixtures/ambient-decls-ts/types/platform-shims.d.ts
new file mode 100644
index 000000000..9b03b63a3
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/types/platform-shims.d.ts
@@ -0,0 +1,212 @@
+// Hand-maintained ambient declarations for the parts of the platform our
+// runtime exposes but the published typings do not cover yet. Edit freely —
+// nothing regenerates this file. Kept alongside the app so module augmentation
+// and the global shims live in one place.
+
+declare global {
+ interface UploadStorage {
+ put(
+ key: string,
+ body: ReadableStream,
+ options?: UploadPutOptions,
+ ): Promise;
+ get(key: string): Promise;
+ head(key: string): Promise;
+ delete(key: string | string[]): Promise;
+ list(options?: UploadListOptions): Promise;
+ }
+
+ interface StoredUploadObject {
+ readonly key: string;
+ readonly size: number;
+ readonly etag: string;
+ readonly uploaded: Date;
+ readonly body: ReadableStream;
+ readonly contentType: string;
+ readonly metadata?: ImageMetadataShim;
+ arrayBuffer(): Promise;
+ text(): Promise;
+ json(): Promise;
+ }
+
+ interface StoredUploadHead {
+ readonly key: string;
+ readonly size: number;
+ readonly etag: string;
+ readonly uploaded: Date;
+ readonly contentType: string;
+ }
+
+ interface UploadPutOptions {
+ contentType?: string;
+ cacheControl?: string;
+ customMetadata?: Record;
+ checksum?: string;
+ storageClass?: 'standard' | 'infrequent';
+ }
+
+ interface UploadListOptions {
+ prefix?: string;
+ cursor?: string;
+ limit?: number;
+ delimiter?: string;
+ include?: ('metadata' | 'contentType')[];
+ }
+
+ interface UploadListResult {
+ objects: StoredUploadHead[];
+ truncated: boolean;
+ cursor?: string;
+ prefixes: string[];
+ }
+
+ interface ImageMetadataShim {
+ format: string;
+ fileSize: number;
+ width: number;
+ height: number;
+ orientation?: number;
+ colorSpace?: string;
+ }
+
+ interface MetadataRowShim {
+ id: string;
+ key: string;
+ recordedAt: number;
+ format: string;
+ bytes: number;
+ width: number;
+ height: number;
+ }
+
+ interface MetadataStoreShim {
+ put(id: string, value: string, options?: MetadataPutOptions): Promise;
+ get(id: string): Promise;
+ getWithMetadata(id: string): Promise<{ value: string | null; metadata: T | null }>;
+ delete(id: string): Promise;
+ list(options?: MetadataListOptions): Promise;
+ }
+
+ interface MetadataPutOptions {
+ expiration?: number;
+ expirationTtl?: number;
+ metadata?: unknown;
+ }
+
+ interface MetadataListOptions {
+ prefix?: string | null;
+ cursor?: string | null;
+ limit?: number;
+ }
+
+ interface MetadataListResult {
+ keys: { name: string; expiration?: number }[];
+ list_complete: boolean;
+ cursor?: string;
+ }
+
+ interface UploadQueueShim {
+ send(body: Body, options?: UploadSendOptions): Promise;
+ sendBatch(bodies: Iterable>): Promise;
+ }
+
+ interface UploadSendOptions {
+ contentType?: UploadContentType;
+ delaySeconds?: number;
+ }
+
+ type UploadContentType = 'text' | 'bytes' | 'json' | 'v8';
+
+ interface UploadSendRequest {
+ body: Body;
+ options?: UploadSendOptions;
+ }
+
+ interface UploadMessageShim {
+ readonly id: string;
+ readonly timestamp: Date;
+ readonly body: Body;
+ readonly attempts: number;
+ retry(options?: UploadRetryOptions): void;
+ ack(): void;
+ }
+
+ interface UploadRetryOptions {
+ delaySeconds?: number;
+ }
+
+ interface UploadMessageBatch {
+ readonly messages: readonly UploadMessageShim[];
+ readonly queue: string;
+ retryAll(options?: UploadRetryOptions): void;
+ ackAll(): void;
+ }
+
+ interface StreamPipeOptionsShim {
+ preventClose?: boolean;
+ preventAbort?: boolean;
+ preventCancel?: boolean;
+ signal?: AbortSignal;
+ }
+
+ interface ByteCounterShim {
+ readonly transform: TransformStream;
+ total(): number;
+ }
+
+ interface StreamLimitShim {
+ readonly limit: number;
+ readonly seen: number;
+ exceeded(): boolean;
+ }
+
+ interface RequestBodyShim {
+ readonly body: ReadableStream | null;
+ readonly bodyUsed: boolean;
+ readonly headers: Headers;
+ readonly url: string;
+ arrayBuffer(): Promise;
+ formData(): Promise;
+ blob(): Promise;
+ }
+
+ interface ParsedUploadShim {
+ key: string;
+ contentType: string;
+ width: number;
+ height: number;
+ format: string;
+ }
+
+ interface ImageTransformerShim {
+ transform(transform: ImageTransformShim): ImageTransformerShim;
+ output(options: ImageOutputShim): Promise;
+ }
+
+ interface ImageTransformShim {
+ width?: number;
+ height?: number;
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
+ rotate?: number;
+ }
+
+ interface ImageOutputShim {
+ format?: string;
+ quality?: number;
+ background?: string;
+ }
+
+ interface ImageResultShim {
+ contentType(): string;
+ image(): ReadableStream;
+ response(): Response;
+ }
+
+ interface UploadEnvShim {
+ UPLOADS: UploadStorage;
+ METADATA: MetadataStoreShim;
+ UPLOAD_QUEUE: UploadQueueShim;
+ }
+}
+
+export {};
diff --git a/__tests__/fixtures/ambient-decls-ts/types/worker-configuration.d.ts b/__tests__/fixtures/ambient-decls-ts/types/worker-configuration.d.ts
new file mode 100644
index 000000000..d430f8cfb
--- /dev/null
+++ b/__tests__/fixtures/ambient-decls-ts/types/worker-configuration.d.ts
@@ -0,0 +1,271 @@
+// Generated by Wrangler by running `wrangler types` (hash: 4f1c8ad2b90e)
+// Runtime types generated with workerd@1.20260701.0 2026-07-01 nodejs_compat
+declare namespace Cloudflare {
+ interface Env {
+ UPLOADS: R2Bucket;
+ METADATA: KVNamespace;
+ UPLOAD_QUEUE: Queue;
+ IMAGES: ImagesBinding;
+ }
+}
+
+interface UploadMessageBody {
+ key: string;
+ metadataId: string;
+ contentType: string;
+}
+
+interface R2Bucket {
+ head(key: string): Promise;
+ get(key: string, options?: R2GetOptions): Promise;
+ put(
+ key: string,
+ value: ReadableStream | ArrayBuffer | string | null,
+ options?: R2PutOptions,
+ ): Promise;
+ delete(keys: string | string[]): Promise;
+ list(options?: R2ListOptions): Promise;
+ createMultipartUpload(key: string, options?: R2MultipartOptions): Promise;
+ resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload;
+}
+
+interface R2Object {
+ readonly key: string;
+ readonly version: string;
+ readonly size: number;
+ readonly etag: string;
+ readonly httpEtag: string;
+ readonly checksums: R2Checksums;
+ readonly uploaded: Date;
+ readonly httpMetadata?: R2HTTPMetadata;
+ readonly customMetadata?: Record;
+ readonly range?: R2Range;
+ readonly storageClass: string;
+ writeHttpMetadata(headers: Headers): void;
+}
+
+interface R2ObjectBody extends R2Object {
+ get body(): ReadableStream;
+ get bodyUsed(): boolean;
+ arrayBuffer(): Promise;
+ text(): Promise;
+ json(): Promise;
+ blob(): Promise;
+ bytes(): Promise;
+}
+
+interface R2GetOptions {
+ onlyIf?: R2Conditional | Headers;
+ range?: R2Range;
+ ssecKey?: ArrayBuffer | string;
+}
+
+interface R2PutOptions {
+ onlyIf?: R2Conditional | Headers;
+ httpMetadata?: R2HTTPMetadata | Headers;
+ customMetadata?: Record;
+ md5?: ArrayBuffer | string;
+ sha1?: ArrayBuffer | string;
+ sha256?: ArrayBuffer | string;
+ storageClass?: string;
+ ssecKey?: ArrayBuffer | string;
+}
+
+interface R2ListOptions {
+ limit?: number;
+ prefix?: string;
+ cursor?: string;
+ delimiter?: string;
+ startAfter?: string;
+ include?: ('httpMetadata' | 'customMetadata')[];
+}
+
+interface R2Objects {
+ objects: R2Object[];
+ truncated: boolean;
+ cursor?: string;
+ delimitedPrefixes: string[];
+}
+
+interface R2MultipartOptions {
+ httpMetadata?: R2HTTPMetadata | Headers;
+ customMetadata?: Record;
+ storageClass?: string;
+}
+
+interface R2MultipartUpload {
+ readonly key: string;
+ readonly uploadId: string;
+ uploadPart(
+ partNumber: number,
+ value: ReadableStream | ArrayBuffer | string | Blob,
+ ): Promise;
+ abort(): Promise;
+ complete(uploadedParts: R2UploadedPart[]): Promise;
+}
+
+interface R2UploadedPart {
+ partNumber: number;
+ etag: string;
+}
+
+interface R2HTTPMetadata {
+ contentType?: string;
+ contentLanguage?: string;
+ contentDisposition?: string;
+ contentEncoding?: string;
+ cacheControl?: string;
+ cacheExpiry?: Date;
+}
+
+interface R2Checksums {
+ readonly md5?: ArrayBuffer;
+ readonly sha1?: ArrayBuffer;
+ readonly sha256?: ArrayBuffer;
+ toJSON(): R2StringChecksums;
+}
+
+interface R2StringChecksums {
+ md5?: string;
+ sha1?: string;
+ sha256?: string;
+}
+
+interface R2Conditional {
+ etagMatches?: string;
+ etagDoesNotMatch?: string;
+ uploadedBefore?: Date;
+ uploadedAfter?: Date;
+ secondsGranularity?: boolean;
+}
+
+interface R2Range {
+ offset?: number;
+ length?: number;
+ suffix?: number;
+}
+
+interface KVNamespace {
+ get(key: Key, options?: Partial>): Promise;
+ getWithMetadata(
+ key: Key,
+ options?: Partial>,
+ ): Promise>;
+ put(
+ key: Key,
+ value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
+ options?: KVNamespacePutOptions,
+ ): Promise;
+ delete(key: Key): Promise;
+ list(
+ options?: KVNamespaceListOptions,
+ ): Promise>;
+}
+
+interface KVNamespaceGetOptions {
+ type: Type;
+ cacheTtl?: number;
+}
+
+interface KVNamespacePutOptions {
+ expiration?: number;
+ expirationTtl?: number;
+ metadata?: unknown | null;
+}
+
+interface KVNamespaceListOptions {
+ limit?: number;
+ prefix?: string | null;
+ cursor?: string | null;
+}
+
+interface KVNamespaceListResult {
+ keys: KVNamespaceListKey[];
+ list_complete: boolean;
+ cursor?: string;
+}
+
+interface KVNamespaceListKey {
+ name: Key;
+ expiration?: number;
+ metadata?: Metadata;
+}
+
+interface KVNamespaceGetWithMetadataResult {
+ value: Value | null;
+ metadata: Metadata | null;
+ cacheStatus: string | null;
+}
+
+interface Queue {
+ send(message: Body, options?: QueueSendOptions): Promise;
+ sendBatch(messages: Iterable>): Promise;
+}
+
+interface QueueSendOptions {
+ contentType?: QueueContentType;
+ delaySeconds?: number;
+}
+
+type QueueContentType = 'text' | 'bytes' | 'json' | 'v8';
+
+interface MessageSendRequest {
+ body: Body;
+ options?: QueueSendOptions;
+}
+
+interface Message {
+ readonly id: string;
+ readonly timestamp: Date;
+ readonly body: Body;
+ readonly attempts: number;
+ retry(options?: QueueRetryOptions): void;
+ ack(): void;
+}
+
+interface QueueRetryOptions {
+ delaySeconds?: number;
+}
+
+interface MessageBatch {
+ readonly messages: readonly Message[];
+ readonly queue: string;
+ retryAll(options?: QueueRetryOptions): void;
+ ackAll(): void;
+}
+
+interface ImagesBinding {
+ info(stream: ReadableStream): Promise;
+ input(stream: ReadableStream): ImageTransformer;
+}
+
+interface ImageMetadata {
+ format: string;
+ fileSize: number;
+ width: number;
+ height: number;
+}
+
+interface ImageTransformer {
+ transform(transform: ImageTransform): ImageTransformer;
+ output(options: ImageOutputOptions): Promise;
+}
+
+interface ImageTransform {
+ width?: number;
+ height?: number;
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
+ rotate?: number;
+}
+
+interface ImageOutputOptions {
+ format?: string;
+ quality?: number;
+ background?: string;
+}
+
+interface ImageTransformationResult {
+ contentType(): string;
+ image(): ReadableStream;
+ response(): Response;
+}
diff --git a/__tests__/fixtures/dense-header-ts/package.json b/__tests__/fixtures/dense-header-ts/package.json
new file mode 100644
index 000000000..52123c6c0
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "dense-header-fixture",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module"
+}
diff --git a/__tests__/fixtures/dense-header-ts/src/core/queue.ts b/__tests__/fixtures/dense-header-ts/src/core/queue.ts
new file mode 100644
index 000000000..e490cdf32
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/src/core/queue.ts
@@ -0,0 +1,23 @@
+import type { URLSessionTask } from './types';
+
+export class RequestQueue {
+ private readonly waiting: URLSessionTask[] = [];
+ private running = 0;
+
+ enqueue(task: URLSessionTask, limit: number): void {
+ if (this.running < limit) {
+ this.running += 1;
+ return;
+ }
+ this.waiting.push(task);
+ }
+
+ release(): URLSessionTask | undefined {
+ this.running = Math.max(0, this.running - 1);
+ return this.waiting.shift();
+ }
+
+ get depth(): number {
+ return this.waiting.length;
+ }
+}
diff --git a/__tests__/fixtures/dense-header-ts/src/core/request-builder.ts b/__tests__/fixtures/dense-header-ts/src/core/request-builder.ts
new file mode 100644
index 000000000..762559671
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/src/core/request-builder.ts
@@ -0,0 +1,27 @@
+import type { CachePolicy, URLRequest } from './types';
+
+export function buildURLRequest(options: {
+ url: string;
+ method: string;
+ body?: Uint8Array;
+ headers: Record;
+ timeout: number;
+ cachePolicy: CachePolicy;
+}): URLRequest {
+ const headers = { ...options.headers };
+ if (options.body && !headers['content-length']) {
+ headers['content-length'] = String(options.body.length);
+ }
+ return {
+ url: normalize(options.url),
+ method: options.method.toUpperCase(),
+ headers,
+ body: options.body,
+ timeout: options.timeout,
+ cachePolicy: options.cachePolicy,
+ };
+}
+
+function normalize(url: string): string {
+ return url.endsWith('/') && url.split('/').length > 4 ? url.slice(0, -1) : url;
+}
diff --git a/__tests__/fixtures/dense-header-ts/src/core/task-factory.ts b/__tests__/fixtures/dense-header-ts/src/core/task-factory.ts
new file mode 100644
index 000000000..0f236264a
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/src/core/task-factory.ts
@@ -0,0 +1,23 @@
+import type { RequestDelegate, TaskResponse, URLRequest, URLSessionTask } from './types';
+
+export function makeTask(options: {
+ identifier: number;
+ request: URLRequest;
+ delegate: RequestDelegate;
+ allowsCellularAccess: boolean;
+ waitsForConnectivity: boolean;
+ resourceTimeout: number;
+}): URLSessionTask {
+ const handlers: Array<(response: TaskResponse) => void> = [];
+ return {
+ identifier: options.identifier,
+ request: options.request,
+ state: 'initialized',
+ cancel() { this.state = 'cancelled'; },
+ onComplete(handler) { handlers.push(handler); },
+ };
+}
+
+export function resumeTask(task: URLSessionTask): void {
+ task.state = 'resumed';
+}
diff --git a/__tests__/fixtures/dense-header-ts/src/core/types.ts b/__tests__/fixtures/dense-header-ts/src/core/types.ts
new file mode 100644
index 000000000..d444e0ad4
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/src/core/types.ts
@@ -0,0 +1,42 @@
+export type CachePolicy = 'useProtocolCachePolicy' | 'reloadIgnoringLocalCacheData' | 'returnCacheDataElseLoad';
+export type RequestState = 'initialized' | 'resumed' | 'suspended' | 'cancelled' | 'finished';
+
+export interface URLRequest {
+ url: string;
+ method: string;
+ headers: Record;
+ body?: Uint8Array;
+ timeout: number;
+ cachePolicy: CachePolicy;
+}
+
+export interface TaskResponse {
+ status: number;
+ headers: Record;
+ body: Uint8Array;
+}
+
+export interface URLSessionTask {
+ identifier: number;
+ request: URLRequest;
+ state: RequestState;
+ cancel(): void;
+ onComplete(handler: (response: TaskResponse) => void): void;
+}
+
+export interface Adapter { adapt(request: URLRequest): URLRequest; }
+export interface Serializer { serialize(value: unknown): Uint8Array; }
+export interface Validator { validate(response: TaskResponse): { ok: boolean; reason?: string }; }
+export interface Retrier { shouldRetry(response: TaskResponse, verdict: { ok: boolean }): boolean; }
+export interface RedirectHandler { resolve(location: string, original: URLRequest): { url: string; method: string; body?: Uint8Array } | null; }
+export interface TrustEvaluator { evaluate(host: string): boolean; }
+export interface Credential { apply(request: URLRequest): URLRequest; }
+export interface Interceptor { name: string; adapt(request: URLRequest, session: unknown): Promise; }
+export interface RequestDelegate { willSend(request: URLRequest): void; }
+export interface EventMonitor {
+ didAdaptRequest(request: URLRequest, interceptor: string): void;
+ didCreateTask(task: URLSessionTask, request: URLRequest): void;
+ didResumeTask(task: URLSessionTask): void;
+ didRetryTask(task: URLSessionTask, previousIdentifier: number): void;
+ didCompleteTask(task: URLSessionTask, response: TaskResponse): void;
+}
diff --git a/__tests__/fixtures/dense-header-ts/src/index.ts b/__tests__/fixtures/dense-header-ts/src/index.ts
new file mode 100644
index 000000000..e17cc6d6d
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/src/index.ts
@@ -0,0 +1,3 @@
+export { Session } from './net/session';
+export { RequestQueue } from './core/queue';
+export { buildURLRequest } from './core/request-builder';
diff --git a/__tests__/fixtures/dense-header-ts/src/net/session.ts b/__tests__/fixtures/dense-header-ts/src/net/session.ts
new file mode 100644
index 000000000..77660e931
--- /dev/null
+++ b/__tests__/fixtures/dense-header-ts/src/net/session.ts
@@ -0,0 +1,285 @@
+import type {
+ Adapter,
+ CachePolicy,
+ Credential,
+ EventMonitor,
+ Interceptor,
+ RedirectHandler,
+ RequestDelegate,
+ RequestState,
+ Retrier,
+ Serializer,
+ TrustEvaluator,
+ URLRequest,
+ URLSessionTask,
+ Validator,
+} from '../core/types';
+import { buildURLRequest } from '../core/request-builder';
+import { makeTask, resumeTask } from '../core/task-factory';
+import { RequestQueue } from '../core/queue';
+
+/**
+ * The shape density-first ranking exists for: a class whose top-of-file header
+ * is a long, tightly-packed property list — dozens of adjacent declarations,
+ * each individually trivial — while the methods a flow question actually asks
+ * about live hundreds of lines below it.
+ *
+ * Ranked by density alone the header wins the file's whole budget and the
+ * methods are buried. The ranking puts importance first for exactly this
+ * reason, and density only breaks ties inside one importance tier.
+ */
+export class Session {
+ readonly identifier: string;
+ readonly adapter: Adapter;
+ readonly serializer: Serializer;
+ readonly validator: Validator;
+ readonly retrier: Retrier;
+ readonly redirectHandler: RedirectHandler;
+ readonly trustEvaluator: TrustEvaluator;
+ readonly eventMonitor: EventMonitor;
+ readonly cachePolicy: CachePolicy;
+ readonly credential: Credential | null;
+ readonly interceptors: Interceptor[];
+ readonly delegate: RequestDelegate;
+ readonly queue: RequestQueue;
+ readonly startRequestsImmediately: boolean;
+ readonly maximumConnectionsPerHost: number;
+ readonly timeoutIntervalForRequest: number;
+ readonly timeoutIntervalForResource: number;
+ readonly allowsCellularAccess: boolean;
+ readonly waitsForConnectivity: boolean;
+ readonly httpShouldUsePipelining: boolean;
+ readonly httpShouldSetCookies: boolean;
+ readonly httpMaximumConnectionsPerHost: number;
+ readonly sessionConfigurationName: string;
+ readonly requestState: RequestState;
+ readonly defaultHeaders: Record;
+ readonly userAgent: string;
+ readonly acceptEncoding: string;
+ readonly acceptLanguage: string;
+ private taskCounter = 0;
+ private active = new Map();
+
+ constructor(options: Partial & { identifier: string }) {
+ this.identifier = options.identifier;
+ this.adapter = options.adapter!;
+ this.serializer = options.serializer!;
+ this.validator = options.validator!;
+ this.retrier = options.retrier!;
+ this.redirectHandler = options.redirectHandler!;
+ this.trustEvaluator = options.trustEvaluator!;
+ this.eventMonitor = options.eventMonitor!;
+ this.cachePolicy = options.cachePolicy ?? 'useProtocolCachePolicy';
+ this.credential = options.credential ?? null;
+ this.interceptors = options.interceptors ?? [];
+ this.delegate = options.delegate!;
+ this.queue = options.queue ?? new RequestQueue();
+ this.startRequestsImmediately = options.startRequestsImmediately ?? true;
+ this.maximumConnectionsPerHost = options.maximumConnectionsPerHost ?? 6;
+ this.timeoutIntervalForRequest = options.timeoutIntervalForRequest ?? 60;
+ this.timeoutIntervalForResource = options.timeoutIntervalForResource ?? 604800;
+ this.allowsCellularAccess = options.allowsCellularAccess ?? true;
+ this.waitsForConnectivity = options.waitsForConnectivity ?? false;
+ this.httpShouldUsePipelining = options.httpShouldUsePipelining ?? false;
+ this.httpShouldSetCookies = options.httpShouldSetCookies ?? true;
+ this.httpMaximumConnectionsPerHost = options.httpMaximumConnectionsPerHost ?? 6;
+ this.sessionConfigurationName = options.sessionConfigurationName ?? 'default';
+ this.requestState = options.requestState ?? 'initialized';
+ this.defaultHeaders = options.defaultHeaders ?? {};
+ this.userAgent = options.userAgent ?? 'session/1.0';
+ this.acceptEncoding = options.acceptEncoding ?? 'br;q=1.0, gzip;q=0.9';
+ this.acceptLanguage = options.acceptLanguage ?? 'en;q=1.0';
+ }
+
+ // -- configuration accessors ----------------------------------------------
+ // Individually trivial, adjacent, and dense. On the density tiebreak alone
+ // this block outranks anything with a body worth reading.
+
+ get isBackground(): boolean {
+ return this.sessionConfigurationName === 'background';
+ }
+
+ get connectionLimit(): number {
+ return Math.min(this.maximumConnectionsPerHost, this.httpMaximumConnectionsPerHost);
+ }
+
+ get headerDefaults(): Record {
+ return { ...this.defaultHeaders, 'user-agent': this.userAgent };
+ }
+
+ get acceptHeaders(): Record {
+ return { 'accept-encoding': this.acceptEncoding, 'accept-language': this.acceptLanguage };
+ }
+
+ get activeCount(): number {
+ return this.active.size;
+ }
+
+ get isIdle(): boolean {
+ return this.active.size === 0;
+ }
+
+ get nextIdentifier(): number {
+ return this.taskCounter + 1;
+ }
+
+ get description(): string {
+ return `Session(${this.identifier}, ${this.sessionConfigurationName})`;
+ }
+
+ cancelAll(): void {
+ for (const task of this.active.values()) task.cancel();
+ this.active.clear();
+ }
+
+ taskFor(identifier: number): URLSessionTask | undefined {
+ return this.active.get(identifier);
+ }
+
+ headers(): Record {
+ return { ...this.headerDefaults, ...this.acceptHeaders };
+ }
+
+ withUserAgent(userAgent: string): Session {
+ return new Session({ ...this, identifier: this.identifier, userAgent });
+ }
+
+ withTimeout(seconds: number): Session {
+ return new Session({ ...this, identifier: this.identifier, timeoutIntervalForRequest: seconds });
+ }
+
+ withInterceptor(interceptor: Interceptor): Session {
+ return new Session({
+ ...this,
+ identifier: this.identifier,
+ interceptors: [...this.interceptors, interceptor],
+ });
+ }
+
+ withCredential(credential: Credential): Session {
+ return new Session({ ...this, identifier: this.identifier, credential });
+ }
+
+ withCachePolicy(cachePolicy: CachePolicy): Session {
+ return new Session({ ...this, identifier: this.identifier, cachePolicy });
+ }
+
+ withQueue(queue: RequestQueue): Session {
+ return new Session({ ...this, identifier: this.identifier, queue });
+ }
+
+ withAdapter(adapter: Adapter): Session {
+ return new Session({ ...this, identifier: this.identifier, adapter });
+ }
+
+ withValidator(validator: Validator): Session {
+ return new Session({ ...this, identifier: this.identifier, validator });
+ }
+
+ withRetrier(retrier: Retrier): Session {
+ return new Session({ ...this, identifier: this.identifier, retrier });
+ }
+
+ withMonitor(eventMonitor: EventMonitor): Session {
+ return new Session({ ...this, identifier: this.identifier, eventMonitor });
+ }
+
+ // -- the flow ---------------------------------------------------------------
+ //
+ // The methods below are what a "how does a request get built and sent" question
+ // is about, and they sit hundreds of lines under the header block.
+
+ /**
+ * Turn a convenience call into a URLRequest, hand it to the adapter chain and
+ * start the resulting task. The entry point of the whole flow.
+ */
+ async perform(url: string, method: string, body?: Uint8Array): Promise {
+ const initial = buildURLRequest({
+ url,
+ method,
+ body,
+ headers: this.headers(),
+ timeout: this.timeoutIntervalForRequest,
+ cachePolicy: this.cachePolicy,
+ });
+ const adapted = await this.adapt(initial);
+ return this.didCreateURLRequest(adapted);
+ }
+
+ /**
+ * Every interceptor gets a chance to rewrite the request before it becomes a
+ * task. Runs in registration order, and a thrown error aborts the whole call.
+ */
+ private async adapt(request: URLRequest): Promise {
+ let current = request;
+ for (const interceptor of this.interceptors) {
+ current = await interceptor.adapt(current, this);
+ this.eventMonitor.didAdaptRequest(current, interceptor.name);
+ }
+ if (this.credential) current = this.credential.apply(current);
+ return current;
+ }
+
+ /**
+ * The adapted request is final: build the task around it, register it and —
+ * unless the session was told to wait — resume it immediately.
+ */
+ didCreateURLRequest(request: URLRequest): URLSessionTask {
+ this.taskCounter += 1;
+ const identifier = this.taskCounter;
+ const created = this.task(request, identifier);
+ this.active.set(identifier, created);
+ this.eventMonitor.didCreateTask(created, request);
+ if (this.startRequestsImmediately) this.resume(created);
+ return created;
+ }
+
+ /**
+ * Build the URLSessionTask for a request. Split out from
+ * `didCreateURLRequest` because retries rebuild the task without going back
+ * through the adapter chain.
+ */
+ task(request: URLRequest, identifier: number): URLSessionTask {
+ const created = makeTask({
+ identifier,
+ request,
+ delegate: this.delegate,
+ allowsCellularAccess: this.allowsCellularAccess,
+ waitsForConnectivity: this.waitsForConnectivity,
+ resourceTimeout: this.timeoutIntervalForResource,
+ });
+ created.onComplete((response) => {
+ this.active.delete(identifier);
+ const verdict = this.validator.validate(response);
+ if (!verdict.ok && this.retrier.shouldRetry(response, verdict)) {
+ this.retry(request, identifier);
+ return;
+ }
+ this.eventMonitor.didCompleteTask(created, response);
+ });
+ return created;
+ }
+
+ /** Put a built task on the queue and start it. */
+ resume(task: URLSessionTask): void {
+ this.queue.enqueue(task, this.connectionLimit);
+ resumeTask(task);
+ this.eventMonitor.didResumeTask(task);
+ }
+
+ /** Rebuild and restart a task the retrier asked for. */
+ private retry(request: URLRequest, previousIdentifier: number): void {
+ this.taskCounter += 1;
+ const retried = this.task(request, this.taskCounter);
+ this.active.set(this.taskCounter, retried);
+ this.eventMonitor.didRetryTask(retried, previousIdentifier);
+ this.resume(retried);
+ }
+
+ /** Follow a redirect by adapting and re-performing the new location. */
+ async follow(response: { location: string }, original: URLRequest): Promise {
+ const target = this.redirectHandler.resolve(response.location, original);
+ if (!target) throw new Error(`redirect to ${response.location} refused`);
+ return this.perform(target.url, target.method, target.body);
+ }
+}
diff --git a/__tests__/fixtures/displacement-ts/package.json b/__tests__/fixtures/displacement-ts/package.json
new file mode 100644
index 000000000..1998004a1
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "displacement-fixture",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module"
+}
diff --git a/__tests__/fixtures/displacement-ts/src/index.ts b/__tests__/fixtures/displacement-ts/src/index.ts
new file mode 100644
index 000000000..c6eadf001
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/index.ts
@@ -0,0 +1,10 @@
+import { ingestRecords } from './pipeline/ingest';
+import { normalizeRecords } from './pipeline/normalize';
+import { enrichRecords } from './pipeline/enrich';
+import { publishRecords } from './pipeline/publish';
+import type { PipelineOptions, PipelineRecord, RawRecord } from './pipeline/types';
+
+/** Run one batch through every pipeline stage, in order. */
+export function runPipeline(batch: RawRecord[], options: PipelineOptions): PipelineRecord[] {
+ return publishRecords(enrichRecords(normalizeRecords(ingestRecords(batch, options), options), options), options);
+}
diff --git a/__tests__/fixtures/displacement-ts/src/pipeline/enrich.ts b/__tests__/fixtures/displacement-ts/src/pipeline/enrich.ts
new file mode 100644
index 000000000..ae098aab9
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/pipeline/enrich.ts
@@ -0,0 +1,127 @@
+import { writeBatch } from './sink';
+import type { PipelineOptions, PipelineRecord } from './types';
+
+/** Enrich every record in a batch. */
+export function enrichRecords(records: PipelineRecord[], options: PipelineOptions): PipelineRecord[] {
+ const out: PipelineRecord[] = [];
+ for (const record of records) {
+ const tags = [...record.tags];
+ const warnings = [...record.warnings];
+ let value = record.value;
+
+ // 1. segment
+ {
+ const hit = tags.find((t) => t.startsWith('segment:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('segment: missing after enrich');
+ } else {
+ value = weightFacet(value, hit.length);
+ tags.push('segment.enri');
+ }
+ }
+
+ // 2. referrer
+ {
+ const hit = tags.find((t) => t.startsWith('referrer:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('referrer: missing after enrich');
+ } else {
+ value = blendFacet(value, hit.length);
+ tags.push('referrer.enri');
+ }
+ }
+
+ // 3. experiment
+ {
+ const hit = tags.find((t) => t.startsWith('experiment:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('experiment: missing after enrich');
+ } else {
+ value = weightFacet(value, hit.length);
+ tags.push('experiment.enri');
+ }
+ }
+
+ // 4. subscription
+ {
+ const hit = tags.find((t) => t.startsWith('subscription:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('subscription: missing after enrich');
+ } else {
+ value = blendFacet(value, hit.length);
+ tags.push('subscription.enri');
+ }
+ }
+
+ // 5. entitlement
+ {
+ const hit = tags.find((t) => t.startsWith('entitlement:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('entitlement: missing after enrich');
+ } else {
+ value = weightFacet(value, hit.length);
+ tags.push('entitlement.enri');
+ }
+ }
+
+ // 6. invoice
+ {
+ const hit = tags.find((t) => t.startsWith('invoice:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('invoice: missing after enrich');
+ } else {
+ value = blendFacet(value, hit.length);
+ tags.push('invoice.enri');
+ }
+ }
+
+ // 7. refund
+ {
+ const hit = tags.find((t) => t.startsWith('refund:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('refund: missing after enrich');
+ } else {
+ value = weightFacet(value, hit.length);
+ tags.push('refund.enri');
+ }
+ }
+
+ // 8. dispute
+ {
+ const hit = tags.find((t) => t.startsWith('dispute:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('dispute: missing after enrich');
+ } else {
+ value = blendFacet(value, hit.length);
+ tags.push('dispute.enri');
+ }
+ }
+
+ // 9. payout
+ {
+ const hit = tags.find((t) => t.startsWith('payout:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('payout: missing after enrich');
+ } else {
+ value = weightFacet(value, hit.length);
+ tags.push('payout.enri');
+ }
+ }
+
+ out.push({ ...record, value, tags: tags.slice(0, options.maxTags), warnings });
+ }
+ writeBatch('enrichRecords', out);
+ return out;
+}
+
+/** weightFacet — a small deterministic helper. */
+export function weightFacet(base: number, width: number): number {
+ const scaled = base + width * 3 - (width % 7);
+ return scaled < 0 ? 0 : scaled;
+}
+
+/** blendFacet — a small deterministic helper. */
+export function blendFacet(base: number, width: number): number {
+ const scaled = base + width * 3 - (width % 7);
+ return scaled < 0 ? 0 : scaled;
+}
diff --git a/__tests__/fixtures/displacement-ts/src/pipeline/ingest.ts b/__tests__/fixtures/displacement-ts/src/pipeline/ingest.ts
new file mode 100644
index 000000000..eccaee79d
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/pipeline/ingest.ts
@@ -0,0 +1,541 @@
+import { scaleFacet, clampFacet } from './normalize';
+import { writeBatch } from './sink';
+import type { PipelineOptions, PipelineRecord, RawRecord } from './types';
+
+/**
+ * Ingest one batch of raw records.
+ *
+ * Every facet is unpacked in its own block so an on-call engineer can read the
+ * ingest end-to-end in one place. The shape is deliberately flat: this single
+ * function is the whole stage, which is exactly the shape that makes it the
+ * biggest cluster member in the file.
+ */
+export function ingestRecords(batch: RawRecord[], options: PipelineOptions): PipelineRecord[] {
+ const out: PipelineRecord[] = [];
+ for (const record of batch) {
+ const tags: string[] = [];
+ const warnings: string[] = [];
+ let value = 0;
+
+ // 1. identity — normalise the identity facet of the record.
+ {
+ const raw = record.payload['identity'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('identity: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('identity:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('identity: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 2. geography — normalise the geography facet of the record.
+ {
+ const raw = record.payload['geography'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('geography: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('geography:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('geography: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 3. currency — normalise the currency facet of the record.
+ {
+ const raw = record.payload['currency'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('currency: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('currency:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('currency: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 4. timestamp — normalise the timestamp facet of the record.
+ {
+ const raw = record.payload['timestamp'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('timestamp: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('timestamp:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('timestamp: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 5. channel — normalise the channel facet of the record.
+ {
+ const raw = record.payload['channel'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('channel: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('channel:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('channel: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 6. campaign — normalise the campaign facet of the record.
+ {
+ const raw = record.payload['campaign'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('campaign: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('campaign:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('campaign: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 7. device — normalise the device facet of the record.
+ {
+ const raw = record.payload['device'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('device: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('device:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('device: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 8. locale — normalise the locale facet of the record.
+ {
+ const raw = record.payload['locale'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('locale: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('locale:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('locale: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 9. consent — normalise the consent facet of the record.
+ {
+ const raw = record.payload['consent'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('consent: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('consent:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('consent: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 10. segment — normalise the segment facet of the record.
+ {
+ const raw = record.payload['segment'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('segment: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('segment:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('segment: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 11. referrer — normalise the referrer facet of the record.
+ {
+ const raw = record.payload['referrer'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('referrer: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('referrer:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('referrer: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 12. experiment — normalise the experiment facet of the record.
+ {
+ const raw = record.payload['experiment'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('experiment: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('experiment:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('experiment: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 13. subscription — normalise the subscription facet of the record.
+ {
+ const raw = record.payload['subscription'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('subscription: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('subscription:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('subscription: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 14. entitlement — normalise the entitlement facet of the record.
+ {
+ const raw = record.payload['entitlement'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('entitlement: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('entitlement:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('entitlement: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 15. invoice — normalise the invoice facet of the record.
+ {
+ const raw = record.payload['invoice'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('invoice: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('invoice:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('invoice: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 16. refund — normalise the refund facet of the record.
+ {
+ const raw = record.payload['refund'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('refund: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('refund:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('refund: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 17. dispute — normalise the dispute facet of the record.
+ {
+ const raw = record.payload['dispute'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('dispute: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('dispute:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('dispute: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 18. payout — normalise the payout facet of the record.
+ {
+ const raw = record.payload['payout'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('payout: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('payout:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('payout: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 19. shipment — normalise the shipment facet of the record.
+ {
+ const raw = record.payload['shipment'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('shipment: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('shipment:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('shipment: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 20. inventory — normalise the inventory facet of the record.
+ {
+ const raw = record.payload['inventory'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('inventory: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('inventory:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('inventory: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 21. warehouse — normalise the warehouse facet of the record.
+ {
+ const raw = record.payload['warehouse'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('warehouse: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('warehouse:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('warehouse: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 22. carrier — normalise the carrier facet of the record.
+ {
+ const raw = record.payload['carrier'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('carrier: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('carrier:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('carrier: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 23. customs — normalise the customs facet of the record.
+ {
+ const raw = record.payload['customs'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('customs: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('customs:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('customs: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 24. tariff — normalise the tariff facet of the record.
+ {
+ const raw = record.payload['tariff'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('tariff: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('tariff:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('tariff: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 25. sensor — normalise the sensor facet of the record.
+ {
+ const raw = record.payload['sensor'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('sensor: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('sensor:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('sensor: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 26. firmware — normalise the firmware facet of the record.
+ {
+ const raw = record.payload['firmware'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('firmware: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('firmware:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('firmware: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 27. telemetry — normalise the telemetry facet of the record.
+ {
+ const raw = record.payload['telemetry'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('telemetry: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('telemetry:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('telemetry: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 28. battery — normalise the battery facet of the record.
+ {
+ const raw = record.payload['battery'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('battery: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('battery:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('battery: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 29. network — normalise the network facet of the record.
+ {
+ const raw = record.payload['network'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('network: empty, dropped');
+ } else {
+ const scaled = scaleFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('network:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('network: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ // 30. roaming — normalise the roaming facet of the record.
+ {
+ const raw = record.payload['roaming'];
+ const text = typeof raw === 'string' ? raw.trim() : raw === null ? '' : String(raw);
+ if (text.length === 0 && options.dropEmpty) {
+ warnings.push('roaming: empty, dropped');
+ } else {
+ const scaled = clampFacet(text.length, options.maxTags);
+ if (Number.isFinite(scaled) && scaled !== 0) {
+ tags.push('roaming:' + text.slice(0, 24));
+ value += scaled;
+ } else if (options.strict) {
+ warnings.push('roaming: not scalable — ' + text.slice(0, 16));
+ }
+ }
+ }
+
+ out.push({
+ id: record.id,
+ source: record.source,
+ kind: options.defaultKind,
+ value,
+ tags: tags.slice(0, options.maxTags),
+ warnings,
+ });
+ }
+ writeBatch('ingest', out);
+ return out;
+}
diff --git a/__tests__/fixtures/displacement-ts/src/pipeline/normalize.ts b/__tests__/fixtures/displacement-ts/src/pipeline/normalize.ts
new file mode 100644
index 000000000..5a50a15a3
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/pipeline/normalize.ts
@@ -0,0 +1,127 @@
+import { writeBatch } from './sink';
+import type { PipelineOptions, PipelineRecord } from './types';
+
+/** Normalize every record in a batch. */
+export function normalizeRecords(records: PipelineRecord[], options: PipelineOptions): PipelineRecord[] {
+ const out: PipelineRecord[] = [];
+ for (const record of records) {
+ const tags = [...record.tags];
+ const warnings = [...record.warnings];
+ let value = record.value;
+
+ // 1. identity
+ {
+ const hit = tags.find((t) => t.startsWith('identity:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('identity: missing after normalize');
+ } else {
+ value = scaleFacet(value, hit.length);
+ tags.push('identity.norm');
+ }
+ }
+
+ // 2. geography
+ {
+ const hit = tags.find((t) => t.startsWith('geography:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('geography: missing after normalize');
+ } else {
+ value = clampFacet(value, hit.length);
+ tags.push('geography.norm');
+ }
+ }
+
+ // 3. currency
+ {
+ const hit = tags.find((t) => t.startsWith('currency:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('currency: missing after normalize');
+ } else {
+ value = scaleFacet(value, hit.length);
+ tags.push('currency.norm');
+ }
+ }
+
+ // 4. timestamp
+ {
+ const hit = tags.find((t) => t.startsWith('timestamp:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('timestamp: missing after normalize');
+ } else {
+ value = clampFacet(value, hit.length);
+ tags.push('timestamp.norm');
+ }
+ }
+
+ // 5. channel
+ {
+ const hit = tags.find((t) => t.startsWith('channel:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('channel: missing after normalize');
+ } else {
+ value = scaleFacet(value, hit.length);
+ tags.push('channel.norm');
+ }
+ }
+
+ // 6. campaign
+ {
+ const hit = tags.find((t) => t.startsWith('campaign:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('campaign: missing after normalize');
+ } else {
+ value = clampFacet(value, hit.length);
+ tags.push('campaign.norm');
+ }
+ }
+
+ // 7. device
+ {
+ const hit = tags.find((t) => t.startsWith('device:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('device: missing after normalize');
+ } else {
+ value = scaleFacet(value, hit.length);
+ tags.push('device.norm');
+ }
+ }
+
+ // 8. locale
+ {
+ const hit = tags.find((t) => t.startsWith('locale:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('locale: missing after normalize');
+ } else {
+ value = clampFacet(value, hit.length);
+ tags.push('locale.norm');
+ }
+ }
+
+ // 9. consent
+ {
+ const hit = tags.find((t) => t.startsWith('consent:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('consent: missing after normalize');
+ } else {
+ value = scaleFacet(value, hit.length);
+ tags.push('consent.norm');
+ }
+ }
+
+ out.push({ ...record, value, tags: tags.slice(0, options.maxTags), warnings });
+ }
+ writeBatch('normalizeRecords', out);
+ return out;
+}
+
+/** scaleFacet — a small deterministic helper. */
+export function scaleFacet(base: number, width: number): number {
+ const scaled = base + width * 3 - (width % 7);
+ return scaled < 0 ? 0 : scaled;
+}
+
+/** clampFacet — a small deterministic helper. */
+export function clampFacet(base: number, width: number): number {
+ const scaled = base + width * 3 - (width % 7);
+ return scaled < 0 ? 0 : scaled;
+}
diff --git a/__tests__/fixtures/displacement-ts/src/pipeline/publish.ts b/__tests__/fixtures/displacement-ts/src/pipeline/publish.ts
new file mode 100644
index 000000000..405f11b97
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/pipeline/publish.ts
@@ -0,0 +1,127 @@
+import { writeBatch } from './sink';
+import type { PipelineOptions, PipelineRecord } from './types';
+
+/** Publish every record in a batch. */
+export function publishRecords(records: PipelineRecord[], options: PipelineOptions): PipelineRecord[] {
+ const out: PipelineRecord[] = [];
+ for (const record of records) {
+ const tags = [...record.tags];
+ const warnings = [...record.warnings];
+ let value = record.value;
+
+ // 1. shipment
+ {
+ const hit = tags.find((t) => t.startsWith('shipment:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('shipment: missing after publish');
+ } else {
+ value = rankFacet(value, hit.length);
+ tags.push('shipment.publ');
+ }
+ }
+
+ // 2. inventory
+ {
+ const hit = tags.find((t) => t.startsWith('inventory:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('inventory: missing after publish');
+ } else {
+ value = sealFacet(value, hit.length);
+ tags.push('inventory.publ');
+ }
+ }
+
+ // 3. warehouse
+ {
+ const hit = tags.find((t) => t.startsWith('warehouse:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('warehouse: missing after publish');
+ } else {
+ value = rankFacet(value, hit.length);
+ tags.push('warehouse.publ');
+ }
+ }
+
+ // 4. carrier
+ {
+ const hit = tags.find((t) => t.startsWith('carrier:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('carrier: missing after publish');
+ } else {
+ value = sealFacet(value, hit.length);
+ tags.push('carrier.publ');
+ }
+ }
+
+ // 5. customs
+ {
+ const hit = tags.find((t) => t.startsWith('customs:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('customs: missing after publish');
+ } else {
+ value = rankFacet(value, hit.length);
+ tags.push('customs.publ');
+ }
+ }
+
+ // 6. tariff
+ {
+ const hit = tags.find((t) => t.startsWith('tariff:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('tariff: missing after publish');
+ } else {
+ value = sealFacet(value, hit.length);
+ tags.push('tariff.publ');
+ }
+ }
+
+ // 7. sensor
+ {
+ const hit = tags.find((t) => t.startsWith('sensor:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('sensor: missing after publish');
+ } else {
+ value = rankFacet(value, hit.length);
+ tags.push('sensor.publ');
+ }
+ }
+
+ // 8. firmware
+ {
+ const hit = tags.find((t) => t.startsWith('firmware:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('firmware: missing after publish');
+ } else {
+ value = sealFacet(value, hit.length);
+ tags.push('firmware.publ');
+ }
+ }
+
+ // 9. telemetry
+ {
+ const hit = tags.find((t) => t.startsWith('telemetry:'));
+ if (hit === undefined) {
+ if (options.strict) warnings.push('telemetry: missing after publish');
+ } else {
+ value = rankFacet(value, hit.length);
+ tags.push('telemetry.publ');
+ }
+ }
+
+ out.push({ ...record, value, tags: tags.slice(0, options.maxTags), warnings });
+ }
+ writeBatch('publishRecords', out);
+ return out;
+}
+
+/** rankFacet — a small deterministic helper. */
+export function rankFacet(base: number, width: number): number {
+ const scaled = base + width * 3 - (width % 7);
+ return scaled < 0 ? 0 : scaled;
+}
+
+/** sealFacet — a small deterministic helper. */
+export function sealFacet(base: number, width: number): number {
+ const scaled = base + width * 3 - (width % 7);
+ return scaled < 0 ? 0 : scaled;
+}
diff --git a/__tests__/fixtures/displacement-ts/src/pipeline/sink.ts b/__tests__/fixtures/displacement-ts/src/pipeline/sink.ts
new file mode 100644
index 000000000..2f91720df
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/pipeline/sink.ts
@@ -0,0 +1,18 @@
+import type { PipelineRecord } from './types';
+
+const sink = new Map();
+
+/** Hand a finished batch to the downstream sink. */
+export function writeBatch(batchId: string, records: PipelineRecord[]): void {
+ sink.set(batchId, records);
+}
+
+/** Read a batch back out of the sink. */
+export function readBatch(batchId: string): PipelineRecord[] {
+ return sink.get(batchId) ?? [];
+}
+
+/** Forget a batch. */
+export function dropBatch(batchId: string): void {
+ sink.delete(batchId);
+}
diff --git a/__tests__/fixtures/displacement-ts/src/pipeline/types.ts b/__tests__/fixtures/displacement-ts/src/pipeline/types.ts
new file mode 100644
index 000000000..32bf2835e
--- /dev/null
+++ b/__tests__/fixtures/displacement-ts/src/pipeline/types.ts
@@ -0,0 +1,25 @@
+/** One raw record as it arrives from the upstream feed. */
+export interface RawRecord {
+ id: string;
+ source: string;
+ payload: Record;
+ receivedAt: number;
+}
+
+/** A record after the pipeline has cleaned and annotated it. */
+export interface PipelineRecord {
+ id: string;
+ source: string;
+ kind: string;
+ value: number;
+ tags: string[];
+ warnings: string[];
+}
+
+/** Per-run knobs shared by every pipeline stage. */
+export interface PipelineOptions {
+ strict: boolean;
+ dropEmpty: boolean;
+ defaultKind: string;
+ maxTags: number;
+}
diff --git a/__tests__/fixtures/factory-closure-ts/package.json b/__tests__/fixtures/factory-closure-ts/package.json
new file mode 100644
index 000000000..f597b571b
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "factory-closure-ts",
+ "version": "0.0.0",
+ "private": true
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/index.ts b/__tests__/fixtures/factory-closure-ts/src/index.ts
new file mode 100644
index 000000000..2a873de32
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/index.ts
@@ -0,0 +1,23 @@
+import { createDashboardStore } from './stores/dashboard-store';
+import { createAlertsStore } from './stores/alerts-store';
+import { mountPanel } from './ui/panel';
+import { parseFilterText } from './services/filter-parser';
+import { refreshMetricCache } from './services/metric-service';
+import type { StoreDeps } from './stores/types';
+
+/** Wire a dashboard: build both stores, mount the panel, boot it. */
+export async function startDashboard(deps: StoreDeps, baseUrl: string, dashboardId: string) {
+ const store = createDashboardStore(deps, baseUrl);
+ const alerts = createAlertsStore(deps, baseUrl);
+ const panel = mountPanel(store, dashboardId);
+ await panel.boot();
+ await alerts.refreshAlerts(dashboardId);
+ return { store, alerts, panel };
+}
+
+/** Apply the filter bar's text to the dashboard store. */
+export function searchDashboard(store: ReturnType, text: string) {
+ return store.applyFilter(parseFilterText(text));
+}
+
+export { refreshMetricCache };
diff --git a/__tests__/fixtures/factory-closure-ts/src/lib/http.ts b/__tests__/fixtures/factory-closure-ts/src/lib/http.ts
new file mode 100644
index 000000000..84d254ee2
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/lib/http.ts
@@ -0,0 +1,25 @@
+/** Minimal fetch helpers the dashboard store depends on. */
+
+export interface RequestOptions {
+ retries: number;
+ timeoutMs: number;
+}
+
+export const defaultRequestOptions: RequestOptions = { retries: 2, timeoutMs: 5_000 };
+
+/** Build a query string from a plain record, skipping empty values. */
+export function toQueryString(params: Record): string {
+ const parts: string[] = [];
+ for (const [key, value] of Object.entries(params)) {
+ if (value === undefined || value === '') continue;
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
+ }
+ return parts.length > 0 ? `?${parts.join('&')}` : '';
+}
+
+/** Join a base path and a resource path without doubling the separator. */
+export function joinPath(base: string, resource: string): string {
+ if (base.endsWith('/') && resource.startsWith('/')) return base + resource.slice(1);
+ if (!base.endsWith('/') && !resource.startsWith('/')) return `${base}/${resource}`;
+ return base + resource;
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/lib/metrics.ts b/__tests__/fixtures/factory-closure-ts/src/lib/metrics.ts
new file mode 100644
index 000000000..be39eccaa
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/lib/metrics.ts
@@ -0,0 +1,37 @@
+import type { MetricSample } from '../stores/types';
+
+/** Statistics helpers shared by the store and the panel. */
+
+export function meanOf(samples: readonly MetricSample[]): number {
+ if (samples.length === 0) return 0;
+ let total = 0;
+ for (const sample of samples) total += sample.value;
+ return total / samples.length;
+}
+
+export function medianOf(samples: readonly MetricSample[]): number {
+ if (samples.length === 0) return 0;
+ const values = samples.map((s) => s.value).sort((a, b) => a - b);
+ const mid = Math.floor(values.length / 2);
+ return values.length % 2 === 0 ? (values[mid - 1]! + values[mid]!) / 2 : values[mid]!;
+}
+
+export function rateOfChange(samples: readonly MetricSample[]): number {
+ if (samples.length < 2) return 0;
+ const ordered = samples.slice().sort((a, b) => a.at - b.at);
+ const first = ordered[0]!;
+ const last = ordered[ordered.length - 1]!;
+ const elapsed = last.at - first.at;
+ return elapsed > 0 ? (last.value - first.value) / elapsed : 0;
+}
+
+export function bucketByHour(samples: readonly MetricSample[]): Map {
+ const buckets = new Map();
+ for (const sample of samples) {
+ const hour = Math.floor(sample.at / 3_600_000);
+ const bucket = buckets.get(hour);
+ if (bucket) bucket.push(sample);
+ else buckets.set(hour, [sample]);
+ }
+ return buckets;
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/services/filter-parser.ts b/__tests__/fixtures/factory-closure-ts/src/services/filter-parser.ts
new file mode 100644
index 000000000..293dbb018
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/services/filter-parser.ts
@@ -0,0 +1,62 @@
+import type { FilterSpec } from '../stores/types';
+
+/** Parse the dashboard's filter bar text into filter specs. */
+
+const OPERATORS: Record = {
+ ':': 'eq',
+ '~': 'contains',
+ '>': 'gt',
+ '<': 'lt',
+};
+
+/** `title~sales kind:chart column>3` → three specs. */
+export function parseFilterText(text: string): FilterSpec[] {
+ const specs: FilterSpec[] = [];
+ for (const token of tokenize(text)) {
+ const spec = parseToken(token);
+ if (spec) specs.push(spec);
+ }
+ return specs;
+}
+
+/** Split on whitespace, honouring double-quoted values. */
+export function tokenize(text: string): string[] {
+ const tokens: string[] = [];
+ let current = '';
+ let quoted = false;
+ for (const ch of text) {
+ if (ch === '"') { quoted = !quoted; continue; }
+ if (!quoted && /\s/.test(ch)) {
+ if (current.length > 0) { tokens.push(current); current = ''; }
+ continue;
+ }
+ current += ch;
+ }
+ if (current.length > 0) tokens.push(current);
+ return tokens;
+}
+
+/** One `fieldvalue` token, or null when it does not parse. */
+export function parseToken(token: string): FilterSpec | null {
+ for (const [symbol, op] of Object.entries(OPERATORS)) {
+ const at = token.indexOf(symbol);
+ if (at <= 0) continue;
+ const field = token.slice(0, at).trim();
+ const value = token.slice(at + symbol.length).trim();
+ if (field.length === 0 || value.length === 0) return null;
+ return { field, op, value };
+ }
+ return null;
+}
+
+/** Render specs back to filter-bar text — the round trip the URL uses. */
+export function formatFilterText(specs: readonly FilterSpec[]): string {
+ const symbolFor = (op: FilterSpec['op']): string =>
+ Object.entries(OPERATORS).find(([, candidate]) => candidate === op)?.[0] ?? ':';
+ return specs
+ .map((spec) => {
+ const value = /\s/.test(spec.value) ? `"${spec.value}"` : spec.value;
+ return `${spec.field}${symbolFor(spec.op)}${value}`;
+ })
+ .join(' ');
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/services/metric-service.ts b/__tests__/fixtures/factory-closure-ts/src/services/metric-service.ts
new file mode 100644
index 000000000..59ddd1d51
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/services/metric-service.ts
@@ -0,0 +1,101 @@
+import type { FilterSpec, MetricSample, Widget } from '../stores/types';
+import { bucketByHour, meanOf, rateOfChange } from '../lib/metrics';
+
+/**
+ * Stateless metric helpers — the server-shaped half of the same domain. These
+ * are ordinary top-level functions, not closures, so they are the control the
+ * factory-closure file is measured against.
+ */
+
+const STALE_AFTER_MS = 15 * 60 * 1000;
+
+/** Refresh a cached metric map in place, returning the widgets that changed. */
+export function refreshMetricCache(
+ cache: Map,
+ incoming: readonly MetricSample[],
+ now: number,
+): string[] {
+ const touched = new Set();
+ for (const sample of incoming) {
+ if (typeof sample.value !== 'number' || Number.isNaN(sample.value)) continue;
+ const bucket = cache.get(sample.widgetId);
+ if (bucket) bucket.push(sample);
+ else cache.set(sample.widgetId, [sample]);
+ touched.add(sample.widgetId);
+ }
+ for (const [widgetId, bucket] of cache) {
+ const fresh = bucket.filter((s) => now - s.at <= STALE_AFTER_MS);
+ if (fresh.length !== bucket.length) {
+ cache.set(widgetId, fresh);
+ touched.add(widgetId);
+ }
+ }
+ return [...touched].sort();
+}
+
+/** Apply a filter spec set to raw samples rather than to widgets. */
+export function filterMetrics(
+ samples: readonly MetricSample[],
+ specs: readonly FilterSpec[],
+): MetricSample[] {
+ if (specs.length === 0) return samples.slice();
+ return samples.filter((sample) => specs.every((spec) => {
+ const field = spec.field === 'unit'
+ ? sample.unit
+ : spec.field === 'widget'
+ ? sample.widgetId
+ : String(sample.value);
+ switch (spec.op) {
+ case 'eq': return field === spec.value;
+ case 'contains': return field.includes(spec.value);
+ case 'gt': return Number(field) > Number(spec.value);
+ case 'lt': return Number(field) < Number(spec.value);
+ default: return false;
+ }
+ }));
+}
+
+/** Per-widget rollup used by the server-rendered summary card. */
+export function rollupByWidget(
+ samples: readonly MetricSample[],
+ widgets: readonly Widget[],
+): Array<{ widgetId: string; title: string; mean: number; slope: number; hours: number }> {
+ const titles = new Map(widgets.map((w) => [w.id, w.title]));
+ const grouped = new Map();
+ for (const sample of samples) {
+ const bucket = grouped.get(sample.widgetId);
+ if (bucket) bucket.push(sample);
+ else grouped.set(sample.widgetId, [sample]);
+ }
+
+ const out: Array<{ widgetId: string; title: string; mean: number; slope: number; hours: number }> = [];
+ for (const [widgetId, bucket] of grouped) {
+ out.push({
+ widgetId,
+ title: titles.get(widgetId) ?? '(unknown)',
+ mean: meanOf(bucket),
+ slope: rateOfChange(bucket),
+ hours: bucketByHour(bucket).size,
+ });
+ }
+ out.sort((a, b) => b.mean - a.mean);
+ return out;
+}
+
+/** Which widgets have not reported inside the staleness window. */
+export function staleWidgets(
+ samples: readonly MetricSample[],
+ widgets: readonly Widget[],
+ now: number,
+): string[] {
+ const newest = new Map();
+ for (const sample of samples) {
+ const seen = newest.get(sample.widgetId) ?? 0;
+ if (sample.at > seen) newest.set(sample.widgetId, sample.at);
+ }
+ return widgets
+ .filter((w) => !w.hidden)
+ .filter((w) => now - (newest.get(w.id) ?? 0) > STALE_AFTER_MS)
+ .map((w) => w.id)
+ .sort();
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/stores/alerts-store.ts b/__tests__/fixtures/factory-closure-ts/src/stores/alerts-store.ts
new file mode 100644
index 000000000..fc079281e
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/stores/alerts-store.ts
@@ -0,0 +1,140 @@
+import type { FilterSpec, StoreDeps } from './types';
+import { joinPath, toQueryString } from '../lib/http';
+
+const ALERT_ENDPOINT = '/api/dashboard/alerts';
+
+export interface Alert {
+ id: string;
+ widgetId: string;
+ severity: 'info' | 'warn' | 'critical';
+ message: string;
+ raisedAt: number;
+ acknowledgedAt: number | null;
+}
+
+/**
+ * The alerts store — the dashboard's second factory closure. Same shape as the
+ * metric store: every operation is a closure over private state.
+ */
+export function createAlertsStore(deps: StoreDeps, baseUrl: string) {
+ let alerts: Alert[] = [];
+ let filters: FilterSpec[] = [];
+ let mutedWidgets = new Set();
+ let lastRefreshedAt = 0;
+
+ /** Pull the current alert set and merge acknowledgements the user made locally. */
+ async function refreshAlerts(dashboardId: string): Promise {
+ const url = joinPath(baseUrl, ALERT_ENDPOINT) + toQueryString({ dashboard: dashboardId });
+ let payload: unknown;
+ try {
+ payload = await deps.fetchJson(url);
+ } catch (error) {
+ deps.log(`refreshAlerts failed: ${error instanceof Error ? error.message : String(error)}`);
+ return alerts;
+ }
+ if (!Array.isArray(payload)) {
+ deps.log('refreshAlerts got a non-array payload');
+ return alerts;
+ }
+
+ const acknowledged = new Map(
+ alerts.filter((a) => a.acknowledgedAt !== null).map((a) => [a.id, a.acknowledgedAt]),
+ );
+ const merged: Alert[] = [];
+ for (const raw of payload as Alert[]) {
+ if (typeof raw.id !== 'string' || raw.id.length === 0) continue;
+ merged.push({
+ ...raw,
+ acknowledgedAt: acknowledged.get(raw.id) ?? raw.acknowledgedAt ?? null,
+ });
+ }
+ merged.sort((a, b) => b.raisedAt - a.raisedAt);
+ alerts = merged;
+ lastRefreshedAt = deps.now();
+ return alerts;
+ }
+
+ /** Filter the alert list the same way the metric store filters widgets. */
+ function applyAlertFilter(specs: readonly FilterSpec[]): Alert[] {
+ filters = specs.slice();
+ if (filters.length === 0) return alerts;
+
+ const fieldOf = (alert: Alert, field: string): string => {
+ switch (field) {
+ case 'severity': return alert.severity;
+ case 'widget': return alert.widgetId;
+ case 'message': return alert.message;
+ default: return '';
+ }
+ };
+
+ return alerts.filter((alert) => filters.every((spec) => {
+ const value = fieldOf(alert, spec.field);
+ switch (spec.op) {
+ case 'eq': return value.toLowerCase() === spec.value.toLowerCase();
+ case 'contains': return value.toLowerCase().includes(spec.value.toLowerCase());
+ case 'gt': return value > spec.value;
+ case 'lt': return value < spec.value;
+ default: return false;
+ }
+ }));
+ }
+
+ /** Mark an alert acknowledged locally; the next refresh preserves it. */
+ function acknowledge(alertId: string): boolean {
+ const target = alerts.find((a) => a.id === alertId);
+ if (!target || target.acknowledgedAt !== null) return false;
+ target.acknowledgedAt = deps.now();
+ deps.log(`acknowledged ${alertId}`);
+ return true;
+ }
+
+ /** Silence a widget's alerts without dropping them from the buffer. */
+ function muteWidget(widgetId: string): void {
+ mutedWidgets.add(widgetId);
+ deps.log(`muted ${widgetId} (${mutedWidgets.size} muted)`);
+ }
+
+ function unmuteWidget(widgetId: string): boolean {
+ return mutedWidgets.delete(widgetId);
+ }
+
+ /** The alerts the dashboard should actually show right now. */
+ function visibleAlerts(): Alert[] {
+ return applyAlertFilter(filters)
+ .filter((a) => !mutedWidgets.has(a.widgetId))
+ .filter((a) => a.acknowledgedAt === null);
+ }
+
+ /** Counts per severity, for the badge on the alerts tab. */
+ function countBySeverity(): Record {
+ const counts: Record = { info: 0, warn: 0, critical: 0 };
+ for (const alert of visibleAlerts()) counts[alert.severity] += 1;
+ return counts;
+ }
+
+ function reset(): void {
+ alerts = [];
+ filters = [];
+ mutedWidgets = new Set();
+ lastRefreshedAt = 0;
+ }
+
+ function snapshot() {
+ return { alerts: visibleAlerts(), counts: countBySeverity(), lastRefreshedAt };
+ }
+
+ return {
+ refreshAlerts,
+ applyAlertFilter,
+ acknowledge,
+ muteWidget,
+ unmuteWidget,
+ visibleAlerts,
+ countBySeverity,
+ reset,
+ snapshot,
+ };
+}
+
+export type AlertsStore = ReturnType;
diff --git a/__tests__/fixtures/factory-closure-ts/src/stores/dashboard-store.ts b/__tests__/fixtures/factory-closure-ts/src/stores/dashboard-store.ts
new file mode 100644
index 000000000..36be5630d
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/stores/dashboard-store.ts
@@ -0,0 +1,384 @@
+import type { FilterSpec, MetricSample, StoreDeps, Widget } from './types';
+import { defaultRequestOptions, joinPath, toQueryString } from '../lib/http';
+
+const WIDGET_ENDPOINT = '/api/dashboard/widgets';
+const METRIC_ENDPOINT = '/api/dashboard/metrics';
+const SAMPLE_RETENTION_MS = 6 * 60 * 60 * 1000;
+const MAX_SAMPLES_PER_WIDGET = 720;
+const COLUMN_COUNT = 12;
+
+/**
+ * The dashboard store: one factory closure holding every operation the
+ * dashboard performs. Callers get an object of closures; nothing inside is
+ * exported on its own.
+ */
+export function createDashboardStore(deps: StoreDeps, baseUrl: string) {
+ let widgets: Widget[] = [];
+ let samples: MetricSample[] = [];
+ let activeFilters: FilterSpec[] = [];
+ let lastSyncedAt = 0;
+ let loading = false;
+ let lastError: string | null = null;
+ const listeners = new Set<(snapshot: ReturnType) => void>();
+
+ function snapshot() {
+ return {
+ widgets: widgets.filter((w) => !w.hidden),
+ sampleCount: samples.length,
+ filters: activeFilters.slice(),
+ lastSyncedAt,
+ loading,
+ lastError,
+ };
+ }
+
+ /**
+ * Fetch the widget set for the current user and merge it into local state,
+ * preserving any layout the user has moved since the last sync.
+ */
+ async function loadWidgets(dashboardId: string, includeHidden = false): Promise {
+ loading = true;
+ lastError = null;
+ const url = joinPath(baseUrl, WIDGET_ENDPOINT) + toQueryString({
+ dashboard: dashboardId,
+ hidden: includeHidden ? '1' : undefined,
+ });
+
+ let attempt = 0;
+ let payload: unknown = null;
+ while (attempt <= defaultRequestOptions.retries) {
+ try {
+ payload = await deps.fetchJson(url);
+ break;
+ } catch (error) {
+ attempt += 1;
+ if (attempt > defaultRequestOptions.retries) {
+ lastError = error instanceof Error ? error.message : String(error);
+ loading = false;
+ deps.log(`loadWidgets failed after ${attempt} attempts: ${lastError}`);
+ notify();
+ return widgets;
+ }
+ deps.log(`loadWidgets retry ${attempt} for ${dashboardId}`);
+ }
+ }
+
+ const incoming = Array.isArray(payload) ? (payload as Widget[]) : [];
+ const byId = new Map(widgets.map((w) => [w.id, w]));
+ const merged: Widget[] = [];
+ for (const next of incoming) {
+ const existing = byId.get(next.id);
+ if (!existing) {
+ merged.push({ ...next });
+ continue;
+ }
+ // Server owns identity and content; the client owns placement.
+ merged.push({
+ ...next,
+ column: existing.column,
+ row: existing.row,
+ span: existing.span,
+ hidden: existing.hidden,
+ });
+ byId.delete(next.id);
+ }
+ for (const orphan of byId.values()) {
+ deps.log(`widget ${orphan.id} no longer exists on the server`);
+ }
+
+ widgets = merged;
+ lastSyncedAt = deps.now();
+ loading = false;
+ notify();
+ return widgets;
+ }
+
+ /**
+ * Pull fresh metric samples for every visible widget, append them to the
+ * rolling buffer, and drop anything past the retention window.
+ */
+ async function refreshMetrics(windowMs = SAMPLE_RETENTION_MS): Promise {
+ if (widgets.length === 0) {
+ deps.log('refreshMetrics called with no widgets loaded');
+ return samples;
+ }
+ loading = true;
+ const visible = widgets.filter((w) => !w.hidden);
+ const collected: MetricSample[] = [];
+
+ for (const widget of visible) {
+ const url = joinPath(baseUrl, METRIC_ENDPOINT) + toQueryString({
+ widget: widget.id,
+ since: deps.now() - windowMs,
+ });
+ let payload: unknown;
+ try {
+ payload = await deps.fetchJson(url);
+ } catch (error) {
+ lastError = error instanceof Error ? error.message : String(error);
+ deps.log(`refreshMetrics failed for ${widget.id}: ${lastError}`);
+ continue;
+ }
+ if (!Array.isArray(payload)) {
+ deps.log(`refreshMetrics got a non-array payload for ${widget.id}`);
+ continue;
+ }
+ for (const raw of payload as MetricSample[]) {
+ if (typeof raw.value !== 'number' || Number.isNaN(raw.value)) continue;
+ if (typeof raw.at !== 'number' || raw.at <= 0) continue;
+ collected.push({
+ widgetId: widget.id,
+ at: raw.at,
+ value: raw.value,
+ unit: raw.unit ?? 'count',
+ });
+ }
+ }
+
+ const cutoff = deps.now() - windowMs;
+ const kept = samples.filter((s) => s.at >= cutoff);
+ samples = kept.concat(collected);
+ pruneSamples(MAX_SAMPLES_PER_WIDGET);
+ lastSyncedAt = deps.now();
+ loading = false;
+ notify();
+ return samples;
+ }
+
+ /**
+ * Replace the active filter set and recompute which widgets stay visible.
+ * A widget survives when every filter matches one of its fields.
+ */
+ function applyFilter(specs: readonly FilterSpec[]): Widget[] {
+ activeFilters = specs.slice();
+ if (activeFilters.length === 0) {
+ widgets = widgets.map((w) => ({ ...w, hidden: false }));
+ notify();
+ return widgets;
+ }
+
+ const matches = (widget: Widget, spec: FilterSpec): boolean => {
+ const field = spec.field === 'title'
+ ? widget.title
+ : spec.field === 'kind'
+ ? widget.kind
+ : spec.field === 'column'
+ ? String(widget.column)
+ : '';
+ switch (spec.op) {
+ case 'eq':
+ return field.toLowerCase() === spec.value.toLowerCase();
+ case 'contains':
+ return field.toLowerCase().includes(spec.value.toLowerCase());
+ case 'gt':
+ return Number(field) > Number(spec.value);
+ case 'lt':
+ return Number(field) < Number(spec.value);
+ default:
+ return false;
+ }
+ };
+
+ let hiddenCount = 0;
+ widgets = widgets.map((widget) => {
+ const visible = activeFilters.every((spec) => matches(widget, spec));
+ if (!visible) hiddenCount += 1;
+ return { ...widget, hidden: !visible };
+ });
+ deps.log(`applyFilter hid ${hiddenCount} of ${widgets.length} widgets`);
+ notify();
+ return widgets;
+ }
+
+ /**
+ * Render the current sample buffer as CSV, one row per sample, ordered by
+ * widget then timestamp so a diff between two exports stays readable.
+ */
+ function exportCsv(separator = ','): string {
+ const header = ['widget', 'title', 'at', 'value', 'unit'].join(separator);
+ if (samples.length === 0) return header;
+
+ const titles = new Map(widgets.map((w) => [w.id, w.title]));
+ const ordered = samples.slice().sort((a, b) => {
+ if (a.widgetId !== b.widgetId) return a.widgetId < b.widgetId ? -1 : 1;
+ return a.at - b.at;
+ });
+
+ const escape = (value: string): string => {
+ if (!value.includes(separator) && !value.includes('"') && !value.includes('\n')) return value;
+ return `"${value.replace(/"/g, '""')}"`;
+ };
+
+ const rows = ordered.map((sample) => [
+ escape(sample.widgetId),
+ escape(titles.get(sample.widgetId) ?? '(unknown)'),
+ String(sample.at),
+ String(sample.value),
+ escape(sample.unit),
+ ].join(separator));
+
+ return [header, ...rows].join('\n');
+ }
+
+ /**
+ * Pack widgets back into a dense grid after a move or a hide, so the layout
+ * never leaves a hole a user has to scroll past.
+ */
+ function reconcileLayout(columnCount = COLUMN_COUNT): Widget[] {
+ const visible = widgets.filter((w) => !w.hidden);
+ const hidden = widgets.filter((w) => w.hidden);
+
+ const ordered = visible.slice().sort((a, b) => {
+ if (a.row !== b.row) return a.row - b.row;
+ return a.column - b.column;
+ });
+
+ const rowWidth = new Map();
+ const placed: Widget[] = [];
+ for (const widget of ordered) {
+ const span = Math.max(1, Math.min(widget.span, columnCount));
+ let row = 0;
+ let column = 0;
+ for (;;) {
+ const used = rowWidth.get(row) ?? 0;
+ if (used + span <= columnCount) {
+ column = used;
+ rowWidth.set(row, used + span);
+ break;
+ }
+ row += 1;
+ }
+ placed.push({ ...widget, row, column, span });
+ }
+
+ let trailing = placed.length > 0 ? Math.max(...placed.map((w) => w.row)) + 1 : 0;
+ for (const widget of hidden) {
+ placed.push({ ...widget, row: trailing, column: 0 });
+ trailing += 1;
+ }
+
+ widgets = placed;
+ notify();
+ return widgets;
+ }
+
+ /**
+ * Cap the rolling buffer per widget, keeping the newest samples. Called after
+ * every refresh so memory stays bounded on a long-lived dashboard.
+ */
+ function pruneSamples(perWidget = MAX_SAMPLES_PER_WIDGET): number {
+ if (samples.length === 0) return 0;
+ const grouped = new Map();
+ for (const sample of samples) {
+ const bucket = grouped.get(sample.widgetId);
+ if (bucket) bucket.push(sample);
+ else grouped.set(sample.widgetId, [sample]);
+ }
+
+ let dropped = 0;
+ const kept: MetricSample[] = [];
+ for (const [, bucket] of grouped) {
+ bucket.sort((a, b) => a.at - b.at);
+ if (bucket.length > perWidget) {
+ dropped += bucket.length - perWidget;
+ kept.push(...bucket.slice(bucket.length - perWidget));
+ } else {
+ kept.push(...bucket);
+ }
+ }
+
+ kept.sort((a, b) => a.at - b.at);
+ samples = kept;
+ if (dropped > 0) deps.log(`pruneSamples dropped ${dropped} samples`);
+ return dropped;
+ }
+
+ /**
+ * Reduce the buffer to one aggregate per widget — the numbers the summary
+ * strip at the top of the dashboard renders.
+ */
+ function summarize(): Array<{ widgetId: string; title: string; min: number; max: number; mean: number; count: number }> {
+ const titles = new Map(widgets.map((w) => [w.id, w.title]));
+ const grouped = new Map();
+ for (const sample of samples) {
+ const bucket = grouped.get(sample.widgetId);
+ if (bucket) bucket.push(sample);
+ else grouped.set(sample.widgetId, [sample]);
+ }
+
+ const out: Array<{ widgetId: string; title: string; min: number; max: number; mean: number; count: number }> = [];
+ for (const [widgetId, bucket] of grouped) {
+ let min = Number.POSITIVE_INFINITY;
+ let max = Number.NEGATIVE_INFINITY;
+ let total = 0;
+ for (const sample of bucket) {
+ if (sample.value < min) min = sample.value;
+ if (sample.value > max) max = sample.value;
+ total += sample.value;
+ }
+ out.push({
+ widgetId,
+ title: titles.get(widgetId) ?? '(unknown)',
+ min: bucket.length > 0 ? min : 0,
+ max: bucket.length > 0 ? max : 0,
+ mean: bucket.length > 0 ? total / bucket.length : 0,
+ count: bucket.length,
+ });
+ }
+
+ out.sort((a, b) => b.count - a.count || (a.title < b.title ? -1 : 1));
+ return out;
+ }
+
+ /** Register a listener and get an unsubscribe back. */
+ function subscribe(listener: (snapshot: ReturnType) => void): () => void {
+ listeners.add(listener);
+ listener(snapshot());
+ return () => {
+ listeners.delete(listener);
+ };
+ }
+
+ function notify(): void {
+ const current = snapshot();
+ for (const listener of listeners) {
+ try {
+ listener(current);
+ } catch (error) {
+ deps.log(`dashboard listener threw: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ }
+
+ /** Drop every sample and widget — used when the user switches dashboards. */
+ function reset(): void {
+ widgets = [];
+ samples = [];
+ activeFilters = [];
+ lastSyncedAt = 0;
+ lastError = null;
+ loading = false;
+ notify();
+ }
+
+ return {
+ loadWidgets,
+ refreshMetrics,
+ applyFilter,
+ exportCsv,
+ reconcileLayout,
+ pruneSamples,
+ summarize,
+ subscribe,
+ reset,
+ snapshot,
+ };
+}
+
+export type DashboardStore = ReturnType;
+
+/** One-line description of a store's state, for the debug panel. */
+export function describeStore(store: DashboardStore): string {
+ const state = store.snapshot();
+ return `${state.widgets.length} widgets · ${state.sampleCount} samples · synced ${state.lastSyncedAt}`;
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/stores/session-store.ts b/__tests__/fixtures/factory-closure-ts/src/stores/session-store.ts
new file mode 100644
index 000000000..031d2ab4b
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/stores/session-store.ts
@@ -0,0 +1,148 @@
+import type { StoreDeps } from './types';
+import { joinPath, toQueryString } from '../lib/http';
+
+/**
+ * The session store — a factory closure and NOTHING else at file scope. No
+ * companion type alias, no tail helper, no exported constants: every other
+ * symbol in this file lives inside the closure. That shape matters, because it
+ * is the one where the enclosing range is the only top-importance symbol the
+ * file can offer a query.
+ */
+export function createSessionStore(deps: StoreDeps, baseUrl: string) {
+ const SESSION_ENDPOINT = '/api/session';
+ const REFRESH_SKEW_MS = 30_000;
+
+ let token: string | null = null;
+ let expiresAt = 0;
+ let profile: { id: string; email: string; roles: string[] } | null = null;
+ let refreshing: Promise | null = null;
+ const auditLog: Array<{ at: number; event: string }> = [];
+
+ function record(event: string): void {
+ auditLog.push({ at: deps.now(), event });
+ if (auditLog.length > 200) auditLog.splice(0, auditLog.length - 200);
+ }
+
+ /** Exchange credentials for a session token and cache the profile. */
+ async function signIn(email: string, password: string): Promise {
+ const url = joinPath(baseUrl, SESSION_ENDPOINT) + toQueryString({ email });
+ let payload: unknown;
+ try {
+ payload = await deps.fetchJson(url);
+ } catch (error) {
+ record(`signIn failed: ${error instanceof Error ? error.message : String(error)}`);
+ return false;
+ }
+ if (typeof payload !== 'object' || payload === null) {
+ record('signIn got a non-object payload');
+ return false;
+ }
+ const body = payload as { token?: string; expiresAt?: number; profile?: typeof profile };
+ if (typeof body.token !== 'string' || body.token.length === 0) {
+ record('signIn payload carried no token');
+ return false;
+ }
+ void password;
+ token = body.token;
+ expiresAt = typeof body.expiresAt === 'number' ? body.expiresAt : deps.now() + 3_600_000;
+ profile = body.profile ?? null;
+ record(`signIn ok for ${email}`);
+ return true;
+ }
+
+ /** Drop every trace of the session, locally and on the server. */
+ async function signOut(): Promise {
+ if (token === null) return;
+ const url = joinPath(baseUrl, SESSION_ENDPOINT) + toQueryString({ action: 'revoke' });
+ try {
+ await deps.fetchJson(url);
+ } catch (error) {
+ record(`signOut revoke failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ token = null;
+ expiresAt = 0;
+ profile = null;
+ refreshing = null;
+ record('signOut complete');
+ }
+
+ /**
+ * Renew the token before it expires. Concurrent callers share one in-flight
+ * request so a burst of requests cannot start a refresh storm.
+ */
+ async function refreshToken(): Promise {
+ if (token === null) return null;
+ if (refreshing !== null) return refreshing;
+
+ refreshing = (async () => {
+ const url = joinPath(baseUrl, SESSION_ENDPOINT) + toQueryString({ action: 'refresh' });
+ try {
+ const payload = await deps.fetchJson(url);
+ const body = payload as { token?: string; expiresAt?: number };
+ if (typeof body?.token === 'string' && body.token.length > 0) {
+ token = body.token;
+ expiresAt = typeof body.expiresAt === 'number' ? body.expiresAt : deps.now() + 3_600_000;
+ record('refreshToken renewed the session');
+ return token;
+ }
+ record('refreshToken payload carried no token');
+ return null;
+ } catch (error) {
+ record(`refreshToken failed: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ } finally {
+ refreshing = null;
+ }
+ })();
+
+ return refreshing;
+ }
+
+ /** The token to send with a request, renewing it first when it is close to expiry. */
+ async function authorize(): Promise {
+ if (token === null) return null;
+ if (deps.now() + REFRESH_SKEW_MS < expiresAt) return token;
+ return refreshToken();
+ }
+
+ /** Does the signed-in user hold every one of these roles? */
+ function hasRoles(...required: string[]): boolean {
+ if (profile === null) return false;
+ const held = new Set(profile.roles);
+ for (const role of required) {
+ if (!held.has(role)) return false;
+ }
+ return true;
+ }
+
+ /** Seconds left on the session, floored at zero. */
+ function secondsRemaining(): number {
+ if (token === null) return 0;
+ return Math.max(0, Math.floor((expiresAt - deps.now()) / 1000));
+ }
+
+ /** The last N audit entries, newest first — what the account page renders. */
+ function recentActivity(limit = 20): Array<{ at: number; event: string }> {
+ return auditLog.slice(-limit).reverse();
+ }
+
+ function snapshot() {
+ return {
+ signedIn: token !== null,
+ email: profile?.email ?? null,
+ roles: profile?.roles ?? [],
+ secondsRemaining: secondsRemaining(),
+ };
+ }
+
+ return {
+ signIn,
+ signOut,
+ refreshToken,
+ authorize,
+ hasRoles,
+ secondsRemaining,
+ recentActivity,
+ snapshot,
+ };
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/stores/types.ts b/__tests__/fixtures/factory-closure-ts/src/stores/types.ts
new file mode 100644
index 000000000..0659cf618
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/stores/types.ts
@@ -0,0 +1,28 @@
+export interface Widget {
+ id: string;
+ kind: 'chart' | 'table' | 'stat';
+ title: string;
+ column: number;
+ row: number;
+ span: number;
+ hidden: boolean;
+}
+
+export interface MetricSample {
+ widgetId: string;
+ at: number;
+ value: number;
+ unit: string;
+}
+
+export interface FilterSpec {
+ field: string;
+ op: 'eq' | 'gt' | 'lt' | 'contains';
+ value: string;
+}
+
+export interface StoreDeps {
+ fetchJson: (url: string) => Promise;
+ now: () => number;
+ log: (message: string) => void;
+}
diff --git a/__tests__/fixtures/factory-closure-ts/src/ui/panel.ts b/__tests__/fixtures/factory-closure-ts/src/ui/panel.ts
new file mode 100644
index 000000000..28204e81a
--- /dev/null
+++ b/__tests__/fixtures/factory-closure-ts/src/ui/panel.ts
@@ -0,0 +1,43 @@
+import type { DashboardStore } from '../stores/dashboard-store';
+import type { FilterSpec } from '../stores/types';
+import { medianOf } from '../lib/metrics';
+
+/** The dashboard panel — the only consumer of the store's closures. */
+export function mountPanel(store: DashboardStore, dashboardId: string) {
+ let disposed = false;
+
+ const unsubscribe = store.subscribe((state) => {
+ if (disposed) return;
+ render(state.widgets.length, state.sampleCount, state.loading);
+ });
+
+ async function boot(): Promise {
+ await store.loadWidgets(dashboardId);
+ await store.refreshMetrics();
+ store.reconcileLayout();
+ }
+
+ function search(text: string): void {
+ const specs: FilterSpec[] = text.trim().length === 0
+ ? []
+ : [{ field: 'title', op: 'contains', value: text.trim() }];
+ store.applyFilter(specs);
+ }
+
+ function download(): string {
+ return store.exportCsv();
+ }
+
+ function render(widgetCount: number, sampleCount: number, loading: boolean): void {
+ void widgetCount;
+ void sampleCount;
+ void loading;
+ }
+
+ function dispose(): void {
+ disposed = true;
+ unsubscribe();
+ }
+
+ return { boot, search, download, dispose, median: medianOf };
+}
diff --git a/__tests__/fixtures/kernel-parity/torture.c b/__tests__/fixtures/kernel-parity/torture.c
index 407263748..57e04a912 100644
--- a/__tests__/fixtures/kernel-parity/torture.c
+++ b/__tests__/fixtures/kernel-parity/torture.c
@@ -152,3 +152,19 @@ static void ratelimited_warn(void) {
static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);
use_ptr(&ratelimit, 0);
}
+
+/* named union definition, forward declaration, and anonymous typedef union —
+ the definition is a node, the forward decl is not (#UNION) */
+union packet_hdr {
+ unsigned int raw;
+ struct { unsigned char ver, flags; } parts;
+};
+
+union opaque_hdr;
+
+typedef union {
+ unsigned int u;
+ float f;
+} word_t;
+
+static unsigned int hdr_raw(union packet_hdr *h) { return h->raw; }
diff --git a/__tests__/fixtures/kernel-parity/torture.rs b/__tests__/fixtures/kernel-parity/torture.rs
index da2a168ff..8e594705a 100644
--- a/__tests__/fixtures/kernel-parity/torture.rs
+++ b/__tests__/fixtures/kernel-parity/torture.rs
@@ -214,3 +214,10 @@ routes![top_level_h];
const INIT_CONST: usize = compute_len();
static INIT_LAZY: Lazy = Lazy::new(|| build_cfg());
static INIT_ALIAS: fn() = free_fn;
+
+pub union Reg {
+ pub raw: u32,
+ pub halves: [u16; 2],
+}
+
+impl Base for Reg {}
diff --git a/__tests__/fixtures/oversize-member-ts/package.json b/__tests__/fixtures/oversize-member-ts/package.json
new file mode 100644
index 000000000..6c8f80efa
--- /dev/null
+++ b/__tests__/fixtures/oversize-member-ts/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "oversize-member-fixture",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module"
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/index.ts b/__tests__/fixtures/oversize-member-ts/src/index.ts
new file mode 100644
index 000000000..156b0bd0b
--- /dev/null
+++ b/__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');
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/report/format.ts b/__tests__/fixtures/oversize-member-ts/src/report/format.ts
new file mode 100644
index 000000000..e1af69815
--- /dev/null
+++ b/__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');
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/report/monthly.ts b/__tests__/fixtures/oversize-member-ts/src/report/monthly.ts
new file mode 100644
index 000000000..4e90fa4b2
--- /dev/null
+++ b/__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();
+
+ // 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`;
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/report/quarterly.ts b/__tests__/fixtures/oversize-member-ts/src/report/quarterly.ts
new file mode 100644
index 000000000..d8fc5f0d7
--- /dev/null
+++ b/__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();
+
+ // 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})`;
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/report/store.ts b/__tests__/fixtures/oversize-member-ts/src/report/store.ts
new file mode 100644
index 000000000..4d65851a3
--- /dev/null
+++ b/__tests__/fixtures/oversize-member-ts/src/report/store.ts
@@ -0,0 +1,18 @@
+import type { ReportRow } from './types';
+
+const saved = new Map();
+
+/** 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);
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/report/types.ts b/__tests__/fixtures/oversize-member-ts/src/report/types.ts
new file mode 100644
index 000000000..4bad635f0
--- /dev/null
+++ b/__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;
+}
diff --git a/__tests__/fixtures/oversize-member-ts/src/report/weekly.ts b/__tests__/fixtures/oversize-member-ts/src/report/weekly.ts
new file mode 100644
index 000000000..9a81d3aa0
--- /dev/null
+++ b/__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();
+
+ // 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})`;
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/package.json b/__tests__/fixtures/starved-cluster-ts/package.json
new file mode 100644
index 000000000..2ade7039f
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "starved-cluster-fixture",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module"
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/app/client.ts b/__tests__/fixtures/starved-cluster-ts/src/app/client.ts
new file mode 100644
index 000000000..53b24a1f7
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/app/client.ts
@@ -0,0 +1,24 @@
+import { RequestChain, describeChain } from '../pipeline/chain';
+import type { PipelineRequest, PipelineResponse } from '../pipeline/types';
+import { openSocket } from '../transport/socket';
+
+/**
+ * The entry point a caller reaches for. Everything the chain does happens
+ * underneath this call, which is why a flow question names it.
+ */
+export async function sendRequest(request: PipelineRequest): Promise {
+ const socket = openSocket(request.host, request.port);
+ const chain = new RequestChain(request, socket);
+ trace(describeChain(chain));
+ return chain.proceed(request);
+}
+
+export function trace(line: string): void {
+ if (process.env.PIPELINE_TRACE) process.stderr.write(`${line}\n`);
+}
+
+export async function sendAll(requests: PipelineRequest[]): Promise {
+ const out: PipelineResponse[] = [];
+ for (const request of requests) out.push(await sendRequest(request));
+ return out;
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/app/config.ts b/__tests__/fixtures/starved-cluster-ts/src/app/config.ts
new file mode 100644
index 000000000..bc584b560
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/app/config.ts
@@ -0,0 +1,14 @@
+export interface ClientConfig {
+ host: string;
+ port: number;
+ retries: number;
+ userAgent: string;
+}
+
+export function defaultConfig(): ClientConfig {
+ return { host: 'localhost', port: 8080, retries: 3, userAgent: 'pipeline/1.0' };
+}
+
+export function withHost(config: ClientConfig, host: string): ClientConfig {
+ return { ...config, host };
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/index.ts b/__tests__/fixtures/starved-cluster-ts/src/index.ts
new file mode 100644
index 000000000..c617c4925
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/index.ts
@@ -0,0 +1,4 @@
+export { sendRequest, sendAll } from './app/client';
+export { RequestChain, describeChain } from './pipeline/chain';
+export { openSocket } from './transport/socket';
+export { defaultConfig } from './app/config';
diff --git a/__tests__/fixtures/starved-cluster-ts/src/pipeline/chain.ts b/__tests__/fixtures/starved-cluster-ts/src/pipeline/chain.ts
new file mode 100644
index 000000000..37ef261d6
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/pipeline/chain.ts
@@ -0,0 +1,318 @@
+import type { PipelineRequest, PipelineResponse, Interceptor, Socket } from './types';
+import { encodeFrame, decodeFrame } from './framing';
+import { defaultInterceptors } from './interceptors';
+
+/**
+ * A one-line summary of a chain, used only by the tracing hook in the caller.
+ * It is TRIVIAL — it answers nothing about how a request travels — but it sits
+ * next to the entry point in the call graph, so its cluster carries the file's
+ * highest per-symbol importance.
+ */
+export function describeChain(chain: RequestChain): string {
+ return `chain(${chain.index}/${chain.size}) -> ${chain.hostLabel}`;
+}
+
+// ---------------------------------------------------------------------------
+//
+// Everything below is the part a "how does a request reach the socket" question
+// is actually asking about. It is separated from the helper above by more than
+// the cluster gap threshold, so it forms its own cluster — a large one, whose
+// symbols are reached transitively rather than named.
+//
+// ---------------------------------------------------------------------------
+
+export class RequestChain {
+ readonly index: number;
+ readonly size: number;
+ readonly hostLabel: string;
+ private readonly interceptors: Interceptor[];
+ private readonly socket: Socket;
+ private readonly request: PipelineRequest;
+ private connectTimeoutMs = 10_000;
+ private readTimeoutMs = 10_000;
+ private writeTimeoutMs = 10_000;
+ private calls = 0;
+
+ constructor(request: PipelineRequest, socket: Socket, index = 0, interceptors?: Interceptor[]) {
+ this.request = request;
+ this.socket = socket;
+ this.index = index;
+ this.interceptors = interceptors ?? defaultInterceptors();
+ this.size = this.interceptors.length;
+ this.hostLabel = `${request.host}:${request.port}`;
+ }
+
+ /**
+ * Run the request through the remaining interceptors and, once they are
+ * exhausted, hand it to the transport. This is the method the flow question
+ * is about: every hop between the caller and the socket passes through here.
+ */
+ async proceed(request: PipelineRequest): Promise {
+ if (this.index >= this.size) {
+ return this.writeAndRead(request);
+ }
+ this.calls += 1;
+ if (this.calls > 1) {
+ throw new Error(`chain link ${this.index} called ${this.calls} times`);
+ }
+ const next = this.advance(request);
+ const interceptor = this.interceptors[this.index]!;
+ const response = await interceptor.intercept(next);
+ if (!response) {
+ throw new Error(`interceptor ${interceptor.name} returned no response`);
+ }
+ if (this.index + 1 < this.size && next.callCount() === 0) {
+ throw new Error(`interceptor ${interceptor.name} must call proceed()`);
+ }
+ return response;
+ }
+
+ /**
+ * The next link in the chain: the same chain with the cursor moved on and the
+ * timeouts carried over. Cloning here is what keeps each interceptor from
+ * mutating the chain the one before it is still holding.
+ */
+ advance(request: PipelineRequest): RequestChain {
+ const next = new RequestChain(request, this.socket, this.index + 1, this.interceptors);
+ next.connectTimeoutMs = this.connectTimeoutMs;
+ next.readTimeoutMs = this.readTimeoutMs;
+ next.writeTimeoutMs = this.writeTimeoutMs;
+ return next;
+ }
+
+ callCount(): number {
+ return this.calls;
+ }
+
+ /**
+ * The end of the chain: frame the request, put the bytes on the socket, wait
+ * for the reply and decode it. Past this point there is no more pipeline —
+ * this is the transport hop the question is looking for.
+ */
+ private async writeAndRead(request: PipelineRequest): Promise {
+ const frame = encodeFrame(request);
+ await this.socket.connect(this.connectTimeoutMs);
+ await this.socket.write(frame, this.writeTimeoutMs);
+ const raw = await this.socket.read(this.readTimeoutMs);
+ const decoded = decodeFrame(raw);
+ return {
+ status: decoded.status,
+ headers: decoded.headers,
+ body: decoded.body,
+ request,
+ };
+ }
+
+ withConnectTimeout(ms: number): RequestChain {
+ const next = this.advance(this.request);
+ next.connectTimeoutMs = checkDuration('connectTimeout', ms);
+ return next;
+ }
+
+ withReadTimeout(ms: number): RequestChain {
+ const next = this.advance(this.request);
+ next.readTimeoutMs = checkDuration('readTimeout', ms);
+ return next;
+ }
+
+ withWriteTimeout(ms: number): RequestChain {
+ const next = this.advance(this.request);
+ next.writeTimeoutMs = checkDuration('writeTimeout', ms);
+ return next;
+ }
+
+ connectTimeout(): number {
+ return this.connectTimeoutMs;
+ }
+
+ readTimeout(): number {
+ return this.readTimeoutMs;
+ }
+
+ writeTimeout(): number {
+ return this.writeTimeoutMs;
+ }
+
+ /**
+ * Retry policy for the transport hop. Sits inside the same cluster as the
+ * proceed/advance pair, so it is part of what a shrink has to choose between.
+ */
+ async retryWrite(request: PipelineRequest, attempts: number): Promise {
+ let lastError: unknown;
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
+ try {
+ return await this.writeAndRead(request);
+ } catch (error) {
+ lastError = error;
+ await backoff(attempt);
+ }
+ }
+ throw lastError;
+ }
+
+ /** Whether the chain may still be resumed after a transport failure. */
+ canRetry(error: unknown): boolean {
+ if (this.index >= this.size) return false;
+ if (!(error instanceof Error)) return false;
+ return error.message.includes('timeout') || error.message.includes('reset');
+ }
+
+ /** The interceptor names, in the order the request will visit them. */
+ route(): string[] {
+ return this.interceptors.slice(this.index).map((i) => i.name);
+ }
+
+ /** A copy of the chain rewound to the first interceptor. */
+ rewind(): RequestChain {
+ return new RequestChain(this.request, this.socket, 0, this.interceptors);
+ }
+
+ /** Drop one interceptor by name and return the shortened chain. */
+ without(name: string): RequestChain {
+ const kept = this.interceptors.filter((i) => i.name !== name);
+ return new RequestChain(this.request, this.socket, this.index, kept);
+ }
+
+ /** Append an interceptor to the end of the chain. */
+ with(interceptor: Interceptor): RequestChain {
+ return new RequestChain(
+ this.request,
+ this.socket,
+ this.index,
+ [...this.interceptors, interceptor],
+ );
+ }
+
+ /** Close the transport this chain was built around. */
+ async close(): Promise {
+ await this.socket.close();
+ }
+
+ /** Headers the transport hop will actually put on the wire. */
+ effectiveHeaders(): Record {
+ const headers: Record = { ...this.request.headers };
+ headers['host'] = this.hostLabel;
+ headers['x-chain-index'] = String(this.index);
+ headers['x-chain-size'] = String(this.size);
+ if (this.request.body) headers['content-length'] = String(this.request.body.length);
+ return headers;
+ }
+
+ /** The request as the next link will see it, with the chain's headers merged. */
+ prepared(): PipelineRequest {
+ return { ...this.request, headers: this.effectiveHeaders() };
+ }
+
+ /**
+ * Send the prepared request through the rest of the chain. The convenience
+ * wrapper most callers use instead of building the request themselves.
+ */
+ async send(): Promise {
+ return this.proceed(this.prepared());
+ }
+
+ /** Whether the chain has any interceptor left before the transport hop. */
+ hasNext(): boolean {
+ return this.index < this.size;
+ }
+
+ /** The interceptor the next `proceed` will run, if there is one. */
+ peek(): Interceptor | undefined {
+ return this.interceptors[this.index];
+ }
+
+ /** Total configured wait for one attempt, across all three timeouts. */
+ totalTimeout(): number {
+ return this.connectTimeoutMs + this.readTimeoutMs + this.writeTimeoutMs;
+ }
+
+ /** Apply one timeout budget to all three phases at once. */
+ withTimeout(ms: number): RequestChain {
+ const next = this.advance(this.request);
+ const checked = checkDuration('timeout', ms);
+ next.connectTimeoutMs = checked;
+ next.readTimeoutMs = checked;
+ next.writeTimeoutMs = checked;
+ return next;
+ }
+
+ /**
+ * Run the chain and translate a transport failure into a response, so a
+ * caller that only cares about the status code never sees an exception.
+ */
+ async sendOrStatus(status: number): Promise {
+ try {
+ return await this.send();
+ } catch {
+ return {
+ status,
+ headers: this.effectiveHeaders(),
+ body: new Uint8Array(),
+ request: this.request,
+ };
+ }
+ }
+
+ /** A short description of where in the chain this link sits. */
+ position(): string {
+ return `${this.index + 1} of ${this.size + 1}`;
+ }
+
+ /** The chain rebuilt around a different transport. */
+ onSocket(socket: Socket): RequestChain {
+ return new RequestChain(this.request, socket, this.index, this.interceptors);
+ }
+
+ /**
+ * Replay the request through the chain from the start, reusing the transport.
+ * Used when an interceptor decides the response it got is not usable and the
+ * whole pipeline has to run again against the same connection.
+ */
+ async replay(): Promise {
+ const fresh = this.rewind();
+ try {
+ return await fresh.send();
+ } finally {
+ if (!fresh.hasNext()) await fresh.close();
+ }
+ }
+
+ /**
+ * Validate the chain before it runs: every interceptor named once, timeouts
+ * inside their bounds, and a transport still open at the end of it.
+ */
+ validate(): string[] {
+ const problems: string[] = [];
+ const seen = new Set();
+ for (const interceptor of this.interceptors) {
+ if (seen.has(interceptor.name)) problems.push(`duplicate interceptor ${interceptor.name}`);
+ seen.add(interceptor.name);
+ }
+ if (this.connectTimeoutMs <= 0) problems.push('connect timeout must be positive');
+ if (this.readTimeoutMs <= 0) problems.push('read timeout must be positive');
+ if (this.writeTimeoutMs <= 0) problems.push('write timeout must be positive');
+ if (this.index > this.size) problems.push('chain cursor is past the end');
+ return problems;
+ }
+
+ /**
+ * The transport hop on its own, with the chain's timeouts but none of its
+ * interceptors — the escape hatch a caller uses to bypass the pipeline.
+ */
+ async direct(request: PipelineRequest): Promise {
+ const problems = this.validate();
+ if (problems.length > 0) throw new Error(problems.join('; '));
+ return this.writeAndRead(request);
+ }
+}
+
+function checkDuration(name: string, ms: number): number {
+ if (!Number.isFinite(ms) || ms < 0) throw new Error(`${name} must be a positive duration`);
+ if (ms > 24 * 60 * 60 * 1000) throw new Error(`${name} is longer than a day`);
+ return Math.round(ms);
+}
+
+async function backoff(attempt: number): Promise {
+ const ms = Math.min(1000, 25 * 2 ** attempt);
+ await new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/pipeline/framing.ts b/__tests__/fixtures/starved-cluster-ts/src/pipeline/framing.ts
new file mode 100644
index 000000000..fbcb229ae
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/pipeline/framing.ts
@@ -0,0 +1,26 @@
+import type { PipelineRequest } from './types';
+
+export function encodeFrame(request: PipelineRequest): Uint8Array {
+ const head = `${request.method} ${request.path}\n`;
+ const headers = Object.entries(request.headers).map(([k, v]) => `${k}: ${v}`).join('\n');
+ const text = `${head}${headers}\n\n`;
+ const body = request.body ?? new Uint8Array();
+ const out = new Uint8Array(text.length + body.length);
+ out.set(new TextEncoder().encode(text), 0);
+ out.set(body, text.length);
+ return out;
+}
+
+export function decodeFrame(raw: Uint8Array): { status: number; headers: Record; body: Uint8Array } {
+ const text = new TextDecoder().decode(raw);
+ const split = text.indexOf('\n\n');
+ const head = split < 0 ? text : text.slice(0, split);
+ const lines = head.split('\n');
+ const status = Number.parseInt(lines[0]?.split(' ')[1] ?? '0', 10);
+ const headers: Record = {};
+ for (const line of lines.slice(1)) {
+ const at = line.indexOf(': ');
+ if (at > 0) headers[line.slice(0, at)] = line.slice(at + 2);
+ }
+ return { status, headers, body: raw.slice(split < 0 ? raw.length : split + 2) };
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/pipeline/interceptors.ts b/__tests__/fixtures/starved-cluster-ts/src/pipeline/interceptors.ts
new file mode 100644
index 000000000..5cb3062de
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/pipeline/interceptors.ts
@@ -0,0 +1,21 @@
+import type { Interceptor } from './types';
+
+export function defaultInterceptors(): Interceptor[] {
+ return [retryInterceptor(), headerInterceptor(), logInterceptor()];
+}
+
+export function retryInterceptor(): Interceptor {
+ return { name: 'retry', intercept: (chain) => chain.proceed(currentRequest()) };
+}
+
+export function headerInterceptor(): Interceptor {
+ return { name: 'headers', intercept: (chain) => chain.proceed(currentRequest()) };
+}
+
+export function logInterceptor(): Interceptor {
+ return { name: 'log', intercept: (chain) => chain.proceed(currentRequest()) };
+}
+
+function currentRequest() {
+ return { host: 'localhost', port: 80, method: 'GET', path: '/', headers: {} };
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/pipeline/types.ts b/__tests__/fixtures/starved-cluster-ts/src/pipeline/types.ts
new file mode 100644
index 000000000..cd575245c
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/pipeline/types.ts
@@ -0,0 +1,27 @@
+export interface PipelineRequest {
+ host: string;
+ port: number;
+ method: string;
+ path: string;
+ headers: Record;
+ body?: Uint8Array;
+}
+
+export interface PipelineResponse {
+ status: number;
+ headers: Record;
+ body: Uint8Array;
+ request: PipelineRequest;
+}
+
+export interface Interceptor {
+ name: string;
+ intercept(chain: { proceed(request: PipelineRequest): Promise }): Promise;
+}
+
+export interface Socket {
+ connect(timeoutMs: number): Promise;
+ write(frame: Uint8Array, timeoutMs: number): Promise;
+ read(timeoutMs: number): Promise;
+ close(): Promise;
+}
diff --git a/__tests__/fixtures/starved-cluster-ts/src/transport/socket.ts b/__tests__/fixtures/starved-cluster-ts/src/transport/socket.ts
new file mode 100644
index 000000000..26b2da7b9
--- /dev/null
+++ b/__tests__/fixtures/starved-cluster-ts/src/transport/socket.ts
@@ -0,0 +1,30 @@
+import type { Socket } from '../pipeline/types';
+
+/** Open a transport socket for a host/port pair. */
+export function openSocket(host: string, port: number): Socket {
+ let open = false;
+ const inbox: Uint8Array[] = [];
+ return {
+ async connect(timeoutMs: number) {
+ if (open) return;
+ await settle(timeoutMs);
+ open = true;
+ },
+ async write(frame: Uint8Array, timeoutMs: number) {
+ if (!open) throw new Error(`socket to ${host}:${port} is not connected`);
+ await settle(timeoutMs);
+ inbox.push(frame);
+ },
+ async read(timeoutMs: number) {
+ await settle(timeoutMs);
+ return inbox.shift() ?? new Uint8Array();
+ },
+ async close() {
+ open = false;
+ },
+ };
+}
+
+async function settle(timeoutMs: number): Promise {
+ if (timeoutMs <= 0) throw new Error('timed out');
+}
diff --git a/__tests__/fixtures/tail-render-ts/README.md b/__tests__/fixtures/tail-render-ts/README.md
new file mode 100644
index 000000000..b0596d83b
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/README.md
@@ -0,0 +1,39 @@
+# tail-render-ts — CG-38
+
+An agent-named symbol sitting in the TAIL of a large file must render.
+
+This mirrors the geometry of the reported file (a 1,414-line Svelte chat store)
+closely enough that the same two defects reproduce, and it is that geometry — not
+any individual line — that the fixture exists to hold:
+
+| | line | why it matters |
+|---|---|---|
+| `QueuedMessage` (interface) | 70 | the DECOY. Same stem as the query token, near the top, cheap to render — it is what the broken build returned *instead of* the functions. |
+| `createSessionStore` (function) | 104–1417 | the ENVELOPE. Spans ~92% of the file, and `function` is deliberately **not** in `ENVELOPE_KINDS` (CG-27), so every symbol inside merges into ONE cluster that must then be shrunk and trimmed. |
+| `handleStreamMessage` | ~554 | a 290-line god-method in the middle, so the head of the file has plenty to spend the budget on. |
+| `queueMessage` | 1088 | TARGET. Past line 1,000. |
+| `removeQueuedMessage` | 1096 | TARGET. |
+| `flushQueuedMessages` | 1102 | TARGET. Past line 1,000. |
+
+Two more pieces are load-bearing:
+
+- **`queueMessage` never calls `flushQueuedMessages`** (both push to / drain the same
+ array instead). That absence is what produced no call chain, no synthesized hop and
+ no dispatch boundary — and so made `buildFlowFromNamedSymbols` throw the
+ named-symbol identity away along with the narrative it had nothing to print.
+- **`types/worker-configuration.d.ts`** — 2,500 lines of generated Wrangler ambient
+ types, carrying the `Generated by wrangler. DO NOT EDIT.` banner so the ranker flags
+ and penalises it. It is what makes the fixture able to test the issue's
+ index-dependence lead: a penalty on this file moves `maxGraph`, which moves the 6%
+ relevance gate, which moves every other file's allowance — and must still not cost
+ the top-ranked file the definitions the agent named.
+
+`src/lib/session-store.ts` is machine-generated to hit those line numbers with real,
+extractable TypeScript. If you need to change it, change the geometry (the target
+line numbers, the closure span, the decoy's position) rather than editing individual
+lines — the fixture-shape assertions in
+`__tests__/explore-named-symbol-render.test.ts` will tell you if it has rotted.
+
+Gate: `__tests__/explore-named-symbol-render.test.ts`.
+Probe: `node scripts/agent-eval/probe-named-symbol.mjs`.
+Numbers: `docs/benchmarks/explore-tail-render-cg38.md`.
diff --git a/__tests__/fixtures/tail-render-ts/package.json b/__tests__/fixtures/tail-render-ts/package.json
new file mode 100644
index 000000000..300f04a33
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "tail-render-fixture",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module"
+}
diff --git a/__tests__/fixtures/tail-render-ts/src/components/ChatComposer.ts b/__tests__/fixtures/tail-render-ts/src/components/ChatComposer.ts
new file mode 100644
index 000000000..40f2629b4
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/src/components/ChatComposer.ts
@@ -0,0 +1,27 @@
+import { createSessionStore } from '../lib/session-store';
+
+/** The composer owns the textarea and decides send-vs-queue. */
+export function createComposer(endpoint: string) {
+ const store = createSessionStore({
+ getProjectId: () => 'demo',
+ getEndpoint: () => endpoint,
+ onError: () => {},
+ });
+ let draft = '';
+
+ function setDraft(next: string) {
+ draft = next;
+ }
+
+ function submit(streaming: boolean) {
+ if (streaming) store.queueMessage(draft);
+ else store.sendMessage(draft, [], []);
+ draft = '';
+ }
+
+ function onTurnEnd() {
+ store.flushQueuedMessages();
+ }
+
+ return { setDraft, submit, onTurnEnd, store };
+}
diff --git a/__tests__/fixtures/tail-render-ts/src/lib/message-builder.ts b/__tests__/fixtures/tail-render-ts/src/lib/message-builder.ts
new file mode 100644
index 000000000..974d6416d
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/src/lib/message-builder.ts
@@ -0,0 +1,32 @@
+import type { AttachedFile, SelectedElementRef } from './session-store';
+
+export interface BuiltMessage {
+ id: string;
+ text: string;
+ attachments: number;
+}
+
+/** Render the selected canvas elements as a fenced block above the prose. */
+export function renderElementBlock(elements: SelectedElementRef[]): string {
+ if (elements.length === 0) return '';
+ const lines = elements.map((e) => `- ${e.kind}: ${e.label} (${e.id})`);
+ return ['```elements', ...lines, '```'].join('\n');
+}
+
+export function formatStylesBlock(files: AttachedFile[]): string {
+ return files.map((f) => `${f.path} (${f.mime}, ${f.bytes}b)`).join('\n');
+}
+
+export function buildMessage(
+ content: string,
+ files: AttachedFile[],
+ elements: SelectedElementRef[],
+): BuiltMessage {
+ const block = renderElementBlock(elements);
+ const styles = formatStylesBlock(files);
+ return {
+ id: `m-${content.length}-${files.length}`,
+ text: [block, styles, content].filter(Boolean).join('\n\n'),
+ attachments: files.length,
+ };
+}
diff --git a/__tests__/fixtures/tail-render-ts/src/lib/session-store.ts b/__tests__/fixtures/tail-render-ts/src/lib/session-store.ts
new file mode 100644
index 000000000..c50e5b337
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/src/lib/session-store.ts
@@ -0,0 +1,1417 @@
+import type { Socket } from './socket';
+import { createDedicatedSocket } from './socket';
+import { buildMessage, type BuiltMessage } from './message-builder';
+
+/** One attachment carried alongside a chat message. */
+export interface AttachedFile {
+ path: string;
+ mime: string;
+ bytes: number;
+}
+
+/** A element the user selected in the canvas and attached to a message. */
+export interface SelectedElementRef {
+ id: string;
+ kind: string;
+ label: string;
+}
+
+export interface ChatMessage {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ files: AttachedFile[];
+ elements: SelectedElementRef[];
+ streaming?: boolean;
+}
+
+export interface BackgroundJobSummary {
+ id: string;
+ label: string;
+ done: boolean;
+}
+
+export interface StreamChunk {
+ type: string;
+ text?: string;
+ jobs?: BackgroundJobSummary[];
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+export interface QueuedMessage {
+ id: string;
+ content: string;
+ files: AttachedFile[];
+ elements: SelectedElementRef[];
+}
+
+interface SessionDeps {
+ getProjectId: () => string;
+ getEndpoint: () => string;
+ onError: (message: string) => void;
+}
+
+type HistoryEntry = { at: number; messages: ChatMessage[] };
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+// ── Factory ────────────────────────────────────────
+
+export function createSessionStore(deps: SessionDeps) {
+ let messages: ChatMessage[] = [];
+ let queuedMessages: QueuedMessage[] = [];
+ let sessionId: string | null = null;
+ let isStreaming = false;
+ let chatSocket: Socket | null = null;
+ let jobs: BackgroundJobSummary[] = [];
+ let lastError: string | null = null;
+
+ function storageKey() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in storageKey';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'storageKey-2');
+ if (sessionId === null) lastError = 'storageKey: no session';
+ // storageKey bookkeeping step 4
+ const step5 = messages.length + 5;
+ }
+
+ function saveHistory() {
+ const step0 = messages.length + 0;
+ void storageKey();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'saveHistory-2');
+ if (sessionId === null) lastError = 'saveHistory: no session';
+ // saveHistory bookkeeping step 4
+ void storageKey();
+ if (step5 > 1000) lastError = 'overflow in saveHistory';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'saveHistory-7');
+ if (sessionId === null) lastError = 'saveHistory: no session';
+ void storageKey();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in saveHistory';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'saveHistory-12');
+ void storageKey();
+ // saveHistory bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in saveHistory';
+ void storageKey();
+ }
+
+ function loadHistory() {
+ const step0 = messages.length + 0;
+ void storageKey();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'loadHistory-2');
+ if (sessionId === null) lastError = 'loadHistory: no session';
+ // loadHistory bookkeeping step 4
+ void storageKey();
+ if (step5 > 1000) lastError = 'overflow in loadHistory';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'loadHistory-7');
+ if (sessionId === null) lastError = 'loadHistory: no session';
+ void storageKey();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in loadHistory';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'loadHistory-12');
+ void storageKey();
+ // loadHistory bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in loadHistory';
+ void storageKey();
+ if (sessionId === null) lastError = 'loadHistory: no session';
+ // loadHistory bookkeeping step 19
+ const step20 = messages.length + 20;
+ void storageKey();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'loadHistory-22');
+ if (sessionId === null) lastError = 'loadHistory: no session';
+ // loadHistory bookkeeping step 24
+ void storageKey();
+ }
+
+ function clearHistory() {
+ const step0 = messages.length + 0;
+ void storageKey();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'clearHistory-2');
+ if (sessionId === null) lastError = 'clearHistory: no session';
+ // clearHistory bookkeeping step 4
+ void storageKey();
+ if (step5 > 1000) lastError = 'overflow in clearHistory';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'clearHistory-7');
+ if (sessionId === null) lastError = 'clearHistory: no session';
+ void storageKey();
+ }
+
+ function checkConfiguration() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in checkConfiguration';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkConfiguration-2');
+ if (sessionId === null) lastError = 'checkConfiguration: no session';
+ // checkConfiguration bookkeeping step 4
+ const step5 = messages.length + 5;
+ if (step5 > 1000) lastError = 'overflow in checkConfiguration';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkConfiguration-7');
+ if (sessionId === null) lastError = 'checkConfiguration: no session';
+ // checkConfiguration bookkeeping step 9
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in checkConfiguration';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkConfiguration-12');
+ if (sessionId === null) lastError = 'checkConfiguration: no session';
+ // checkConfiguration bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in checkConfiguration';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkConfiguration-17');
+ if (sessionId === null) lastError = 'checkConfiguration: no session';
+ // checkConfiguration bookkeeping step 19
+ const step20 = messages.length + 20;
+ if (step20 > 1000) lastError = 'overflow in checkConfiguration';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkConfiguration-22');
+ if (sessionId === null) lastError = 'checkConfiguration: no session';
+ }
+
+ function checkInitialization() {
+ const step0 = messages.length + 0;
+ void loadHistory();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkInitialization-2');
+ if (sessionId === null) lastError = 'checkInitialization: no session';
+ // checkInitialization bookkeeping step 4
+ void loadHistory();
+ if (step5 > 1000) lastError = 'overflow in checkInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkInitialization-7');
+ if (sessionId === null) lastError = 'checkInitialization: no session';
+ void loadHistory();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in checkInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkInitialization-12');
+ void loadHistory();
+ // checkInitialization bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in checkInitialization';
+ void loadHistory();
+ if (sessionId === null) lastError = 'checkInitialization: no session';
+ // checkInitialization bookkeeping step 19
+ const step20 = messages.length + 20;
+ void loadHistory();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkInitialization-22');
+ if (sessionId === null) lastError = 'checkInitialization: no session';
+ // checkInitialization bookkeeping step 24
+ void loadHistory();
+ if (step25 > 1000) lastError = 'overflow in checkInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkInitialization-27');
+ if (sessionId === null) lastError = 'checkInitialization: no session';
+ void loadHistory();
+ const step30 = messages.length + 30;
+ if (step30 > 1000) lastError = 'overflow in checkInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'checkInitialization-32');
+ void loadHistory();
+ // checkInitialization bookkeeping step 34
+ const step35 = messages.length + 35;
+ if (step35 > 1000) lastError = 'overflow in checkInitialization';
+ void loadHistory();
+ if (sessionId === null) lastError = 'checkInitialization: no session';
+ // checkInitialization bookkeeping step 39
+ }
+
+ function startInitialization() {
+ const step0 = messages.length + 0;
+ void checkInitialization();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-2');
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ // startInitialization bookkeeping step 4
+ void checkInitialization();
+ if (step5 > 1000) lastError = 'overflow in startInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-7');
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ void checkInitialization();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in startInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-12');
+ void checkInitialization();
+ // startInitialization bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in startInitialization';
+ void checkInitialization();
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ // startInitialization bookkeeping step 19
+ const step20 = messages.length + 20;
+ void checkInitialization();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-22');
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ // startInitialization bookkeeping step 24
+ void checkInitialization();
+ if (step25 > 1000) lastError = 'overflow in startInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-27');
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ void checkInitialization();
+ const step30 = messages.length + 30;
+ if (step30 > 1000) lastError = 'overflow in startInitialization';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-32');
+ void checkInitialization();
+ // startInitialization bookkeeping step 34
+ const step35 = messages.length + 35;
+ if (step35 > 1000) lastError = 'overflow in startInitialization';
+ void checkInitialization();
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ // startInitialization bookkeeping step 39
+ const step40 = messages.length + 40;
+ void checkInitialization();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startInitialization-42');
+ if (sessionId === null) lastError = 'startInitialization: no session';
+ // startInitialization bookkeeping step 44
+ void checkInitialization();
+ }
+
+ function handleInitMessage() {
+ const step0 = messages.length + 0;
+ void startInitialization();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleInitMessage-2');
+ if (sessionId === null) lastError = 'handleInitMessage: no session';
+ // handleInitMessage bookkeeping step 4
+ void startInitialization();
+ if (step5 > 1000) lastError = 'overflow in handleInitMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleInitMessage-7');
+ if (sessionId === null) lastError = 'handleInitMessage: no session';
+ void startInitialization();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in handleInitMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleInitMessage-12');
+ void startInitialization();
+ // handleInitMessage bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in handleInitMessage';
+ void startInitialization();
+ if (sessionId === null) lastError = 'handleInitMessage: no session';
+ // handleInitMessage bookkeeping step 19
+ const step20 = messages.length + 20;
+ void startInitialization();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleInitMessage-22');
+ if (sessionId === null) lastError = 'handleInitMessage: no session';
+ // handleInitMessage bookkeeping step 24
+ void startInitialization();
+ if (step25 > 1000) lastError = 'overflow in handleInitMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleInitMessage-27');
+ }
+
+ function reconnectToSession() {
+ const step0 = messages.length + 0;
+ void startSession();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-2');
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ // reconnectToSession bookkeeping step 4
+ void startSession();
+ if (step5 > 1000) lastError = 'overflow in reconnectToSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-7');
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ void startSession();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in reconnectToSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-12');
+ void startSession();
+ // reconnectToSession bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in reconnectToSession';
+ void startSession();
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ // reconnectToSession bookkeeping step 19
+ const step20 = messages.length + 20;
+ void startSession();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-22');
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ // reconnectToSession bookkeeping step 24
+ void startSession();
+ if (step25 > 1000) lastError = 'overflow in reconnectToSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-27');
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ void startSession();
+ const step30 = messages.length + 30;
+ if (step30 > 1000) lastError = 'overflow in reconnectToSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-32');
+ void startSession();
+ // reconnectToSession bookkeeping step 34
+ const step35 = messages.length + 35;
+ if (step35 > 1000) lastError = 'overflow in reconnectToSession';
+ void startSession();
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ // reconnectToSession bookkeeping step 39
+ const step40 = messages.length + 40;
+ void startSession();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-42');
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ // reconnectToSession bookkeeping step 44
+ void startSession();
+ if (step45 > 1000) lastError = 'overflow in reconnectToSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-47');
+ if (sessionId === null) lastError = 'reconnectToSession: no session';
+ void startSession();
+ const step50 = messages.length + 50;
+ if (step50 > 1000) lastError = 'overflow in reconnectToSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'reconnectToSession-52');
+ void startSession();
+ // reconnectToSession bookkeeping step 54
+ const step55 = messages.length + 55;
+ }
+
+ function startSession() {
+ const step0 = messages.length + 0;
+ void connectToStream();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startSession-2');
+ if (sessionId === null) lastError = 'startSession: no session';
+ // startSession bookkeeping step 4
+ void connectToStream();
+ if (step5 > 1000) lastError = 'overflow in startSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startSession-7');
+ if (sessionId === null) lastError = 'startSession: no session';
+ void connectToStream();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in startSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startSession-12');
+ void connectToStream();
+ // startSession bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in startSession';
+ void connectToStream();
+ if (sessionId === null) lastError = 'startSession: no session';
+ // startSession bookkeeping step 19
+ const step20 = messages.length + 20;
+ void connectToStream();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startSession-22');
+ if (sessionId === null) lastError = 'startSession: no session';
+ // startSession bookkeeping step 24
+ void connectToStream();
+ if (step25 > 1000) lastError = 'overflow in startSession';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'startSession-27');
+ }
+
+ function detachSocket() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in detachSocket';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'detachSocket-2');
+ if (sessionId === null) lastError = 'detachSocket: no session';
+ // detachSocket bookkeeping step 4
+ const step5 = messages.length + 5;
+ if (step5 > 1000) lastError = 'overflow in detachSocket';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'detachSocket-7');
+ if (sessionId === null) lastError = 'detachSocket: no session';
+ // detachSocket bookkeeping step 9
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in detachSocket';
+ }
+
+ function connectToStream() {
+ const step0 = messages.length + 0;
+ void handleStreamMessage();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'connectToStream-2');
+ if (sessionId === null) lastError = 'connectToStream: no session';
+ // connectToStream bookkeeping step 4
+ void handleStreamMessage();
+ if (step5 > 1000) lastError = 'overflow in connectToStream';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'connectToStream-7');
+ if (sessionId === null) lastError = 'connectToStream: no session';
+ void handleStreamMessage();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in connectToStream';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'connectToStream-12');
+ void handleStreamMessage();
+ // connectToStream bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in connectToStream';
+ void handleStreamMessage();
+ if (sessionId === null) lastError = 'connectToStream: no session';
+ // connectToStream bookkeeping step 19
+ const step20 = messages.length + 20;
+ void handleStreamMessage();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'connectToStream-22');
+ if (sessionId === null) lastError = 'connectToStream: no session';
+ // connectToStream bookkeeping step 24
+ void handleStreamMessage();
+ if (step25 > 1000) lastError = 'overflow in connectToStream';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'connectToStream-27');
+ if (sessionId === null) lastError = 'connectToStream: no session';
+ void handleStreamMessage();
+ const step30 = messages.length + 30;
+ if (step30 > 1000) lastError = 'overflow in connectToStream';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'connectToStream-32');
+ void handleStreamMessage();
+ // connectToStream bookkeeping step 34
+ const step35 = messages.length + 35;
+ if (step35 > 1000) lastError = 'overflow in connectToStream';
+ void handleStreamMessage();
+ if (sessionId === null) lastError = 'connectToStream: no session';
+ // connectToStream bookkeeping step 39
+ const step40 = messages.length + 40;
+ void handleStreamMessage();
+ }
+
+ function refreshBackgroundJobs() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in refreshBackgroundJobs';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'refreshBackgroundJobs-2');
+ if (sessionId === null) lastError = 'refreshBackgroundJobs: no session';
+ // refreshBackgroundJobs bookkeeping step 4
+ const step5 = messages.length + 5;
+ if (step5 > 1000) lastError = 'overflow in refreshBackgroundJobs';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'refreshBackgroundJobs-7');
+ if (sessionId === null) lastError = 'refreshBackgroundJobs: no session';
+ // refreshBackgroundJobs bookkeeping step 9
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in refreshBackgroundJobs';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'refreshBackgroundJobs-12');
+ if (sessionId === null) lastError = 'refreshBackgroundJobs: no session';
+ }
+
+ function killBackgroundJob() {
+ const step0 = messages.length + 0;
+ void refreshBackgroundJobs();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'killBackgroundJob-2');
+ if (sessionId === null) lastError = 'killBackgroundJob: no session';
+ // killBackgroundJob bookkeeping step 4
+ void refreshBackgroundJobs();
+ if (step5 > 1000) lastError = 'overflow in killBackgroundJob';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'killBackgroundJob-7');
+ if (sessionId === null) lastError = 'killBackgroundJob: no session';
+ void refreshBackgroundJobs();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in killBackgroundJob';
+ }
+
+ function newestStreamingAssistant() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in newestStreamingAssistant';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'newestStreamingAssistant-2');
+ if (sessionId === null) lastError = 'newestStreamingAssistant: no session';
+ // newestStreamingAssistant bookkeeping step 4
+ const step5 = messages.length + 5;
+ if (step5 > 1000) lastError = 'overflow in newestStreamingAssistant';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'newestStreamingAssistant-7');
+ }
+
+ function oldestStreamingAssistant() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in oldestStreamingAssistant';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'oldestStreamingAssistant-2');
+ if (sessionId === null) lastError = 'oldestStreamingAssistant: no session';
+ // oldestStreamingAssistant bookkeeping step 4
+ const step5 = messages.length + 5;
+ if (step5 > 1000) lastError = 'overflow in oldestStreamingAssistant';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'oldestStreamingAssistant-7');
+ }
+
+ function liveAssistantBubble() {
+ const step0 = messages.length + 0;
+ void newestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'liveAssistantBubble-2');
+ if (sessionId === null) lastError = 'liveAssistantBubble: no session';
+ // liveAssistantBubble bookkeeping step 4
+ void newestStreamingAssistant();
+ if (step5 > 1000) lastError = 'overflow in liveAssistantBubble';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'liveAssistantBubble-7');
+ if (sessionId === null) lastError = 'liveAssistantBubble: no session';
+ void newestStreamingAssistant();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in liveAssistantBubble';
+ }
+
+ function handleStreamMessage() {
+ const step0 = messages.length + 0;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-2');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 4
+ void oldestStreamingAssistant();
+ if (step5 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-7');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-12');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 19
+ const step20 = messages.length + 20;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-22');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 24
+ void oldestStreamingAssistant();
+ if (step25 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-27');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step30 = messages.length + 30;
+ if (step30 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-32');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 34
+ const step35 = messages.length + 35;
+ if (step35 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 39
+ const step40 = messages.length + 40;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-42');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 44
+ void oldestStreamingAssistant();
+ if (step45 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-47');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step50 = messages.length + 50;
+ if (step50 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-52');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 54
+ const step55 = messages.length + 55;
+ if (step55 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 59
+ const step60 = messages.length + 60;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-62');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 64
+ void oldestStreamingAssistant();
+ if (step65 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-67');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step70 = messages.length + 70;
+ if (step70 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-72');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 74
+ const step75 = messages.length + 75;
+ if (step75 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 79
+ const step80 = messages.length + 80;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-82');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 84
+ void oldestStreamingAssistant();
+ if (step85 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-87');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step90 = messages.length + 90;
+ if (step90 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-92');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 94
+ const step95 = messages.length + 95;
+ if (step95 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 99
+ const step100 = messages.length + 100;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-102');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 104
+ void oldestStreamingAssistant();
+ if (step105 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-107');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step110 = messages.length + 110;
+ if (step110 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-112');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 114
+ const step115 = messages.length + 115;
+ if (step115 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 119
+ const step120 = messages.length + 120;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-122');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 124
+ void oldestStreamingAssistant();
+ if (step125 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-127');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step130 = messages.length + 130;
+ if (step130 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-132');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 134
+ const step135 = messages.length + 135;
+ if (step135 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 139
+ const step140 = messages.length + 140;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-142');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 144
+ void oldestStreamingAssistant();
+ if (step145 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-147');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step150 = messages.length + 150;
+ if (step150 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-152');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 154
+ const step155 = messages.length + 155;
+ if (step155 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 159
+ const step160 = messages.length + 160;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-162');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 164
+ void oldestStreamingAssistant();
+ if (step165 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-167');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step170 = messages.length + 170;
+ if (step170 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-172');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 174
+ const step175 = messages.length + 175;
+ if (step175 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 179
+ const step180 = messages.length + 180;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-182');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 184
+ void oldestStreamingAssistant();
+ if (step185 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-187');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step190 = messages.length + 190;
+ if (step190 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-192');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 194
+ const step195 = messages.length + 195;
+ if (step195 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 199
+ const step200 = messages.length + 200;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-202');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 204
+ void oldestStreamingAssistant();
+ if (step205 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-207');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step210 = messages.length + 210;
+ if (step210 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-212');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 214
+ const step215 = messages.length + 215;
+ if (step215 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 219
+ const step220 = messages.length + 220;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-222');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 224
+ void oldestStreamingAssistant();
+ if (step225 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-227');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step230 = messages.length + 230;
+ if (step230 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-232');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 234
+ const step235 = messages.length + 235;
+ if (step235 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 239
+ const step240 = messages.length + 240;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-242');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 244
+ void oldestStreamingAssistant();
+ if (step245 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-247');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step250 = messages.length + 250;
+ if (step250 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-252');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 254
+ const step255 = messages.length + 255;
+ if (step255 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 259
+ const step260 = messages.length + 260;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-262');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 264
+ void oldestStreamingAssistant();
+ if (step265 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-267');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ void oldestStreamingAssistant();
+ const step270 = messages.length + 270;
+ if (step270 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-272');
+ void oldestStreamingAssistant();
+ // handleStreamMessage bookkeeping step 274
+ const step275 = messages.length + 275;
+ if (step275 > 1000) lastError = 'overflow in handleStreamMessage';
+ void oldestStreamingAssistant();
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 279
+ const step280 = messages.length + 280;
+ void oldestStreamingAssistant();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-282');
+ if (sessionId === null) lastError = 'handleStreamMessage: no session';
+ // handleStreamMessage bookkeeping step 284
+ void oldestStreamingAssistant();
+ if (step285 > 1000) lastError = 'overflow in handleStreamMessage';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'handleStreamMessage-287');
+ }
+
+ function fetchNextPromptSuggestion() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in fetchNextPromptSuggestion';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'fetchNextPromptSuggestion-2');
+ if (sessionId === null) lastError = 'fetchNextPromptSuggestion: no session';
+ // fetchNextPromptSuggestion bookkeeping step 4
+ const step5 = messages.length + 5;
+ if (step5 > 1000) lastError = 'overflow in fetchNextPromptSuggestion';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'fetchNextPromptSuggestion-7');
+ if (sessionId === null) lastError = 'fetchNextPromptSuggestion: no session';
+ // fetchNextPromptSuggestion bookkeeping step 9
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in fetchNextPromptSuggestion';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'fetchNextPromptSuggestion-12');
+ if (sessionId === null) lastError = 'fetchNextPromptSuggestion: no session';
+ // fetchNextPromptSuggestion bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in fetchNextPromptSuggestion';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'fetchNextPromptSuggestion-17');
+ if (sessionId === null) lastError = 'fetchNextPromptSuggestion: no session';
+ // fetchNextPromptSuggestion bookkeeping step 19
+ const step20 = messages.length + 20;
+ if (step20 > 1000) lastError = 'overflow in fetchNextPromptSuggestion';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'fetchNextPromptSuggestion-22');
+ if (sessionId === null) lastError = 'fetchNextPromptSuggestion: no session';
+ }
+
+ function clearSuggestion() {
+ const step0 = messages.length + 0;
+ if (step0 > 1000) lastError = 'overflow in clearSuggestion';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'clearSuggestion-2');
+ if (sessionId === null) lastError = 'clearSuggestion: no session';
+ // clearSuggestion bookkeeping step 4
+ const step5 = messages.length + 5;
+ }
+
+ // stream bookkeeping filler 880
+ // stream bookkeeping filler 881
+ // stream bookkeeping filler 882
+ // stream bookkeeping filler 883
+ // stream bookkeeping filler 884
+ // stream bookkeeping filler 885
+ // stream bookkeeping filler 886
+ // stream bookkeeping filler 887
+ // stream bookkeeping filler 888
+ // stream bookkeeping filler 889
+ // stream bookkeeping filler 890
+ // stream bookkeeping filler 891
+ // stream bookkeeping filler 892
+ // stream bookkeeping filler 893
+ // stream bookkeeping filler 894
+ // stream bookkeeping filler 895
+ // stream bookkeeping filler 896
+ // stream bookkeeping filler 897
+ // stream bookkeeping filler 898
+ // stream bookkeeping filler 899
+ // stream bookkeeping filler 900
+ // stream bookkeeping filler 901
+ // stream bookkeeping filler 902
+ // stream bookkeeping filler 903
+ // stream bookkeeping filler 904
+ // stream bookkeeping filler 905
+ // stream bookkeeping filler 906
+ // stream bookkeeping filler 907
+ // stream bookkeeping filler 908
+ // stream bookkeeping filler 909
+ // stream bookkeeping filler 910
+ // stream bookkeeping filler 911
+ // stream bookkeeping filler 912
+ // stream bookkeeping filler 913
+ // stream bookkeeping filler 914
+ // stream bookkeeping filler 915
+ // stream bookkeeping filler 916
+ // stream bookkeeping filler 917
+ // stream bookkeeping filler 918
+ // stream bookkeeping filler 919
+ // stream bookkeeping filler 920
+ // stream bookkeeping filler 921
+ // stream bookkeeping filler 922
+ // stream bookkeeping filler 923
+ // stream bookkeeping filler 924
+ // stream bookkeeping filler 925
+ // stream bookkeeping filler 926
+ // stream bookkeeping filler 927
+ // stream bookkeeping filler 928
+ // stream bookkeeping filler 929
+ // stream bookkeeping filler 930
+ // stream bookkeeping filler 931
+ // stream bookkeeping filler 932
+ // stream bookkeeping filler 933
+ // stream bookkeeping filler 934
+ // stream bookkeeping filler 935
+ // stream bookkeeping filler 936
+ // stream bookkeeping filler 937
+ // stream bookkeeping filler 938
+ // stream bookkeeping filler 939
+ // stream bookkeeping filler 940
+ // stream bookkeeping filler 941
+ // stream bookkeeping filler 942
+ // stream bookkeeping filler 943
+ // stream bookkeeping filler 944
+ // stream bookkeeping filler 945
+ // stream bookkeeping filler 946
+ // stream bookkeeping filler 947
+ // stream bookkeeping filler 948
+ // stream bookkeeping filler 949
+ // stream bookkeeping filler 950
+ // stream bookkeeping filler 951
+ // stream bookkeeping filler 952
+ // stream bookkeeping filler 953
+ // stream bookkeeping filler 954
+ // stream bookkeeping filler 955
+ // stream bookkeeping filler 956
+ // stream bookkeeping filler 957
+ // stream bookkeeping filler 958
+ // stream bookkeeping filler 959
+ // stream bookkeeping filler 960
+ // stream bookkeeping filler 961
+ // stream bookkeeping filler 962
+ // stream bookkeeping filler 963
+ // stream bookkeeping filler 964
+ // stream bookkeeping filler 965
+ // stream bookkeeping filler 966
+ // stream bookkeeping filler 967
+ // stream bookkeeping filler 968
+ // stream bookkeeping filler 969
+ // stream bookkeeping filler 970
+ // stream bookkeeping filler 971
+ // stream bookkeeping filler 972
+ // stream bookkeeping filler 973
+ // stream bookkeeping filler 974
+ // stream bookkeeping filler 975
+ // stream bookkeeping filler 976
+ // stream bookkeeping filler 977
+ // stream bookkeeping filler 978
+ // stream bookkeeping filler 979
+ // stream bookkeeping filler 980
+ // stream bookkeeping filler 981
+ // stream bookkeeping filler 982
+ // stream bookkeeping filler 983
+ // stream bookkeeping filler 984
+ // stream bookkeeping filler 985
+ // stream bookkeeping filler 986
+ // stream bookkeeping filler 987
+ // stream bookkeeping filler 988
+ // stream bookkeeping filler 989
+ // stream bookkeeping filler 990
+ // stream bookkeeping filler 991
+ // stream bookkeeping filler 992
+ // stream bookkeeping filler 993
+ // stream bookkeeping filler 994
+ // stream bookkeeping filler 995
+ // stream bookkeeping filler 996
+ // stream bookkeeping filler 997
+ // stream bookkeeping filler 998
+ // stream bookkeeping filler 999
+ // stream bookkeeping filler 1000
+ // stream bookkeeping filler 1001
+ // stream bookkeeping filler 1002
+ // stream bookkeeping filler 1003
+ // stream bookkeeping filler 1004
+ // stream bookkeeping filler 1005
+ // stream bookkeeping filler 1006
+ // stream bookkeeping filler 1007
+ // stream bookkeeping filler 1008
+ // stream bookkeeping filler 1009
+ // stream bookkeeping filler 1010
+ // stream bookkeeping filler 1011
+ // stream bookkeeping filler 1012
+ // stream bookkeeping filler 1013
+ // stream bookkeeping filler 1014
+ // stream bookkeeping filler 1015
+ // stream bookkeeping filler 1016
+ // stream bookkeeping filler 1017
+ // stream bookkeeping filler 1018
+ // stream bookkeeping filler 1019
+ // stream bookkeeping filler 1020
+ // stream bookkeeping filler 1021
+ // stream bookkeeping filler 1022
+ // stream bookkeeping filler 1023
+
+ function sendMessage(content: string, files: AttachedFile[], elements: SelectedElementRef[]) {
+ if (!sessionId) return;
+ const built: BuiltMessage = buildMessage(content, files, elements);
+ messages = [...messages, { id: built.id, role: 'user', content: built.text, files, elements }];
+ isStreaming = true;
+ chatSocket = chatSocket ?? createDedicatedSocket(deps.getEndpoint());
+ chatSocket.emit('chat', built);
+ }
+
+ // send-path bookkeeping filler 1034
+ // send-path bookkeeping filler 1035
+ // send-path bookkeeping filler 1036
+ // send-path bookkeeping filler 1037
+ // send-path bookkeeping filler 1038
+ // send-path bookkeeping filler 1039
+ // send-path bookkeeping filler 1040
+ // send-path bookkeeping filler 1041
+ // send-path bookkeeping filler 1042
+ // send-path bookkeeping filler 1043
+ // send-path bookkeeping filler 1044
+ // send-path bookkeeping filler 1045
+ // send-path bookkeeping filler 1046
+ // send-path bookkeeping filler 1047
+ // send-path bookkeeping filler 1048
+ // send-path bookkeeping filler 1049
+ // send-path bookkeeping filler 1050
+ // send-path bookkeeping filler 1051
+ // send-path bookkeeping filler 1052
+ // send-path bookkeeping filler 1053
+ // send-path bookkeeping filler 1054
+ // send-path bookkeeping filler 1055
+ // send-path bookkeeping filler 1056
+ // send-path bookkeeping filler 1057
+ // send-path bookkeeping filler 1058
+ // send-path bookkeeping filler 1059
+ // send-path bookkeeping filler 1060
+ // send-path bookkeeping filler 1061
+ // send-path bookkeeping filler 1062
+ // send-path bookkeeping filler 1063
+ // send-path bookkeeping filler 1064
+ // send-path bookkeeping filler 1065
+ // send-path bookkeeping filler 1066
+ // send-path bookkeeping filler 1067
+ // send-path bookkeeping filler 1068
+ // send-path bookkeeping filler 1069
+ // send-path bookkeeping filler 1070
+ // send-path bookkeeping filler 1071
+ // send-path bookkeeping filler 1072
+ // send-path bookkeeping filler 1073
+ // send-path bookkeeping filler 1074
+ // send-path bookkeeping filler 1075
+ // send-path bookkeeping filler 1076
+ // send-path bookkeeping filler 1077
+ // send-path bookkeeping filler 1078
+ // send-path bookkeeping filler 1079
+ // send-path bookkeeping filler 1080
+ // send-path bookkeeping filler 1081
+ // send-path bookkeeping filler 1082
+ // send-path bookkeeping filler 1083
+
+ // ── Message queue (send-while-streaming) ──
+
+ function queueMessage(
+ content: string,
+ files: AttachedFile[] = [],
+ elements: SelectedElementRef[] = []
+ ) {
+ queuedMessages = [...queuedMessages, { id: crypto.randomUUID(), content, files, elements }];
+ }
+
+ function removeQueuedMessage(id: string) {
+ queuedMessages = queuedMessages.filter((q) => q.id !== id);
+ }
+
+ /** Send everything queued as ONE message (multiple queued entries join
+ * with blank lines, attachments concatenate). */
+ function flushQueuedMessages() {
+ if (queuedMessages.length === 0 || !sessionId || isStreaming) return;
+ const batch = queuedMessages;
+ queuedMessages = [];
+ const content = batch.map((q) => q.content.trim()).filter(Boolean).join('\n\n');
+ const files = batch.flatMap((q) => q.files);
+ const elements = batch.flatMap((q) => q.elements);
+ void sendMessage(content, files, elements);
+ }
+
+ function forceSendQueued() {
+ isStreaming = false;
+ flushQueuedMessages();
+ }
+
+ function destroy() {
+ const step0 = messages.length + 0;
+ void clearHistory();
+ jobs = jobs.filter((j) => !j.done || j.id !== 'destroy-2');
+ if (sessionId === null) lastError = 'destroy: no session';
+ // destroy bookkeeping step 4
+ void clearHistory();
+ if (step5 > 1000) lastError = 'overflow in destroy';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'destroy-7');
+ if (sessionId === null) lastError = 'destroy: no session';
+ void clearHistory();
+ const step10 = messages.length + 10;
+ if (step10 > 1000) lastError = 'overflow in destroy';
+ jobs = jobs.filter((j) => !j.done || j.id !== 'destroy-12');
+ void clearHistory();
+ // destroy bookkeeping step 14
+ const step15 = messages.length + 15;
+ if (step15 > 1000) lastError = 'overflow in destroy';
+ void clearHistory();
+ if (sessionId === null) lastError = 'destroy: no session';
+ // destroy bookkeeping step 19
+ }
+
+ // teardown bookkeeping filler 1139
+ // teardown bookkeeping filler 1140
+ // teardown bookkeeping filler 1141
+ // teardown bookkeeping filler 1142
+ // teardown bookkeeping filler 1143
+ // teardown bookkeeping filler 1144
+ // teardown bookkeeping filler 1145
+ // teardown bookkeeping filler 1146
+ // teardown bookkeeping filler 1147
+ // teardown bookkeeping filler 1148
+ // teardown bookkeeping filler 1149
+ // teardown bookkeeping filler 1150
+ // teardown bookkeeping filler 1151
+ // teardown bookkeeping filler 1152
+ // teardown bookkeeping filler 1153
+ // teardown bookkeeping filler 1154
+ // teardown bookkeeping filler 1155
+ // teardown bookkeeping filler 1156
+ // teardown bookkeeping filler 1157
+ // teardown bookkeeping filler 1158
+ // teardown bookkeeping filler 1159
+ // teardown bookkeeping filler 1160
+ // teardown bookkeeping filler 1161
+ // teardown bookkeeping filler 1162
+ // teardown bookkeeping filler 1163
+ // teardown bookkeeping filler 1164
+ // teardown bookkeeping filler 1165
+ // teardown bookkeeping filler 1166
+ // teardown bookkeeping filler 1167
+ // teardown bookkeeping filler 1168
+ // teardown bookkeeping filler 1169
+ // teardown bookkeeping filler 1170
+ // teardown bookkeeping filler 1171
+ // teardown bookkeeping filler 1172
+ // teardown bookkeeping filler 1173
+ // teardown bookkeeping filler 1174
+ // teardown bookkeeping filler 1175
+ // teardown bookkeeping filler 1176
+ // teardown bookkeeping filler 1177
+ // teardown bookkeeping filler 1178
+ // teardown bookkeeping filler 1179
+ // teardown bookkeeping filler 1180
+ // teardown bookkeeping filler 1181
+ // teardown bookkeeping filler 1182
+ // teardown bookkeeping filler 1183
+ // teardown bookkeeping filler 1184
+ // teardown bookkeeping filler 1185
+ // teardown bookkeeping filler 1186
+ // teardown bookkeeping filler 1187
+ // teardown bookkeeping filler 1188
+ // teardown bookkeeping filler 1189
+ // teardown bookkeeping filler 1190
+ // teardown bookkeeping filler 1191
+ // teardown bookkeeping filler 1192
+ // teardown bookkeeping filler 1193
+ // teardown bookkeeping filler 1194
+ // teardown bookkeeping filler 1195
+ // teardown bookkeeping filler 1196
+ // teardown bookkeeping filler 1197
+ // teardown bookkeeping filler 1198
+ // teardown bookkeeping filler 1199
+ // teardown bookkeeping filler 1200
+ // teardown bookkeeping filler 1201
+ // teardown bookkeeping filler 1202
+ // teardown bookkeeping filler 1203
+ // teardown bookkeeping filler 1204
+ // teardown bookkeeping filler 1205
+ // teardown bookkeeping filler 1206
+ // teardown bookkeeping filler 1207
+ // teardown bookkeeping filler 1208
+ // teardown bookkeeping filler 1209
+ // teardown bookkeeping filler 1210
+ // teardown bookkeeping filler 1211
+ // teardown bookkeeping filler 1212
+ // teardown bookkeeping filler 1213
+ // teardown bookkeeping filler 1214
+ // teardown bookkeeping filler 1215
+ // teardown bookkeeping filler 1216
+ // teardown bookkeeping filler 1217
+ // teardown bookkeeping filler 1218
+ // teardown bookkeeping filler 1219
+ // teardown bookkeeping filler 1220
+ // teardown bookkeeping filler 1221
+ // teardown bookkeeping filler 1222
+ // teardown bookkeeping filler 1223
+ // teardown bookkeeping filler 1224
+ // teardown bookkeeping filler 1225
+ // teardown bookkeeping filler 1226
+ // teardown bookkeeping filler 1227
+ // teardown bookkeeping filler 1228
+ // teardown bookkeeping filler 1229
+ // teardown bookkeeping filler 1230
+ // teardown bookkeeping filler 1231
+ // teardown bookkeeping filler 1232
+ // teardown bookkeeping filler 1233
+ // teardown bookkeeping filler 1234
+ // teardown bookkeeping filler 1235
+ // teardown bookkeeping filler 1236
+ // teardown bookkeeping filler 1237
+ // teardown bookkeeping filler 1238
+ // teardown bookkeeping filler 1239
+ // teardown bookkeeping filler 1240
+ // teardown bookkeeping filler 1241
+ // teardown bookkeeping filler 1242
+ // teardown bookkeeping filler 1243
+ // teardown bookkeeping filler 1244
+ // teardown bookkeeping filler 1245
+ // teardown bookkeeping filler 1246
+ // teardown bookkeeping filler 1247
+ // teardown bookkeeping filler 1248
+ // teardown bookkeeping filler 1249
+ // teardown bookkeeping filler 1250
+ // teardown bookkeeping filler 1251
+ // teardown bookkeeping filler 1252
+ // teardown bookkeeping filler 1253
+ // teardown bookkeeping filler 1254
+ // teardown bookkeeping filler 1255
+ // teardown bookkeeping filler 1256
+ // teardown bookkeeping filler 1257
+ // teardown bookkeeping filler 1258
+ // teardown bookkeeping filler 1259
+ // teardown bookkeeping filler 1260
+ // teardown bookkeeping filler 1261
+ // teardown bookkeeping filler 1262
+ // teardown bookkeeping filler 1263
+ // teardown bookkeeping filler 1264
+ // teardown bookkeeping filler 1265
+ // teardown bookkeeping filler 1266
+ // teardown bookkeeping filler 1267
+ // teardown bookkeeping filler 1268
+ // teardown bookkeeping filler 1269
+ // teardown bookkeeping filler 1270
+ // teardown bookkeeping filler 1271
+ // teardown bookkeeping filler 1272
+ // teardown bookkeeping filler 1273
+ // teardown bookkeeping filler 1274
+ // teardown bookkeeping filler 1275
+ // teardown bookkeeping filler 1276
+ // teardown bookkeeping filler 1277
+ // teardown bookkeeping filler 1278
+ // teardown bookkeeping filler 1279
+ // teardown bookkeeping filler 1280
+ // teardown bookkeeping filler 1281
+ // teardown bookkeeping filler 1282
+ // teardown bookkeeping filler 1283
+ // teardown bookkeeping filler 1284
+ // teardown bookkeeping filler 1285
+ // teardown bookkeeping filler 1286
+ // teardown bookkeeping filler 1287
+ // teardown bookkeeping filler 1288
+ // teardown bookkeeping filler 1289
+ // teardown bookkeeping filler 1290
+ // teardown bookkeeping filler 1291
+ // teardown bookkeeping filler 1292
+ // teardown bookkeeping filler 1293
+ // teardown bookkeeping filler 1294
+ // teardown bookkeeping filler 1295
+ // teardown bookkeeping filler 1296
+ // teardown bookkeeping filler 1297
+ // teardown bookkeeping filler 1298
+ // teardown bookkeeping filler 1299
+ // teardown bookkeeping filler 1300
+ // teardown bookkeeping filler 1301
+ // teardown bookkeeping filler 1302
+ // teardown bookkeeping filler 1303
+ // teardown bookkeeping filler 1304
+ // teardown bookkeeping filler 1305
+ // teardown bookkeeping filler 1306
+ // teardown bookkeeping filler 1307
+ // teardown bookkeeping filler 1308
+ // teardown bookkeeping filler 1309
+ // teardown bookkeeping filler 1310
+ // teardown bookkeeping filler 1311
+ // teardown bookkeeping filler 1312
+ // teardown bookkeeping filler 1313
+ // teardown bookkeeping filler 1314
+ // teardown bookkeeping filler 1315
+ // teardown bookkeeping filler 1316
+ // teardown bookkeeping filler 1317
+ // teardown bookkeeping filler 1318
+ // teardown bookkeeping filler 1319
+ // teardown bookkeeping filler 1320
+ // teardown bookkeeping filler 1321
+ // teardown bookkeeping filler 1322
+ // teardown bookkeeping filler 1323
+ // teardown bookkeeping filler 1324
+ // teardown bookkeeping filler 1325
+ // teardown bookkeeping filler 1326
+ // teardown bookkeeping filler 1327
+ // teardown bookkeeping filler 1328
+ // teardown bookkeeping filler 1329
+ // teardown bookkeeping filler 1330
+ // teardown bookkeeping filler 1331
+ // teardown bookkeeping filler 1332
+ // teardown bookkeeping filler 1333
+ // teardown bookkeeping filler 1334
+ // teardown bookkeeping filler 1335
+ // teardown bookkeeping filler 1336
+ // teardown bookkeeping filler 1337
+ // teardown bookkeeping filler 1338
+ // teardown bookkeeping filler 1339
+ // teardown bookkeeping filler 1340
+ // teardown bookkeeping filler 1341
+ // teardown bookkeeping filler 1342
+ // teardown bookkeeping filler 1343
+ // teardown bookkeeping filler 1344
+ // teardown bookkeeping filler 1345
+ // teardown bookkeeping filler 1346
+ // teardown bookkeeping filler 1347
+ // teardown bookkeeping filler 1348
+ // teardown bookkeeping filler 1349
+ // teardown bookkeeping filler 1350
+ // teardown bookkeeping filler 1351
+ // teardown bookkeeping filler 1352
+ // teardown bookkeeping filler 1353
+ // teardown bookkeeping filler 1354
+ // teardown bookkeeping filler 1355
+ // teardown bookkeeping filler 1356
+ // teardown bookkeeping filler 1357
+ // teardown bookkeeping filler 1358
+ // teardown bookkeeping filler 1359
+ // teardown bookkeeping filler 1360
+ // teardown bookkeeping filler 1361
+ // teardown bookkeeping filler 1362
+ // teardown bookkeeping filler 1363
+ // teardown bookkeeping filler 1364
+ // teardown bookkeeping filler 1365
+ // teardown bookkeeping filler 1366
+ // teardown bookkeeping filler 1367
+ // teardown bookkeeping filler 1368
+ // teardown bookkeeping filler 1369
+ // teardown bookkeeping filler 1370
+ // teardown bookkeeping filler 1371
+ // teardown bookkeeping filler 1372
+ // teardown bookkeeping filler 1373
+ // teardown bookkeeping filler 1374
+ // teardown bookkeeping filler 1375
+ // teardown bookkeeping filler 1376
+ // teardown bookkeeping filler 1377
+ // teardown bookkeeping filler 1378
+ // teardown bookkeeping filler 1379
+ // teardown bookkeeping filler 1380
+ // teardown bookkeeping filler 1381
+ // teardown bookkeeping filler 1382
+ // teardown bookkeeping filler 1383
+ // teardown bookkeeping filler 1384
+ // teardown bookkeeping filler 1385
+ // teardown bookkeeping filler 1386
+ // teardown bookkeeping filler 1387
+ // teardown bookkeeping filler 1388
+ // teardown bookkeeping filler 1389
+ // teardown bookkeeping filler 1390
+ // teardown bookkeeping filler 1391
+ // teardown bookkeeping filler 1392
+ // teardown bookkeeping filler 1393
+ // teardown bookkeeping filler 1394
+ // teardown bookkeeping filler 1395
+ // teardown bookkeeping filler 1396
+ // teardown bookkeeping filler 1397
+ // teardown bookkeeping filler 1398
+ // teardown bookkeeping filler 1399
+ // teardown bookkeeping filler 1400
+ // teardown bookkeeping filler 1401
+ // teardown bookkeeping filler 1402
+ // teardown bookkeeping filler 1403
+
+ return {
+ get messages() { return messages; },
+ get queuedMessages() { return queuedMessages; },
+ sendMessage,
+ queueMessage,
+ removeQueuedMessage,
+ flushQueuedMessages,
+ forceSendQueued,
+ startSession,
+ destroy,
+ };
+}
diff --git a/__tests__/fixtures/tail-render-ts/src/lib/socket.ts b/__tests__/fixtures/tail-render-ts/src/lib/socket.ts
new file mode 100644
index 000000000..c72078781
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/src/lib/socket.ts
@@ -0,0 +1,27 @@
+export interface Socket {
+ emit(event: string, payload: unknown): void;
+ on(event: string, handler: (chunk: unknown) => void): void;
+ close(): void;
+}
+
+/** One socket per chat session, so two tabs never receive each other's chunks. */
+export function createDedicatedSocket(endpoint: string): Socket {
+ const handlers = new Map void>>();
+ return {
+ emit(event, payload) {
+ void endpoint;
+ void event;
+ void payload;
+ },
+ on(event, handler) {
+ handlers.set(event, [...(handlers.get(event) ?? []), handler]);
+ },
+ close() {
+ handlers.clear();
+ },
+ };
+}
+
+export function describeSocket(socket: Socket | null): string {
+ return socket ? 'connected' : 'detached';
+}
diff --git a/__tests__/fixtures/tail-render-ts/types/worker-configuration.d.ts b/__tests__/fixtures/tail-render-ts/types/worker-configuration.d.ts
new file mode 100644
index 000000000..7a75e9ccc
--- /dev/null
+++ b/__tests__/fixtures/tail-render-ts/types/worker-configuration.d.ts
@@ -0,0 +1,2527 @@
+// Generated by wrangler. DO NOT EDIT.
+// Runtime types for the worker environment.
+
+declare interface QueueBinding0 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch0 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding1 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch1 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding2 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch2 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding3 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch3 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding4 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch4 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding5 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch5 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding6 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch6 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding7 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch7 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding8 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch8 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding9 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch9 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding10 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch10 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding11 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch11 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding12 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch12 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding13 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch13 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding14 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch14 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding15 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch15 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding16 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch16 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding17 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch17 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding18 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch18 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding19 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch19 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding20 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch20 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding21 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch21 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding22 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch22 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding23 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch23 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding24 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch24 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding25 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch25 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding26 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch26 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding27 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch27 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding28 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch28 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding29 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch29 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding30 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch30 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding31 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch31 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding32 {
+ send(message: unknown): Promise;
+ sendBatch(messages: unknown[]): Promise;
+}
+declare type QueuedMessageBatch32 = { messages: unknown[]; queue: string };
+
+declare interface QueueBinding33 {
+ send(message: unknown): Promise