Skip to content

Commit 7c537d8

Browse files
Merge pull request #9 from engineeredsoftware/v28/gate-4-read-need-fit-settlement
Gate 4: staged Read-Need Fit settlement
2 parents cafbf93 + d543636 commit 7c537d8

19 files changed

Lines changed: 1268 additions & 30 deletions

.github/workflows/bitcode-gate-quality.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ jobs:
6060
pnpm --filter @bitcode/pipeline-asset-pack exec jest --config jest.config.cjs --passWithNoTests --forceExit
6161
pnpm --filter @bitcode/pipeline-hosts exec jest --config jest.config.cjs --passWithNoTests --forceExit
6262
63+
- name: Test staged Reading route and Terminal harness contracts
64+
run: |
65+
pnpm --dir uapi exec jest --runTestsByPath \
66+
tests/api/readReviewRoute.test.ts \
67+
tests/api/pipelineHarnessRoute.test.ts \
68+
tests/terminalPipelineHarnessClient.test.ts \
69+
tests/terminalDepositReadWorkbench.test.ts \
70+
tests/pipelineExecutionLogHeader.test.tsx \
71+
--runInBand
72+
6373
- name: Test protocol demonstration
6474
run: npm --prefix protocol-demonstration run test:v28-mvp-qa
6575

.github/workflows/v28-canon-promotion.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ jobs:
7474
pnpm --filter @bitcode/pipeline-hosts typecheck
7575
pnpm --filter @bitcode/pipeline-asset-pack exec jest --config jest.config.cjs --passWithNoTests --forceExit
7676
pnpm --filter @bitcode/pipeline-hosts exec jest --config jest.config.cjs --passWithNoTests --forceExit
77+
pnpm --dir uapi exec jest --runTestsByPath \
78+
tests/api/readReviewRoute.test.ts \
79+
tests/api/pipelineHarnessRoute.test.ts \
80+
tests/terminalPipelineHarnessClient.test.ts \
81+
tests/terminalDepositReadWorkbench.test.ts \
82+
tests/pipelineExecutionLogHeader.test.tsx \
83+
--runInBand
7784
git diff --check
7885
7986
- name: Promote BITCODE_SPEC pointer

BITCODE_SPEC_V28.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,27 @@ fail-closed: if the Need measurement, Fit measurement, fee schedule, wallet
875875
authorization, BTC fee, BTD range, or ledger readback is missing, the Terminal
876876
shows blocked readiness rather than implying settlement.
877877

878+
Gate 4 implementation state:
879+
880+
- `/api/read-review` exposes server-side Read-Need synthesis, resynthesis, and
881+
acceptance actions while preserving the prior Read review boundary. The
882+
response stores prompt input, interpolated source context, parsed Need,
883+
measurement root, review state, feedback, and synthesis telemetry.
884+
- Terminal live Need-Fit execution requires an accepted `bitcode.read.need`
885+
object in addition to Deposit and admitted-Read activity ids. The live
886+
harness request carries `acceptedReadNeed` and
887+
`requireAcceptedReadNeed=true`, so depository candidate recall cannot run
888+
from a raw Read request.
889+
- The AssetPack pipeline package owns the source-safe preview contract:
890+
preview id, AssetPack id, Need root, Fit roots, selected candidate ids, score
891+
band, disclosure policy, access policy id/hash, deterministic Share-to-Fee
892+
quote root, BTD range projection, settlement boundary, and locked/unlocked
893+
read-right state.
894+
- The Vercel Sandbox harness writes that source-safe preview into the exported
895+
evidence artifact before ledger settlement readback. Terminal can stream the
896+
preview, fee quote, range projection, access state, ledger state, and
897+
pull-request target without exposing protected source before settlement.
898+
878899
### Long-running full-profile pipeline gate
879900

880901
The current V28 route-streaming gate uses

