Skip to content

Commit f079673

Browse files
Gate 5 journal reconciliation detail
1 parent 7c537d8 commit f079673

24 files changed

Lines changed: 1429 additions & 14 deletions

BITCODE_SPEC_V28.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,26 @@ Acceptance criteria:
643643
- repair receipts and blocking drift reasons are visible.
644644
- operator can distinguish retryable, repairable, and approval-required states.
645645

646+
Implementation posture:
647+
648+
- `/api/executions/history/[runId]` must return the selected execution with
649+
Terminal journal readback, related BTC fee row, ledger anchor row,
650+
AssetPack range row, ownership/license projection rows, recent
651+
reconciliation repair receipts, and readback errors as typed detail payload
652+
fields rather than requiring browser-network raw JSON inspection.
653+
- Terminal detail owns a Journal section that separates ledger observed facts,
654+
database projected facts, and metaphysical canonical root facts. The visual
655+
surface must preserve the raw payload accordion for audit while making the
656+
operator state readable at a glance.
657+
- Confirmed, reorged, and failed finality observations are blocking facts for
658+
contradictory projections. Reorged/failed observations block unlock;
659+
confirmed observations that contradict missing projections require explicit
660+
approval or repair rather than silent retry.
661+
- Retryable means expected rows or readback are not visible yet without a
662+
final blocking observation. Repairable means reconciliation receipts or drift
663+
evidence can be applied without override. Approval-required means a
664+
confirmed ledger observation contradicts the projected database state.
665+
646666
### Gate 6: Terminal Organization And Access Policy
647667

648668
Purpose:

BITCODE_SPEC_V28_PARITY_MATRIX.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ It links manual screenshots, console logs, server logs, Supabase SQL checks, and
147147
| access policy id/hash shown before commitment | V27 access primitives and `/btd/[assetPackId]` disclosure route | implemented prerequisite | Gate 3 |
148148
| AssetPack range detail from registry state | `range.ts`, registry route, `/btd/[assetPackId]` | implemented prerequisite | Gate 4 |
149149
| owner-read/licensed-read/denied branches | `access.ts`, read-access route, licensed-read revenue route | implemented prerequisite | Gate 4 |
150-
| Terminal journal rows as transaction detail | `terminal-journal.ts`, terminal-journal route | implemented prerequisite | Gate 5 |
151-
| ledger/database reconciliation as operator read | `reconciliation.ts`, reconciliation route | implemented prerequisite | Gate 5 |
150+
| Terminal journal rows as transaction detail | `packages/api/src/routes/executions.ts`, `uapi/app/terminal/terminal-transaction-detail-snapshot.ts`, `uapi/app/terminal/TerminalTransactionJournalReconciliationCard.tsx`, `terminal-journal.ts` | implemented | Gate 5 |
151+
| ledger/database reconciliation as operator read | `uapi/app/terminal/terminal-journal-reconciliation.ts`, reconciliation repair table readback, `reconciliation.ts` | implemented | Gate 5 |
152152
| organization holdings and read-license usage from registry | organization BTD models plus V27 registry docs | pending | Gate 6 |
153153
| MCP authorization based on range/read-license/policy truth | MCP holding-gate work remains aggregate-compatibility oriented; retained in V28 MVP scope. | pending | Gate 6 |
154154
| ChatGPT App authorization based on range/read-license/policy truth | ChatGPT App MVP parity must use the same registry-derived access posture as MCP and Terminal; retained in V28 MVP scope. | not yet implemented | Gate 6 |
@@ -178,7 +178,7 @@ It links manual screenshots, console logs, server logs, Supabase SQL checks, and
178178
| Terminal BTC fees | PSBT/finality UX built over V27 primitives | pending |
179179
| Terminal Read/Fit | Read, Fit, semantic volume, measuremint, policy UX | pending |
180180
| Terminal range/read | AssetPack range and read-right detail | pending |
181-
| Terminal journal/reconcile | journal diff and repair detail | pending |
181+
| Terminal journal/reconcile | selected transaction Journal section separates ledger observations, database projections, canonical roots, repair receipts, and blocking drift reasons | substantially advanced |
182182
| Terminal operations | telemetry, lanes, upgrade, migration readiness | pending |
183183

184184
## Later-Version Deferrals

