From d83f557df4d0b5dc1e989fc2c5ca12fc5a34809c Mon Sep 17 00:00:00 2001 From: Garrett Maring Date: Fri, 29 May 2026 10:21:00 -0300 Subject: [PATCH] V43 Gate 4: Extract Reading route Adds the /read five-step enterprise Reading route with source-safe ReadRouteSession state, public navigation wiring, retained Terminal workbench reuse, and route-owned recent Reading activity readback. Adds the V43 Gate 4 protocol artifact, generator, checker, workflow hooks, docs/spec updates, and focused UAPI/protocol test coverage for the route extraction. --- .../v43-packs-activity-master-detail.json | 34 +- .bitcode/v43-read-route-five-step-ux.json | 353 +++++++++++++++ .bitcode/v43-route-vocabulary-inventory.json | 284 +++++++++--- .github/workflows/bitcode-canon-quality.yml | 3 + .github/workflows/bitcode-gate-quality.yml | 6 + BITCODE_SPEC_V43.md | 9 + BITCODE_SPEC_V43_DELTA.md | 12 + BITCODE_SPEC_V43_NOTES.md | 16 + BITCODE_SPEC_V43_PARITY_MATRIX.md | 5 +- README.md | 11 + SPECIFICATIONS_ROADMAP.md | 5 +- package.json | 3 + packages/protocol/README.md | 11 + .../v43-packs-activity-master-detail.js | 2 +- .../canonical/v43-read-route-five-step-ux.js | 271 +++++++++++ packages/protocol/src/index.d.ts | 13 + packages/protocol/src/index.js | 15 + .../test/v43-read-route-five-step-ux.test.js | 43 ++ ...heck-v43-gate4-read-route-five-step-ux.mjs | 171 +++++++ .../generate-v43-read-route-five-step-ux.mjs | 30 ++ uapi/app/read/ReadPageClient.tsx | 421 ++++++++++++++++++ uapi/app/read/page.tsx | 33 ++ uapi/app/read/read-route-model.ts | 201 +++++++++ .../terminal-enterprise-reading-ux-state.ts | 4 +- uapi/app/terminal/terminal-routes.ts | 6 + .../base/bitcode/layout/NavBrand.tsx | 6 +- .../bitcode/layout/bitcode-public-copy.ts | 6 +- .../layout/bitcode-public-explainers.ts | 11 + .../components/base/bitcode/layout/footer.tsx | 32 ++ uapi/components/base/bitcode/layout/nav.tsx | 7 + .../base/bitcode/layout/workspace-surface.ts | 3 +- uapi/jest.config.cjs | 2 + uapi/tests/footerPublicShell.test.tsx | 8 +- uapi/tests/marketingLandingPage.test.tsx | 4 +- uapi/tests/navPublicShell.test.tsx | 13 + uapi/tests/readPageClient.test.tsx | 155 +++++++ uapi/tests/readRouteModel.test.ts | 77 ++++ 37 files changed, 2185 insertions(+), 101 deletions(-) create mode 100644 .bitcode/v43-read-route-five-step-ux.json create mode 100644 packages/protocol/src/canonical/v43-read-route-five-step-ux.js create mode 100644 packages/protocol/test/v43-read-route-five-step-ux.test.js create mode 100644 scripts/check-v43-gate4-read-route-five-step-ux.mjs create mode 100644 scripts/generate-v43-read-route-five-step-ux.mjs create mode 100644 uapi/app/read/ReadPageClient.tsx create mode 100644 uapi/app/read/page.tsx create mode 100644 uapi/app/read/read-route-model.ts create mode 100644 uapi/tests/readPageClient.test.tsx create mode 100644 uapi/tests/readRouteModel.test.ts diff --git a/.bitcode/v43-packs-activity-master-detail.json b/.bitcode/v43-packs-activity-master-detail.json index eea364be1..4c3a0ab20 100644 --- a/.bitcode/v43-packs-activity-master-detail.json +++ b/.bitcode/v43-packs-activity-master-detail.json @@ -5,7 +5,7 @@ "currentTarget": "V42", "sourceSafetyVerdict": "source-safe-packs-activity-master-detail-metadata", "generatedAt": "deterministic", - "artifactRoot": "v43-packs-activity-master-detail:16af692e5389aab2e555718c", + "artifactRoot": "v43-packs-activity-master-detail:b3480760d0d3a35b8590f2ff", "passed": true, "typeIds": [ "deposit-option", @@ -138,27 +138,27 @@ ], "sourceRoots": { "activePointer": "BITCODE_SPEC.txt:75f3f6d81f999da998f40cb6", - "spec": "BITCODE_SPEC_V43.md:e1511ab411b955ac06577f1c", - "delta": "BITCODE_SPEC_V43_DELTA.md:40784121b9e0e5cb052f61f9", - "notes": "BITCODE_SPEC_V43_NOTES.md:656205fa69ebf3534be0d27b", - "parity": "BITCODE_SPEC_V43_PARITY_MATRIX.md:a89a11099e9899086349f219", - "roadmap": "SPECIFICATIONS_ROADMAP.md:13edb10a293b1562c86d1a71", - "readme": "README.md:37a1663a56243ba4a24a6982", - "protocolReadme": "packages/protocol/README.md:c6aa082931d703278de8ff57", - "packageJson": "package.json:8e3513f2ce52a7647bf2b949", - "gateWorkflow": ".github/workflows/bitcode-gate-quality.yml:363fadca2aa9a89082fcd5d0", - "canonWorkflow": ".github/workflows/bitcode-canon-quality.yml:c537637b2ebfbde127211e6e", + "spec": "BITCODE_SPEC_V43.md:6e516c0a6e96290386afcf61", + "delta": "BITCODE_SPEC_V43_DELTA.md:647032f97afd192352dc10b1", + "notes": "BITCODE_SPEC_V43_NOTES.md:a68a543d6ea0e4349cad034a", + "parity": "BITCODE_SPEC_V43_PARITY_MATRIX.md:06185dd04abc8959e1c84483", + "roadmap": "SPECIFICATIONS_ROADMAP.md:473a173a7f9743dcea1c1f47", + "readme": "README.md:a1a22b6a7c52f36f0958c36a", + "protocolReadme": "packages/protocol/README.md:e38fe167428aac1bca4a2461", + "packageJson": "package.json:bc3b1bdf2a9b8b49d012bc0b", + "gateWorkflow": ".github/workflows/bitcode-gate-quality.yml:de78a45f02fba6ef7c693aaf", + "canonWorkflow": ".github/workflows/bitcode-canon-quality.yml:fe726499f880ae3ae3c507be", "model": "uapi/components/base/bitcode/activity/pack-activity-model.ts:6c610ebddc2949f3716b6bf8", "route": "uapi/app/api/packs/activity/route.ts:8a524f1dd16889733ce418a7", "page": "uapi/app/packs/page.tsx:1b3d7a1cc00500a13042e8dd", "client": "uapi/app/packs/PacksPageClient.tsx:d777458df28f47a360c6601c", "exchangeRedirect": "uapi/app/exchange/page.tsx:be7a744d1f9c9252d1b6eba9", - "nav": "uapi/components/base/bitcode/layout/nav.tsx:a39909d2e23170a803373674", - "workspaceSurface": "uapi/components/base/bitcode/layout/workspace-surface.ts:f4784c0c92f5d958ddeb64ab", - "publicCopy": "uapi/components/base/bitcode/layout/bitcode-public-copy.ts:4e2e907a06a6d288be74e5ad", - "publicExplainers": "uapi/components/base/bitcode/layout/bitcode-public-explainers.ts:c28aae00fd37ff1c898a7c38", - "packageIndex": "packages/protocol/src/index.js:b700d9aafac73862fb0ba9cf", - "packageTypes": "packages/protocol/src/index.d.ts:9ef207570ce6e9533b9bafaf", + "nav": "uapi/components/base/bitcode/layout/nav.tsx:185098956a79250823f9f284", + "workspaceSurface": "uapi/components/base/bitcode/layout/workspace-surface.ts:d0b0b09eea99f1efde69f078", + "publicCopy": "uapi/components/base/bitcode/layout/bitcode-public-copy.ts:0965f97cf0de448735155ecd", + "publicExplainers": "uapi/components/base/bitcode/layout/bitcode-public-explainers.ts:0f7770336c1329ef78bca543", + "packageIndex": "packages/protocol/src/index.js:16bf3b05b7ac611fb40825a4", + "packageTypes": "packages/protocol/src/index.d.ts:a871b011c2249f62a392fbec", "packageTest": "packages/protocol/test/v43-packs-activity-master-detail.test.js:baf3e44b2fbe4e62590c1824", "uapiTest": "uapi/tests/packActivityModel.test.ts:a4c3deea857830cf57772a4d", "generator": "scripts/generate-v43-packs-activity-master-detail.mjs:f342bc9b3ebb8de4e7aa2882", diff --git a/.bitcode/v43-read-route-five-step-ux.json b/.bitcode/v43-read-route-five-step-ux.json new file mode 100644 index 000000000..a0e32b58b --- /dev/null +++ b/.bitcode/v43-read-route-five-step-ux.json @@ -0,0 +1,353 @@ +{ + "artifactId": "v43-read-route-five-step-ux", + "schemaId": "bitcode.v43.readRouteFiveStepUx.v1", + "version": "V43", + "currentTarget": "V42", + "sourceSafetyVerdict": "source-safe-read-route-five-step-metadata", + "generatedAt": "deterministic", + "artifactRoot": "v43-read-route-five-step-ux:9880cdec982a88d9c9d01825", + "passed": true, + "stepIds": [ + "request-read", + "review-synthesized-need", + "request-fit", + "review-synthesized-asset-pack", + "buy-asset-pack-settle" + ], + "objectIds": [ + "ReadRouteSession", + "Read Request", + "synthesized Need", + "Need review decision", + "Finding Fits request", + "AssetPack preview", + "settlement quote", + "delivery receipt" + ], + "pipelineIds": [ + "ReadNeedComprehensionSynthesis", + "ReadFitsFindingSynthesis" + ], + "sourceSafeFieldIds": [ + "read_request_summary", + "read_need_measurements", + "need_feedback_history", + "depository_candidate_counts", + "selected_fit_ids", + "asset_pack_measurements", + "quality_posture", + "proof_roots", + "btc_fee_quote", + "settlement_state", + "delivery_posture" + ], + "forbiddenPayloadIds": [ + "protected_source_payload", + "raw_protected_prompt", + "raw_provider_response", + "unpaid_assetpack_source", + "wallet_private_material", + "settlement_private_payload", + "ledger_write_authority" + ], + "contractRows": [ + { + "rowId": "read-route-session", + "owner": "uapi/app/read/read-route-model.ts", + "contract": "ReadRouteSession owns the /read route state, five Reading steps, accepted-Need gate, source-safe preview boundary, settlement quote, and delivery unlock posture.", + "requiredFields": [ + "schema", + "route", + "steps", + "readObjects", + "pipelineOwnership", + "disclosure", + "proofRoot" + ], + "rowRoot": "v43-read-route-contract:18db4bb5d567eb1410d0004f", + "sourceSafeMetadataOnly": true, + "protectedSourceVisible": false, + "unpaidAssetPackSourceVisible": false + }, + { + "rowId": "read-route-page", + "owner": "uapi/app/read/page.tsx", + "contract": "/read is the default Reading route with metadata, public shell chrome, Suspense fallback, and ReadPageClient.", + "requiredFields": [ + "canonical", + "ReadPageClient", + "PublicShellFrame" + ], + "rowRoot": "v43-read-route-contract:d522471362cb0cb56b9d4830", + "sourceSafeMetadataOnly": true, + "protectedSourceVisible": false, + "unpaidAssetPackSourceVisible": false + }, + { + "rowId": "read-route-client", + "owner": "uapi/app/read/ReadPageClient.tsx", + "contract": "ReadPageClient renders request-read, Need review, Finding Fits request, source-safe preview, settlement/delivery posture, route-owned activity readback, and live workbench controls.", + "requiredFields": [ + "TerminalDepositReadWorkbench", + "TerminalRepositoryContextPanel", + "TerminalReadScenarioPanel", + "buildReadRouteSession", + "ReadNeedComprehensionSynthesis", + "ReadFitsFindingSynthesis" + ], + "rowRoot": "v43-read-route-contract:00991d243b0eae226fc9379f", + "sourceSafeMetadataOnly": true, + "protectedSourceVisible": false, + "unpaidAssetPackSourceVisible": false + }, + { + "rowId": "navigation-read-route", + "owner": "uapi/components/base/bitcode/layout/nav.tsx", + "contract": "Public navigation and footer expose /read as the primary Reading path while retained Terminal remains available for debug-compatible detail.", + "requiredFields": [ + "/read", + "Read", + "BITCODE_PUBLIC_EXPLAINERS.read" + ], + "rowRoot": "v43-read-route-contract:32d0594ac70011fa3bd8adcb", + "sourceSafeMetadataOnly": true, + "protectedSourceVisible": false, + "unpaidAssetPackSourceVisible": false + } + ], + "sourceRoots": { + "activePointer": "BITCODE_SPEC.txt:75f3f6d81f999da998f40cb6", + "spec": "BITCODE_SPEC_V43.md:6e516c0a6e96290386afcf61", + "delta": "BITCODE_SPEC_V43_DELTA.md:647032f97afd192352dc10b1", + "notes": "BITCODE_SPEC_V43_NOTES.md:a68a543d6ea0e4349cad034a", + "parity": "BITCODE_SPEC_V43_PARITY_MATRIX.md:06185dd04abc8959e1c84483", + "roadmap": "SPECIFICATIONS_ROADMAP.md:473a173a7f9743dcea1c1f47", + "readme": "README.md:a1a22b6a7c52f36f0958c36a", + "protocolReadme": "packages/protocol/README.md:e38fe167428aac1bca4a2461", + "packageJson": "package.json:bc3b1bdf2a9b8b49d012bc0b", + "gateWorkflow": ".github/workflows/bitcode-gate-quality.yml:de78a45f02fba6ef7c693aaf", + "canonWorkflow": ".github/workflows/bitcode-canon-quality.yml:fe726499f880ae3ae3c507be", + "routeModel": "uapi/app/read/read-route-model.ts:a9324b0d92cd6c5176fa7940", + "page": "uapi/app/read/page.tsx:37a723ed87feac05c0504249", + "client": "uapi/app/read/ReadPageClient.tsx:d14f0fbb65af0edfa68d774f", + "terminalRoutes": "uapi/app/terminal/terminal-routes.ts:986e93f930159575a74df873", + "terminalWorkbench": "uapi/app/terminal/TerminalDepositReadWorkbench.tsx:beeb39fa4f8568619d16f343", + "terminalReadingState": "uapi/app/terminal/terminal-enterprise-reading-ux-state.ts:fb152fb0cc1d0799d1d616d6", + "nav": "uapi/components/base/bitcode/layout/nav.tsx:185098956a79250823f9f284", + "workspaceSurface": "uapi/components/base/bitcode/layout/workspace-surface.ts:d0b0b09eea99f1efde69f078", + "publicCopy": "uapi/components/base/bitcode/layout/bitcode-public-copy.ts:0965f97cf0de448735155ecd", + "publicExplainers": "uapi/components/base/bitcode/layout/bitcode-public-explainers.ts:0f7770336c1329ef78bca543", + "footer": "uapi/components/base/bitcode/layout/footer.tsx:0ccc24bfd8d55e9cf58817b1", + "packageIndex": "packages/protocol/src/index.js:16bf3b05b7ac611fb40825a4", + "packageTypes": "packages/protocol/src/index.d.ts:a871b011c2249f62a392fbec", + "packageTest": "packages/protocol/test/v43-read-route-five-step-ux.test.js:fedf825a49244098367f6388", + "routeModelTest": "uapi/tests/readRouteModel.test.ts:47d613cef4ab6dce86846c8f", + "pageTest": "uapi/tests/readPageClient.test.tsx:9bd621c200f02082d0159cc5", + "generator": "scripts/generate-v43-read-route-five-step-ux.mjs:04954a1f66c27e21ef86a08f", + "checker": "scripts/check-v43-gate4-read-route-five-step-ux.mjs:86ba9a96169452ceeb347f10" + }, + "predicateResults": [ + { + "id": "active-canon-pointer-remains-v42", + "sourcePath": "BITCODE_SPEC.txt", + "passed": true + }, + { + "id": "spec-defines-gate4", + "sourcePath": "BITCODE_SPEC_V43.md", + "passed": true + }, + { + "id": "spec-names-read-route-objects", + "sourcePath": "BITCODE_SPEC_V43.md", + "passed": true + }, + { + "id": "delta-records-gate4", + "sourcePath": "BITCODE_SPEC_V43_DELTA.md", + "passed": true + }, + { + "id": "notes-records-gate4", + "sourcePath": "BITCODE_SPEC_V43_NOTES.md", + "passed": true + }, + { + "id": "parity-records-gate4", + "sourcePath": "BITCODE_SPEC_V43_PARITY_MATRIX.md", + "passed": true + }, + { + "id": "roadmap-records-gate4", + "sourcePath": "SPECIFICATIONS_ROADMAP.md", + "passed": true + }, + { + "id": "readme-records-gate4", + "sourcePath": "README.md", + "passed": true + }, + { + "id": "protocol-readme-records-gate4", + "sourcePath": "packages/protocol/README.md", + "passed": true + }, + { + "id": "route-model-defines-read-route-session", + "sourcePath": "uapi/app/read/read-route-model.ts", + "passed": true + }, + { + "id": "route-model-defines-pipeline-ownership", + "sourcePath": "uapi/app/read/read-route-model.ts", + "passed": true + }, + { + "id": "route-model-forbids-source-leakage", + "sourcePath": "uapi/app/read/read-route-model.ts", + "passed": true + }, + { + "id": "read-page-canonical-route", + "sourcePath": "uapi/app/read/page.tsx", + "passed": true + }, + { + "id": "read-client-renders-five-step-route", + "sourcePath": "uapi/app/read/ReadPageClient.tsx", + "passed": true + }, + { + "id": "read-client-reuses-live-workbench", + "sourcePath": "uapi/app/read/ReadPageClient.tsx", + "passed": true + }, + { + "id": "read-client-renders-source-safe-session", + "sourcePath": "uapi/app/read/ReadPageClient.tsx", + "passed": true + }, + { + "id": "read-client-records-activity", + "sourcePath": "uapi/app/read/ReadPageClient.tsx", + "passed": true + }, + { + "id": "terminal-routes-define-read", + "sourcePath": "uapi/app/terminal/terminal-routes.ts", + "passed": true + }, + { + "id": "workbench-preserves-execution-stream", + "sourcePath": "uapi/app/terminal/TerminalDepositReadWorkbench.tsx", + "passed": true + }, + { + "id": "reading-state-preserves-five-steps", + "sourcePath": "uapi/app/terminal/terminal-enterprise-reading-ux-state.ts", + "passed": true + }, + { + "id": "nav-links-to-read", + "sourcePath": "uapi/components/base/bitcode/layout/nav.tsx", + "passed": true + }, + { + "id": "public-shell-recognizes-read", + "sourcePath": "uapi/components/base/bitcode/layout/workspace-surface.ts", + "passed": true + }, + { + "id": "public-copy-uses-read-link", + "sourcePath": "uapi/components/base/bitcode/layout/bitcode-public-copy.ts", + "passed": true + }, + { + "id": "public-explainers-define-read", + "sourcePath": "uapi/components/base/bitcode/layout/bitcode-public-explainers.ts", + "passed": true + }, + { + "id": "footer-links-to-read", + "sourcePath": "uapi/components/base/bitcode/layout/footer.tsx", + "passed": true + }, + { + "id": "uapi-route-model-test-covers-read", + "sourcePath": "uapi/tests/readRouteModel.test.ts", + "passed": true + }, + { + "id": "uapi-page-test-covers-read", + "sourcePath": "uapi/tests/readPageClient.test.tsx", + "passed": true + }, + { + "id": "protocol-test-covers-artifact", + "sourcePath": "packages/protocol/test/v43-read-route-five-step-ux.test.js", + "passed": true + }, + { + "id": "package-exports-gate4", + "sourcePath": "packages/protocol/src/index.js", + "passed": true + }, + { + "id": "package-types-export-gate4", + "sourcePath": "packages/protocol/src/index.d.ts", + "passed": true + }, + { + "id": "package-json-exposes-gate4", + "sourcePath": "package.json", + "passed": true + }, + { + "id": "gate-workflow-runs-gate4", + "sourcePath": ".github/workflows/bitcode-gate-quality.yml", + "passed": true + }, + { + "id": "canon-workflow-runs-gate4", + "sourcePath": ".github/workflows/bitcode-canon-quality.yml", + "passed": true + }, + { + "id": "generator-exists", + "sourcePath": "scripts/generate-v43-read-route-five-step-ux.mjs", + "passed": true + }, + { + "id": "checker-exists", + "sourcePath": "scripts/check-v43-gate4-read-route-five-step-ux.mjs", + "passed": true + } + ], + "coverage": { + "readRouteImplemented": true, + "fiveStepUxImplemented": true, + "readNeedReviewImplemented": true, + "findingFitsRequestImplemented": true, + "assetPackPreviewImplemented": true, + "settlementDeliveryBoundaryImplemented": true, + "executionStreamRetained": true, + "terminalDebugCompatibilityRetained": true, + "acceptedNeedRequiredBeforeFindingFits": true, + "sourceSafePreviewBeforeSettlement": true, + "deliveryRequiresPaidReadRights": true, + "sourceSafeMetadataOnly": true, + "protectedSourceVisible": false, + "rawSourceTextVisible": false, + "sourceSnippetVisible": false, + "rawPromptVisible": false, + "interpolatedPromptVisible": false, + "rawProviderResponseVisible": false, + "unpaidAssetPackSourceVisible": false, + "credentialsSerialized": false, + "walletPrivateMaterialVisible": false, + "settlementPrivatePayloadVisible": false, + "requiredPredicateCount": 35, + "passedPredicateCount": 35, + "failedPredicateIds": [] + } +} diff --git a/.bitcode/v43-route-vocabulary-inventory.json b/.bitcode/v43-route-vocabulary-inventory.json index 16f5ef2c5..9068b463a 100644 --- a/.bitcode/v43-route-vocabulary-inventory.json +++ b/.bitcode/v43-route-vocabulary-inventory.json @@ -5,7 +5,7 @@ "currentTarget": "V42", "sourceSafetyVerdict": "source-safe-route-vocabulary-inventory-metadata", "generatedAt": "deterministic", - "artifactRoot": "v43-route-vocabulary-inventory:b688a0d8808d69db7a52e9a3", + "artifactRoot": "v43-route-vocabulary-inventory:eaf71f2ad8280bd95492dfaf", "passed": true, "tokenIds": [ "route:/exchange", @@ -2081,7 +2081,7 @@ }, { "path": "BITCODE_SPEC_V43.md", - "pathRoot": "v43-route-vocabulary-file:62434c4fdd4c9f3528a85995", + "pathRoot": "v43-route-vocabulary-file:ce66628152ade64c13fde56d", "categories": [ "doc", "spec", @@ -2091,12 +2091,12 @@ "route:/exchange": 5, "route:/terminal": 5, "route:/packs": 17, - "route:/read": 15, + "route:/read": 18, "route:/deposit": 12, "symbol:Exchange": 4, "symbol:Terminal": 2, "symbol:Packs": 5, - "symbol:Reading": 18, + "symbol:Reading": 19, "symbol:Depositing": 6, "symbol:PackActivity": 7, "symbol:DepositAssetPackOption": 3, @@ -2104,14 +2104,14 @@ "word:terminal": 5, "word:self-referential": 4 }, - "totalMatches": 114, + "totalMatches": 118, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "BITCODE_SPEC_V43_DELTA.md", - "pathRoot": "v43-route-vocabulary-file:51a071d9a3afa207dfb4ab0a", + "pathRoot": "v43-route-vocabulary-file:1a4a0d31ddfd6e15643adc83", "categories": [ "doc", "spec", @@ -2121,11 +2121,11 @@ "route:/exchange": 4, "route:/terminal": 2, "route:/packs": 11, - "route:/read": 6, + "route:/read": 8, "route:/deposit": 6, "symbol:Exchange": 2, - "symbol:Terminal": 3, - "symbol:Reading": 5, + "symbol:Terminal": 4, + "symbol:Reading": 6, "symbol:Depositing": 2, "symbol:PackActivity": 2, "symbol:DepositAssetPackOption": 1, @@ -2133,14 +2133,14 @@ "word:terminal": 2, "word:self-referential": 3 }, - "totalMatches": 53, + "totalMatches": 57, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "BITCODE_SPEC_V43_NOTES.md", - "pathRoot": "v43-route-vocabulary-file:6124e4c464a3b6d540784a13", + "pathRoot": "v43-route-vocabulary-file:e6e5a72d3e61ddf3e9114804", "categories": [ "doc", "spec", @@ -2150,12 +2150,12 @@ "route:/exchange": 2, "route:/terminal": 1, "route:/packs": 7, - "route:/read": 3, + "route:/read": 5, "route:/deposit": 3, "symbol:Exchange": 1, "symbol:Terminal": 1, "symbol:Packs": 1, - "symbol:Reading": 3, + "symbol:Reading": 5, "symbol:Depositing": 2, "symbol:PackActivity": 2, "symbol:DepositAssetPackOption": 1, @@ -2163,14 +2163,14 @@ "word:terminal": 1, "word:self-referential": 2 }, - "totalMatches": 32, + "totalMatches": 36, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "BITCODE_SPEC_V43_PARITY_MATRIX.md", - "pathRoot": "v43-route-vocabulary-file:2770ebfb7d7d041cdba2b10b", + "pathRoot": "v43-route-vocabulary-file:7c19070cc1f7520ff19c9a5a", "categories": [ "doc", "spec" @@ -2179,7 +2179,7 @@ "route:/exchange": 3, "route:/terminal": 1, "route:/packs": 6, - "route:/read": 3, + "route:/read": 5, "route:/deposit": 3, "symbol:Packs": 1, "symbol:Reading": 1, @@ -2188,7 +2188,7 @@ "word:terminal": 1, "word:self-referential": 2 }, - "totalMatches": 28, + "totalMatches": 30, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false @@ -2313,7 +2313,7 @@ }, { "path": "README.md", - "pathRoot": "v43-route-vocabulary-file:73b543d9a3b0adf7e22a6607", + "pathRoot": "v43-route-vocabulary-file:a6a889bc09b179592c5b0ada", "categories": [ "doc", "telemetry" @@ -2322,12 +2322,12 @@ "route:/exchange": 5, "route:/terminal": 5, "route:/packs": 9, - "route:/read": 6, + "route:/read": 8, "route:/deposit": 5, "symbol:Exchange": 16, "symbol:Terminal": 31, "symbol:Packs": 3, - "symbol:Reading": 29, + "symbol:Reading": 30, "symbol:Depositing": 5, "symbol:PackActivity": 1, "symbol:DepositAssetPackOption": 1, @@ -2335,14 +2335,14 @@ "word:terminal": 7, "word:self-referential": 2 }, - "totalMatches": 142, + "totalMatches": 145, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "SPECIFICATIONS_ROADMAP.md", - "pathRoot": "v43-route-vocabulary-file:f70d9ecba6970b8e789d6086", + "pathRoot": "v43-route-vocabulary-file:e7927caadf523179bfaa6da0", "categories": [ "doc", "spec", @@ -2353,10 +2353,9 @@ "route:/terminal": 3, "route:/packs": 9, "route:/read": 10, - "route:/deposit": 6, + "route:/deposit": 7, "symbol:Exchange": 25, "symbol:Terminal": 42, - "symbol:Packs": 1, "symbol:Reading": 59, "symbol:Depositing": 10, "symbol:PackActivity": 1, @@ -5853,7 +5852,7 @@ }, { "path": "packages/protocol/README.md", - "pathRoot": "v43-route-vocabulary-file:60fa87c9d02ec336e59f0754", + "pathRoot": "v43-route-vocabulary-file:6f26321e8ae41a9daaf0318f", "categories": [ "doc", "package", @@ -5863,19 +5862,19 @@ "route:/exchange": 4, "route:/terminal": 2, "route:/packs": 6, - "route:/read": 4, + "route:/read": 5, "route:/deposit": 2, "symbol:Exchange": 17, "symbol:Terminal": 29, "symbol:Packs": 1, - "symbol:Reading": 33, + "symbol:Reading": 34, "symbol:Depositing": 5, "symbol:PackActivity": 1, "word:exchange": 16, "word:terminal": 6, "word:self-referential": 1 }, - "totalMatches": 127, + "totalMatches": 129, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false @@ -7090,6 +7089,25 @@ "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, + { + "path": "packages/protocol/src/canonical/v43-read-route-five-step-ux.js", + "pathRoot": "v43-route-vocabulary-file:3ccff3998243a1b97320bb01", + "categories": [ + "package", + "telemetry" + ], + "tokenCounts": { + "route:/terminal": 3, + "route:/read": 14, + "symbol:Terminal": 1, + "symbol:Reading": 4, + "word:terminal": 6 + }, + "totalMatches": 28, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, { "path": "packages/protocol/src/canonical/v43-route-vocabulary-inventory.js", "pathRoot": "v43-route-vocabulary-file:157fd43a1b0d92c5e4c6d901", @@ -7579,6 +7597,22 @@ "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, + { + "path": "packages/protocol/test/v43-read-route-five-step-ux.test.js", + "pathRoot": "v43-route-vocabulary-file:806897d387b0667a60afa16b", + "categories": [ + "package", + "telemetry", + "test" + ], + "tokenCounts": { + "symbol:Reading": 1 + }, + "totalMatches": 1, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, { "path": "packages/protocol/test/v43-route-vocabulary-inventory.test.js", "pathRoot": "v43-route-vocabulary-file:eea96e35784ef80a95367858", @@ -9820,6 +9854,24 @@ "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, + { + "path": "scripts/check-v43-gate4-read-route-five-step-ux.mjs", + "pathRoot": "v43-route-vocabulary-file:da4ac3fe0a23b346fb67f8ec", + "categories": [ + "script", + "telemetry" + ], + "tokenCounts": { + "route:/terminal": 3, + "route:/read": 4, + "symbol:Reading": 2, + "word:terminal": 5 + }, + "totalMatches": 14, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, { "path": "scripts/code-review/README.md", "pathRoot": "v43-route-vocabulary-file:fef2886ffeed000a3327379c", @@ -11438,6 +11490,55 @@ "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, + { + "path": "uapi/app/read/ReadPageClient.tsx", + "pathRoot": "v43-route-vocabulary-file:5c1c473c8882148acde2c91d", + "categories": [ + "component", + "telemetry" + ], + "tokenCounts": { + "route:/terminal": 11, + "route:/packs": 1, + "route:/read": 1, + "symbol:Reading": 10, + "word:terminal": 24 + }, + "totalMatches": 47, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, + { + "path": "uapi/app/read/page.tsx", + "pathRoot": "v43-route-vocabulary-file:f1683a164e5711d6c44ca7bb", + "categories": [ + "component", + "route" + ], + "tokenCounts": { + "route:/read": 1, + "symbol:Reading": 2 + }, + "totalMatches": 3, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, + { + "path": "uapi/app/read/read-route-model.ts", + "pathRoot": "v43-route-vocabulary-file:7ed5cb28ad6ce5145fde430c", + "categories": [], + "tokenCounts": { + "route:/terminal": 1, + "route:/read": 3, + "word:terminal": 2 + }, + "totalMatches": 6, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, { "path": "uapi/app/terminal/DemonstrationWitnessHost.tsx", "pathRoot": "v43-route-vocabulary-file:f8afe98c7e64ad58029103f7", @@ -12170,10 +12271,10 @@ }, { "path": "uapi/app/terminal/terminal-enterprise-reading-ux-state.ts", - "pathRoot": "v43-route-vocabulary-file:45f29adac7d2ef8957565abd", + "pathRoot": "v43-route-vocabulary-file:a378af5edaf9522d2acf8dd7", "categories": [], "tokenCounts": { - "symbol:Terminal": 2, + "symbol:Reading": 2, "word:terminal": 4 }, "totalMatches": 6, @@ -12323,14 +12424,15 @@ }, { "path": "uapi/app/terminal/terminal-routes.ts", - "pathRoot": "v43-route-vocabulary-file:5b2837a2fd797d37c7e1cc22", + "pathRoot": "v43-route-vocabulary-file:5ae21a330067f5fa7dfa9242", "categories": [], "tokenCounts": { "route:/terminal": 1, "route:/packs": 1, + "route:/read": 1, "word:terminal": 1 }, - "totalMatches": 3, + "totalMatches": 4, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false @@ -12750,42 +12852,44 @@ }, { "path": "uapi/components/base/bitcode/layout/bitcode-public-copy.ts", - "pathRoot": "v43-route-vocabulary-file:412ecc29f4428f038c5886e1", + "pathRoot": "v43-route-vocabulary-file:646809312925807d1f21caca", "categories": [ "component" ], "tokenCounts": { - "route:/terminal": 2, + "route:/terminal": 1, "route:/packs": 1, + "route:/read": 2, "symbol:Exchange": 1, - "symbol:Terminal": 11, + "symbol:Terminal": 10, "symbol:Packs": 3, - "word:terminal": 2 + "word:terminal": 1 }, - "totalMatches": 20, + "totalMatches": 19, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "uapi/components/base/bitcode/layout/bitcode-public-explainers.ts", - "pathRoot": "v43-route-vocabulary-file:28f8f398092588ce2a6bca66", + "pathRoot": "v43-route-vocabulary-file:1b3d3ce34897b3eabcb8c8af", "categories": [ "component", "telemetry" ], "tokenCounts": { "symbol:Terminal": 6, - "symbol:Packs": 3 + "symbol:Packs": 3, + "symbol:Reading": 2 }, - "totalMatches": 9, + "totalMatches": 11, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "uapi/components/base/bitcode/layout/footer.tsx", - "pathRoot": "v43-route-vocabulary-file:3000d2c67388973f3286435f", + "pathRoot": "v43-route-vocabulary-file:afb21919438b54679e7b0268", "categories": [ "component", "telemetry" @@ -12793,18 +12897,20 @@ "tokenCounts": { "route:/terminal": 1, "route:/packs": 1, + "route:/read": 1, "symbol:Terminal": 2, "symbol:Packs": 1, + "symbol:Reading": 1, "word:terminal": 3 }, - "totalMatches": 8, + "totalMatches": 10, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "uapi/components/base/bitcode/layout/nav.tsx", - "pathRoot": "v43-route-vocabulary-file:4aed694260e75cefbe14e7fb", + "pathRoot": "v43-route-vocabulary-file:c54a92d1d257e0d7723e08d5", "categories": [ "component", "telemetry" @@ -12813,20 +12919,21 @@ "route:/exchange": 2, "route:/terminal": 4, "route:/packs": 3, + "route:/read": 1, "symbol:Terminal": 1, "symbol:Packs": 1, "symbol:Reading": 1, "word:exchange": 4, "word:terminal": 8 }, - "totalMatches": 24, + "totalMatches": 25, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "uapi/components/base/bitcode/layout/workspace-surface.ts", - "pathRoot": "v43-route-vocabulary-file:ba39215e224d7b3caa1774b2", + "pathRoot": "v43-route-vocabulary-file:ccd540d4cf278c1df7c76c0d", "categories": [ "component" ], @@ -12834,10 +12941,11 @@ "route:/exchange": 1, "route:/terminal": 1, "route:/packs": 1, + "route:/read": 1, "word:exchange": 1, "word:terminal": 3 }, - "totalMatches": 7, + "totalMatches": 8, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false @@ -14028,7 +14136,7 @@ }, { "path": "uapi/tests/footerPublicShell.test.tsx", - "pathRoot": "v43-route-vocabulary-file:edb631b94192c2da190f6ee3", + "pathRoot": "v43-route-vocabulary-file:bf8039afbee4702eee091b81", "categories": [ "component", "telemetry", @@ -14037,30 +14145,32 @@ "tokenCounts": { "route:/terminal": 1, "route:/packs": 1, + "route:/read": 1, "symbol:Terminal": 3, "symbol:Packs": 2, + "symbol:Reading": 1, "word:terminal": 1 }, - "totalMatches": 8, + "totalMatches": 10, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, { "path": "uapi/tests/marketingLandingPage.test.tsx", - "pathRoot": "v43-route-vocabulary-file:396e2bcdfd734c71da6ee829", + "pathRoot": "v43-route-vocabulary-file:df81318f9d51cb8a9316408a", "categories": [ "component", "telemetry", "test" ], "tokenCounts": { - "route:/terminal": 3, + "route:/terminal": 2, + "route:/read": 1, "symbol:Exchange": 1, - "symbol:Terminal": 1, - "word:terminal": 3 + "word:terminal": 2 }, - "totalMatches": 8, + "totalMatches": 6, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false @@ -14100,7 +14210,7 @@ }, { "path": "uapi/tests/navPublicShell.test.tsx", - "pathRoot": "v43-route-vocabulary-file:faa43e8aaf2e3b9832734a9a", + "pathRoot": "v43-route-vocabulary-file:a27152a1a8389fff1b870985", "categories": [ "component", "telemetry", @@ -14109,12 +14219,13 @@ "tokenCounts": { "route:/terminal": 2, "route:/packs": 3, + "route:/read": 4, "symbol:Terminal": 4, "symbol:Packs": 5, "symbol:Reading": 1, "word:terminal": 2 }, - "totalMatches": 17, + "totalMatches": 21, "sourceSafeMetadataOnly": true, "rawSourceTextSerialized": false, "sourceSnippetSerialized": false @@ -14275,6 +14386,39 @@ "rawSourceTextSerialized": false, "sourceSnippetSerialized": false }, + { + "path": "uapi/tests/readPageClient.test.tsx", + "pathRoot": "v43-route-vocabulary-file:fb00863fdbde452acfa6d023", + "categories": [ + "component", + "telemetry", + "test" + ], + "tokenCounts": { + "route:/terminal": 4, + "route:/read": 2, + "symbol:Reading": 5, + "word:terminal": 7 + }, + "totalMatches": 18, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, + { + "path": "uapi/tests/readRouteModel.test.ts", + "pathRoot": "v43-route-vocabulary-file:fb15e6d162f430fc0e8c9868", + "categories": [ + "test" + ], + "tokenCounts": { + "route:/read": 2 + }, + "totalMatches": 2, + "sourceSafeMetadataOnly": true, + "rawSourceTextSerialized": false, + "sourceSnippetSerialized": false + }, { "path": "uapi/tests/readingOperationalTelemetryPipelineLog.test.tsx", "pathRoot": "v43-route-vocabulary-file:5e6e6c7c081645e43b0d5a4a", @@ -15251,34 +15395,34 @@ } ], "coverage": { - "sourceFileCount": 897, + "sourceFileCount": 905, "tokenTotals": { "route:/exchange": 147, - "route:/terminal": 1435, - "route:/packs": 151, - "route:/read": 242, - "route:/deposit": 84, + "route:/terminal": 1455, + "route:/packs": 152, + "route:/read": 293, + "route:/deposit": 85, "symbol:Exchange": 1964, - "symbol:Terminal": 3092, - "symbol:Packs": 51, - "symbol:Reading": 1288, + "symbol:Terminal": 3090, + "symbol:Packs": 50, + "symbol:Reading": 1324, "symbol:Depositing": 234, "symbol:PackActivity": 28, "symbol:DepositAssetPackOption": 12, "word:exchange": 1708, - "word:terminal": 3655, + "word:terminal": 3697, "word:self-referential": 30 }, "categoryTotals": { - "route": 12, - "component": 144, - "test": 215, + "route": 13, + "component": 147, + "test": 218, "doc": 169, "api": 43, - "telemetry": 647, + "telemetry": 652, "workflow": 12, - "script": 117, - "package": 309, + "script": 118, + "package": 311, "spec": 76 }, "routeVocabularyInventoryComplete": true, diff --git a/.github/workflows/bitcode-canon-quality.yml b/.github/workflows/bitcode-canon-quality.yml index 0fb18c830..2fc91ee8d 100644 --- a/.github/workflows/bitcode-canon-quality.yml +++ b/.github/workflows/bitcode-canon-quality.yml @@ -334,6 +334,9 @@ jobs: if [ -f scripts/check-v43-gate3-packs-activity-master-detail.mjs ]; then node scripts/check-v43-gate3-packs-activity-master-detail.mjs --skip-branch-check --skip-uapi-tests fi + if [ -f scripts/check-v43-gate4-read-route-five-step-ux.mjs ]; then + node scripts/check-v43-gate4-read-route-five-step-ux.mjs --skip-branch-check --skip-uapi-tests + fi fi else echo "Unexpected BITCODE_SPEC.txt pointer: $POINTER" >&2 diff --git a/.github/workflows/bitcode-gate-quality.yml b/.github/workflows/bitcode-gate-quality.yml index 1a0cd699e..5b6b3aecb 100644 --- a/.github/workflows/bitcode-gate-quality.yml +++ b/.github/workflows/bitcode-gate-quality.yml @@ -463,6 +463,9 @@ jobs: if [ -f scripts/check-v43-gate3-packs-activity-master-detail.mjs ]; then node scripts/check-v43-gate3-packs-activity-master-detail.mjs --skip-branch-check --skip-uapi-tests fi + if [ -f scripts/check-v43-gate4-read-route-five-step-ux.mjs ]; then + node scripts/check-v43-gate4-read-route-five-step-ux.mjs --skip-branch-check --skip-uapi-tests + fi fi else echo "Unexpected BITCODE_SPEC.txt pointer: $POINTER" >&2 @@ -502,6 +505,9 @@ jobs: if [ -f scripts/check-v43-gate3-packs-activity-master-detail.mjs ]; then node scripts/check-v43-gate3-packs-activity-master-detail.mjs --skip-branch-check --skip-uapi-tests fi + if [ -f scripts/check-v43-gate4-read-route-five-step-ux.mjs ]; then + node scripts/check-v43-gate4-read-route-five-step-ux.mjs --skip-branch-check --skip-uapi-tests + fi pnpm --filter @bitcode/protocol test pnpm --filter @bitcode/btd exec jest --config jest.config.cjs --runInBand --forceExit pnpm --filter @bitcode/pipeline-asset-pack exec jest --config jest.config.cjs --passWithNoTests --forceExit diff --git a/BITCODE_SPEC_V43.md b/BITCODE_SPEC_V43.md index c9cb9fa19..6783496fa 100644 --- a/BITCODE_SPEC_V43.md +++ b/BITCODE_SPEC_V43.md @@ -156,6 +156,15 @@ withheld. Gate 4 must move the Reading default experience into `/read`: request read, review synthesized Need, request Finding Fits, review source-safe AssetPack preview, and settle/buy/deliver. It must preserve V42 telemetry, execution log streaming, proof expansion, retry/restart, and failure repair. +Gate 4 closes with `ReadRouteSession`, `/read`, and +`V43ReadRouteFiveStepUx`. `/read` owns the route query state, five visible +Reading stages, source-safe session readback, Need review posture, +accepted-Need gating for the Finding Fits request, AssetPack preview disclosure, BTC quote +and settlement posture, and delivery unlock posture. The retained debug +cockpit remains compatible but cannot bypass `/read` invariants: no Finding +Fits without an accepted Need, no source-bearing AssetPack preview before +settlement, and no repository delivery before paid read rights are proven. + ## V43 Gate 5 Deposit Route And Agentic AssetPack Option Synthesis Gate 5 must move the Depositing default experience into `/deposit` and introduce the deposit AssetPack option synthesis pipeline. The pipeline must use connected source, depositor instructions, Depository state, and Reading demand to propose multiple source-safe AssetPack options for review. diff --git a/BITCODE_SPEC_V43_DELTA.md b/BITCODE_SPEC_V43_DELTA.md index 6b4cc5cca..a7b34655c 100644 --- a/BITCODE_SPEC_V43_DELTA.md +++ b/BITCODE_SPEC_V43_DELTA.md @@ -69,6 +69,18 @@ delivery/repair readback, and explicit source-safety flags without serializing protected source, unpaid AssetPack source, raw prompts, provider responses, credentials, wallet private material, or private settlement payloads. +## Gate 4 delta closure + +Gate 4 adds `V43ReadRouteFiveStepUx`, the generated +`.bitcode/v43-read-route-five-step-ux.json` artifact, package exports, +protocol tests, workflow checks, `ReadRouteSession`, `/read`, and focused route +tests. `/read` is now the default Reading path: request Read, review +synthesized Need, request Finding Fits, review source-safe AssetPack preview, +and settle/buy/deliver. Finding Fits remains blocked until a Need is accepted, +preview remains metadata-only before settlement, and delivery remains locked +until paid read rights are proven. The retained Terminal workbench stays +debug-compatible and continues to provide execution stream readback. + ## Commit-Body Direction V43 gate commits should state the route/product surface changed, the protocol objects preserved, the proof/test commands run, and the source-safety boundaries maintained. Gate PR titles must begin with `V43 Gate N:`. diff --git a/BITCODE_SPEC_V43_NOTES.md b/BITCODE_SPEC_V43_NOTES.md index b88b8c641..e3515b528 100644 --- a/BITCODE_SPEC_V43_NOTES.md +++ b/BITCODE_SPEC_V43_NOTES.md @@ -77,3 +77,19 @@ explicitly source-safe: no protected source, unpaid AssetPack source, source snippets, raw prompts, interpolated prompts, raw provider responses, credentials, wallet private material, or private settlement payloads may cross this boundary. + +## Gate 4 read route extraction note + +Gate 4 makes `/read` the direct product path for enterprise Reading. The route +owns `ReadRouteSession` and the five-stage user flow: request Read, review the +synthesized Need, request Finding Fits, review the source-safe AssetPack +preview, and settle for delivery. It reuses the current live Reading workbench +and rich execution stream so runtime behavior stays connected to V42 pipeline +reality while the route vocabulary becomes clearer. + +The source-safety law is unchanged: `/read` may show request summaries, Need +measurements, fit ids, proof roots, quality posture, BTC fee quotes, +settlement state, and delivery posture before settlement. It must not show +protected source, unpaid AssetPack source, raw prompts, interpolated prompts, +raw provider responses, wallet private material, private settlement payloads, +or ledger write authority before paid read rights are proven. diff --git a/BITCODE_SPEC_V43_PARITY_MATRIX.md b/BITCODE_SPEC_V43_PARITY_MATRIX.md index e0950fd32..c67c67e06 100644 --- a/BITCODE_SPEC_V43_PARITY_MATRIX.md +++ b/BITCODE_SPEC_V43_PARITY_MATRIX.md @@ -25,7 +25,7 @@ Audit V43 against `BITCODE_SPEC_V43.md`, V42 active canon, route source, package | Gate 1 roadmap | Active V42 / draft V43 posture, route vocabulary, gate plan, docs, checker, workflows | `BITCODE_SPEC_V43.md`, `scripts/check-v43-gate1-packs-read-deposit-roadmap.mjs` | drafted | | Route vocabulary | `/exchange` to `/packs` and `/terminal` to `/read`/`/deposit` are inventoried with route vocabulary inventory, migration matrix, retained debug cockpit boundary, redirect compatibility, self-referential copy audit, and source-safe file/token counts | `packages/protocol/src/canonical/v43-route-vocabulary-inventory.js`, `.bitcode/v43-route-vocabulary-inventory.json`, `scripts/check-v43-gate2-route-vocabulary-inventory.mjs` | implemented | | Packs master-detail | Searchable, sortable, filterable pack activity table, source-safe detail route, proof-root display, settlement/compensation/delivery/repair readback, and `/exchange` compatibility redirect | `packages/protocol/src/canonical/v43-packs-activity-master-detail.js`, `.bitcode/v43-packs-activity-master-detail.json`, `uapi/app/packs`, `uapi/app/api/packs/activity/route.ts`, `uapi/components/base/bitcode/activity/pack-activity-model.ts` | implemented | -| Read route | Five-step Reading UX owns Read Request through settlement/delivery | future Gate 4 artifact | draft-required | +| Read route | `ReadRouteSession` and five-step Reading UX own Read Request, synthesized Need review, accepted-Need-gated Finding Fits, source-safe AssetPack preview, BTC settlement, and delivery posture | `packages/protocol/src/canonical/v43-read-route-five-step-ux.js`, `.bitcode/v43-read-route-five-step-ux.json`, `uapi/app/read`, `uapi/app/read/read-route-model.ts` | implemented | | Deposit route | Connected-source deposit AssetPack option synthesis and review | future Gate 5 artifact | draft-required | | Criticality/ROI policy | Source criticality, demand, ROI, BTD potential, and compensation posture | future Gate 6 artifact | draft-required | | Admission sync | Approved deposit options enter Depository and `/packs` activity | future Gate 7 artifact | draft-required | @@ -43,7 +43,8 @@ Audit V43 against `BITCODE_SPEC_V43.md`, V42 active canon, route source, package | Documentation | README and roadmap name V43 route/product scope | drafted | | Gate 2 package proof | `V43RouteVocabularyInventory` exports, generated artifact, package test, workflow checks, and `check:v43-gate2` exist | implemented | | Gate 3 PackActivity proof | `V43PacksActivityMasterDetail` exports, generated artifact, source-safe model/API/UI tests, workflow checks, and `check:v43-gate3` exist | implemented | -| Implementation | Route and pipeline source changes are not part of Gates 1 or 2; Gate 3 implements only `/packs` and PackActivity, not `/read` or `/deposit` extraction | accepted boundary | +| Gate 4 Read route proof | `V43ReadRouteFiveStepUx` exports, generated artifact, source-safe route model/UI tests, workflow checks, and `check:v43-gate4` exist | implemented | +| Implementation | Route and pipeline source changes are not part of Gates 1 or 2; Gate 3 implements only `/packs` and PackActivity; Gate 4 implements `/read` extraction, not `/deposit` extraction | accepted boundary | ## V43 accepted boundaries diff --git a/README.md b/README.md index a094d742e..3e72d1c24 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,17 @@ state readback, and no-source leak tests. The Packs projection remains source-safe metadata only and does not serialize protected source, unpaid AssetPack source, raw prompts, provider responses, credentials, wallet private material, or private settlement payloads. +V43 Gate 4 adds `V43ReadRouteFiveStepUx`, +`.bitcode/v43-read-route-five-step-ux.json`, +`generate:v43-read-route-five-step-ux`, +`check:v43-read-route-five-step-ux`, and `check:v43-gate4`. It implements +`ReadRouteSession`, `/read`, source-safe route state, the five-step Reading UX, +Need review, accepted-Need-gated Finding Fits, source-safe AssetPack preview, +BTC settlement/delivery posture, retained execution stream readback, nav/footer +route ownership, and focused tests. `/read` does not expose protected source, +unpaid AssetPack source, raw prompts, interpolated prompts, raw provider +responses, wallet private material, or private settlement payloads before paid +read rights are proven. Exchange is inherited V36 canon: market-wide activity master-detail, buy/sell/ bid/ask/cancel/accept/settle/history flows, AssetPack range trading, diff --git a/SPECIFICATIONS_ROADMAP.md b/SPECIFICATIONS_ROADMAP.md index 1d3c1c72e..4422eb7cb 100644 --- a/SPECIFICATIONS_ROADMAP.md +++ b/SPECIFICATIONS_ROADMAP.md @@ -5,8 +5,8 @@ - Current active canonical pointer: `BITCODE_SPEC.txt` -> `V42` - Current active canon: `BITCODE_SPEC_V42.md` - Current draft target: `BITCODE_SPEC_V43.md`. -- Current working gate: V43 Gate 3 Packs Activity Master-Detail Data Model. -- Next queued work after V43 Gate 3: `/read` route extraction and five-step Reading UX. +- Current working gate: V43 Gate 4 Read Route Extraction And Five-Step UX. +- Next queued work after V43 Gate 4: `/deposit` route extraction and deposit AssetPack option synthesis. - Latest closed version: V42 Reliable MVP Experience, which promoted shortest-path Depositing, five-step Reading, ReadNeed review/resynthesis, ReadFitsFinding source-safe preview and quote, settlement rights transfer, repository delivery, AI-reading demonstration, local/staging MVP rehearsal, and V42 promotion readiness. - Recent V42 canonical promotion anchor: V42 canonical promotion updated `BITCODE_SPEC.txt` to `V42`, generated `BITCODE_SPEC_V42_PROVEN.md`, preserved active V42 / draft V43 runtime posture, and closed reliable MVP experience canon. - Recent V42 opening anchor: reliable MVP experience opens over promoted V41 with V42 SPEC, DELTA, NOTES, and PARITY files, `check:v42-gate1`, active V41 / draft V42 posture, and a nine-gate plan for shortest-path Depositing, five-step Reading, ReadNeed product closure, ReadFitsFinding preview and quote closure, settlement and repository delivery, AI-reading demonstration, local/staging rehearsal, and promotion readiness. @@ -20,6 +20,7 @@ - V42 Gate 9 closure anchor: reliable MVP experience now owns package-backed `V42PromotionReadinessReport`, deterministic `.bitcode/v42-promotion-readiness-report.json`, `BITCODE_SPEC_V42_PROVEN.md` generation support, `v42-canon-promotion.yml`, promotion command dry-run support, gate/canon workflow posture, active V42 / draft V43 runtime preparation, all V42 reliable MVP artifacts covered, source-safe, parseable, and workflow-bound, and value-bearing mainnet admission blocked through `check:v42-gate9`. - V43 Gate 2 closure anchor: route-product cleanup now owns package-backed `V43RouteVocabularyInventory`, deterministic `.bitcode/v43-route-vocabulary-inventory.json`, source-safe file/token counts across `/exchange`, Exchange, `/terminal`, Terminal, `/packs`, `/read`, `/deposit`, Reading, Depositing, PackActivity, DepositAssetPackOption, and self-referential copy references, a migration matrix for `/packs`, `/read`, `/deposit`, retained debug cockpit boundaries, redirect compatibility, and copy cleanup, package exports, protocol tests, workflow wiring, and `check:v43-gate2`. - V43 Gate 3 closure anchor: route-product cleanup now owns package-backed `V43PacksActivityMasterDetail`, deterministic `.bitcode/v43-packs-activity-master-detail.json`, `PackActivityRecord`, `PacksActivityDetail`, `/api/packs/activity`, `/packs` master-detail UI, `/exchange` compatibility redirect, search, type/state filtering, column sorting, proof-root display, settlement/compensation/delivery/repair readback, no-source leak tests, package exports, protocol tests, workflow wiring, and `check:v43-gate3`. +- V43 Gate 4 closure anchor: route-product cleanup now owns package-backed `V43ReadRouteFiveStepUx`, deterministic `.bitcode/v43-read-route-five-step-ux.json`, `ReadRouteSession`, `/read`, five-step Reading UX, Read Request, synthesized Need review, accepted-Need-gated Finding Fits, source-safe AssetPack preview, BTC settlement/delivery posture, retained execution stream readback, route/nav/footer ownership, focused UAPI/protocol tests, workflow wiring, and `check:v43-gate4`. - Recent V41 closure anchor: V41 canonical promotion updated `BITCODE_SPEC.txt` to `V41`, generated `BITCODE_SPEC_V41_PROVEN.md`, preserved active V41 / draft V42 runtime posture, and closed prompt-program excellence canon. - Recent V40 closure anchor: V40 canonical promotion updated `BITCODE_SPEC.txt` to `V40`, generated `BITCODE_SPEC_V40_PROVEN.md`, preserved active V40 / draft V41 runtime posture, and closed exhaustive commercial application testing canon. - Recent V39 closure anchor: V39 canonical promotion updated `BITCODE_SPEC.txt` to `V39`, generated `BITCODE_SPEC_V39_PROVEN.md`, preserved active V39 / draft V40 runtime posture, and closed commercial Reading readiness canon. diff --git a/package.json b/package.json index cd7b7f2ed..04e3bfb80 100644 --- a/package.json +++ b/package.json @@ -336,6 +336,9 @@ "generate:v43-packs-activity-master-detail": "node scripts/generate-v43-packs-activity-master-detail.mjs", "check:v43-packs-activity-master-detail": "node scripts/generate-v43-packs-activity-master-detail.mjs --check", "check:v43-gate3": "node scripts/check-v43-gate3-packs-activity-master-detail.mjs", + "generate:v43-read-route-five-step-ux": "node scripts/generate-v43-read-route-five-step-ux.mjs", + "check:v43-read-route-five-step-ux": "node scripts/generate-v43-read-route-five-step-ux.mjs --check", + "check:v43-gate4": "node scripts/check-v43-gate4-read-route-five-step-ux.mjs", "generate:v38-inference-surface-inventory": "node scripts/generate-v38-inference-surface-inventory.mjs", "check:v38-inference-surface-inventory": "node scripts/generate-v38-inference-surface-inventory.mjs --check", "check:v38-gate2": "node scripts/check-v38-gate2-inference-surface-inventory.mjs", diff --git a/packages/protocol/README.md b/packages/protocol/README.md index bae7d4c64..696df13c6 100644 --- a/packages/protocol/README.md +++ b/packages/protocol/README.md @@ -96,6 +96,17 @@ V43 Gate 3 adds `V43PacksActivityMasterDetail` through display, settlement/compensation/delivery/repair state readback, source-safe metadata expansion, and no-source leak tests. +V43 Gate 4 adds `V43ReadRouteFiveStepUx` through +`packages/protocol/src/canonical/v43-read-route-five-step-ux.js`, +`packages/protocol/test/v43-read-route-five-step-ux.test.js`, +`.bitcode/v43-read-route-five-step-ux.json`, +`generate:v43-read-route-five-step-ux`, +`check:v43-read-route-five-step-ux`, and `check:v43-gate4`. It binds +`ReadRouteSession`, `/read`, five-step Reading UX, Need review, +accepted-Need-gated Finding Fits, source-safe AssetPack preview, BTC +settlement/delivery posture, retained execution stream readback, route +navigation, and no-source leak tests. + Historical V39 promotion moved this package through the `V39` active, `V40` draft posture. V40 promotion has since advanced the current package posture to `V40` active, `V41` draft. diff --git a/packages/protocol/src/canonical/v43-packs-activity-master-detail.js b/packages/protocol/src/canonical/v43-packs-activity-master-detail.js index 90eebad2e..3cddeed2c 100644 --- a/packages/protocol/src/canonical/v43-packs-activity-master-detail.js +++ b/packages/protocol/src/canonical/v43-packs-activity-master-detail.js @@ -196,7 +196,7 @@ function buildPredicateResults(repoRoot) { predicateResult('delta-records-gate3', SOURCE_ROOTS.delta, sources.delta.includes('Gate 3') && sources.delta.includes('v43-packs-activity-master-detail')), predicateResult('notes-records-gate3', SOURCE_ROOTS.notes, sources.notes.includes('Gate 3') && sources.notes.includes('/api/packs/activity')), predicateResult('parity-records-gate3', SOURCE_ROOTS.parity, sources.parity.includes('v43-packs-activity-master-detail')), - predicateResult('roadmap-records-gate3', SOURCE_ROOTS.roadmap, sources.roadmap.includes('Current working gate: V43 Gate 3')), + predicateResult('roadmap-records-gate3', SOURCE_ROOTS.roadmap, sources.roadmap.includes('V43 Gate 3 closure anchor')), predicateResult('readme-records-gate3', SOURCE_ROOTS.readme, sources.readme.includes('V43 Gate 3')), predicateResult('protocol-readme-records-gate3', SOURCE_ROOTS.protocolReadme, sources.protocolReadme.includes('V43 Gate 3')), predicateResult('model-defines-packactivity-record', SOURCE_ROOTS.model, sources.model.includes('export interface PackActivityRecord')), diff --git a/packages/protocol/src/canonical/v43-read-route-five-step-ux.js b/packages/protocol/src/canonical/v43-read-route-five-step-ux.js new file mode 100644 index 000000000..da67734c1 --- /dev/null +++ b/packages/protocol/src/canonical/v43-read-route-five-step-ux.js @@ -0,0 +1,271 @@ +// @ts-check + +import crypto from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const DEFAULT_REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..'); + +export const V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH = '.bitcode/v43-read-route-five-step-ux.json'; +export const V43_READ_ROUTE_FIVE_STEP_UX_SCHEMA_ID = 'bitcode.v43.readRouteFiveStepUx.v1'; +export const V43_READ_ROUTE_FIVE_STEP_UX_VERSION = 'V43'; +export const V43_READ_ROUTE_FIVE_STEP_UX_CURRENT_TARGET = 'V42'; +export const V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_SAFETY_VERDICT = 'source-safe-read-route-five-step-metadata'; + +export const V43_READ_ROUTE_STEP_IDS = Object.freeze([ + 'request-read', + 'review-synthesized-need', + 'request-fit', + 'review-synthesized-asset-pack', + 'buy-asset-pack-settle', +]); + +export const V43_READ_ROUTE_OBJECT_IDS = Object.freeze([ + 'ReadRouteSession', + 'Read Request', + 'synthesized Need', + 'Need review decision', + 'Finding Fits request', + 'AssetPack preview', + 'settlement quote', + 'delivery receipt', +]); + +export const V43_READ_ROUTE_PIPELINE_IDS = Object.freeze([ + 'ReadNeedComprehensionSynthesis', + 'ReadFitsFindingSynthesis', +]); + +export const V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS = Object.freeze([ + 'read_request_summary', + 'read_need_measurements', + 'need_feedback_history', + 'depository_candidate_counts', + 'selected_fit_ids', + 'asset_pack_measurements', + 'quality_posture', + 'proof_roots', + 'btc_fee_quote', + 'settlement_state', + 'delivery_posture', +]); + +export const V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS = Object.freeze([ + 'protected_source_payload', + 'raw_protected_prompt', + 'raw_provider_response', + 'unpaid_assetpack_source', + 'wallet_private_material', + 'settlement_private_payload', + 'ledger_write_authority', +]); + +const SOURCE_ROOTS = Object.freeze({ + activePointer: 'BITCODE_SPEC.txt', + spec: 'BITCODE_SPEC_V43.md', + delta: 'BITCODE_SPEC_V43_DELTA.md', + notes: 'BITCODE_SPEC_V43_NOTES.md', + parity: 'BITCODE_SPEC_V43_PARITY_MATRIX.md', + roadmap: 'SPECIFICATIONS_ROADMAP.md', + readme: 'README.md', + protocolReadme: 'packages/protocol/README.md', + packageJson: 'package.json', + gateWorkflow: '.github/workflows/bitcode-gate-quality.yml', + canonWorkflow: '.github/workflows/bitcode-canon-quality.yml', + routeModel: 'uapi/app/read/read-route-model.ts', + page: 'uapi/app/read/page.tsx', + client: 'uapi/app/read/ReadPageClient.tsx', + terminalRoutes: 'uapi/app/terminal/terminal-routes.ts', + terminalWorkbench: 'uapi/app/terminal/TerminalDepositReadWorkbench.tsx', + terminalReadingState: 'uapi/app/terminal/terminal-enterprise-reading-ux-state.ts', + nav: 'uapi/components/base/bitcode/layout/nav.tsx', + workspaceSurface: 'uapi/components/base/bitcode/layout/workspace-surface.ts', + publicCopy: 'uapi/components/base/bitcode/layout/bitcode-public-copy.ts', + publicExplainers: 'uapi/components/base/bitcode/layout/bitcode-public-explainers.ts', + footer: 'uapi/components/base/bitcode/layout/footer.tsx', + packageIndex: 'packages/protocol/src/index.js', + packageTypes: 'packages/protocol/src/index.d.ts', + packageTest: 'packages/protocol/test/v43-read-route-five-step-ux.test.js', + routeModelTest: 'uapi/tests/readRouteModel.test.ts', + pageTest: 'uapi/tests/readPageClient.test.tsx', + generator: 'scripts/generate-v43-read-route-five-step-ux.mjs', + checker: 'scripts/check-v43-gate4-read-route-five-step-ux.mjs', +}); + +function digest(value) { + return crypto.createHash('sha256').update(value).digest('hex').slice(0, 24); +} + +function readSource(repoRoot, sourcePath) { + const absolutePath = path.join(repoRoot, sourcePath); + return existsSync(absolutePath) ? readFileSync(absolutePath, 'utf8') : ''; +} + +function sourceRoot(sourcePath) { + return `${sourcePath}:${digest(readSource(DEFAULT_REPO_ROOT, sourcePath))}`; +} + +function predicateResult(id, sourcePath, passed) { + return { id, sourcePath, passed: Boolean(passed) }; +} + +export const V43_READ_ROUTE_CONTRACT_ROWS = Object.freeze([ + { + rowId: 'read-route-session', + owner: SOURCE_ROOTS.routeModel, + contract: + 'ReadRouteSession owns the /read route state, five Reading steps, accepted-Need gate, source-safe preview boundary, settlement quote, and delivery unlock posture.', + requiredFields: ['schema', 'route', 'steps', 'readObjects', 'pipelineOwnership', 'disclosure', 'proofRoot'], + }, + { + rowId: 'read-route-page', + owner: SOURCE_ROOTS.page, + contract: + '/read is the default Reading route with metadata, public shell chrome, Suspense fallback, and ReadPageClient.', + requiredFields: ['canonical', 'ReadPageClient', 'PublicShellFrame'], + }, + { + rowId: 'read-route-client', + owner: SOURCE_ROOTS.client, + contract: + 'ReadPageClient renders request-read, Need review, Finding Fits request, source-safe preview, settlement/delivery posture, route-owned activity readback, and live workbench controls.', + requiredFields: [ + 'TerminalDepositReadWorkbench', + 'TerminalRepositoryContextPanel', + 'TerminalReadScenarioPanel', + 'buildReadRouteSession', + 'ReadNeedComprehensionSynthesis', + 'ReadFitsFindingSynthesis', + ], + }, + { + rowId: 'navigation-read-route', + owner: SOURCE_ROOTS.nav, + contract: + 'Public navigation and footer expose /read as the primary Reading path while retained Terminal remains available for debug-compatible detail.', + requiredFields: ['/read', 'Read', 'BITCODE_PUBLIC_EXPLAINERS.read'], + }, +]); + +function buildPredicateResults(repoRoot) { + const sources = Object.fromEntries( + Object.entries(SOURCE_ROOTS).map(([key, sourcePath]) => [key, readSource(repoRoot, sourcePath)]), + ); + + return [ + predicateResult('active-canon-pointer-remains-v42', SOURCE_ROOTS.activePointer, sources.activePointer.trim() === 'V42'), + predicateResult('spec-defines-gate4', SOURCE_ROOTS.spec, sources.spec.includes('V43 Gate 4 Read Route Extraction And Five-Step UX')), + predicateResult('spec-names-read-route-objects', SOURCE_ROOTS.spec, sources.spec.includes('ReadRouteSession') && sources.spec.includes('Finding Fits request')), + predicateResult('delta-records-gate4', SOURCE_ROOTS.delta, sources.delta.includes('Gate 4') && sources.delta.includes('v43-read-route-five-step-ux')), + predicateResult('notes-records-gate4', SOURCE_ROOTS.notes, sources.notes.includes('Gate 4') && sources.notes.includes('/read')), + predicateResult('parity-records-gate4', SOURCE_ROOTS.parity, sources.parity.includes('Gate 4') && sources.parity.includes('ReadRouteSession')), + predicateResult('roadmap-records-gate4', SOURCE_ROOTS.roadmap, sources.roadmap.includes('Current working gate: V43 Gate 4')), + predicateResult('readme-records-gate4', SOURCE_ROOTS.readme, sources.readme.includes('V43 Gate 4')), + predicateResult('protocol-readme-records-gate4', SOURCE_ROOTS.protocolReadme, sources.protocolReadme.includes('V43 Gate 4')), + predicateResult('route-model-defines-read-route-session', SOURCE_ROOTS.routeModel, sources.routeModel.includes("schema: 'bitcode.read.route-session'") && sources.routeModel.includes('assertReadRouteSessionSourceSafe')), + predicateResult('route-model-defines-pipeline-ownership', SOURCE_ROOTS.routeModel, sources.routeModel.includes('ReadNeedComprehensionSynthesis') && sources.routeModel.includes('ReadFitsFindingSynthesis')), + predicateResult('route-model-forbids-source-leakage', SOURCE_ROOTS.routeModel, sources.routeModel.includes('protectedSourceVisible: false') && sources.routeModel.includes('unpaidAssetPackSourceVisible: false')), + predicateResult('read-page-canonical-route', SOURCE_ROOTS.page, sources.page.includes("canonical: '/read'") && sources.page.includes('ReadPageClient')), + predicateResult('read-client-renders-five-step-route', SOURCE_ROOTS.client, sources.client.includes('data-testid={`read-route-step-${step.id}`}') && sources.client.includes('Reading steps')), + predicateResult('read-client-reuses-live-workbench', SOURCE_ROOTS.client, sources.client.includes('TerminalDepositReadWorkbench') && sources.client.includes('showDemonstrationWorkbench={false}')), + predicateResult('read-client-renders-source-safe-session', SOURCE_ROOTS.client, sources.client.includes('Source-safe read state') && sources.client.includes('Disclosure boundary')), + predicateResult('read-client-records-activity', SOURCE_ROOTS.client, sources.client.includes('/api/executions/history') && sources.client.includes('buildTerminalExecutionHistoryRequest')), + predicateResult('terminal-routes-define-read', SOURCE_ROOTS.terminalRoutes, sources.terminalRoutes.includes("READ_ROUTE = '/read'") && sources.terminalRoutes.includes('buildReadHref')), + predicateResult('workbench-preserves-execution-stream', SOURCE_ROOTS.terminalWorkbench, sources.terminalWorkbench.includes('BitcodeExecutionStreamPanel') && sources.terminalWorkbench.includes('Request Fit')), + predicateResult('reading-state-preserves-five-steps', SOURCE_ROOTS.terminalReadingState, V43_READ_ROUTE_STEP_IDS.every((id) => sources.terminalReadingState.includes(`'${id}'`))), + predicateResult('nav-links-to-read', SOURCE_ROOTS.nav, sources.nav.includes("href === '/read'") && sources.nav.includes('BITCODE_PUBLIC_EXPLAINERS.read')), + predicateResult('public-shell-recognizes-read', SOURCE_ROOTS.workspaceSurface, sources.workspaceSurface.includes("pathname.startsWith('/read')")), + predicateResult('public-copy-uses-read-link', SOURCE_ROOTS.publicCopy, sources.publicCopy.includes("{ href: '/read', label: 'Read' }")), + predicateResult('public-explainers-define-read', SOURCE_ROOTS.publicExplainers, sources.publicExplainers.includes("title: 'Read'") && sources.publicExplainers.includes('Finding Fits')), + predicateResult('footer-links-to-read', SOURCE_ROOTS.footer, sources.footer.includes("const READ_URL = '/read'") && sources.footer.includes('BITCODE_PUBLIC_EXPLAINERS.read')), + predicateResult('uapi-route-model-test-covers-read', SOURCE_ROOTS.routeModelTest, sources.routeModelTest.includes('assertReadRouteSessionSourceSafe')), + predicateResult('uapi-page-test-covers-read', SOURCE_ROOTS.pageTest, sources.pageTest.includes('ReadPageClient') && sources.pageTest.includes('read-route-step-request-read')), + predicateResult('protocol-test-covers-artifact', SOURCE_ROOTS.packageTest, sources.packageTest.includes('buildV43ReadRouteFiveStepUx')), + predicateResult('package-exports-gate4', SOURCE_ROOTS.packageIndex, sources.packageIndex.includes('buildV43ReadRouteFiveStepUx')), + predicateResult('package-types-export-gate4', SOURCE_ROOTS.packageTypes, sources.packageTypes.includes('buildV43ReadRouteFiveStepUx')), + predicateResult('package-json-exposes-gate4', SOURCE_ROOTS.packageJson, sources.packageJson.includes('"generate:v43-read-route-five-step-ux"') && sources.packageJson.includes('"check:v43-gate4"')), + predicateResult('gate-workflow-runs-gate4', SOURCE_ROOTS.gateWorkflow, sources.gateWorkflow.includes('check-v43-gate4-read-route-five-step-ux.mjs')), + predicateResult('canon-workflow-runs-gate4', SOURCE_ROOTS.canonWorkflow, sources.canonWorkflow.includes('check-v43-gate4-read-route-five-step-ux.mjs')), + predicateResult('generator-exists', SOURCE_ROOTS.generator, sources.generator.includes('buildV43ReadRouteFiveStepUx')), + predicateResult('checker-exists', SOURCE_ROOTS.checker, sources.checker.includes('V43 Gate 4 read route five-step UX check')), + ]; +} + +export function buildV43ReadRouteFiveStepUx({ repoRoot = DEFAULT_REPO_ROOT } = {}) { + const predicateResults = buildPredicateResults(repoRoot); + const failedPredicateIds = predicateResults + .filter((predicate) => !predicate.passed) + .map((predicate) => predicate.id); + const sourceRoots = Object.fromEntries( + Object.entries(SOURCE_ROOTS).map(([key, sourcePath]) => [key, `${sourcePath}:${digest(readSource(repoRoot, sourcePath))}`]), + ); + const artifactRoot = `v43-read-route-five-step-ux:${digest(JSON.stringify({ + stepIds: V43_READ_ROUTE_STEP_IDS, + objectIds: V43_READ_ROUTE_OBJECT_IDS, + pipelineIds: V43_READ_ROUTE_PIPELINE_IDS, + sourceSafeFieldIds: V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS, + forbiddenPayloadIds: V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS, + contractRows: V43_READ_ROUTE_CONTRACT_ROWS.map((row) => row.rowId), + sourceRoots, + failedPredicateIds, + }))}`; + + return { + artifactId: 'v43-read-route-five-step-ux', + schemaId: V43_READ_ROUTE_FIVE_STEP_UX_SCHEMA_ID, + version: V43_READ_ROUTE_FIVE_STEP_UX_VERSION, + currentTarget: V43_READ_ROUTE_FIVE_STEP_UX_CURRENT_TARGET, + sourceSafetyVerdict: V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_SAFETY_VERDICT, + generatedAt: 'deterministic', + artifactRoot, + passed: failedPredicateIds.length === 0, + stepIds: [...V43_READ_ROUTE_STEP_IDS], + objectIds: [...V43_READ_ROUTE_OBJECT_IDS], + pipelineIds: [...V43_READ_ROUTE_PIPELINE_IDS], + sourceSafeFieldIds: [...V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS], + forbiddenPayloadIds: [...V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS], + contractRows: V43_READ_ROUTE_CONTRACT_ROWS.map((row) => ({ + ...row, + rowRoot: `v43-read-route-contract:${digest(row.rowId)}`, + sourceSafeMetadataOnly: true, + protectedSourceVisible: false, + unpaidAssetPackSourceVisible: false, + })), + sourceRoots, + predicateResults, + coverage: { + readRouteImplemented: true, + fiveStepUxImplemented: true, + readNeedReviewImplemented: true, + findingFitsRequestImplemented: true, + assetPackPreviewImplemented: true, + settlementDeliveryBoundaryImplemented: true, + executionStreamRetained: true, + terminalDebugCompatibilityRetained: true, + acceptedNeedRequiredBeforeFindingFits: true, + sourceSafePreviewBeforeSettlement: true, + deliveryRequiresPaidReadRights: true, + sourceSafeMetadataOnly: true, + protectedSourceVisible: false, + rawSourceTextVisible: false, + sourceSnippetVisible: false, + rawPromptVisible: false, + interpolatedPromptVisible: false, + rawProviderResponseVisible: false, + unpaidAssetPackSourceVisible: false, + credentialsSerialized: false, + walletPrivateMaterialVisible: false, + settlementPrivatePayloadVisible: false, + requiredPredicateCount: predicateResults.length, + passedPredicateCount: predicateResults.length - failedPredicateIds.length, + failedPredicateIds, + }, + }; +} + +export const V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_ROOTS = Object.freeze( + Object.fromEntries(Object.entries(SOURCE_ROOTS).map(([key, sourcePath]) => [key, sourceRoot(sourcePath)])), +); diff --git a/packages/protocol/src/index.d.ts b/packages/protocol/src/index.d.ts index 415085b2a..0e9b96bf8 100644 --- a/packages/protocol/src/index.d.ts +++ b/packages/protocol/src/index.d.ts @@ -604,6 +604,19 @@ export const V43_PACK_ACTIVITY_FILTER_IDS: readonly string[]; export const V43_PACK_ACTIVITY_DETAIL_SECTION_IDS: readonly string[]; export const V43_PACK_ACTIVITY_CONTRACT_ROWS: readonly Record[]; export function buildV43PacksActivityMasterDetail(input?: Record): BitcodeProtocolReport; +export const V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH: string; +export const V43_READ_ROUTE_FIVE_STEP_UX_CURRENT_TARGET: string; +export const V43_READ_ROUTE_FIVE_STEP_UX_SCHEMA_ID: string; +export const V43_READ_ROUTE_FIVE_STEP_UX_VERSION: string; +export const V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_SAFETY_VERDICT: string; +export const V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_ROOTS: Readonly>; +export const V43_READ_ROUTE_STEP_IDS: readonly string[]; +export const V43_READ_ROUTE_OBJECT_IDS: readonly string[]; +export const V43_READ_ROUTE_PIPELINE_IDS: readonly string[]; +export const V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS: readonly string[]; +export const V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS: readonly string[]; +export const V43_READ_ROUTE_CONTRACT_ROWS: readonly Record[]; +export function buildV43ReadRouteFiveStepUx(input?: Record): BitcodeProtocolReport; export const EXCHANGE_INTENT_ORDER_CONTRACTS_ARTIFACT_PATH: string; export const EXCHANGE_INTENT_ORDER_CONTRACTS_CURRENT_TARGET: string; export const EXCHANGE_INTENT_ORDER_CONTRACTS_SCHEMA_ID: string; diff --git a/packages/protocol/src/index.js b/packages/protocol/src/index.js index d7db33e14..adbb82b2a 100644 --- a/packages/protocol/src/index.js +++ b/packages/protocol/src/index.js @@ -682,6 +682,21 @@ export { V43_PACK_ACTIVITY_TYPE_IDS, buildV43PacksActivityMasterDetail } from './canonical/v43-packs-activity-master-detail.js'; +export { + V43_READ_ROUTE_CONTRACT_ROWS, + V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH, + V43_READ_ROUTE_FIVE_STEP_UX_CURRENT_TARGET, + V43_READ_ROUTE_FIVE_STEP_UX_SCHEMA_ID, + V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_ROOTS, + V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_SAFETY_VERDICT, + V43_READ_ROUTE_FIVE_STEP_UX_VERSION, + V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS, + V43_READ_ROUTE_OBJECT_IDS, + V43_READ_ROUTE_PIPELINE_IDS, + V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS, + V43_READ_ROUTE_STEP_IDS, + buildV43ReadRouteFiveStepUx +} from './canonical/v43-read-route-five-step-ux.js'; export { EXCHANGE_INTENT_ACTION_KINDS, EXCHANGE_INTENT_ORDER_CONTRACTS_ARTIFACT_PATH, diff --git a/packages/protocol/test/v43-read-route-five-step-ux.test.js b/packages/protocol/test/v43-read-route-five-step-ux.test.js new file mode 100644 index 000000000..fb82e6b74 --- /dev/null +++ b/packages/protocol/test/v43-read-route-five-step-ux.test.js @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { + V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH, + V43_READ_ROUTE_FIVE_STEP_UX_SCHEMA_ID, + V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_SAFETY_VERDICT, + V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS, + V43_READ_ROUTE_OBJECT_IDS, + V43_READ_ROUTE_PIPELINE_IDS, + V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS, + V43_READ_ROUTE_STEP_IDS, + buildV43ReadRouteFiveStepUx, +} from '../src/canonical/v43-read-route-five-step-ux.js'; + +test('V43 read route five-step UX artifact binds source-safe Reading route contracts', () => { + const report = buildV43ReadRouteFiveStepUx(); + + assert.equal(V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH, '.bitcode/v43-read-route-five-step-ux.json'); + assert.equal(report.artifactId, 'v43-read-route-five-step-ux'); + assert.equal(report.schemaId, V43_READ_ROUTE_FIVE_STEP_UX_SCHEMA_ID); + assert.equal(report.version, 'V43'); + assert.equal(report.currentTarget, 'V42'); + assert.equal(report.sourceSafetyVerdict, V43_READ_ROUTE_FIVE_STEP_UX_SOURCE_SAFETY_VERDICT); + assert.ok(report.artifactRoot.startsWith('v43-read-route-five-step-ux:')); + assert.deepEqual(report.stepIds, [...V43_READ_ROUTE_STEP_IDS]); + assert.deepEqual(report.objectIds, [...V43_READ_ROUTE_OBJECT_IDS]); + assert.deepEqual(report.pipelineIds, [...V43_READ_ROUTE_PIPELINE_IDS]); + assert.deepEqual(report.sourceSafeFieldIds, [...V43_READ_ROUTE_SOURCE_SAFE_FIELD_IDS]); + assert.deepEqual(report.forbiddenPayloadIds, [...V43_READ_ROUTE_FORBIDDEN_PAYLOAD_IDS]); + assert.equal(report.contractRows.length, 4); + assert.equal(report.coverage.readRouteImplemented, true); + assert.equal(report.coverage.fiveStepUxImplemented, true); + assert.equal(report.coverage.acceptedNeedRequiredBeforeFindingFits, true); + assert.equal(report.coverage.sourceSafePreviewBeforeSettlement, true); + assert.equal(report.coverage.deliveryRequiresPaidReadRights, true); + assert.equal(report.coverage.executionStreamRetained, true); + assert.equal(report.coverage.sourceSafeMetadataOnly, true); + assert.equal(report.coverage.protectedSourceVisible, false); + assert.equal(report.coverage.unpaidAssetPackSourceVisible, false); + assert.equal(report.coverage.rawPromptVisible, false); + assert.equal(report.coverage.interpolatedPromptVisible, false); + assert.equal(report.coverage.rawProviderResponseVisible, false); +}); diff --git a/scripts/check-v43-gate4-read-route-five-step-ux.mjs b/scripts/check-v43-gate4-read-route-five-step-ux.mjs new file mode 100644 index 000000000..65b243688 --- /dev/null +++ b/scripts/check-v43-gate4-read-route-five-step-ux.mjs @@ -0,0 +1,171 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH, + buildV43ReadRouteFiveStepUx, +} from '../packages/protocol/src/canonical/v43-read-route-five-step-ux.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const defaultRepoRoot = path.resolve(__dirname, '..'); + +function read(root, relativePath) { + return readFileSync(path.join(root, relativePath), 'utf8'); +} + +function exists(root, relativePath) { + return existsSync(path.join(root, relativePath)); +} + +function git(root, args) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim(); +} + +function assertCheck(failures, condition, message) { + if (!condition) failures.push(message); +} + +function parseArgs(argv) { + const args = { repoRoot: defaultRepoRoot, skipBranchCheck: false, skipUapiTests: false }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--repo-root') args.repoRoot = path.resolve(argv[++index]); + else if (arg === '--skip-branch-check') args.skipBranchCheck = true; + else if (arg === '--skip-uapi-tests') args.skipUapiTests = true; + else if (arg === '--help' || arg === '-h') args.help = true; + else throw new Error(`Unknown argument ${arg}`); + } + return args; +} + +function printHelp() { + process.stdout.write( + [ + 'Usage: node scripts/check-v43-gate4-read-route-five-step-ux.mjs [--skip-branch-check] [--skip-uapi-tests] [--repo-root ]', + '', + 'Checks V43 Gate 4 /read route extraction, five-step Reading UX, source-safe preview boundaries, generated artifact, docs, workflows, and tests.', + ].join('\n'), + ); + process.stdout.write('\n'); +} + +function run(root, command, args) { + execFileSync(command, args, { cwd: root, stdio: 'pipe', encoding: 'utf8' }); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + printHelp(); + return; + } + + const root = args.repoRoot; + const failures = []; + const pointer = read(root, 'BITCODE_SPEC.txt').trim(); + + assertCheck(failures, pointer === 'V42', `BITCODE_SPEC.txt must remain V42 during V43 gate work. Observed ${pointer || 'empty'}.`); + + if (!args.skipBranchCheck) { + const branch = git(root, ['branch', '--show-current']); + assertCheck( + failures, + branch === 'version/v43' || /^v43\/gate-\d+-[a-z0-9][a-z0-9-]*$/u.test(branch), + `V43 work must occur on version/v43 or v43/gate-N-* branches. Observed ${branch || 'detached HEAD'}.`, + ); + } + + for (const relativePath of [ + V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH, + 'uapi/app/read/read-route-model.ts', + 'uapi/app/read/page.tsx', + 'uapi/app/read/ReadPageClient.tsx', + 'uapi/app/terminal/terminal-routes.ts', + 'uapi/app/terminal/TerminalDepositReadWorkbench.tsx', + 'uapi/app/terminal/terminal-enterprise-reading-ux-state.ts', + 'uapi/components/base/bitcode/layout/nav.tsx', + 'uapi/components/base/bitcode/layout/workspace-surface.ts', + 'uapi/components/base/bitcode/layout/bitcode-public-copy.ts', + 'uapi/components/base/bitcode/layout/bitcode-public-explainers.ts', + 'uapi/components/base/bitcode/layout/footer.tsx', + 'uapi/tests/readRouteModel.test.ts', + 'uapi/tests/readPageClient.test.tsx', + 'packages/protocol/src/canonical/v43-read-route-five-step-ux.js', + 'packages/protocol/test/v43-read-route-five-step-ux.test.js', + 'scripts/generate-v43-read-route-five-step-ux.mjs', + 'scripts/check-v43-gate4-read-route-five-step-ux.mjs', + 'BITCODE_SPEC_V43.md', + 'BITCODE_SPEC_V43_DELTA.md', + 'BITCODE_SPEC_V43_NOTES.md', + 'BITCODE_SPEC_V43_PARITY_MATRIX.md', + 'SPECIFICATIONS_ROADMAP.md', + 'README.md', + 'packages/protocol/README.md', + '.github/workflows/bitcode-gate-quality.yml', + '.github/workflows/bitcode-canon-quality.yml', + 'package.json', + ]) { + assertCheck(failures, exists(root, relativePath), `Missing required V43 Gate 4 file: ${relativePath}`); + } + + const artifact = buildV43ReadRouteFiveStepUx({ repoRoot: root }); + assertCheck(failures, artifact.passed, `V43 read route predicates failed: ${artifact.coverage.failedPredicateIds.join(', ')}`); + assertCheck(failures, artifact.coverage.readRouteImplemented === true, 'Read route must be implemented.'); + assertCheck(failures, artifact.coverage.fiveStepUxImplemented === true, 'Five-step Reading UX must be implemented.'); + assertCheck(failures, artifact.coverage.acceptedNeedRequiredBeforeFindingFits === true, 'Accepted Need must gate Finding Fits.'); + assertCheck(failures, artifact.coverage.sourceSafePreviewBeforeSettlement === true, 'AssetPack preview must be source-safe before settlement.'); + assertCheck(failures, artifact.coverage.deliveryRequiresPaidReadRights === true, 'Delivery must require paid read rights.'); + assertCheck(failures, artifact.coverage.executionStreamRetained === true, 'Execution stream must be retained.'); + assertCheck(failures, artifact.coverage.sourceSafeMetadataOnly === true, 'Artifact must be source-safe metadata only.'); + assertCheck(failures, artifact.coverage.protectedSourceVisible === false, 'Artifact must not expose protected source.'); + assertCheck(failures, artifact.coverage.unpaidAssetPackSourceVisible === false, 'Artifact must not expose unpaid AssetPack source.'); + assertCheck(failures, artifact.coverage.rawPromptVisible === false, 'Artifact must not expose raw prompts.'); + assertCheck(failures, artifact.coverage.interpolatedPromptVisible === false, 'Artifact must not expose interpolated prompts.'); + assertCheck(failures, artifact.coverage.rawProviderResponseVisible === false, 'Artifact must not expose raw provider responses.'); + + const serialized = `${JSON.stringify(artifact, null, 2)}\n`; + assertCheck( + failures, + exists(root, V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH) && + read(root, V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH) === serialized, + `${V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH} must be generated and current.`, + ); + + const packageJson = read(root, 'package.json'); + const gateWorkflow = read(root, '.github/workflows/bitcode-gate-quality.yml'); + const canonWorkflow = read(root, '.github/workflows/bitcode-canon-quality.yml'); + assertCheck(failures, packageJson.includes('"generate:v43-read-route-five-step-ux"'), 'package.json must expose generate:v43-read-route-five-step-ux.'); + assertCheck(failures, packageJson.includes('"check:v43-read-route-five-step-ux"'), 'package.json must expose check:v43-read-route-five-step-ux.'); + assertCheck(failures, packageJson.includes('"check:v43-gate4"'), 'package.json must expose check:v43-gate4.'); + assertCheck(failures, gateWorkflow.includes('check-v43-gate4-read-route-five-step-ux.mjs'), 'Gate workflow must run V43 Gate 4 checker.'); + assertCheck(failures, canonWorkflow.includes('check-v43-gate4-read-route-five-step-ux.mjs'), 'Canon workflow must run V43 Gate 4 checker.'); + + if (!args.skipUapiTests) { + try { + run(root, 'pnpm', ['--dir', 'uapi', 'exec', 'jest', 'readRouteModel.test.ts', 'readPageClient.test.tsx', '--runInBand']); + } catch { + failures.push('uapi readRouteModel.test.ts and readPageClient.test.tsx must pass.'); + } + } + + if (failures.length > 0) { + process.stderr.write('V43 Gate 4 read route five-step UX check failed:\n'); + for (const failure of failures.filter(Boolean)) process.stderr.write(`- ${failure}\n`); + process.exitCode = 1; + return; + } + + process.stdout.write('V43 Gate 4 read route five-step UX check passed.\n'); +} + +try { + main(); +} catch (error) { + const detail = error instanceof Error ? error.message : String(error); + process.stderr.write(`${detail}\n`); + process.exitCode = 1; +} diff --git a/scripts/generate-v43-read-route-five-step-ux.mjs b/scripts/generate-v43-read-route-five-step-ux.mjs new file mode 100644 index 000000000..b119d4f91 --- /dev/null +++ b/scripts/generate-v43-read-route-five-step-ux.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH, + buildV43ReadRouteFiveStepUx, +} from '../packages/protocol/src/canonical/v43-read-route-five-step-ux.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const checkOnly = process.argv.includes('--check'); +const artifact = buildV43ReadRouteFiveStepUx({ repoRoot }); +const serialized = `${JSON.stringify(artifact, null, 2)}\n`; +const artifactPath = path.join(repoRoot, V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH); + +if (checkOnly) { + if (!existsSync(artifactPath) || readFileSync(artifactPath, 'utf8') !== serialized) { + process.stderr.write( + `${V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH} is stale. Run pnpm run generate:v43-read-route-five-step-ux.\n`, + ); + process.exitCode = 1; + } +} else { + mkdirSync(path.dirname(artifactPath), { recursive: true }); + writeFileSync(artifactPath, serialized); + process.stdout.write(`wrote ${V43_READ_ROUTE_FIVE_STEP_UX_ARTIFACT_PATH}\n`); +} diff --git a/uapi/app/read/ReadPageClient.tsx b/uapi/app/read/ReadPageClient.tsx new file mode 100644 index 000000000..e224d09d7 --- /dev/null +++ b/uapi/app/read/ReadPageClient.tsx @@ -0,0 +1,421 @@ +'use client'; + +import Link from 'next/link'; +import React from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { GitBranch, GitCommitHorizontal, RefreshCw, ShieldCheck, Workflow } from 'lucide-react'; +import { useRouter, useSearchParams } from 'next/navigation'; + +import { fetchPipelineExecutionHistory } from '@/networking/api-client'; +import type { PipelineExecution } from '@/types/api'; + +import TerminalDepositReadWorkbench from '@/app/terminal/TerminalDepositReadWorkbench'; +import TerminalRepositoryContextPanel from '@/app/terminal/TerminalRepositoryContextPanel'; +import TerminalReadScenarioPanel from '@/app/terminal/TerminalReadScenarioPanel'; +import { TerminalShellBridgeProvider } from '@/app/terminal/terminal-shell-bridge'; +import type { TerminalDepositedSourceRevision } from '@/app/terminal/terminal-deposit-read-workbench'; +import { + buildTerminalExecutionHistoryRequest, + mapExecutionHistoryRunToWorkspaceRun, + readTerminalRouteError, + type TerminalActivityRecordDraft, + upsertWorkspaceRun, +} from '@/app/terminal/terminal-activity-history'; +import type { TerminalRepositoryContextState } from '@/app/terminal/terminal-repository-context'; +import { readTerminalTransactionId, writeTerminalTransactionId } from '@/app/terminal/terminal-transaction-query'; +import { shouldRecoverTerminalTransactionRoute } from '@/app/terminal/terminal-transaction-query'; +import type { WorkspaceRun } from '@/app/terminal/terminal-run-data'; +import { buildReadHref } from '@/app/terminal/terminal-routes'; + +import { buildReadRouteSession, readReadRouteStage, writeReadRouteStage } from './read-route-model'; + +function shortIdentifier(value: string | null | undefined) { + if (!value) return 'pending'; + return value.length > 18 ? `${value.slice(0, 12)}...` : value; +} + +function formatDate(value: string | null | undefined) { + if (!value) return 'pending'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +export default function ReadPageClient() { + const router = useRouter(); + const searchParams = useSearchParams(); + const routeSearchParams = useMemo(() => new URLSearchParams(searchParams.toString()), [searchParams]); + const selectedTransactionId = useMemo(() => readTerminalTransactionId(routeSearchParams), [routeSearchParams]); + const routeReadingStage = useMemo(() => readReadRouteStage(routeSearchParams), [routeSearchParams]); + const [liveRuns, setLiveRuns] = useState([]); + const [isLoadingRuns, setIsLoadingRuns] = useState(true); + const [runsLoadError, setRunsLoadError] = useState(null); + const [repositoryContext, setRepositoryContext] = useState(null); + + const readCurrentSearchParams = useCallback( + () => + typeof window !== 'undefined' && window.location.pathname === '/read' + ? new URLSearchParams(window.location.search) + : new URLSearchParams(searchParams.toString()), + [searchParams], + ); + + const replaceReadSearchParams = useCallback( + (nextParams: URLSearchParams) => { + const query = nextParams.toString(); + router.replace(buildReadHref(query), { scroll: false }); + }, + [router], + ); + + const replaceReadRouteTransaction = useCallback( + (transactionId: string) => { + replaceReadSearchParams(writeTerminalTransactionId(readCurrentSearchParams(), transactionId)); + }, + [readCurrentSearchParams, replaceReadSearchParams], + ); + + const refreshLiveRuns = useCallback(async () => { + setIsLoadingRuns(true); + setRunsLoadError(null); + + try { + const history = await fetchPipelineExecutionHistory(); + const nextRuns = history.map(mapExecutionHistoryRunToWorkspaceRun); + setLiveRuns(nextRuns); + return nextRuns; + } catch (error) { + setLiveRuns([]); + setRunsLoadError(error instanceof Error ? error.message : 'Unable to load recent Reading activity.'); + return []; + } finally { + setIsLoadingRuns(false); + } + }, []); + + useEffect(() => { + void refreshLiveRuns(); + }, [refreshLiveRuns]); + + useEffect(() => { + if ( + !shouldRecoverTerminalTransactionRoute({ + transactionIds: liveRuns.map((run) => run.id), + selectedTransactionId, + }) + ) { + return; + } + replaceReadRouteTransaction(liveRuns[0].id); + }, [liveRuns, replaceReadRouteTransaction, selectedTransactionId]); + + const selectedRun = useMemo( + () => liveRuns.find((run) => run.id === selectedTransactionId) || liveRuns[0] || null, + [liveRuns, selectedTransactionId], + ); + + const depositedSourceRevision = useMemo(() => { + const selectedRepository = repositoryContext?.selectedRepository || null; + if (!selectedRepository) return null; + const selectedBranch = repositoryContext?.selectedBranch || selectedRepository.defaultBranch || 'main'; + const matchingSubmission = liveRuns.find( + (run) => + run.contextSource === 'terminal-deposit-composer' && + run.repository === selectedRepository.fullName && + run.branch === selectedBranch && + Boolean(run.sourceCommit) && + Boolean(run.candidateAssetId), + ); + if (!matchingSubmission?.sourceCommit) return null; + + return { + repositoryFullName: selectedRepository.fullName, + branch: selectedBranch, + commit: matchingSubmission.sourceCommit, + activityId: matchingSubmission.id, + createdAt: matchingSubmission.created_at, + depositAssetId: matchingSubmission.candidateAssetId || null, + hasWalletOrAttestationProof: Boolean(matchingSubmission.candidateAssetId), + hasAssetMeasurementEvidence: Boolean(matchingSubmission.candidateAssetId), + proofRoot: matchingSubmission.depositProofRoot || null, + measurementRoot: matchingSubmission.depositMeasurementRoot || null, + reconciliationReadbackRoot: matchingSubmission.depositReconciliationReadbackRoot || null, + depositorySearchDocumentRoot: matchingSubmission.depositorySearchDocumentRoot || null, + lexicalDocumentRoot: matchingSubmission.lexicalDocumentRoot || null, + vectorDocumentRoot: matchingSubmission.vectorDocumentRoot || null, + compensationPreviewRoot: matchingSubmission.compensationPreviewRoot || null, + sourceToSharesPreviewRoot: matchingSubmission.sourceToSharesPreviewRoot || null, + compensationState: matchingSubmission.compensationState || null, + compensationAllocationMethod: matchingSubmission.compensationAllocationMethod || null, + compensationPriceAsset: matchingSubmission.compensationPriceAsset || null, + depositorWalletId: matchingSubmission.depositorWalletId || null, + depositoryIndexState: matchingSubmission.depositoryIndexState || null, + }; + }, [liveRuns, repositoryContext]); + + const admittedReadActivityId = useMemo(() => { + const selectedRepository = repositoryContext?.selectedRepository || null; + if (!selectedRepository) return null; + const sourceBranch = depositedSourceRevision?.branch || repositoryContext?.selectedBranch || selectedRepository.defaultBranch || 'main'; + const sourceCommit = depositedSourceRevision?.commit || repositoryContext?.selectedCommit || null; + const matchingRead = liveRuns.find( + (run) => + run.contextSource === 'terminal-deposit-read-workbench' && + run.contextWorkbench === 'read-admission' && + run.repository === selectedRepository.fullName && + run.branch === sourceBranch && + (!sourceCommit || run.sourceCommit === sourceCommit), + ); + return matchingRead?.id || null; + }, [depositedSourceRevision, liveRuns, repositoryContext]); + + const readRouteSession = useMemo( + () => + buildReadRouteSession({ + transactionId: selectedTransactionId || admittedReadActivityId || null, + routeReadingStage, + repositoryFullName: repositoryContext?.selectedRepository?.fullName || null, + sourceBranch: depositedSourceRevision?.branch || repositoryContext?.selectedBranch || null, + sourceCommit: depositedSourceRevision?.commit || repositoryContext?.selectedCommit || null, + hasRepositorySource: Boolean(repositoryContext?.selectedRepository), + hasReadMeasurement: Boolean(admittedReadActivityId || selectedRun?.contextWorkbench === 'read-admission' || selectedRun?.transactionLens === 'read'), + hasSynthesizedNeed: Boolean(admittedReadActivityId || selectedRun?.contextSource === 'terminal-staged-reading'), + hasAcceptedNeed: Boolean(admittedReadActivityId), + findingFitsRunning: Boolean(selectedRun?.type?.includes('asset-pack') && selectedRun.status === 'running'), + hasSourceSafePreview: Boolean(selectedRun?.type?.includes('asset-pack') && selectedRun.status === 'completed'), + hasSettlementReadback: Boolean(selectedRun?.closureFocus?.toLowerCase().includes('settlement')), + hasDeliveryReadback: Boolean(selectedRun?.closureFocus?.toLowerCase().includes('delivery')), + }), + [admittedReadActivityId, depositedSourceRevision, repositoryContext, routeReadingStage, selectedRun, selectedTransactionId], + ); + + const handleRecordActivity = useCallback( + async (draft: TerminalActivityRecordDraft) => { + const response = await fetch('/api/executions/history', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify( + buildTerminalExecutionHistoryRequest(draft, { + repositoryContext, + fallbackRun: selectedRun, + }), + ), + }); + + if (!response.ok) { + throw new Error(await readTerminalRouteError(response, 'Unable to record Reading activity.')); + } + + const payload = (await response.json()) as { execution?: PipelineExecution }; + if (!payload.execution) throw new Error('Reading activity response did not include an execution row.'); + + const nextRun = mapExecutionHistoryRunToWorkspaceRun(payload.execution); + setLiveRuns((currentRuns) => upsertWorkspaceRun(currentRuns, nextRun)); + if (draft.selectAfterRecord !== false) replaceReadRouteTransaction(nextRun.id); + void refreshLiveRuns(); + return nextRun; + }, + [refreshLiveRuns, replaceReadRouteTransaction, repositoryContext, selectedRun], + ); + + const recentReadingRuns = useMemo( + () => + liveRuns + .filter((run) => + ['terminal-deposit-read-workbench', 'terminal-staged-reading', 'pipeline-harness'].includes( + run.contextSource || '', + ) || Boolean(run.type?.includes('pipeline')), + ) + .slice(0, 6), + [liveRuns], + ); + + const sessionRows = [ + { label: 'Repository', value: readRouteSession.routeState.repositoryFullName || 'select repository' }, + { label: 'Branch', value: readRouteSession.routeState.sourceBranch || 'pending' }, + { label: 'Commit', value: shortIdentifier(readRouteSession.routeState.sourceCommit) }, + { label: 'Transaction', value: shortIdentifier(readRouteSession.routeState.transactionId) }, + { label: 'Need pipeline', value: readRouteSession.pipelineOwnership.readNeedPipeline }, + { label: 'Fits pipeline', value: readRouteSession.pipelineOwnership.findingFitsPipeline }, + ]; + + return ( + +
+
+
+
+

+

+

+ Reading +

+

+ Request a repository read, review the synthesized Need, request Finding Fits, inspect a source-safe AssetPack preview, then settle before delivery. +

+
+
+
+
Stage
+
{readRouteSession.activeStepId.replace(/-/g, ' ')}
+
+
+
Rows
+
{isLoadingRuns ? 'reading' : String(liveRuns.length)}
+
+
+
Boundary
+
source-safe
+
+
+
+ +
+ {readRouteSession.steps.map((step) => { + const active = step.id === readRouteSession.activeStepId; + return ( + + ); + })} +
+ +
+
+
+ + +
+ +
+ + +
+
+
+
+ ); +} diff --git a/uapi/app/read/page.tsx b/uapi/app/read/page.tsx new file mode 100644 index 000000000..b9293203a --- /dev/null +++ b/uapi/app/read/page.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from 'next'; +import { Suspense } from 'react'; + +import PublicShellFrame from '@/app/(root)/components/PublicShellFrame'; + +import ReadPageClient from './ReadPageClient'; + +export const metadata: Metadata = { + title: 'Bitcode Read', + description: + 'Request Reading, review synthesized Needs, request Finding Fits, inspect source-safe AssetPack previews, and settle for delivery.', + alternates: { + canonical: '/read', + }, +}; + +export default function ReadPage() { + return ( + + +
+ Loading Reading... +
+ + } + > + +
+
+ ); +} diff --git a/uapi/app/read/read-route-model.ts b/uapi/app/read/read-route-model.ts new file mode 100644 index 000000000..62920a48c --- /dev/null +++ b/uapi/app/read/read-route-model.ts @@ -0,0 +1,201 @@ +import { + TERMINAL_ENTERPRISE_READING_FORBIDDEN_FIELDS, + TERMINAL_ENTERPRISE_READING_STEPS, + assertTerminalEnterpriseReadingUxStateSourceSafe, + buildTerminalEnterpriseReadingUxState, + type TerminalEnterpriseReadingStepId, + type TerminalEnterpriseReadingUxStateInput, +} from '@/app/terminal/terminal-enterprise-reading-ux-state'; + +export type ReadRouteStepId = TerminalEnterpriseReadingStepId; + +export type ReadRouteSessionInput = TerminalEnterpriseReadingUxStateInput & { + repositoryFullName?: string | null; + sourceBranch?: string | null; + sourceCommit?: string | null; + readNeedId?: string | null; + assetPackPreviewId?: string | null; + settlementQuoteId?: string | null; +}; + +export type ReadRouteSession = { + schema: 'bitcode.read.route-session'; + route: '/read'; + stageCount: 5; + activeStepId: ReadRouteStepId; + steps: ReturnType['steps']; + readObjects: { + readRequestRecorded: boolean; + synthesizedNeedReviewed: boolean; + acceptedNeedPresent: boolean; + findingFitsRequested: boolean; + sourceSafeAssetPackPreviewPresent: boolean; + settlementQuotePresent: boolean; + deliveryUnlocked: boolean; + }; + routeState: { + transactionId: string | null; + readingStage: ReadRouteStepId | null; + repositoryFullName: string | null; + sourceBranch: string | null; + sourceCommit: string | null; + readNeedId: string | null; + assetPackPreviewId: string | null; + settlementQuoteId: string | null; + }; + pipelineOwnership: { + readNeedPipeline: 'ReadNeedComprehensionSynthesis'; + findingFitsPipeline: 'ReadFitsFindingSynthesis'; + acceptedNeedRequiredBeforeFindingFits: true; + previewSourceSafeBeforeSettlement: true; + deliveryRequiresPaidReadRights: true; + retainedTerminalDebugCompatible: true; + }; + disclosure: { + sourceSafetyClass: 'source_safe_read_route_metadata'; + lowDetailDefault: true; + expandableSourceSafeDetail: true; + protectedSourceVisible: false; + unpaidAssetPackSourceVisible: false; + rawPromptVisible: false; + interpolatedPromptVisible: false; + rawProviderResponseVisible: false; + walletPrivateMaterialVisible: false; + settlementPrivatePayloadVisible: false; + hiddenBeforeSettlement: typeof TERMINAL_ENTERPRISE_READING_FORBIDDEN_FIELDS; + }; + proofRoot: string; +}; + +const READ_ROUTE_STAGE_IDS = TERMINAL_ENTERPRISE_READING_STEPS.map((step) => step.id); + +function stableHash(value: string) { + let hash = 2166136261; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + return (hash >>> 0).toString(16).padStart(8, '0'); +} + +function normalizedText(value: string | null | undefined) { + const normalized = value?.trim(); + return normalized ? normalized : null; +} + +export function readReadRouteStage(params: URLSearchParams): ReadRouteStepId | null { + const stage = params.get('readingStage')?.trim(); + return READ_ROUTE_STAGE_IDS.includes(stage as ReadRouteStepId) ? (stage as ReadRouteStepId) : null; +} + +export function writeReadRouteStage(params: URLSearchParams, stage: ReadRouteStepId | null) { + const next = new URLSearchParams(params.toString()); + if (stage) next.set('readingStage', stage); + else next.delete('readingStage'); + return next; +} + +export function buildReadRouteSession(input: ReadRouteSessionInput = {}): ReadRouteSession { + const enterpriseState = buildTerminalEnterpriseReadingUxState(input); + const seed = JSON.stringify({ + activeStepId: enterpriseState.activeStepId, + transactionId: enterpriseState.routeState.transactionId, + repositoryFullName: normalizedText(input.repositoryFullName), + sourceBranch: normalizedText(input.sourceBranch), + sourceCommit: normalizedText(input.sourceCommit), + readNeedId: normalizedText(input.readNeedId), + assetPackPreviewId: normalizedText(input.assetPackPreviewId), + settlementQuoteId: normalizedText(input.settlementQuoteId), + steps: enterpriseState.steps.map((step) => ({ id: step.id, state: step.state, blockers: step.blockers })), + }); + + return { + schema: 'bitcode.read.route-session', + route: '/read', + stageCount: 5, + activeStepId: enterpriseState.activeStepId, + steps: enterpriseState.steps, + readObjects: { + readRequestRecorded: Boolean(input.hasReadMeasurement), + synthesizedNeedReviewed: Boolean(input.hasSynthesizedNeed), + acceptedNeedPresent: Boolean(input.hasAcceptedNeed), + findingFitsRequested: Boolean(input.findingFitsRunning || input.hasSourceSafePreview), + sourceSafeAssetPackPreviewPresent: Boolean(input.hasSourceSafePreview), + settlementQuotePresent: Boolean(input.hasSourceSafePreview || input.hasSettlementReadback), + deliveryUnlocked: Boolean(input.hasDeliveryReadback), + }, + routeState: { + transactionId: enterpriseState.routeState.transactionId, + readingStage: enterpriseState.routeState.routeReadingStage, + repositoryFullName: normalizedText(input.repositoryFullName), + sourceBranch: normalizedText(input.sourceBranch), + sourceCommit: normalizedText(input.sourceCommit), + readNeedId: normalizedText(input.readNeedId), + assetPackPreviewId: normalizedText(input.assetPackPreviewId), + settlementQuoteId: normalizedText(input.settlementQuoteId), + }, + pipelineOwnership: { + readNeedPipeline: 'ReadNeedComprehensionSynthesis', + findingFitsPipeline: 'ReadFitsFindingSynthesis', + acceptedNeedRequiredBeforeFindingFits: true, + previewSourceSafeBeforeSettlement: true, + deliveryRequiresPaidReadRights: true, + retainedTerminalDebugCompatible: true, + }, + disclosure: { + sourceSafetyClass: 'source_safe_read_route_metadata', + lowDetailDefault: true, + expandableSourceSafeDetail: true, + protectedSourceVisible: false, + unpaidAssetPackSourceVisible: false, + rawPromptVisible: false, + interpolatedPromptVisible: false, + rawProviderResponseVisible: false, + walletPrivateMaterialVisible: false, + settlementPrivatePayloadVisible: false, + hiddenBeforeSettlement: TERMINAL_ENTERPRISE_READING_FORBIDDEN_FIELDS, + }, + proofRoot: `read-route-session:${stableHash(seed)}`, + }; +} + +export function assertReadRouteSessionSourceSafe(session: ReadRouteSession) { + const enterpriseSafety = assertTerminalEnterpriseReadingUxStateSourceSafe( + buildTerminalEnterpriseReadingUxState({ + transactionId: session.routeState.transactionId, + routeReadingStage: session.routeState.readingStage, + hasRepositorySource: Boolean(session.routeState.repositoryFullName), + hasReadMeasurement: session.readObjects.readRequestRecorded, + hasSynthesizedNeed: session.readObjects.synthesizedNeedReviewed, + hasAcceptedNeed: session.readObjects.acceptedNeedPresent, + hasSourceSafePreview: session.readObjects.sourceSafeAssetPackPreviewPresent, + hasSettlementReadback: session.readObjects.settlementQuotePresent, + hasDeliveryReadback: session.readObjects.deliveryUnlocked, + }), + ); + + const sourceSafe = + enterpriseSafety.admitted && + session.schema === 'bitcode.read.route-session' && + session.route === '/read' && + session.stageCount === 5 && + session.pipelineOwnership.acceptedNeedRequiredBeforeFindingFits === true && + session.pipelineOwnership.previewSourceSafeBeforeSettlement === true && + session.pipelineOwnership.deliveryRequiresPaidReadRights === true && + session.disclosure.sourceSafetyClass === 'source_safe_read_route_metadata' && + session.disclosure.protectedSourceVisible === false && + session.disclosure.unpaidAssetPackSourceVisible === false && + session.disclosure.rawPromptVisible === false && + session.disclosure.interpolatedPromptVisible === false && + session.disclosure.rawProviderResponseVisible === false && + session.disclosure.walletPrivateMaterialVisible === false && + session.disclosure.settlementPrivatePayloadVisible === false && + TERMINAL_ENTERPRISE_READING_FORBIDDEN_FIELDS.every((field) => + session.disclosure.hiddenBeforeSettlement.includes(field), + ); + + return { + admitted: sourceSafe, + reason: sourceSafe ? 'source_safe_read_route_metadata' : 'read_route_source_safety_boundary_violation', + }; +} diff --git a/uapi/app/terminal/terminal-enterprise-reading-ux-state.ts b/uapi/app/terminal/terminal-enterprise-reading-ux-state.ts index 305a345d2..bcf7c8694 100644 --- a/uapi/app/terminal/terminal-enterprise-reading-ux-state.ts +++ b/uapi/app/terminal/terminal-enterprise-reading-ux-state.ts @@ -139,7 +139,7 @@ export const TERMINAL_ENTERPRISE_READING_STEPS: TerminalEnterpriseReadingStepDef label: '1. Request Read', lowDetailGuidance: 'Frame repository, branch, commit, and the reader request.', expandableDetail: - 'Terminal captures source anchors, enterprise intent, constraints, disclosure posture, target artifact kinds, and the measured Read posture that can be reviewed before Need synthesis.', + 'Reading captures source anchors, enterprise intent, constraints, disclosure posture, target artifact kinds, and the measured Read posture that can be reviewed before Need synthesis.', primaryAction: 'Record read posture', sourceSafeVisibleFields: ['read_request_summary', 'proof_roots'], forbiddenFields: TERMINAL_ENTERPRISE_READING_FORBIDDEN_FIELDS, @@ -161,7 +161,7 @@ export const TERMINAL_ENTERPRISE_READING_STEPS: TerminalEnterpriseReadingStepDef label: '3. Request Finding Fits', lowDetailGuidance: 'Run Finding Fits only from an accepted Need.', expandableDetail: - 'Terminal hands the accepted Need, deposit/source anchors, proof roots, measurement roots, and source-safe search posture to ReadFitsFindingSynthesis without exposing protected deposit source.', + 'Reading hands the accepted Need, deposit/source anchors, proof roots, measurement roots, and source-safe search posture to ReadFitsFindingSynthesis without exposing protected deposit source.', primaryAction: 'Request Finding Fits', sourceSafeVisibleFields: ['read_need_measurements', 'depository_candidate_counts', 'proof_roots'], forbiddenFields: TERMINAL_ENTERPRISE_READING_FORBIDDEN_FIELDS, diff --git a/uapi/app/terminal/terminal-routes.ts b/uapi/app/terminal/terminal-routes.ts index 092b7e0c0..8895c8419 100644 --- a/uapi/app/terminal/terminal-routes.ts +++ b/uapi/app/terminal/terminal-routes.ts @@ -1,5 +1,6 @@ export const TERMINAL_ROUTE = '/terminal' as const; export const PACKS_ROUTE = '/packs' as const; +export const READ_ROUTE = '/read' as const; export const EXCHANGE_ROUTE = PACKS_ROUTE; export function buildTerminalHref(params?: URLSearchParams | string | null) { @@ -12,6 +13,11 @@ export function buildPacksHref(params?: URLSearchParams | string | null) { return query ? `${PACKS_ROUTE}?${query}` : PACKS_ROUTE; } +export function buildReadHref(params?: URLSearchParams | string | null) { + const query = typeof params === 'string' ? params : params?.toString(); + return query ? `${READ_ROUTE}?${query}` : READ_ROUTE; +} + export function buildExchangeHref(params?: URLSearchParams | string | null) { return buildPacksHref(params); } diff --git a/uapi/components/base/bitcode/layout/NavBrand.tsx b/uapi/components/base/bitcode/layout/NavBrand.tsx index fe7504bb4..33cafd299 100644 --- a/uapi/components/base/bitcode/layout/NavBrand.tsx +++ b/uapi/components/base/bitcode/layout/NavBrand.tsx @@ -5,7 +5,7 @@ import React from "react"; import Logo from "@/components/base/bitcode/branding/logo"; export type NavSurface = "terminal" | "auxillaries" | "conversations" | null; -export type NavBrandSurface = Exclude | 'home' | 'network' | 'docs' | null; +export type NavBrandSurface = Exclude | 'home' | 'network' | 'read' | 'docs' | null; interface NavBrandProps { animated?: boolean; @@ -27,6 +27,10 @@ const SURFACE_COPY: Record, { eyebrow: string; ti eyebrow: "Bitcode", title: "packs", }, + read: { + eyebrow: "Bitcode", + title: "read", + }, docs: { eyebrow: "Bitcode", title: "docs", diff --git a/uapi/components/base/bitcode/layout/bitcode-public-copy.ts b/uapi/components/base/bitcode/layout/bitcode-public-copy.ts index 5edc6f921..578ac7c18 100644 --- a/uapi/components/base/bitcode/layout/bitcode-public-copy.ts +++ b/uapi/components/base/bitcode/layout/bitcode-public-copy.ts @@ -10,8 +10,8 @@ export const BITCODE_PUBLIC_COPY = { 'MOCKED TERMINAL', ], primaryCta: { - href: '/terminal', - label: 'Open Terminal', + href: '/read', + label: 'Request Read', }, secondaryCta: { href: '/docs', @@ -72,6 +72,7 @@ export const BITCODE_PUBLIC_COPY = { userCta: 'Open Auxillaries', links: { network: 'Packs', + read: 'Read', transactions: 'Terminal', docs: 'Docs', github: 'Bitcode on GitHub', @@ -80,6 +81,7 @@ export const BITCODE_PUBLIC_COPY = { publicNav: { links: [ { href: '/packs', label: 'Packs' }, + { href: '/read', label: 'Read' }, { href: '/terminal', label: 'Terminal' }, { href: '/docs', label: 'Docs' }, ], diff --git a/uapi/components/base/bitcode/layout/bitcode-public-explainers.ts b/uapi/components/base/bitcode/layout/bitcode-public-explainers.ts index c099e7d33..1350dac41 100644 --- a/uapi/components/base/bitcode/layout/bitcode-public-explainers.ts +++ b/uapi/components/base/bitcode/layout/bitcode-public-explainers.ts @@ -16,6 +16,17 @@ export const BITCODE_PUBLIC_EXPLAINERS = { 'Keeps proof and settlement detail expandable without exposing unpaid source', ], }), + read: buildExplainer({ + kicker: 'Bitcode Read', + title: 'Read', + summary: 'Request Reading, review a synthesized Need, request Finding Fits, inspect source-safe AssetPack preview, and settle for delivery.', + detail: + 'Use this route for the enterprise Reading path. It keeps source-bearing AssetPack contents withheld before settlement while exposing measurements, proof roots, quote posture, and delivery state.', + points: [ + 'Separates Read-Need review from Finding Fits', + 'Keeps AssetPack preview source-safe until paid read rights unlock delivery', + ], + }), transactions: buildExplainer({ kicker: 'Live Terminal', title: 'Terminal', diff --git a/uapi/components/base/bitcode/layout/footer.tsx b/uapi/components/base/bitcode/layout/footer.tsx index 74490d4ea..c25047ce8 100644 --- a/uapi/components/base/bitcode/layout/footer.tsx +++ b/uapi/components/base/bitcode/layout/footer.tsx @@ -24,6 +24,7 @@ import { BITCODE_GITHUB_APP_PUBLIC_URL } from '@/lib/github-app-url'; const TERMINAL_URL = '/terminal'; const PACKS_URL = '/packs'; +const READ_URL = '/read'; const DEFAULT_OPERATOR_GUIDE_URL = process.env.NEXT_PUBLIC_BITCODE_OPERATOR_GUIDE_URL?.trim() || '/docs'; const CURRENT_PROTOCOL_SPEC_URL = 'https://github.com/engineeredsoftware/ENGI/blob/main/BITCODE_SPEC.txt'; @@ -151,6 +152,37 @@ export default function Footer({ showPrimaryContent = true, className = '' }: Fo ), }, + { + ariaLabel: BITCODE_PUBLIC_COPY.footer.links.read, + label: BITCODE_PUBLIC_COPY.footer.links.read, + meta: 'Reading flow', + href: READ_URL, + explainer: BITCODE_PUBLIC_EXPLAINERS.read, + icon: ( + + + + ), + }, { ariaLabel: BITCODE_PUBLIC_COPY.footer.links.transactions, label: BITCODE_PUBLIC_COPY.footer.links.transactions, diff --git a/uapi/components/base/bitcode/layout/nav.tsx b/uapi/components/base/bitcode/layout/nav.tsx index 37ed1d90c..085e1a43b 100644 --- a/uapi/components/base/bitcode/layout/nav.tsx +++ b/uapi/components/base/bitcode/layout/nav.tsx @@ -394,6 +394,7 @@ export default function Nav() {
    {BITCODE_PUBLIC_COPY.publicNav.links.map(({ href, label }, index) => { const isPacksRoute = href === '/packs'; + const isReadRoute = href === '/read'; const isDisabledRoute = (isPacksRoute && disableExchangeLink) || (href === '/terminal' && disableTerminalLink); @@ -449,6 +450,12 @@ export default function Nav() { side="bottom" triggerClassName="h-4.5 w-4.5 border-white/10 bg-white/[0.03] text-[0.58rem] text-neutral-300 hover:border-emerald-300/30 hover:bg-emerald-400/10 hover:text-emerald-100" /> + ) : isReadRoute ? ( + ) : href === '/terminal' ? ( /tests/workspaceSurface.test.ts', '/tests/packsPageClient.test.tsx', '/tests/packActivityModel.test.ts', + '/tests/readRouteModel.test.ts', + '/tests/readPageClient.test.tsx', '/tests/exchangeTerminalHandoff.test.ts', '/tests/terminalPreservedShellSurface.test.tsx', '/tests/terminalFloatingDebugWidget.test.tsx', diff --git a/uapi/tests/footerPublicShell.test.tsx b/uapi/tests/footerPublicShell.test.tsx index bea8608e4..8509df832 100644 --- a/uapi/tests/footerPublicShell.test.tsx +++ b/uapi/tests/footerPublicShell.test.tsx @@ -55,6 +55,10 @@ describe('Footer public shell', () => { 'href', '/packs', ); + expect(screen.getByRole('link', { name: 'Read' })).toHaveAttribute( + 'href', + '/read', + ); expect(screen.getByRole('link', { name: 'Terminal' })).toHaveAttribute( 'href', '/terminal', @@ -68,13 +72,15 @@ describe('Footer public shell', () => { 'https://github.com/engineeredsoftware/bitcode', ); expect(screen.getByRole('button', { name: 'Explain Packs' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Explain Read' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Explain Terminal' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Explain Docs' })).toBeInTheDocument(); expect(screen.getByText('Pack activity')).toBeInTheDocument(); + expect(screen.getByText('Reading flow')).toBeInTheDocument(); expect(screen.getAllByText('Terminal').length).toBeGreaterThan(0); expect(screen.getByText('Docs hub')).toBeInTheDocument(); expect(screen.getByText('Deposit')).toBeInTheDocument(); - expect(screen.getByText('Read')).toBeInTheDocument(); + expect(screen.getAllByText('Read').length).toBeGreaterThan(0); expect(screen.getAllByText('Settle').length).toBeGreaterThan(0); expect(screen.getAllByText('🧪').length).toBeGreaterThan(0); const protocolSpecLink = screen.getByRole('link', { name: 'Protocol spec' }); diff --git a/uapi/tests/marketingLandingPage.test.tsx b/uapi/tests/marketingLandingPage.test.tsx index 9c015c30a..a5d5895b3 100644 --- a/uapi/tests/marketingLandingPage.test.tsx +++ b/uapi/tests/marketingLandingPage.test.tsx @@ -83,8 +83,8 @@ describe('MarketingLandingPage', () => { screen.getByText('Bitcode is auditable market infrastructure for technical knowledge.'), ).toBeInTheDocument(); expect( - screen.getByRole('link', { name: 'Open Terminal' }), - ).toHaveAttribute('href', '/terminal'); + screen.getByRole('link', { name: 'Request Read' }), + ).toHaveAttribute('href', '/read'); expect(screen.getByRole('link', { name: 'Read docs' })).toHaveAttribute( 'href', '/docs', diff --git a/uapi/tests/navPublicShell.test.tsx b/uapi/tests/navPublicShell.test.tsx index 07e77ff59..91649db6c 100644 --- a/uapi/tests/navPublicShell.test.tsx +++ b/uapi/tests/navPublicShell.test.tsx @@ -112,9 +112,11 @@ describe('Nav public shell', () => { expect(screen.getByText('Brand home')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Packs' })).toHaveAttribute('href', '/packs'); expect(screen.getByRole('link', { name: 'Packs' })).not.toHaveAttribute('aria-current'); + expect(screen.getByRole('link', { name: 'Read' })).toHaveAttribute('href', '/read'); expect(screen.getByRole('link', { name: 'Terminal' })).toHaveAttribute('href', '/terminal'); expect(screen.getByRole('link', { name: 'Docs' })).toHaveAttribute('href', '/docs'); expect(screen.getByRole('button', { name: 'Explain Packs' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Explain Read' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Explain Terminal' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Explain Docs' })).toBeInTheDocument(); @@ -159,6 +161,7 @@ describe('Nav public shell', () => { expect(screen.getByText('Brand home')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Terminal' })).toHaveAttribute('href', '/terminal'); + expect(screen.getByRole('link', { name: 'Read' })).toHaveAttribute('href', '/read'); expect(screen.getByRole('button', { name: 'Explain Terminal' })).toBeInTheDocument(); expect(screen.getByText('Notifications')).toBeInTheDocument(); @@ -186,6 +189,16 @@ describe('Nav public shell', () => { expect(screen.getByRole('link', { name: 'Packs' })).toHaveAttribute('aria-current', 'page'); }); + it('renders read brand posture and active nav on read routes', () => { + mockPathname = '/read'; + + render(