Skip to content

Commit 3764427

Browse files
V48 Gate 3 (implementation-only): deposit provisions the full checkout via the Host (retire the GitHub-API sample inventory)
The deposit harness run now provisions the FULL repository checkout on the primitive Host and builds the inventory from it — the measurement reads real full source, not 10 API samples. - uapi/lib/deposit-source-provisioning.ts: resolveDepositPipelineHost() (InlineHost in-process; the Vercel Sandbox host in prod) + provisionDepositSourceInventory() — provisions via the Host, reads every tracked file verbatim (readWorkspaceSources → sources), derives bounded prompt excerpts (samples), disposes the workspace. - route: replaced buildSourceInventory (git/trees + ≤10×1600 contents API) with host provisioning at harness-run start (the host clones, not the dispatching request); removed the GitHub-API helpers + sample constants. Emits "Provisioning … on the <host> host" / "Checkout ready: N files (full source measured)". - asset-packs-synthesis.ts: AssetPacksSynthesisSourceFile + inventory.sources (full verbatim content); applyExclusionsToInventory filters sources fail-closed (protected-IP paths never measured). - deposit-validation-agent.ts: the absolutes measurement reads inventory.sources (full content) so covered files are measured from real content; samples remain the fallback. - Tests: provisionDepositSourceInventory (injected fake host — full read, bounded samples, dispose); the route test mocks the Host provisioning (full inventory, exclusions filter both paths AND sources); jest moduleNameMapper + testMatch updated for the new lib + tests/lib. asset-pack 45 suites/227, uapi deposit provisioning+route+client 8/8, tsc clean (asset-pack + uapi). Inline host is correct in dev today; prod rides the Vercel Sandbox host (standing loose end). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ead4f13 commit 3764427

7 files changed

Lines changed: 271 additions & 116 deletions

File tree

