fix(explore): damp ambient declaration files on flow queries (CG-28)

A file that declares nothing but types and that nothing in the index depends
on — a hand-written ambient `.d.ts` of global shims, vendored typings, module
augmentation — cannot answer a flow question: no bodies, no call edges, no
behaviour, nothing typed by it. 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. Measured
on the new fixture: rank #1 and 51% of delivered source, with the flow's own
entry file pushed out of the response entirely.

Measured first, per the issue: the Wrangler `worker-configuration.d.ts` that
opened this is already handled by CG-25's banner detection, worth 15-46 points
of envelope share across four flow queries. CG-25 credited; only the un-bannered
case needed anything.

`rankPenalty` now multiplies score and graph mass by 0.5 for such files, taken
as the STRONGER of it and the generated penalty rather than multiplied — one
property two signals see must not be charged twice. Detection is structural, not
by extension, and four conditions deep. Two of them were forced by measurement:
requiring every symbol to be type-level takes the corpus flag rate from 1-18%
(which swept in Kotlin sealed classes, Rust mod.rs re-exports and django's
locale tables) down to 0-4%; requiring that nothing depends on the file
separates an ambient shim from a working types module, and without it the rule
demoted displacement-ts's pipeline `types.ts` and broke the CG-31 gate.

A query that NAMES a declared type is exempt, so a question about a type still
reaches its declaration at full weight. Precise tokens only, so "…the file
body…" cannot exempt a `Body` interface it never meant to name; this needs its
own set because `namedSeedIds` is callable-only and a type never becomes one.

