Skip to content

Commit a239496

Browse files
Implement staged Read Need-Fit boundary
1 parent 6e83901 commit a239496

13 files changed

Lines changed: 793 additions & 8 deletions

File tree

BITCODE_SPEC_V28.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,20 @@ Settlement cannot proceed unless the previewed AssetPack id, fee quote, BTD
782782
range projection, wallet authorization, BTC fee transaction, ownership boundary,
783783
journal entry, and ledger/database readback all agree.
784784

785+
The product pipeline now carries a typed `bitcode.read.need` object before
786+
Need-Fit search. The Need object contains `needId`, `measurementRoot`,
787+
requirements, closure criteria, failure modes, target artifact kinds, source
788+
constraints, proof expectations, pricing measurement inputs, review state, and
789+
feedback history. Strict Need-Fit execution is admitted only when
790+
`acceptedReadNeed` is present and has `reviewState='accepted'`; otherwise the
791+
pipeline returns `blocked_readiness` before depository candidate recall. The
792+
Vercel Sandbox harness synthesizes and accepts a Need before invoking the
793+
bounded source-bound AssetPack pipeline so the closed staging-testnet evidence
794+
path remains runnable while Terminal moves to the separate user review step.
795+
`BITCODE_PIPELINE_REQUIRE_ACCEPTED_READ_NEED=1` or request-level
796+
`requireAcceptedReadNeed=true` activates the strict boundary for API and route
797+
callers.
798+
785799
The protocol demonstration carries only the minimal deterministic witness of
786800
this path: local Need synthesis, explicit Need acceptance, local Need-Fit
787801
ranking over fixture deposits, source-safe preview, and deterministic fee-quote

BITCODE_V28_QA.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,28 @@ a synchronous route, raw-prompt Read, or source-leaking preview model.
13631363
| Settle and unlock | Deterministic Share-to-Fee and BTD settlement. | Price is derived from `sum(measurement_weight * measurement_volume * admitted_fit_quality)` and the staging fee schedule. Reader pays BTC fee, BTD range/ownership/license/journal/anchor rows are written and read back, then full AssetPack source/right surface is unlocked. |
13641364
| Full-profile async pipeline | Run the full PTRR profile for long-running audits. | Vercel Sandbox execution may run for dozens of minutes and must push completion artifacts to a server-side stream/socket handler or durable queue; the push is run-id correlated, authenticated, idempotent, and durable before sandbox stop. Terminal can reattach and read final state without the starter route waiting. |
13651365
1366+
2026-05-18 implementation start for the staged Reading gate:
1367+
1368+
- `@bitcode/pipeline-asset-pack` now owns a typed `bitcode.read.need`
1369+
contract with `needId`, `measurementRoot`, requirements, closure criteria,
1370+
failure modes, target artifact kinds, source constraints, proof
1371+
expectations, pricing measurement inputs, review state, and feedback
1372+
history.
1373+
- Strict Need-Fit search blocks before depository candidate recall unless an
1374+
accepted Need is present. The blocked state is explicit
1375+
`blocked_readiness` with `accepted_read_need_missing`.
1376+
- The depository search path consumes accepted Need source constraints and
1377+
measurement roots as the Fit search input, rather than raw prompt text, when
1378+
`acceptedReadNeed` is supplied.
1379+
- The Vercel Sandbox harness now lists Need stages in the manifest and
1380+
synthesizes plus accepts a Need before invoking the existing source-bound
1381+
AssetPack pipeline. This preserves the current proven staging-testnet path
1382+
while Terminal is split into user-visible Need review and Need-Fit execution
1383+
steps.
1384+
- `buildShareToFeePreview` provides the initial source-safe quote shape from
1385+
accepted Need measurement vector and admitted Fit quality:
1386+
`sum(measurement.weight * measurement.volume * admitted_fit_quality)`.
1387+
13661388
Observed staging-testnet harness evidence on 2026-05-17:
13671389
13681390
- Vercel Sandbox run `sbx_ktb5Z6VnP5A16m9k4a0FkBcJg1d3` completed all six

packages/pipeline-hosts/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ helpers used only by the harness are installed under `.bitcode/pipeline-harness`
4444
so historical deposited source revisions do not need to carry newer harness
4545
dependencies.
4646

47+
The harness manifest includes the staged Reading boundary. The active stage
48+
sequence begins with Need synthesis, Need review, and Need-Fit search before
49+
candidate ranking and AssetPack synthesis. The live runner synthesizes a
50+
typed `bitcode.read.need` object from the Read request, source revision, and
51+
Deposit context, accepts it for the current harness invocation, and passes it
52+
to the AssetPack pipeline as `acceptedReadNeed` with
53+
`requireAcceptedReadNeed=true`. Product routes that already have a user-reviewed
54+
Need should pass that accepted object directly; if strict Need-Fit execution is
55+
requested without an accepted Need, candidate recall must return
56+
`blocked_readiness` before searching the depository.
57+
4758
Structured database telemetry is part of the harness contract. A real Read/Fit
4859
pipeline run must write the deliverable hierarchy:
4960
`deliverable_pipeline_runs`, `deliverable_pipeline_events`,