packages/pipelines/asset-pack/src/agents/validation/deposit-validation-agent.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,19 @@ export default async function runDepositValidationAgent(input: any, execution: a
301301
// Per pack in parallel so wall-clock ≈ one measurement; each LLM call is bounded
302302
// by the F25 per-call timeout.
303303
if (packs.length > 0) {
304-
// The available source for static analysis: the inventory SAMPLES (real,
305-
// truncated source). Mapped to {path, content}; the static-analysis tool
306-
// measures density from these and applies it to each pack's covered file set.
307-
const inventorySources = Array.isArray((inventory as any)?.samples)
308-
? (inventory as any).samples
309-
.filter((s: any) => s && typeof s.path === 'string' && typeof s.excerpt === 'string')
310-
.map((s: any) => ({ path: s.path as string, content: s.excerpt as string }))
311-
: [];
304+
// The source for static analysis: the FULL checkout content (inventory.sources —
305+
// every tracked file, verbatim, provisioned by the Host) so the covered files are
306+
// measured from real content; fall back to the bounded samples when only those
307+
// are present (e.g. a pre-Host inventory).
308+
const inventorySources = Array.isArray((inventory as any)?.sources)
309+
? (inventory as any).sources
310+
.filter((s: any) => s && typeof s.path === 'string' && typeof s.content === 'string')
311+
.map((s: any) => ({ path: s.path as string, content: s.content as string }))
312+
: Array.isArray((inventory as any)?.samples)
313+
? (inventory as any).samples
314+
.filter((s: any) => s && typeof s.path === 'string' && typeof s.excerpt === 'string')
315+
.map((s: any) => ({ path: s.path as string, content: s.excerpt as string }))
316+
: [];
312317
await Promise.all(
313318
packs.map(async (pack: any) => {
314319
try {

packages/pipelines/asset-pack/src/asset-packs-synthesis.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,22 @@ export interface AssetPacksSynthesisSourceSample {
206206
excerpt: string;
207207
}
208208

209+
/** A full-content source file from the host checkout (every blob, verbatim). */
210+
export interface AssetPacksSynthesisSourceFile {
211+
path: string;
212+
content: string;
213+
}
214+
209215
export interface AssetPacksSynthesisSourceInventory {
210216
paths: string[];
211217
samples: AssetPacksSynthesisSourceSample[];
218+
/**
219+
* The FULL verbatim source of the host checkout (V48 Gate 3) — every tracked
220+
* file's content, provisioned by the primitive Host. Feeds measurement (the
221+
* static-analysis tool reads real full content); the bounded `samples` feed
222+
* prompts. Optional for back-compat (absent when only samples were available).
223+
*/
224+
sources?: AssetPacksSynthesisSourceFile[];
212225
totalPathCount: number;
213226
excludedPathCount: number;
214227
}
@@ -334,12 +347,22 @@ export function isPathExcluded(path: string, exclusions: string[]): boolean {
334347
}
335348

336349
export function applyExclusionsToInventory(
337-
inventory: { paths: string[]; samples: AssetPacksSynthesisSourceSample[] },
350+
inventory: {
351+
paths: string[];
352+
samples: AssetPacksSynthesisSourceSample[];
353+
sources?: AssetPacksSynthesisSourceFile[];
354+
},
338355
exclusions: string[],
339356
): AssetPacksSynthesisSourceInventory {
340357
const keptPaths = inventory.paths.filter((path) => !isPathExcluded(path, exclusions));
341358
const keptSamples = inventory.samples.filter((sample) => !isPathExcluded(sample.path, exclusions));
359+
// Protected-IP exclusions are honored on the FULL source too (fail-closed): an
360+
// excluded file is never measured, sampled, or otherwise carried forward.
361+
const keptSources = Array.isArray(inventory.sources)
362+
? inventory.sources.filter((file) => !isPathExcluded(file.path, exclusions))
363+
: undefined;
342364
return {
365+
...(keptSources ? { sources: keptSources } : {}),
343366
paths: keptPaths,
344367
samples: keptSamples,
345368
totalPathCount: inventory.paths.length,

uapi/app/api/deposit/synthesize-options/route.ts

Lines changed: 19 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
import { synthesizeAssetPacksPipeline } from '@bitcode/pipeline-asset-pack';
1717
import { buildRealDepositAssetPackOptionSynthesis } from '@bitcode/pipeline-asset-pack/deposit-option-real-synthesis';
1818
import { isAssetPackRealInferenceEnabled } from '@bitcode/pipeline-asset-pack/runtime-inference-policy';
19+
import {
20+
provisionDepositSourceInventory,
21+
resolveDepositPipelineHost,
22+
} from '@/lib/deposit-source-provisioning';
1923
import {
2024
bitcodeServerTelemetry,
2125
compactBitcodeServerId,
@@ -24,9 +28,6 @@ import {
2428
export const runtime = 'nodejs';
2529
export const maxDuration = 300;
2630

27-
const MAX_INVENTORY_PATHS = 400;
28-
const MAX_SAMPLE_FILES = 10;
29-
const MAX_SAMPLE_CHARS = 1600;
3031
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3132

3233
type SynthesizeOptionsBody = {
@@ -69,78 +70,6 @@ function readSignals(value: unknown) {
6970
}));
7071
}
7172

72-
const SAMPLE_PRIORITY_PATTERNS = [
73-
/^readme/i,
74-
/^package\.json$/i,
75-
/^pyproject\.toml$/i,
76-
/^cargo\.toml$/i,
77-
/^go\.mod$/i,
78-
/^setup\.(py|cfg)$/i,
79-
/^requirements.*\.txt$/i,
80-
];
81-
82-
function pickSamplePaths(paths: string[]): string[] {
83-
const prioritized = paths.filter((path) =>
84-
SAMPLE_PRIORITY_PATTERNS.some((pattern) => pattern.test(path.split('/').pop() || '')),
85-
);
86-
const sourceLike = paths.filter(
87-
(path) =>
88-
!prioritized.includes(path) &&
89-
/\.(ts|tsx|js|jsx|py|rs|go|rb|java|cs|swift|sol|md)$/i.test(path) &&
90-
path.split('/').length <= 3,
91-
);
92-
return [...prioritized, ...sourceLike].slice(0, MAX_SAMPLE_FILES);
93-
}
94-
95-
async function githubJson(token: string, url: string) {
96-
const response = await fetch(url, {
97-
headers: {
98-
authorization: `Bearer ${token}`,
99-
accept: 'application/vnd.github+json',
100-
'x-github-api-version': '2022-11-28',
101-
},
102-
cache: 'no-store',
103-
});
104-
if (!response.ok) {
105-
throw new Error(`GitHub API ${response.status} for ${url.replace(/\?.*$/, '')}`);
106-
}
107-
return response.json() as Promise<Record<string, unknown>>;
108-
}
109-
110-
async function buildSourceInventory(input: {
111-
token: string;
112-
repositoryFullName: string;
113-
reference: string;
114-
}) {
115-
const tree = await githubJson(
116-
input.token,
117-
`https://api.github.com/repos/${input.repositoryFullName}/git/trees/${encodeURIComponent(input.reference)}?recursive=1`,
118-
);
119-
const entries = Array.isArray(tree.tree) ? (tree.tree as Array<Record<string, unknown>>) : [];
120-
const paths = entries
121-
.filter((entry) => entry.type === 'blob' && typeof entry.path === 'string')
122-
.map((entry) => entry.path as string)
123-
.slice(0, MAX_INVENTORY_PATHS);
124-
125-
const samples: Array<{ path: string; excerpt: string }> = [];
126-
for (const path of pickSamplePaths(paths)) {
127-
try {
128-
const content = await githubJson(
129-
input.token,
130-
`https://api.github.com/repos/${input.repositoryFullName}/contents/${encodeURI(path)}?ref=${encodeURIComponent(input.reference)}`,
131-
);
132-
if (typeof content.content === 'string' && content.encoding === 'base64') {
133-
const text = Buffer.from(content.content, 'base64').toString('utf8');
134-
samples.push({ path, excerpt: text.slice(0, MAX_SAMPLE_CHARS) });
135-
}
136-
} catch {
137-
// A missing or oversized sample never blocks synthesis.
138-
}
139-
}
140-
141-
return { paths, samples, truncated: entries.length > MAX_INVENTORY_PATHS };
142-
}
143-
14473
export async function POST(request: Request) {
14574
const supabase = await createClient();
14675
const {
@@ -274,15 +203,24 @@ export async function POST(request: Request) {
274203
supabaseAdmin,
275204
);
276205
const reference = sourceCommit || sourceBranch || 'HEAD';
277-
await emitStatus(`Building source inventory from ${repositoryFullName}@${reference}…`);
278-
const rawInventory = await buildSourceInventory({
279-
token: auth.accessToken,
206+
// Provision the FULL repository checkout on the primitive Host (InlineHost
207+
// in-process; the Vercel Sandbox host in prod). The static-analysis measurement
208+
// reads the real full source (inventory.sources); bounded samples feed the
209+
// prompts. This is the host's job — the dispatching request does not clone.
210+
const host = resolveDepositPipelineHost();
211+
await emitStatus(
212+
`Provisioning ${repositoryFullName}@${reference} on the ${host.capabilities.hostKind} host…`,
213+
);
214+
const provisioned = await provisionDepositSourceInventory({
215+
host,
280216
repositoryFullName,
281-
reference,
217+
url: `https://github.com/${repositoryFullName}.git`,
218+
revision: reference,
219+
token: auth.accessToken,
282220
});
283-
const inventory = applyExclusionsToInventory(rawInventory, protectedIpExclusions);
221+
const inventory = applyExclusionsToInventory(provisioned, protectedIpExclusions);
284222
await emitStatus(
285-
`Inventory ready: ${inventory.paths.length} paths (${inventory.excludedPathCount} withheld by ${protectedIpExclusions.length} protected-IP exclusions; ${inventory.samples.length} excerpts sampled).`,
223+
`Checkout ready: ${inventory.paths.length} files (${inventory.excludedPathCount} withheld by ${protectedIpExclusions.length} protected-IP exclusions; full source measured, ${inventory.samples.length} prompt excerpts).`,
286224
);
287225

288226
const DEPOSIT_OPTION_KINDS = ['capability-slice', 'implementation-pattern', 'proof-operations-slice'];

uapi/jest.config.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ module.exports = {
8484
'^@/lib/bitcoin-wallet-client$': '<rootDir>/lib/bitcoin-wallet-client.ts',
8585
'^@/lib/github-app-url$': '<rootDir>/lib/github-app-url.ts',
8686
'^@/lib/bitcode-server-telemetry$': '<rootDir>/lib/bitcode-server-telemetry.ts',
87+
'^@/lib/deposit-source-provisioning$': '<rootDir>/lib/deposit-source-provisioning.ts',
8788
'^@/lib/bitcode-qa-telemetry$': '<rootDir>/lib/bitcode-qa-telemetry.ts',
8889
'^@/lib/bitcode-wallet-local$': '<rootDir>/lib/bitcode-wallet-local.ts',
8990
'^@/lib/supabase-auth-redirect$': '<rootDir>/lib/supabase-auth-redirect.ts',
@@ -108,6 +109,8 @@ module.exports = {
108109
// Include API integration tests for AssetPack and Shippable routes
109110
'<rootDir>/tests/api/**/*.test.ts',
110111
'<rootDir>/tests/api/**/*.test.tsx',
112+
// Library unit tests (deposit source provisioning, …)
113+
'<rootDir>/tests/lib/**/*.test.ts',
111114
'<rootDir>/tests/webhookRoute.test.ts',
112115
// Include mock system tests
113116
'<rootDir>/tests/MockOrchestrator.test.ts',
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Deposit source provisioning (V48 Gate 3).
3+
*
4+
* The deposit harness run provisions the FULL repository checkout on the primitive
5+
* Host (InlineHost in-process here; the Vercel Sandbox host in prod), then builds the
6+
* synthesis inventory FROM the checkout — every tracked file's verbatim content for
7+
* measurement (`sources`), plus bounded representative excerpts for the prompts
8+
* (`samples`). This retires the GitHub-API sample stopgap; the same `{path, content}`
9+
* shape works on either Host implementation.
10+
*
11+
* Host selection: InlineHost is valid only where the runtime has git + a filesystem
12+
* (the dev persistent Node server, NOT a serverless function). Prod deposit runs on
13+
* the Vercel Sandbox host (the standing host loose end); when wired,
14+
* resolveDepositPipelineHost returns it.
15+
*/
16+
17+
import {
18+
InlineHost,
19+
readWorkspaceSources,
20+
type BitcodePipelineHost,
21+
type HostSourceFile,
22+
} from '@bitcode/pipeline-hosts';
23+
24+
export interface ProvisionedDepositInventory {
25+
paths: string[];
26+
samples: { path: string; excerpt: string }[];
27+
sources: HostSourceFile[];
28+
truncated: boolean;
29+
}
30+
31+
const SAMPLE_PRIORITY_PATTERNS = [
32+
/^readme/i,
33+
/^package\.json$/i,
34+
/^pyproject\.toml$/i,
35+
/^cargo\.toml$/i,
36+
/^go\.mod$/i,
37+
/^setup\.(py|cfg)$/i,
38+
/^requirements.*\.txt$/i,
39+
];
40+
const MAX_SAMPLE_FILES = 24;
41+
const MAX_SAMPLE_CHARS = 4000;
42+
43+
/** Bounded prompt excerpts derived from the real checkout (manifests/README + shallow source). */
44+
function pickSamples(sources: HostSourceFile[]): { path: string; excerpt: string }[] {
45+
const byPath = new Map(sources.map((file) => [file.path, file.content]));
46+
const allPaths = sources.map((file) => file.path);
47+
const prioritized = allPaths.filter((path) =>
48+
SAMPLE_PRIORITY_PATTERNS.some((pattern) => pattern.test(path.split('/').pop() || '')),
49+
);
50+
const sourceLike = allPaths.filter(
51+
(path) =>
52+
!prioritized.includes(path) &&
53+
/\.(ts|tsx|js|jsx|py|rs|go|rb|java|cs|swift|sol|md)$/i.test(path) &&
54+
path.split('/').length <= 3,
55+
);
56+
return [...prioritized, ...sourceLike]
57+
.slice(0, MAX_SAMPLE_FILES)
58+
.map((path) => ({ path, excerpt: (byPath.get(path) || '').slice(0, MAX_SAMPLE_CHARS) }));
59+
}
60+
61+
/**
62+
* Resolve the deposit pipeline Host. In-process (InlineHost) today; the Vercel
63+
* Sandbox host in prod once wired. The deposit run is the harness run, so the host
64+
* provisions the source — the dispatching request does not.
65+
*/
66+
export function resolveDepositPipelineHost(): BitcodePipelineHost {
67+
return new InlineHost();
68+
}
69+
70+
/**
71+
* Provision the full checkout on the Host and build the deposit inventory from it.
72+
* Reads every tracked file's verbatim content (`sources`, for measurement), derives
73+
* bounded `samples` (for prompts), and disposes the workspace. The host is passed in
74+
* so callers/tests choose the implementation.
75+
*/
76+
export async function provisionDepositSourceInventory(input: {
77+
host: BitcodePipelineHost;
78+
repositoryFullName: string;
79+
url: string;
80+
revision: string;
81+
token?: string;
82+
}): Promise<ProvisionedDepositInventory> {
83+
const workspace = await input.host.provisionRepository({
84+
repositoryFullName: input.repositoryFullName,
85+
url: input.url,
86+
revision: input.revision,
87+
username: input.token ? 'x-access-token' : undefined,
88+
password: input.token,
89+
});
90+
try {
91+
// Every tracked file, verbatim — the full source the static-analysis measurement reads.
92+
const sources = await readWorkspaceSources(workspace);
93+
return {
94+
paths: sources.map((file) => file.path),
95+
samples: pickSamples(sources),
96+
sources,
97+
truncated: false,
98+
};
99+
} finally {
100+
await workspace.dispose();
101+
}
102+
}

0 commit comments

Comments
 (0)