BITCODE_V28_QA.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,25 @@ a synchronous route, raw-prompt Read, or source-leaking preview model.
13841384
- `buildShareToFeePreview` provides the initial source-safe quote shape from
13851385
accepted Need measurement vector and admitted Fit quality:
13861386
`sum(measurement.weight * measurement.volume * admitted_fit_quality)`.
1387+
- `/api/read-review` now supports `synthesize_read_need`,
1388+
`resynthesize_read_need`, and `accept_read_need` actions. The synthesis
1389+
response includes prompt input, interpolated source context, parsed Need,
1390+
measurement root, review state, feedback, and telemetry; the acceptance
1391+
response returns the only admissible `acceptedReadNeed` for Need-Fit search.
1392+
- Terminal live harness requests now include `acceptedReadNeed` and
1393+
`requireAcceptedReadNeed=true`. The live fit button remains blocked until a
1394+
typed accepted Need is present, even when Deposit and admitted-Read activity
1395+
rows exist.
1396+
- `buildAssetPackSourceSafePreview` records preview id, AssetPack id, Need/Fit
1397+
roots, selected candidate ids, score band, deterministic Share-to-Fee quote
1398+
root, BTD range projection, disclosure policy, access policy id/hash,
1399+
settlement boundary, and read-right state. The Vercel Sandbox harness exports
1400+
this object into evidence for Terminal stream readback.
1401+
- Focused validation added for this gate:
1402+
`pnpm --filter @bitcode/pipeline-asset-pack exec jest --config jest.config.cjs --runTestsByPath src/__tests__/read-need.test.ts --runInBand`,
1403+
`pnpm --filter @bitcode/pipeline-hosts exec jest --config jest.config.cjs --runTestsByPath src/__tests__/asset-pack-harness.test.ts --runInBand`,
1404+
and
1405+
`pnpm --dir uapi exec jest --runTestsByPath tests/api/readReviewRoute.test.ts tests/api/pipelineHarnessRoute.test.ts tests/terminalPipelineHarnessClient.test.ts tests/terminalDepositReadWorkbench.test.ts tests/pipelineExecutionLogHeader.test.tsx --runInBand`.
13871406
13881407
Observed staging-testnet harness evidence on 2026-05-17:
13891408

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ describe('asset-pack sandbox harness plan', () => {
162162
expect(diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)).toEqual([]);
163163
expect(source).toContain('pipeline-stream-event');
164164
expect(source).toContain('synthesizeReadNeedForPipelineInput');
165+
expect(source).toContain('buildAssetPackSourceSafePreview');
165166
expect(source).toContain('acceptedReadNeed: readNeed');
166167
expect(source).toContain('requireAcceptedReadNeed');
167168
expect(source).toContain('artifact-streaming-enabled');
@@ -180,6 +181,8 @@ describe('asset-pack sandbox harness plan', () => {
180181
expect(source).toContain('toolOutputPresent');
181182
expect(source).toContain('toolErrorPresent');
182183
expect(source).toContain('Pipeline produced ');
184+
expect(source).toContain('sourceSafePreview,');
185+
expect(source).toContain("execution.store('asset-pack/preview', 'feeQuote'");
183186
expect(source).toContain('ledgerSettlement,');
184187
});
185188
});

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,7 @@ try {
14991499
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
15001500
manifestRoot = createHash('sha256').update(JSON.stringify(manifest)).digest('hex');
15011501
userId = process.env.BITCODE_PIPELINE_USER_ID || manifest.deposit?.userId || DEFAULT_USER_ID;
1502-
const [{ assetPackPipeline, acceptReadNeed, isAcceptedReadNeed, synthesizeReadNeedForPipelineInput }, { enablePipelineStreaming, factoryPipelineExecution }] = await Promise.all([
1502+
const [{ assetPackPipeline, acceptReadNeed, buildAssetPackSourceSafePreview, isAcceptedReadNeed, synthesizeReadNeedForPipelineInput }, { enablePipelineStreaming, factoryPipelineExecution }] = await Promise.all([
15031503
import('../../packages/pipelines/asset-pack/src/index'),
15041504
import('../../packages/pipelines-generics/src/index'),
15051505
]);
@@ -1609,6 +1609,16 @@ try {
16091609
resultState = manifest.sourceOverlay ? 'blocked_readiness' : settlementResultState;
16101610
const fitResult = output?.fitResult || output?.fit || null;
16111611
const depositorySearch = output?.depositorySearch || null;
1612+
const sourceSafePreview = buildAssetPackSourceSafePreview({
1613+
need: readNeed,
1614+
fitResult,
1615+
assetPackId: output?.assetPack?.assetPackId || output?.assetPackId || null,
1616+
proofRoot: output?.assetPack?.proofRoot || output?.proofRoot || null,
1617+
sourceManifestRoot: output?.assetPack?.sourceManifestRoot || output?.sourceManifestRoot || null,
1618+
pullRequestTarget: pullRequestUrl || null,
1619+
});
1620+
execution.store('asset-pack/preview', 'sourceSafe', sourceSafePreview);
1621+
execution.store('asset-pack/preview', 'feeQuote', sourceSafePreview.feeQuote);
16121622
const pipelineResultReasons = Array.isArray(fitResult?.resultReasons)
16131623
? fitResult.resultReasons
16141624
: Array.isArray(depositorySearch?.resultReasons)
@@ -1643,6 +1653,7 @@ try {
16431653
const ledgerSettlement = await settleAssetPackLedger(settlementResultState);
16441654
output = {
16451655
...(output || {}),
1656+
sourceSafePreview,
16461657
ledgerSettlement,
16471658
};
16481659
resultReasons.push(ledgerSettlement.reason);
@@ -1671,6 +1682,7 @@ try {
16711682
output,
16721683
fitResult,
16731684
depositorySearch,
1685+
sourceSafePreview,
16741686
assetPackSynthesisArtifacts: output?.assetPackSynthesisArtifacts || null,
16751687
writtenAssets: output?.writtenAssets || null,
16761688
deliveryMechanism: output?.deliveryMechanism || null,

packages/pipelines/asset-pack/src/__tests__/read-need.test.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import {
22
acceptReadNeed,
33
admitNeedFitSearch,
4+
buildAssetPackSourceSafePreview,
45
buildShareToFeePreview,
56
isAcceptedReadNeed,
67
readNeedToDepositorySearchRead,
8+
resolveAssetPackReadRightState,
79
synthesizeReadNeedForPipelineInput,
810
} from '../read-need';
9-
import { runDepositorySearchForPipelineInput } from '../depository-search';
11+
import {
12+
buildDepositoryFitResultEvidence,
13+
runDepositorySearchForPipelineInput,
14+
} from '../depository-search';
1015

1116
const input = {
1217
read: {
@@ -136,6 +141,86 @@ describe('Read-Need synthesis and Need-Fit admission', () => {
136141
finalityState: 'preview_not_paid',
137142
payer: 'reader',
138143
});
144+
expect(preview.feeSchedule).toMatchObject({
145+
satsPerWeightedVolume: 1000,
146+
minimumSats: 546,
147+
dustFloorSats: 546,
148+
networkFeePosture: 'reader_wallet_authorized_before_broadcast',
149+
});
150+
expect(preview.measurementVector).toBe(acceptedReadNeed.pricingMeasurementInputs.measurementVector);
151+
expect(preview.quoteRoot).toMatch(/^sha256:/);
139152
expect(preview.sats).toBeGreaterThanOrEqual(546);
140153
});
154+
155+
it('builds a source-safe AssetPack preview without unlocking protected source', async () => {
156+
const acceptedReadNeed = acceptReadNeed(
157+
synthesizeReadNeedForPipelineInput(input),
158+
'2026-05-18T00:00:00.000Z'
159+
);
160+
const search = await runDepositorySearchForPipelineInput({
161+
...input,
162+
acceptedReadNeed,
163+
requireAcceptedReadNeed: true,
164+
depositoryAssets: [depositoryAsset()],
165+
});
166+
const fitResult = buildDepositoryFitResultEvidence(search);
167+
const preview = buildAssetPackSourceSafePreview({
168+
need: acceptedReadNeed,
169+
fitResult,
170+
rangeStart: 42,
171+
pullRequestTarget: 'https://github.com/engineeredsoftware/ENGI/pull/6',
172+
createdAt: '2026-05-18T00:00:00.000Z',
173+
});
174+
175+
expect(preview).toMatchObject({
176+
schema: 'bitcode.asset-pack.source-safe-preview',
177+
need: {
178+
needId: acceptedReadNeed.needId,
179+
measurementRoot: acceptedReadNeed.measurementRoot,
180+
reviewState: 'accepted',
181+
},
182+
fit: {
183+
resultState: 'worthy_fit',
184+
selectedCandidateAssetIds: ['deposit-asset-1'],
185+
},
186+
disclosurePolicy: {
187+
protectedSourceDisclosure: 'forbidden_before_settlement',
188+
},
189+
settlementBoundary: {
190+
payer: 'reader',
191+
payee: 'depositor',
192+
serverCustody: false,
193+
ownershipTruth: 'asset_pack_range_and_license_rows',
194+
},
195+
unlock: {
196+
state: 'pending_settlement',
197+
sourceAvailable: false,
198+
},
199+
delivery: {
200+
pullRequestTarget: 'https://github.com/engineeredsoftware/ENGI/pull/6',
201+
availableAfterSettlement: true,
202+
},
203+
});
204+
expect(preview.roots.previewRoot).toMatch(/^sha256:/);
205+
expect(preview.roots.sourceManifestRoot).toMatch(/^sha256:/);
206+
expect(preview.feeQuote.quoteRoot).toMatch(/^sha256:/);
207+
expect(preview.rangeProjection).toMatchObject({
208+
status: 'projected_not_minted',
209+
rangeStart: 42,
210+
});
211+
expect(preview.rangeProjection.rangeEndExclusive).toBe(
212+
preview.rangeProjection.rangeStart! + preview.rangeProjection.tokenCount
213+
);
214+
expect(preview.disclosurePolicy.withheldBeforeSettlement).toContain('protected source content');
215+
});
216+
217+
it('keeps owner, licensed, pending-settlement, and denied read rights distinct', () => {
218+
expect(resolveAssetPackReadRightState({ hasOwnerClaim: true })).toBe('owner_read');
219+
expect(resolveAssetPackReadRightState({
220+
hasReadLicense: true,
221+
settlementReadbackComplete: true,
222+
})).toBe('licensed_read');
223+
expect(resolveAssetPackReadRightState({ settlementPending: true })).toBe('pending_settlement');
224+
expect(resolveAssetPackReadRightState({ policyDenied: true })).toBe('denied');
225+
});
141226
});

packages/pipelines/asset-pack/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,21 @@ export { AssetPackSynthesizeArtifactsAgent } from './agents/implementation/asset
277277
export {
278278
acceptReadNeed,
279279
admitNeedFitSearch,
280+
buildAssetPackSourceSafePreview,
280281
buildShareToFeePreview,
281282
isAcceptedReadNeed,
282283
readNeedToDepositorySearchRead,
284+
resolveAssetPackReadRightState,
283285
resolveReadNeedFromPipelineInput,
284286
shouldRequireAcceptedReadNeed,
285287
synthesizeReadNeedForPipelineInput,
288+
type AssetPackReadRightState,
289+
type AssetPackSourceSafePreview,
286290
type ReadNeed,
287291
type ReadNeedFitAdmission,
288292
type ReadNeedMeasurementDimension,
289293
type ReadNeedReviewState,
294+
type ShareToFeeQuote,
290295
} from './read-need';
291296
export default assetPackPipeline;
292297
export const runSDIVFPipeline = assetPackPipeline;

0 commit comments

Comments
 (0)