BITCODE_V28_QA.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,30 @@ Saved query name: `v28_qa_terminal_03_btd_ledger_after_terminal`
20282028
20292029
Use `supabase/queries/v28_qa_terminal_03_btd_ledger_after_terminal.sql` after any Terminal action that claims Fit closure, AssetPack minting, BTC fee payment, ledger anchoring, settlement, or reconciliation.
20302030
2031+
Terminal transaction detail journal diff:
2032+
2033+
- Open a completed Read/Fit or AssetPack pipeline execution in Terminal and
2034+
switch `transactionDetail=journal`.
2035+
- Expected payload carriers are `run.ledger_settlement` and
2036+
`run.terminal_journal` from `/api/executions/history/[runId]`.
2037+
- The Journal section must show ledger observed facts separately from database
2038+
projected facts and metaphysical canonical facts. Operators should not need
2039+
the browser Network panel to answer whether journal rows, BTC fee rows,
2040+
ledger anchors, ownership/license projections, and repair receipts were read.
2041+
- Expected state labels:
2042+
- `Aligned`: settled status, all readback booleans present, expected journal
2043+
entries observed, no blocking repairs.
2044+
- `Retryable`: missing rows or readback while no confirmed/reorged/failed
2045+
observation blocks the projection.
2046+
- `Repairable`: reconciliation repair receipts or drift evidence are present
2047+
and do not require override.
2048+
- `Approval required`: a confirmed ledger fact contradicts a missing or
2049+
conflicting database projection.
2050+
- `Blocked`: reorged/failed finality or a blocking repair receipt prevents
2051+
unlock.
2052+
- The raw payload accordion remains available for audit, but the default visual
2053+
state must surface blocking drift reasons and repair receipts directly.
2054+
20312055
### Top Chrome Wallet Readiness
20322056
20332057
Expected V28 behavior:

packages/api/src/routes/executions.ts

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ type ExecutionEventRow = {
4646
phase: string | null;
4747
};
4848

49+
type TerminalJournalReadback = {
50+
expectedJournalEntryIds: string[];
51+
entries: JsonRecord[];
52+
repairs: JsonRecord[];
53+
ledgerRows: {
54+
assetPackRanges: JsonRecord[];
55+
btcFeeTransactions: JsonRecord[];
56+
ledgerAnchors: JsonRecord[];
57+
ownershipEvents: JsonRecord[];
58+
readLicenses: JsonRecord[];
59+
};
60+
readErrors: string[];
61+
};
62+
4963
function toErrorMessage(error: unknown, fallback: string) {
5064
if (error instanceof Error && error.message) return error.message;
5165
if (typeof error === 'string' && error.trim()) return error;
@@ -427,6 +441,17 @@ function buildNormalizedAssetPackCompletion(row: ExecutionHistoryRow) {
427441
};
428442
}
429443

