diff --git a/.bd/the-pitch.pdf b/.bd/the-pitch.pdf new file mode 100644 index 000000000..00cf53c31 Binary files /dev/null and b/.bd/the-pitch.pdf differ diff --git a/.docs/ASSET_PACKS.md b/.docs/ASSET_PACKS.md index a8eb72870..b9ab60b2c 100644 --- a/.docs/ASSET_PACKS.md +++ b/.docs/ASSET_PACKS.md @@ -526,6 +526,17 @@ Presentation envelope for UI selection: Plus `finish:completion` (cleanup posture: Host dispose owned by **dispatch** after Finish). +**Critical distinction — Finish store ≠ product close (do not regress):** + +| Layer | Scope | What happens | Stream / UI meaning | +| --- | --- | --- | --- | +| **SDIVF Finish** | Shared substrate (deposit *and* read synth) | Agents store selection envelope + `finish/completion` on the execution tree | Telemetry may show Finish / READY TO FINISH; **not** product-ready option cards | +| **Stream type inference** | **Shared** `ExecutionStreamAdapter` | Must not map store `key=completion` → terminal `completion` | Avoids early tail close for all synth pipelines | +| **Deposit dispatch close** | **Deposit only** (`dispatch-deposit-synthesis`) | Builds `depositOptionSynthesis`, **`finalizeExecutionRow`**, **then** SSE `completion` with `depositOptionsReady` + envelope | **Only then** may `/deposits` hydrate option cards | +| **Read dispatch close** | Read (mirror when implemented) | Same pattern with read envelope — do not assume deposit code covers it | `/reads` option hydrate | + +Never map execution **store** keys named `completion` (e.g. `finish`/`completion`) to stream type `completion` — that closes the client tail and hydrates before the row write (deposit false “options not found”). Terminal product completion is **route-owned** `emitEvent(..., 'completion')` after finalize, or store namespace `final` only. Deposit hydrate order: row/event payload first; history GET retry is fallback. Full map: `apps/uapi/components/deposits/README.md` § Product terminal vs stream telemetry. + ### 8.4 What is ultimately stored / shipped / journaled | Layer | What | diff --git a/.docs/BITCODE_EXECUTIONS.md b/.docs/BITCODE_EXECUTIONS.md index a9efa1888..b97bd5ba6 100644 --- a/.docs/BITCODE_EXECUTIONS.md +++ b/.docs/BITCODE_EXECUTIONS.md @@ -52,3 +52,28 @@ Operators should be able to: - Replace pre-Finish labels with Finish and delivery-mechanism labels. - Keep computer-use hidden unless the server admits it internally for Read measurement. - Ensure all execution UI reads from the same Exchange activity model as `/packs`. + +## Stream telemetry vs product terminal (synthesis runs) + +Pipeline SSE is **progress telemetry**, not the product close signal by itself. + +| Concern | Shared vs product-specific | +| --- | --- | +| SDIVF Finish stores (`finish/completion`) | Shared substrate (deposit + read) | +| `ExecutionStreamAdapter` not mapping `key=completion` → terminal | Shared stream law | +| Persist envelope + emit product `completion` | **Per product route** (deposit: `depositOptionSynthesis`; read: mirror when wired) | +| Option-card hydrate | **Per experience** (`/deposits` today; `/reads` when parity lands) | + +1. **In-band Finish** — SDIVF may store `finish/completion` while the host run is + still owned by the route dispatcher. Those stores must stream as ordinary + status/progress events, **not** as terminal `completion`. +2. **Product terminal (deposit today)** — After + `dispatch-deposit-synthesis` persists `depositOptionSynthesis`, it emits + `completion` with `depositOptionsReady: true` + envelope. That is when + `/deposits` may treat the run as closed for option cards. +3. **Hydrate order (deposit)** — Prefer `executions.output` or the product + completion payload; history GET retry is fallback only. See deposits README + and `ASSET_PACKS.md` §8.3. + +**Deposit regress symptom:** Finish / READY TO FINISH in the log, then false +“Synthesized options were not found” while the row already has options. diff --git a/.github/workflows/bitcode-gate-quality.yml b/.github/workflows/bitcode-gate-quality.yml index dc9bdf914..91858c5b4 100644 --- a/.github/workflows/bitcode-gate-quality.yml +++ b/.github/workflows/bitcode-gate-quality.yml @@ -49,9 +49,11 @@ jobs: if [[ "$PR_BASE_REF" == version/* && "$PR_HEAD_REF" =~ ^v([0-9]+)/gate-([0-9]+)- ]]; then expected_version="V${BASH_REMATCH[1]}" expected_gate="${BASH_REMATCH[2]}" - title_re="^${expected_version}[[:space:]]+Gate[[:space:]]+${expected_gate}([[:space:]]\((specification-only|implementation-only|specification-implementation)\))?:[[:space:]].+" + # AGENTS / SPECIFYING §2.8: abbreviated (spec-only|impl-only|spec-impl) + # only. Expanded forms are illegal in subjects — do not re-accept them. + title_re="^${expected_version}[[:space:]]+Gate[[:space:]]+${expected_gate}([[:space:]]\((spec-only|impl-only|spec-impl)\))?:[[:space:]].+" if [[ ! "$PR_TITLE" =~ $title_re ]]; then - echo "Gate pull request titles must begin with: ${expected_version} Gate ${expected_gate}: or ${expected_version} Gate ${expected_gate} (specification-only|implementation-only|specification-implementation):" + echo "Gate pull request titles must begin with: ${expected_version} Gate ${expected_gate}: or ${expected_version} Gate ${expected_gate} (spec-only|impl-only|spec-impl):" echo "Observed: ${PR_TITLE}" exit 1 fi diff --git a/.qa/BITCODE_V48_QA.md b/.qa/BITCODE_V48_QA.md index 2c3c84547..1dfea92ab 100644 --- a/.qa/BITCODE_V48_QA.md +++ b/.qa/BITCODE_V48_QA.md @@ -8,6 +8,40 @@ 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. + +**Live monitor (Gate 4 close):** deposit synthesis run +`e359b78a-7c73-49bb-a54b-35e32dd1ef94` on staging-testnet +(`tkpyosihuouusyaxtbau`). + +- **Terminal:** `completed` (~490s sandbox host, repo + `advancedengineeredsoftware/Bitcode` @ `version/v48`). +- **Options:** 3 presentable packs, each with **8 absolute measurements** + + contents (path-op): Documentation Capability, Proof Artifacts Operations, + Application Manifest Patterns. +- **Next operator step:** select all 3 → confirm deposit → expect **3** + `/packs` depository rows with absolute chips only (no session candidate + counts). Reload + `/deposits?transactionId=e359b78a-7c73-49bb-a54b-35e32dd1ef94` → options + + telemetry rehydrate. + +### Gate 4 follow-up fixes (post-e359 screenshots) + +- DIV header badge: phase-gated **DIV pass N** (not bare ITER on Setup/Finish). +- Packs **Kind** column = capability/pattern/operations; **BTD** column = + unsettled absolute-derived estimate (not function-count). +- Measurement detail rows carry buyer descriptors. +- Patch download = `bitcode.artifact.patch` path-op-json (`.path-op.json`). +- Pipeline master excludes admission receipts; openRunDetail redirects admit + rows to `synthesisRunId` so logs/options remain on the synthesis run. +- Policy UI: admitted packs show admitted + soft-warning copy, not active block. + - 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/mcp/docs/openapi/bitcode-mcp-openapi.json b/apps/mcp/docs/openapi/bitcode-mcp-openapi.json index fd8b8d29f..d96378167 100644 --- a/apps/mcp/docs/openapi/bitcode-mcp-openapi.json +++ b/apps/mcp/docs/openapi/bitcode-mcp-openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Bitcode MCP Server API", "version": "1.0.0", - "description": "\n# Bitcode MCP Server API\n\nThe Model Context Protocol (MCP) server exposing Bitcode’s technical knowledge exchange via standardized tools, resources, and prompts.\n\n## Features\n\n- Tools interface for AssetPack execution and optional Shippable delivery\n- Real‑time streaming (WebSocket)\n- Multi‑modal attachments (Figma, documents, images, audio, video)\n- BTC fee posture plus measured non-fungible $BTD AssetPack amount\n\n## Getting Started\n\n1. **Get API Key** in your Bitcode account\n2. **Connect an MCP Client** (e.g., Claude Desktop)\n3. **Call a Tool**: \n - \n name: `bitcode://pipelines/asset-pack/execute`\n \n## Support\n\n- **Documentation**: https://docs.bitcode.dev/mcp\n- **Community**: https://discord.gg/bitcode-mcp\n- **Support**: mcp-support@bitcode.dev\n ", + "description": "\n# Bitcode MCP Server API\n\nThe Model Context Protocol (MCP) server exposing Bitcode’s technical knowledge exchange via standardized tools, resources, and prompts.\n\n## Features\n\n- Tools interface for AssetPack execution and optional settle delivery\n- Real‑time streaming (WebSocket)\n- Multi‑modal attachments (Figma, documents, images, audio, video)\n- BTC fee posture plus measured non-fungible $BTD AssetPack amount\n\n## Getting Started\n\n1. **Get API Key** in your Bitcode account\n2. **Connect an MCP Client** (e.g., Claude Desktop)\n3. **Call a Tool**: \n - \n name: `bitcode://pipelines/asset-pack/execute`\n \n## Support\n\n- **Documentation**: https://docs.bitcode.dev/mcp\n- **Community**: https://discord.gg/bitcode-mcp\n- **Support**: mcp-support@bitcode.dev\n ", "contact": { "name": "Bitcode API Support", "url": "https://bitcode.dev/support", @@ -195,7 +195,7 @@ "url": { "type": "string", "format": "uri", - "description": "External URL for the PR-backed Shippable", + "description": "External URL for the settle PR delivery", "example": "https://github.com/bitcode-labs/webapp/pull/123" }, "content": { @@ -231,7 +231,7 @@ "enum": [ "pull_request" ], - "description": "V26 Shippable type to create" + "description": "Delivery template type to create" }, "attachments": { "type": "array", @@ -293,7 +293,7 @@ "items": { "$ref": "#/components/schemas/AssetPack" }, - "description": "Generated AssetPack results and PR Shippables" + "description": "Generated AssetPack results and settle PR deliveries" }, "metrics": { "$ref": "#/components/schemas/ExecutionMetrics" diff --git a/apps/uapi/app/api/deck/route.ts b/apps/uapi/app/api/deck/route.ts index 32dacc4ed..1f721f4f4 100644 --- a/apps/uapi/app/api/deck/route.ts +++ b/apps/uapi/app/api/deck/route.ts @@ -1,6 +1,7 @@ /** - * Serve the pitch deck from the monorepo SoT (`.bd/the-pitch.key`). + * Serve the pitch deck PDF from the monorepo SoT (`.bd/the-pitch.pdf`). * Does not copy into `public/` — one source of truth on disk. + * (Keynote source remains at `.bd/the-pitch.key` for authoring only.) */ import { createReadStream, existsSync } from 'node:fs'; @@ -10,10 +11,10 @@ import { Readable } from 'node:stream'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; -const DECK_RELATIVE = path.join('.bd', 'the-pitch.key'); -const DECK_FILENAME = 'the-pitch.key'; +const DECK_RELATIVE = path.join('.bd', 'the-pitch.pdf'); +const DECK_FILENAME = 'the-pitch.pdf'; -/** Resolve monorepo-root `.bd/the-pitch.key` from app or repo cwd. */ +/** Resolve monorepo-root `.bd/the-pitch.pdf` from app or repo cwd. */ function resolveDeckPath(): string | null { const cwd = process.cwd(); const candidates = [ @@ -42,8 +43,7 @@ export async function GET() { return new Response(webStream, { status: 200, headers: { - // Keynote packages are zip-based; keep generic so clients download cleanly. - 'Content-Type': 'application/octet-stream', + 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="${DECK_FILENAME}"`, 'Cache-Control': 'public, max-age=300', 'X-Bitcode-Deck-Source': DECK_RELATIVE, diff --git a/apps/uapi/app/api/deposit/synthesize-options/dispatch-deposit-synthesis.ts b/apps/uapi/app/api/deposit/synthesize-options/dispatch-deposit-synthesis.ts index 3684c9b00..7c4027ab1 100644 --- a/apps/uapi/app/api/deposit/synthesize-options/dispatch-deposit-synthesis.ts +++ b/apps/uapi/app/api/deposit/synthesize-options/dispatch-deposit-synthesis.ts @@ -622,11 +622,20 @@ export async function runDepositOptionSynthesis( duration_ms: durationMs, }); + // Terminal product signal AFTER row output is written. Carry the selection + // envelope on the event so the UI can render options from the same signal + // that closes the stream — no second GET race with finish/completion stores. await ExecutionStreamAdapter.emitEvent(execution.id, 'completion' as never, { message: isBudgetPartial ? `AssetPacksSynthesis partial: recovered ${synthesis.optionCount} measured options after host budget (Validation incomplete).` : `AssetPacksSynthesis completed with ${synthesis.optionCount} measured options.`, runId, + productTerminal: true, + depositOptionsReady: true, + optionCount: synthesis.optionCount, + depositOptionSynthesis: synthesis, + reviewProjections, + partial: isBudgetPartial, }); bitcodeServerTelemetry('info', 'deposit-synthesize-options', 'synthesized', { diff --git a/apps/uapi/app/api/packs/activity/route.ts b/apps/uapi/app/api/packs/activity/route.ts index 4abafa750..ce68da08f 100644 --- a/apps/uapi/app/api/packs/activity/route.ts +++ b/apps/uapi/app/api/packs/activity/route.ts @@ -105,15 +105,90 @@ async function readGlobalDepositoryRecords(limit: number): Promise>admissionState', 'admitted-to-depository') .order('created_at', { ascending: false }) .limit(Math.min(limit, 50)); - return (data || []).map((row: Record) => - buildBitcodeActivityRecordFromExecutionHistory({ + return (data || []).map((row: Record) => { + const output = (row.output as Record | null) || {}; + const context = (row.context as Record | null) || {}; + const assetPackTitle = + (typeof output.assetPackTitle === 'string' && output.assetPackTitle) || + (typeof context.assetPackTitle === 'string' && context.assetPackTitle) || + null; + const assetPackKind = + (typeof output.assetPackKind === 'string' && output.assetPackKind) || + (typeof output.optionKind === 'string' && output.optionKind) || + (typeof output.kind === 'string' && output.kind) || + (typeof context.assetPackKind === 'string' && context.assetPackKind) || + (typeof context.optionKind === 'string' && context.optionKind) || + null; + const estimatedBtd = + typeof output.estimatedBtd === 'number' + ? output.estimatedBtd + : typeof context.estimatedBtd === 'number' + ? context.estimatedBtd + : null; + const estimatedBtdCells = + typeof output.estimatedBtdCells === 'number' + ? output.estimatedBtdCells + : typeof context.estimatedBtdCells === 'number' + ? context.estimatedBtdCells + : null; + // Flatten per-pack absolute measurements onto context+output so pack + // activity projection prefers catalog chips over any residual session + // aggregates still present on older admission rows. + const measurements = + (Array.isArray(output.measurements) && output.measurements) || + (Array.isArray(output.absolutes) && output.absolutes) || + (Array.isArray(context.measurements) && context.measurements) || + []; + const summary = + typeof output.summary === 'string' + ? String(output.summary) + : assetPackTitle + ? `Admitted ${assetPackTitle} to the Depository.` + : null; + return buildBitcodeActivityRecordFromExecutionHistory({ ...row, - summary: - typeof (row.output as Record | null)?.summary === 'string' - ? String((row.output as Record).summary) - : null, - } as never), - ); + summary, + context: { + ...context, + source: 'deposit-option-review-admission', + admissionState: 'admitted-to-depository', + packActivityType: 'depository-assetpack', + activityType: 'depository-assetpack', + assetPackTitle, + assetPackKind, + optionKind: assetPackKind, + kind: assetPackKind, + optionId: context.optionId || output.optionId || null, + depositoryAssetPackId: + context.depositoryAssetPackId || output.depositoryAssetPackId || null, + compensationState: + context.compensationState || output.compensationState || null, + measurementRoot: context.measurementRoot || output.measurementRoot || null, + estimatedBtd, + estimatedBtdCells, + btdHonesty: 'estimate', + // Source-safe absolute catalog only — never patch / fileChanges. + measurements, + }, + output: { + ...output, + packActivityType: 'depository-assetpack', + assetPackTitle, + assetPackKind, + optionKind: assetPackKind, + kind: assetPackKind, + estimatedBtd, + estimatedBtdCells, + btdHonesty: 'estimate', + measurements, + // Strip session-aggregate noise if present on legacy rows. + candidateCount: undefined, + admittedCount: undefined, + optionCount: undefined, + depositAdmission: undefined, + }, + } as never); + }); } catch { return []; } 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..496562a46 100644 --- a/apps/uapi/components/bitcode/activity/PackActivityModel/pack-activity-model.ts +++ b/apps/uapi/components/bitcode/activity/PackActivityModel/pack-activity-model.ts @@ -9,6 +9,7 @@ import { toSourceSafeAssetPackCommodityStateDisplay, type AssetPackCommodityStateDisplay, } from '@bitcode/asset-packs-pipelines-domain/asset-pack-commodity-state'; +import { descriptorForAbsoluteKind } from '@/components/packs/models/packs-measurement-descriptors'; export type PackActivityType = | 'deposit-option' @@ -39,6 +40,12 @@ export interface PackActivityMeasurement { value: number | string; unit: string | null; root: string | null; + /** Buyer-facing source-safe descriptor paragraph (optional). */ + descriptor?: string | null; + /** Absolute / neediness kind id when known. */ + kind?: string | null; + weight?: number | null; + volume?: number | null; } export interface PackActivityValue { @@ -99,6 +106,14 @@ export interface PackActivityRecord { state: string | null; repository: string | null; assetPackTitle: string | null; + /** + * AssetPack product kind (capability-slice | implementation-pattern | + * proof-operations-slice) — not activity taxonomy. + */ + assetPackKind: string | null; + /** Unsettled absolute-derived BTD estimate (honesty class: estimate). */ + estimatedBtd: number | null; + estimatedBtdCells: number | null; settlementState: string | null; rightsState: string | null; compensationState: string | null; @@ -160,6 +175,9 @@ export interface PackActivityDetailProjection { scope: BitcodeActivityScope; repository: string | null; assetPackTitle: string | null; + assetPackKind: string | null; + estimatedBtd: number | null; + estimatedBtdCells: number | null; }; measurements: PackActivityMeasurement[]; values: PackActivityValue[]; @@ -176,6 +194,9 @@ export interface PackActivityDetailProjection { }; /** Live or projected PR URL for settled AssetPack delivery (source-safe). */ deliveryReference: string | null; + assetPackKind: string | null; + estimatedBtd: number | null; + estimatedBtdCells: number | null; telemetry: { sourceEventId: string; sourceKind: string | null; @@ -507,24 +528,44 @@ 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) { + const catalog = descriptorForAbsoluteKind(kind); + const explicitDescriptor = + typeof record.descriptor === 'string' && record.descriptor.trim() + ? record.descriptor.trim() + : null; measurements.push({ id, - label: normalizeLabel(kind), + label: typeof record.label === 'string' && record.label.trim() + ? record.label.trim() + : catalog?.label || 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' + : catalog?.unit || null, root: null, + kind, + weight: typeof record.weight === 'number' ? record.weight : null, + volume, + descriptor: explicitDescriptor || catalog?.descriptor || null, }); } } @@ -539,15 +580,23 @@ 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; + const catalog = descriptorForAbsoluteKind(kind); + const explicitDescriptor = + typeof a.descriptor === 'string' && a.descriptor.trim() ? a.descriptor.trim() : null; measurements.push({ id, - label: normalizeLabel(kind), + label: typeof a.label === 'string' && a.label.trim() ? a.label.trim() : catalog?.label || normalizeLabel(kind), value, - unit: typeof a.unit === 'string' ? a.unit : null, + unit: typeof a.unit === 'string' ? a.unit : catalog?.unit || null, root: null, + kind, + weight: typeof a.weight === 'number' ? a.weight : null, + volume, + descriptor: explicitDescriptor || catalog?.descriptor || null, }); } const needinesses = Array.isArray(record.needinesses) ? record.needinesses : []; @@ -578,51 +627,113 @@ 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[] { const payload = asRecord(record.payload); const values: PackActivityValue[] = []; + const packType = inferPackActivityType(record); + + // Unsettled depository packs: absolute-derived BTD estimate is the commercial value. + if (packType === 'depository-assetpack') { + const estimatedBtd = findFirstNumber(payload, [ + 'estimatedBtd', + 'estimated_btd', + 'estimatedKnowledgeVolume', + ]); + const estimatedBtdCells = findFirstNumber(payload, [ + 'estimatedBtdCells', + 'estimated_btd_cells', + ]); + if (estimatedBtd !== null) { + values.push({ + id: 'estimated-btd', + label: 'BTD (est.)', + amount: estimatedBtd, + unit: 'BTD (est.)', + }); + } else if (estimatedBtdCells !== null) { + values.push({ + id: 'estimated-btd-cells', + label: 'BTD (est.)', + amount: estimatedBtdCells, + unit: 'BTD cells (est.)', + }); + } + } + const candidates: Array<[string, string[], string]> = [ ['btc-fee', ['btcFee', 'btc_fee', 'btcFeeSats', 'feeSats'], 'sats'], ['usd-equivalent', ['btcFeeUsdEquivalent', 'usdEquivalent', 'total_cost'], 'USD'], @@ -631,6 +742,12 @@ function buildValues(record: BitcodeActivityRecord): PackActivityValue[] { ]; for (const [id, keys, unit] of candidates) { + if (values.some((v) => v.id === id || v.id.startsWith('estimated-btd'))) { + // Prefer estimated-btd for depository rows over measuredBtd fallbacks. + if (packType === 'depository-assetpack' && (id === 'btd-potential' || id === 'btc-fee')) { + continue; + } + } const value = findFirstNumber(payload, keys); if (value !== null) values.push({ id, label: normalizeLabel(id), amount: value, unit }); } @@ -638,6 +755,35 @@ function buildValues(record: BitcodeActivityRecord): PackActivityValue[] { return values; } +function inferAssetPackKind(record: BitcodeActivityRecord): string | null { + return ( + findFirstString(record.payload, [ + 'assetPackKind', + 'optionKind', + 'kind', + 'depositOptionKind', + ]) || null + ); +} + +function inferEstimatedBtd(record: BitcodeActivityRecord): { + estimatedBtd: number | null; + estimatedBtdCells: number | null; +} { + const payload = asRecord(record.payload); + return { + estimatedBtd: findFirstNumber(payload, [ + 'estimatedBtd', + 'estimated_btd', + 'estimatedKnowledgeVolume', + ]), + estimatedBtdCells: findFirstNumber(payload, [ + 'estimatedBtdCells', + 'estimated_btd_cells', + ]), + }; +} + function buildAccountingReadback(record: BitcodeActivityRecord): PackActivityAccountingReadback | null { const payload = asRecord(record.payload); const statements = findFirstRecord( @@ -804,7 +950,10 @@ export function normalizePackActivityRecord(record: BitcodeActivityRecord): Pack (commodityState.repairRequired ? 'repair-required' : null); const assetPackTitle = inferAssetPackTitle(record); + const assetPackKind = inferAssetPackKind(record); + const { estimatedBtd, estimatedBtdCells } = inferEstimatedBtd(record); // Prefer settle/deposit authored titles over generic execution-history labels. + // Prefer clean pack title (not "Depository AssetPack: Admitted …" noise). const title = (type === 'settled-assetpack' && (assetPackTitle @@ -813,7 +962,7 @@ export function normalizePackActivityRecord(record: BitcodeActivityRecord): Pack ? record.summary.split('.')[0] : null)) || (type === 'depository-assetpack' && assetPackTitle - ? `Depository AssetPack: ${assetPackTitle}` + ? assetPackTitle.replace(/^Admitted\s+/i, '').replace(/\s+to the Depository\.?$/i, '') : null) || record.title || normalizeLabel(type); @@ -828,6 +977,15 @@ export function normalizePackActivityRecord(record: BitcodeActivityRecord): Pack state: record.state || commodityState.assetPackState, repository: inferRepository(record), assetPackTitle, + assetPackKind: + assetPackKind && + (assetPackKind === 'capability-slice' || + assetPackKind === 'implementation-pattern' || + assetPackKind === 'proof-operations-slice') + ? assetPackKind + : assetPackKind, + estimatedBtd, + estimatedBtdCells, settlementState, rightsState, compensationState, @@ -1148,6 +1306,9 @@ export function buildPackActivityDetailProjection( scope: record.scope, repository: record.repository, assetPackTitle: record.assetPackTitle, + assetPackKind: record.assetPackKind, + estimatedBtd: record.estimatedBtd, + estimatedBtdCells: record.estimatedBtdCells, }, measurements: record.measurements, values: record.values, @@ -1163,6 +1324,9 @@ export function buildPackActivityDetailProjection( repair: record.repairState, }, deliveryReference: record.deliveryReference, + assetPackKind: record.assetPackKind, + estimatedBtd: record.estimatedBtd, + estimatedBtdCells: record.estimatedBtdCells, telemetry: { sourceEventId: record.id, sourceKind: String(record.metadata.kind || record.metadata.type || '') || null, diff --git a/apps/uapi/components/bitcode/branding/AssetPackMark/AssetPackMark.tsx b/apps/uapi/components/bitcode/branding/AssetPackMark/AssetPackMark.tsx new file mode 100644 index 000000000..fdcb4c957 --- /dev/null +++ b/apps/uapi/components/bitcode/branding/AssetPackMark/AssetPackMark.tsx @@ -0,0 +1,83 @@ +/** + * Bitcode AssetPack mark — technical knowledge-in-a-box. + * Monochrome (currentColor) for emerald product chrome and marketing glows. + * + * Geometry: isometric vault + lattice nodes (measured pack) + code brackets. + */ + +import React from 'react'; + +type AssetPackMarkProps = { + className?: string; + /** Tailwind size classes, e.g. h-7 w-7 */ + height?: string; + width?: string; + /** Accessible name when used as standalone image; omit when parent is aria-hidden */ + title?: string | null; +}; + +export default function AssetPackMark({ + className = '', + height = 'h-8', + width = 'w-8', + title = 'AssetPack', +}: AssetPackMarkProps) { + const decorative = title == null || title === ''; + return ( + + {!decorative ? {title} : null} + {/* Outer isometric vault shell */} + + {/* Shell edges */} + + {/* Mid belt (pack seal) */} + + {/* Front face knowledge plate */} + + {/* Lattice: measured knowledge nodes */} + + + + + {/* Code brackets — technical knowledge cue */} + + + ); +} diff --git a/apps/uapi/components/bitcode/layout/BitcodePublicCopy/bitcode-public-copy.ts b/apps/uapi/components/bitcode/layout/BitcodePublicCopy/bitcode-public-copy.ts index c95a2353f..d5deb0851 100644 --- a/apps/uapi/components/bitcode/layout/BitcodePublicCopy/bitcode-public-copy.ts +++ b/apps/uapi/components/bitcode/layout/BitcodePublicCopy/bitcode-public-copy.ts @@ -83,7 +83,8 @@ export const BITCODE_PUBLIC_COPY = { // Claim anchors in meaning: * ERC-1155 · ** Measured · *** AssetPacks (footnotes). testnetLaunch: { badge: 'Productionized Protocol', - title: 'Exchange Bitcodes for Bitcoins.', + // Marketing exchange strip: Code ⇄ Coin (AssetPacks/BTD ↔ multi-chain money). + title: 'Exchange Code for Coin.', meaning: "Bitcode's canonical, commercial deployments are its mainnet ERC-1155* tokens, tradable Measured** AssetPacks*** artifacts, and delightful user applications.", flow: [ diff --git a/apps/uapi/components/bitcode/layout/NavBrand/NavBrand.tsx b/apps/uapi/components/bitcode/layout/NavBrand/NavBrand.tsx index 0386a1148..021b14eb7 100644 --- a/apps/uapi/components/bitcode/layout/NavBrand/NavBrand.tsx +++ b/apps/uapi/components/bitcode/layout/NavBrand/NavBrand.tsx @@ -49,7 +49,7 @@ export const BITCODE_X_URL = const DOCS_HREF = "/docs"; /** - * Pitch deck — served from monorepo SoT `.bd/the-pitch.key` via API (no public copy). + * Pitch deck — PDF from monorepo SoT `.bd/the-pitch.pdf` via API (no public copy). */ export const BITCODE_DECK_HREF = "/api/deck"; 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/components/deposits/DepositOptionCard/DepositOptionCard.tsx b/apps/uapi/components/deposits/DepositOptionCard/DepositOptionCard.tsx index e77198e84..a7c5f7e2b 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 ? (
@@ -202,8 +227,17 @@ export function DepositOptionCard(props: DepositOptionCardProps) { Policy
- {policyEvaluation.policyDecision} + {admittedToDepository + ? "admitted-to-depository" + : policyEvaluation.policyDecision}
+ {admittedToDepository && + policyEvaluation.policyDecision === "blocked-before-admission" ? ( +

+ Depositor confirmed deposit. Soft policy signals (ROI / demand / + compensation) remain estimates — not active blocks. +

+ ) : null}
@@ -356,9 +390,17 @@ export function DepositOptionCard(props: DepositOptionCardProps) {
- Option roots + Option roots + full details
+
+
+ optionId +
+
+ {option.optionId} +
+
{Object.entries(option.roots).map(([label, value]) => (
@@ -370,15 +412,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

    - {absolutes.map((row) => ( -
  • - {row.measurementKind || row.label} - - v={typeof row.volume === "number" ? row.volume.toFixed(2) : "—"} - {typeof row.magnitude === "number" - ? ` · m=${row.magnitude}` - : ""} - -
  • - ))} + {absolutes.map((row) => { + const key = row.measurementKind || row.kind || row.label || "absolute"; + return ( +
  • + {row.label || row.measurementKind || row.kind} + + {typeof row.magnitude === "number" + ? `${row.magnitude}${row.unit ? ` ${row.unit}` : ""}` + : typeof row.volume === "number" + ? row.volume.toFixed(2) + : "—"} + {typeof row.magnitude === "number" && + typeof row.volume === "number" + ? ` · ${(row.volume * 100).toFixed(0)}%` + : ""} + +
  • + ); + })} {absolutes.length === 0 ? (
  • None attached
  • ) : null} @@ -156,22 +217,30 @@ export function ReadsOptionCard(props: {

- Needinesses (*-fit) + Needinesses (*-fit) — reader-relative

    - {needinesses.map((row) => ( -
  • - {row.measurementKind || row.label} - - v={typeof row.volume === "number" ? row.volume.toFixed(2) : "—"} - -
  • - ))} + {needinesses.map((row) => { + const key = + row.measurementKind || row.kind || row.label || "neediness"; + return ( +
  • + {row.label || row.measurementKind || row.kind} + + {typeof row.volume === "number" + ? `${(row.volume * 100).toFixed(0)}% fit` + : "—"} + +
  • + ); + })} {needinesses.length === 0 ? ( -
  • None attached
  • +
  • + None attached — neediness is the Read measurement delta +
  • ) : null}
diff --git a/apps/uapi/jest.config.cjs b/apps/uapi/jest.config.cjs index 45d79c3f1..da04f49fd 100644 --- a/apps/uapi/jest.config.cjs +++ b/apps/uapi/jest.config.cjs @@ -194,6 +194,8 @@ module.exports = { '/tests/workspaceSurface.test.ts', '/tests/packsPageClient.test.tsx', '/tests/packActivityModel.test.ts', + '/tests/depositAdmissionActivity.test.ts', + '/tests/depositSynthesisOptionsHydrate.test.ts', '/tests/depositRouteModel.test.ts', '/tests/depositPageClient.test.tsx', '/tests/depositSourceSelection.test.tsx', 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/next.config.mjs b/apps/uapi/next.config.mjs index 01f349e54..d79e6ce07 100644 --- a/apps/uapi/next.config.mjs +++ b/apps/uapi/next.config.mjs @@ -84,10 +84,10 @@ let nextConfig = { // Monorepo root so workspace packages outside apps/uapi trace correctly // without falling back to over-broad project trees on Vercel. outputFileTracingRoot: path.join(__dirname, '../..'), - // Pitch deck SoT lives at monorepo `.bd/the-pitch.key` (not public/). + // Pitch deck PDF SoT lives at monorepo `.bd/the-pitch.pdf` (not public/). // Include it only for the deck route so NFT can stream the single file. outputFileTracingIncludes: { - '/api/deck': ['./.bd/the-pitch.key', '../../.bd/the-pitch.key'], + '/api/deck': ['./.bd/the-pitch.pdf', '../../.bd/the-pitch.pdf'], }, // Next 14.x: tracing excludes live under experimental (stable top-level // only in later majors). Specifying is repo metadevelopment only — never diff --git a/apps/uapi/public/assetpack-icon.svg b/apps/uapi/public/assetpack-icon.svg new file mode 100644 index 000000000..3a2adb28c --- /dev/null +++ b/apps/uapi/public/assetpack-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/apps/uapi/public/ethereum-logo.svg b/apps/uapi/public/ethereum-logo.svg new file mode 100644 index 000000000..ec2544101 --- /dev/null +++ b/apps/uapi/public/ethereum-logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/uapi/public/solana-logo.svg b/apps/uapi/public/solana-logo.svg new file mode 100644 index 000000000..ed6f34d95 --- /dev/null +++ b/apps/uapi/public/solana-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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/depositActivityLedger.test.ts b/apps/uapi/tests/depositActivityLedger.test.ts index b3f95bf09..0b0585c2b 100644 --- a/apps/uapi/tests/depositActivityLedger.test.ts +++ b/apps/uapi/tests/depositActivityLedger.test.ts @@ -22,7 +22,7 @@ function run(partial: Partial & { id: string }): WorkspaceRun { } describe("deposit-activity-ledger", () => { - it("filters activity-ledger sources out of pipeline table runs", () => { + it("filters anchors and admission receipts out of pipeline table runs", () => { const runs = [ run({ id: "pipe-1", contextSource: "deposit-option-synthesis" }), run({ id: "anchor-1", contextSource: "deposit-obfuscations-anchor" }), @@ -31,6 +31,11 @@ describe("deposit-activity-ledger", () => { id: "anchor-summary", summary: "Recorded repository anchor for acme/app.", }), + run({ + id: "admit-1", + contextSource: "deposit-option-review-admission", + synthesisRunId: "pipe-1", + }), run({ id: "pipe-2" }), ]; const filtered = filterPipelineTableRuns(runs); diff --git a/apps/uapi/tests/depositAdmissionActivity.test.ts b/apps/uapi/tests/depositAdmissionActivity.test.ts new file mode 100644 index 000000000..7948db14a --- /dev/null +++ b/apps/uapi/tests/depositAdmissionActivity.test.ts @@ -0,0 +1,162 @@ +/** + * Per-option deposit admission ledger payloads for /packs projection. + */ +import { + buildDepositOptionAdmissionActivityDraft, + buildDepositOptionPatchfileDownload, + projectOptionAbsoluteMeasurements, +} from "@/components/deposits/models/deposit-admission-activity"; +import type { DepositOptionAdmissionReceipt } from "@bitcode/asset-packs-pipelines-execution-pipeline-sdivf-synthesize-deposits-asset-packs/deposit-asset-pack-option-admission"; +import type { DepositAssetPackOption } from "@bitcode/asset-packs-pipelines-execution-pipeline-sdivf-synthesize-deposits-asset-packs/deposit-asset-pack-options"; + +const option = { + schema: "bitcode.deposit.asset-pack-option", + optionId: "opt-1", + kind: "capability-slice", + title: "Auth middleware slice", + summary: "Extracts auth middleware.", + sourceBinding: { + repositoryFullName: "octocat/Spoon-Knife", + sourceBranch: "main", + sourceCommit: "abc", + sourcePathRoots: ["src/auth.ts"], + sourcePathCount: 1, + rawSourceStoredExternally: true as const, + protectedSourceVisibleInOption: false as const, + }, + demandAlignment: { + posture: "source-safe-demand-signals-only" as const, + depositorySignalRoots: [], + readingSignalRoots: [], + existingDepositorySignalRoots: [], + confidence: 0.5, + }, + measurements: [ + { + id: "function-count", + label: "Function count", + measurementKind: "function-count", + weight: 0.14, + volume: 0.4, + category: "absolute" as const, + magnitude: 8, + unit: "functions", + evidenceRoot: "ev-fn", + }, + ], + contents: { + patchSummary: "Add auth middleware helper", + fileChanges: [{ path: "src/auth.ts", op: "modify" }], + provenantSourcePaths: ["src/auth.ts"], + provenantSourceCount: 1, + }, + reviewBoundary: { + state: "reviewable-source-safe-option" as const, + decision: "pending-depositor-review" as const, + depositAdmissionBoundary: "not-admitted-until-depositor-approval" as const, + btdMintBoundary: "not-minted-by-deposit-option" as const, + settlementBoundary: + "future-reader-settlement-required-for-source-bearing-assetpack" as const, + }, + policyBoundary: { + sourceCriticalityPolicy: "deferred-to-gate6" as const, + demandRoiPolicy: "deferred-to-gate6" as const, + compensationPolicy: "deferred-to-gate6" as const, + }, + visibility: { + sourceSafeMetadataOnly: true as const, + protectedSourceVisible: false as const, + rawSourceTextVisible: false as const, + unpaidAssetPackSourceVisible: false as const, + rawPromptVisible: false as const, + interpolatedPromptVisible: false as const, + rawProviderResponseVisible: false as const, + walletPrivateMaterialVisible: false as const, + }, + roots: { + optionRoot: "option-root-1", + sourceBindingRoot: "src-root-1", + demandAlignmentRoot: "demand-root-1", + measurementRoot: "measurement-root-1", + contentsRoot: "contents-root-1", + reviewBoundaryRoot: "review-root-1", + }, +} as DepositAssetPackOption; + +const receipt = { + optionId: "opt-1", + optionKind: "capability-slice", + title: "Auth middleware slice", + admission: { + state: "admitted-to-depository", + depositoryAssetPackId: "depository-assetpack-abc", + blockers: [], + warnings: [], + }, + compensationPreview: { state: "compensation-preview-ready" }, + packsActivitySync: { + state: "synchronized-to-packs", + route: "/packs", + activityType: "depository-assetpack", + activityRoot: "packs-activity-root-1", + }, + roots: { + admissionReceiptRoot: "admission-receipt-1", + packsActivityRoot: "packs-activity-root-1", + }, +} as unknown as DepositOptionAdmissionReceipt; + +describe("deposit-admission-activity", () => { + it("projects absolute measurements from the option", () => { + const rows = projectOptionAbsoluteMeasurements(option); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + kind: "function-count", + category: "absolute", + magnitude: 8, + unit: "functions", + }); + }); + + it("builds per-option admission draft without session aggregates or patch body", () => { + const draft = buildDepositOptionAdmissionActivityDraft({ + receipt, + option, + synthesisRunId: "synth-run-1", + }); + expect(draft.type).toBe("pipeline:deposit-option-admission"); + expect(draft.output).toMatchObject({ + assetPackTitle: "Auth middleware slice", + optionId: "opt-1", + admissionState: "admitted-to-depository", + }); + expect(draft.output).not.toHaveProperty("depositAdmission"); + expect(draft.output).not.toHaveProperty("candidateCount"); + expect(draft.output).not.toHaveProperty("admittedCount"); + expect(draft.output).not.toHaveProperty("ownerContents"); + expect(JSON.stringify(draft)).not.toContain("Add auth middleware helper"); + const measurements = (draft.output as { measurements: unknown[] }).measurements; + expect(measurements).toHaveLength(1); + expect(draft.context).toMatchObject({ + source: "deposit-option-review-admission", + optionId: "opt-1", + synthesisRunId: "synth-run-1", + }); + }); + + it("builds a path-op AssetPack patchfile download payload", () => { + const file = buildDepositOptionPatchfileDownload(option); + expect(file.filename).toMatch(/\.path-op\.json$/); + expect(file.mimeType).toBe("application/json"); + const parsed = JSON.parse(file.body) as { + schema: string; + format: string; + files: unknown[]; + assetPack: { measurements: unknown[] }; + }; + expect(parsed.schema).toBe("bitcode.artifact.patch"); + expect(parsed.format).toBe("path-op-json"); + expect(parsed.files).toHaveLength(1); + expect(parsed.assetPack.measurements).toHaveLength(1); + }); +}); diff --git a/apps/uapi/tests/depositPageClient.test.tsx b/apps/uapi/tests/depositPageClient.test.tsx index 7009dead5..242f4ad48 100644 --- a/apps/uapi/tests/depositPageClient.test.tsx +++ b/apps/uapi/tests/depositPageClient.test.tsx @@ -537,7 +537,10 @@ describe("DepositPageClient", () => { // completion effect) and the route stage advances to review-options — // with NO dispatch call. await waitFor(() => - expect(fetchMock).toHaveBeenCalledWith("/api/executions/history/resume-run-1"), + expect(fetchMock).toHaveBeenCalledWith( + "/api/executions/history/resume-run-1", + expect.anything(), + ), ); await waitFor(() => expect(mockReplace).toHaveBeenCalledWith( @@ -1096,7 +1099,13 @@ describe("DepositPageClient", () => { return { ok: true, json: async () => ({ - run: { id: url.split("/").pop(), output: {} }, + // status completed without depositOptionSynthesis after retries + run: { + id: url.split("/").pop(), + status: "completed", + context: { source: "deposit-option-synthesis" }, + output: {}, + }, events: [ { id: "c1", event: { type: "completion" }, created_at: "2026-07-01T22:00:05.000Z" }, ], @@ -1108,7 +1117,8 @@ describe("DepositPageClient", () => { await dispatchSynthesis(); - const alert = await screen.findByRole("alert"); + // Hydrate retries (test delayMs=0) then fail-closed — allow multi-attempt. + const alert = await screen.findByRole("alert", {}, { timeout: 5000 }); expect(alert).toHaveTextContent( "Synthesized options were not found for this run.", ); @@ -1121,6 +1131,75 @@ describe("DepositPageClient", () => { }); }); + it("recovers options when history first omits depositOptionSynthesis then lands it", async () => { + const synthesis = buildMeasuredSynthesisFixture(); + let historyCalls = 0; + global.fetch = jest.fn(async (url: string) => { + if (url === "/api/deposit/synthesize-options") { + return { + ok: true, + json: async () => ({ + ok: true, + runId: "late-options-run", + status: "dispatched", + }), + }; + } + if (url.startsWith("/api/executions/history/")) { + historyCalls += 1; + if (historyCalls < 3) { + return { + ok: true, + json: async () => ({ + run: { + id: "late-options-run", + status: "completed", + context: { source: "deposit-option-synthesis" }, + output: { summary: "writing options" }, + }, + events: [ + { + id: "c1", + event: { type: "completion" }, + created_at: "2026-07-01T22:00:05.000Z", + }, + ], + }), + }; + } + return { + ok: true, + json: async () => ({ + run: { + id: "late-options-run", + status: "completed", + context: { source: "deposit-option-synthesis" }, + output: { + depositOptionSynthesis: synthesis, + reviewProjections: [], + }, + }, + events: [ + { + id: "c1", + event: { type: "completion" }, + created_at: "2026-07-01T22:00:05.000Z", + }, + ], + }), + }; + } + return { ok: true, json: async () => ({}) }; + }) as unknown as typeof fetch; + + await dispatchSynthesis(); + + // Options land after retries — no permanent false error banner. + await screen.findByTestId("deposit-option-capability-slice", {}, { timeout: 5000 }); + expect(screen.queryByText(/Synthesized options were not found/i)).toBeNull(); + expect(historyCalls).toBeGreaterThanOrEqual(3); + }); + it("fails the dispatch itself when the synthesize route rejects the request", async () => { global.fetch = jest.fn(async (url: string) => { if (url === "/api/deposit/synthesize-options") { diff --git a/apps/uapi/tests/depositSynthesisOptionsHydrate.test.ts b/apps/uapi/tests/depositSynthesisOptionsHydrate.test.ts new file mode 100644 index 000000000..7af117fcb --- /dev/null +++ b/apps/uapi/tests/depositSynthesisOptionsHydrate.test.ts @@ -0,0 +1,174 @@ +/** + * Deposit option history hydrate retry — false "options not found" race. + */ + +import { + extractDepositOptionSynthesisFromEvents, + extractDepositOptionSynthesisFromExecution, + fetchDepositOptionSynthesisWithRetry, + isDepositProductTerminalForOptions, + isRecoverableMissingOptionsError, + shouldRetryMissingDepositOptions, +} from "@/components/deposits/models/deposit-synthesis-options-hydrate"; + +describe("deposit-synthesis-options-hydrate", () => { + it("retries completed rows until depositOptionSynthesis appears", async () => { + let calls = 0; + const fetchImpl = jest.fn(async () => { + calls += 1; + if (calls < 3) { + return { + ok: true, + status: 200, + json: async () => ({ + run: { + status: "completed", + context: { source: "deposit-option-synthesis" }, + output: { summary: "still writing" }, + }, + }), + } as Response; + } + return { + ok: true, + status: 200, + json: async () => ({ + run: { + status: "completed", + context: { source: "deposit-option-synthesis" }, + output: { + depositOptionSynthesis: { + options: [{ optionId: "o1" }], + }, + reviewProjections: [], + }, + }, + }), + } as Response; + }); + + const result = await fetchDepositOptionSynthesisWithRetry({ + runId: "run-1", + maxAttempts: 5, + delayMs: 0, + fetchImpl: fetchImpl as unknown as typeof fetch, + sleepImpl: async () => undefined, + }); + + expect(result.kind).toBe("found"); + if (result.kind === "found") { + expect( + (result.synthesis as { options: unknown[] }).options, + ).toHaveLength(1); + } + expect(calls).toBe(3); + }); + + it("does not spin on failed rows without synthesis", async () => { + let calls = 0; + const fetchImpl = jest.fn(async () => { + calls += 1; + return { + ok: true, + status: 200, + json: async () => ({ + run: { + status: "failed", + context: { source: "deposit-option-synthesis" }, + output: { summary: "host died" }, + }, + }), + } as Response; + }); + + const result = await fetchDepositOptionSynthesisWithRetry({ + runId: "run-fail", + maxAttempts: 5, + delayMs: 0, + fetchImpl: fetchImpl as unknown as typeof fetch, + sleepImpl: async () => undefined, + }); + + expect(result.kind).toBe("missing"); + expect(calls).toBe(1); + }); + + it("shouldRetryMissingDepositOptions only for soft terminal states", () => { + expect( + shouldRetryMissingDepositOptions({ status: "completed", hasSynthesis: false }), + ).toBe(true); + expect( + shouldRetryMissingDepositOptions({ status: "failed", hasSynthesis: false }), + ).toBe(false); + expect( + shouldRetryMissingDepositOptions({ status: "completed", hasSynthesis: true }), + ).toBe(false); + }); + + it("recognizes recoverable false-fail banners", () => { + expect( + isRecoverableMissingOptionsError( + "Synthesized options were not found for this run.", + ), + ).toBe(true); + expect(isRecoverableMissingOptionsError("host budget exceeded")).toBe( + false, + ); + }); + + it("extracts synthesis from execution.output without network", () => { + const found = extractDepositOptionSynthesisFromExecution({ + id: "r1", + output: { + depositOptionSynthesis: { options: [{ optionId: "a" }] }, + reviewProjections: [{ optionId: "a" }], + }, + }); + expect(found?.synthesis).toEqual({ options: [{ optionId: "a" }] }); + expect(found?.reviewProjections).toHaveLength(1); + expect(extractDepositOptionSynthesisFromExecution({ output: {} })).toBeNull(); + }); + + it("extracts synthesis only from product-terminal completion events", () => { + const earlyNoise = extractDepositOptionSynthesisFromEvents([ + { event: { type: "completion", message: "finish store noise" } }, + ]); + expect(earlyNoise).toBeNull(); + + const product = extractDepositOptionSynthesisFromEvents([ + { event: { type: "status", message: "working" } }, + { + event: { + type: "completion", + depositOptionsReady: true, + depositOptionSynthesis: { options: [1, 2, 3] }, + reviewProjections: [], + }, + }, + ]); + expect(product?.synthesis).toEqual({ options: [1, 2, 3] }); + }); + + it("product terminal requires row status or depositOptionsReady completion", () => { + expect( + isDepositProductTerminalForOptions({ + events: [{ event: { type: "completion", message: "noise" } }], + }), + ).toBe(false); + expect( + isDepositProductTerminalForOptions({ + events: [ + { + event: { + type: "completion", + depositOptionsReady: true, + }, + }, + ], + }), + ).toBe(true); + expect( + isDepositProductTerminalForOptions({ rowStatus: "completed" }), + ).toBe(true); + }); +}); 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/apps/uapi/tests/marketingLandingPage.test.tsx b/apps/uapi/tests/marketingLandingPage.test.tsx index 659c658cf..74f77e1c0 100644 --- a/apps/uapi/tests/marketingLandingPage.test.tsx +++ b/apps/uapi/tests/marketingLandingPage.test.tsx @@ -137,11 +137,11 @@ describe('MarketingLandingPage', () => { expect( screen.getByText((_, node) => { if (node?.tagName !== 'H2') return false; - return (node.textContent ?? '').includes('Exchange Bitcodes for Bitcoins.'); + return (node.textContent ?? '').includes('Exchange Code for Coin.'); }), ).toBeInTheDocument(); - expect(screen.getByText('Bitcodes', { selector: 'h2 span' })).toBeInTheDocument(); - expect(screen.getByText('Bitcoins', { selector: 'h2 span' })).toBeInTheDocument(); + expect(screen.getByText('Code', { selector: 'h2 span' })).toBeInTheDocument(); + expect(screen.getByText('Coin', { selector: 'h2 span' })).toBeInTheDocument(); expect( screen.getByText( /Bitcode's canonical, commercial deployments are its mainnet ERC-1155/u, diff --git a/apps/uapi/tests/packActivityModel.test.ts b/apps/uapi/tests/packActivityModel.test.ts index 2f028aa5e..bcb0b0df8 100644 --- a/apps/uapi/tests/packActivityModel.test.ts +++ b/apps/uapi/tests/packActivityModel.test.ts @@ -215,25 +215,66 @@ describe('pack-activity-model', () => { payload: { type: 'pipeline:deposit-option-admission', assetPackTitle: 'Repository capability AssetPack option', + optionKind: 'capability-slice', + assetPackKind: 'capability-slice', + estimatedBtd: 0.42, + estimatedBtdCells: 420, + // Session aggregates must NOT become pack measurements. optionCount: 3, admittedCount: 1, + candidateCount: 2, admissionState: 'admitted-to-depository', compensationState: 'compensation-preview-ready', packActivitySyncState: 'synchronized-to-packs', admissionReportRoot: 'deposit-admission-report-root', + measurementRoot: 'deposit-option-measurements:abc', + measurements: [ + { + kind: 'function-count', + category: 'absolute', + label: 'Functions', + volume: 0.42, + magnitude: 12, + unit: 'functions', + weight: 0.14, + }, + { + kind: 'file-span', + category: 'absolute', + label: 'File span', + volume: 0.31, + magnitude: 4, + unit: 'files', + weight: 0.1, + }, + ], protectedSource: 'protected source body', rawProviderResponse: 'raw provider response', + patch: 'should-not-leak-patch-body', }, }); expect(record.type).toBe('depository-assetpack'); expect(record.assetPackTitle).toBe('Repository capability AssetPack option'); - expect(record.measurements.some((measurement) => measurement.id === 'admitted-count')).toBe(true); + expect(record.assetPackKind).toBe('capability-slice'); + expect(record.estimatedBtd).toBe(0.42); + // Session counters are not pack measurements. + expect(record.measurements.some((m) => m.id === 'admitted-count')).toBe(false); + expect(record.measurements.some((m) => m.id === 'candidate-count')).toBe(false); + expect(record.measurements.some((m) => m.id === 'measurement-root')).toBe(false); + // Absolute catalog chips for commercial pack detail. + expect(record.measurements.some((m) => m.id === 'absolute:function-count')).toBe(true); + expect(record.measurements.some((m) => m.id === 'absolute:file-span')).toBe(true); + expect( + record.measurements.find((m) => m.id === 'absolute:function-count')?.descriptor, + ).toMatch(/behaviors/i); + expect(record.values.some((v) => v.id === 'estimated-btd')).toBe(true); expect(record.compensationState).toBe('compensation-preview-ready'); expect(record.proofRoots.map((proofRoot) => proofRoot.root)).toContain('deposit-admission-report-root'); expect(assertPackActivitySourceSafe(record)).toBe(true); expect(JSON.stringify(record)).not.toContain('protected source body'); expect(JSON.stringify(record)).not.toContain('raw provider response'); + expect(JSON.stringify(record)).not.toContain('should-not-leak-patch-body'); }); it('fails malformed Pack commodity state closed into source-safe repair activity', () => { diff --git a/packages/asset-packs-pipelines/syntheses/deposit/src/__tests__/deposit-asset-pack-option-admission.test.ts b/packages/asset-packs-pipelines/syntheses/deposit/src/__tests__/deposit-asset-pack-option-admission.test.ts index 0b876bc91..04402fa22 100644 --- a/packages/asset-packs-pipelines/syntheses/deposit/src/__tests__/deposit-asset-pack-option-admission.test.ts +++ b/packages/asset-packs-pipelines/syntheses/deposit/src/__tests__/deposit-asset-pack-option-admission.test.ts @@ -109,4 +109,44 @@ describe('deposit-asset-pack-option-admission', () => { expect(report.receipts.every((receipt) => receipt.packsActivitySync.state === 'not-synchronized')).toBe(true); expect(assertDepositAssetPackOptionAdmissionReportSourceSafe(report).admitted).toBe(true); }); + + it('admits depositor-confirmed options even when compensation route is soft-incomplete', () => { + // Solo testnet depositors often lack org/wallet compensation eligibility. + // Soft compensation incompleteness must not silently drop a confirmed deposit. + const synthesis = buildDepositAssetPackOptionSynthesis({ + repositoryFullName: 'octocat/Spoon-Knife', + sourceBranch: 'main', + sourceCommit: '31bbc0c5227b6b3aed5d107fd8507d35ec22970a', + permissibleSources: ['apps/uapi/app/deposits/DepositPageClient.tsx'], + depositoryDemandSignals: [{ id: 'depository-demand', weight: 0.8 }], + readingDemandSignals: [{ id: 'reading-demand', weight: 0.86 }], + }); + const policy = buildDepositAssetPackOptionPolicyReport({ + synthesis, + sourceCriticalitySignals: [{ id: 'sub-critical', severity: 'sub-critical', weight: 0.82 }], + developmentCostSats: 1200, + expectedSettlementSats: 6800, + // No depositorWalletId → compensation soft-incomplete + }); + + const report = buildDepositAssetPackOptionAdmissionReport({ + synthesis, + policy, + reviewerId: 'depositor-1', + decisions: synthesis.options.map((option) => ({ + optionId: option.optionId, + decision: 'approved-for-admission' as const, + })), + }); + + expect(report.admittedCount).toBe(synthesis.options.length); + expect( + report.receipts.every((receipt) => receipt.admission.state === 'admitted-to-depository'), + ).toBe(true); + expect( + report.receipts.every((receipt) => + receipt.packsActivitySync.state === 'synchronized-to-packs', + ), + ).toBe(true); + }); }); diff --git a/packages/asset-packs-pipelines/syntheses/deposit/src/deposit-asset-pack-option-admission-helpers.ts b/packages/asset-packs-pipelines/syntheses/deposit/src/deposit-asset-pack-option-admission-helpers.ts index dc800189f..5e761c82a 100644 --- a/packages/asset-packs-pipelines/syntheses/deposit/src/deposit-asset-pack-option-admission-helpers.ts +++ b/packages/asset-packs-pipelines/syntheses/deposit/src/deposit-asset-pack-option-admission-helpers.ts @@ -39,6 +39,14 @@ export function policyByOption(policy: DepositAssetPackOptionPolicyReport) { return new Map(policy.evaluations.map((evaluation) => [evaluation.optionId, evaluation])); } +/** + * Hard admission blockers vs soft policy warnings. + * + * Depositor-confirmed approval is sovereign for their own measured options: + * compensation/ROI soft incompleteness must not silently drop a confirmed + * deposit (V48 Gate 4 — batch admit reliability). Critical source policy and + * non-reviewable options remain hard blocks. + */ export function blockerState(input: { option: DepositAssetPackOption; evaluation: DepositAssetPackOptionPolicyEvaluation | null; @@ -46,6 +54,7 @@ export function blockerState(input: { }) { const blockers: string[] = []; const warnings: string[] = []; + const depositorApproved = input.decision === 'approved-for-admission'; if (input.option.reviewBoundary.state !== 'reviewable-source-safe-option') { blockers.push(input.option.reviewBoundary.state); @@ -54,15 +63,37 @@ export function blockerState(input: { if (!input.evaluation) { blockers.push('missing-policy-evaluation'); } else { + // Critical-source blockers always hard-block. blockers.push(...input.evaluation.sourceCriticality.blockers); - blockers.push(...input.evaluation.compensation.blockers); warnings.push(...input.evaluation.sourceCriticality.warnings); warnings.push(...input.evaluation.compensation.warnings); + // Compensation incompleteness is informational once the depositor confirms; + // before confirm it remains a hard gate so "select" still surfaces repair. + if (input.evaluation.compensation.blockers.length) { + if (depositorApproved) { + warnings.push(...input.evaluation.compensation.blockers); + } else { + blockers.push(...input.evaluation.compensation.blockers); + } + } if (input.evaluation.policyDecision === 'blocked-before-admission') { - blockers.push('policy-blocked-before-admission'); + // Only hard-block when criticality (or non-reviewable) actually fires — + // ROI/compensation soft blocks alone must not veto a confirmed deposit. + const criticalHard = + input.evaluation.sourceCriticality.blockers.length > 0 || + input.option.reviewBoundary.state !== 'reviewable-source-safe-option'; + if (criticalHard || !depositorApproved) { + blockers.push('policy-blocked-before-admission'); + } else { + warnings.push('policy-soft-blocked-before-admission-overridden-by-depositor'); + } } if (input.evaluation.compensation.state !== 'eligible-if-approved-and-selected') { - blockers.push('compensation-route-repair-required-before-admission'); + if (depositorApproved) { + warnings.push('compensation-route-not-fully-eligible'); + } else { + blockers.push('compensation-route-repair-required-before-admission'); + } } } diff --git a/packages/execution-generics/src/__tests__/execution-stream-adapter.test.ts b/packages/execution-generics/src/__tests__/execution-stream-adapter.test.ts index 9227a4967..1a18e2bb5 100644 --- a/packages/execution-generics/src/__tests__/execution-stream-adapter.test.ts +++ b/packages/execution-generics/src/__tests__/execution-stream-adapter.test.ts @@ -105,7 +105,9 @@ describe('ExecutionStreamAdapter — event type inference', () => { expect(await inferredType('error', 'anything')).toBe('error'); expect(await inferredType('misc', 'error')).toBe('error'); expect(await inferredType('final', 'result')).toBe('completion'); - expect(await inferredType('misc', 'completion')).toBe('completion'); + // Finish stores finish/completion mid-pipeline — must NOT terminal-close the stream. + expect(await inferredType('finish', 'completion')).toBe('status'); + expect(await inferredType('misc', 'completion')).toBe('status'); expect(await inferredType('misc', 'anything')).toBe('status'); }); diff --git a/packages/execution-generics/src/storage/ExecutionStreamAdapter.ts b/packages/execution-generics/src/storage/ExecutionStreamAdapter.ts index 06aec1a5b..8f44337c4 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; } @@ -331,8 +351,14 @@ export class ExecutionStreamAdapter { return ExecutionStreamEventType.ERROR; } - // Completion - if (namespace === 'final' || key === 'completion') { + // Terminal completion is ONLY the product dispatcher's emitEvent(..., 'completion') + // (typed directly, not via this inferrer). Do NOT map key === 'completion': + // SDIVF Finish agents store cross-phase artifacts as finish/completion *during* + // the run — long before the route finalizes depositOptionSynthesis. Mapping + // that store to stream type 'completion' closed the client tail and hydrated + // options too early → false "Synthesized options were not found" (95bf1a4b). + // Namespace `final` remains the only store-path terminal signal. + if (namespace === 'final') { return ExecutionStreamEventType.COMPLETION; } @@ -359,12 +385,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 +422,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..eff1c0f88 100644 --- a/packages/pipeline-hosts/src/asset-pack-host-runners.ts +++ b/packages/pipeline-hosts/src/asset-pack-host-runners.ts @@ -319,6 +319,24 @@ function summarizeExecutionNode(node, depth = 0) { }; } +// 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 : []), + 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 +360,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 +379,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());