From 9b986d7d5e57c8a4d9e5bbb0976c050ac2d08744 Mon Sep 17 00:00:00 2001 From: Garrett Maring Date: Mon, 20 Jul 2026 09:09:49 -0700 Subject: [PATCH 01/19] V48 Gate 3 (impl-only): Tool log titles use tool name Resolve tool constructor names from path tool:Name, stream stubs, and host telemetry; title tool-use rows as that name, not "tool". --- .../pipeline-execution-log-render-line.tsx | 34 ++++++-- .../pipeline/models/pipeline-run-activity.ts | 23 ++++-- .../uapi/lib/deposit-host-telemetry-bridge.ts | 37 ++++++++- apps/uapi/streaming/stream-parser.ts | 12 ++- .../lib/depositHostTelemetryBridge.test.ts | 28 +++++++ .../src/storage/ExecutionStreamAdapter.ts | 81 +++++++++++++++---- .../src/asset-pack-host-runners.ts | 31 ++++++- .../ExecutionPipelineToolRegistry.ts | 3 +- 8 files changed, 211 insertions(+), 38 deletions(-) diff --git a/apps/uapi/components/bitcode/pipeline/PipelineExecutionLog/pipeline-execution-log-render-line.tsx b/apps/uapi/components/bitcode/pipeline/PipelineExecutionLog/pipeline-execution-log-render-line.tsx index d79ce8629..7cf64fd44 100644 --- a/apps/uapi/components/bitcode/pipeline/PipelineExecutionLog/pipeline-execution-log-render-line.tsx +++ b/apps/uapi/components/bitcode/pipeline/PipelineExecutionLog/pipeline-execution-log-render-line.tsx @@ -155,7 +155,25 @@ export function renderLogLine( ? typeof logLine.tool === 'string' ? logLine.tool : logLine.tool.name || String(logLine.tool) - : null; + : typeof logLine.metadata?.toolName === 'string' + ? logLine.metadata.toolName + : typeof logLine.details?.data?.tool === 'string' + ? logLine.details.data.tool + : typeof logLine.details?.metadata?.toolName === 'string' + ? logLine.details.metadata.toolName + : null; + // Tool-use rows must title as the tool constructor name, never the bare word "tool". + const isGenericToolTitle = + !logLine.text || + String(logLine.text).trim().toLowerCase() === 'tool' || + String(logLine.text).trim().toLowerCase() === 'tool (failed)'; + const displayTitle = + (logLine.type === 'tool-use' || Boolean(toolLabel)) && + isGenericToolTitle && + toolLabel && + toolLabel.toLowerCase() !== 'tool' + ? toolLabel + : logLine.text; const rowMode = pipelineMode ?? (logLine.pipelineMode as SynthesisPipelineMode | undefined) ?? null; const rowIconExplainer = getTelemetryRowIconExplainer( logLine.type === 'tool-use' || logLine.tool ? 'tool' : 'llm', @@ -250,10 +268,10 @@ export function renderLogLine( /> )} - {logLine.text} + {displayTitle} {hasPills && } @@ -390,13 +408,13 @@ export function renderLogLine( {/* Desktop inline row */}
- {/* Main text */} + {/* Main text — tool-use titles prefer constructor name over bare "tool" */} - {logLine.text} + {displayTitle} {/* Meta cluster + timestamp: the pill row flows right of the title @@ -444,10 +462,10 @@ export function renderLogLine( )} - {logLine.text} + {displayTitle} {hasPills && } diff --git a/apps/uapi/components/bitcode/pipeline/models/pipeline-run-activity.ts b/apps/uapi/components/bitcode/pipeline/models/pipeline-run-activity.ts index 46d5d5394..ba195e9d3 100644 --- a/apps/uapi/components/bitcode/pipeline/models/pipeline-run-activity.ts +++ b/apps/uapi/components/bitcode/pipeline/models/pipeline-run-activity.ts @@ -531,16 +531,27 @@ export function buildPipelineRunActivityFromEvents( let resolvedToolName = ''; if (kind === 'tool') { const acc = toolByNode.get(nodeId) || {}; - const nodeToolSegment = nodeId - .split('/') + const pathSegments = Array.isArray(payload?.executionPath) + ? payload.executionPath.map((s: unknown) => String(s || '')) + : []; + const nodeToolSegment = [...pathSegments, ...nodeId.split('/')] .reverse() - .find((segment) => segment.startsWith('tool:')); - const toolNameFromNode = nodeToolSegment ? nodeToolSegment.slice('tool:'.length) : ''; + .find((segment) => String(segment).startsWith('tool:') && String(segment).length > 5); + const toolNameFromNode = nodeToolSegment + ? String(nodeToolSegment).slice('tool:'.length) + : ''; + const ownState = + payload?.executionState && typeof payload.executionState === 'object' + ? (payload.executionState as Record) + : null; resolvedToolName = String( toolNameFromNode || acc.name || - payload?.data?.tool || - payload?.metadata?.toolName || + (typeof payload?.data?.tool === 'string' && payload.data.tool) || + (typeof payload?.data?.toolName === 'string' && payload.data.toolName) || + (typeof payload?.metadata?.toolName === 'string' && payload.metadata.toolName) || + (typeof ownState?.tool === 'string' && ownState.tool) || + (typeof payload?.tool === 'string' && payload.tool) || (key === 'error' ? 'tool (failed)' : 'tool'), ); } diff --git a/apps/uapi/lib/deposit-host-telemetry-bridge.ts b/apps/uapi/lib/deposit-host-telemetry-bridge.ts index f7cdfeeaf..ba953c126 100644 --- a/apps/uapi/lib/deposit-host-telemetry-bridge.ts +++ b/apps/uapi/lib/deposit-host-telemetry-bridge.ts @@ -84,16 +84,47 @@ export function bridgeHostTelemetryArtifactToExecutionStream( ((ns === 'tool' || ns === 'tools') && (key === 'result' || key === 'error')) ) { const data = asRecord(te.data) || {}; + // Prefer explicit tool fields, then tool:Name on path/node id (pipeline tools). + let toolName = + readString(te.tool) || + readString(data.tool) || + readString(data.toolName) || + readString((te as { toolId?: unknown }).toolId) || + ''; + if (!toolName && Array.isArray(path)) { + for (let i = path.length - 1; i >= 0; i -= 1) { + const segment = String(path[i] || ''); + const leaf = segment.includes('/') + ? segment.split('/').filter(Boolean).pop() || segment + : segment; + if (leaf.startsWith('tool:') && leaf.length > 5) { + toolName = leaf.slice(5); + break; + } + } + } + if (!toolName && nodeId) { + const leaf = nodeId.includes('/') + ? nodeId.split('/').filter(Boolean).pop() || nodeId + : nodeId; + if (leaf.startsWith('tool:') && leaf.length > 5) toolName = leaf.slice(5); + } + const title = toolName || (key === 'error' ? 'tool (failed)' : 'tool'); void ExecutionStreamAdapter.emitEvent(executionId, 'tool-use' as never, { namespace: ns || 'tool', key: key || 'result', - message, - executionState: executionState || undefined, + // Product log title is the tool constructor name, not the word "tool". + message: title, + executionState: { + ...(executionState || {}), + ...(toolName ? { tool: toolName } : {}), + }, executionPath: path, executionNodeId: nodeId, + metadata: toolName ? { toolName } : undefined, data: { ...data, - tool: readString(te.tool) || data.tool || null, + tool: toolName || data.tool || null, ok: typeof te.toolOk === 'boolean' ? te.toolOk : data.ok, contentWithheld: true, sourceSafetyClass: 'source_safe', diff --git a/apps/uapi/streaming/stream-parser.ts b/apps/uapi/streaming/stream-parser.ts index 3a9498cbe..e8cdc0c22 100644 --- a/apps/uapi/streaming/stream-parser.ts +++ b/apps/uapi/streaming/stream-parser.ts @@ -230,10 +230,18 @@ export const parseStreamChunk = (chunk: string): ParsedStreamData => { // Tool usage log: show tool name and context const ctx = data.executionState; const tag = formatExecutionTag(ctx); - const toolName = data.metadata?.toolName || 'tool'; + const toolName = + data.metadata?.toolName || + data.data?.tool || + data.executionState?.tool || + data.message || + 'tool'; parsedData.text += `🛠 Tool Use: ${tag}${toolName}${data.detail ? ` (${toSingleLine(data.detail)})` : ''}\n`; parsedData.type = 'tool-use'; - parsedData.executionState = ctx; + parsedData.executionState = { + ...(ctx && typeof ctx === 'object' ? ctx : {}), + ...(toolName && toolName !== 'tool' ? { tool: toolName } : {}), + }; if (data.metadata) (parsedData as any).metadata = data.metadata; break; } diff --git a/apps/uapi/tests/lib/depositHostTelemetryBridge.test.ts b/apps/uapi/tests/lib/depositHostTelemetryBridge.test.ts index a7e2ca860..308a81f80 100644 --- a/apps/uapi/tests/lib/depositHostTelemetryBridge.test.ts +++ b/apps/uapi/tests/lib/depositHostTelemetryBridge.test.ts @@ -63,7 +63,35 @@ describe('bridgeHostTelemetryArtifactToExecutionStream', () => { expect.objectContaining({ namespace: 'tool', key: 'result', + message: 'asset-pack-clone-vcs-repository-tool', data: expect.objectContaining({ tool: 'asset-pack-clone-vcs-repository-tool' }), + executionState: expect.objectContaining({ + tool: 'asset-pack-clone-vcs-repository-tool', + }), + }), + ); + }); + + it('resolves tool title from tool:Name on executionPath when tool field is absent', () => { + const ok = bridgeHostTelemetryArtifactToExecutionStream('exec-1', { + type: 'pipeline-stream-event', + streamEventType: 'tool-use', + namespace: 'tool', + key: 'result', + executionPath: [ + 'pipeline:synthesize_deposit_asset_packs', + 'finish:finish-synthesize-asset-packs-for-deposit-run', + 'tool:AssetPackPatchWriteTool', + ], + executionState: { phase: 'finish', agent: 'finish-synthesize', step: 'try' }, + }); + expect(ok).toBe(true); + expect(emitEvent).toHaveBeenCalledWith( + 'exec-1', + 'tool-use', + expect.objectContaining({ + message: 'AssetPackPatchWriteTool', + data: expect.objectContaining({ tool: 'AssetPackPatchWriteTool' }), }), ); }); diff --git a/packages/execution-generics/src/storage/ExecutionStreamAdapter.ts b/packages/execution-generics/src/storage/ExecutionStreamAdapter.ts index 06aec1a5b..e334b1aaa 100644 --- a/packages/execution-generics/src/storage/ExecutionStreamAdapter.ts +++ b/packages/execution-generics/src/storage/ExecutionStreamAdapter.ts @@ -232,6 +232,7 @@ export class ExecutionStreamAdapter { // input/output when already redacted) so structured deliverable rows and // sourceSafeStreamEvent can still attribute the call without leaking // verbatim args/results. + const toolStub = this.extractToolMetadataStub(namespace, value, nodeInfo); const streamData = contentBearing ? { contentWithheld: true, @@ -240,9 +241,25 @@ export class ExecutionStreamAdapter { namespace, contentChars: this.estimateSerializedChars(value), ...this.extractExecutionState(value), - ...this.extractToolMetadataStub(namespace, value), + ...toolStub, } - : this.sanitizeData(value); + : (() => { + const sanitized = this.sanitizeData(value); + // Surface tool name on non-content stores (tool/name string) for UI titles. + if (toolStub.tool && (typeof sanitized !== 'object' || sanitized === null)) { + return { tool: toolStub.tool, value: sanitized }; + } + if ( + toolStub.tool && + sanitized && + typeof sanitized === 'object' && + !Array.isArray(sanitized) && + !(sanitized as Record).tool + ) { + return { ...(sanitized as Record), tool: toolStub.tool }; + } + return sanitized; + })(); // Build stream message const message = { @@ -293,10 +310,13 @@ export class ExecutionStreamAdapter { if (key === 'complete') return ExecutionStreamEventType.AGENT_COMPLETE; } - // Tool usage: prefer 'result' as primary event; treat 'invocation' as status - if (namespace === 'tools') { - if (key === 'result') return ExecutionStreamEventType.TOOL_USE; - if (key === 'invocation') return ExecutionStreamEventType.STATUS; + // Tool usage: prefer 'result' as primary event; treat 'invocation'/'name' as status. + // Pipeline tools store under namespace `tool` (singular); agent path uses `tools`. + if (namespace === 'tools' || namespace === 'tool') { + if (key === 'result' || key === 'error') return ExecutionStreamEventType.TOOL_USE; + if (key === 'invocation' || key === 'name' || key === 'input') { + return ExecutionStreamEventType.STATUS; + } return ExecutionStreamEventType.STATUS; } @@ -359,12 +379,31 @@ export class ExecutionStreamAdapter { * Tool metadata that may ride on a content-withheld stream stub. * Prefer already shape-redacted input/output; otherwise omit payloads. */ + /** Prefer `tool:ToolName` segment on the execution path / node id. */ + private static toolNameFromNodeInfo(nodeInfo?: { + nodeId?: string; + path?: string[]; + }): string { + const segments = [ + ...(Array.isArray(nodeInfo?.path) ? nodeInfo!.path! : []), + typeof nodeInfo?.nodeId === 'string' ? nodeInfo.nodeId : '', + ]; + for (let i = segments.length - 1; i >= 0; i -= 1) { + const segment = String(segments[i] || ''); + const leaf = segment.includes('/') + ? segment.split('/').filter(Boolean).pop() || segment + : segment; + if (leaf.startsWith('tool:') && leaf.length > 5) return leaf.slice(5); + } + return ''; + } + private static extractToolMetadataStub( namespace: string, value: any, + nodeInfo?: { nodeId?: string; path?: string[] }, ): Record { if (namespace !== 'tools' && namespace !== 'tool') return {}; - if (!value || typeof value !== 'object') return {}; const isShapeOnly = (payload: unknown): payload is Record => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false; @@ -377,17 +416,25 @@ export class ExecutionStreamAdapter { }; const stub: Record = {}; - if (typeof value.tool === 'string') stub.tool = value.tool; - if (typeof value.ok === 'boolean') stub.ok = value.ok; - if (isShapeOnly(value.input)) stub.input = value.input; - if (isShapeOnly(value.output)) stub.output = value.output; - if (value.error != null && typeof value.error !== 'object') { - stub.error = String(value.error); - } else if (value.error && typeof value.error === 'object') { - const err = value.error as Record; - stub.error = - typeof err.message === 'string' ? { message: err.message } : { message: 'tool error' }; + const fromNode = this.toolNameFromNodeInfo(nodeInfo); + if (typeof value === 'string' && value.trim() && (namespace === 'tool' || namespace === 'tools')) { + // `tool`/`name` stores are plain strings of the constructor name. + if (!stub.tool) stub.tool = value.trim(); + } + if (value && typeof value === 'object' && !Array.isArray(value)) { + if (typeof value.tool === 'string') stub.tool = value.tool; + if (typeof value.ok === 'boolean') stub.ok = value.ok; + if (isShapeOnly(value.input)) stub.input = value.input; + if (isShapeOnly(value.output)) stub.output = value.output; + if (value.error != null && typeof value.error !== 'object') { + stub.error = String(value.error); + } else if (value.error && typeof value.error === 'object') { + const err = value.error as Record; + stub.error = + typeof err.message === 'string' ? { message: err.message } : { message: 'tool error' }; + } } + if (!stub.tool && fromNode) stub.tool = fromNode; return stub; } diff --git a/packages/pipeline-hosts/src/asset-pack-host-runners.ts b/packages/pipeline-hosts/src/asset-pack-host-runners.ts index f578e5fd3..00adb3e06 100644 --- a/packages/pipeline-hosts/src/asset-pack-host-runners.ts +++ b/packages/pipeline-hosts/src/asset-pack-host-runners.ts @@ -319,6 +319,22 @@ function summarizeExecutionNode(node, depth = 0) { }; } +/** Resolve tool constructor name from path / node id (`tool:Name` segment). */ +function toolNameFromExecutionPath(event) { + const segments = [ + ...(Array.isArray(event?.executionPath) ? event.executionPath : []), + typeof event?.executionNodeId === 'string' ? event.executionNodeId : '', + ]; + for (let i = segments.length - 1; i >= 0; i -= 1) { + const segment = String(segments[i] || ''); + const leaf = segment.includes('/') + ? segment.split('/').filter(Boolean).pop() || segment + : segment; + if (leaf.startsWith('tool:') && leaf.length > 5) return leaf.slice(5); + } + return ''; +} + function summarizeStreamEvent(event) { const data = event?.data && typeof event.data === 'object' && !Array.isArray(event.data) ? event.data @@ -342,6 +358,18 @@ function summarizeStreamEvent(event) { }, }) : null; + const toolFromPath = toolNameFromExecutionPath(event); + const toolFromData = + (data && typeof data.tool === 'string' && data.tool) || + (typeof event?.data === 'string' && event.namespace === 'tool' && event.key === 'name' + ? event.data + : null) || + null; + const toolName = + toolFromData || + (readingPipelineTelemetry?.toolId ? String(readingPipelineTelemetry.toolId) : null) || + toolFromPath || + null; return { type: 'pipeline-stream-event', stage: stageForStreamEvent(event), @@ -349,10 +377,11 @@ function summarizeStreamEvent(event) { namespace: event?.namespace || null, key: event?.key || null, executionPath: Array.isArray(event?.executionPath) ? event.executionPath : [], + executionNodeId: event?.executionNodeId || null, executionState: event?.executionState || null, message: event?.message || null, dataKeys: data ? Object.keys(data).sort() : [], - tool: data?.tool ? String(data.tool) : null, + tool: toolName, toolOk: typeof data?.ok === 'boolean' ? data.ok : null, toolInputPresent: Boolean(data?.input), toolOutputPresent: Boolean(data?.output), diff --git a/packages/pipelines-generics/src/execution/ExecutionPipelineToolRegistry.ts b/packages/pipelines-generics/src/execution/ExecutionPipelineToolRegistry.ts index 4f922bff2..8d759a955 100644 --- a/packages/pipelines-generics/src/execution/ExecutionPipelineToolRegistry.ts +++ b/packages/pipelines-generics/src/execution/ExecutionPipelineToolRegistry.ts @@ -57,7 +57,8 @@ export abstract class ExecutionTool any = (...args try { const result = await this.use(...args); - // Track success + // Track success (raw result; tool name rides on the node id `tool:Name` + // and is lifted onto stream stubs by ExecutionStreamAdapter). toolExec.store('tool', 'result', result); toolExec.store('tool', 'status', 'success'); toolExec.store('tool', 'endTime', Date.now()); From 7437a217519378a3cc6f66011a3634b34e8c6f8c Mon Sep 17 00:00:00 2001 From: Garrett Maring Date: Mon, 20 Jul 2026 09:17:12 -0700 Subject: [PATCH 02/19] V48 Gate 3 (impl-only): Fix tool-name comment parse Drop raw backticks around tool:Name in a comment inside the embedded live pipeline runner template so SWC/Vercel can parse the host file. --- packages/pipeline-hosts/src/asset-pack-host-runners.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-hosts/src/asset-pack-host-runners.ts b/packages/pipeline-hosts/src/asset-pack-host-runners.ts index 00adb3e06..eff1c0f88 100644 --- a/packages/pipeline-hosts/src/asset-pack-host-runners.ts +++ b/packages/pipeline-hosts/src/asset-pack-host-runners.ts @@ -319,7 +319,9 @@ function summarizeExecutionNode(node, depth = 0) { }; } -/** Resolve tool constructor name from path / node id (`tool:Name` segment). */ +// Resolve tool constructor name from path / node id (tool:Name segment). +// Note: this file embeds runners in template strings — never use raw backticks +// in comments inside createLiveAssetPackPipelineRunner. function toolNameFromExecutionPath(event) { const segments = [ ...(Array.isArray(event?.executionPath) ? event.executionPath : []), From cb438b1465c009114966a5ff8786dd860e62eeec Mon Sep 17 00:00:00 2001 From: Garrett Maring Date: Mon, 20 Jul 2026 09:22:33 -0700 Subject: [PATCH 03/19] V48 Gate 4 (spec-impl): Admit packs + absolutes Finish depositor batch admit and packs measurement projection: per-option ledger rows with absolute catalog chips, no session candidate/admitted counts on pack detail, soft compensation no longer drops confirmed deposits, deposit synthesis reload rehydrates options, patchfile download on deposit/read review only (never network packs). Gate 4/5 plans recorded in NOTES/QA. --- .qa/BITCODE_V48_QA.md | 8 + .specifications/BITCODE_SPEC_V48_NOTES.md | 71 ++++++++ .../PackActivityModel/pack-activity-model.ts | 128 ++++++++----- .../DepositOptionCard/DepositOptionCard.tsx | 56 +++++- .../hooks/use-deposit-option-actions.ts | 168 ++++++++++-------- .../hooks/use-deposit-synthesis-lifecycle.ts | 92 ++++++++-- .../models/deposit-admission-activity.ts | 144 +++++++++++++++ .../reads/ReadsOptionCard/ReadsOptionCard.tsx | 131 ++++++++++---- apps/uapi/jest.config.cjs | 1 + .../tests/depositAdmissionActivity.test.ts | 160 +++++++++++++++++ apps/uapi/tests/packActivityModel.test.ts | 33 +++- ...eposit-asset-pack-option-admission.test.ts | 40 +++++ ...sit-asset-pack-option-admission-helpers.ts | 37 +++- 13 files changed, 902 insertions(+), 167 deletions(-) create mode 100644 apps/uapi/components/deposits/models/deposit-admission-activity.ts create mode 100644 apps/uapi/tests/depositAdmissionActivity.test.ts diff --git a/.qa/BITCODE_V48_QA.md b/.qa/BITCODE_V48_QA.md index 2c3c84547..3c6fdd3ca 100644 --- a/.qa/BITCODE_V48_QA.md +++ b/.qa/BITCODE_V48_QA.md @@ -8,6 +8,14 @@ with rebuild-alone canon in `BITCODE_SPEC_V48.md` §G3-1…G3-15. Optional live smoke remains in the Gate 3 runbook §6; it does not block version-branch merge when CI is green. +**Gate 4 open** on `v48/gate-4-depositor-packs-finalization`: depositor website +completion (batch admit → `/packs`, deposit detail reload, absolute +measurements on packs, patchfile download on deposit review only). Live defect +class (2026-07-20): selecting 2 options produced 1 packs row; packs detail +showed session `Candidate count` / `Admitted count` / admission-report root as +fake measurements; reloading synthesis run +`e2f1f110-b6b6-43ce-a7b9-a3b80bcf891a` dropped options/logs. + - Version: `V48` - Active canon during QA: `V47` diff --git a/.specifications/BITCODE_SPEC_V48_NOTES.md b/.specifications/BITCODE_SPEC_V48_NOTES.md index e2ae5f2cc..308341e95 100644 --- a/.specifications/BITCODE_SPEC_V48_NOTES.md +++ b/.specifications/BITCODE_SPEC_V48_NOTES.md @@ -1206,6 +1206,77 @@ option projection); real in-sandbox execution is verified against deployed sandb These ops items do **not** block Gate 3 PR merge into `version/v48` once CI is green. +## V48 Gate 4 open: Depositor website completion + Packs absolute measurements + +**Branch:** `v48/gate-4-depositor-packs-finalization` +**Canon surface:** Gate 4 completion artifact law in DELTA + depositor website +completion proofs; this NOTES entry records live finishing work after Gate 3 +deposit SDIVF closed. + +### Gate 4 product objectives (binding for finish) + +1. **Batch admit reliability** — every selected presentable option that the + depositor confirms becomes one `admitted-to-depository` ledger row and one + `/packs` network `depository-assetpack` row. Soft compensation/ROI + incompleteness must not silently drop confirmed deposits; critical source + policy remains hard-block. +2. **Per-pack admission payload** — never embed the full session admission + report (`candidateCount`, `admittedCount`, report roots) as pack + measurements. Each admitted pack carries **that option's absolute + measurements** + identity roots only. +3. **Deposit detail reload** — revisiting a synthesis run + (`transactionId=`) rehydrates options, telemetry history, and + admission state from the execution row + ledger (not only live SSE). +4. **Depositor review completeness** — option cards expose full absolutes, + roots, contents summary, and **Download patchfile** (path-op JSON + descriptor). "Admitted" UI requires receipt state + `admitted-to-depository`, not merely a review decision label. +5. **Packs source-safety** — patch/fileChanges never projected on `/packs` + network detail. Patch is visible only to the depositor on `/deposits` + (owner) or, later, entitled buyers after settle (Gate 5). Absolute + measurement chips + detail are the commercial measurement surface on + `/packs`. +6. **Then Gate 5** — Read experience parity with deposit, neediness as the + critical measurement difference. + +### Closure checklist + +- [ ] Admit N selected → N packs rows with absolute chips +- [ ] Reload deposit synthesis detail → options + logs + admitted cards +- [ ] Packs detail never shows candidate/admitted session counts as measurements +- [ ] Unit tests: admission soft-block override, pack measurement projection, + admission activity draft +- [ ] Gate 4 checker / depositor-website-completion artifact green + +## V48 Gate 5 plan: Reader website completion (neediness delta) + +**After Gate 4 packs/deposit solid.** Branch target: +`v48/gate-5-reader-website-completion`. + +### Deposit ↔ Read parity map + +| Concern | Deposit | Read (delta) | +| --- | --- | --- | +| SDIVF product package | `syntheses/deposit` | `syntheses/read` | +| Steering | Obfuscations + path pickers | Need text + Relevant/Irrelevant paths | +| Measurements | **absolutes only** | **absolutes + needinesses (*-fit)** | +| Option review | Select → batch admit | Select → settle quote | +| Finish envelope | presentable deposit options | presentable read options | +| Next pipeline | (none — Depository) | `ExecutionPipelineSimpleSettleAssetPack` | +| Packs projection | depository-assetpack + absolutes | settled-assetpack + absolutes + needinesses | + +### Gate 5 objectives + +1. Cold reload of read synthesis run rehydrates Need, options, telemetry (mirror + deposit hydrate). +2. Option cards show neediness catalog + download path-op patchfile for the + reader reviewing unpaid options (source-safe descriptor only until buy). +3. Settle path journals one packs row per settled pack with neediness + absolute + chips; no session aggregate counts as measurements. +4. Source-safety: patch never on `/packs` until rights transfer / delivery + entitlement. +5. Five-step session law from DELTA Gate 5 remains binding. + ## Non-goals during V48 opening - Do not implement V48 product behavior from this notes-only opening. diff --git a/apps/uapi/components/bitcode/activity/PackActivityModel/pack-activity-model.ts b/apps/uapi/components/bitcode/activity/PackActivityModel/pack-activity-model.ts index 486f1471d..455e78f59 100644 --- a/apps/uapi/components/bitcode/activity/PackActivityModel/pack-activity-model.ts +++ b/apps/uapi/components/bitcode/activity/PackActivityModel/pack-activity-model.ts @@ -507,23 +507,34 @@ function collectNestedKindMeasurements( } const record = asRecord(source); // Settled packActivity.measurements[] rows: { kind, category, volume, magnitude } + // Prefer magnitude+unit for absolute size properties (functions/files/types); + // fall back to volume (0..1 weighted component) when magnitude absent. if (typeof record.kind === 'string' && (record.category === 'absolute' || record.category === 'neediness')) { const kind = record.kind; const id = `${record.category}:${kind}`; if (!seen.has(id)) { seen.add(id); + const magnitude = typeof record.magnitude === 'number' ? record.magnitude : null; + const volume = typeof record.volume === 'number' ? record.volume : null; const value = - typeof record.volume === 'number' - ? record.volume - : typeof record.magnitude === 'number' - ? record.magnitude - : null; + record.category === 'absolute' && magnitude !== null + ? magnitude + : volume !== null + ? volume + : magnitude; if (value !== null) { measurements.push({ id, - label: normalizeLabel(kind), + label: typeof record.label === 'string' && record.label.trim() + ? record.label.trim() + : normalizeLabel(kind), value, - unit: typeof record.unit === 'string' ? record.unit : record.category === 'neediness' ? 'fit' : null, + unit: + typeof record.unit === 'string' + ? record.unit + : record.category === 'neediness' + ? 'fit' + : null, root: null, }); } @@ -539,12 +550,13 @@ function collectNestedKindMeasurements( const id = `absolute:${kind}`; if (seen.has(id)) continue; seen.add(id); - const value = - typeof a.volume === 'number' ? a.volume : typeof a.magnitude === 'number' ? a.magnitude : null; + const magnitude = typeof a.magnitude === 'number' ? a.magnitude : null; + const volume = typeof a.volume === 'number' ? a.volume : null; + const value = magnitude !== null ? magnitude : volume; if (value === null) continue; measurements.push({ id, - label: normalizeLabel(kind), + label: typeof a.label === 'string' && a.label.trim() ? a.label.trim() : normalizeLabel(kind), value, unit: typeof a.unit === 'string' ? a.unit : null, root: null, @@ -578,46 +590,78 @@ function collectNestedKindMeasurements( function buildMeasurements(record: BitcodeActivityRecord): PackActivityMeasurement[] { const payload = asRecord(record.payload); + const packType = inferPackActivityType(record); + const isDepositedOrSettledPack = + packType === 'depository-assetpack' || packType === 'settled-assetpack'; const measurements: PackActivityMeasurement[] = []; - const candidates: Array<[string, string[], string | null]> = [ - ['measured-btd', ['measuredBtd', 'measured_btd', 'btdVolume', 'weightedRequestedVolume'], 'BTD'], - ['token-total', ['total_tokens', 'tokenTotal', 'totalTokens'], 'tokens'], - ['duration', ['duration_ms', 'durationMs', 'runtimeMs'], 'ms'], - ['cost', ['total_cost', 'totalCost'], 'USD'], - ['candidate-count', ['candidateCount', 'fitCandidateCount', 'targetKindCount', 'optionCount'], 'count'], - ['admitted-count', ['admittedCount'], 'count'], - ['closure-criteria', ['closureCriteriaCount', 'closureCount'], 'count'], - ]; + // Absolute / neediness kind rows first (commercial material properties). + collectNestedKindMeasurements(payload, measurements); - for (const [id, keys, unit] of candidates) { - const value = findFirstNumber(payload, keys); - if (value !== null) { - measurements.push({ id, label: normalizeLabel(id), value, unit, root: null }); + // Session-aggregate counters are NOT pack measurements. A deposited AssetPack + // is one pack; "candidate count" / "admitted count" describe a synthesis + // session and must not appear on /packs detail for depository rows. + if (!isDepositedOrSettledPack) { + const sessionCandidates: Array<[string, string[], string | null]> = [ + ['measured-btd', ['measuredBtd', 'measured_btd', 'btdVolume', 'weightedRequestedVolume'], 'BTD'], + ['token-total', ['total_tokens', 'tokenTotal', 'totalTokens'], 'tokens'], + ['duration', ['duration_ms', 'durationMs', 'runtimeMs'], 'ms'], + ['cost', ['total_cost', 'totalCost'], 'USD'], + ['candidate-count', ['candidateCount', 'fitCandidateCount', 'targetKindCount', 'optionCount'], 'count'], + ['admitted-count', ['admittedCount'], 'count'], + ['closure-criteria', ['closureCriteriaCount', 'closureCount'], 'count'], + ]; + for (const [id, keys, unit] of sessionCandidates) { + const value = findFirstNumber(payload, keys); + if (value !== null) { + measurements.push({ id, label: normalizeLabel(id), value, unit, root: null }); + } + } + } else { + // Commercial value scalar only when present (not session counts). + const measuredBtd = findFirstNumber(payload, [ + 'measuredBtd', + 'measured_btd', + 'btdVolume', + 'weightedRequestedVolume', + ]); + if (measuredBtd !== null) { + measurements.push({ + id: 'measured-btd', + label: normalizeLabel('measured-btd'), + value: measuredBtd, + unit: 'BTD', + root: null, + }); } } - collectNestedKindMeasurements(payload, measurements); - - const measurementRoot = findFirstString(payload, [ - 'measurementRoot', - 'depositMeasurementRoot', - 'assetPackMeasurementRoot', - 'readNeedMeasurementRoot', - 'admissionReportRoot', - 'admissionRoot', - ]); - if (measurementRoot) { - measurements.push({ - id: 'measurement-root', - label: 'Measurement root', - value: measurementRoot, - unit: null, - root: measurementRoot, - }); + // Prefer absolute:* rows at the front for table chips (deposit catalog order). + const absolutes = measurements.filter((m) => m.id.startsWith('absolute:')); + const rest = measurements.filter((m) => !m.id.startsWith('absolute:')); + const ordered = [...absolutes, ...rest]; + + // Measurement *root proofs* live in proofRoots — never as a measurement chip + // labeled "Measurement root" on a deposited pack (confuses with catalog). + if (!isDepositedOrSettledPack) { + const measurementRoot = findFirstString(payload, [ + 'measurementRoot', + 'depositMeasurementRoot', + 'assetPackMeasurementRoot', + 'readNeedMeasurementRoot', + ]); + if (measurementRoot) { + ordered.push({ + id: 'measurement-root', + label: 'Measurement root', + value: measurementRoot, + unit: null, + root: measurementRoot, + }); + } } - return measurements; + return ordered; } function buildValues(record: BitcodeActivityRecord): PackActivityValue[] { diff --git a/apps/uapi/components/deposits/DepositOptionCard/DepositOptionCard.tsx b/apps/uapi/components/deposits/DepositOptionCard/DepositOptionCard.tsx index e77198e84..46f391371 100644 --- a/apps/uapi/components/deposits/DepositOptionCard/DepositOptionCard.tsx +++ b/apps/uapi/components/deposits/DepositOptionCard/DepositOptionCard.tsx @@ -14,6 +14,7 @@ import type { DepositRealSynthesis, DepositRealSynthesisOption, } from "@/components/deposits/models/deposit-real-synthesis"; +import { buildDepositOptionPatchfileDownload } from "@/components/deposits/models/deposit-admission-activity"; export type DepositOptionCardProps = { option: DepositRealSynthesisOption; @@ -61,6 +62,22 @@ export function DepositOptionCard(props: DepositOptionCardProps) { const admissionReceipt = depositRouteSession.admission.receipts.find( (receipt) => receipt.optionId === option.optionId, ); + /** True only when the pack is actually in the Depository — not mere review decision. */ + const admittedToDepository = + admissionReceipt?.admission.state === "admitted-to-depository"; + const handleDownloadPatchfile = () => { + const file = buildDepositOptionPatchfileDownload(option); + const blob = new Blob([file.body], { type: file.mimeType }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = file.filename; + anchor.rel = "noopener"; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); + }; const earningStatement = depositRouteSession.earningSupplyIntelligence.earningStatements.find( (statement) => statement.optionId === option.optionId, @@ -176,6 +193,14 @@ export function DepositOptionCard(props: DepositOptionCardProps) { {projection.measurementRationale}

) : null} +
) : projection ? (
@@ -356,9 +381,17 @@ export function DepositOptionCard(props: DepositOptionCardProps) {
- Option roots + Option roots + full details
+
+
+ optionId +
+
+ {option.optionId} +
+
{Object.entries(option.roots).map(([label, value]) => (
@@ -370,15 +403,34 @@ export function DepositOptionCard(props: DepositOptionCardProps) {
))}
+ {!option.contents ? ( + + ) : null}
{/* North-star step D: select packs to deposit; one batch action admits the selected set. Archive (re-depositable) and Resynthesize are secondary. */} - {reviewDecision === "approved-for-admission" ? ( + {admittedToDepository ? (

Admitted to Depository — permanent

+ ) : reviewDecision === "approved-for-admission" && + admissionReceipt && + admissionReceipt.admission.state !== "admitted-to-depository" ? ( +

+ Not admitted + {admissionReceipt.admission.blockers.length + ? `: ${admissionReceipt.admission.blockers.join(", ")}` + : " — policy blocked."} +

) : ( <>
- ) : null} + ) : ( + + )} {paths.length > 0 ? (

@@ -135,20 +188,28 @@ export function ReadsOptionCard(props: { Absolutes