444+
function buildLedgerSettlement(row: ExecutionHistoryRow) {
445+
const output = readOutputRecord(row);
446+
const assetPackCompletion = readAssetPackCompletion(row);
447+
448+
return (
449+
asRecord(assetPackCompletion?.ledgerSettlement) ||
450+
asRecord(output?.ledgerSettlement) ||
451+
null
452+
);
453+
}
454+
430455
export function normalizeExecutionHistoryRow(row: ExecutionHistoryRow) {
431456
const agenticExecution = buildAgenticExecutionSummary({
432457
type: row.type,
@@ -457,6 +482,7 @@ export function normalizeExecutionHistoryRow(row: ExecutionHistoryRow) {
457482
written_asset_type: buildWrittenAssetType(row),
458483
asset_pack: buildAssetPack(row),
459484
asset_pack_completion: buildNormalizedAssetPackCompletion(row),
485+
ledger_settlement: buildLedgerSettlement(row),
460486
error: row.error ?? null,
461487
};
462488
}
@@ -628,8 +654,159 @@ export async function getExecutionHistoryRunRoute(
628654
);
629655
}
630656

657+
const normalizedRun = normalizeExecutionHistoryRow(run);
658+
const terminalJournal = await fetchTerminalJournalReadback(run.id, normalizedRun);
659+
631660
return createJsonResponse({
632-
run: normalizeExecutionHistoryRow(run),
661+
run: {
662+
...normalizedRun,
663+
terminal_journal: terminalJournal,
664+
},
633665
events: (events || []).map(normalizeExecutionEventRow),
666+
terminal_journal: terminalJournal,
634667
});
635668
}
669+
670+
function readNormalizedLedgerSettlement(run: JsonRecord) {
671+
return (
672+
asRecord(run.ledger_settlement) ||
673+
asRecord(asRecord(run.asset_pack_completion)?.ledgerSettlement) ||
674+
asRecord(asRecord(run.output)?.ledgerSettlement) ||
675+
null
676+
);
677+
}
678+
679+
function readStringArray(value: unknown) {
680+
return asArray(value)
681+
.map((entry) => asString(entry))
682+
.filter((entry): entry is string => Boolean(entry));
683+
}
684+
685+
function dedupeStrings(values: Array<string | null | undefined>) {
686+
return Array.from(new Set(values.filter((value): value is string => Boolean(value))));
687+
}
688+
689+
function expectedHarnessJournalEntryIds(runId: string, ledgerSettlement: JsonRecord | null) {
690+
if (!ledgerSettlement) return [];
691+
692+
return dedupeStrings([
693+
...readStringArray(ledgerSettlement?.journalEntryIds),
694+
`journal-mint-${runId}`,
695+
`journal-btc-fee-${runId}`,
696+
`journal-anchor-${runId}`,
697+
`journal-settlement-${runId}`,
698+
]);
699+
}
700+
701+
async function readRowsByIds(table: string, column: string, ids: string[], readErrors: string[]) {
702+
if (!ids.length) return [];
703+
704+
const { data, error } = await supabaseAdmin
705+
.from(table)
706+
.select('*')
707+
.in(column, ids);
708+
709+
if (error) {
710+
readErrors.push(`${table} readback failed: ${toErrorMessage(error, 'unknown error')}`);
711+
return [];
712+
}
713+
714+
return asArray<JsonRecord>(data).filter((entry) => asRecord(entry));
715+
}
716+
717+
async function readRowById(table: string, column: string, id: string | null, readErrors: string[]) {
718+
if (!id) return [];
719+
720+
const { data, error } = await supabaseAdmin
721+
.from(table)
722+
.select('*')
723+
.eq(column, id)
724+
.maybeSingle();
725+
726+
if (error) {
727+
readErrors.push(`${table} readback failed: ${toErrorMessage(error, 'unknown error')}`);
728+
return [];
729+
}
730+
731+
return asRecord(data) ? [data] : [];
732+
}
733+
734+
async function readRecentRepairRows(runId: string, factIds: string[], readErrors: string[]) {
735+
const { data, error } = await supabaseAdmin
736+
.from('btd_ledger_database_reconciliation_repairs')
737+
.select('*')
738+
.order('issued_at', { ascending: false })
739+
.limit(100);
740+
741+
if (error) {
742+
readErrors.push(`btd_ledger_database_reconciliation_repairs readback failed: ${toErrorMessage(error, 'unknown error')}`);
743+
return [];
744+
}
745+
746+
const factIdSet = new Set(factIds);
747+
return asArray<JsonRecord>(data)
748+
.filter((row) => {
749+
const repairId = asString(row.repair_id);
750+
const reconciliationId = asString(row.reconciliation_id);
751+
const factId = asString(row.fact_id);
752+
return (
753+
Boolean(repairId?.includes(runId)) ||
754+
Boolean(reconciliationId?.includes(runId)) ||
755+
Boolean(factId && factIdSet.has(factId))
756+
);
757+
})
758+
.slice(0, 25);
759+
}
760+
761+
async function fetchTerminalJournalReadback(runId: string, normalizedRun: JsonRecord): Promise<TerminalJournalReadback> {
762+
const readErrors: string[] = [];
763+
const ledgerSettlement = readNormalizedLedgerSettlement(normalizedRun);
764+
const assetPackId = asString(ledgerSettlement?.assetPackId);
765+
const ledgerAnchorId = asString(ledgerSettlement?.ledgerAnchorId);
766+
const btcFeeReceiptId = asString(ledgerSettlement?.btcFeeReceiptId);
767+
const ownershipEventId = asString(ledgerSettlement?.ownershipEventId) || `ownership-mint-${runId}`;
768+
const readLicenseId = asString(ledgerSettlement?.readLicenseId) || `read-license-${runId}`;
769+
const expectedJournalEntryIds = expectedHarnessJournalEntryIds(runId, ledgerSettlement);
770+
const entries = await readRowsByIds(
771+
'btd_terminal_journal_entries',
772+
'journal_entry_id',
773+
expectedJournalEntryIds,
774+
readErrors,
775+
);
776+
const [
777+
assetPackRanges,
778+
btcFeeTransactions,
779+
ledgerAnchors,
780+
ownershipEvents,
781+
readLicenses,
782+
] = await Promise.all([
783+
readRowById('btd_asset_pack_ranges', 'asset_pack_id', assetPackId, readErrors),
784+
readRowById('btc_fee_transactions', 'receipt_id', btcFeeReceiptId, readErrors),
785+
readRowById('btd_asset_pack_ledger_anchors', 'anchor_id', ledgerAnchorId, readErrors),
786+
readRowById('btd_ownership_events', 'ownership_event_id', ownershipEventId, readErrors),
787+
readRowById('btd_read_licenses', 'license_id', readLicenseId, readErrors),
788+
]);
789+
const factIds = dedupeStrings([
790+
assetPackId,
791+
ledgerAnchorId,
792+
btcFeeReceiptId,
793+
ownershipEventId,
794+
readLicenseId,
795+
...expectedJournalEntryIds,
796+
]);
797+
const repairs = await readRecentRepairRows(runId, factIds, readErrors);
798+
799+
return {
800+
expectedJournalEntryIds,
801+
entries: entries.sort((left, right) => Number(left.exchange_sequence || 0) - Number(right.exchange_sequence || 0)),
802+
repairs,
803+
ledgerRows: {
804+
assetPackRanges,
805+
btcFeeTransactions,
806+
ledgerAnchors,
807+
ownershipEvents,
808+
readLicenses,
809+
},
810+
readErrors,
811+
};
812+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,9 @@ async function settleAssetPackLedger(pipelineResultState) {
10701070
},
10711071
ledgerAnchorId,
10721072
btcFeeReceiptId,
1073+
ownershipEventId,
1074+
readLicenseId,
1075+
journalEntryIds,
10731076
depositorWalletId,
10741077
readerWalletId,
10751078
ownershipBoundary: settlementOwnershipBoundary({
@@ -1445,6 +1448,9 @@ async function settleAssetPackLedger(pipelineResultState) {
14451448
},
14461449
ledgerAnchorId,
14471450
btcFeeReceiptId,
1451+
ownershipEventId,
1452+
readLicenseId,
1453+
journalEntryIds,
14481454
depositorWalletId,
14491455
readerWalletId,
14501456
btcFee: {

uapi/app/api/pipeline-harness/asset-pack/runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,9 @@ function summarizeEvidence(evidence: unknown): Record<string, unknown> | null {
540540
btdRange: ledgerSettlement.btdRange,
541541
ledgerAnchorId: ledgerSettlement.ledgerAnchorId,
542542
btcFeeReceiptId: ledgerSettlement.btcFeeReceiptId,
543+
ownershipEventId: ledgerSettlement.ownershipEventId,
544+
readLicenseId: ledgerSettlement.readLicenseId,
545+
journalEntryIds: ledgerSettlement.journalEntryIds,
543546
depositorWalletId: ledgerSettlement.depositorWalletId,
544547
readerWalletId: ledgerSettlement.readerWalletId,
545548
btcFee: ledgerSettlement.btcFee,

uapi/app/terminal/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Terminal Implementation Module
22

3-
This module owns the commercial Terminal implementation. The canonical product
3+
This module owns the Terminal implementation. The canonical product
44
route is `/terminal`. No retained compatibility route belongs to this surface;
55
old route boundaries must be reformed in place into Terminal source.
66

@@ -13,7 +13,7 @@ Its job is to keep one operator route coherent:
1313

1414
## Main experience model
1515

16-
V28 commercial MVP hardening keeps these product experiences route-correct:
16+
V28 MVP hardening keeps these product experiences route-correct:
1717
- `terminal activity`
1818
- `conversations`
1919
- `auxillaries`
@@ -33,6 +33,9 @@ presenting them as part of Terminal itself.
3333
Main Terminal activity and selected-result shell.
3434
- `terminal-transaction-query.ts`
3535
Route-owned filter, paging, and selected-activity state.
36+
- `terminal-journal-reconciliation.ts` and `TerminalTransactionJournalReconciliationCard.tsx`
37+
Selected-activity Journal section owner for ledger observations, database
38+
projections, canonical root facts, repair receipts, and drift state.
3639
- `TerminalCommandDeck.tsx`
3740
Scenario, projection, branch mode, reset, and flow-guide entry posture.
3841
- `TerminalDepositReadWorkbench.tsx`
@@ -44,7 +47,7 @@ presenting them as part of Terminal itself.
4447

4548
## Current V28 checkpoint expectations
4649

47-
Commercial MVP checkpoint confidence requires `/terminal` to be:
50+
MVP checkpoint confidence requires `/terminal` to be:
4851
- renderable in mock-mode review,
4952
- route-query owned for activity selection and filtering,
5053
- user-facing in copy and help posture,
@@ -88,6 +91,13 @@ Those queries are the operator evidence path for proving Terminal UI, API
8891
history, GitHub source inventory, and BTD ledger projections stay synchronized
8992
while the MVP is being QAed.
9093

94+
When `transactionDetail=journal` is selected, the detail route must read
95+
Terminal journal rows and reconciliation repair receipts through
96+
`/api/executions/history/[runId]`; the UI may expose the raw payload accordion
97+
for audit, but the operator-facing visual state must make retryable,
98+
repairable, approval-required, blocked, and aligned states distinguishable
99+
without browser-network inspection.
100+
91101
## Related shared systems
92102

93103
- [../../components/base/bitcode/execution/README.md](../../components/base/bitcode/execution/README.md)

uapi/app/terminal/TerminalTransactionDetailActionBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const DETAIL_ACTIONS: DetailAction[] = [
1515
{ id: 'closure', label: 'Closure' },
1616
{ id: 'proofs', label: 'Proofs' },
1717
{ id: 'history', label: 'History' },
18+
{ id: 'journal', label: 'Journal' },
1819
{ id: 'activity', label: 'Execution stream' },
1920
{ id: 'console', label: 'Console' },
2021
];

0 commit comments

Comments
 (0)