packages/pipeline-hosts/src/__tests__/asset-pack-harness.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ describe('asset-pack sandbox harness plan', () => {
161161

162162
expect(diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)).toEqual([]);
163163
expect(source).toContain('pipeline-stream-event');
164+
expect(source).toContain('synthesizeReadNeedForPipelineInput');
165+
expect(source).toContain('acceptedReadNeed: readNeed');
166+
expect(source).toContain('requireAcceptedReadNeed');
164167
expect(source).toContain('artifact-streaming-enabled');
165168
expect(source).toContain('execution: execution ? summarizeExecution(execution) : null');
166169
expect(source).toContain('PipelineHarnessTimeoutError');

packages/pipeline-hosts/src/__tests__/manifest.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ describe('pipeline harness manifest', () => {
3232
expect(manifest.host.isolationBoundary).toBe('firecracker-microvm');
3333
expect(manifest.host.defaultWorkingDirectory).toBe('/vercel/sandbox');
3434
expect(manifest.stages).toEqual(ASSET_PACK_HARNESS_STAGES);
35+
expect(manifest.stages).toEqual(
36+
expect.arrayContaining(['need-synthesis', 'need-review', 'need-fit-search'])
37+
);
38+
expect(manifest.requireAcceptedReadNeed).toBe(true);
3539
expect(manifest.expectedEvidenceTables).toEqual(ASSET_PACK_HARNESS_EVIDENCE_TABLES);
3640
expect(manifest.resultStates).toEqual([
3741
'worthy_fit',

packages/pipeline-hosts/src/asset-pack-harness.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const SANDBOX_PNPM_VERSION = '10.33.0';
3434
export interface BuildAssetPackSandboxHarnessOptions {
3535
mode?: PipelineHarnessMode;
3636
read: PipelineReadRequest;
37+
readNeed?: unknown;
3738
deposit: PipelineDepositReference;
3839
sourceRevision: PipelineSourceRevision;
3940
source?: PipelineSandboxSource;
@@ -80,6 +81,8 @@ export function buildAssetPackSandboxHarness(
8081
const manifest = buildAssetPackPipelineHarnessManifest({
8182
mode,
8283
read: options.read,
84+
readNeed: options.readNeed,
85+
requireAcceptedReadNeed: true,
8386
deposit,
8487
sourceRevision: options.sourceRevision,
8588
sourceOverlay,
@@ -1496,7 +1499,7 @@ try {
14961499
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
14971500
manifestRoot = createHash('sha256').update(JSON.stringify(manifest)).digest('hex');
14981501
userId = process.env.BITCODE_PIPELINE_USER_ID || manifest.deposit?.userId || DEFAULT_USER_ID;
1499-
const [{ assetPackPipeline }, { enablePipelineStreaming, factoryPipelineExecution }] = await Promise.all([
1502+
const [{ assetPackPipeline, acceptReadNeed, isAcceptedReadNeed, synthesizeReadNeedForPipelineInput }, { enablePipelineStreaming, factoryPipelineExecution }] = await Promise.all([
15001503
import('../../packages/pipelines/asset-pack/src/index'),
15011504
import('../../packages/pipelines-generics/src/index'),
15021505
]);
@@ -1540,9 +1543,29 @@ try {
15401543
record({ type: 'artifact-streaming-enabled', stage: 'telemetry-readback' });
15411544
}
15421545
1546+
const readNeed = isAcceptedReadNeed(manifest.readNeed)
1547+
? manifest.readNeed
1548+
: acceptReadNeed(synthesizeReadNeedForPipelineInput({
1549+
read: manifest.read,
1550+
readRequest: manifest.read,
1551+
sourceRevision: manifest.sourceRevision,
1552+
repository: {
1553+
fullName: manifest.sourceRevision.repositoryFullName,
1554+
branch: manifest.sourceRevision.branch,
1555+
commit: manifest.sourceRevision.commit,
1556+
},
1557+
}));
1558+
execution.store('read/need', 'accepted', readNeed);
1559+
execution.store('read/need', 'needId', readNeed.needId);
1560+
execution.store('read/need', 'measurementRoot', readNeed.measurementRoot);
1561+
execution.store('read/need', 'reviewState', readNeed.reviewState);
1562+
15431563
const input = {
15441564
read: manifest.read.prompt,
15451565
definitionOfRead: manifest.read.prompt,
1566+
readNeed,
1567+
acceptedReadNeed: readNeed,
1568+
requireAcceptedReadNeed: manifest.requireAcceptedReadNeed !== false,
15461569
repository: {
15471570
fullName: manifest.sourceRevision.repositoryFullName,
15481571
branch: manifest.sourceRevision.branch,

packages/pipeline-hosts/src/manifest.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ export const VERCEL_SANDBOX_HOST_CAPABILITIES: PipelineHostCapabilities = {
4949
};
5050

5151
export const ASSET_PACK_HARNESS_STAGES: readonly PipelineHarnessStage[] = [
52+
'need-synthesis',
53+
'need-review',
54+
'need-fit-search',
5255
'deposit-search',
5356
'candidate-ranking',
5457
'read-comprehension',
5558
'asset-pack-synthesis',
59+
'source-safe-preview',
5660
'validation',
5761
'finish',
5862
'telemetry-readback',
@@ -106,6 +110,8 @@ export function buildAssetPackPipelineHarnessManifest(input: {
106110
sourceRevision: PipelineSourceRevision;
107111
sourceOverlay?: PipelineHarnessSourceOverlay;
108112
commandEnvironment?: Record<string, string | undefined>;
113+
readNeed?: unknown;
114+
requireAcceptedReadNeed?: boolean;
109115
createdAt?: string;
110116
}): PipelineHarnessManifest {
111117
assertNonEmpty(input.read.id, 'read.id');
@@ -119,6 +125,8 @@ export function buildAssetPackPipelineHarnessManifest(input: {
119125
pipelineName: 'asset-pack-read-fit',
120126
harnessMode: input.mode,
121127
read: input.read,
128+
requireAcceptedReadNeed: input.requireAcceptedReadNeed ?? true,
129+
readNeed: input.readNeed,
122130
deposit: input.deposit,
123131
sourceRevision: input.sourceRevision,
124132
sourceOverlay: input.sourceOverlay,

packages/pipeline-hosts/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ export type BitcodePipelineResultState =
1010
| 'blocked_readiness';
1111

1212
export type PipelineHarnessStage =
13+
| 'need-synthesis'
14+
| 'need-review'
15+
| 'need-fit-search'
1316
| 'deposit-search'
1417
| 'candidate-ranking'
1518
| 'read-comprehension'
1619
| 'asset-pack-synthesis'
20+
| 'source-safe-preview'
1721
| 'validation'
1822
| 'finish'
1923
| 'telemetry-readback';
@@ -93,6 +97,8 @@ export interface PipelineHarnessManifest {
9397
pipelineName: 'asset-pack-read-fit';
9498
harnessMode: PipelineHarnessMode;
9599
read: PipelineReadRequest;
100+
requireAcceptedReadNeed?: boolean;
101+
readNeed?: unknown;
96102
deposit: PipelineDepositReference;
97103
sourceRevision: PipelineSourceRevision;
98104
sourceOverlay?: PipelineHarnessSourceOverlay;

packages/pipelines/asset-pack/src/__tests__/metrics-output.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,57 @@ describe('AssetPack pipeline bring-up (setup + PTRR plan: prepare→reason)', ()
2727
try {
2828
const exec = new Execution('asset-pack:test');
2929
const inserts: any[] = [];
30+
const selectedRows = (table: string, filters: Array<[string, any]>) => {
31+
return inserts
32+
.filter((insert) => insert.table === table)
33+
.map((insert) => insert.row)
34+
.filter((row) => filters.every(([column, value]) => row?.[column] === value));
35+
};
36+
const selectBuilder = (table: string) => {
37+
const filters: Array<[string, any]> = [];
38+
const builder: any = {
39+
eq: (column: string, value: any) => {
40+
filters.push([column, value]);
41+
return builder;
42+
},
43+
order: () => builder,
44+
limit: () => builder,
45+
maybeSingle: async () => ({ data: selectedRows(table, filters).at(-1) || null }),
46+
then: (resolve: any) => Promise.resolve({ data: selectedRows(table, filters), error: null }).then(resolve),
47+
};
48+
return builder;
49+
};
50+
const updateBuilder = (table: string, patch: any) => {
51+
const filters: Array<(row: any) => boolean> = [];
52+
const apply = async () => {
53+
const rows = inserts.filter((insert) => insert.table === table).map((insert) => insert.row);
54+
for (const row of rows) {
55+
if (filters.every((filter) => filter(row))) Object.assign(row, patch);
56+
}
57+
return { data: rows.filter((row) => filters.every((filter) => filter(row))), error: null };
58+
};
59+
const builder: any = {
60+
eq: (column: string, value: any) => {
61+
filters.push((row) => row?.[column] === value);
62+
return builder;
63+
},
64+
in: (column: string, values: any[]) => {
65+
filters.push((row) => values.includes(row?.[column]));
66+
return builder;
67+
},
68+
then: (resolve: any) => apply().then(resolve),
69+
};
70+
return builder;
71+
};
3072
const supabaseStub: any = {
3173
from(table: string) {
3274
return {
3375
insert: (row: any) => {
3476
inserts.push({ table, row });
3577
return { select: (_?: any) => ({ single: () => Promise.resolve({ data: { id: 'id-' + inserts.length } }) }) } as any;
3678
},
37-
update: (_: any) => ({ eq: () => Promise.resolve({}) }),
38-
select: () => ({ eq: () => ({ order: () => ({ limit: () => ({ maybeSingle: async () => ({ data: { id: 'id-last' } } as any) }) }) }) })
79+
update: (patch: any) => updateBuilder(table, patch),
80+
select: () => selectBuilder(table),
3981
} as any;
4082
}
4183
};

0 commit comments

Comments
 (0)