Regression evidence in docs/benchmarks/explore-declaration-only-cg28.md:
6-repo envelope sweep byte-identical against a clean baseline build, zero
ambient files reach the candidate set on VS Code across five queries, corpus
flag rate 0-0.74%, both allocation fixtures PASS, full suite 2,978 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-06 14:35:56 -05:00
co-authored by Claude Opus 5
parent 463f6e7844
commit 9efae0f8f2
18 changed files with 1582 additions and 3 deletions
+207
View File
@@ -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);
});
});
});
@@ -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."
}
@@ -0,0 +1,46 @@
export interface BucketObject {
key: string;
body: ReadableStream<Uint8Array>;
size: number;
}
export interface Bucket {
put(
key: string,
value: ReadableStream<Uint8Array>,
options?: { httpMetadata?: { contentType?: string } },
): Promise<void>;
get(key: string): Promise<BucketObject | null>;
}
export interface MetadataStore {
put(id: string, value: string): Promise<void>;
get(id: string): Promise<string | null>;
}
const objects = new Map<string, BucketObject>();
const rows = new Map<string, string>();
/** 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;
},
};
}
@@ -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<void> {
const queue = openUploadQueue();
await queue.send(body, { contentType: 'json' });
}
/** Consumer side: process a batch of upload messages. */
export async function consumeUploadBatch(messages: UploadMessageBody[]): Promise<number> {
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<void>;
}
/** The binding lookup, isolated so tests can swap it. */
export function openUploadQueue(): UploadQueue {
return {
async send() {
/* binding provided by the runtime */
},
};
}
@@ -0,0 +1,44 @@
export interface ParsedUpload {
ok: true;
key: string;
body: ReadableStream<Uint8Array>;
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<ParsedUpload | ParseFailure> {
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<Uint8Array>,
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;
}
@@ -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<Response> {
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<Response> {
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 });
}
@@ -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<ImageMetadataRecord> {
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<ImageMetadataRecord | null> {
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;
}
@@ -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<Uint8Array>,
key: string,
contentType: string,
): Promise<StoredObject> {
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<Uint8Array, Uint8Array>({
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<ReadableStream<Uint8Array> | 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<Uint8Array>,
limit: number,
): ReadableStream<Uint8Array> {
let seen = 0;
const guard = new TransformStream<Uint8Array, Uint8Array>({
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);
}
@@ -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;
}
@@ -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<Uint8Array>,
options?: UploadPutOptions,
): Promise<StoredUploadObject>;
get(key: string): Promise<StoredUploadObject | null>;
head(key: string): Promise<StoredUploadHead | null>;
delete(key: string | string[]): Promise<void>;
list(options?: UploadListOptions): Promise<UploadListResult>;
}
interface StoredUploadObject {
readonly key: string;
readonly size: number;
readonly etag: string;
readonly uploaded: Date;
readonly body: ReadableStream<Uint8Array>;
readonly contentType: string;
readonly metadata?: ImageMetadataShim;
arrayBuffer(): Promise<ArrayBuffer>;
text(): Promise<string>;
json<T>(): Promise<T>;
}
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<string, string>;
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<void>;
get(id: string): Promise<string | null>;
getWithMetadata<T>(id: string): Promise<{ value: string | null; metadata: T | null }>;
delete(id: string): Promise<void>;
list(options?: MetadataListOptions): Promise<MetadataListResult>;
}
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<Body = unknown> {
send(body: Body, options?: UploadSendOptions): Promise<void>;
sendBatch(bodies: Iterable<UploadSendRequest<Body>>): Promise<void>;
}
interface UploadSendOptions {
contentType?: UploadContentType;
delaySeconds?: number;
}
type UploadContentType = 'text' | 'bytes' | 'json' | 'v8';
interface UploadSendRequest<Body = unknown> {
body: Body;
options?: UploadSendOptions;
}
interface UploadMessageShim<Body = unknown> {
readonly id: string;
readonly timestamp: Date;
readonly body: Body;
readonly attempts: number;
retry(options?: UploadRetryOptions): void;
ack(): void;
}
interface UploadRetryOptions {
delaySeconds?: number;
}
interface UploadMessageBatch<Body = unknown> {
readonly messages: readonly UploadMessageShim<Body>[];
readonly queue: string;
retryAll(options?: UploadRetryOptions): void;
ackAll(): void;
}
interface StreamPipeOptionsShim {
preventClose?: boolean;
preventAbort?: boolean;
preventCancel?: boolean;
signal?: AbortSignal;
}
interface ByteCounterShim {
readonly transform: TransformStream<Uint8Array, Uint8Array>;
total(): number;
}
interface StreamLimitShim {
readonly limit: number;
readonly seen: number;
exceeded(): boolean;
}
interface RequestBodyShim {
readonly body: ReadableStream<Uint8Array> | null;
readonly bodyUsed: boolean;
readonly headers: Headers;
readonly url: string;
arrayBuffer(): Promise<ArrayBuffer>;
formData(): Promise<FormData>;
blob(): Promise<Blob>;
}
interface ParsedUploadShim {
key: string;
contentType: string;
width: number;
height: number;
format: string;
}
interface ImageTransformerShim {
transform(transform: ImageTransformShim): ImageTransformerShim;
output(options: ImageOutputShim): Promise<ImageResultShim>;
}
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<Uint8Array>;
response(): Response;
}
interface UploadEnvShim {
UPLOADS: UploadStorage;
METADATA: MetadataStoreShim;
UPLOAD_QUEUE: UploadQueueShim<unknown>;
}
}
export {};
@@ -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<UploadMessageBody>;
IMAGES: ImagesBinding;
}
}
interface UploadMessageBody {
key: string;
metadataId: string;
contentType: string;
}
interface R2Bucket {
head(key: string): Promise<R2Object | null>;
get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;
put(
key: string,
value: ReadableStream | ArrayBuffer | string | null,
options?: R2PutOptions,
): Promise<R2Object>;
delete(keys: string | string[]): Promise<void>;
list(options?: R2ListOptions): Promise<R2Objects>;
createMultipartUpload(key: string, options?: R2MultipartOptions): Promise<R2MultipartUpload>;
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<string, string>;
readonly range?: R2Range;
readonly storageClass: string;
writeHttpMetadata(headers: Headers): void;
}
interface R2ObjectBody extends R2Object {
get body(): ReadableStream;
get bodyUsed(): boolean;
arrayBuffer(): Promise<ArrayBuffer>;
text(): Promise<string>;
json<T>(): Promise<T>;
blob(): Promise<Blob>;
bytes(): Promise<Uint8Array>;
}
interface R2GetOptions {
onlyIf?: R2Conditional | Headers;
range?: R2Range;
ssecKey?: ArrayBuffer | string;
}
interface R2PutOptions {
onlyIf?: R2Conditional | Headers;
httpMetadata?: R2HTTPMetadata | Headers;
customMetadata?: Record<string, string>;
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<string, string>;
storageClass?: string;
}
interface R2MultipartUpload {
readonly key: string;
readonly uploadId: string;
uploadPart(
partNumber: number,
value: ReadableStream | ArrayBuffer | string | Blob,
): Promise<R2UploadedPart>;
abort(): Promise<void>;
complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;
}
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<Key extends string = string> {
get(key: Key, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<string | null>;
getWithMetadata<Metadata = unknown>(
key: Key,
options?: Partial<KVNamespaceGetOptions<undefined>>,
): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;
put(
key: Key,
value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
options?: KVNamespacePutOptions,
): Promise<void>;
delete(key: Key): Promise<void>;
list<Metadata = unknown>(
options?: KVNamespaceListOptions,
): Promise<KVNamespaceListResult<Metadata, Key>>;
}
interface KVNamespaceGetOptions<Type> {
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<Metadata, Key extends string = string> {
keys: KVNamespaceListKey<Metadata, Key>[];
list_complete: boolean;
cursor?: string;
}
interface KVNamespaceListKey<Metadata, Key extends string = string> {
name: Key;
expiration?: number;
metadata?: Metadata;
}
interface KVNamespaceGetWithMetadataResult<Value, Metadata> {
value: Value | null;
metadata: Metadata | null;
cacheStatus: string | null;
}
interface Queue<Body = unknown> {
send(message: Body, options?: QueueSendOptions): Promise<void>;
sendBatch(messages: Iterable<MessageSendRequest<Body>>): Promise<void>;
}
interface QueueSendOptions {
contentType?: QueueContentType;
delaySeconds?: number;
}
type QueueContentType = 'text' | 'bytes' | 'json' | 'v8';
interface MessageSendRequest<Body = unknown> {
body: Body;
options?: QueueSendOptions;
}
interface Message<Body = unknown> {
readonly id: string;
readonly timestamp: Date;
readonly body: Body;
readonly attempts: number;
retry(options?: QueueRetryOptions): void;
ack(): void;
}
interface QueueRetryOptions {
delaySeconds?: number;
}
interface MessageBatch<Body = unknown> {
readonly messages: readonly Message<Body>[];
readonly queue: string;
retryAll(options?: QueueRetryOptions): void;
ackAll(): void;
}
interface ImagesBinding {
info(stream: ReadableStream<Uint8Array>): Promise<ImageMetadata>;
input(stream: ReadableStream<Uint8Array>): ImageTransformer;
}
interface ImageMetadata {
format: string;
fileSize: number;
width: number;
height: number;
}
interface ImageTransformer {
transform(transform: ImageTransform): ImageTransformer;
output(options: ImageOutputOptions): Promise<ImageTransformationResult>;
}
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<Uint8Array>;
response(): Response;
}