From cc11e784caeae0a1270af3939414e59c68d9b87b Mon Sep 17 00:00:00 2001 From: Travis Gilbert <1travisgilbert@gmail.com> Date: Sat, 22 Aug 2026 15:43:49 -0400 Subject: [PATCH] feat(mobile): consume generated Theorem contracts --- .commonplace-canonical | 10 +- .github/workflows/mobile-contracts-update.yml | 177 + apps/commonplace-api/.env.example | 51 - apps/commonplace-api/Cargo.lock | 8881 ----------------- apps/commonplace-api/Cargo.toml | 37 - apps/commonplace-api/Dockerfile | 35 - apps/commonplace-api/README.md | 19 - apps/commonplace-api/railway.toml | 28 - apps/commonplace-api/src/auth.rs | 61 - .../src/bin/commonplace-mcp.rs | 87 - apps/commonplace-api/src/briefing.rs | 141 - apps/commonplace-api/src/discover.rs | 145 - apps/commonplace-api/src/find.rs | 691 -- apps/commonplace-api/src/growth.rs | 371 - apps/commonplace-api/src/keystore.rs | 199 - apps/commonplace-api/src/lib.rs | 92 - apps/commonplace-api/src/main.rs | 9 - apps/commonplace-api/src/mcp.rs | 217 - apps/commonplace-api/src/organize.rs | 549 - apps/commonplace-api/src/portability.rs | 225 - apps/commonplace-api/src/publish.rs | 1337 --- apps/commonplace-api/src/reconstruction.rs | 88 - apps/commonplace-api/src/retrieve.rs | 992 -- apps/commonplace-api/src/salience.rs | 811 -- apps/commonplace-api/src/save_url.rs | 261 - apps/commonplace-api/src/schema/find.rs | 385 - apps/commonplace-api/src/schema/mod.rs | 3353 ------- apps/commonplace-api/src/schema/scatter.rs | 167 - apps/commonplace-api/src/serve.rs | 1599 --- apps/commonplace-api/src/voice.rs | 313 - apps/commonplace-api/tests/capture_live.rs | 539 - .../tests/collector_source_ref_acceptance.rs | 187 - .../tests/durable_http_acceptance.rs | 145 - .../tests/durable_mcp_acceptance.rs | 70 - .../tests/f3_api_acceptance.rs | 782 -- apps/commonplace-api/tests/find_acceptance.rs | 351 - .../tests/i1_ask_acceptance.rs | 223 - .../tests/i2_briefing_acceptance.rs | 145 - .../tests/i4_discovery_acceptance.rs | 120 - .../tests/mcp_stdio_acceptance.rs | 86 - .../tests/mobile_wiring_acceptance.rs | 100 - .../tests/objects_http_acceptance.rs | 344 - .../tests/organize_acceptance.rs | 404 - .../tests/pt008_remind_acceptance.rs | 158 - .../tests/pt017_blob_capture_acceptance.rs | 209 - .../tests/x2_portability_acceptance.rs | 153 - apps/mobile/README.md | 12 + apps/mobile/nativewind-env.d.ts | 2 + apps/mobile/package.json | 1 + apps/mobile/src/api/client.ts | 24 + apps/mobile/src/api/harness.ts | 40 + apps/mobile/src/api/types.ts | 19 +- docs/architecture/api-topology.md | 9 +- package.json | 6 +- packages/mobile-contracts/README.md | 16 + .../fixtures/approval_request.json | 11 + .../fixtures/attention_item.json | 8 + .../fixtures/context_lens.json | 32 + .../fixtures/contract_catalog.json | 14 + .../fixtures/control_lease.json | 10 + .../fixtures/field_definition.json | 16 + .../fixtures/graph_neighborhood.json | 33 + .../fixtures/object_type.json | 43 + .../fixtures/remote_surface_token.json | 11 + .../fixtures/resume_page.json | 21 + .../mobile-contracts/fixtures/run_event.json | 12 + .../fixtures/semantic_frame.json | 24 + .../fixtures/snapshot_with_tail.json | 25 + .../fixtures/view_definition.json | 15 + .../mobile-contract-source.json | 20 + packages/mobile-contracts/package.json | 28 + .../scripts/generate-barrels.mjs | 54 + .../ContractDescriptor.ts | 18 + .../ContractValidation.ts | 9 + .../object_schema/AgentSchemaAction.ts | 7 + .../object_schema/Cardinality.ts | 6 + .../object_schema/ContextLens.ts | 48 + .../object_schema/ContextSnapshotRef.ts | 10 + .../object_schema/Enforcement.ts | 7 + .../object_schema/FieldDefinition.ts | 12 + .../object_schema/FieldSensitivity.ts | 7 + .../object_schema/FieldWireType.ts | 9 + .../object_schema/GraphEdge.ts | 7 + .../object_schema/GraphNeighborhood.ts | 10 + .../object_schema/GraphNode.ts | 6 + .../object_schema/LensScope.ts | 8 + .../object_schema/ObjectType.ts | 21 + .../object_schema/Record.ts | 7 + .../object_schema/RelationDefinition.ts | 20 + .../object_schema/RelevancePolicy.ts | 7 + .../object_schema/SchemaActionPolicy.ts | 3 + .../object_schema/SchemaChange.ts | 4 + .../object_schema/SchemaChangeKind.ts | 3 + .../object_schema/SchemaProposal.ts | 55 + .../object_schema/SchemaProposalStatus.ts | 8 + .../object_schema/SchemaRevision.ts | 12 + .../object_schema/SensitiveFieldPolicy.ts | 6 + .../object_schema/TimeWindow.ts | 6 + .../object_schema/ViewDefinition.ts | 19 + .../object_schema/ViewKind.ts | 7 + .../object_schema/ViewOrigin.ts | 3 + .../object_schema/ViewTarget.ts | 8 + .../remote_surface/ControlLease.ts | 12 + .../remote_surface/ControlMode.ts | 7 + .../remote_surface/InputOwner.ts | 3 + .../remote_surface/Point.ts | 3 + .../remote_surface/RemoteSurfaceToken.ts | 10 + .../remote_surface/RemoteSurfaceTransport.ts | 5 + .../remote_surface/SemanticFrame.ts | 30 + .../remote_surface/SurfaceChannel.ts | 6 + .../remote_surface/SurfaceFrame.ts | 11 + .../remote_surface/SurfaceKind.ts | 7 + .../remote_surface/TransportKind.ts | 9 + .../remote_surface/Viewport.ts | 8 + .../mobile-kernel-contract/run_event/Actor.ts | 4 + .../run_event/ActorKind.ts | 3 + .../run_event/ApprovalRequest.ts | 12 + .../run_event/ApprovalStatus.ts | 3 + .../run_event/Artifact.ts | 10 + .../run_event/ArtifactKind.ts | 6 + .../run_event/AttentionItem.ts | 18 + .../run_event/AttentionSource.ts | 7 + .../run_event/CapabilityGrant.ts | 7 + .../run_event/EventCursor.ts | 7 + .../run_event/ExecutionTarget.ts | 9 + .../run_event/ExecutionTargetKind.ts | 6 + .../run_event/MachineClass.ts | 6 + .../run_event/Receipt.ts | 21 + .../run_event/ReceiptAction.ts | 6 + .../run_event/ResumePage.ts | 9 + .../run_event/RiskLevel.ts | 7 + .../mobile-kernel-contract/run_event/Run.ts | 18 + .../run_event/RunEvent.ts | 17 + .../run_event/RunEventKind.ts | 7 + .../run_event/RunStateSnapshot.ts | 12 + .../run_event/RunStatus.ts | 3 + .../run_event/Session.ts | 18 + .../run_event/SnapshotWithTail.ts | 10 + .../src/generated/serde_json/JsonValue.ts | 3 + packages/mobile-contracts/src/index.ts | 6 + .../mobile-contracts/src/object-schema.ts | 29 + .../mobile-contracts/src/remote-surface.ts | 13 + packages/mobile-contracts/src/run-event.ts | 25 + .../mobile-contracts/tests/conformance.ts | 109 + packages/mobile-contracts/tsconfig.json | 16 + pnpm-lock.yaml | 9 + 146 files changed, 1584 insertions(+), 25169 deletions(-) create mode 100644 .github/workflows/mobile-contracts-update.yml delete mode 100644 apps/commonplace-api/.env.example delete mode 100644 apps/commonplace-api/Cargo.lock delete mode 100644 apps/commonplace-api/Cargo.toml delete mode 100644 apps/commonplace-api/Dockerfile delete mode 100644 apps/commonplace-api/README.md delete mode 100644 apps/commonplace-api/railway.toml delete mode 100644 apps/commonplace-api/src/auth.rs delete mode 100644 apps/commonplace-api/src/bin/commonplace-mcp.rs delete mode 100644 apps/commonplace-api/src/briefing.rs delete mode 100644 apps/commonplace-api/src/discover.rs delete mode 100644 apps/commonplace-api/src/find.rs delete mode 100644 apps/commonplace-api/src/growth.rs delete mode 100644 apps/commonplace-api/src/keystore.rs delete mode 100644 apps/commonplace-api/src/lib.rs delete mode 100644 apps/commonplace-api/src/main.rs delete mode 100644 apps/commonplace-api/src/mcp.rs delete mode 100644 apps/commonplace-api/src/organize.rs delete mode 100644 apps/commonplace-api/src/portability.rs delete mode 100644 apps/commonplace-api/src/publish.rs delete mode 100644 apps/commonplace-api/src/reconstruction.rs delete mode 100644 apps/commonplace-api/src/retrieve.rs delete mode 100644 apps/commonplace-api/src/salience.rs delete mode 100644 apps/commonplace-api/src/save_url.rs delete mode 100644 apps/commonplace-api/src/schema/find.rs delete mode 100644 apps/commonplace-api/src/schema/mod.rs delete mode 100644 apps/commonplace-api/src/schema/scatter.rs delete mode 100644 apps/commonplace-api/src/serve.rs delete mode 100644 apps/commonplace-api/src/voice.rs delete mode 100644 apps/commonplace-api/tests/capture_live.rs delete mode 100644 apps/commonplace-api/tests/collector_source_ref_acceptance.rs delete mode 100644 apps/commonplace-api/tests/durable_http_acceptance.rs delete mode 100644 apps/commonplace-api/tests/durable_mcp_acceptance.rs delete mode 100644 apps/commonplace-api/tests/f3_api_acceptance.rs delete mode 100644 apps/commonplace-api/tests/find_acceptance.rs delete mode 100644 apps/commonplace-api/tests/i1_ask_acceptance.rs delete mode 100644 apps/commonplace-api/tests/i2_briefing_acceptance.rs delete mode 100644 apps/commonplace-api/tests/i4_discovery_acceptance.rs delete mode 100644 apps/commonplace-api/tests/mcp_stdio_acceptance.rs delete mode 100644 apps/commonplace-api/tests/mobile_wiring_acceptance.rs delete mode 100644 apps/commonplace-api/tests/objects_http_acceptance.rs delete mode 100644 apps/commonplace-api/tests/organize_acceptance.rs delete mode 100644 apps/commonplace-api/tests/pt008_remind_acceptance.rs delete mode 100644 apps/commonplace-api/tests/pt017_blob_capture_acceptance.rs delete mode 100644 apps/commonplace-api/tests/x2_portability_acceptance.rs create mode 100644 packages/mobile-contracts/README.md create mode 100644 packages/mobile-contracts/fixtures/approval_request.json create mode 100644 packages/mobile-contracts/fixtures/attention_item.json create mode 100644 packages/mobile-contracts/fixtures/context_lens.json create mode 100644 packages/mobile-contracts/fixtures/contract_catalog.json create mode 100644 packages/mobile-contracts/fixtures/control_lease.json create mode 100644 packages/mobile-contracts/fixtures/field_definition.json create mode 100644 packages/mobile-contracts/fixtures/graph_neighborhood.json create mode 100644 packages/mobile-contracts/fixtures/object_type.json create mode 100644 packages/mobile-contracts/fixtures/remote_surface_token.json create mode 100644 packages/mobile-contracts/fixtures/resume_page.json create mode 100644 packages/mobile-contracts/fixtures/run_event.json create mode 100644 packages/mobile-contracts/fixtures/semantic_frame.json create mode 100644 packages/mobile-contracts/fixtures/snapshot_with_tail.json create mode 100644 packages/mobile-contracts/fixtures/view_definition.json create mode 100644 packages/mobile-contracts/mobile-contract-source.json create mode 100644 packages/mobile-contracts/package.json create mode 100644 packages/mobile-contracts/scripts/generate-barrels.mjs create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractDescriptor.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractValidation.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/AgentSchemaAction.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Cardinality.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextLens.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextSnapshotRef.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Enforcement.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldDefinition.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldSensitivity.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldWireType.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphEdge.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNeighborhood.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNode.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/LensScope.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ObjectType.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Record.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelationDefinition.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelevancePolicy.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaActionPolicy.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChange.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChangeKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposal.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposalStatus.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaRevision.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SensitiveFieldPolicy.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/TimeWindow.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewDefinition.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewOrigin.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewTarget.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlLease.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlMode.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/InputOwner.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Point.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceToken.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceTransport.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SemanticFrame.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceChannel.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceFrame.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/TransportKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Viewport.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Actor.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ActorKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalRequest.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalStatus.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Artifact.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ArtifactKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionItem.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionSource.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/CapabilityGrant.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/EventCursor.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTarget.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTargetKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/MachineClass.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Receipt.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ReceiptAction.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ResumePage.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RiskLevel.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Run.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEvent.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEventKind.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStateSnapshot.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStatus.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Session.ts create mode 100644 packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/SnapshotWithTail.ts create mode 100644 packages/mobile-contracts/src/generated/serde_json/JsonValue.ts create mode 100644 packages/mobile-contracts/src/index.ts create mode 100644 packages/mobile-contracts/src/object-schema.ts create mode 100644 packages/mobile-contracts/src/remote-surface.ts create mode 100644 packages/mobile-contracts/src/run-event.ts create mode 100644 packages/mobile-contracts/tests/conformance.ts create mode 100644 packages/mobile-contracts/tsconfig.json diff --git a/.commonplace-canonical b/.commonplace-canonical index 82572382..ca86184f 100644 --- a/.commonplace-canonical +++ b/.commonplace-canonical @@ -18,23 +18,27 @@ }, "consumer_graphql": { "railway_service": "commonplace-api", - "railway_config": "apps/commonplace-api/railway.toml", + "source_repo": "Travis-Gilbert/Theorem", + "source_path": "apps/commonplace-api", "public_url": "https://commonplace-api-production.up.railway.app/graphql", "env_keys": [ "THEOREM_GRAPHQL_URL", "THEOREM_API_KEY" ], - "health": "https://commonplace-api-production.up.railway.app/healthz" + "health": "https://commonplace-api-production.up.railway.app/healthz", + "notes": "Canonical source is Theorem/apps/commonplace-api. The Railway service is the surviving deployment; the prior Fly deployment is retired. Relink Railway source builds to Theorem before the next deploy." }, "object_seam_data_api": { "railway_service": "commonplace-api", + "source_repo": "Travis-Gilbert/Theorem", + "source_path": "apps/commonplace-api", "internal_url": "http://commonplace-api.railway.internal:8080", "public_url": "https://commonplace-api-production.up.railway.app", "env_keys": [ "CONSOLE_DATA_API_URL", "CONSOLE_DATA_API_KEY" ], - "notes": "Object seam required by the Railway launcher. data.theoremharness.com currently points at commonplace-data, a second Console deploy, and is NOT this seam." + "notes": "Object seam required by the Railway launcher. Canonical source is Theorem/apps/commonplace-api. data.theoremharness.com currently points at commonplace-data, a second Console deploy, and is NOT this seam." }, "harness_http": { "railway_service": "theorem", diff --git a/.github/workflows/mobile-contracts-update.yml b/.github/workflows/mobile-contracts-update.yml new file mode 100644 index 00000000..1c24f5ac --- /dev/null +++ b/.github/workflows/mobile-contracts-update.yml @@ -0,0 +1,177 @@ +name: Mobile contracts update + +on: + repository_dispatch: + types: + - mobile-contracts-updated + workflow_dispatch: + inputs: + source_repo: + description: Theorem source repository in owner/repo form. + default: Travis-Gilbert/Theorem + required: true + source_ref: + description: Source branch or ref. + default: main + required: true + source_sha: + description: Exact Theorem commit SHA containing generated bindings. + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + update-mobile-contracts: + runs-on: ubuntu-latest + steps: + - name: Checkout CommonPlace + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Resolve update payload + id: payload + env: + EVENT_PATH: ${{ github.event_path }} + run: | + node <<'NODE' >> "$GITHUB_OUTPUT" + const fs = require('node:fs'); + const event = JSON.parse(fs.readFileSync(process.env.EVENT_PATH, 'utf8')); + const payload = event.client_payload || {}; + const inputs = event.inputs || {}; + console.log(`repo=${payload.repository || inputs.source_repo || 'Travis-Gilbert/Theorem'}`); + console.log(`ref=${payload.ref || inputs.source_ref || 'main'}`); + console.log(`sha=${payload.sha || inputs.source_sha || ''}`); + console.log(`updated_at=${payload.updated_at || new Date().toISOString()}`); + console.log(`object_schema_version=${payload.object_schema_version || 'object-schema-v2'}`); + console.log(`run_event_version=${payload.run_event_version || 'run-event-v1'}`); + console.log(`remote_surface_version=${payload.remote_surface_version || 'remote-surface-v1'}`); + NODE + + - name: Require private source access + env: + SOURCE_TOKEN: ${{ secrets.THEOREM_SOURCE_TOKEN }} + SOURCE_SHA: ${{ steps.payload.outputs.sha }} + run: | + if [ -z "$SOURCE_TOKEN" ]; then + echo "THEOREM_SOURCE_TOKEN is required to read the private Theorem repository." >&2 + exit 1 + fi + if [ -z "$SOURCE_SHA" ]; then + echo "The dispatch must name an exact source SHA." >&2 + exit 1 + fi + + - name: Checkout pinned Theorem contracts + uses: actions/checkout@v4 + with: + repository: ${{ steps.payload.outputs.repo }} + ref: ${{ steps.payload.outputs.sha }} + token: ${{ secrets.THEOREM_SOURCE_TOKEN }} + path: theorem-source + sparse-checkout: | + rustyredcore_THG/bindings/mobile-kernel-contract + rustyredcore_THG/bindings/serde_json + rustyredcore_THG/crates/mobile-kernel-contract/fixtures + + - name: Synchronize generated contracts and fixtures + env: + SOURCE_REPO: ${{ steps.payload.outputs.repo }} + SOURCE_REF: ${{ steps.payload.outputs.ref }} + SOURCE_SHA: ${{ steps.payload.outputs.sha }} + UPDATED_AT: ${{ steps.payload.outputs.updated_at }} + OBJECT_SCHEMA_VERSION: ${{ steps.payload.outputs.object_schema_version }} + RUN_EVENT_VERSION: ${{ steps.payload.outputs.run_event_version }} + REMOTE_SURFACE_VERSION: ${{ steps.payload.outputs.remote_surface_version }} + run: | + if [ "$OBJECT_SCHEMA_VERSION" != "object-schema-v2" ] \ + || [ "$RUN_EVENT_VERSION" != "run-event-v1" ] \ + || [ "$REMOTE_SURFACE_VERSION" != "remote-surface-v1" ]; then + echo "Dispatch named an unsupported mobile contract revision." >&2 + exit 1 + fi + + rm -rf \ + packages/mobile-contracts/src/generated/mobile-kernel-contract \ + packages/mobile-contracts/src/generated/serde_json \ + packages/mobile-contracts/fixtures + mkdir -p packages/mobile-contracts/src/generated packages/mobile-contracts/fixtures + cp -R \ + theorem-source/rustyredcore_THG/bindings/mobile-kernel-contract \ + packages/mobile-contracts/src/generated/ + cp -R \ + theorem-source/rustyredcore_THG/bindings/serde_json \ + packages/mobile-contracts/src/generated/ + cp -R \ + theorem-source/rustyredcore_THG/crates/mobile-kernel-contract/fixtures/. \ + packages/mobile-contracts/fixtures/ + + node <<'NODE' + const fs = require('node:fs'); + const pinPath = 'packages/mobile-contracts/mobile-contract-source.json'; + const pin = JSON.parse(fs.readFileSync(pinPath, 'utf8')); + pin.source = { + repo: process.env.SOURCE_REPO, + ref: process.env.SOURCE_REF, + sha: process.env.SOURCE_SHA, + url: `https://github.com/${process.env.SOURCE_REPO}/tree/${process.env.SOURCE_SHA}`, + }; + pin.contracts = { + objectSchema: process.env.OBJECT_SCHEMA_VERSION, + runEvent: process.env.RUN_EVENT_VERSION, + remoteSurface: process.env.REMOTE_SURFACE_VERSION, + }; + pin.updatedAt = process.env.UPDATED_AT; + fs.writeFileSync(pinPath, `${JSON.stringify(pin, null, 2)}\n`); + NODE + + npm --prefix packages/mobile-contracts run generate + npm --prefix packages/mobile-contracts run check + + - name: Open update PR + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REPO: ${{ steps.payload.outputs.repo }} + SOURCE_REF: ${{ steps.payload.outputs.ref }} + SOURCE_SHA: ${{ steps.payload.outputs.sha }} + run: | + if git diff --quiet -- packages/mobile-contracts; then + echo "Mobile contracts are already current." + exit 0 + fi + + short_sha="${SOURCE_SHA:0:12}" + branch="automation/mobile-contracts-${short_sha}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$branch" + git add packages/mobile-contracts + git commit -m "chore(contracts): update mobile contracts to ${short_sha}" + git push --force-with-lease origin "$branch" + + body_file="$(mktemp)" + { + echo "Updates generated CommonPlace mobile contracts from Theorem." + echo + echo "- Source: ${SOURCE_REPO}@${SOURCE_SHA}" + echo "- Ref: ${SOURCE_REF}" + echo "- Contracts: object-schema-v2, run-event-v1, remote-surface-v1" + echo "- Validation: \`npm --prefix packages/mobile-contracts run check\`" + } > "$body_file" + + if gh pr view "$branch" --json url --jq .url >/dev/null 2>&1; then + gh pr edit "$branch" \ + --title "chore(contracts): update mobile contracts to ${short_sha}" \ + --body-file "$body_file" + else + gh pr create \ + --base main \ + --head "$branch" \ + --title "chore(contracts): update mobile contracts to ${short_sha}" \ + --body-file "$body_file" + fi diff --git a/apps/commonplace-api/.env.example b/apps/commonplace-api/.env.example deleted file mode 100644 index 8c718200..00000000 --- a/apps/commonplace-api/.env.example +++ /dev/null @@ -1,51 +0,0 @@ -# commonplace-api voice configuration (STT + TTS). -# -# Copy to `.env`, fill in real values, and source it before `npm run api:dev`: -# set -a && source apps/commonplace-api/.env && set +a -# The server reads process env; it does NOT auto-load this file. `.env` is -# gitignored; only this `.env.example` template (placeholders) is committed. -# -# SECURITY: never paste a real key into chat, code, or a commit. If a key is -# ever exposed, rotate it at the provider dashboard and put the new value ONLY -# in the local `.env`. - -# --- Speech-to-text (voice capture -> transcript). Unset = audio stored as-is. --- -# Provider: "whisper" (self-hosted OpenAI-compatible) or "elevenlabs" (Scribe). -COMMONPLACE_STT_PROVIDER=elevenlabs -# Self-hosted Whisper node (OpenAI /v1/audio/transcriptions). Used when provider=whisper. -WHISPER_STT_URL=http://127.0.0.1:8080/v1/audio/transcriptions -WHISPER_STT_MODEL=whisper-1 -# ElevenLabs Scribe model. Used when provider=elevenlabs. -ELEVENLABS_STT_MODEL=scribe_v1 - -# --- Text-to-speech (answer read-back). Default provider = elevenlabs. --- -# Provider: "elevenlabs" (hosted) or "kokoro" (self-hosted, OpenAI-compatible). -COMMONPLACE_TTS_PROVIDER=elevenlabs -ELEVENLABS_TTS_VOICE_ID=21m00Tcm4TlvDq8ikWAM -ELEVENLABS_TTS_MODEL=eleven_turbo_v2_5 -# Self-hosted Kokoro node (e.g. kokoro-fastapi /v1/audio/speech). Used when provider=kokoro. -# bm_george is a British-male "butler" register; swap to any installed Kokoro voice. -KOKORO_TTS_URL=http://127.0.0.1:8081/v1/audio/speech -KOKORO_TTS_VOICE=bm_george -KOKORO_TTS_MODEL=kokoro - -# --- Provider key (referenced by name only; required for elevenlabs STT/TTS). --- -ELEVENLABS_API_KEY= - -# --- Mobile runtime bootstrap (all values returned to the authenticated app are public) --- -# The deployed Console POST/SSE route. Mobile may override this per instance. -COMMONPLACE_CHAT_URL=https://v2.theoremharness.com/api/chat/stream -# Enable only when the Console route consumes file/image content parts. -COMMONPLACE_CHAT_ATTACHMENTS=false -# Enables the chat surface's live web-research request. -COMMONPLACE_WEB_SEARCH=false -# Expo project id is public; APNs/FCM credentials remain in Expo/EAS. -COMMONPLACE_EXPO_PROJECT_ID= -# Theorem's authenticated MCP endpoint for the reconstruction GraphQL bridge. -# Both are server-only values; set neither to keep reconstruction unavailable. -THEOREM_RECONSTRUCTION_MCP_URL= -THEOREM_RECONSTRUCTION_MCP_TOKEN= -# Authenticated token-registration relay, normally owned by theorem-harness-server. -COMMONPLACE_PUSH_REGISTRATION_URL= -# Optional deployment additions to plugin/skill items already stored in CommonPlace. -# COMMONPLACE_MOBILE_CATALOG_JSON='[{"id":"theorems-harness","kind":"plugin","name":"Theorems Harness","description":"Grounded theorem runtime tools"}]' diff --git a/apps/commonplace-api/Cargo.lock b/apps/commonplace-api/Cargo.lock deleted file mode 100644 index fc3fae82..00000000 --- a/apps/commonplace-api/Cargo.lock +++ /dev/null @@ -1,8881 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - -[[package]] -name = "accesskit" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "uuid", -] - -[[package]] -name = "addr2line" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "const-random", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.13.0", - "cc", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 2.0.18", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ar_archive_writer" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" -dependencies = [ - "object 0.37.3", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - -[[package]] -name = "archery" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "arrow-array" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9486151b2f0785bafc6fa04fc5c99fcb4495455662e58787ea32eaaed33c4192" -dependencies = [ - "ahash", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "chrono", - "half", - "hashbrown 0.17.1", - "num-complex", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-buffer" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4776577a87794bfdf0b4e90e2ea12454fa7738ea2823c4be5b9d1851da7b434" -dependencies = [ - "bytes", - "half", - "num-bigint", - "num-traits", -] - -[[package]] -name = "arrow-cast" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ad451ce4f98710828a455b96991b8f031deb2e67f5fcad6773f017e4a69c3a" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-ord", - "arrow-schema", - "arrow-select", - "atoi", - "base64 0.22.1", - "chrono", - "half", - "lexical-core", - "num-traits", - "ryu", -] - -[[package]] -name = "arrow-data" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b38fe43e2e8704360f1464e6e8cc4fc381ef02cc4fb0192afa8df1aaa0115c66" -dependencies = [ - "arrow-buffer", - "arrow-schema", - "half", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-ipc" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29dac499fcbc6ba74ee0324057821d381929a48526a3966bd9dffb44aa06d98c" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", - "flatbuffers", -] - -[[package]] -name = "arrow-json" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe05e916ddc50f4c7a363cd69c0ef5894fcee063517e9a0b8582f0c56746af6" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-ord", - "arrow-schema", - "arrow-select", - "chrono", - "half", - "indexmap", - "itoa", - "lexical-core", - "memchr", - "num-traits", - "ryu", - "serde_core", - "serde_json", - "simdutf8", -] - -[[package]] -name = "arrow-ord" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13dbdc2a9c053c10c7baa6e30faee04a180aa7ce88e471835850ce37abd20b" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", -] - -[[package]] -name = "arrow-schema" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9e4969dc350d571766247143ab36a5187d095d3d3690970408bc630d47c69e5" - -[[package]] -name = "arrow-select" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402770dba90865359d98d1ef92ef16e23d75c0cca9c2c880c8a05468b7743bf9" -dependencies = [ - "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "num-traits", -] - -[[package]] -name = "ascii_utils" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" - -[[package]] -name = "assert_type_match" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "astral-tl" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d90933ffb0f97e2fc2e0de21da9d3f20597b804012d199843a6fe7c2810d28f3" -dependencies = [ - "memchr", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-graphql" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1057a9f7ccf2404d94571dec3451ade1cb524790df6f1ada0d19c2a49f6b0f40" -dependencies = [ - "async-graphql-derive", - "async-graphql-parser", - "async-graphql-value", - "async-io", - "async-trait", - "asynk-strim", - "base64 0.22.1", - "bytes", - "fast_chemail", - "fnv", - "futures-util", - "handlebars", - "http", - "indexmap", - "mime", - "multer", - "num-traits", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "static_assertions_next", - "tempfile", - "thiserror 2.0.18", -] - -[[package]] -name = "async-graphql-axum" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e37c5532e4b686acf45e7162bc93da91fc2c702fb0d465efc2c20c8f973795" -dependencies = [ - "async-graphql", - "axum", - "bytes", - "futures-util", - "serde_json", - "tokio", - "tokio-stream", - "tokio-util", - "tower-service", -] - -[[package]] -name = "async-graphql-derive" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6cbeadc8515e66450fba0985ce722192e28443697799988265d86304d7cc68" -dependencies = [ - "Inflector", - "async-graphql-parser", - "darling 0.23.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "strum", - "syn 2.0.118", - "thiserror 2.0.18", -] - -[[package]] -name = "async-graphql-parser" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64ef70f77a1c689111e52076da1cd18f91834bcb847de0a9171f83624b07fbf" -dependencies = [ - "async-graphql-value", - "pest", - "serde", - "serde_json", -] - -[[package]] -name = "async-graphql-value" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3ef112905abea9dea592fc868a6873b10ebd3f983e83308f995d6284e9ba41" -dependencies = [ - "bytes", - "indexmap", - "serde", - "serde_json", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "asynk-strim" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "base64 0.22.1", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "multer", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper", - "tokio", - "tokio-tungstenite", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bevy" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950238d3049d81006a6fd6b898a34cfc01bb5462a7668795a54d640aed19fd0a" -dependencies = [ - "bevy_internal", -] - -[[package]] -name = "bevy_a11y" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86f2cb45b000a9d121a5a7606f9af4a49e72b8b6a3885dc2acac2f0897a04f1" -dependencies = [ - "accesskit", - "bevy_app", - "bevy_derive", - "bevy_ecs", - "bevy_reflect", -] - -[[package]] -name = "bevy_android" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e5e2a949be318bf7184eb084622892afcc1e8b2af44973ac53ab53201756e0" -dependencies = [ - "android-activity", -] - -[[package]] -name = "bevy_app" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671ae6f3a8d84f9ed696c531a138558596e041231a8414b6efeafa8469e841f9" -dependencies = [ - "bevy_derive", - "bevy_ecs", - "bevy_platform", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "ctrlc", - "downcast-rs", - "log", - "thiserror 2.0.18", - "variadics_please", -] - -[[package]] -name = "bevy_color" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2dfc0091be6eb62fec9158b2d3e6f21b002be7bdb42dd3b8322e949c8426b0" -dependencies = [ - "bevy_math", - "bevy_reflect", - "bytemuck", - "derive_more", - "serde", - "thiserror 2.0.18", - "wgpu-types", -] - -[[package]] -name = "bevy_derive" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293df2b2f7ed65ef2ce02b2e7359faac1a9a92a8cc96c182c9de77eb06049ae0" -dependencies = [ - "bevy_macro_utils", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "bevy_diagnostic" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a73e11555051f37c3e0e9ef7775e77108f2482310242996fb5bb6ef8b685b78e" -dependencies = [ - "atomic-waker", - "bevy_app", - "bevy_ecs", - "bevy_platform", - "bevy_tasks", - "bevy_time", - "const-fnv1a-hash", - "log", - "serde", -] - -[[package]] -name = "bevy_ecs" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4654b9b3535fa7813d2878a50e1b5ad80a361dc1c913b96ded57667f65d70a01" -dependencies = [ - "arrayvec", - "bevy_ecs_macros", - "bevy_platform", - "bevy_ptr", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "bitflags 2.13.0", - "bumpalo", - "concurrent-queue", - "derive_more", - "fixedbitset 0.5.7", - "indexmap", - "log", - "nonmax", - "serde", - "slotmap", - "smallvec", - "thiserror 2.0.18", - "variadics_please", -] - -[[package]] -name = "bevy_ecs_macro_logic" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee0b98a74141ce8dce4654c60020ebbaefab32646f42af6bbae55f282ae65ff7" -dependencies = [ - "bevy_macro_utils", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "bevy_ecs_macros" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc958a194d226d618404e0fea99a3c8b4146207a00a3ba07fa712bf71607240" -dependencies = [ - "bevy_ecs_macro_logic", - "bevy_macro_utils", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "bevy_input" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd221cf0732f5bf06caf594bdb233361ac735d4d416cf5849757b64bf4e8472a" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_math", - "bevy_platform", - "bevy_reflect", - "derive_more", - "log", - "thiserror 2.0.18", -] - -[[package]] -name = "bevy_input_focus" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1a14f56c6e722048ec9dc20dbbc6f46b8037f61e319ea776829199278da166" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_input", - "bevy_math", - "bevy_reflect", - "bevy_window", - "log", - "thiserror 2.0.18", -] - -[[package]] -name = "bevy_internal" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a85632a749f956f92b7b391c31ce1f229e57b06de879d1b60a3bb91e4e240" -dependencies = [ - "bevy_a11y", - "bevy_android", - "bevy_app", - "bevy_color", - "bevy_derive", - "bevy_diagnostic", - "bevy_ecs", - "bevy_input", - "bevy_input_focus", - "bevy_math", - "bevy_platform", - "bevy_ptr", - "bevy_reflect", - "bevy_state", - "bevy_tasks", - "bevy_time", - "bevy_transform", - "bevy_utils", - "bevy_window", -] - -[[package]] -name = "bevy_macro_utils" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "746a19912c6dc1bbe79188778573e8a253d5832c696b2fcb95578c17b29ff7ba" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "toml_edit", -] - -[[package]] -name = "bevy_math" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c280440d2a5bc07ea393b5346b5712eec73ea30f0133097b8b13dade385beb" -dependencies = [ - "approx", - "arrayvec", - "bevy_reflect", - "derive_more", - "glam", - "itertools", - "libm", - "rand 0.10.2", - "rand_distr", - "serde", - "thiserror 2.0.18", - "variadics_please", -] - -[[package]] -name = "bevy_platform" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3120670bb2308980f723477c9d82476fcda31f08be9b256c3f0acdee82a65094" -dependencies = [ - "critical-section", - "foldhash 0.2.0", - "futures-channel", - "futures-lite", - "hashbrown 0.16.1", - "js-sys", - "portable-atomic", - "portable-atomic-util", - "serde", - "spin 0.10.1", - "wasm-bindgen", - "wasm-bindgen-futures", - "windows-sys 0.61.2", -] - -[[package]] -name = "bevy_ptr" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b511e89f7078fd21fdea77061a0ca3e737297c7011bb10c073e0aecb17c9fea6" - -[[package]] -name = "bevy_reflect" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92abd59cbba1524c1847e9a50f74d94e6b7836c80a8edfa9c47e59f7fd81021d" -dependencies = [ - "assert_type_match", - "bevy_platform", - "bevy_ptr", - "bevy_reflect_derive", - "bevy_utils", - "derive_more", - "disqualified", - "downcast-rs", - "erased-serde", - "foldhash 0.2.0", - "glam", - "indexmap", - "serde", - "smallvec", - "smol_str", - "thiserror 2.0.18", - "uuid", - "variadics_please", - "wgpu-types", -] - -[[package]] -name = "bevy_reflect_derive" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fa4e6b97fd2d582dd5ed96762a70d04505a477c833cf4f69d016dcd03d2d04" -dependencies = [ - "bevy_macro_utils", - "indexmap", - "proc-macro2", - "quote", - "syn 2.0.118", - "uuid", -] - -[[package]] -name = "bevy_state" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c22e694ea52e42994aea6ed2d0b378fa1e9d5a9b615db88626e33508bc03f35" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_platform", - "bevy_reflect", - "bevy_state_macros", - "bevy_utils", - "log", - "variadics_please", -] - -[[package]] -name = "bevy_state_macros" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835f45091fa0df1ff3cb10f4ca5a397d4e9249b550f1be6893b08a2a06d6eed7" -dependencies = [ - "bevy_macro_utils", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "bevy_tasks" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa34e6b9b804851ec08a41c0346c859d82e2fa98c906d74b965ee61bbbb688e4" -dependencies = [ - "async-executor", - "async-task", - "atomic-waker", - "bevy_platform", - "crossbeam-queue", - "derive_more", - "futures-lite", - "heapless", - "web-task", -] - -[[package]] -name = "bevy_time" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21e3be2aa4426ea1e0a7036d3d1dbbded84c319459d9f3e2a616b33563372f2" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_platform", - "bevy_reflect", - "crossbeam-channel", - "log", - "serde", -] - -[[package]] -name = "bevy_transform" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95fefe69c9223d10e6b52a0fdf12811bab9d0c7dcb27570cb86824b451f180d" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "derive_more", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "bevy_utils" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aceea6c7ffdd568f866d5ca4dfe50c33440c54f7bc230c13a9ba7ab0c91aed1" -dependencies = [ - "async-channel", - "bevy_platform", - "disqualified", - "indexmap", - "thread_local", -] - -[[package]] -name = "bevy_window" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcc255b43db156fba393b2ff8fa552aac5e7e02ae4c6298eacdbab655ddf988" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_input", - "bevy_math", - "bevy_platform", - "bevy_reflect", - "log", - "raw-window-handle", - "serde", -] - -[[package]] -name = "bigdecimal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - -[[package]] -name = "bitmaps" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" - -[[package]] -name = "blake3" -version = "1.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq 0.4.2", - "cpufeatures 0.3.0", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "bm25" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cbd8ffdfb7b4c2ff038726178a780a94f90525ed0ad264c0afaa75dd8c18a64" -dependencies = [ - "cached", - "deunicode", - "fxhash", - "rust-stemmers", - "stop-words", - "unicode-segmentation", -] - -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata 0.1.10", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -dependencies = [ - "allocator-api2", -] - -[[package]] -name = "bytecheck" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "rancor", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" -dependencies = [ - "serde", -] - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "cached" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" -dependencies = [ - "ahash", - "cached_proc_macro", - "cached_proc_macro_types", - "hashbrown 0.15.5", - "once_cell", - "thiserror 2.0.18", - "web-time", -] - -[[package]] -name = "cached_proc_macro" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" - -[[package]] -name = "cap-fs-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes", - "windows-sys 0.52.0", -] - -[[package]] -name = "cap-primitives" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix 1.1.4", - "rustix-linux-procfs", - "windows-sys 0.52.0", - "winx", -] - -[[package]] -name = "cap-rand" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" -dependencies = [ - "ambient-authority", - "rand 0.8.6", -] - -[[package]] -name = "cap-std" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", -] - -[[package]] -name = "cap-time-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" -dependencies = [ - "ambient-authority", - "cap-primitives", - "iana-time-zone", - "once_cell", - "rustix 1.1.4", - "winx", -] - -[[package]] -name = "cbindgen" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20" -dependencies = [ - "heck", - "indexmap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 2.0.118", - "tempfile", - "toml", -] - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "commonplace" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "rustyred-thg-reconstruct-fact", - "serde", - "serde_json", - "sha2", - "theorem-harness-core", - "theorem-oplog", -] - -[[package]] -name = "commonplace" -version = "0.1.1" -dependencies = [ - "blake3", - "chrono", - "rustyred-thg-core", - "rustyred-thg-reconstruct-fact", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "commonplace-api" -version = "0.1.0" -dependencies = [ - "async-graphql", - "async-graphql-axum", - "axum", - "base64 0.22.1", - "chrono", - "commonplace 0.1.1", - "ed25519-dalek", - "hex", - "reqwest 0.12.28", - "rustyred-thg-core", - "rustyred-thg-find", - "rustyred-web", - "serde", - "serde_json", - "theorem-harness-core", - "theorem-harness-runtime", - "tokio", - "tower-http", - "yrs", - "zeroize", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", - "portable-atomic", -] - -[[package]] -name = "const-fnv1a-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpp_demangle" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "cranelift-assembler-x64" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" -dependencies = [ - "cranelift-assembler-x64-meta", -] - -[[package]] -name = "cranelift-assembler-x64-meta" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" -dependencies = [ - "cranelift-srcgen", -] - -[[package]] -name = "cranelift-bforest" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" -dependencies = [ - "cranelift-entity", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-bitset" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" -dependencies = [ - "serde", - "serde_derive", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.16.1", - "libm", - "log", - "pulley-interpreter", - "regalloc2", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" -dependencies = [ - "cranelift-assembler-x64-meta", - "cranelift-codegen-shared", - "cranelift-srcgen", - "heck", - "pulley-interpreter", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" - -[[package]] -name = "cranelift-control" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" -dependencies = [ - "cranelift-bitset", - "serde", - "serde_derive", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-frontend" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-isle" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" - -[[package]] -name = "cranelift-native" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon", -] - -[[package]] -name = "cranelift-srcgen" -version = "0.130.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.13.1", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ctrlc" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" -dependencies = [ - "dispatch2", - "nix", - "windows-sys 0.61.2", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.118", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.118", - "unicode-xid", -] - -[[package]] -name = "design-check" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-reconstruct-fact", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "disqualified" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" - -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.6.4", - "serde", - "sha2", - "signature", - "subtle", - "zeroize", -] - -[[package]] -name = "egg" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb749745461743bb477fba3ef87c663d5965876155c676c9489cfe0963de5ab" -dependencies = [ - "env_logger", - "hashbrown 0.15.5", - "indexmap", - "log", - "num-bigint", - "num-traits", - "quanta", - "rustc-hash", - "saturating", - "smallvec", - "symbol_table", - "symbolic_expressions", - "thiserror 1.0.69", -] - -[[package]] -name = "egglog" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e053dcef2cd92b0dee0acfad08d83a98c1c1e55b4c55b8eefa2c0e3f4d055" -dependencies = [ - "csv", - "dyn-clone", - "egglog-add-primitive", - "egglog-ast", - "egglog-bridge", - "egglog-core-relations", - "egglog-numeric-id", - "egglog-reports", - "egraph-serialize", - "hashbrown 0.16.1", - "im-rc", - "indexmap", - "log", - "num", - "ordered-float", - "rayon", - "rustc-hash", - "serde_json", - "thiserror 2.0.18", - "web-time", -] - -[[package]] -name = "egglog-add-primitive" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea7ffb4d889c382918cfa139256f2a4d3f46de6e08d9c071e33543393bd3663" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "egglog-ast" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c67f2aa84cdabd48b266cff0ad4bc6d4cb4386ca51d8c8d93e089f182bc162b" -dependencies = [ - "ordered-float", -] - -[[package]] -name = "egglog-bridge" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf9e5c5f59f5edb456367dd1e7e30fe5ecfd5b434954ec7ad2d90b3169d9a2f" -dependencies = [ - "anyhow", - "dyn-clone", - "egglog-core-relations", - "egglog-numeric-id", - "egglog-reports", - "egglog-union-find", - "hashbrown 0.16.1", - "indexmap", - "log", - "num-rational", - "once_cell", - "ordered-float", - "petgraph 0.8.3", - "rayon", - "smallvec", - "thiserror 2.0.18", - "web-time", -] - -[[package]] -name = "egglog-concurrency" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5cf71f21545d7b14aceb21e51fdc16b1077e5063df03d57253fc13f098ea2cd" -dependencies = [ - "arc-swap", - "egglog-numeric-id", - "rayon", - "smallvec", -] - -[[package]] -name = "egglog-core-relations" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d477debbbe77c0fb24df98c37560cce6b8dcd8d05918a009926204cbf13c73" -dependencies = [ - "anyhow", - "bumpalo", - "crossbeam", - "crossbeam-queue", - "dashmap", - "dyn-clone", - "egglog-concurrency", - "egglog-numeric-id", - "egglog-reports", - "egglog-union-find", - "fixedbitset 0.5.7", - "hashbrown 0.16.1", - "indexmap", - "log", - "num", - "once_cell", - "petgraph 0.8.3", - "rand 0.9.4", - "rayon", - "rustc-hash", - "smallvec", - "thiserror 2.0.18", - "web-time", -] - -[[package]] -name = "egglog-numeric-id" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b207a4ee3241bc8c84bb314d8c6802fa4974c4742a9b1af5e7646ec6ddb5ae5" -dependencies = [ - "rayon", -] - -[[package]] -name = "egglog-reports" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a58e0caea9c3d0fe0486245784f579645576f3b4a8be97fb5828655b281208" -dependencies = [ - "clap", - "hashbrown 0.16.1", - "indexmap", - "rustc-hash", - "serde", - "serde_json", - "web-time", -] - -[[package]] -name = "egglog-union-find" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175f8e99129dcdac7f96b12cc4703c4145bb74a1a53accc46073a19ace10b9f9" -dependencies = [ - "crossbeam", - "egglog-concurrency", - "egglog-numeric-id", -] - -[[package]] -name = "egraph-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0977732fb537ace6f8c15ce160ebdda78b6502b4866d3b904e4fe752e2be4702" -dependencies = [ - "indexmap", - "once_cell", - "ordered-float", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ensemble" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "ed25519-dalek", - "hex", - "rustyred-thg-affordances", - "rustyred-thg-core", - "serde", - "serde_json", - "theorem-harness-core", -] - -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "extism" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b66cd9ac5c64b49c9bac69db3d1b10d8f9386e7caab73489c2f197ba43d5e05" -dependencies = [ - "anyhow", - "async-trait", - "cbindgen", - "extism-convert", - "extism-manifest", - "glob", - "libc", - "serde", - "serde_json", - "sha2", - "toml", - "tracing", - "tracing-subscriber", - "url", - "uuid", - "wasi-common", - "wasmtime", - "wiggle", -] - -[[package]] -name = "extism-convert" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bytemuck", - "extism-convert-macros", - "prost", - "rmp-serde", - "serde", - "serde_json", -] - -[[package]] -name = "extism-convert-macros" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2932799f6d9f9646f97b65287f6bb2addc75a0ee61e40fb24559a7540dd928" -dependencies = [ - "manyhow", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "extism-manifest" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383" -dependencies = [ - "base64 0.22.1", - "serde", - "serde_json", -] - -[[package]] -name = "fancy-regex" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" -dependencies = [ - "bit-set", - "regex-automata 0.4.14", - "regex-syntax", -] - -[[package]] -name = "fast_chemail" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -dependencies = [ - "ascii_utils", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.52.0", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "file-format" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d9ccda37e95b4f0978a3074b4a9939979103a7256459cfb449c9c84d1adf23" - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" -dependencies = [ - "bitflags 2.13.0", - "rustc_version", -] - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float_eq" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" - -[[package]] -name = "fluent-uri" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fraction" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" -dependencies = [ - "lazy_static", - "num", -] - -[[package]] -name = "fs-set-times" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" -dependencies = [ - "io-lifetimes", - "rustix 1.1.4", - "windows-sys 0.52.0", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "fuzzyhash" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f79db37cf4b2ca9a6b735f62ca679926ba0d233d47e164147298fd4fa4786db" - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "fxprof-processed-profile" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" -dependencies = [ - "bitflags 2.13.0", - "debugid", - "rustc-hash", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gimli" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" -dependencies = [ - "fnv", - "hashbrown 0.16.1", - "indexmap", - "stable_deref_trait", -] - -[[package]] -name = "glam" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" -dependencies = [ - "bytemuck", - "libm", - "serde_core", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "h2" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h3o" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b141fa7998c2c993b9431247f6e2eb69d606bd51173ab85394792f3a7cdf7" -dependencies = [ - "ahash", - "either", - "float_eq", - "h3o-bit", - "libm", -] - -[[package]] -name = "h3o-bit" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b42eb4efef1f96510ae1a33b2682562a677d504641e9903a77bf5c666b9013e" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "handlebars" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43ccdfe15a81ab0a8af639e90254227c9a46afd9c5f5b6ec7efaa345c4b0f00" -dependencies = [ - "derive_builder", - "log", - "num-order", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", - "serde", - "serde_core", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heapless" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" -dependencies = [ - "hash32", - "portable-atomic", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "html-escape" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c1ff2d1cbf39efe5af0900ced8a069b5e61557a17544eb0c4a50239937389e" - -[[package]] -name = "html-to-markdown-rs" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeca9cd5e5ac266afd281a144fc73cbd2115af488d9f06858af5f835d0fee363" -dependencies = [ - "ahash", - "astral-tl", - "base64 0.23.0", - "bitflags 2.13.0", - "html-escape", - "html5ever 0.39.0", - "lru", - "memchr", - "once_cell", - "phf 0.14.0", - "regex", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "html2text" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf7722c2ffdd62628b6e13065b6ab6cf154a236bd476c6e89af1352d745b83e" -dependencies = [ - "html5ever 0.29.1", - "markup5ever 0.14.1", - "nom", - "tendril 0.4.3", - "thiserror 2.0.18", - "unicode-width", -] - -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - -[[package]] -name = "html5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" -dependencies = [ - "log", - "markup5ever 0.39.0", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "im-rc" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" -dependencies = [ - "bitmaps 2.1.0", - "rand_core 0.6.4", - "rand_xoshiro 0.6.0", - "sized-chunks", - "typenum", - "version_check", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", -] - -[[package]] -name = "imbl" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e525189e5f603908d0c6e0d402cb5de9c4b2c8866151fabc4ebd771ed2630a2e" -dependencies = [ - "archery", - "bitmaps 3.2.1", - "imbl-sized-chunks", - "rand_core 0.9.5", - "rand_xoshiro 0.7.0", - "serde_core", - "version_check", - "wide", -] - -[[package]] -name = "imbl-sized-chunks" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" -dependencies = [ - "bitmaps 3.2.1", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - -[[package]] -name = "io-extras" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" -dependencies = [ - "io-lifetimes", - "windows-sys 0.52.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "ittapi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" -dependencies = [ - "anyhow", - "ittapi-sys", - "log", -] - -[[package]] -name = "ittapi-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" -dependencies = [ - "cc", -] - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.118", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "jsonschema" -version = "0.46.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" -dependencies = [ - "ahash", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "jsonschema-regex", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "reqwest 0.13.4", - "rustls", - "serde", - "serde_json", - "unicode-general-category", - "uuid-simd", -] - -[[package]] -name = "jsonschema-regex" -version = "0.46.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "kqueue" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags 2.13.0", - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "lexical-core" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" -dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", -] - -[[package]] -name = "lexical-parse-float" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" -dependencies = [ - "lexical-parse-integer", - "lexical-util", -] - -[[package]] -name = "lexical-parse-integer" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" -dependencies = [ - "lexical-util", -] - -[[package]] -name = "lexical-util" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" - -[[package]] -name = "lexical-write-float" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" -dependencies = [ - "lexical-util", - "lexical-write-integer", -] - -[[package]] -name = "lexical-write-integer" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" -dependencies = [ - "lexical-util", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "liteparse" -version = "2.4.0" -source = "git+https://github.com/run-llama/liteparse.git?rev=a8288d09cb6bf93c2f7b5257e9634afd26a4566b#a8288d09cb6bf93c2f7b5257e9634afd26a4566b" -dependencies = [ - "blake3", - "clap", - "file-format", - "image", - "liteparse-pdfium", - "liteparse-pdfium-sys", - "ordered-float", - "reqwest 0.13.4", - "rmp", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "url", - "web-time", -] - -[[package]] -name = "liteparse-pdfium" -version = "1.3.0" -source = "git+https://github.com/run-llama/liteparse.git?rev=a8288d09cb6bf93c2f7b5257e9634afd26a4566b#a8288d09cb6bf93c2f7b5257e9634afd26a4566b" -dependencies = [ - "liteparse-pdfium-sys", -] - -[[package]] -name = "liteparse-pdfium-sys" -version = "1.3.0" -source = "git+https://github.com/run-llama/liteparse.git?rev=a8288d09cb6bf93c2f7b5257e9634afd26a4566b#a8288d09cb6bf93c2f7b5257e9634afd26a4566b" -dependencies = [ - "flate2", - "libloading", - "tar", - "ureq", -] - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" - -[[package]] -name = "lol_html" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00aad58f6ec3990e795943872f13651e7a5fa59dca2c8f31a74faf8a0e0fb652" -dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "cssparser", - "encoding_rs", - "foldhash 0.2.0", - "hashbrown 0.17.1", - "memchr", - "mime", - "precomputed-hash", - "selectors", - "thiserror 2.0.18", -] - -[[package]] -name = "lru" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" -dependencies = [ - "hashbrown 0.17.1", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "manyhow" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" -dependencies = [ - "manyhow-macros", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "manyhow-macros" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", -] - -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", -] - -[[package]] -name = "markup5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" -dependencies = [ - "log", - "tendril 0.5.1", - "web_atoms", -] - -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata 0.4.14", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memfd" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" -dependencies = [ - "rustix 1.1.4", -] - -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - -[[package]] -name = "micromap" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin 0.9.8", - "version_check", -] - -[[package]] -name = "munge" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" -dependencies = [ - "munge_macro", -] - -[[package]] -name = "munge_macro" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonmax" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" - -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.13.0", - "filetime", - "inotify", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-modular" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc41a1374056e9672221567958a66c16be12d0e2c1b408761e14d901c237d5e0" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "object" -version = "0.38.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" -dependencies = [ - "crc32fast", - "hashbrown 0.16.1", - "indexmap", - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "ordered-float" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "parquet" -version = "59.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5302d4da74d6596a1f11f9928767995b53bca657cbeea1e4e8c5074f8a1157dd" -dependencies = [ - "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-ipc", - "arrow-schema", - "arrow-select", - "base64 0.22.1", - "bytes", - "chrono", - "half", - "hashbrown 0.17.1", - "num-bigint", - "num-integer", - "num-traits", - "paste", - "seq-macro", - "twox-hash", -] - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap", -] - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset 0.5.7", - "hashbrown 0.15.5", - "indexmap", - "serde", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" -dependencies = [ - "phf_macros 0.14.0", - "phf_shared 0.14.0", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.6", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" -dependencies = [ - "fastrand", - "phf_shared 0.14.0", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "phf_macros" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" -dependencies = [ - "phf_generator 0.14.0", - "phf_shared 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pilot-core" -version = "0.0.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "portable-atomic" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-utils" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" -dependencies = [ - "proc-macro2", - "quote", - "smallvec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prose-check" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "psm" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "ptr_meta" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "pulley-interpreter" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" -dependencies = [ - "cranelift-bitset", - "log", - "pulley-macros", - "wasmtime-internal-core", -] - -[[package]] -name = "pulley-macros" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rancor" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" -dependencies = [ - "ptr_meta", -] - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_distr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" -dependencies = [ - "num-traits", - "rand 0.10.2", -] - -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "rand_xoshiro" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "recursive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" -dependencies = [ - "recursive-proc-macro-impl", - "stacker", -] - -[[package]] -name = "recursive-proc-macro-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "ref-cast" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.2", -] - -[[package]] -name = "referencing" -version = "0.46.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" -dependencies = [ - "ahash", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "itoa", - "micromap", - "parking_lot", - "percent-encoding", - "serde_json", -] - -[[package]] -name = "regalloc2" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.17.1", - "log", - "rustc-hash", - "smallvec", -] - -[[package]] -name = "regex" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.14", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rend" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" -dependencies = [ - "bytecheck", - "bytes", - "hashbrown 0.17.1", - "indexmap", - "munge", - "ptr_meta", - "rancor", - "rend", - "rkyv_derive", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "roaring" -version = "0.10.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" -dependencies = [ - "bytemuck", - "byteorder", -] - -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustix-linux-procfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" -dependencies = [ - "once_cell", - "rustix 1.1.4", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rustyred-code-embedding" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde_json", -] - -[[package]] -name = "rustyred-hipporag" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "futures-util", - "rustyred-membrane", - "rustyred-thg-core", - "serde", - "serde_json", -] - -[[package]] -name = "rustyred-membrane" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "rustyred-thg-core", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "rustyred-plugin" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "extism", - "rustyred-thg-core", - "serde", - "serde_json", - "sha2", - "theorem-harness-core", - "wat", -] - -[[package]] -name = "rustyred-rerank" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "reqwest 0.12.28", - "rustyred-membrane", - "serde", - "serde_json", -] - -[[package]] -name = "rustyred-thg-affordances" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-code-embedding", - "rustyred-plugin", - "rustyred-thg-core", - "serde", - "serde_json", - "theorem-bayes", - "theorem-harness-core", -] - -[[package]] -name = "rustyred-thg-agentfs" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "crossbeam-channel", - "notify", - "rustyred-thg-core", - "serde", - "serde_json", -] - -[[package]] -name = "rustyred-thg-checkpoint" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "ed25519-dalek", - "half", - "hex", - "memmap2", - "rustyred-thg-core", - "serde", - "serde_json", - "sha2", - "zeroize", -] - -[[package]] -name = "rustyred-thg-core" -version = "0.5.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "bm25", - "crc32fast", - "ed25519-dalek", - "egg", - "egglog", - "fs2", - "h3o", - "html2text", - "imbl", - "memmap2", - "rkyv", - "roaring", - "rustyred-thg-derivation", - "serde", - "serde_json", - "sha2", - "theorem-bayes", - "zip", - "zstd 0.13.3", -] - -[[package]] -name = "rustyred-thg-datawave" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "bigdecimal", - "fuzzyhash", - "liteparse", - "regex", - "rustyred-thg-checkpoint", - "rustyred-thg-core", - "rustyred-thg-reconstruct-fact", - "serde", - "serde_json", - "sha2", - "unicode-normalization", -] - -[[package]] -name = "rustyred-thg-derivation" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "egglog", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-episode-supply-chain" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "arrow-array", - "arrow-json", - "arrow-schema", - "clap", - "parquet", - "reqwest 0.12.28", - "rustyred-thg-core", - "rustyred-thg-datawave", - "rustyred-web", - "serde", - "serde_json", - "theorem-harness-core", - "tokio", -] - -[[package]] -name = "rustyred-thg-find" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "aho-corasick", - "blake3", - "memchr", - "rustyred-hipporag", - "rustyred-membrane", - "rustyred-thg-core", - "rustyred-thg-datawave", - "scene-os-core", - "serde", - "serde_json", - "ureq", -] - -[[package]] -name = "rustyred-thg-graph-lisp" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", - "sha2", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-memory" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "regex", - "rustyred-thg-core", - "rustyred-thg-derivation", - "rustyred-thg-nlp", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "rustyred-thg-ml" -version = "0.5.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "serde", -] - -[[package]] -name = "rustyred-thg-nlp" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "phf 0.11.3", - "regex", - "rust-stemmers", - "rustyred-thg-resolve", - "serde", - "serde_json", -] - -[[package]] -name = "rustyred-thg-offload" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "egg", - "rustyred-thg-core", - "serde", - "serde_json", - "sha2", - "theorem-bayes", -] - -[[package]] -name = "rustyred-thg-programmable-graph" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "rustyred-thg-derivation", - "rustyred-thg-graph-lisp", - "rustyred-thg-schema", - "serde", - "serde_json", - "theorem-blocks", - "theorem-evolve", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-reconstruct-fact" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "serde", - "serde_json", - "theorem-bayes", -] - -[[package]] -name = "rustyred-thg-resolve" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "rustyred-thg-core", - "rustyred-thg-datawave", - "rustyred-thg-offload", - "serde", - "serde_json", -] - -[[package]] -name = "rustyred-thg-schema" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "regex", - "rustyred-thg-core", - "schemars", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-standing-brief" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "rustyred-thg-derivation", - "rustyred-thg-standing-topics", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-standing-topics" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "rustyred-thg-programmable-graph", - "rustyred-thg-watermark", - "rustyred-thg-work-unit", - "serde", - "serde_json", - "sha2", - "smartcore", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "rustyred-thg-vfs" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "bytes", - "commonplace 0.1.0", - "libc", - "notify", - "rustyred-thg-core", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-watermark" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "serde", - "serde_json", - "sha2", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-thg-work-unit" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", - "sha2", - "thiserror 2.0.18", -] - -[[package]] -name = "rustyred-web" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "async-trait", - "blake3", - "futures-util", - "html-to-markdown-rs", - "lol_html", - "pilot-core", - "reqwest 0.12.28", - "rustyred-hipporag", - "rustyred-membrane", - "rustyred-rerank", - "rustyred-thg-core", - "rustyred-thg-datawave", - "rustyred-thg-find", - "rustyred-thg-nlp", - "rustyred-thg-offload", - "rustyred-thg-resolve", - "serde", - "serde_json", - "texting_robots", - "tokio", - "url", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "saturating" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" - -[[package]] -name = "scene-os-core" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.118", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.13.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "selectors" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfaaa6035167f0e604e42723c7650d59ee269ef220d7bbe0565602c8a0173b9" -dependencies = [ - "bitflags 2.13.0", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf 0.13.1", - "phf_codegen 0.13.1", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "seq-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps 2.1.0", - "typenum", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallstr" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b" -dependencies = [ - "smallvec", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -dependencies = [ - "serde", -] - -[[package]] -name = "smartcore" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b38ed6e632271ca6f51685a4d00f49faf6e63c65be667855816114dc944321a" -dependencies = [ - "approx", - "cfg-if", - "num", - "num-traits", - "ordered-float", - "rand 0.10.2", - "serde", - "typetag", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spin" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlparser" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" -dependencies = [ - "log", - "recursive", - "sqlparser_derive", -] - -[[package]] -name = "sqlparser_derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.61.2", -] - -[[package]] -name = "static_assertions_next" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" - -[[package]] -name = "stop-words" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645a3d441ccf4bf47f2e4b7681461986681a6eeea9937d4c3bc9febd61d17c71" -dependencies = [ - "serde_json", -] - -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.13.1", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "symbol_table" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f19bffd69fb182e684d14e3c71d04c0ef33d1641ac0b9e81c712c734e83703bc" -dependencies = [ - "crossbeam-utils", - "foldhash 0.1.5", - "hashbrown 0.15.5", -] - -[[package]] -name = "symbolic_expressions" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c68d531d83ec6c531150584c42a4290911964d5f0d79132b193b67252a23b71" - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags 2.13.0", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.52.0", - "winx", -] - -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "tendril" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" -dependencies = [ - "new_debug_unreachable", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "texting_robots" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82a718a28dda2e67ad6e0464597b58eae39e2e4d0451e03d1028d71e81bb4a" -dependencies = [ - "anyhow", - "bstr", - "lazy_static", - "nom", - "percent-encoding", - "regex", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "theorem-bayes" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "theorem-blocks" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "ed25519-dalek", - "hex", - "hmac", - "jsonschema", - "mime_guess", - "rustyred-plugin", - "rustyred-thg-core", - "rustyred-thg-schema", - "serde", - "serde_json", - "sha2", - "theorem-harness-core", -] - -[[package]] -name = "theorem-capability-registry" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "theorem-env-core" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "axum", - "rustyred-thg-core", - "rustyred-thg-episode-supply-chain", - "rustyred-thg-ml", - "serde", - "serde_json", - "theorem-harness-core", - "tokio", -] - -[[package]] -name = "theorem-evolve" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-core", - "serde", - "serde_json", - "theorem-bayes", - "theorem-env-core", - "theorem-worlds", - "thiserror 1.0.69", -] - -[[package]] -name = "theorem-harness-core" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "base64 0.22.1", - "ed25519-dalek", - "rustyred-thg-ml", - "serde", - "serde_json", - "serde_yaml", - "sha2", - "sqlparser", - "theorem-bayes", - "theorem-capability-registry", - "ulid", - "uuid", -] - -[[package]] -name = "theorem-harness-runtime" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "commonplace 0.1.0", - "design-check", - "ensemble", - "libc", - "prose-check", - "reqwest 0.12.28", - "rustyred-hipporag", - "rustyred-membrane", - "rustyred-thg-affordances", - "rustyred-thg-agentfs", - "rustyred-thg-core", - "rustyred-thg-derivation", - "rustyred-thg-find", - "rustyred-thg-memory", - "rustyred-thg-ml", - "rustyred-thg-nlp", - "rustyred-thg-offload", - "rustyred-thg-vfs", - "rustyred-thg-work-unit", - "serde", - "serde_json", - "theorem-bayes", - "theorem-capability-registry", - "theorem-harness-core", - "theorem-oplog", - "theorem-prompt", - "tokio", - "tracing", -] - -[[package]] -name = "theorem-oplog" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "blake3", - "rustyred-thg-core", - "serde", - "serde_json", - "theorem-blocks", - "ulid", -] - -[[package]] -name = "theorem-prompt" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "rustyred-thg-standing-brief", - "serde", - "serde_json", - "theorem-harness-core", -] - -[[package]] -name = "theorem-worlds" -version = "0.1.0" -source = "git+https://github.com/Travis-Gilbert/Theorem.git?rev=b02a09efc9a03622f57cb515dd7ffd621f7bc426#b02a09efc9a03622f57cb515dd7ffd621f7bc426" -dependencies = [ - "bevy", - "rustyred-thg-core", - "rustyred-thg-ml", - "serde", - "serde_json", - "theorem-bayes", - "theorem-env-core", - "theorem-harness-core", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio 1.2.1", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.3", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.0", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata 0.4.14", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.4", - "sha1", - "thiserror 2.0.18", -] - -[[package]] -name = "twox-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "typetag" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" -dependencies = [ - "erased-serde", - "inventory", - "once_cell", - "serde", - "typetag-impl", -] - -[[package]] -name = "typetag-impl" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.2", -] - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "ulid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" -dependencies = [ - "rand 0.9.4", - "serde", - "web-time", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" -dependencies = [ - "base64 0.22.1", - "flate2", - "log", - "percent-encoding", - "rustls", - "rustls-pki-types", - "ureq-proto", - "utf8-zero", - "webpki-roots", -] - -[[package]] -name = "ureq-proto" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" -dependencies = [ - "base64 0.22.1", - "http", - "httparse", - "log", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-zero" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "variadics_please" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi-common" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46137f5bcc41a0f002ed14688e463665388a6f3a6662a12a8c315d4b8849791c" -dependencies = [ - "async-trait", - "bitflags 2.13.0", - "cap-fs-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "io-extras", - "io-lifetimes", - "log", - "rustix 1.1.4", - "system-interface", - "thiserror 2.0.18", - "tracing", - "wasmtime", - "wasmtime-environ", - "wiggle", - "windows-sys 0.61.2", -] - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-compose" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" -dependencies = [ - "anyhow", - "heck", - "im-rc", - "indexmap", - "log", - "petgraph 0.6.5", - "serde", - "serde_derive", - "serde_yaml", - "smallvec", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", - "wat", -] - -[[package]] -name = "wasm-encoder" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" -dependencies = [ - "leb128fmt", - "wasmparser 0.245.1", -] - -[[package]] -name = "wasm-encoder" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" -dependencies = [ - "leb128fmt", - "wasmparser 0.252.0", -] - -[[package]] -name = "wasmparser" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.16.1", - "indexmap", - "semver", - "serde", -] - -[[package]] -name = "wasmparser" -version = "0.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" -dependencies = [ - "bitflags 2.13.0", - "indexmap", - "semver", -] - -[[package]] -name = "wasmprinter" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.245.1", -] - -[[package]] -name = "wasmtime" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" -dependencies = [ - "addr2line", - "async-trait", - "bitflags 2.13.0", - "bumpalo", - "cc", - "cfg-if", - "encoding_rs", - "futures", - "fxprof-processed-profile", - "gimli", - "ittapi", - "libc", - "log", - "mach2", - "memfd", - "object 0.38.1", - "once_cell", - "postcard", - "pulley-interpreter", - "rayon", - "rustix 1.1.4", - "semver", - "serde", - "serde_derive", - "serde_json", - "smallvec", - "target-lexicon", - "tempfile", - "wasm-compose", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", - "wasmtime-environ", - "wasmtime-internal-cache", - "wasmtime-internal-component-macro", - "wasmtime-internal-component-util", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", - "wasmtime-internal-fiber", - "wasmtime-internal-jit-debug", - "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", - "wasmtime-internal-winch", - "wat", - "windows-sys 0.61.2", -] - -[[package]] -name = "wasmtime-environ" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" -dependencies = [ - "anyhow", - "cpp_demangle", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-entity", - "gimli", - "hashbrown 0.16.1", - "indexmap", - "log", - "object 0.38.1", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "sha2", - "smallvec", - "target-lexicon", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", - "wasmprinter", - "wasmtime-internal-component-util", - "wasmtime-internal-core", -] - -[[package]] -name = "wasmtime-internal-cache" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" -dependencies = [ - "base64 0.22.1", - "directories-next", - "log", - "postcard", - "rustix 1.1.4", - "serde", - "serde_derive", - "sha2", - "toml", - "wasmtime-environ", - "windows-sys 0.61.2", - "zstd 0.13.3", -] - -[[package]] -name = "wasmtime-internal-component-macro" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5ec9fff073ff13b81732d56a9515d761c245750bcda09093827f84130ebc25" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasmtime-internal-component-util", - "wasmtime-internal-wit-bindgen", - "wit-parser", -] - -[[package]] -name = "wasmtime-internal-component-util" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" - -[[package]] -name = "wasmtime-internal-core" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" -dependencies = [ - "anyhow", - "hashbrown 0.16.1", - "libm", - "serde", -] - -[[package]] -name = "wasmtime-internal-cranelift" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" -dependencies = [ - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli", - "itertools", - "log", - "object 0.38.1", - "pulley-interpreter", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.245.1", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", -] - -[[package]] -name = "wasmtime-internal-fiber" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" -dependencies = [ - "cc", - "cfg-if", - "libc", - "rustix 1.1.4", - "wasmtime-environ", - "wasmtime-internal-versioned-export-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "wasmtime-internal-jit-debug" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" -dependencies = [ - "cc", - "object 0.38.1", - "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros", -] - -[[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" -dependencies = [ - "cfg-if", - "libc", - "wasmtime-internal-core", - "windows-sys 0.61.2", -] - -[[package]] -name = "wasmtime-internal-unwinder" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" -dependencies = [ - "cfg-if", - "cranelift-codegen", - "log", - "object 0.38.1", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-internal-versioned-export-macros" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "wasmtime-internal-winch" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f599b79545e3bba0b7913406055ebede5bb0dabee9ba2015ef25a9f4c9f47807" -dependencies = [ - "cranelift-codegen", - "gimli", - "log", - "object 0.38.1", - "target-lexicon", - "wasmparser 0.245.1", - "wasmtime-environ", - "wasmtime-internal-cranelift", - "winch-codegen", -] - -[[package]] -name = "wasmtime-internal-wit-bindgen" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "heck", - "indexmap", - "wit-parser", -] - -[[package]] -name = "wast" -version = "35.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" -dependencies = [ - "leb128", -] - -[[package]] -name = "wast" -version = "252.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" -dependencies = [ - "bumpalo", - "leb128fmt", - "memchr", - "unicode-width", - "wasm-encoder 0.252.0", -] - -[[package]] -name = "wat" -version = "1.252.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" -dependencies = [ - "wast 252.0.0", -] - -[[package]] -name = "web-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-task" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" -dependencies = [ - "async-task", - "cfg-if", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" -dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "wgpu-types" -version = "29.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" -dependencies = [ - "bitflags 2.13.0", - "bytemuck", - "js-sys", - "log", - "raw-window-handle", - "serde", - "web-sys", -] - -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "wiggle" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8cfd3db2f05619c6f36f257d84327c11546e28d61e3a1c1220aaad553bc4b0" -dependencies = [ - "bitflags 2.13.0", - "thiserror 2.0.18", - "tracing", - "wasmtime", - "wasmtime-environ", - "wiggle-macro", - "witx", -] - -[[package]] -name = "wiggle-generate" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd7a197903e5b4ff5e13aef9c891960d71e92073600ecf4c86c7e795ac1c803" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasmtime-environ", - "witx", -] - -[[package]] -name = "wiggle-macro" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6410b86fcec207070d9372b215d3470bad67215e6bbac46981a16999c4abbc28" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "wiggle-generate", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "winch-codegen" -version = "43.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dbb0cf07b0dfe7b7a1ca8efb8f94ba98bd0fb144c411ea1665c78f0449e958" -dependencies = [ - "cranelift-assembler-x64", - "cranelift-codegen", - "gimli", - "regalloc2", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.245.1", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] -name = "winx" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" -dependencies = [ - "bitflags 2.13.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-parser" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" -dependencies = [ - "anyhow", - "hashbrown 0.16.1", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.245.1", -] - -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror 1.0.69", - "wast 35.0.2", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.4", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "synstructure", -] - -[[package]] -name = "yrs" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc6a0094c76c87b1b72ca0579bf7aa00f540957182264e6561cbde707c2ce51" -dependencies = [ - "arc-swap", - "async-lock", - "async-trait", - "dashmap", - "fastrand", - "serde", - "serde_json", - "smallstr", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "aes", - "byteorder", - "bzip2", - "constant_time_eq 0.1.5", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2", - "sha1", - "time", - "zstd 0.11.2+zstd.1.5.2", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe 5.0.2+zstd.1.5.2", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe 7.2.4", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/apps/commonplace-api/Cargo.toml b/apps/commonplace-api/Cargo.toml deleted file mode 100644 index a22e9a40..00000000 --- a/apps/commonplace-api/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "commonplace-api" -version = "0.1.0" -edition = "2021" -description = "CommonPlace interoperability API seam (plan unit F3): a typed consumer GraphQL profile over the CommonPlace object model, gated by per-instance API keys. The universal connection point so any front end or self-hosted instance talks to the database with one API." -license = "MIT" -rust-version = "1.85" - -# Own Cargo root (bare [workspace]) so the pinned Theorem/RustyRed substrate -# graph stays isolated from the CommonPlace root workspace. -[workspace] - -[dependencies] -commonplace = { path = "../../crates/commonplace" } -rustyred-thg-core = { git = "https://github.com/Travis-Gilbert/Theorem.git", rev = "b02a09efc9a03622f57cb515dd7ffd621f7bc426" } -rustyred-thg-find = { git = "https://github.com/Travis-Gilbert/Theorem.git", rev = "b02a09efc9a03622f57cb515dd7ffd621f7bc426" } -# saveUrl fetches through `web_consume_to_graph` in-process, so the page graph -# and the filed item land in one store under one lock (SPEC B7). -rustyred-web = { git = "https://github.com/Travis-Gilbert/Theorem.git", rev = "b02a09efc9a03622f57cb515dd7ffd621f7bc426" } -theorem-harness-core = { git = "https://github.com/Travis-Gilbert/Theorem.git", rev = "b02a09efc9a03622f57cb515dd7ffd621f7bc426" } -theorem-harness-runtime = { git = "https://github.com/Travis-Gilbert/Theorem.git", rev = "b02a09efc9a03622f57cb515dd7ffd621f7bc426" } -async-graphql = "7" -async-graphql-axum = "7" -axum = { version = "0.8", features = ["multipart"] } -base64 = "0.22" -chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "fs"] } -tower-http = { version = "0.6", features = ["cors"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -reqwest = { version = "0.12.22", default-features = false, features = ["blocking", "json", "rustls-tls", "multipart"] } -yrs = "0.27.2" -# Block attestation signing (HANDOFF-PUBLISH FD-P1): per-tenant Ed25519 over the -# block content identity. Secret bytes handled through Zeroizing; hex for wire. -ed25519-dalek = "2" -hex = "0.4" -zeroize = "1" diff --git a/apps/commonplace-api/Dockerfile b/apps/commonplace-api/Dockerfile deleted file mode 100644 index fa4a4671..00000000 --- a/apps/commonplace-api/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -# Dockerfile for commonplace-api from the CommonPlace product repo. -# -# The API source lives here now, but its RustyRed/Theorem crate dependencies are -# still source-of-truth in the Theorem repo. For remote builds this bridge clones -# Theorem next to CommonPlace so the local path dependencies in Cargo.toml -# resolve. Pin THEOREM_REF to a branch or commit that contains the required -# crate API. Long term, replace this with published or git-pinned crate deps. - -FROM rust:1.95-bookworm AS builder -WORKDIR /workspace - -ARG THEOREM_GIT=https://github.com/Travis-Gilbert/Theorem.git -ARG THEOREM_REF=main - -RUN apt-get update \ - && apt-get install -y --no-install-recommends git ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -COPY . /workspace/CommonPlace -RUN git clone --depth 1 --branch "${THEOREM_REF}" "${THEOREM_GIT}" /workspace/Theorem - -WORKDIR /workspace/CommonPlace/apps/commonplace-api -RUN cargo build --release --bin commonplace-api - -FROM debian:bookworm-slim -# Runtime: ca-certificates for outbound TLS (rustls roots). -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates \ - && rm -rf /var/lib/apt/lists/* -COPY --from=builder /workspace/CommonPlace/apps/commonplace-api/target/release/commonplace-api /usr/local/bin/commonplace-api - -# Reads PORT from env, binds [::]:$PORT (IPv6 dual-stack so Railway's IPv6 -# private network + healthcheck reach it; also accepts IPv4). EXPOSE is docs only. -EXPOSE 8080 -CMD ["commonplace-api"] diff --git a/apps/commonplace-api/README.md b/apps/commonplace-api/README.md deleted file mode 100644 index c6fd870e..00000000 --- a/apps/commonplace-api/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# CommonPlace API - -This is the backend block contract/API seam in the CommonPlace product repo. It -exposes the local `crates/commonplace` object model through GraphQL, MCP, -import, export, organize, briefing, discovery, and ask surfaces. - -The crate still bridges to the Theorem checkout for the underlying RustyRed and -harness crates: - -```text -Website/ - CommonPlace/ - Theorem/ -``` - -That keeps this migration honest without duplicating Theorem internals. The -CommonPlace product object model lives locally; the next backend packaging pass -should replace the remaining sibling `path` dependencies with published crates, -git-pinned crates, or vendored CommonPlace-owned adapters. diff --git a/apps/commonplace-api/railway.toml b/apps/commonplace-api/railway.toml deleted file mode 100644 index e8ebbb1e..00000000 --- a/apps/commonplace-api/railway.toml +++ /dev/null @@ -1,28 +0,0 @@ -# Railway deploy config for commonplace-api from the CommonPlace product repo. -# -# Deploy as a CommonPlace service with this repository root as Docker context. -# The Dockerfile clones Theorem as a sibling bridge until Theorem/RustyRed crates -# are published or git-pinned. Set THEOREM_REF to the promoted branch or commit. -# Give the service a public domain: the website's Next proxy calls /graphql -# server-side with x-api-key = COMMONPLACE_API_KEY. -# -# Required env: COMMONPLACE_API_KEY (the instance key the proxy must send). -# Mobile bootstrap env: COMMONPLACE_CHAT_URL, COMMONPLACE_WEB_SEARCH, -# COMMONPLACE_EXPO_PROJECT_ID, COMMONPLACE_PUSH_REGISTRATION_URL, and optional -# COMMONPLACE_MOBILE_CATALOG_JSON. These advertise public capability metadata; -# APNs/FCM/provider credentials never belong in this service response. -# Exposes HTTP, so a real healthcheck is wired: GET /healthz returns "ok". -# -# NOTE: backing is the in-memory store for this slice, so data resets on every -# deploy/restart. Durable RedCore+Disk backing behind the identical schema is the -# named follow-up; until then the website populates it live via Capture/ingest. - -[build] -builder = "DOCKERFILE" -dockerfilePath = "apps/commonplace-api/Dockerfile" - -[deploy] -startCommand = "commonplace-api" -healthcheckPath = "/healthz" -restartPolicyType = "ON_FAILURE" -restartPolicyMaxRetries = 3 diff --git a/apps/commonplace-api/src/auth.rs b/apps/commonplace-api/src/auth.rs deleted file mode 100644 index 944da231..00000000 --- a/apps/commonplace-api/src/auth.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Per-instance API-key auth (plan unit F3). -//! -//! A client connects to an instance with a URL and a key. The key resolves to a -//! [`Principal`] (the identity behind the key); an unknown key is rejected. The -//! presented key arrives as an [`ApiKeyToken`] in the GraphQL request data, -//! injected by the HTTP layer from the `x-api-key` header (or by tests). - -use std::collections::HashMap; - -/// The identity a valid API key resolves to. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Principal { - pub id: String, - pub label: String, -} - -/// The set of valid API keys for an instance. -#[derive(Clone, Debug, Default)] -pub struct ApiKeyRegistry { - keys: HashMap, -} - -impl ApiKeyRegistry { - pub fn new() -> Self { - Self::default() - } - - /// Register a key mapped to a principal id (label defaults to the id). - pub fn with_key(mut self, key: impl Into, principal_id: impl Into) -> Self { - let id = principal_id.into(); - self.keys.insert( - key.into(), - Principal { - id: id.clone(), - label: id, - }, - ); - self - } - - pub fn insert(&mut self, key: impl Into, principal: Principal) { - self.keys.insert(key.into(), principal); - } - - /// Resolve a presented key to its principal, or `None` if unknown. - pub fn resolve(&self, key: &str) -> Option<&Principal> { - self.keys.get(key) - } - - pub fn is_empty(&self) -> bool { - self.keys.is_empty() - } - - pub fn len(&self) -> usize { - self.keys.len() - } -} - -/// The API key presented on a single request. -#[derive(Clone, Debug)] -pub struct ApiKeyToken(pub String); diff --git a/apps/commonplace-api/src/bin/commonplace-mcp.rs b/apps/commonplace-api/src/bin/commonplace-mcp.rs deleted file mode 100644 index d16e4328..00000000 --- a/apps/commonplace-api/src/bin/commonplace-mcp.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! The CommonPlace MCP stdio server (plan unit AG3 + durable backing). -//! -//! Any model reaches the store over MCP: this binary speaks newline-delimited -//! JSON-RPC 2.0 over stdin/stdout (the MCP stdio transport). Spawn it as an MCP -//! server and it exposes read/write item tools over the consumer store. -//! -//! Backing: set `COMMONPLACE_DATA_DIR` to persist durably (a `RedCoreGraphStore` -//! over `/graph` + a `DiskObjectStore` over `/blobs`), so items written -//! by a model survive a restart. Unset = an ephemeral in-memory store (one -//! dataset per process). `handle_request` is generic over the store, so the -//! single `serve` loop runs over either backing. - -use std::io::{BufRead, Write}; -use std::path::PathBuf; - -use commonplace::{BlobStore, Commonplace, EmbeddingGraphStore, InMemoryBlobStore}; -use commonplace_api::mcp; -use rustyred_thg_core::{ - DiskObjectStore, InMemoryGraphStore, RedCoreDurability, RedCoreGraphStore, RedCoreOptions, -}; -use serde_json::{json, Value}; - -fn main() { - match std::env::var("COMMONPLACE_DATA_DIR") { - Ok(dir) if !dir.trim().is_empty() => { - let root = PathBuf::from(dir); - let options = RedCoreOptions { - durability: RedCoreDurability::AofAlways, - ..RedCoreOptions::default() - }; - let store = RedCoreGraphStore::open(root.join("graph"), options) - .expect("open RedCore graph store"); - let blobs = DiskObjectStore::open(root.join("blobs")).expect("open disk blob store"); - serve(Commonplace::new(store, blobs)); - } - _ => serve(Commonplace::new( - InMemoryGraphStore::new(), - InMemoryBlobStore::new(), - )), - } -} - -fn serve(mut cp: Commonplace) -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let stdin = std::io::stdin(); - let stdout = std::io::stdout(); - let mut out = stdout.lock(); - - for line in stdin.lock().lines() { - let Ok(line) = line else { break }; - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - let request: Value = match serde_json::from_str(trimmed) { - Ok(value) => value, - Err(error) => { - let response = json!({ - "jsonrpc": "2.0", - "id": Value::Null, - "error": { "code": -32700, "message": format!("parse error: {error}") } - }); - if writeln!(out, "{response}").is_err() { - break; - } - let _ = out.flush(); - continue; - } - }; - if let Some(response) = mcp::handle_request(&mut cp, &request) { - let json = match serde_json::to_string(&response) { - Ok(json) => json, - Err(error) => { - eprintln!("mcp response serialization error: {error}"); - break; - } - }; - if writeln!(out, "{json}").is_err() { - break; - } - let _ = out.flush(); - } - } -} diff --git a/apps/commonplace-api/src/briefing.rs b/apps/commonplace-api/src/briefing.rs deleted file mode 100644 index 2e8978ba..00000000 --- a/apps/commonplace-api/src/briefing.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Proactive surfacing: the briefing / graph-weather (plan unit I2). -//! -//! Reads the consumer store along three axes, unprompted: what is new (recency), -//! what connects (`SIMILAR_TO` degree), and what is unresolved (open-tagged or -//! unfiled items). The same graph I1 answers questions against, surfaced without -//! being asked. -//! -//! Scope note (surfaced): "newly connected" ranks by connection count (degree), -//! since the object model carries no per-edge timestamp yet; true recency of -//! linking needs an edge `created_at` (named follow-up). "Unfiled" leans on F2's -//! design (ingest always files into a collection), so an item in no collection -//! is a raw capture not yet organized. - -use std::collections::HashSet; - -use commonplace::{BlobStore, Commonplace, Item, SIMILAR_TO_EDGE}; -use rustyred_thg_core::{GraphStore, GraphStoreResult, NeighborQuery}; - -/// An item plus how it connects to the rest of the store. -#[derive(Clone, Debug)] -pub struct ConnectedItem { - pub item: Item, - pub connections: usize, - pub related: Vec, -} - -/// The proactive briefing over a store. -#[derive(Clone, Debug)] -pub struct Briefing { - pub recent: Vec, - pub newly_connected: Vec, - pub open_threads: Vec, -} - -/// Limits for each briefing axis. -#[derive(Clone, Debug)] -pub struct BriefingConfig { - pub recent_limit: usize, - pub connected_limit: usize, - pub open_limit: usize, - /// How many related neighbors to attach per connected item. - pub related_limit: usize, -} - -impl Default for BriefingConfig { - fn default() -> Self { - Self { - recent_limit: 10, - connected_limit: 10, - open_limit: 10, - related_limit: 3, - } - } -} - -/// Compute the briefing over the consumer store. -pub fn briefing(cp: &Commonplace, config: &BriefingConfig) -> GraphStoreResult -where - S: GraphStore, - B: BlobStore, -{ - let items = cp.all_items()?; - - // What is new: most recently updated items. - let mut recent = items.clone(); - recent.sort_by(|a, b| { - b.updated_at_ms - .cmp(&a.updated_at_ms) - .then(b.created_at_ms.cmp(&a.created_at_ms)) - .then(a.id.cmp(&b.id)) - }); - recent.truncate(config.recent_limit); - - // What connects: items by SIMILAR_TO degree, recency as tie-break. - let mut connected: Vec = Vec::new(); - for item in &items { - let neighbor_ids = similar_neighbor_ids(cp, &item.id); - if neighbor_ids.is_empty() { - continue; - } - let mut related = Vec::new(); - for id in neighbor_ids.iter().take(config.related_limit) { - if let Some(neighbor) = cp.get_item(id)? { - related.push(neighbor); - } - } - connected.push(ConnectedItem { - item: item.clone(), - connections: neighbor_ids.len(), - related, - }); - } - connected.sort_by(|a, b| { - b.connections - .cmp(&a.connections) - .then(b.item.updated_at_ms.cmp(&a.item.updated_at_ms)) - .then(a.item.id.cmp(&b.item.id)) - }); - connected.truncate(config.connected_limit); - - // What is unresolved: open-tagged or unfiled (in no collection) items. - let mut open_threads: Vec = items.into_iter().filter(is_open_thread).collect(); - open_threads.sort_by(|a, b| b.updated_at_ms.cmp(&a.updated_at_ms).then(a.id.cmp(&b.id))); - open_threads.truncate(config.open_limit); - - Ok(Briefing { - recent, - newly_connected: connected, - open_threads, - }) -} - -fn similar_neighbor_ids(cp: &Commonplace, item_id: &str) -> Vec -where - S: GraphStore, - B: BlobStore, -{ - let mut seen = HashSet::new(); - let mut ids = Vec::new(); - for direction in [ - NeighborQuery::out(item_id).with_edge_type(SIMILAR_TO_EDGE), - NeighborQuery::in_(item_id).with_edge_type(SIMILAR_TO_EDGE), - ] { - for hit in cp.store().neighbors(direction) { - if hit.node_id != item_id && seen.insert(hit.node_id.clone()) { - ids.push(hit.node_id); - } - } - } - ids -} - -fn is_open_thread(item: &Item) -> bool { - let open_tag = item.tags.iter().any(|tag| { - matches!( - tag.trim().to_lowercase().as_str(), - "open" | "todo" | "to-do" | "unresolved" | "followup" | "follow-up" - ) - }); - open_tag || item.collections.is_empty() -} diff --git a/apps/commonplace-api/src/discover.rs b/apps/commonplace-api/src/discover.rs deleted file mode 100644 index f6bdc47c..00000000 --- a/apps/commonplace-api/src/discover.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Discovery: propose connections the user did not make (plan unit I4). -//! -//! Finds item pairs that are semantically similar but NOT yet joined by a -//! `SIMILAR_TO` edge, ranked by similarity. These are the latent connections the -//! F2 auto-linker missed (e.g. pairs that fell below its link threshold, or items -//! that became related only after later items were added). It is the inverse of -//! the I2 briefing's connectedness axis: surface the edges that should exist but -//! don't. -//! -//! Deterministic: for each embedded item it pulls nearest neighbors from the -//! engine index (via the F2 ingest pipeline's search over the item's own text), -//! drops self and already-linked pairs, keeps the strongest similarity per -//! unordered pair, and ranks. - -use std::collections::{HashMap, HashSet}; - -use commonplace::{ - BlobStore, Commonplace, EmbeddingGraphStore, IngestPipeline, Item, ItemBody, SIMILAR_TO_EDGE, -}; -use rustyred_thg_core::{GraphStoreResult, NeighborQuery}; - -/// A proposed connection between two not-yet-linked items. -#[derive(Clone, Debug)] -pub struct CandidateLink { - pub a: Item, - pub b: Item, - pub similarity: f64, - pub reason: String, -} - -/// Tuning for discovery. -#[derive(Clone, Debug)] -pub struct DiscoverConfig { - /// Minimum cosine similarity for a pair to be proposed. - pub min_similarity: f64, - /// Max candidate links returned. - pub max_results: usize, - /// Nearest neighbors examined per item. - pub per_item_neighbors: usize, -} - -impl Default for DiscoverConfig { - fn default() -> Self { - Self { - min_similarity: 0.5, - max_results: 20, - per_item_neighbors: 10, - } - } -} - -/// Propose ranked candidate links over the consumer store. -pub fn discover( - cp: &Commonplace, - config: &DiscoverConfig, -) -> GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let items = cp.all_items()?; - - // Already-connected unordered pairs, to exclude. - let mut linked: HashSet<(String, String)> = HashSet::new(); - for item in &items { - for direction in [ - NeighborQuery::out(&item.id).with_edge_type(SIMILAR_TO_EDGE), - NeighborQuery::in_(&item.id).with_edge_type(SIMILAR_TO_EDGE), - ] { - for hit in cp.store().neighbors(direction) { - linked.insert(ordered_pair(&item.id, &hit.node_id)); - } - } - } - - // Strongest similarity per candidate (unordered) pair. - let pipeline = IngestPipeline::default(); - let mut candidates: HashMap<(String, String), f64> = HashMap::new(); - for item in &items { - let query = item_query_text(item); - if query.trim().is_empty() { - continue; - } - let hits = pipeline.search(cp, &query, config.per_item_neighbors + 1)?; - for (other_id, distance) in hits { - if other_id == item.id { - continue; - } - let similarity = 1.0 - distance as f64; - if similarity < config.min_similarity { - continue; - } - let pair = ordered_pair(&item.id, &other_id); - if linked.contains(&pair) { - continue; - } - let best = candidates.entry(pair).or_insert(0.0); - if similarity > *best { - *best = similarity; - } - } - } - - let mut ranked: Vec<((String, String), f64)> = candidates.into_iter().collect(); - ranked.sort_by(|a, b| { - b.1.partial_cmp(&a.1) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.0.cmp(&b.0)) - }); - ranked.truncate(config.max_results); - - let mut links = Vec::with_capacity(ranked.len()); - for ((a_id, b_id), similarity) in ranked { - if let (Some(a), Some(b)) = (cp.get_item(&a_id)?, cp.get_item(&b_id)?) { - links.push(CandidateLink { - a, - b, - similarity, - reason: format!("semantically similar ({similarity:.2}) but not yet linked"), - }); - } - } - Ok(links) -} - -fn ordered_pair(a: &str, b: &str) -> (String, String) { - if a <= b { - (a.to_string(), b.to_string()) - } else { - (b.to_string(), a.to_string()) - } -} - -fn item_query_text(item: &Item) -> String { - let mut text = item.title.clone(); - if let ItemBody::Inline { text: body } = &item.body { - text.push(' '); - text.push_str(body); - } - for tag in &item.tags { - text.push(' '); - text.push_str(tag); - } - text -} diff --git a/apps/commonplace-api/src/find.rs b/apps/commonplace-api/src/find.rs deleted file mode 100644 index d58b5181..00000000 --- a/apps/commonplace-api/src/find.rs +++ /dev/null @@ -1,691 +0,0 @@ -//! Universal Find over the consumer store (plan unit B7). -//! -//! The retrieval itself is not implemented here. `rustyred-thg-find` already -//! carries the composed executor (match, expand, admit) with lane attribution, -//! scope widening, graph-relation annotation, and MMR selection under a lambda. -//! This module is the adapter that hands that executor real CommonPlace data. -//! -//! ## Why a projection, and what it costs -//! -//! [`FindContext`] needs three things `Commonplace` does not hold: a -//! [`TrigramIndex`], a [`LexicalLane`] (BM25), and an edge list. It also needs -//! document text to live in a *flat* string property, because the exact lane -//! verifies byte offsets against `properties.body_text` (or one of its -//! fallbacks) and reads nothing else. -//! -//! CommonPlace item nodes do not carry such a property. An item's body is the -//! serde form of [`ItemBody`], so the text sits nested at -//! `properties.body.text` and every flat lookup misses it. The honest fix is -//! not to pretend the lane found something; it is to build the index the lane -//! needs. -//! -//! [`FindIndex::build`] therefore projects the live store once per request: -//! every node is copied verbatim (so relation annotation still reads the real -//! labels, embeddings, and neighbors), item nodes gain a flat -//! [`DEFAULT_TEXT_PROPERTY`] holding title + body + source, and every edge is -//! copied so the structural lane and the classifier see the real graph. The -//! trigram and BM25 indexes are built over the same text in the same pass. -//! -//! The projection is **maintained, not rebuilt**. [`FindIndexCache`] holds it -//! across requests and [`FindIndex::refresh`] brings it up to date through two -//! gates: the store's global version decides whether anything changed at all, -//! and each node's own version decides whether that node needs re-tokenizing. -//! Repeated queries between writes cost one integer compare. A write costs a -//! re-index of what the write touched, not of the corpus. -//! -//! The residual O(corpus) cost after a write is the live node scan, live-edge -//! clone, and version scan: pointer walks and integer compares, not text -//! processing. That floor is `GraphStore`'s, not this module's, because there -//! is no changefeed and no version-filtered node query to ask instead. Closing -//! it means emitting changes from the store. -//! -//! Persisting a flat text property at write time remains a separate improvement -//! and belongs in `crates/commonplace`: it would remove the projection's need to -//! flatten item bodies at all. - -use std::collections::{HashMap, HashSet}; - -use std::sync::{Arc, Mutex}; - -use commonplace::{BlobStore, Commonplace, EmbeddingGraphStore, Item, ITEM_LABEL}; -use rustyred_thg_core::fulltext::FullTextDesignation; -use rustyred_thg_core::index::TrigramIndex; -use rustyred_thg_core::{ - EdgeRecord, GraphSnapshotSource, GraphStoreResult, InMemoryGraphStore, NodeQuery, NodeRecord, - TensorBlockPayloadStore, -}; -use rustyred_thg_find::lanes::node_text; -use rustyred_thg_find::{ - find as run_find, FindContext, FindRequest, FindResponse, LexicalLane, DEFAULT_TEXT_PROPERTY, -}; -use serde_json::json; - -/// Tuning for the projection. The lane dials themselves live on -/// [`FindRequest`], which is the caller's contract. -#[derive(Clone, Debug)] -pub struct FindConfig { - /// Hard cap on live nodes pulled into the projection. With a fixed cap, - /// refresh maintains a stable id-ordered admitted prefix rather than - /// cloning the entire store. - pub node_limit: usize, -} - -impl Default for FindConfig { - fn default() -> Self { - Self { node_limit: 20_000 } - } -} - -/// The retrieval projection: a flat-text copy of the store plus the trigram and -/// BM25 indexes built over it. -/// -/// The projection is maintained, not rebuilt. `indexed` records the graph -/// version each node was last indexed at, which is what makes -/// [`FindIndex::refresh`] able to touch only what moved. -pub struct FindIndex { - store: InMemoryGraphStore, - trigram: TrigramIndex, - edges: Vec, - lexical: LexicalLane, - /// Node id to the `NodeRecord.version` it was last indexed at. - indexed: HashMap, - /// Graph version this projection was last brought up to date with. - /// `None` means nothing has been indexed yet. - graph_version: Option, - /// The admitted live-node prefix this projection was built against. - projection_limit: Option, -} - -/// What one [`FindIndex::refresh`] actually did. Returned so the cost is -/// observable rather than assumed, and asserted in tests. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct RefreshStats { - /// Nodes whose version was compared. Zero when the global version gate - /// short-circuited before scanning nodes and live edges. - pub scanned: usize, - /// Nodes re-projected and re-indexed. - pub reindexed: usize, - /// Nodes dropped from the projection because they left the store. - pub removed: usize, -} - -impl RefreshStats { - /// True when the projection was already current and no work was done. - pub fn was_noop(&self) -> bool { - self.scanned == 0 && self.reindexed == 0 && self.removed == 0 - } -} - -impl Default for FindIndex { - fn default() -> Self { - Self::empty() - } -} - -impl FindIndex { - /// An empty projection. Bring it up to date with [`FindIndex::refresh`]. - pub fn empty() -> Self { - Self::empty_with_store(InMemoryGraphStore::new()) - } - - fn empty_with_store(store: InMemoryGraphStore) -> Self { - Self { - store, - trigram: TrigramIndex::new(), - edges: Vec::new(), - lexical: LexicalLane::new(FullTextDesignation { - label: ITEM_LABEL.to_string(), - property: DEFAULT_TEXT_PROPERTY.to_string(), - }), - indexed: HashMap::new(), - graph_version: None, - projection_limit: None, - } - } - - /// Project the live store into a searchable index, from cold. - pub fn build(cp: &Commonplace, config: &FindConfig) -> GraphStoreResult - where - S: EmbeddingGraphStore + GraphSnapshotSource, - B: BlobStore, - { - let mut index = Self::empty(); - index.refresh(cp, config)?; - Ok(index) - } - - /// Bring the projection up to date with the store, touching only what moved. - /// - /// Two gates, coarse then fine: - /// - /// 1. The store's global version. If it has not moved, nothing in the graph - /// has changed and there is nothing to do, so this returns without even - /// scanning nodes and live edges. Repeated queries between writes cost one integer - /// compare, which is the case a search box spends its life in. - /// 2. Each node's own version. Only nodes whose version differs from the one - /// recorded in `indexed` are re-projected, re-tokenized, and re-indexed. - /// Tokenizing text is the expensive part and it is now proportional to the - /// write, not to the corpus. - /// - /// The residual O(corpus) work after a write is the live node scan, - /// live-edge clone, and version scan, all pointer walks and integer - /// compares. That floor exists because `GraphStore` has no changefeed and no - /// version-filtered query; closing it means emitting changes from the store, - /// not doing anything smarter here. - /// - /// ## What the global gate cannot see - /// - /// `evict_node`, `readmit_node`, `evict_edge`, and `readmit_edge` are - /// **deliberately version-neutral**: they are the cold-tier parking path, and - /// their contract is to move a record between tiers without making it look - /// like an edit. A store that parks a subgraph therefore does not advance - /// `stats().version`, and gate one will short-circuit past it. - /// - /// This is correct for the consumer API, which never parks: every mutation it - /// makes goes through `upsert_node` or `upsert_edge`, and a delete is a - /// tombstoned upsert, all of which bump the version. If the find projection - /// is ever pointed at a store that uses the cold tier, this gate needs a - /// second signal and the caller must not assume otherwise. - pub fn refresh( - &mut self, - cp: &Commonplace, - config: &FindConfig, - ) -> GraphStoreResult - where - S: EmbeddingGraphStore + GraphSnapshotSource, - B: BlobStore, - { - if self.projection_limit != Some(config.node_limit) { - self.reset_projection(cp.store().vector_payload_store()?); - self.projection_limit = Some(config.node_limit); - } - - let graph_version = cp.store().stats().version; - if self.graph_version == Some(graph_version) { - return Ok(RefreshStats::default()); - } - if self.graph_version.is_none() { - // Projected nodes retain their content-addressed vector references. - // Share the source payload backend so semantic lanes can resolve - // those references without copying or expanding dense vectors. - self.store = - InMemoryGraphStore::with_vector_payload_store(cp.store().vector_payload_store()?); - } - - let nodes = cp.store().query_nodes(NodeQuery { - limit: Some(config.node_limit), - ..NodeQuery::default() - }); - let edges = cp.store().live_edge_records(); - let nodes: Vec = nodes.into_iter().take(config.node_limit).collect(); - let mut stats = RefreshStats { - scanned: nodes.len(), - ..RefreshStats::default() - }; - - // Which nodes actually moved. `query_nodes` is a stable, id-ordered live - // scan, so with a fixed `projection_limit` the `indexed` set always - // represents the same admitted prefix semantics across refreshes. - let live: HashSet = nodes - .iter() - .filter(|node| !node.tombstone) - .map(|node| node.id.clone()) - .collect(); - let changed: Vec<&NodeRecord> = nodes - .iter() - .filter(|node| !node.tombstone) - .filter(|node| self.indexed.get(&node.id) != Some(&node.version)) - .collect(); - - // Hydrating items is O(items), so it is paid only when something moved. - let items: HashMap = if changed.is_empty() { - HashMap::new() - } else { - cp.all_items()? - .into_iter() - .map(|item| (item.id.clone(), item)) - .collect() - }; - - for node in changed { - let mut node = node.clone(); - if let Some(text) = projected_text(&node, items.get(&node.id)) { - if let Some(properties) = node.properties.as_object_mut() { - properties.insert(DEFAULT_TEXT_PROPERTY.to_string(), json!(text)); - } - // Both indexes replace on insert, so a re-index is an update. - self.trigram.insert(node.id.clone(), &text); - self.lexical.upsert(&node.id, &text); - } else { - // A node that lost its text must lose its postings too. - self.trigram.remove(node.id.clone()); - self.lexical.remove(&node.id); - } - self.indexed.insert(node.id.clone(), node.version); - self.store.upsert_node(node)?; - stats.reindexed += 1; - } - - // Nodes that left the admitted prefix, or left the store entirely, - // leave the projection. - let departed: Vec = self - .indexed - .keys() - .filter(|id| !live.contains(*id)) - .cloned() - .collect(); - for id in departed { - self.trigram.remove(id.clone()); - self.lexical.remove(&id); - self.indexed.remove(&id); - self.store.evict_node(&id)?; - stats.removed += 1; - } - - // Edges carry no text, so they are taken wholesale: the cost is a record - // clone, not tokenization, and the structural lane and the classifier - // both need the current set. - // - // An edge whose endpoint left the store is dropped rather than carried. - // The projected store enforces referential integrity, so re-upserting a - // dangling edge is refused outright; and a classifier that walked one - // would be citing evidence that no longer exists. - let admitted: HashSet = self.indexed.keys().cloned().collect(); - let edges: Vec = edges - .into_iter() - .filter(|edge| admitted.contains(&edge.from_id) && admitted.contains(&edge.to_id)) - .collect(); - let next_edge_ids: HashSet = edges.iter().map(|edge| edge.id.clone()).collect(); - let departed_edges: Vec = self - .edges - .iter() - .map(|edge| edge.id.clone()) - .filter(|id| !next_edge_ids.contains(id)) - .collect(); - for id in departed_edges { - self.store.evict_edge(&id)?; - } - for edge in &edges { - self.store.upsert_edge(edge.clone())?; - } - self.edges = edges; - self.graph_version = Some(graph_version); - Ok(stats) - } - - fn reset_projection(&mut self, payload_store: Arc) { - *self = - Self::empty_with_store(InMemoryGraphStore::with_vector_payload_store(payload_store)); - } - - /// Everything the composed executor needs to reach the projected data. - pub fn context(&self) -> FindContext<'_, InMemoryGraphStore> { - FindContext::new(&self.store, &self.trigram, &self.edges).with_lexical(&self.lexical) - } - - /// Documents carrying searchable text in this projection. - pub fn document_count(&self) -> usize { - self.lexical.document_count() - } -} - -/// Holds the projection across requests and keeps it current. -/// -/// The projection is shared mutable state behind a `Mutex`, and the borrow that -/// [`FindIndex::context`] hands out cannot outlive the guard. So the cache lends -/// the index to a closure rather than returning it: the refresh and the query -/// happen inside one lock, and a caller cannot accidentally hold a stale -/// reference past the next write. -#[derive(Default)] -pub struct FindIndexCache { - inner: Mutex, - last: Mutex, -} - -impl FindIndexCache { - pub fn new() -> Self { - Self::default() - } - - /// Refresh the projection, then run `f` against it. - pub fn with( - &self, - cp: &Commonplace, - config: &FindConfig, - f: impl FnOnce(&FindIndex) -> R, - ) -> GraphStoreResult - where - S: EmbeddingGraphStore + GraphSnapshotSource, - B: BlobStore, - { - let mut index = match self.inner.lock() { - Ok(guard) => guard, - // A poisoned projection is a cache, not a corruption: carry on and - // let the version gates bring it back into line. - Err(poisoned) => poisoned.into_inner(), - }; - let stats = index.refresh(cp, config)?; - if let Ok(mut last) = self.last.lock() { - *last = stats; - } - Ok(f(&index)) - } - - /// What the most recent refresh did. For tests and diagnostics. - pub fn last_refresh(&self) -> RefreshStats { - self.last.lock().map(|stats| *stats).unwrap_or_default() - } -} - -/// Run a composed find over the consumer store. -pub fn find( - cp: &Commonplace, - request: &FindRequest, - config: &FindConfig, -) -> GraphStoreResult -where - S: EmbeddingGraphStore + GraphSnapshotSource, - B: BlobStore, -{ - let index = FindIndex::build(cp, config)?; - Ok(run_find(&index.context(), request)) -} - -/// The flat text a node contributes to the projection. -/// -/// Items are authoritative when present: their body lives in a nested enum the -/// lanes cannot read, so the projection flattens title + body and appends the -/// source so a saved page is findable by its URL. Nodes that are not items -/// (crawl pages written by `saveUrl`, extracted entities) already carry flat -/// text under one of the names the lanes know, so they are read as-is. -fn projected_text(node: &NodeRecord, item: Option<&Item>) -> Option { - let text = match item { - Some(item) => { - let mut text = item.text_for_embedding(); - if let Some(source) = &item.source { - text.push('\n'); - text.push_str(source); - } - text - } - None => node_text(node)?, - }; - let trimmed = text.trim(); - (!trimmed.is_empty()).then(|| text.clone()) -} - -#[cfg(test)] -mod tests { - use super::*; - use commonplace::{InMemoryBlobStore, IngestInput, IngestPipeline}; - use rustyred_thg_core::GraphRead; - use rustyred_thg_find::{FindScope, Lane}; - - fn seeded() -> Commonplace { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - let pipeline = IngestPipeline::default(); - for (title, body) in [ - ( - "Rust ownership", - "ownership and borrowing govern memory safety", - ), - ( - "Rust borrowing", - "borrowing rules keep memory safety honest", - ), - ( - "Sourdough", - "a levain needs twelve hours at room temperature", - ), - ] { - pipeline - .ingest(&mut cp, IngestInput::note(title, body)) - .expect("ingest"); - } - cp - } - - #[test] - fn a_query_between_writes_reindexes_nothing() { - let cp = seeded(); - let cache = FindIndexCache::new(); - let config = FindConfig::default(); - - cache - .with(&cp, &config, |index| index.document_count()) - .expect("cold"); - let cold = cache.last_refresh(); - assert!( - cold.reindexed >= 3, - "cold build indexed {} nodes, fewer than the three seeded items", - cold.reindexed - ); - - cache - .with(&cp, &config, |index| index.document_count()) - .expect("warm"); - let warm = cache.last_refresh(); - assert!( - warm.was_noop(), - "a query with no intervening write did work: {warm:?}" - ); - assert_eq!( - warm.scanned, 0, - "the global version gate should short-circuit before the graph scan" - ); - } - - #[test] - fn a_write_reindexes_only_what_it_touched() { - let mut cp = seeded(); - let cache = FindIndexCache::new(); - let config = FindConfig::default(); - cache - .with(&cp, &config, |index| index.document_count()) - .expect("cold"); - - IngestPipeline::default() - .ingest( - &mut cp, - IngestInput::note("Trigrams", "trigram indexes narrow the scan"), - ) - .expect("ingest"); - - let count = cache - .with(&cp, &config, |index| index.document_count()) - .expect("refresh"); - let stats = cache.last_refresh(); - assert_eq!(count, 4, "the new item is searchable"); - assert!( - stats.reindexed <= 2, - "one ingest re-indexed {} nodes; it should touch the new item and at \ - most one node the ingest also rewrote", - stats.reindexed - ); - assert!( - stats.scanned >= 4, - "the version scan should still see the whole corpus" - ); - } - - #[test] - fn a_tombstoned_node_leaves_the_indexes() { - let mut cp = seeded(); - let cache = FindIndexCache::new(); - let config = FindConfig::default(); - cache - .with(&cp, &config, |index| index.document_count()) - .expect("cold"); - - // A delete is a tombstoned upsert, which bumps the graph version. - // `evict_node` deliberately does not: it is cold-tier parking, and the - // global gate cannot see it (see `FindIndex::refresh`). - let victim = cp.all_items().expect("items")[0].id.clone(); - let mut record = cp.store().get_node(&victim).expect("victim node").clone(); - record.tombstone = true; - cp.store_mut().upsert_node(record).expect("tombstone"); - - let count = cache - .with(&cp, &config, |index| index.document_count()) - .expect("refresh"); - let stats = cache.last_refresh(); - assert_eq!(stats.removed, 1, "the tombstoned node was not dropped"); - assert_eq!(count, 2, "the tombstoned item is still searchable"); - } - - #[test] - fn changing_the_projection_limit_rebuilds_the_admitted_prefix() { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - for id in ["doc:a", "doc:b", "doc:c"] { - cp.store_mut() - .upsert_node(NodeRecord::new( - id, - ["Doc"], - json!({ DEFAULT_TEXT_PROPERTY: format!("body for {id}") }), - )) - .expect("node"); - } - let cache = FindIndexCache::new(); - let wide = FindConfig { node_limit: 10 }; - let bounded = FindConfig { node_limit: 2 }; - let wide_count = cache - .with(&cp, &wide, |index| index.document_count()) - .expect("wide build"); - assert!( - wide_count == 3, - "manual corpus should index exactly the three admitted docs" - ); - - let bounded_count = cache - .with(&cp, &bounded, |index| index.document_count()) - .expect("bounded rebuild"); - let stats = cache.last_refresh(); - assert_eq!( - bounded_count, 2, - "bounded rebuild should admit only two docs" - ); - assert!( - stats.scanned <= 2 && stats.reindexed <= 2, - "rebuild under the tighter cap should only touch the admitted prefix: {stats:?}" - ); - } - - #[test] - fn bounded_refresh_keeps_only_edges_within_the_admitted_prefix() { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - for id in ["doc:a", "doc:b", "doc:c"] { - let node = NodeRecord::new( - id, - ["Doc"], - json!({ DEFAULT_TEXT_PROPERTY: format!("body for {id}") }), - ); - cp.store_mut().upsert_node(node).expect("node"); - } - cp.store_mut() - .upsert_edge(EdgeRecord::new( - "edge:ab", - "doc:a", - "LINKS_TO", - "doc:b", - json!({}), - )) - .expect("edge ab"); - cp.store_mut() - .upsert_edge(EdgeRecord::new( - "edge:bc", - "doc:b", - "LINKS_TO", - "doc:c", - json!({}), - )) - .expect("edge bc"); - - let mut index = FindIndex::build(&cp, &FindConfig { node_limit: 2 }).expect("build"); - assert_eq!(index.document_count(), 2); - assert_eq!( - index - .edges - .iter() - .map(|edge| edge.id.as_str()) - .collect::>(), - vec!["edge:ab"] - ); - assert_eq!( - index - .store - .live_edge_records() - .iter() - .map(|edge| edge.id.as_str()) - .collect::>(), - vec!["edge:ab"] - ); - - let mut delete = cp.store().get_node("doc:a").expect("doc a").clone(); - delete.tombstone = true; - cp.store_mut().upsert_node(delete).expect("tombstone doc a"); - index - .refresh(&cp, &FindConfig { node_limit: 2 }) - .expect("refresh"); - assert_eq!( - index.document_count(), - 2, - "the bounded prefix should refill after a deletion" - ); - assert_eq!( - index - .edges - .iter() - .map(|edge| edge.id.as_str()) - .collect::>(), - vec!["edge:bc"] - ); - assert_eq!( - index - .store - .live_edge_records() - .iter() - .map(|edge| edge.id.as_str()) - .collect::>(), - vec!["edge:bc"] - ); - } - - #[test] - fn projection_flattens_item_body_into_searchable_text() { - let cp = seeded(); - let index = FindIndex::build(&cp, &FindConfig::default()).expect("build"); - assert_eq!(index.document_count(), 3); - } - - #[test] - fn find_returns_lane_and_scope_attributed_hits() { - let cp = seeded(); - let request = FindRequest::new("borrowing").with_k(10); - let response = find(&cp, &request, &FindConfig::default()).expect("find"); - assert!(!response.results.is_empty(), "seeded corpus returns hits"); - for result in &response.results { - assert!(Lane::ALL.contains(&result.hit.lane)); - assert_eq!(result.hit.scope.as_str(), "corpus"); - } - assert_eq!(response.lanes.len(), 4, "every lane reports a budget"); - } - - #[test] - fn page_scope_narrows_to_one_document() { - let cp = seeded(); - let item = cp - .all_items() - .expect("items") - .into_iter() - .find(|item| item.title == "Rust borrowing") - .expect("seeded item"); - let request = FindRequest::new("borrowing") - .with_scopes(vec![FindScope::Page(item.id.clone())]) - .with_k(10); - let response = find(&cp, &request, &FindConfig::default()).expect("find"); - assert!(response - .results - .iter() - .all(|result| result.hit.doc == item.id)); - } -} diff --git a/apps/commonplace-api/src/growth.rs b/apps/commonplace-api/src/growth.rs deleted file mode 100644 index 8da56d35..00000000 --- a/apps/commonplace-api/src/growth.rs +++ /dev/null @@ -1,371 +0,0 @@ -use std::time::Duration; - -use async_graphql::SimpleObject; -use serde::de::Deserializer; -use serde::Deserialize; - -const GROWTH_SNAPSHOT_SCHEMA_VERSION: i32 = 1; -const GROWTH_SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(5); - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthContextLevelGql { - pub leaf: String, - pub level: i32, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthSkillGql { - pub name: String, - pub unlocked_at: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthBadgeGql { - pub name: String, - pub awarded_at: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthDisplayedStatsGql { - pub form: String, - pub evolution_stage: i32, - pub context_levels: Vec, - pub nature_bucket: String, - pub skills: Vec, - pub calibration_grade: String, - pub lineage_depth: i64, - pub episode_count: i64, - pub badges: Vec, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthCardManifestGql { - pub owner_public_fingerprint: String, - pub genesis_digest: String, - pub epoch_number: i64, - pub epoch_date: String, - pub initial_commit: String, - pub initial_message: String, - pub lineage_head: String, - pub displayed_stats: GrowthDisplayedStatsGql, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthCardBundleGql { - pub manifest: GrowthCardManifestGql, - pub face_svg: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthCardGql { - pub bundle: GrowthCardBundleGql, - pub stats: GrowthDisplayedStatsGql, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthContextXpGql { - pub leaf: String, - pub xp: i64, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthXpGql { - pub total: i64, - #[serde(deserialize_with = "deserialize_context_xp")] - pub by_context: Vec, - pub session_delta: i64, -} - -fn deserialize_context_xp<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let entries = Vec::<(String, i64)>::deserialize(deserializer)?; - Ok(entries - .into_iter() - .map(|(leaf, xp)| GrowthContextXpGql { leaf, xp }) - .collect()) -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthReadinessGql { - pub leaf: String, - pub level_current: i32, - pub ready: bool, - pub ci_width: f64, - pub shrinkage_since_level: f64, - pub evidence_n: i64, - pub failing_predicates: Vec, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthTimelinePointGql { - pub commit_hash: String, - pub parent_commits: Vec, - pub observed_at_ms: i64, - pub xp: i64, - pub context_levels: Vec, - pub stats: GrowthDisplayedStatsGql, - pub form_svg: String, - pub beats: Vec, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthGraphNodeGql { - pub id: String, - pub leaf: String, - pub label: String, - pub posterior_mass: f64, - pub uncertainty_width: f64, - pub level: i32, - pub ready: bool, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthGraphEdgeGql { - pub source: String, - pub target: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthStampEdgeGql { - pub edge_id: String, - pub from_id: String, - pub to_id: String, - pub edge_type: String, - pub class: String, - pub callout: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthStampGql { - pub note_id: String, - pub saved_at_ms: i64, - pub snapshot_digest: String, - pub empty_mark: bool, - pub edges: Vec, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthPublicLineageGql { - pub domain_families: Option>, - pub most_exercised_skills: Option>, - pub dominant_languages: Option>, - pub world_families: Option>, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthListingGql { - pub listing_id: String, - pub card: GrowthCardBundleGql, - pub lineage: GrowthPublicLineageGql, - pub public_timeline: Vec, - pub published_at_ms: i64, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, SimpleObject)] -pub struct GrowthSnapshotGql { - pub schema_version: i32, - pub generated_at_ms: i64, - pub source: String, - pub tenant_slug: String, - pub xp: GrowthXpGql, - pub readiness: Vec, - pub card: GrowthCardGql, - pub timeline: Vec, - pub graph_nodes: Vec, - pub graph_edges: Vec, - pub stamps: Vec, - pub listings: Vec, -} - -impl GrowthSnapshotGql { - fn validate(&self) -> Result<(), String> { - if self.schema_version != GROWTH_SNAPSHOT_SCHEMA_VERSION { - return Err(format!( - "unsupported Growth snapshot schema {}", - self.schema_version - )); - } - if self.source != "live" { - return Err("CommonPlace accepts live Growth snapshots only".to_string()); - } - if self.tenant_slug.trim().is_empty() { - return Err("Growth snapshot tenant is empty".to_string()); - } - if self.card.stats != self.card.bundle.manifest.displayed_stats { - return Err("Growth card stats disagree with the signed manifest".to_string()); - } - for node in &self.graph_nodes { - let Some(readiness) = self - .readiness - .iter() - .find(|report| report.leaf == node.leaf) - else { - return Err(format!( - "Growth graph node {} has no readiness report", - node.id - )); - }; - if readiness.ready != node.ready || readiness.ci_width != node.uncertainty_width { - return Err(format!( - "Growth graph node {} disagrees with readiness", - node.id - )); - } - } - Ok(()) - } -} - -#[derive(Clone, Debug, SimpleObject)] -pub struct GrowthSnapshotResultGql { - pub available: bool, - pub message: Option, - pub snapshot: Option, -} - -impl GrowthSnapshotResultGql { - fn unavailable(message: impl Into) -> Self { - Self { - available: false, - message: Some(message.into()), - snapshot: None, - } - } - - fn ready(snapshot: GrowthSnapshotGql) -> Self { - Self { - available: true, - message: None, - snapshot: Some(snapshot), - } - } -} - -pub async fn load_growth_snapshot_from_env() -> GrowthSnapshotResultGql { - match load_growth_snapshot().await { - Ok(Some(snapshot)) => GrowthSnapshotResultGql::ready(snapshot), - Ok(None) => GrowthSnapshotResultGql::unavailable( - "No live Growth snapshot is configured. Set THEOREM_GROWTH_SNAPSHOT_URL or THEOREM_GROWTH_SNAPSHOT_PATH.", - ), - Err(error) => GrowthSnapshotResultGql::unavailable(error), - } -} - -async fn load_growth_snapshot() -> Result, String> { - if let Some(url) = nonempty_env("THEOREM_GROWTH_SNAPSHOT_URL") { - let client = reqwest::Client::builder() - .timeout(GROWTH_SNAPSHOT_TIMEOUT) - .build() - .map_err(|error| format!("could not build Growth snapshot client: {error}"))?; - let response = client - .get(url) - .header(reqwest::header::ACCEPT, "application/json") - .send() - .await - .map_err(|error| format!("Growth snapshot request failed: {error}"))?; - if !response.status().is_success() { - return Err(format!( - "Growth snapshot upstream returned {}", - response.status() - )); - } - let bytes = response - .bytes() - .await - .map_err(|error| format!("Growth snapshot body failed: {error}"))?; - return parse_growth_snapshot(&bytes).map(Some); - } - - if let Some(path) = nonempty_env("THEOREM_GROWTH_SNAPSHOT_PATH") { - let bytes = tokio::fs::read(path) - .await - .map_err(|error| format!("could not read Growth snapshot: {error}"))?; - return parse_growth_snapshot(&bytes).map(Some); - } - - Ok(None) -} - -fn nonempty_env(name: &str) -> Option { - std::env::var(name) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) -} - -fn parse_growth_snapshot(bytes: &[u8]) -> Result { - let snapshot: GrowthSnapshotGql = serde_json::from_slice(bytes) - .map_err(|error| format!("invalid Growth snapshot: {error}"))?; - snapshot.validate()?; - Ok(snapshot) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::parse_growth_snapshot; - - fn stats() -> serde_json::Value { - json!({ - "form": "Lantern Stag", - "evolution_stage": 2, - "context_levels": [{ "leaf": "rust/refactor", "level": 7 }], - "nature_bucket": "Deliberate", - "skills": [{ "name": "Dependency untangling", "unlocked_at": "2026-07-08" }], - "calibration_grade": "A-", - "lineage_depth": 18, - "episode_count": 142, - "badges": [] - }) - } - - fn snapshot(source: &str) -> Vec { - serde_json::to_vec(&json!({ - "schema_version": 1, - "generated_at_ms": 1, - "source": source, - "tenant_slug": "Travis-Gilbert", - "xp": { "total": 18420, "by_context": [["rust/refactor", 18420]], "session_delta": 340 }, - "readiness": [{ - "leaf": "rust/refactor", "level_current": 7, "ready": true, - "ci_width": 0.1, "shrinkage_since_level": 0.04, "evidence_n": 142, - "failing_predicates": [] - }], - "card": { - "bundle": { - "manifest": { - "owner_public_fingerprint": "key:123", "genesis_digest": "genesis", - "epoch_number": 2, "epoch_date": "2026-07-01", "initial_commit": "initial", - "initial_message": "Born curious.", "lineage_head": "head", - "displayed_stats": stats() - }, - "face_svg": "" - }, - "stats": stats() - }, - "timeline": [], - "graph_nodes": [{ - "id": "rust", "leaf": "rust/refactor", "label": "Rust refactor", - "posterior_mass": 142.0, "uncertainty_width": 0.1, "level": 7, "ready": true - }], - "graph_edges": [], "stamps": [], "listings": [] - })) - .unwrap() - } - - #[test] - fn accepts_a_consistent_live_snapshot() { - let parsed = parse_growth_snapshot(&snapshot("live")).expect("live snapshot parses"); - assert_eq!(parsed.xp.total, 18420); - assert_eq!(parsed.card.stats.form, "Lantern Stag"); - } - - #[test] - fn rejects_fixture_snapshots_on_the_product_edge() { - let error = parse_growth_snapshot(&snapshot("fixture")).unwrap_err(); - assert!(error.contains("live Growth snapshots only")); - } -} diff --git a/apps/commonplace-api/src/keystore.rs b/apps/commonplace-api/src/keystore.rs deleted file mode 100644 index 3e851ee9..00000000 --- a/apps/commonplace-api/src/keystore.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Per-tenant Ed25519 keystore for block attestation signing (HANDOFF-PUBLISH FD-P1). -//! -//! A published block carries a [`BlockAttestation`]: an Ed25519 signature over the -//! block's **content identity** (version hash + shape + origin). Verification is -//! self-contained — the attestation embeds the public key and the exact signed -//! message — and the public host additionally checks that key against the -//! tenant's current verifying key. -//! -//! Key handling: -//! - The secret **never leaves this module** and never crosses the API. Only the -//! verifying (public) key and a signature are ever exposed. -//! - Trust is honest, not overclaimed. With `COMMONPLACE_ED25519_SEED` set the -//! key is derived from that tenant secret (`signing_mode = "tenant"`). Without -//! it a deterministic dev key is used (`signing_mode = "dev"`): a real, -//! verifiable Ed25519 signature that is honestly *not* trust-rooted. The public -//! host surfaces the mode so a dev signature never reads as a trusted one. -//! - Keys are **stable per tenant** (derived, not random), so a signature -//! verifies across process restarts and re-publishes without a persisted vault. -//! Swapping in a real per-tenant vault later only changes `signing_key`. - -use async_graphql::SimpleObject; -use commonplace::content_hash; -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; -use zeroize::Zeroizing; - -/// Insecure, well-known seed used only when no tenant seed is configured. The -/// resulting signature is cryptographically valid but not trust-rooted; the -/// attestation reports `signing_mode = "dev"` so the host never presents it as -/// trusted. -const DEV_SEED: &str = "commonplace-dev-insecure-attestation-seed-v1"; - -/// Domain separator so a block attestation can never be confused with another -/// Ed25519 message signed by the same key. -const MESSAGE_DOMAIN: &str = "commonplace-block-attestation-v1"; - -/// The attestation attached to a published block. Self-verifying: it carries the -/// public key, the signature, and the exact message that was signed. -#[derive(Clone, Debug, SimpleObject, serde::Serialize, serde::Deserialize)] -pub struct BlockAttestation { - /// Always `ed25519`. - pub algorithm: String, - /// `tenant` (trust-rooted) or `dev` (valid but not trust-rooted). - pub signing_mode: String, - /// Hex-encoded Ed25519 verifying (public) key. - pub public_key_hex: String, - /// Hex-encoded detached Ed25519 signature over `message`. - pub signature_hex: String, - /// The exact content-identity message that was signed, so any verifier can - /// reconstruct and check it. - pub message: String, -} - -/// Resolve the active signing mode and seed. `tenant` when a tenant seed is -/// configured; `dev` otherwise. -fn mode_and_seed() -> (&'static str, String) { - match std::env::var("COMMONPLACE_ED25519_SEED") { - // Trim before use: leading/trailing whitespace in the env value would - // otherwise silently derive different tenant keys across deployments. - Ok(seed) if !seed.trim().is_empty() => ("tenant", seed.trim().to_string()), - _ => ("dev", DEV_SEED.to_string()), - } -} - -/// Derive stable 32-byte secret key material from `(seed, tenant)`. SHA-256 is -/// 32 bytes, exactly an Ed25519 secret key. Zeroized on drop. -fn secret_bytes(seed: &str, tenant: &str) -> Zeroizing<[u8; 32]> { - let digest = content_hash(format!("{seed}\u{0}{tenant}").as_bytes()); - let hex = digest.trim_start_matches("sha256:"); - let raw = hex::decode(hex).unwrap_or_default(); - let mut out = Zeroizing::new([0u8; 32]); - let n = raw.len().min(32); - out[..n].copy_from_slice(&raw[..n]); - out -} - -/// The signing key for `tenant`, plus the honest mode label. Secret is dropped -/// (zeroized) once the key is constructed. -fn signing_key(tenant: &str) -> (SigningKey, &'static str) { - let (mode, seed) = mode_and_seed(); - let bytes = secret_bytes(&seed, tenant); - (SigningKey::from_bytes(&bytes), mode) -} - -/// The canonical content-identity message a block attestation signs. Binding the -/// version hash makes the signature payload-specific: any content edit changes -/// the version hash and therefore the message. -pub fn attestation_message(version_hash: &str, shape_id: &str, origin_id: &str) -> String { - format!("{MESSAGE_DOMAIN}\nversion:{version_hash}\nshape:{shape_id}\norigin:{origin_id}") -} - -/// Sign a block's content identity for `tenant`. The secret never leaves here. -pub fn sign_block( - tenant: &str, - version_hash: &str, - shape_id: &str, - origin_id: &str, -) -> BlockAttestation { - let (key, mode) = signing_key(tenant); - let message = attestation_message(version_hash, shape_id, origin_id); - let signature = key.sign(message.as_bytes()); - BlockAttestation { - algorithm: "ed25519".into(), - signing_mode: mode.into(), - public_key_hex: hex::encode(key.verifying_key().as_bytes()), - signature_hex: hex::encode(signature.to_bytes()), - message, - } -} - -/// The tenant's current verifying (public) key, hex-encoded. Safe to expose; the -/// secret is never returned. -pub fn verifying_key_hex(tenant: &str) -> String { - let (key, _) = signing_key(tenant); - hex::encode(key.verifying_key().as_bytes()) -} - -/// Verify an attestation against a block's content identity. Returns true only -/// when the signature is valid over its message **and** that message binds -/// exactly this `(version_hash, shape_id, origin_id)`. Any tamper — to the stored -/// version hash, the shape, or the origin — breaks the match and fails. -pub fn verify_attestation( - att: &BlockAttestation, - version_hash: &str, - shape_id: &str, - origin_id: &str, -) -> bool { - if att.algorithm != "ed25519" { - return false; - } - if att.message != attestation_message(version_hash, shape_id, origin_id) { - return false; - } - let public: Option<[u8; 32]> = hex::decode(&att.public_key_hex) - .ok() - .and_then(|b| b.try_into().ok()); - let Some(public) = public else { - return false; - }; - let Ok(verifying_key) = VerifyingKey::from_bytes(&public) else { - return false; - }; - let signature: Option<[u8; 64]> = hex::decode(&att.signature_hex) - .ok() - .and_then(|b| b.try_into().ok()); - let Some(signature) = signature else { - return false; - }; - let signature = Signature::from_bytes(&signature); - verifying_key - .verify(att.message.as_bytes(), &signature) - .is_ok() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sign_then_verify_roundtrips() { - let att = sign_block("owner", "sha256:abc", "doc", "origin-1"); - assert_eq!(att.algorithm, "ed25519"); - assert!(verify_attestation(&att, "sha256:abc", "doc", "origin-1")); - } - - #[test] - fn wrong_content_identity_fails() { - let att = sign_block("owner", "sha256:abc", "doc", "origin-1"); - // A different version hash (a payload edit) must not verify. - assert!(!verify_attestation(&att, "sha256:xyz", "doc", "origin-1")); - // A different shape or origin must not verify. - assert!(!verify_attestation(&att, "sha256:abc", "note", "origin-1")); - assert!(!verify_attestation(&att, "sha256:abc", "doc", "origin-2")); - } - - #[test] - fn tampered_signature_fails() { - let mut att = sign_block("owner", "sha256:abc", "doc", "origin-1"); - att.signature_hex.replace_range(0..2, "00"); - assert!(!verify_attestation(&att, "sha256:abc", "doc", "origin-1")); - } - - #[test] - fn verifying_key_is_stable_per_tenant() { - // Stable across calls (so it verifies across restarts and re-publishes). - assert_eq!(verifying_key_hex("owner"), verifying_key_hex("owner")); - // Distinct tenants get distinct keys. - assert_ne!(verifying_key_hex("owner"), verifying_key_hex("stranger")); - // The signing key's public half matches the exposed verifying key. - let att = sign_block("owner", "sha256:abc", "doc", "origin-1"); - assert_eq!(att.public_key_hex, verifying_key_hex("owner")); - } - - #[test] - fn dev_mode_is_labelled_honestly() { - // Without COMMONPLACE_ED25519_SEED the mode is the honest "dev". - let att = sign_block("owner", "sha256:abc", "doc", "origin-1"); - assert_eq!(att.signing_mode, "dev"); - } -} diff --git a/apps/commonplace-api/src/lib.rs b/apps/commonplace-api/src/lib.rs deleted file mode 100644 index 3f836b11..00000000 --- a/apps/commonplace-api/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! CommonPlace interoperability API (plan unit F3). -//! -//! The universal connection point: a typed consumer GraphQL profile over the -//! CommonPlace object model, gated by per-instance API keys, so any front end or -//! self-hosted instance talks to the database with one API. Anything that speaks -//! this schema plus a valid key is a client. -//! -//! The schema is store-agnostic in shape; this crate wires it to an in-memory -//! instance store ([`in_memory_store`]) that is fully testable in-process. A -//! durable self-hosted instance swaps the [`schema::ApiStore`] backing to -//! `RedCoreGraphStore` + `DiskObjectStore` behind the identical schema (named -//! follow-up; F1's restart test already proves that backing is durable). - -pub mod auth; -pub mod briefing; -pub mod discover; -pub mod find; -pub mod growth; -pub mod keystore; -pub mod mcp; -pub mod organize; -pub mod portability; -pub mod publish; -pub mod reconstruction; -pub mod retrieve; -pub mod salience; -pub mod save_url; -pub mod schema; -pub mod serve; -pub mod voice; - -pub use auth::{ApiKeyRegistry, ApiKeyToken, Principal}; -pub use briefing::{briefing, Briefing, BriefingConfig, ConnectedItem}; -pub use discover::{discover, CandidateLink, DiscoverConfig}; -pub use growth::{GrowthSnapshotGql, GrowthSnapshotResultGql}; -pub use organize::{ - organize, DailyProgress, OrganizeConfig, OrganizeFiled, OrganizeGroup, OrganizeItem, - OrganizeSnapshot, OrganizedToday, Subtask, Timeframe, -}; -pub use portability::{ - export, export_json, export_markdown, import, ExportDocument, ImportSummary, EXPORT_VERSION, -}; -pub use retrieve::{ - answer_model_from_env, ask, retrieve_grounding_measured, AnswerKind, AnswerModel, AskConfig, - AskResult, GroundingResult, LocalOpenAiAnswerModel, NoModel, PprExpansionMeasurement, - RetrievedItem, -}; -pub use salience::{ - salience, salience_cached, CacheStatus, SalienceAnchor, SalienceCache, SalienceCandidate, - SalienceConfig, SalienceResult, SalienceTier, -}; -pub use schema::{ - build_schema, build_schema_with_model, AnswerKindGql, ApiStore, AskResultGql, BriefingGql, - CandidateLinkGql, CollectionGql, ConnectedItemGql, ConsumerSchema, DurableSchema, - DurableShared, EmbeddingSpaceGql, EmbeddingSpaceRowGql, ExportFormat, ImportResultGql, - InMemoryShared, IngestInputGql, ItemGql, Mutation, PprExpansionMeasurementGql, ProvenanceGql, - Query, SearchHitGql, SharedStore, VectorNeighborGql, -}; -pub use serve::{build_router_with_model, run_from_env, serve_loopback, serve_loopback_with_ready}; - -use std::path::Path; -use std::sync::{Arc, Mutex}; - -use commonplace::{Commonplace, InMemoryBlobStore}; -use rustyred_thg_core::{ - DiskObjectStore, GraphStoreResult, InMemoryGraphStore, RedCoreDurability, RedCoreGraphStore, - RedCoreOptions, -}; - -/// A fresh in-memory CommonPlace instance store (one instance / one dataset). -pub fn in_memory_store() -> InMemoryShared { - Arc::new(Mutex::new(Commonplace::new( - InMemoryGraphStore::new(), - InMemoryBlobStore::new(), - ))) -} - -/// A durable CommonPlace instance store rooted at `dir`: a `RedCoreGraphStore` -/// over `/graph` plus a `DiskObjectStore` over `/blobs`. Items written -/// through it survive a restart (re-opening the same `dir` rehydrates them). -pub fn redcore_store(dir: impl AsRef) -> GraphStoreResult { - let root = dir.as_ref(); - // fsync per commit: a self-hosted instance must survive an abrupt stop, not - // just a graceful shutdown (the default AofEverysec can lose the last second). - let options = RedCoreOptions { - durability: RedCoreDurability::AofAlways, - ..RedCoreOptions::default() - }; - let store = RedCoreGraphStore::open(root.join("graph"), options)?; - let blobs = DiskObjectStore::open(root.join("blobs"))?; - Ok(Arc::new(Mutex::new(Commonplace::new(store, blobs)))) -} diff --git a/apps/commonplace-api/src/main.rs b/apps/commonplace-api/src/main.rs deleted file mode 100644 index 4f973dbc..00000000 --- a/apps/commonplace-api/src/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! The CommonPlace API HTTP server (plan unit F3 + durable backing). - -#[tokio::main] -async fn main() { - if let Err(error) = commonplace_api::run_from_env().await { - eprintln!("{error}"); - std::process::exit(1); - } -} diff --git a/apps/commonplace-api/src/mcp.rs b/apps/commonplace-api/src/mcp.rs deleted file mode 100644 index 907e2fef..00000000 --- a/apps/commonplace-api/src/mcp.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! MCP access over the consumer store (plan unit AG3). -//! -//! Any model reaches the store over MCP, so the workspace is not tied to one -//! frontend model. This is the protocol core: a JSON-RPC 2.0 dispatcher over the -//! [`Commonplace`] store. The MCP stdio transport is newline-delimited JSON-RPC, -//! so the `commonplace-mcp` binary just pipes lines through [`handle_request`]. -//! -//! Tools: `put_note` + `ingest` (write), `get_item` + `list_items` + `search` -//! (read). Tool execution failures come back as `isError: true` content (the -//! model sees them); unknown methods are JSON-RPC errors. - -use commonplace::{ - BlobStore, Commonplace, EmbeddingGraphStore, IngestInput, IngestPipeline, Item, ItemBody, - ItemKind, -}; -use serde_json::{json, Value}; - -const PROTOCOL_VERSION: &str = "2024-11-05"; - -/// Dispatch one JSON-RPC request against the store. Returns the JSON-RPC -/// response, or `None` for notifications (requests without an `id`). -pub fn handle_request(cp: &mut Commonplace, request: &Value) -> Option -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let id = request.get("id").cloned(); - let method = request.get("method").and_then(Value::as_str).unwrap_or(""); - - match method { - "initialize" => Some(ok( - id, - json!({ - "protocolVersion": PROTOCOL_VERSION, - "capabilities": { "tools": {} }, - "serverInfo": { "name": "commonplace-mcp", "version": env!("CARGO_PKG_VERSION") }, - }), - )), - "notifications/initialized" => None, - "ping" => Some(ok(id, json!({}))), - "tools/list" => Some(ok(id, json!({ "tools": tool_specs() }))), - "tools/call" => { - let params = request.get("params").cloned().unwrap_or_else(|| json!({})); - let name = params.get("name").and_then(Value::as_str).unwrap_or(""); - let arguments = params - .get("arguments") - .cloned() - .unwrap_or_else(|| json!({})); - let result = match call_tool(cp, name, &arguments) { - Ok(value) => json!({ - "content": [ { "type": "text", "text": value.to_string() } ], - "isError": false, - }), - Err(message) => json!({ - "content": [ { "type": "text", "text": message } ], - "isError": true, - }), - }; - Some(ok(id, result)) - } - _ => { - if id.is_some() { - Some(err(id, -32601, &format!("method not found: {method}"))) - } else { - None - } - } - } -} - -fn ok(id: Option, result: Value) -> Value { - json!({ "jsonrpc": "2.0", "id": id.unwrap_or(Value::Null), "result": result }) -} - -fn err(id: Option, code: i64, message: &str) -> Value { - json!({ "jsonrpc": "2.0", "id": id.unwrap_or(Value::Null), "error": { "code": code, "message": message } }) -} - -fn tool_specs() -> Value { - json!([ - { - "name": "put_note", - "description": "Create a note item in the store.", - "inputSchema": { "type": "object", "properties": { - "title": { "type": "string" }, "text": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } } - }, "required": ["title", "text"] } - }, - { - "name": "ingest", - "description": "Auto-structure a document into the store: embed, classify into a collection, file, and link to similar items.", - "inputSchema": { "type": "object", "properties": { - "title": { "type": "string" }, "text": { "type": "string" }, "kind": { "type": "string" } - }, "required": ["title", "text"] } - }, - { - "name": "get_item", - "description": "Fetch one item by id.", - "inputSchema": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } - }, - { - "name": "list_items", - "description": "List items, optionally filtered to a kind.", - "inputSchema": { "type": "object", "properties": { "kind": { "type": "string" } } } - }, - { - "name": "search", - "description": "Similarity search over items.", - "inputSchema": { "type": "object", "properties": { - "query": { "type": "string" }, "k": { "type": "integer" } - }, "required": ["query"] } - } - ]) -} - -fn call_tool(cp: &mut Commonplace, name: &str, args: &Value) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - match name { - "put_note" => { - let title = str_arg(args, "title")?; - let text = str_arg(args, "text")?; - let mut item = Item::note(title, text); - if let Some(tags) = args.get("tags").and_then(Value::as_array) { - item = item.with_tags( - tags.iter() - .filter_map(Value::as_str) - .map(str::to_string) - .collect::>(), - ); - } - let stored = cp.put_item(item).map_err(store_err)?; - Ok(lean_item(&stored)) - } - "ingest" => { - let title = str_arg(args, "title")?; - let text = str_arg(args, "text")?; - let kind = args - .get("kind") - .and_then(Value::as_str) - .map(|kind| ItemKind::from(kind.to_string())) - .unwrap_or(ItemKind::Doc); - let receipt = IngestPipeline::default() - .ingest(cp, IngestInput::text(title, text, kind)) - .map_err(store_err)?; - Ok(lean_item(&receipt.item)) - } - "get_item" => { - let id = str_arg(args, "id")?; - match cp.get_item(&id).map_err(store_err)? { - Some(item) => Ok(lean_item(&item)), - None => Ok(Value::Null), - } - } - "list_items" => { - let items = match args.get("kind").and_then(Value::as_str) { - Some(kind) => cp - .items_by_kind(&ItemKind::from(kind.to_string())) - .map_err(store_err)?, - None => cp.all_items().map_err(store_err)?, - }; - Ok(Value::Array(items.iter().map(lean_item).collect())) - } - "search" => { - let query = str_arg(args, "query")?; - let k = args.get("k").and_then(Value::as_u64).unwrap_or(10).max(1) as usize; - let hits = IngestPipeline::default() - .search(cp, &query, k) - .map_err(store_err)?; - let mut results = Vec::with_capacity(hits.len()); - for (id, distance) in hits { - if let Some(item) = cp.get_item(&id).map_err(store_err)? { - let mut value = lean_item(&item); - if let Some(object) = value.as_object_mut() { - object.insert("score".to_string(), json!(1.0 - distance as f64)); - } - results.push(value); - } - } - Ok(Value::Array(results)) - } - other => Err(format!("unknown tool: {other}")), - } -} - -fn str_arg(args: &Value, key: &str) -> Result { - args.get(key) - .and_then(Value::as_str) - .map(str::to_string) - .ok_or_else(|| format!("missing required string argument: {key}")) -} - -fn store_err(error: rustyred_thg_core::GraphStoreError) -> String { - format!("{error:?}") -} - -fn lean_item(item: &Item) -> Value { - let body = match &item.body { - ItemBody::Inline { text } => Some(text.clone()), - ItemBody::Blob { content_hash, .. } => Some(format!("[blob {content_hash}]")), - ItemBody::Empty => None, - }; - json!({ - "id": item.id, - "kind": item.kind.as_str(), - "title": item.title, - "body": body, - "residency": item.residency.as_str(), - "tags": item.tags, - "collections": item.collections, - "classification": item.classification, - "created_at_ms": item.created_at_ms, - "updated_at_ms": item.updated_at_ms, - }) -} diff --git a/apps/commonplace-api/src/organize.rs b/apps/commonplace-api/src/organize.rs deleted file mode 100644 index 01f405f5..00000000 --- a/apps/commonplace-api/src/organize.rs +++ /dev/null @@ -1,549 +0,0 @@ -//! Organize: the daily-driver triage surface over the consumer store. -//! -//! Reuses the engine's classification signal (cosine of an item's stored -//! embedding to each collection's label embedding) to partition items that -//! arrived in a timeframe into two piles: what the engine filed confidently -//! ("organized today") and what still needs a human ("needs you"). The -//! classification is engine-sourced via [`IngestPipeline::classify_item`], so -//! this surface ranks and presents; it never re-derives the embedding model. -//! -//! Scope notes (surfaced): -//! - "now" is wall-clock by default (`SystemTime::now()`), overridable on the -//! config so tests are deterministic. -//! - `arrived_at` / `filed_at` are rendered to ISO-8601 UTC from the item's -//! millisecond timestamps with an in-crate proleptic-Gregorian helper, so no -//! date dependency is added (the crate has neither `chrono` nor `time`). -//! - "time sensitive" leans on the kind heuristic (email -> reply expected); a -//! richer urgency model is a named follow-up. - -use std::collections::BTreeMap; -use std::time::{SystemTime, UNIX_EPOCH}; - -use commonplace::{ - mentions::MENTION_CANDIDATE_KIND, BlobStore, Classification, ClassificationRank, Commonplace, - EmbeddingGraphStore, IngestPipeline, Item, ItemBody, ItemKind, -}; -use rustyred_thg_core::GraphStoreResult; - -const DAY_MS: i64 = 86_400_000; -const WEEK_MS: i64 = 7 * DAY_MS; -const MONTH_MS: i64 = 30 * DAY_MS; - -/// The window an organize call considers. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum Timeframe { - #[default] - Day, - Week, - Month, -} - -impl Timeframe { - fn window_ms(self) -> i64 { - match self { - Timeframe::Day => DAY_MS, - Timeframe::Week => WEEK_MS, - Timeframe::Month => MONTH_MS, - } - } - - /// The token surfaced in `daily_progress.timeframe`. - pub fn as_str(self) -> &'static str { - match self { - Timeframe::Day => "day", - Timeframe::Week => "week", - Timeframe::Month => "month", - } - } -} - -impl From<&str> for Timeframe { - fn from(value: &str) -> Self { - match value.trim().to_ascii_lowercase().as_str() { - "week" => Timeframe::Week, - "month" => Timeframe::Month, - _ => Timeframe::Day, - } - } -} - -/// Knobs for an organize call. -#[derive(Clone, Copy, Debug)] -pub struct OrganizeConfig { - /// Confidence at or above which the engine files an item without a human. - pub needs_you_ceiling: f32, - pub timeframe: Timeframe, - /// Cap on the returned `needs_you` vec (the full count still feeds progress). - pub needs_you_cap: usize, - /// "Now" in epoch millis; overridable for deterministic tests. - pub now_ms: i64, -} - -impl Default for OrganizeConfig { - fn default() -> Self { - Self { - needs_you_ceiling: 0.58, - timeframe: Timeframe::Day, - needs_you_cap: 24, - now_ms: now_ms(), - } - } -} - -/// A checkbox line parsed out of an item's body: the actionable subtasks that -/// make an item a "task". Order is preserved from the document. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Subtask { - pub text: String, - pub done: bool, -} - -/// One arrived item, with the engine's classification verdict attached. -#[derive(Clone, Debug)] -pub struct OrganizeItem { - pub item: Item, - /// Derived presentation kind: "email" | "event" | "task" | "file" | "note". - pub kind: String, - /// First ~200 chars of body text, trimmed. - pub preview: String, - pub source: String, - pub target_collection_id: Option, - pub target_collection_label: Option, - pub confidence: f32, - /// Next-best candidate collections (up to 3), excluding the target. - pub alternatives: Vec<(String, String)>, - pub time_sensitive: bool, - /// "reply" | "open" | None. - pub expected_action: Option, - /// Checkbox subtasks parsed from the body (empty unless the item is a task). - pub subtasks: Vec, - /// The item's tags (note cards surface these). - pub tags: Vec, -} - -/// An item the engine filed, plus when it filed it. -#[derive(Clone, Debug)] -pub struct OrganizeFiled { - pub item: OrganizeItem, - /// ISO-8601 UTC of the item's `updated_at_ms`. - pub filed_at: String, -} - -/// A filed-into collection with its member count for the timeframe. -#[derive(Clone, Debug)] -pub struct OrganizeGroup { - pub collection_id: String, - pub label: String, - pub count: usize, -} - -/// What the engine organized in the timeframe, without a human. -#[derive(Clone, Debug, Default)] -pub struct OrganizedToday { - pub most_recent: Option, - pub groups: Vec, - pub total_count: usize, -} - -/// How much of the timeframe's intake is done vs. total. -#[derive(Clone, Debug)] -pub struct DailyProgress { - /// "day" | "week" | "month". - pub timeframe: String, - pub done: usize, - pub total: usize, -} - -/// The full organize snapshot. -#[derive(Clone, Debug)] -pub struct OrganizeSnapshot { - pub needs_you: Vec, - pub organized_today: OrganizedToday, - pub daily_progress: DailyProgress, - pub needs_you_ceiling: f32, -} - -/// Partition the items that arrived in the timeframe into "needs you" (low -/// confidence or time-sensitive) and "organized today" (filed confidently), -/// reusing the engine's classification signal for both. -pub fn organize( - cp: &Commonplace, - pipeline: &IngestPipeline, - config: &OrganizeConfig, -) -> GraphStoreResult -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let window_ms = config.timeframe.window_ms(); - let cutoff = config.now_ms.saturating_sub(window_ms); - - // Mention candidates are system seam metadata awaiting confirmation, not - // captured material for the person's daily organization queue. - let all: Vec = cp - .all_items()? - .into_iter() - .filter(|item| item.kind.as_str() != MENTION_CANDIDATE_KIND) - .collect(); - - // Member counts over the WHOLE store: a collection is "established" once it - // holds more than one item. Confidence is measured only against established - // collections, so an item that merely seeded its own singleton bucket at - // ingest (the F2 no-button path files everything) does not read as filed -- - // it surfaces in needs-you, which is the genuinely-ambiguous case. - let mut member_counts: BTreeMap = BTreeMap::new(); - for item in &all { - for collection_id in &item.collections { - *member_counts.entry(collection_id.clone()).or_insert(0) += 1; - } - } - - let arrived: Vec = all - .into_iter() - .filter(|item| item.updated_at_ms >= cutoff) - .collect(); - let total = arrived.len(); - - // Classify every arrived item against the live collections. - let mut prepared: Vec = Vec::with_capacity(arrived.len()); - for item in arrived { - let classification = pipeline.classify_item(cp, &item)?; - prepared.push(build_item( - item, - &classification, - config.needs_you_ceiling, - &member_counts, - )); - } - - // Partition: needs-you is low-confidence OR time-sensitive; the rest is filed. - let (mut needs_you, filed): (Vec, Vec) = prepared - .into_iter() - .partition(|item| item.confidence < config.needs_you_ceiling || item.time_sensitive); - - // Needs-you ordering: most-urgent first, then least confident, then recent. - needs_you.sort_by(|a, b| { - b.time_sensitive - .cmp(&a.time_sensitive) - .then( - a.confidence - .partial_cmp(&b.confidence) - .unwrap_or(std::cmp::Ordering::Equal), - ) - .then(b.item.updated_at_ms.cmp(&a.item.updated_at_ms)) - .then(a.item.id.cmp(&b.item.id)) - }); - needs_you.truncate(config.needs_you_cap); - - let organized_today = organized_today(filed); - - let snapshot = OrganizeSnapshot { - needs_you, - daily_progress: DailyProgress { - timeframe: config.timeframe.as_str().to_string(), - done: organized_today.total_count, - total, - }, - organized_today, - needs_you_ceiling: config.needs_you_ceiling, - }; - Ok(snapshot) -} - -fn organized_today(mut filed: Vec) -> OrganizedToday { - let total_count = filed.len(); - - // Most recent first. - filed.sort_by(|a, b| { - b.item - .updated_at_ms - .cmp(&a.item.updated_at_ms) - .then(a.item.id.cmp(&b.item.id)) - }); - - let most_recent = filed.first().map(|item| OrganizeFiled { - filed_at: iso_from_ms(item.item.updated_at_ms), - item: item.clone(), - }); - - // Group by target collection (skip null-target items; they still count in total). - let mut counts: BTreeMap = BTreeMap::new(); - for item in &filed { - if let (Some(id), label) = ( - item.target_collection_id.clone(), - item.target_collection_label.clone().unwrap_or_default(), - ) { - let entry = counts.entry(id).or_insert((label, 0)); - entry.1 += 1; - } - } - let mut groups: Vec = counts - .into_iter() - .map(|(collection_id, (label, count))| OrganizeGroup { - collection_id, - label, - count, - }) - .collect(); - groups.sort_by(|a, b| b.count.cmp(&a.count).then(a.label.cmp(&b.label))); - - OrganizedToday { - most_recent, - groups, - total_count, - } -} - -fn build_item( - item: Item, - classification: &Classification, - ceiling: f32, - member_counts: &BTreeMap, -) -> OrganizeItem { - // Only established collections (more than one member) count as a confident - // home. This naturally keeps a confidently-matched item filed (its own - // collection is established) while dropping a novel item whose only home is - // the singleton it just seeded. - let established: Vec<&ClassificationRank> = classification - .ranked - .iter() - .filter(|rank| member_counts.get(&rank.collection_id).copied().unwrap_or(0) > 1) - .collect(); - let confidence = established.first().map(|rank| rank.score).unwrap_or(0.0); - let (target_collection_id, target_collection_label) = established - .first() - .map(|rank| { - ( - Some(rank.collection_id.clone()), - Some(rank.collection_name.clone()), - ) - }) - .unwrap_or((None, None)); - let alternatives: Vec<(String, String)> = established - .iter() - .skip(1) - .take(3) - .map(|rank| (rank.collection_id.clone(), rank.collection_name.clone())) - .collect(); - - let source = item.source.clone().unwrap_or_default(); - let tags = item.tags.clone(); - let body = match &item.body { - ItemBody::Inline { text } => text.as_str(), - _ => "", - }; - let subtasks = parse_subtasks(body); - let kind = derive_kind(&item, &source, &subtasks); - let time_sensitive = kind == "email"; - let expected_action = if time_sensitive { - Some("reply".to_string()) - } else if confidence >= ceiling { - Some("open".to_string()) - } else { - None - }; - let preview = preview_text(&item); - - OrganizeItem { - item, - kind, - preview, - source, - target_collection_id, - target_collection_label, - confidence, - alternatives, - time_sensitive, - expected_action, - subtasks, - tags, - } -} - -/// Derive the presentation kind. Precedence (first match wins): -/// 1. email: `source` mentions email/gmail/mail. -/// 2. event: a tag in {event, meeting, calendar}. -/// 3. task: the body has subtasks, OR the source is an `mcp:` task connector -/// (linear/asana/jira/todo/task), OR a tag in {task, todo}. -/// 4. file: the item kind is File or Image. -/// 5. note: everything else. -fn derive_kind(item: &Item, source: &str, subtasks: &[Subtask]) -> String { - let source_lower = source.to_ascii_lowercase(); - if source_lower.contains("email") - || source_lower.contains("gmail") - || source_lower.contains("mail") - { - return "email".to_string(); - } - - let has_tag = |candidates: &[&str]| { - item.tags.iter().any(|tag| { - let tag = tag.trim().to_ascii_lowercase(); - candidates.contains(&tag.as_str()) - }) - }; - - if has_tag(&["event", "meeting", "calendar"]) { - return "event".to_string(); - } - - let mcp_task_source = source_lower.starts_with("mcp:") - && ["linear", "asana", "jira", "todo", "task"] - .iter() - .any(|needle| source_lower.contains(needle)); - if !subtasks.is_empty() || mcp_task_source || has_tag(&["task", "todo"]) { - return "task".to_string(); - } - - match item.kind { - ItemKind::File | ItemKind::Image => "file".to_string(), - _ => "note".to_string(), - } -} - -/// Parse markdown checkbox lines out of a body. Accepts a bullet prefix -/// (`- `, `* `, `+ `) followed by `[ ]` (open) or `[x]`/`[X]` (done); the trimmed -/// remainder is the subtask text. Lines with empty text or no checkbox are -/// skipped. Returns the subtasks in document order. -fn parse_subtasks(body: &str) -> Vec { - let mut subtasks = Vec::new(); - for line in body.lines() { - let trimmed = line.trim_start(); - let after_bullet = trimmed - .strip_prefix("- ") - .or_else(|| trimmed.strip_prefix("* ")) - .or_else(|| trimmed.strip_prefix("+ ")); - let Some(after_bullet) = after_bullet else { - continue; - }; - let after_bullet = after_bullet.trim_start(); - let (done, rest) = if let Some(rest) = after_bullet.strip_prefix("[ ]") { - (false, rest) - } else if let Some(rest) = after_bullet - .strip_prefix("[x]") - .or_else(|| after_bullet.strip_prefix("[X]")) - { - (true, rest) - } else { - continue; - }; - let text = rest.trim(); - if text.is_empty() { - continue; - } - subtasks.push(Subtask { - text: text.to_string(), - done, - }); - } - subtasks -} - -fn preview_text(item: &Item) -> String { - let text = match &item.body { - ItemBody::Inline { text } => text.trim(), - _ => item.title.trim(), - }; - text.chars().take(200).collect() -} - -fn now_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis() as i64) - .unwrap_or(0) -} - -/// Render epoch milliseconds to an ISO-8601 UTC timestamp ("YYYY-MM-DDTHH:MM:SSZ") -/// with no date dependency (proleptic Gregorian, days since the Unix epoch). -pub fn iso_from_ms(ms: i64) -> String { - let ms = ms.max(0); - let total_secs = ms / 1000; - let secs_of_day = total_secs.rem_euclid(DAY_MS / 1000); - let days = total_secs.div_euclid(DAY_MS / 1000); - - let hour = secs_of_day / 3600; - let minute = (secs_of_day % 3600) / 60; - let second = secs_of_day % 60; - - let (year, month, day) = civil_from_days(days); - format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z") -} - -/// Days since 1970-01-01 -> (year, month, day), proleptic Gregorian. -/// Howard Hinnant's `civil_from_days` algorithm. -fn civil_from_days(days: i64) -> (i64, u32, u32) { - let z = days + 719_468; - let era = if z >= 0 { z } else { z - 146_096 } / 146_097; - let doe = z - era * 146_097; // [0, 146096] - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399] - let y = yoe + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] - let mp = (5 * doy + 2) / 153; // [0, 11] - let day = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31] - let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12] - let year = if month <= 2 { y + 1 } else { y }; - (year, month, day) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn iso_renders_known_epochs() { - assert_eq!(iso_from_ms(0), "1970-01-01T00:00:00Z"); - // 2021-01-01T00:00:00Z = 1609459200 seconds. - assert_eq!(iso_from_ms(1_609_459_200_000), "2021-01-01T00:00:00Z"); - // 2024-02-29 (leap day) 12:34:56 UTC = 1709210096 seconds. - assert_eq!(iso_from_ms(1_709_210_096_000), "2024-02-29T12:34:56Z"); - } - - #[test] - fn timeframe_parses_from_str() { - assert_eq!(Timeframe::from("week"), Timeframe::Week); - assert_eq!(Timeframe::from("MONTH"), Timeframe::Month); - assert_eq!(Timeframe::from("anything-else"), Timeframe::Day); - } - - #[test] - fn parse_subtasks_reads_checkboxes() { - let body = "- [x] book the room\n\ - - [ ] send the agenda\n\ - * [ ] invite finance\n\ - a plain line that is not a checkbox"; - let subtasks = parse_subtasks(body); - assert_eq!( - subtasks.len(), - 3, - "three checkbox lines parse, the plain line is ignored" - ); - assert_eq!( - subtasks.iter().filter(|s| s.done).count(), - 1, - "exactly one is done" - ); - // Order preserved, first done. - assert_eq!( - subtasks[0], - Subtask { - text: "book the room".to_string(), - done: true, - } - ); - assert_eq!( - subtasks[1], - Subtask { - text: "send the agenda".to_string(), - done: false, - } - ); - assert_eq!( - subtasks[2], - Subtask { - text: "invite finance".to_string(), - done: false, - } - ); - } -} diff --git a/apps/commonplace-api/src/portability.rs b/apps/commonplace-api/src/portability.rs deleted file mode 100644 index 7948410d..00000000 --- a/apps/commonplace-api/src/portability.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Import and export: no lock-in (plan unit X2). -//! -//! Plain JSON and markdown in and out, so data enters and leaves cleanly. JSON -//! is the lossless interchange (the full item + collection records round-trip); -//! markdown is a human-readable one-way rendering. -//! -//! Identity is preserved on import: collections are recreated with their -//! original ids first (so item->collection references resolve), then items are -//! written with their original ids (so `IN_COLLECTION` edges re-link). `created_at` -//! survives; `updated_at` refreshes because an import is itself a write. - -use std::collections::BTreeMap; - -use commonplace::{ - content_hash, BlobStore, Collection, Commonplace, EmbeddingGraphStore, Item, ItemBody, - COLLECTION_LABEL, -}; -use rustyred_thg_core::{GraphStore, GraphStoreError, GraphStoreResult, NodeQuery, NodeRecord}; -use serde::{Deserialize, Serialize}; -use serde_json::json; - -/// Current export schema version. -pub const EXPORT_VERSION: u32 = 1; - -/// A portable snapshot of a CommonPlace instance. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ExportDocument { - pub version: u32, - pub items: Vec, - pub collections: Vec, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub blobs: BTreeMap>, -} - -/// What an import wrote. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ImportSummary { - pub items: usize, - pub collections: usize, -} - -/// Build the full export document for an instance. -pub fn export(cp: &Commonplace) -> GraphStoreResult -where - S: GraphStore, - B: BlobStore, -{ - let items = cp.all_items()?; - let mut blobs = BTreeMap::new(); - for item in &items { - if let ItemBody::Blob { content_hash, .. } = &item.body { - let bytes = cp.read_blob(item)?.ok_or_else(|| { - GraphStoreError::new( - "commonplace_export_missing_blob", - format!("item {} references missing blob {content_hash}", item.id), - ) - })?; - blobs.insert(content_hash.clone(), bytes); - } - } - let collection_ids: Vec = cp - .store() - .query_nodes(NodeQuery::label(COLLECTION_LABEL).with_limit(usize::MAX)) - .into_iter() - .map(|node| node.id) - .collect(); - let mut collections = Vec::with_capacity(collection_ids.len()); - for id in collection_ids { - if let Some(collection) = cp.get_collection(&id)? { - collections.push(collection); - } - } - Ok(ExportDocument { - version: EXPORT_VERSION, - items, - collections, - blobs, - }) -} - -/// Lossless JSON export. -pub fn export_json(cp: &Commonplace) -> GraphStoreResult -where - S: GraphStore, - B: BlobStore, -{ - let document = export(cp)?; - serde_json::to_string_pretty(&document).map_err(serde_err) -} - -/// Human-readable markdown export (one-way rendering). -pub fn export_markdown(cp: &Commonplace) -> GraphStoreResult -where - S: GraphStore, - B: BlobStore, -{ - let document = export(cp)?; - let mut out = String::from("# CommonPlace export\n"); - for collection in &document.collections { - out.push_str(&format!( - "\n## {} ({})\n", - collection.name, - collection.kind.as_str() - )); - for item in document - .items - .iter() - .filter(|item| item.collections.contains(&collection.id)) - { - push_item_markdown(&mut out, item); - } - } - let unfiled: Vec<&Item> = document - .items - .iter() - .filter(|item| item.collections.is_empty()) - .collect(); - if !unfiled.is_empty() { - out.push_str("\n## Unfiled\n"); - for item in unfiled { - push_item_markdown(&mut out, item); - } - } - Ok(out) -} - -fn push_item_markdown(out: &mut String, item: &Item) { - out.push_str(&format!("\n### {} [{}]\n", item.title, item.kind.as_str())); - if !item.tags.is_empty() { - out.push_str(&format!("tags: {}\n", item.tags.join(", "))); - } - match &item.body { - ItemBody::Inline { text } => out.push_str(&format!("\n{text}\n")), - ItemBody::Blob { content_hash, .. } => out.push_str(&format!("\n[blob {content_hash}]\n")), - ItemBody::Empty => {} - } -} - -/// Import an export document, preserving ids so memberships survive. -pub fn import( - cp: &mut Commonplace, - document: &ExportDocument, -) -> GraphStoreResult -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut embedding_dimension = None; - for item in &document.items { - if let Some(embedding) = item.embedding.as_ref() { - if embedding.is_empty() { - return Err(GraphStoreError::new( - "commonplace_import_empty_embedding", - format!("item {} has an empty embedding vector", item.id), - )); - } - match embedding_dimension { - Some(dimension) if dimension != embedding.len() => { - return Err(GraphStoreError::new( - "commonplace_import_embedding_dimension_mismatch", - format!( - "item {} has embedding dimension {}, expected {dimension}", - item.id, - embedding.len() - ), - )); - } - None => embedding_dimension = Some(embedding.len()), - _ => {} - } - } - if let ItemBody::Blob { content_hash, .. } = &item.body { - if !document.blobs.contains_key(content_hash) { - return Err(GraphStoreError::new( - "commonplace_import_missing_blob", - format!("item {} references missing blob {content_hash}", item.id), - )); - } - } - } - - for (expected_hash, bytes) in &document.blobs { - let actual_hash = cp.blobs().put(bytes)?; - if actual_hash != *expected_hash || content_hash(bytes) != *expected_hash { - return Err(GraphStoreError::new( - "commonplace_import_blob_hash_mismatch", - format!("blob bytes do not match exported hash {expected_hash}"), - )); - } - } - - // Re-index item embeddings if present, so imported items stay searchable. - if let Some(dimension) = embedding_dimension { - cp.store_mut() - .designate_commonplace_item_embedding(dimension)?; - } - - // Recreate collections with their original ids first. - for collection in &document.collections { - let record = NodeRecord::new( - collection.id.clone(), - [COLLECTION_LABEL], - json!({ - "name": collection.name, - "kind": collection.kind.as_str(), - "created_at_ms": collection.created_at_ms, - }), - ); - cp.store_mut().upsert_node(record)?; - } - - // Recreate items with their original ids; put_item re-links memberships. - for item in &document.items { - cp.put_item(item.clone())?; - } - - Ok(ImportSummary { - items: document.items.len(), - collections: document.collections.len(), - }) -} - -fn serde_err(error: serde_json::Error) -> rustyred_thg_core::GraphStoreError { - rustyred_thg_core::GraphStoreError::new("commonplace_export_serde", error.to_string()) -} diff --git a/apps/commonplace-api/src/publish.rs b/apps/commonplace-api/src/publish.rs deleted file mode 100644 index 52c4fee8..00000000 --- a/apps/commonplace-api/src/publish.rs +++ /dev/null @@ -1,1337 +0,0 @@ -//! Publish pipeline (HANDOFF-PUBLISH D1/D3/D5, native model). -//! -//! A published artifact is a block hosted at a stable URL. Rather than couple to -//! the inert cross-repo `theorem-blocks` noun layer, publish is modelled on the -//! RustyRed graph that `commonplace-api` already owns: a `published_block` item -//! carries the version hash, shape id, visibility Grant, alias, and view counter, -//! and links to the origin artifact via a `PUBLISHES` edge. -//! -//! - **Content addressing** (immutable version): `content_hash` over the origin's -//! canonical bytes. Re-publish re-points the same alias and pushes the old -//! version onto `history`; the version-addressed hash resolves permanently. -//! - **Publish floor**: Tenon L1 (shape) — the origin's shape must resolve, else -//! publish is refused with the conformance result named. L0 (identity) is a -//! precondition; L3 (grant) is attached, not gated. -//! - **Attestation signing** (HANDOFF-PUBLISH FD-P1, `keystore.rs`): at publish -//! the block's content identity (version hash + shape + origin) is signed with -//! the tenant's Ed25519 key and attached as a `BlockAttestation`. The public -//! host verifies it and surfaces a genuine signature-verified state, degrading -//! to identity + conformance if verification fails. Provenance is now the -//! content-hash identity, the conformance result, **and** the signature. - -use std::collections::BTreeMap; -use std::time::{SystemTime, UNIX_EPOCH}; - -use async_graphql::{Enum, SimpleObject}; -use commonplace::{ - content_hash, BlobStore, Commonplace, EmbeddingGraphStore, Item, ItemBody, ItemKind, -}; - -use crate::keystore::{self, BlockAttestation}; -use rustyred_thg_core::GraphStoreResult; -use serde_json::{json, Value}; - -/// Item kind for the public projection record. -pub const PUBLISHED_BLOCK_KIND: &str = "published_block"; -/// Item kind for a doorway reference into a visitor's space. -pub const REFERENCE_KIND: &str = "block_reference"; -/// Edge from a published block to the origin artifact it projects. -pub const PUBLISHES_EDGE: &str = "PUBLISHES"; -/// Edge from a doorway reference (or fork) to the origin block. -pub const REFERENCES_EDGE: &str = "REFERENCES"; - -/// Shapes that clear the L1 publish floor. Custom shapes register via type-defs -/// later; for now an unknown shape with no renderer is refused. -// ponytail: static allowlist is the L1 floor; widen to type-def lookup when -// custom publishable shapes land. -const PUBLISHABLE_SHAPES: &[&str] = &[ - "doc", - "note", - "link", - "image", - "table", - "chart", - "dashboard", - "scene", - "evidence_board", - "graph_neighborhood", - "mechanism_diagram", - "patent_diagram", - "model_3d", -]; - -/// The three visibilities, expressed over the Grant machinery. public/unlisted -/// differ only in indexability; private is grant-gated. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Enum)] -pub enum Visibility { - Public, - Unlisted, - Private, -} - -impl Visibility { - pub fn as_str(self) -> &'static str { - match self { - Visibility::Public => "public", - Visibility::Unlisted => "unlisted", - Visibility::Private => "private", - } - } - #[allow(clippy::should_implement_trait)] - pub fn from_str(value: &str) -> Self { - match value { - "public" => Visibility::Public, - "private" => Visibility::Private, - _ => Visibility::Unlisted, - } - } - /// public and unlisted differ only here: public is indexable. - pub fn is_indexable(self) -> bool { - matches!(self, Visibility::Public) - } -} - -/// One conformance check line, surfaced as quiet provenance. -#[derive(Clone, Debug, SimpleObject)] -pub struct ConformanceCheck { - pub level: String, - pub name: String, - pub passed: bool, - pub detail: String, -} - -/// The conformance result attached to a publish attempt. -#[derive(Clone, Debug, SimpleObject)] -pub struct ConformanceReport { - /// Highest level reached (the publish floor is L1). - pub level: String, - pub passed: bool, - pub checks: Vec, -} - -/// The receipt returned by `publish`. -#[derive(Clone, Debug, SimpleObject)] -pub struct PublishReceipt { - /// Stable alias URL path, e.g. `/p/`. - pub url: String, - pub alias: String, - /// The published block record id. - pub block_id: String, - /// Content hash of the origin's canonical bytes (immutable version). - pub version_hash: String, - pub shape_id: String, - pub visibility: Visibility, - pub conformance: ConformanceReport, - /// Ed25519 attestation over the block content identity (FD-P1). - pub attestation: Option, -} - -/// A resolved published block for the public host (projection + provenance). -#[derive(Clone, Debug, SimpleObject)] -pub struct PublishedBlock { - pub block_id: String, - pub alias: String, - pub origin_id: String, - pub shape_id: String, - pub title: String, - /// The renderable payload the public host hands to the view registry. - pub payload: async_graphql::Json, - pub version_hash: String, - pub visibility: Visibility, - /// `published` or `gone` (unpublished but preserved). - pub state: String, - pub view_count: i64, - pub conformance: ConformanceReport, - /// Ed25519 attestation over the block content identity (FD-P1). - pub attestation: Option, - /// True when the stored attestation verifies against this block's content - /// identity. The host shows a genuine signature-verified state; false - /// degrades to identity + conformance provenance. - pub signature_verified: bool, -} - -/// Error a publish refusal or resolution failure produces. -#[derive(Clone, Debug)] -pub enum PublishError { - OriginNotFound, - Refused(ConformanceReport), - AliasNotFound, - VersionNotFound, - /// Alias exists but was unpublished — the host renders the designed gone state. - Gone, - Forbidden, -} - -/// Public-host resolution outcome: distinguishes ok / gone / forbidden / -/// not_found so the host renders the right designed state. -#[derive(Clone, Debug, SimpleObject)] -pub struct PublishResolution { - /// One of `ok`, `gone`, `forbidden`, `not_found`. - pub status: String, - pub block: Option, -} - -fn now_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as i64) - .unwrap_or(0) -} - -/// A stable, URL-safe alias derived from the origin id (same origin → same -/// alias across every re-publish). -fn alias_for(origin_id: &str) -> String { - let full = content_hash(origin_id.as_bytes()); - full.trim_start_matches("sha256:") - .chars() - .take(12) - .collect() -} - -/// Canonical bytes for the version hash: kind + title + body + extra, with -/// deterministic key ordering, excluding the origin's own publish bookkeeping. -fn canonical_version_hash(item: &Item) -> String { - let extra: BTreeMap<&String, &Value> = item - .extra - .iter() - .filter(|(k, _)| k.as_str() != "publish") - .collect(); - let body = serde_json::to_value(&item.body).unwrap_or(Value::Null); - let canonical = json!({ - "kind": item.kind.as_str(), - "title": item.title, - "body": body, - "extra": extra, - }); - content_hash( - serde_json::to_vec(&canonical) - .unwrap_or_default() - .as_slice(), - ) -} - -/// Run the conformance ladder to the publish floor (L1). L0 = identity present, -/// L1 = shape resolves. Returns the report; `passed` false means publish is refused. -fn check_conformance(item: &Item) -> ConformanceReport { - let mut checks = Vec::new(); - - // L0 Identity: a non-empty id and some content. - let has_identity = !item.id.trim().is_empty() - && (!item.title.trim().is_empty() || !matches!(item.body, ItemBody::Empty)); - checks.push(ConformanceCheck { - level: "L0".into(), - name: "identity".into(), - passed: has_identity, - detail: if has_identity { - "block has an id and content".into() - } else { - "block is missing an id or is empty".into() - }, - }); - - // L1 Shape: the shape id must resolve in the publishable-shape registry. - let shape = item.kind.as_str(); - let shape_ok = PUBLISHABLE_SHAPES.contains(&shape); - checks.push(ConformanceCheck { - level: "L1".into(), - name: "shape".into(), - passed: shape_ok, - detail: if shape_ok { - format!("shape '{shape}' resolves") - } else { - format!("unknown shape '{shape}' — not publishable") - }, - }); - - let passed = has_identity && shape_ok; - ConformanceReport { - level: "L1".into(), - passed, - checks, - } -} - -fn published_kind() -> ItemKind { - ItemKind::from(PUBLISHED_BLOCK_KIND.to_string()) -} - -/// Find the published-block record for an origin, if one exists. -fn find_by_origin(cp: &Commonplace, origin_id: &str) -> GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - Ok(cp - .items_by_kind(&published_kind())? - .into_iter() - .find(|it| it.extra.get("origin_id").and_then(Value::as_str) == Some(origin_id))) -} - -/// Find the published-block record for an alias. -fn find_by_alias(cp: &Commonplace, alias: &str) -> GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - Ok(cp - .items_by_kind(&published_kind())? - .into_iter() - .find(|it| it.extra.get("alias").and_then(Value::as_str) == Some(alias))) -} - -fn renderable_payload(origin: &Item) -> Value { - let text = match &origin.body { - ItemBody::Inline { text } => Some(text.clone()), - _ => None, - }; - json!({ - "kind": origin.kind.as_str(), - "title": origin.title, - "text": text, - "extra": origin.extra, - }) -} - -fn to_published_block(record: &Item) -> PublishedBlock { - let e = &record.extra; - let get_str = |k: &str| { - e.get(k) - .and_then(Value::as_str) - .unwrap_or_default() - .to_string() - }; - let visibility = Visibility::from_str(&get_str("visibility")); - let conformance = e - .get("conformance") - .and_then(|v| serde_json::from_value::(v.clone()).ok()) - .map(|c| c.into_report()) - .unwrap_or(ConformanceReport { - level: "L1".into(), - passed: true, - checks: Vec::new(), - }); - let version_hash = get_str("version_hash"); - let shape_id = get_str("shape_id"); - let origin_id = get_str("origin_id"); - let attestation = e - .get("attestation") - .and_then(|v| serde_json::from_value::(v.clone()).ok()); - // Verify against the *stored* content identity: a tamper with the stored - // version hash / shape / origin breaks the match even though the signature - // itself is intact. - let signature_verified = attestation - .as_ref() - .map(|a| keystore::verify_attestation(a, &version_hash, &shape_id, &origin_id)) - .unwrap_or(false); - PublishedBlock { - block_id: record.id.clone(), - alias: get_str("alias"), - origin_id, - shape_id, - title: record.title.clone(), - payload: async_graphql::Json(e.get("payload").cloned().unwrap_or(Value::Null)), - version_hash, - visibility, - state: { - let s = get_str("state"); - if s.is_empty() { - "published".into() - } else { - s - } - }, - view_count: e.get("view_count").and_then(Value::as_i64).unwrap_or(0), - conformance, - attestation, - signature_verified, - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct StoredConformance { - level: String, - passed: bool, - checks: Vec, -} -#[derive(serde::Serialize, serde::Deserialize)] -struct StoredCheck { - level: String, - name: String, - passed: bool, - detail: String, -} -impl StoredConformance { - fn from_report(r: &ConformanceReport) -> Self { - Self { - level: r.level.clone(), - passed: r.passed, - checks: r - .checks - .iter() - .map(|c| StoredCheck { - level: c.level.clone(), - name: c.name.clone(), - passed: c.passed, - detail: c.detail.clone(), - }) - .collect(), - } - } - fn into_report(self) -> ConformanceReport { - ConformanceReport { - level: self.level, - passed: self.passed, - checks: self - .checks - .into_iter() - .map(|c| ConformanceCheck { - level: c.level, - name: c.name, - passed: c.passed, - detail: c.detail, - }) - .collect(), - } - } -} - -/// An immutable snapshot of a version that has been superseded at its alias. -/// Re-publishing freezes the version being replaced here so its version URL -/// keeps serving that exact content and its own per-version visibility, rather -/// than whatever the alias now points at (HANDOFF-PUBLISH D1: a version URL that -/// was ever public "is a frozen snapshot" that "resolves forever"). -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct VersionSnapshot { - version_hash: String, - shape_id: String, - origin_id: String, - title: String, - payload: Value, - visibility: String, - conformance: Value, - attestation: Option, - published_at_ms: i64, -} - -/// Freeze a published record's current version into a snapshot, before its extra -/// fields are overwritten by the next re-publish. -fn snapshot_from_record(record: &Item) -> VersionSnapshot { - let e = &record.extra; - let get_str = |k: &str| { - e.get(k) - .and_then(Value::as_str) - .unwrap_or_default() - .to_string() - }; - let visibility = { - let v = get_str("visibility"); - if v.is_empty() { - "unlisted".into() - } else { - v - } - }; - VersionSnapshot { - version_hash: get_str("version_hash"), - shape_id: get_str("shape_id"), - origin_id: get_str("origin_id"), - title: record.title.clone(), - payload: e.get("payload").cloned().unwrap_or(Value::Null), - visibility, - conformance: e.get("conformance").cloned().unwrap_or(Value::Null), - attestation: e.get("attestation").cloned(), - published_at_ms: e - .get("published_at_ms") - .and_then(Value::as_i64) - .unwrap_or(0), - } -} - -/// Build a resolved block from a frozen snapshot. Signature verification runs -/// against the snapshot's own content identity, so an old version keeps its own -/// signed provenance independent of the alias's current version. -fn published_block_from_snapshot(record: &Item, snapshot: &VersionSnapshot) -> PublishedBlock { - let conformance = serde_json::from_value::(snapshot.conformance.clone()) - .map(|c| c.into_report()) - .unwrap_or(ConformanceReport { - level: "L1".into(), - passed: true, - checks: Vec::new(), - }); - let attestation = snapshot - .attestation - .as_ref() - .and_then(|v| serde_json::from_value::(v.clone()).ok()); - let signature_verified = attestation - .as_ref() - .map(|a| { - keystore::verify_attestation( - a, - &snapshot.version_hash, - &snapshot.shape_id, - &snapshot.origin_id, - ) - }) - .unwrap_or(false); - PublishedBlock { - block_id: record.id.clone(), - alias: record - .extra - .get("alias") - .and_then(Value::as_str) - .unwrap_or_default() - .to_string(), - origin_id: snapshot.origin_id.clone(), - shape_id: snapshot.shape_id.clone(), - title: snapshot.title.clone(), - payload: async_graphql::Json(snapshot.payload.clone()), - version_hash: snapshot.version_hash.clone(), - visibility: Visibility::from_str(&snapshot.visibility), - // A frozen snapshot is always the live content of its own hash. - state: "published".into(), - view_count: 0, - conformance, - attestation, - signature_verified, - } -} - -/// Whether `caller` owns this published record. Records carry an `owner` (the -/// principal that first published them); a record without a recorded owner -/// (legacy) is treated as unowned and modifiable, but every fresh publish sets -/// one so this stays closed going forward. -fn owned_by(record: &Item, caller: &str) -> bool { - match record.extra.get("owner").and_then(Value::as_str) { - Some(owner) => owner == caller, - None => true, - } -} - -/// Publish (or re-publish) `origin_id` at `visibility`. Runs the L1 floor; on -/// refusal returns `PublishError::Refused` with the conformance report. -pub fn publish_block( - cp: &mut Commonplace, - origin_id: &str, - visibility: Visibility, - owner_principal: &str, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let origin = cp - .get_item(origin_id) - .map_err(|_| PublishError::OriginNotFound)? - .ok_or(PublishError::OriginNotFound)?; - - let conformance = check_conformance(&origin); - if !conformance.passed { - return Err(PublishError::Refused(conformance)); - } - - let version_hash = canonical_version_hash(&origin); - let shape_id = origin.kind.as_str().to_string(); - let alias = alias_for(origin_id); - let payload = renderable_payload(&origin); - let now = now_ms(); - let stored_conformance = - serde_json::to_value(StoredConformance::from_report(&conformance)).unwrap_or(Value::Null); - - // FD-P1: sign the block content identity with the tenant (owner) key. Secret - // never leaves the keystore; only the attestation is stored and returned. - let attestation = keystore::sign_block(owner_principal, &version_hash, &shape_id, origin_id); - let stored_attestation = serde_json::to_value(&attestation).unwrap_or(Value::Null); - - let mut record = - match find_by_origin(cp, origin_id).map_err(|_| PublishError::OriginNotFound)? { - Some(existing) => { - // Ownership: only the principal that first published this block may - // re-publish it. A different caller who knows the origin id cannot - // overwrite or re-sign someone else's published block. - if !owned_by(&existing, owner_principal) { - return Err(PublishError::Forbidden); - } - existing - } - None => Item::new(published_kind(), origin.title.clone()), - }; - record.title = origin.title.clone(); - - // Re-publish: freeze the version being replaced as an immutable snapshot so - // its version URL keeps serving that exact content and visibility, then - // re-point the alias at the new version. - let prev_hash = record - .extra - .get("version_hash") - .and_then(Value::as_str) - .map(str::to_string); - if let Some(prev) = prev_hash { - if prev != version_hash { - let snapshot = snapshot_from_record(&record); - let mut history = record - .extra - .get("history") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - let already = history - .iter() - .any(|v| v.get("version_hash").and_then(Value::as_str) == Some(prev.as_str())); - if !already { - history.push(serde_json::to_value(&snapshot).unwrap_or(Value::Null)); - } - record.extra.insert("history".into(), json!(history)); - } - } - - record.extra.insert("origin_id".into(), json!(origin_id)); - record.extra.insert("alias".into(), json!(alias)); - record - .extra - .insert("visibility".into(), json!(visibility.as_str())); - record - .extra - .insert("version_hash".into(), json!(version_hash)); - record.extra.insert("shape_id".into(), json!(shape_id)); - record.extra.insert("payload".into(), payload); - record.extra.insert("state".into(), json!("published")); - record - .extra - .insert("conformance".into(), stored_conformance); - record - .extra - .insert("attestation".into(), stored_attestation); - record - .extra - .entry("view_count".to_string()) - .or_insert(json!(0)); - record - .extra - .entry("granted".to_string()) - .or_insert(json!([owner_principal])); - // The first publisher claims ownership; subsequent re-publishes are gated on - // this above and never reassign it. - record - .extra - .entry("owner".to_string()) - .or_insert(json!(owner_principal)); - record - .extra - .entry("created_at_ms".to_string()) - .or_insert(json!(now)); - record.extra.insert("published_at_ms".into(), json!(now)); - - let saved = cp - .put_item(record) - .map_err(|_| PublishError::OriginNotFound)?; - // Idempotent provenance edge published_block -> origin. - let _ = cp.link_explicit(PUBLISHES_EDGE, &saved.id, origin_id, "published projection"); - - Ok(PublishReceipt { - url: format!("/p/{alias}"), - alias, - block_id: saved.id, - version_hash, - shape_id, - visibility, - conformance, - attestation: Some(attestation), - }) -} - -/// Structured publish result for the in-product publish moment: on refusal the -/// conformance report travels so the UI can name the failed check. -#[derive(Clone, Debug, SimpleObject)] -pub struct PublishOutcome { - pub ok: bool, - pub receipt: Option, - pub conformance: ConformanceReport, - pub error: Option, -} - -/// `publish_block` wrapped into a `PublishOutcome` (never errors on refusal). -pub fn publish_block_outcome( - cp: &mut Commonplace, - origin_id: &str, - visibility: Visibility, - owner_principal: &str, -) -> PublishOutcome -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - match publish_block(cp, origin_id, visibility, owner_principal) { - Ok(receipt) => PublishOutcome { - ok: true, - conformance: receipt.conformance.clone(), - receipt: Some(receipt), - error: None, - }, - Err(PublishError::Refused(report)) => { - let named = report - .checks - .iter() - .filter(|c| !c.passed) - .map(|c| format!("{} {}: {}", c.level, c.name, c.detail)) - .collect::>() - .join("; "); - PublishOutcome { - ok: false, - receipt: None, - conformance: report, - error: Some(format!("publish refused: {named}")), - } - } - Err(PublishError::OriginNotFound) => PublishOutcome { - ok: false, - receipt: None, - conformance: ConformanceReport { - level: "L0".into(), - passed: false, - checks: Vec::new(), - }, - error: Some("origin artifact not found".into()), - }, - Err(PublishError::Forbidden) => PublishOutcome { - ok: false, - receipt: None, - conformance: ConformanceReport { - level: "L0".into(), - passed: false, - checks: Vec::new(), - }, - error: Some("not authorized to publish over this block".into()), - }, - Err(_) => PublishOutcome { - ok: false, - receipt: None, - conformance: ConformanceReport { - level: "L0".into(), - passed: false, - checks: Vec::new(), - }, - error: Some("publish failed".into()), - }, - } -} - -/// Unpublish by alias: flip the alias to a `gone` state and revoke the grant; -/// the block record survives in the graph. -pub fn unpublish_block( - cp: &mut Commonplace, - alias: &str, - caller: &str, -) -> Result<(), PublishError> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut record = find_by_alias(cp, alias) - .map_err(|_| PublishError::AliasNotFound)? - .ok_or(PublishError::AliasNotFound)?; - if !owned_by(&record, caller) { - return Err(PublishError::Forbidden); - } - record.extra.insert("state".into(), json!("gone")); - record - .extra - .insert("granted".into(), json!([] as [String; 0])); - cp.put_item(record) - .map_err(|_| PublishError::AliasNotFound)?; - Ok(()) -} - -/// Change visibility of an already-published block; takes effect on next request. -pub fn set_visibility( - cp: &mut Commonplace, - alias: &str, - caller: &str, - visibility: Visibility, -) -> Result<(), PublishError> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut record = find_by_alias(cp, alias) - .map_err(|_| PublishError::AliasNotFound)? - .ok_or(PublishError::AliasNotFound)?; - if !owned_by(&record, caller) { - return Err(PublishError::Forbidden); - } - record - .extra - .insert("visibility".into(), json!(visibility.as_str())); - cp.put_item(record) - .map_err(|_| PublishError::AliasNotFound)?; - Ok(()) -} - -/// Resolve an alias for the public host. `principal` is `Some(id)` for a signed-in -/// visitor. Enforces visibility: public/unlisted resolve anonymously; private -/// requires a granted principal. A `gone` block returns `AliasNotFound` so the -/// host renders the designed gone state (distinct from unknown → 404 upstream). -pub fn resolve_alias( - cp: &mut Commonplace, - alias: &str, - principal: Option<&str>, - count_view: bool, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut record = find_by_alias(cp, alias) - .map_err(|_| PublishError::AliasNotFound)? - .ok_or(PublishError::AliasNotFound)?; - - let state = record - .extra - .get("state") - .and_then(Value::as_str) - .unwrap_or("published"); - if state == "gone" { - return Err(PublishError::Gone); - } - - let visibility = Visibility::from_str( - record - .extra - .get("visibility") - .and_then(Value::as_str) - .unwrap_or("unlisted"), - ); - if visibility == Visibility::Private { - let granted = record - .extra - .get("granted") - .and_then(Value::as_array) - .map(|a| { - a.iter() - .filter_map(Value::as_str) - .map(str::to_string) - .collect::>() - }) - .unwrap_or_default(); - match principal { - Some(p) if granted.iter().any(|g| g == p) => {} - _ => return Err(PublishError::Forbidden), - } - } - - if count_view { - let next = record - .extra - .get("view_count") - .and_then(Value::as_i64) - .unwrap_or(0) - + 1; - record.extra.insert("view_count".into(), json!(next)); - record = cp - .put_item(record) - .map_err(|_| PublishError::AliasNotFound)?; - } - - Ok(to_published_block(&record)) -} - -/// Public-host resolution with a designed status for each outcome. -pub fn resolve_alias_status( - cp: &mut Commonplace, - alias: &str, - viewer: Option<&str>, - count_view: bool, -) -> PublishResolution -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - match resolve_alias(cp, alias, viewer, count_view) { - Ok(block) => PublishResolution { - status: "ok".into(), - block: Some(block), - }, - Err(PublishError::Gone) => PublishResolution { - status: "gone".into(), - block: None, - }, - Err(PublishError::Forbidden) => PublishResolution { - status: "forbidden".into(), - block: None, - }, - _ => PublishResolution { - status: "not_found".into(), - block: None, - }, - } -} - -/// Resolve a permanent version-addressed hash (current or historical). A version -/// URL is the was-ever-public citation surface (HANDOFF-PUBLISH D1): a public or -/// unlisted version resolves forever as a frozen snapshot, even after the alias -/// is unpublished or re-pointed. A private version never resolves here, even to -/// someone holding the hash. -pub fn resolve_version( - cp: &Commonplace, - version_hash: &str, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let records = cp - .items_by_kind(&published_kind()) - .map_err(|_| PublishError::VersionNotFound)?; - - // The version currently live at an alias: serve it unless it is private. - for record in &records { - if record.extra.get("version_hash").and_then(Value::as_str) == Some(version_hash) { - let visibility = Visibility::from_str( - record - .extra - .get("visibility") - .and_then(Value::as_str) - .unwrap_or("unlisted"), - ); - if visibility == Visibility::Private { - return Err(PublishError::VersionNotFound); - } - return Ok(to_published_block(record)); - } - } - - // A frozen historical snapshot, gated on its own per-version visibility so a - // version published while public keeps resolving even after the alias flips - // private, while a version that was private never resolves. - for record in &records { - let Some(history) = record.extra.get("history").and_then(Value::as_array) else { - continue; - }; - for entry in history { - let Ok(snapshot) = serde_json::from_value::(entry.clone()) else { - continue; - }; - if snapshot.version_hash == version_hash { - if Visibility::from_str(&snapshot.visibility) == Visibility::Private { - return Err(PublishError::VersionNotFound); - } - return Ok(published_block_from_snapshot(record, &snapshot)); - } - } - } - - Err(PublishError::VersionNotFound) -} - -/// Aliases of all currently-public, live blocks, for the public sitemap (P3.2). -/// Unlisted and private blocks are excluded (they carry noindex and never enter -/// the sitemap); gone blocks are excluded too. -pub fn public_aliases(cp: &Commonplace) -> Vec -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - cp.items_by_kind(&published_kind()) - .into_iter() - .flatten() - .filter(|it| { - let live = it - .extra - .get("state") - .and_then(Value::as_str) - .unwrap_or("published") - != "gone"; - let public = Visibility::from_str( - it.extra - .get("visibility") - .and_then(Value::as_str) - .unwrap_or("unlisted"), - ) == Visibility::Public; - live && public - }) - .filter_map(|it| { - it.extra - .get("alias") - .and_then(Value::as_str) - .map(str::to_string) - }) - .collect() -} - -/// Doorway: reference a published block into a visitor's space (not a copy). -/// Returns the new reference item id. `fork` makes an owned divergent copy where -/// the grant permits it. -pub fn reference_block( - cp: &mut Commonplace, - alias: &str, - visitor_principal: &str, - fork: bool, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - // Authorize through the same resolution the public host uses: a gone block, - // or a private block the visitor has no grant for, is refused here too, so a - // caller who merely knows the alias cannot reference or fork restricted - // content. - let block = resolve_alias(cp, alias, Some(visitor_principal), false)?; - let origin_id = block.origin_id.clone(); - let title = block.title.clone(); - - let mut item = if fork { - // Fork: an owned, divergent copy of the origin's renderable content. - let text = block - .payload - .0 - .get("text") - .and_then(Value::as_str) - .unwrap_or_default(); - Item::new( - ItemKind::from(REFERENCE_KIND.to_string()), - format!("Fork of {title}"), - ) - .with_text(text) - } else { - Item::new( - ItemKind::from(REFERENCE_KIND.to_string()), - format!("Ref: {title}"), - ) - }; - item.extra.insert( - "provenance".into(), - json!({ - "origin_block_id": block.block_id, - "origin_alias": alias, - "origin_id": origin_id, - "kind": if fork { "fork" } else { "reference" }, - "by": visitor_principal, - }), - ); - let saved = cp.put_item(item).map_err(|_| PublishError::AliasNotFound)?; - let _ = cp.link_explicit( - REFERENCES_EDGE, - &saved.id, - &block.block_id, - if fork { "fork" } else { "reference" }, - ); - Ok(saved.id) -} - -#[cfg(test)] -mod tests { - use super::*; - use commonplace::{Commonplace, InMemoryBlobStore}; - use rustyred_thg_core::InMemoryGraphStore; - - fn store() -> Commonplace { - Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()) - } - - fn seed_doc(cp: &mut Commonplace) -> String { - let item = Item::new(ItemKind::Doc, "My Document").with_text("hello world"); - cp.put_item(item).unwrap().id - } - - #[test] - fn publish_returns_url_and_version_hash() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Unlisted, "owner").unwrap(); - assert!(receipt.url.starts_with("/p/")); - assert!(receipt.version_hash.starts_with("sha256:")); - assert!(receipt.conformance.passed); - // Resolves back through the public host path. - let block = resolve_alias(&mut cp, &receipt.alias, None, false).unwrap(); - assert_eq!(block.origin_id, id); - assert_eq!(block.shape_id, "doc"); - } - - #[test] - fn shape_invalid_block_is_refused_with_conformance() { - let mut cp = store(); - let bogus = Item::new(ItemKind::from("nonsense".to_string()), "x").with_text("y"); - let id = cp.put_item(bogus).unwrap().id; - let err = publish_block(&mut cp, &id, Visibility::Unlisted, "owner").unwrap_err(); - match err { - PublishError::Refused(report) => { - assert!(!report.passed); - assert!(report.checks.iter().any(|c| c.name == "shape" && !c.passed)); - } - _ => panic!("expected refusal"), - } - } - - #[test] - fn unpublish_makes_alias_gone_but_block_survives() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - unpublish_block(&mut cp, &receipt.alias, "owner").unwrap(); - // Alias resolution now yields the gone signal. - assert!(matches!( - resolve_alias(&mut cp, &receipt.alias, None, false), - Err(PublishError::Gone) - )); - assert_eq!( - resolve_alias_status(&mut cp, &receipt.alias, None, false).status, - "gone" - ); - // But the version-addressed hash still resolves (block preserved). - let by_version = resolve_version(&cp, &receipt.version_hash).unwrap(); - assert_eq!(by_version.origin_id, id); - } - - #[test] - fn republish_serves_new_version_at_alias_old_at_hash() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let first = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - // Edit the origin, re-publish. - let mut origin = cp.get_item(&id).unwrap().unwrap(); - origin.body = ItemBody::Inline { - text: "hello edited".into(), - }; - cp.put_item(origin).unwrap(); - let second = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - assert_ne!(first.version_hash, second.version_hash); - assert_eq!(first.alias, second.alias); // stable alias - // Alias serves the new version and its new content. - let at_alias = resolve_alias(&mut cp, &second.alias, None, false).unwrap(); - assert_eq!(at_alias.version_hash, second.version_hash); - assert_eq!( - at_alias.payload.0.get("text").and_then(Value::as_str), - Some("hello edited") - ); - // The old hash serves the frozen original content, not the current alias - // content (D1: a version URL is a frozen snapshot). - let old = resolve_version(&cp, &first.version_hash).unwrap(); - assert_eq!(old.origin_id, id); - assert_eq!(old.version_hash, first.version_hash); - assert_eq!( - old.payload.0.get("text").and_then(Value::as_str), - Some("hello world") - ); - } - - #[test] - fn private_requires_granted_principal() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Private, "owner").unwrap(); - // Anonymous is forbidden. - assert!(matches!( - resolve_alias(&mut cp, &receipt.alias, None, false), - Err(PublishError::Forbidden) - )); - // A non-granted principal is forbidden. - assert!(matches!( - resolve_alias(&mut cp, &receipt.alias, Some("stranger"), false), - Err(PublishError::Forbidden) - )); - // The owner resolves. - assert!(resolve_alias(&mut cp, &receipt.alias, Some("owner"), false).is_ok()); - } - - #[test] - fn flipping_public_to_private_takes_effect() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - assert!(resolve_alias(&mut cp, &receipt.alias, None, false).is_ok()); - set_visibility(&mut cp, &receipt.alias, "owner", Visibility::Private).unwrap(); - assert!(matches!( - resolve_alias(&mut cp, &receipt.alias, None, false), - Err(PublishError::Forbidden) - )); - } - - #[test] - fn view_counter_increments() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - resolve_alias(&mut cp, &receipt.alias, None, true).unwrap(); - let block = resolve_alias(&mut cp, &receipt.alias, None, true).unwrap(); - assert!(block.view_count >= 1); - } - - #[test] - fn reference_and_fork_carry_provenance() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - let ref_id = reference_block(&mut cp, &receipt.alias, "visitor", false).unwrap(); - let reference = cp.get_item(&ref_id).unwrap().unwrap(); - let prov = reference.extra.get("provenance").unwrap(); - assert_eq!( - prov.get("origin_alias").and_then(Value::as_str), - Some(receipt.alias.as_str()) - ); - assert_eq!(prov.get("kind").and_then(Value::as_str), Some("reference")); - - let fork_id = reference_block(&mut cp, &receipt.alias, "visitor", true).unwrap(); - let fork = cp.get_item(&fork_id).unwrap().unwrap(); - assert_eq!( - fork.extra - .get("provenance") - .and_then(|p| p.get("kind")) - .and_then(Value::as_str), - Some("fork") - ); - } - - #[test] - fn public_aliases_lists_only_public_live_blocks() { - let mut cp = store(); - let pub_id = seed_doc(&mut cp); - let public = publish_block(&mut cp, &pub_id, Visibility::Public, "owner").unwrap(); - - // An unlisted block: excluded from the sitemap. - let unlisted_item = Item::new(ItemKind::Doc, "Unlisted").with_text("u"); - let unlisted_id = cp.put_item(unlisted_item).unwrap().id; - publish_block(&mut cp, &unlisted_id, Visibility::Unlisted, "owner").unwrap(); - - // A public-then-unpublished block: gone, excluded. - let gone_item = Item::new(ItemKind::Doc, "Gone").with_text("g"); - let gone_id = cp.put_item(gone_item).unwrap().id; - let gone = publish_block(&mut cp, &gone_id, Visibility::Public, "owner").unwrap(); - unpublish_block(&mut cp, &gone.alias, "owner").unwrap(); - - let aliases = public_aliases(&cp); - assert!(aliases.contains(&public.alias)); - assert!(!aliases.iter().any(|a| a == &gone.alias)); - assert_eq!(aliases.len(), 1); - } - - #[test] - fn published_block_carries_verifiable_attestation() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - // The receipt carries the attestation. - let att = receipt - .attestation - .as_ref() - .expect("attestation on receipt"); - assert_eq!(att.algorithm, "ed25519"); - // It verifies against the tenant's current verifying key. - assert_eq!(att.public_key_hex, keystore::verifying_key_hex("owner")); - // Resolving through the public host shows a genuine signature-verified state. - let block = resolve_alias(&mut cp, &receipt.alias, None, false).unwrap(); - assert!(block.attestation.is_some()); - assert!(block.signature_verified); - } - - #[test] - fn tampering_the_stored_block_breaks_verification() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - // Tamper with the stored version hash (the signed content identity), - // leaving the intact signature attached. - let mut record = find_by_alias(&cp, &receipt.alias).unwrap().unwrap(); - record - .extra - .insert("version_hash".into(), json!("sha256:tampered")); - cp.put_item(record).unwrap(); - // The signature no longer matches the stored identity: degrades honestly. - let block = resolve_alias(&mut cp, &receipt.alias, None, false).unwrap(); - assert!(block.attestation.is_some()); - assert!(!block.signature_verified); - } - - #[test] - fn private_version_hash_does_not_resolve_publicly() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Private, "owner").unwrap(); - // A private block's version hash is not a public citation surface: - // holding the hash does not resolve the content. - assert!(matches!( - resolve_version(&cp, &receipt.version_hash), - Err(PublishError::VersionNotFound) - )); - } - - #[test] - fn version_public_when_published_survives_a_later_private_flip() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let first = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - // Edit + re-publish to push the first (public) version into history. - let mut origin = cp.get_item(&id).unwrap().unwrap(); - origin.body = ItemBody::Inline { text: "v2".into() }; - cp.put_item(origin).unwrap(); - let second = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - // Flip the alias to private. - set_visibility(&mut cp, &second.alias, "owner", Visibility::Private).unwrap(); - // The historical version that was public still resolves as a frozen - // snapshot; the now-private current version does not. - assert!(resolve_version(&cp, &first.version_hash).is_ok()); - assert!(matches!( - resolve_version(&cp, &second.version_hash), - Err(PublishError::VersionNotFound) - )); - } - - #[test] - fn republish_by_a_non_owner_is_forbidden() { - let mut cp = store(); - let id = seed_doc(&mut cp); - publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - // A different principal who knows the origin id cannot overwrite it. - assert!(matches!( - publish_block(&mut cp, &id, Visibility::Public, "stranger"), - Err(PublishError::Forbidden) - )); - // The outcome wrapper reports the refusal without erroring. - let outcome = publish_block_outcome(&mut cp, &id, Visibility::Public, "stranger"); - assert!(!outcome.ok); - } - - #[test] - fn unpublish_and_set_visibility_require_ownership() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - assert!(matches!( - unpublish_block(&mut cp, &receipt.alias, "stranger"), - Err(PublishError::Forbidden) - )); - assert!(matches!( - set_visibility(&mut cp, &receipt.alias, "stranger", Visibility::Private), - Err(PublishError::Forbidden) - )); - // The stranger changed nothing; the owner still controls the block. - assert!(resolve_alias(&mut cp, &receipt.alias, None, false).is_ok()); - assert!(set_visibility(&mut cp, &receipt.alias, "owner", Visibility::Unlisted).is_ok()); - } - - #[test] - fn reference_of_private_block_without_grant_is_forbidden() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Private, "owner").unwrap(); - // A stranger who knows the alias cannot reference or fork a private block. - assert!(matches!( - reference_block(&mut cp, &receipt.alias, "stranger", false), - Err(PublishError::Forbidden) - )); - assert!(matches!( - reference_block(&mut cp, &receipt.alias, "stranger", true), - Err(PublishError::Forbidden) - )); - // The granted owner can reference it. - assert!(reference_block(&mut cp, &receipt.alias, "owner", false).is_ok()); - } - - #[test] - fn reference_of_gone_block_is_forbidden() { - let mut cp = store(); - let id = seed_doc(&mut cp); - let receipt = publish_block(&mut cp, &id, Visibility::Public, "owner").unwrap(); - unpublish_block(&mut cp, &receipt.alias, "owner").unwrap(); - // An unpublished (gone) block cannot be referenced or forked. - assert!(reference_block(&mut cp, &receipt.alias, "visitor", false).is_err()); - } -} diff --git a/apps/commonplace-api/src/reconstruction.rs b/apps/commonplace-api/src/reconstruction.rs deleted file mode 100644 index 844f053f..00000000 --- a/apps/commonplace-api/src/reconstruction.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! The typed CommonPlace GraphQL bridge to Theorem's reconstruction executor. -//! -//! Reconstruction runs live in Theorem's tenant graph, not in CommonPlace's -//! object graph. This module therefore forwards a bounded GraphQL document -//! through Theorem's authenticated MCP transport rather than duplicating the -//! executor or materializing a second run store. - -use std::env; -use std::time::Duration; - -use serde_json::{json, Value}; - -const MCP_URL_ENV: &str = "THEOREM_RECONSTRUCTION_MCP_URL"; -const MCP_TOKEN_ENV: &str = "THEOREM_RECONSTRUCTION_MCP_TOKEN"; -const REQUEST_TIMEOUT: Duration = Duration::from_secs(15); - -/// Call Theorem's pinned GraphQL-over-MCP tools and return the GraphQL -/// `{ data, errors }` envelope carried in MCP structured content. -pub async fn execute( - tenant: &str, - tool: &str, - query: &str, - variables: Value, -) -> Result { - let endpoint = configured_endpoint()?; - let token = configured_token()?; - let client = reqwest::Client::builder() - .timeout(REQUEST_TIMEOUT) - .build() - .map_err(|error| format!("could not construct reconstruction MCP client: {error}"))?; - let response = client - .post(endpoint) - .bearer_auth(token) - .json(&json!({ - "jsonrpc": "2.0", - "id": "commonplace-reconstruction", - "method": "tools/call", - "params": { - "name": tool, - "arguments": { - "tenant": tenant, - "query": query, - "variables": variables, - } - } - })) - .send() - .await - .map_err(|error| format!("reconstruction MCP request failed: {error}"))?; - let status = response.status(); - let body = response - .json::() - .await - .map_err(|error| format!("reconstruction MCP response was not JSON: {error}"))?; - if !status.is_success() { - return Err(format!("reconstruction MCP returned HTTP {status}")); - } - if let Some(error) = body.get("error") { - return Err(format!("reconstruction MCP rejected the request: {error}")); - } - body.pointer("/result/structuredContent") - .cloned() - .ok_or_else(|| "reconstruction MCP response omitted structuredContent".to_string()) -} - -fn configured_endpoint() -> Result { - let endpoint = nonempty_env(MCP_URL_ENV).ok_or_else(|| { - format!("reconstruction is unavailable: set {MCP_URL_ENV} to Theorem's /mcp endpoint") - })?; - if endpoint.starts_with("http://") || endpoint.starts_with("https://") { - Ok(endpoint) - } else { - Err(format!("{MCP_URL_ENV} must be an HTTP(S) URL")) - } -} - -fn configured_token() -> Result { - nonempty_env(MCP_TOKEN_ENV).ok_or_else(|| { - format!("reconstruction is unavailable: set {MCP_TOKEN_ENV} for Theorem MCP authentication") - }) -} - -fn nonempty_env(key: &str) -> Option { - env::var(key) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) -} diff --git a/apps/commonplace-api/src/retrieve.rs b/apps/commonplace-api/src/retrieve.rs deleted file mode 100644 index b45e47de..00000000 --- a/apps/commonplace-api/src/retrieve.rs +++ /dev/null @@ -1,992 +0,0 @@ -//! Ask-over-your-store: unified retrieval (plan unit I1). -//! -//! Points a unified retrieve at the CommonPlace consumer store so a question -//! answers from everything saved, with provenance. Three arms fused with -//! reciprocal-rank fusion (RRF): -//! - vector: the substrate's embedding index (via the F2 ingest pipeline's search); -//! - lexical: an in-crate idf-weighted token-overlap scorer over item text; -//! - graph: personalized PageRank over the F2 `SIMILAR_TO` graph from the -//! strongest vector/lexical seeds. -//! -//! The answer itself comes from an [`AnswerModel`] seam; with no model configured -//! ([`NoModel`]) the result is an honest extractive answer drawn from the top -//! items (still grounded and fully traceable). A local OpenAI-compatible model -//! (Gemma via RustyRed's resident llama-server path) drops in behind the same -//! seam. -//! -//! Scope notes (surfaced): the lexical arm is an in-crate scorer, not the core -//! `FullTextIndex`/tantivy backend (the native-FTS upgrade path). The graph arm -//! uses core personalized PageRank today and reports how many candidates it -//! adds beyond the flat vector-plus-lexical candidate set. - -use std::collections::{HashMap, HashSet, VecDeque}; -use std::sync::Arc; -use std::time::Duration; - -use commonplace::{ - BlobStore, Commonplace, EmbeddingGraphStore, IngestPipeline, Item, ItemBody, SIMILAR_TO_EDGE, -}; -use rustyred_thg_core::{personalized_pagerank, GraphStoreResult, NeighborQuery}; -use serde_json::{json, Value}; - -const DEFAULT_LOCAL_OPENAI_CHAT_URL: &str = "http://127.0.0.1:8080/v1/chat/completions"; -const DEFAULT_GEMMA_MODEL: &str = "gemma-4-12b-it-q4"; -const DEFAULT_MODEL_TIMEOUT_SECS: u64 = 90; -const DEFAULT_MODEL_MAX_TOKENS: u32 = 700; -const DEFAULT_MODEL_TEMPERATURE: f32 = 0.2; -const GRAPH_PROJECTION_NODE_FACTOR: usize = 8; -const GRAPH_PROJECTION_EDGE_FACTOR: usize = 32; - -#[derive(Clone, Copy, Debug)] -struct SimilarityProjectionBudget { - node_limit: usize, - edge_limit: usize, -} - -impl SimilarityProjectionBudget { - fn for_pool(pool: usize, seed_count: usize) -> Self { - Self { - node_limit: seed_count - .saturating_add(pool.saturating_mul(GRAPH_PROJECTION_NODE_FACTOR)) - .max(seed_count) - .max(1), - edge_limit: pool.saturating_mul(GRAPH_PROJECTION_EDGE_FACTOR).max(1), - } - } -} - -/// One retrieved item with its fused score and the arms that surfaced it. -#[derive(Clone, Debug)] -pub struct RetrievedItem { - pub item: Item, - pub score: f64, - pub arms: Vec, -} - -/// A measurable comparison between flat retrieval and graph expansion. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct PprExpansionMeasurement { - /// Unique vector and lexical seeds supplied to personalized PageRank. - pub seed_count: usize, - /// Unique candidates found by vector and lexical retrieval before graph expansion. - pub flat_candidate_count: usize, - /// Ranked non-seed candidates returned by personalized PageRank. - pub ppr_candidate_count: usize, - /// PPR candidates absent from the flat vector-plus-lexical candidate set. - pub ppr_only_candidate_count: usize, -} - -/// Grounding provenance together with its graph-expansion measurement. -#[derive(Clone, Debug)] -pub struct GroundingResult { - pub provenance: Vec, - pub ppr_expansion: PprExpansionMeasurement, -} - -/// How the answer was produced. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AnswerKind { - Model, - Extractive, - Empty, -} - -/// The result of an ask: an answer plus the items it is grounded in. -#[derive(Clone, Debug)] -pub struct AskResult { - pub answer: String, - pub answer_kind: AnswerKind, - pub provenance: Vec, - pub ppr_expansion: PprExpansionMeasurement, -} - -/// The answer-synthesis seam. A generative model implements this; the default -/// [`NoModel`] returns `None` so the caller falls back to an extractive answer. -pub trait AnswerModel: Send + Sync { - fn synthesize(&self, question: &str, context: &[RetrievedItem]) -> Option; -} - -/// The honest default: no generative model configured. -pub struct NoModel; - -impl AnswerModel for NoModel { - fn synthesize(&self, _question: &str, _context: &[RetrievedItem]) -> Option { - None - } -} - -/// OpenAI-compatible answer model for the local RustyRed/Gemma runtime. -/// -/// The model is intentionally optional: request errors return `None`, preserving -/// the extractive answer path when the resident model is not running. -pub struct LocalOpenAiAnswerModel { - chat_url: String, - model: String, - api_key: Option, - temperature: f32, - max_tokens: u32, - timeout: Duration, -} - -impl LocalOpenAiAnswerModel { - pub fn new( - endpoint: impl AsRef, - model: impl Into, - api_key: Option, - temperature: f32, - max_tokens: u32, - timeout: Duration, - ) -> Result { - Ok(Self { - chat_url: chat_completions_url(endpoint.as_ref()), - model: model.into(), - api_key, - temperature, - max_tokens, - timeout, - }) - } - - pub fn from_env() -> Option { - if !env_flag("COMMONPLACE_ASK_MODEL_ENABLED", true) { - return None; - } - - let endpoint = first_env(&["COMMONPLACE_ASK_MODEL_URL", "THEOREM_LOCAL_OPENAI_URL"]) - .or_else(|| { - env_flag("COMMONPLACE_ASK_MODEL_AUTO", true) - .then(|| DEFAULT_LOCAL_OPENAI_CHAT_URL.to_string()) - })?; - let model = first_env(&[ - "COMMONPLACE_ASK_MODEL", - "COMMONPLACE_GEMMA_MODEL", - "THEOREM_LOCAL_OPENAI_MODEL", - "AGENTD_SMOKE_MODEL", - ]) - .unwrap_or_else(|| DEFAULT_GEMMA_MODEL.to_string()); - let api_key = first_env(&["COMMONPLACE_ASK_MODEL_API_KEY", "THEOREM_MODEL_API_KEY"]); - let timeout = Duration::from_secs(env_u64( - "COMMONPLACE_ASK_MODEL_TIMEOUT_SECS", - DEFAULT_MODEL_TIMEOUT_SECS, - )); - let max_tokens = env_u32("COMMONPLACE_ASK_MODEL_MAX_TOKENS", DEFAULT_MODEL_MAX_TOKENS); - let temperature = env_f32( - "COMMONPLACE_ASK_MODEL_TEMPERATURE", - DEFAULT_MODEL_TEMPERATURE, - ); - - Self::new(endpoint, model, api_key, temperature, max_tokens, timeout).ok() - } -} - -impl AnswerModel for LocalOpenAiAnswerModel { - fn synthesize(&self, question: &str, context: &[RetrievedItem]) -> Option { - if context.is_empty() { - return None; - } - - let body = json!({ - "model": self.model, - "messages": [ - { - "role": "system", - "content": "You answer questions for CommonPlace using only the provided grounding items. Be concise, faithful to the sources, and say what is missing when the grounding is thin." - }, - { - "role": "user", - "content": grounded_prompt(question, context) - } - ], - "temperature": self.temperature, - "max_tokens": self.max_tokens, - "stream": false - }); - - let chat_url = self.chat_url.clone(); - let api_key = self.api_key.clone(); - let timeout = self.timeout; - std::thread::spawn(move || { - let http = reqwest::blocking::Client::builder() - .connect_timeout(Duration::from_secs(2)) - .timeout(timeout) - .build() - .ok()?; - let mut request = http.post(chat_url).json(&body); - if let Some(api_key) = api_key { - request = request.bearer_auth(api_key); - } - let value: Value = request.send().ok()?.error_for_status().ok()?.json().ok()?; - parse_chat_content(&value) - }) - .join() - .ok() - .flatten() - } -} - -/// Build the configured answer model for HTTP serving. -/// -/// By default this tries RustyRed's local OpenAI-compatible Gemma endpoint at -/// `127.0.0.1:8080`; set `COMMONPLACE_ASK_MODEL_ENABLED=0` or -/// `COMMONPLACE_ASK_MODEL_AUTO=0` to keep the API extractive-only unless an -/// explicit model URL is provided. -pub fn answer_model_from_env() -> Arc { - LocalOpenAiAnswerModel::from_env() - .map(|model| Arc::new(model) as Arc) - .unwrap_or_else(|| Arc::new(NoModel)) -} - -/// Tuning for unified retrieval. -#[derive(Clone, Debug)] -pub struct AskConfig { - /// Number of provenance items to return. - pub k: usize, - /// Per-arm candidate pool depth before fusion. - pub pool: usize, - /// RRF damping constant (standard is 60). - pub rrf_k: f64, - /// How many top seeds (per arm) feed the graph-propagation arm. - pub graph_seeds: usize, -} - -impl Default for AskConfig { - fn default() -> Self { - Self { - k: 5, - pool: 20, - rrf_k: 60.0, - graph_seeds: 5, - } - } -} - -/// Run unified retrieval over the consumer store and synthesize an answer. -pub fn ask( - cp: &Commonplace, - model: &dyn AnswerModel, - question: &str, - config: &AskConfig, -) -> GraphStoreResult -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let grounding = retrieve_grounding_measured(cp, question, config)?; - let mut result = answer_from_provenance(model, question, grounding.provenance); - result.ppr_expansion = grounding.ppr_expansion; - Ok(result) -} - -/// Retrieve the grounding set for a question without running answer synthesis. -pub fn retrieve_grounding( - cp: &Commonplace, - question: &str, - config: &AskConfig, -) -> GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - Ok(retrieve_grounding_measured(cp, question, config)?.provenance) -} - -/// Retrieve grounding plus an explicit flat-versus-PPR comparison. -pub fn retrieve_grounding_measured( - cp: &Commonplace, - question: &str, - config: &AskConfig, -) -> GraphStoreResult -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - // Arm 1: vector (semantic) over the engine embedding index. - let vector: Vec = IngestPipeline::default() - .search(cp, question, config.pool)? - .into_iter() - .map(|(id, _distance)| id) - .collect(); - - // Arm 2: lexical (exact-term) over all item text. - let items = cp.all_items()?; - let lexical = lexical_rank(question, &items, config.pool); - - // Arm 3: graph propagation over SIMILAR_TO from the strongest seeds. - let vector_seeds: Vec = vector.iter().take(config.graph_seeds).cloned().collect(); - let lexical_seeds: Vec = lexical.iter().take(config.graph_seeds).cloned().collect(); - let seeds: Vec = vector_seeds - .iter() - .chain(lexical_seeds.iter()) - .cloned() - .collect(); - let graph = graph_rank(cp, &[&vector_seeds, &lexical_seeds], config.pool); - let flat_candidates: Vec<&str> = vector - .iter() - .chain(lexical.iter()) - .map(String::as_str) - .collect(); - let ppr_expansion = measure_ppr_expansion(&seeds, &flat_candidates, &graph); - - // Reciprocal-rank fusion of the three ranked lists. - let mut fused: HashMap)> = HashMap::new(); - for (arm, list) in [ - ("vector", &vector), - ("lexical", &lexical), - ("graph", &graph), - ] { - for (rank, id) in list.iter().enumerate() { - let entry = fused.entry(id.clone()).or_insert_with(|| (0.0, Vec::new())); - entry.0 += 1.0 / (config.rrf_k + (rank as f64) + 1.0); - if !entry.1.iter().any(|a| a == arm) { - entry.1.push(arm.to_string()); - } - } - } - let mut ranked: Vec<(String, f64, Vec)> = fused - .into_iter() - .map(|(id, (score, arms))| (id, score, arms)) - .collect(); - ranked.sort_by(|a, b| { - b.1.partial_cmp(&a.1) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.0.cmp(&b.0)) - }); - ranked.truncate(config.k); - - let mut provenance = Vec::with_capacity(ranked.len()); - for (id, score, arms) in ranked { - if let Some(item) = cp.get_item(&id)? { - provenance.push(RetrievedItem { item, score, arms }); - } - } - - Ok(GroundingResult { - provenance, - ppr_expansion, - }) -} - -/// Synthesize an ask result from already-retrieved provenance. -pub fn answer_from_provenance( - model: &dyn AnswerModel, - question: &str, - provenance: Vec, -) -> AskResult { - let (answer, answer_kind) = match model.synthesize(question, &provenance) { - Some(answer) => (answer, AnswerKind::Model), - None if provenance.is_empty() => ( - "No matching items were found in your store.".to_string(), - AnswerKind::Empty, - ), - None => (extractive_answer(&provenance), AnswerKind::Extractive), - }; - - AskResult { - answer, - answer_kind, - provenance, - ppr_expansion: PprExpansionMeasurement::default(), - } -} - -fn tokenize(text: &str) -> Vec { - text.split(|c: char| !c.is_alphanumeric()) - .filter(|token| token.len() >= 2) - .map(|token| token.to_lowercase()) - .collect() -} - -fn item_text(item: &Item) -> String { - let mut text = item.title.clone(); - if let ItemBody::Inline { text: body } = &item.body { - text.push(' '); - text.push_str(body); - } - if let Some(classification) = &item.classification { - text.push(' '); - text.push_str(classification); - } - for tag in &item.tags { - text.push(' '); - text.push_str(tag); - } - text -} - -fn lexical_rank(question: &str, items: &[Item], pool: usize) -> Vec { - let query: HashSet = tokenize(question).into_iter().collect(); - if query.is_empty() || items.is_empty() { - return Vec::new(); - } - let docs: Vec<(String, HashSet)> = items - .iter() - .map(|item| { - ( - item.id.clone(), - tokenize(&item_text(item)).into_iter().collect(), - ) - }) - .collect(); - let total = docs.len() as f64; - let mut document_frequency: HashMap = HashMap::new(); - for (_, tokens) in &docs { - for token in tokens { - *document_frequency.entry(token.clone()).or_insert(0.0) += 1.0; - } - } - let mut scored: Vec<(String, f64)> = docs - .iter() - .map(|(id, tokens)| { - let score = query - .iter() - .filter(|token| tokens.contains(*token)) - .map(|token| { - let df = document_frequency.get(token).copied().unwrap_or(1.0); - (total / (1.0 + df)).ln().max(0.0001) - }) - .sum::(); - (id.clone(), score) - }) - .filter(|(_, score)| *score > 0.0) - .collect(); - scored.sort_by(|a, b| { - b.1.partial_cmp(&a.1) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.0.cmp(&b.0)) - }); - scored.truncate(pool); - scored.into_iter().map(|(id, _)| id).collect() -} - -fn graph_rank(cp: &Commonplace, seed_arms: &[&[String]], pool: usize) -> Vec -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - if seed_arms.iter().all(|arm| arm.is_empty()) || pool == 0 { - return Vec::new(); - } - - let seed_weights = normalized_seed_weights(seed_arms); - if seed_weights.is_empty() { - return Vec::new(); - } - let adjacency = seed_reachable_similarity_adjacency( - cp, - seed_weights.keys(), - SimilarityProjectionBudget::for_pool(pool, seed_weights.len()), - ); - - let seed_ids: HashSet<&str> = seed_weights.keys().map(String::as_str).collect(); - let mut ranked: Vec<(String, f64)> = - personalized_pagerank(&adjacency, &seed_weights, 0.15, 1e-6, 100_000) - .into_iter() - .filter(|(id, score)| !seed_ids.contains(id.as_str()) && *score > 0.0) - .collect(); - ranked.sort_by(|a, b| { - b.1.partial_cmp(&a.1) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.0.cmp(&b.0)) - }); - ranked.truncate(pool); - ranked.into_iter().map(|(id, _)| id).collect() -} - -fn seed_reachable_similarity_adjacency<'a, S, B>( - cp: &Commonplace, - seeds: impl Iterator, - budget: SimilarityProjectionBudget, -) -> HashMap> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut seed_ids: Vec = seeds.cloned().collect(); - seed_ids.sort(); - seed_ids.dedup(); - let mut pending: VecDeque = seed_ids.iter().cloned().collect(); - let mut admitted_nodes: HashSet = seed_ids.into_iter().collect(); - let mut visited_nodes = HashSet::new(); - let mut visited_edges = HashSet::new(); - let mut admitted_edge_count = 0_usize; - let mut adjacency: HashMap> = HashMap::new(); - let node_limit = budget.node_limit.max(admitted_nodes.len()); - - 'projection: while let Some(node_id) = pending.pop_front() { - if !visited_nodes.insert(node_id.clone()) { - continue; - } - - for query in [ - NeighborQuery::out(&node_id).with_edge_type(SIMILAR_TO_EDGE), - NeighborQuery::in_(&node_id).with_edge_type(SIMILAR_TO_EDGE), - ] { - let mut hits: Vec<_> = cp.store().neighbors(query).into_iter().collect(); - hits.sort_by(|left, right| left.edge_id.cmp(&right.edge_id)); - for hit in hits { - if admitted_edge_count >= budget.edge_limit { - break 'projection; - } - if !visited_edges.insert(hit.edge_id.clone()) { - continue; - } - let Some(edge) = cp.store().get_edge(&hit.edge_id) else { - continue; - }; - let weight = edge - .confidence - .or_else(|| edge.properties.get("score").and_then(Value::as_f64)) - .unwrap_or(1.0); - if !weight.is_finite() || weight <= 0.0 { - continue; - } - let new_endpoints = [&edge.from_id, &edge.to_id] - .into_iter() - .filter(|id| !admitted_nodes.contains(id.as_str())) - .count(); - if admitted_nodes.len().saturating_add(new_endpoints) > node_limit { - continue; - } - - adjacency - .entry(edge.from_id.clone()) - .or_default() - .push((edge.to_id.clone(), weight)); - adjacency - .entry(edge.to_id.clone()) - .or_default() - .push((edge.from_id.clone(), weight)); - for endpoint in [&edge.from_id, &edge.to_id] { - if admitted_nodes.insert(endpoint.clone()) { - pending.push_back(endpoint.clone()); - } - } - admitted_edge_count += 1; - } - } - } - - adjacency -} - -fn normalized_seed_weights(seed_arms: &[&[String]]) -> HashMap { - let mut seed_weights: HashMap = HashMap::new(); - for arm in seed_arms { - for (rank, seed) in arm.iter().enumerate() { - *seed_weights.entry(seed.clone()).or_insert(0.0) += 1.0 / (rank as f64 + 1.0); - } - } - let total_seed_weight: f64 = seed_weights.values().sum(); - if total_seed_weight <= 0.0 { - return HashMap::new(); - } - for weight in seed_weights.values_mut() { - *weight /= total_seed_weight; - } - seed_weights -} - -fn measure_ppr_expansion( - seeds: &[String], - flat_candidates: &[&str], - ppr_candidates: &[String], -) -> PprExpansionMeasurement { - let unique_seeds: HashSet<&str> = seeds.iter().map(String::as_str).collect(); - let unique_flat: HashSet<&str> = flat_candidates.iter().copied().collect(); - let unique_ppr: HashSet<&str> = ppr_candidates.iter().map(String::as_str).collect(); - - PprExpansionMeasurement { - seed_count: unique_seeds.len(), - flat_candidate_count: unique_flat.len(), - ppr_candidate_count: unique_ppr.len(), - ppr_only_candidate_count: unique_ppr.difference(&unique_flat).count(), - } -} - -fn extractive_answer(provenance: &[RetrievedItem]) -> String { - let titles: Vec<&str> = provenance - .iter() - .take(3) - .map(|hit| hit.item.title.as_str()) - .collect(); - let lead = match &provenance[0].item.body { - ItemBody::Inline { text } => first_sentence(text), - _ => provenance[0].item.title.clone(), - }; - format!( - "{lead} [grounded in {} item(s): {}; no generative model configured]", - provenance.len(), - titles.join(", ") - ) -} - -fn first_sentence(text: &str) -> String { - let trimmed = text.trim(); - match trimmed.find(['.', '!', '?']) { - Some(idx) => trimmed[..=idx].trim().to_string(), - None => trimmed.chars().take(200).collect(), - } -} - -fn grounded_prompt(question: &str, context: &[RetrievedItem]) -> String { - let mut prompt = format!( - "Question:\n{question}\n\nGrounding items:\n{}\n\nAnswer in 2-6 sentences. Include item titles when they help the user trace the answer.", - context - .iter() - .enumerate() - .map(|(idx, hit)| grounding_item(idx + 1, hit)) - .collect::>() - .join("\n\n") - ); - if prompt.chars().count() > 24_000 { - prompt = prompt.chars().take(24_000).collect(); - prompt.push_str("\n\n[grounding truncated]"); - } - prompt -} - -fn grounding_item(index: usize, hit: &RetrievedItem) -> String { - let item = &hit.item; - let mut lines = vec![ - format!("[{index}] title: {}", item.title), - format!("id: {}", item.id), - format!("kind: {}", item.kind.as_str()), - ]; - if let Some(source) = &item.source { - lines.push(format!("source: {source}")); - } - if !item.tags.is_empty() { - lines.push(format!("tags: {}", item.tags.join(", "))); - } - if let Some(classification) = &item.classification { - lines.push(format!("classification: {classification}")); - } - let body = match &item.body { - ItemBody::Inline { text } => truncate_chars(text.trim(), 1800), - ItemBody::Blob { mime, .. } => mime - .as_deref() - .map(|mime| format!("[blob item; mime: {mime}]")) - .unwrap_or_else(|| "[blob item]".to_string()), - ItemBody::Empty => "[empty item]".to_string(), - }; - lines.push(format!("text: {body}")); - lines.push(format!( - "retrieval: score {:.4}, arms {}", - hit.score, - hit.arms.join(", ") - )); - lines.join("\n") -} - -fn truncate_chars(text: &str, limit: usize) -> String { - let mut output: String = text.chars().take(limit).collect(); - if text.chars().count() > limit { - output.push_str("..."); - } - output -} - -fn parse_chat_content(value: &Value) -> Option { - if value.get("error").is_some() { - return None; - } - let content = value - .get("choices") - .and_then(Value::as_array) - .and_then(|choices| choices.first()) - .and_then(|choice| choice.get("message")) - .and_then(|message| message.get("content")) - .and_then(Value::as_str)? - .trim(); - (!content.is_empty()).then(|| content.to_string()) -} - -fn chat_completions_url(endpoint: &str) -> String { - let trimmed = endpoint.trim().trim_end_matches('/'); - if trimmed.ends_with("/chat/completions") { - trimmed.to_string() - } else if trimmed.ends_with("/v1") { - format!("{trimmed}/chat/completions") - } else { - format!("{trimmed}/v1/chat/completions") - } -} - -fn first_env(names: &[&str]) -> Option { - names.iter().find_map(|name| { - std::env::var(name) - .ok() - .filter(|value| !value.trim().is_empty()) - }) -} - -fn env_flag(name: &str, default: bool) -> bool { - std::env::var(name) - .ok() - .and_then(|value| match value.trim().to_ascii_lowercase().as_str() { - "1" | "true" | "yes" | "on" => Some(true), - "0" | "false" | "no" | "off" => Some(false), - _ => None, - }) - .unwrap_or(default) -} - -fn env_u64(name: &str, default: u64) -> u64 { - std::env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(default) -} - -fn env_u32(name: &str, default: u32) -> u32 { - std::env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(default) -} - -fn env_f32(name: &str, default: f32) -> f32 { - std::env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(default) -} - -#[cfg(test)] -mod tests { - use super::*; - use commonplace::InMemoryBlobStore; - use rustyred_thg_core::{InMemoryGraphStore, NodeRecord}; - use std::io::{BufRead, BufReader, Read, Write}; - use std::net::TcpListener; - use std::thread; - - #[test] - fn chat_url_accepts_base_v1_or_full_endpoint() { - assert_eq!( - chat_completions_url("http://127.0.0.1:8080"), - "http://127.0.0.1:8080/v1/chat/completions" - ); - assert_eq!( - chat_completions_url("http://127.0.0.1:8080/v1"), - "http://127.0.0.1:8080/v1/chat/completions" - ); - assert_eq!( - chat_completions_url("http://127.0.0.1:8080/v1/chat/completions"), - "http://127.0.0.1:8080/v1/chat/completions" - ); - } - - #[test] - fn local_openai_model_posts_grounded_prompt_and_reads_answer() { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); - let handle = thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - let mut reader = BufReader::new(stream.try_clone().unwrap()); - let mut headers = String::new(); - loop { - let mut line = String::new(); - reader.read_line(&mut line).unwrap(); - if line == "\r\n" || line.is_empty() { - break; - } - headers.push_str(&line); - } - let content_length = headers - .lines() - .find_map(|line| { - let (name, value) = line.split_once(':')?; - name.eq_ignore_ascii_case("content-length") - .then(|| value.trim().parse::().ok()) - .flatten() - }) - .unwrap(); - let mut body = vec![0; content_length]; - reader.read_exact(&mut body).unwrap(); - let payload = - r#"{"choices":[{"message":{"content":"Gemma answered from grounded notes."}}]}"#; - let response = format!( - "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{}", - payload.len(), - payload - ); - stream.write_all(response.as_bytes()).unwrap(); - String::from_utf8(body).unwrap() - }); - - let model = LocalOpenAiAnswerModel::new( - format!("http://{addr}/v1"), - "test-gemma", - None, - 0.1, - 64, - Duration::from_secs(5), - ) - .unwrap(); - let context = vec![RetrievedItem { - item: Item::note( - "Gemma note", - "The local Gemma model should synthesize grounded answers.", - ), - score: 1.0, - arms: vec!["lexical".to_string()], - }]; - - let answer = model.synthesize("what should answer?", &context).unwrap(); - let body = handle.join().unwrap(); - - assert_eq!(answer, "Gemma answered from grounded notes."); - assert!( - body.contains("test-gemma"), - "body should include model: {body}" - ); - assert!( - body.contains("Gemma note"), - "body should include grounding title: {body}" - ); - assert!( - body.contains("what should answer?"), - "body should include the question: {body}" - ); - } - - #[test] - fn ppr_expansion_surfaces_a_two_hop_item_missing_from_flat_top_n() { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - let seed = cp - .put_item(Item::note( - "Seed", - "A directly retrieved note about the target question.", - )) - .unwrap(); - let bridge = cp - .put_item(Item::note( - "Bridge", - "A structurally related note connecting the seed to hidden evidence.", - )) - .unwrap(); - let hidden = cp - .put_item(Item::note( - "Hidden evidence", - "A second-hop passage that flat top-N retrieval did not return.", - )) - .unwrap(); - cp.add_similarity(&seed.id, &bridge.id, 0.95).unwrap(); - cp.add_similarity(&bridge.id, &hidden.id, 0.9).unwrap(); - - let seeds = vec![seed.id.clone()]; - let ppr_candidates = graph_rank(&cp, &[&seeds], 10); - let flat_top_n = vec![seed.id.as_str(), bridge.id.as_str()]; - let measurement = measure_ppr_expansion(&seeds, &flat_top_n, &ppr_candidates); - - assert!(ppr_candidates.contains(&bridge.id)); - assert!( - ppr_candidates.contains(&hidden.id), - "PPR should traverse the similarity chain beyond one hop" - ); - assert!(!flat_top_n.contains(&hidden.id.as_str())); - assert_eq!(measurement.seed_count, 1); - assert_eq!(measurement.flat_candidate_count, 2); - assert_eq!(measurement.ppr_candidate_count, 2); - assert_eq!(measurement.ppr_only_candidate_count, 1); - } - - #[test] - fn graph_seed_weights_merge_per_arm_before_duplicate_collapse() { - let vector = vec!["seed:a".to_string(), "seed:b".to_string()]; - let lexical = vec!["seed:a".to_string(), "seed:c".to_string()]; - let weights = normalized_seed_weights(&[&vector, &lexical]); - - assert!( - (weights["seed:b"] - weights["seed:c"]).abs() < 1e-9, - "rank-two seeds from separate arms should contribute equally after duplicate collapse" - ); - assert!(weights["seed:a"] > weights["seed:b"]); - } - - #[test] - fn graph_projection_excludes_similarity_components_unreachable_from_seeds() { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - for id in ["seed", "reachable", "unrelated:a", "unrelated:b"] { - cp.store_mut() - .upsert_node(NodeRecord::new(id, ["Item"], json!({}))) - .expect("node"); - } - cp.add_similarity("seed", "reachable", 0.9) - .expect("reachable edge"); - cp.add_similarity("unrelated:a", "unrelated:b", 0.8) - .expect("unrelated edge"); - - let seeds = ["seed".to_string()]; - let adjacency = seed_reachable_similarity_adjacency( - &cp, - seeds.iter(), - SimilarityProjectionBudget::for_pool(10, seeds.len()), - ); - - assert_eq!(adjacency.len(), 2); - assert!(adjacency.contains_key("seed")); - assert!(adjacency.contains_key("reachable")); - assert!(!adjacency.contains_key("unrelated:a")); - assert!(!adjacency.contains_key("unrelated:b")); - } - - #[test] - fn graph_projection_obeys_node_and_edge_budgets_on_dense_components() { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - cp.store_mut() - .upsert_node(NodeRecord::new("seed", ["Item"], json!({}))) - .expect("seed node"); - for index in 0..20 { - let id = format!("neighbor:{index:02}"); - cp.store_mut() - .upsert_node(NodeRecord::new(&id, ["Item"], json!({}))) - .expect("neighbor node"); - cp.add_similarity("seed", &id, 0.9) - .expect("similarity edge"); - } - - let seeds = ["seed".to_string()]; - let adjacency = seed_reachable_similarity_adjacency( - &cp, - seeds.iter(), - SimilarityProjectionBudget { - node_limit: 4, - edge_limit: 3, - }, - ); - let undirected_edges = adjacency.values().map(Vec::len).sum::() / 2; - - assert!(adjacency.len() <= 4); - assert!(undirected_edges <= 3); - } - - #[test] - fn graph_projection_budget_is_independent_of_seed_iteration_order() { - let mut cp = Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()); - for id in ["seed:a", "seed:b", "neighbor:a", "neighbor:b"] { - cp.store_mut() - .upsert_node(NodeRecord::new(id, ["Item"], json!({}))) - .expect("node"); - } - cp.add_similarity("seed:a", "neighbor:a", 0.9) - .expect("first similarity"); - cp.add_similarity("seed:b", "neighbor:b", 0.9) - .expect("second similarity"); - - let forward = ["seed:a".to_string(), "seed:b".to_string()]; - let reverse = ["seed:b".to_string(), "seed:a".to_string()]; - let budget = SimilarityProjectionBudget { - node_limit: 3, - edge_limit: 1, - }; - - let forward_adjacency = seed_reachable_similarity_adjacency(&cp, forward.iter(), budget); - let reverse_adjacency = seed_reachable_similarity_adjacency(&cp, reverse.iter(), budget); - let undirected_edges = forward_adjacency.values().map(Vec::len).sum::() / 2; - - assert_eq!(forward_adjacency, reverse_adjacency); - assert_eq!(undirected_edges, 1); - } -} diff --git a/apps/commonplace-api/src/salience.rs b/apps/commonplace-api/src/salience.rs deleted file mode 100644 index dfb2c91f..00000000 --- a/apps/commonplace-api/src/salience.rs +++ /dev/null @@ -1,811 +0,0 @@ -//! Salience: which passages of a page connect back to the tenant's knowledge -//! (HANDOFF-MARGIN-RECALL D2). The margin-recall product surface. -//! -//! Where [`discover`](crate::discover) finds latent item<->item links *inside* -//! the store, salience runs over an *external* page's visible text and asks the -//! inverse question: which spans of what the user is reading connect to what -//! they already saved? Each answer is a [`SalienceCandidate`] whose anchor is a -//! W3C text-quote + position over the *page* text, reusing the exact -//! [`commonplace::annotation`] wire types the D3 store persists, so a highlight -//! the pipeline proposes and a highlight the store keeps are the same shape. -//! -//! Two tiers, both honest about their floor (spec: "anchoring/precision degrades -//! honestly"): -//! - **Semantic** rides the existing embedding index (the same -//! [`IngestPipeline::search`] cosine seam `discover` uses): a passage whose -//! embedding is near a stored item's is a connection. -//! - **Exact** is the local verbatim floor: a passage that *names* a stored -//! item (its title appears word-bounded in the span) is a high-confidence -//! connection needing no similarity model. The richer DATAWAVE field-fact -//! source the spec names would *extend* this tier; it is absent in this -//! checkout (VF5), so exact is the local title match alone, and that absence -//! is named rather than faked. -//! -//! Thresholds and budget are [`SalienceConfig`] args, not constants, so the D7 -//! dial can hand `quiet()` (exact only, higher bar) or `active()` (both tiers) -//! without the pipeline hard-coding a policy. Results cache by page content hash -//! (D2-3) and the compute path is timed (D2-4); the *measured cold number on a -//! real node* stays a named gap (VF8), the instrumentation does not. - -use std::collections::{HashMap, HashSet}; -use std::time::Instant; - -use commonplace::annotation::{TextPositionSelector, TextQuoteSelector}; -use commonplace::{BlobStore, Commonplace, EmbeddingGraphStore, IngestPipeline, Item, ItemBody}; -use rustyred_thg_core::GraphStoreResult; -use serde::{Deserialize, Serialize}; - -/// Which retrieval tier surfaced a candidate. Wire value is snake_case, parity -/// with the TS `SalienceTier` union. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SalienceTier { - /// The passage verbatim names a stored item (or, when wired, a DATAWAVE - /// field-fact). Ranks ahead of semantic. - Exact, - /// The passage embeds near a stored item under the ingest index. - Semantic, -} - -/// A page span plus the context that disambiguates repeats: a W3C text quote and -/// the character range it was resolved at. The quote is authoritative; the -/// position is the re-anchor hint the overlay prefers on an unchanged revisit -/// (same contract as [`commonplace::annotation::Anchor::TextQuote`]). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct SalienceAnchor { - pub quote: TextQuoteSelector, - pub position: TextPositionSelector, -} - -/// One proposed margin-recall highlight: where on the page, which tier found it, -/// the connection explanation shown in the margin, its score, and the stored -/// records it connects to (the openable provenance chain, D6-2). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct SalienceCandidate { - pub anchor: SalienceAnchor, - pub tier: SalienceTier, - pub explanation: String, - pub score: f64, - /// Ids of the stored items this passage connects to, strongest first. Each - /// is an openable record in the provenance chain. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub refs: Vec, -} - -/// Tuning for salience. Thresholds and budget are args (D2 acceptance: "threshold -/// + budget are config args, not constants") so the D7 dial supplies the policy. -#[derive(Clone, Debug)] -pub struct SalienceConfig { - /// Minimum cosine similarity for a semantic connection. - pub min_similarity: f64, - /// Max candidates returned (the D4 overlay budget cap). - pub max_candidates: usize, - /// Stored neighbors examined per passage in the semantic tier. - pub per_passage_neighbors: usize, - /// Passages shorter than this (chars) are never anchored. - pub min_passage_chars: usize, - /// Information gate: a passage needs at least this many content tokens - /// (length > 3, non-stopword) to be considered. An in-repo term-frequency - /// proxy standing in for a real corpus-IDF gate (absent without Valkey). - pub min_content_tokens: usize, - /// Chars of prefix/suffix captured around a span for quote disambiguation. - pub context_chars: usize, - /// Run the exact (verbatim-title) tier. - pub exact_tier_enabled: bool, - /// Run the semantic (embedding) tier. - pub semantic_tier_enabled: bool, - /// Shortest stored title eligible for exact matching (guards against noisy - /// short titles matching common words). - pub min_exact_title_chars: usize, -} - -impl Default for SalienceConfig { - fn default() -> Self { - Self { - min_similarity: 0.55, - max_candidates: 5, - per_passage_neighbors: 6, - min_passage_chars: 24, - min_content_tokens: 3, - context_chars: 32, - exact_tier_enabled: true, - semantic_tier_enabled: true, - min_exact_title_chars: 6, - } - } -} - -impl SalienceConfig { - /// D7 **Active**: both tiers at the standard threshold. - pub fn active() -> Self { - Self::default() - } - - /// D7 **Quiet**: exact tier only, higher threshold. Honors non-goal 4 (no - /// proactive semantic surfacing in Quiet) and D2-5 (not auto-surfaced unless - /// Active) at the config layer: with the semantic tier off, a page that only - /// embeds-near a note yields nothing. - pub fn quiet() -> Self { - Self { - semantic_tier_enabled: false, - min_similarity: 0.72, - ..Self::default() - } - } -} - -/// Whether a cached page hash produced this result or it was recomputed. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CacheStatus { - Miss, - Hit, -} - -/// A salience run plus its cache disposition and compute latency (D2-4). -#[derive(Clone, Debug)] -pub struct SalienceResult { - pub candidates: Vec, - pub cache: CacheStatus, - /// Wall-clock of the compute path in ms; 0 on a cache hit. The measured - /// cold number on a real node is a named gap (VF8); this is the seam. - pub elapsed_ms: u64, -} - -/// Result cache keyed by page content hash (`blake3:`, D2-3). A repeat -/// visit to an unchanged page returns the prior candidates without recomputing. -#[derive(Clone, Debug, Default)] -pub struct SalienceCache { - entries: HashMap>, -} - -impl SalienceCache { - pub fn new() -> Self { - Self::default() - } - - pub fn get(&self, content_hash: &str) -> Option<&Vec> { - self.entries.get(content_hash) - } - - pub fn insert(&mut self, content_hash: impl Into, candidates: Vec) { - self.entries.insert(content_hash.into(), candidates); - } - - pub fn len(&self) -> usize { - self.entries.len() - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } -} - -/// Run salience over `page_text` against the tenant store. Returns ranked -/// candidates (exact tier first, then score), deduplicated by span and capped to -/// the config budget. A page with no connections returns an empty vec (zero UI). -pub fn salience( - cp: &Commonplace, - page_text: &str, - config: &SalienceConfig, -) -> GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let chars: Vec = page_text.chars().collect(); - let passages = segment_passages(&chars, config.min_passage_chars); - if passages.is_empty() { - return Ok(Vec::new()); - } - - // Exact-tier corpus: stored titles. DATAWAVE field-facts would extend this - // (VF5, named gap); absent here, the exact tier is the verbatim-title floor. - let items = if config.exact_tier_enabled { - cp.all_items()? - } else { - Vec::new() - }; - let pipeline = IngestPipeline::default(); - - let mut candidates: Vec = Vec::new(); - for passage in &passages { - if !passes_information_gate(&passage.text, config.min_content_tokens) { - continue; - } - - // Exact tier wins a passage outright: a page that literally names a - // saved item is a stronger signal than an embedding neighbor, and one - // span should not carry two overlapping highlights. - let mut claimed = false; - if config.exact_tier_enabled { - if let Some(candidate) = exact_candidate(&chars, passage, &items, config) { - candidates.push(candidate); - claimed = true; - } - } - if config.semantic_tier_enabled && !claimed { - if let Some(candidate) = semantic_candidate(cp, &pipeline, &chars, passage, config)? { - candidates.push(candidate); - } - } - } - - rank_and_cap(&mut candidates, config.max_candidates); - Ok(candidates) -} - -/// Salience with the D2-3 result cache and D2-4 latency seam. On a cache hit the -/// prior candidates are returned as-is with `elapsed_ms == 0`; on a miss the -/// compute path is timed and the result stored under `content_hash`. -pub fn salience_cached( - cp: &Commonplace, - page_text: &str, - content_hash: &str, - config: &SalienceConfig, - cache: &mut SalienceCache, -) -> GraphStoreResult -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - if let Some(cached) = cache.get(content_hash) { - return Ok(SalienceResult { - candidates: cached.clone(), - cache: CacheStatus::Hit, - elapsed_ms: 0, - }); - } - let started = Instant::now(); - let candidates = salience(cp, page_text, config)?; - let elapsed_ms = started.elapsed().as_millis() as u64; - cache.insert(content_hash.to_string(), candidates.clone()); - Ok(SalienceResult { - candidates, - cache: CacheStatus::Miss, - elapsed_ms, - }) -} - -/// A trimmed page span with its char offsets into the page text. -struct Passage { - text: String, - start: u32, - end: u32, -} - -/// Split page text into sentence-ish passages, tracking char offsets. A boundary -/// is `.`/`!`/`?`/newline followed by whitespace or end-of-text. Offsets are -/// character indices (the position is a hint; the quote is authoritative, so -/// exact code-unit parity with the JS side is not required, per D3-3). -fn segment_passages(chars: &[char], min_chars: usize) -> Vec { - let mut passages = Vec::new(); - let mut start = 0usize; - let mut index = 0usize; - while index < chars.len() { - let ch = chars[index]; - let boundary = matches!(ch, '.' | '!' | '?' | '\n') - && chars - .get(index + 1) - .map(|next| next.is_whitespace()) - .unwrap_or(true); - if boundary { - push_passage(chars, start, index + 1, min_chars, &mut passages); - let mut next = index + 1; - while next < chars.len() && chars[next].is_whitespace() { - next += 1; - } - start = next; - index = next; - continue; - } - index += 1; - } - if start < chars.len() { - push_passage(chars, start, chars.len(), min_chars, &mut passages); - } - passages -} - -fn push_passage( - chars: &[char], - start: usize, - end: usize, - min_chars: usize, - out: &mut Vec, -) { - let mut begin = start; - let mut finish = end; - while begin < finish && chars[begin].is_whitespace() { - begin += 1; - } - while finish > begin && chars[finish - 1].is_whitespace() { - finish -= 1; - } - if finish <= begin { - return; - } - let text: String = chars[begin..finish].iter().collect(); - if text.chars().count() < min_chars { - return; - } - out.push(Passage { - text, - start: begin as u32, - end: finish as u32, - }); -} - -/// In-repo term-frequency information gate: a passage needs at least -/// `min_content_tokens` content tokens (length > 3, non-stopword). This is the -/// honest degrade of a corpus-IDF gate (real document frequencies live in -/// Valkey, absent here); it keeps low-information spans (nav chrome, stopword -/// runs) from ever anchoring. -fn passes_information_gate(text: &str, min_content_tokens: usize) -> bool { - let content = text - .split(|ch: char| !ch.is_alphanumeric()) - .filter(|token| token.len() > 3 && !is_stopword(&token.to_ascii_lowercase())) - .count(); - content >= min_content_tokens -} - -fn is_stopword(lower: &str) -> bool { - matches!( - lower, - "the" - | "and" - | "for" - | "that" - | "this" - | "with" - | "from" - | "have" - | "were" - | "was" - | "are" - | "but" - | "not" - | "you" - | "your" - | "its" - | "into" - | "than" - | "then" - | "they" - | "them" - | "our" - | "their" - | "which" - | "would" - | "could" - ) -} - -/// Build the anchor for a passage: the exact quote plus prefix/suffix context -/// windows and the resolved char range. -fn anchor_for(chars: &[char], passage: &Passage, context_chars: usize) -> SalienceAnchor { - let start = passage.start as usize; - let end = passage.end as usize; - let prefix_start = start.saturating_sub(context_chars); - let prefix: String = chars[prefix_start..start].iter().collect(); - let suffix_end = (end + context_chars).min(chars.len()); - let suffix: String = chars[end..suffix_end].iter().collect(); - SalienceAnchor { - quote: TextQuoteSelector { - exact: passage.text.clone(), - prefix: (!prefix.trim().is_empty()).then_some(prefix), - suffix: (!suffix.trim().is_empty()).then_some(suffix), - }, - position: TextPositionSelector { - start: passage.start, - end: passage.end, - }, - } -} - -/// Exact tier: the first stored item whose title appears word-bounded in the -/// passage. Score 1.0; the connection is that the page names a saved record. -fn exact_candidate( - chars: &[char], - passage: &Passage, - items: &[Item], - config: &SalienceConfig, -) -> Option { - let haystack = passage.text.to_ascii_lowercase(); - for item in items { - let title = item.title.trim(); - if title.chars().count() < config.min_exact_title_chars { - continue; - } - let needle = title.to_ascii_lowercase(); - if contains_word_bounded(&haystack, &needle) { - return Some(SalienceCandidate { - anchor: anchor_for(chars, passage, config.context_chars), - tier: SalienceTier::Exact, - explanation: format!( - "This passage names \"{title}\", which is already in your library." - ), - score: 1.0, - refs: vec![item.id.clone()], - }); - } - } - None -} - -/// Semantic tier: the strongest stored neighbor of the passage above threshold -/// that also shares content vocabulary with it. -/// -/// The lexical floor is a precision guard, not the ranking signal: an embedding -/// neighbor that shares no content token with the passage is a false positive -/// (a real error class for any embedder, and acute under the low-dimensional -/// offline embedder, whose hash buckets collide). Cosine still ranks; the floor -/// only vetoes. Genuine connections share vocabulary, so it never suppresses a -/// real match. -fn semantic_candidate( - cp: &Commonplace, - pipeline: &IngestPipeline, - chars: &[char], - passage: &Passage, - config: &SalienceConfig, -) -> GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let hits = pipeline.search(cp, &passage.text, config.per_passage_neighbors)?; - let mut scored: Vec<(String, f64)> = hits - .into_iter() - .map(|(id, distance)| (id, 1.0 - distance as f64)) - .filter(|(_, similarity)| *similarity >= config.min_similarity) - .collect(); - if scored.is_empty() { - return Ok(None); - } - scored.sort_by(|left, right| { - right - .1 - .partial_cmp(&left.1) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| left.0.cmp(&right.0)) - }); - - let passage_tokens = content_tokens(&passage.text); - let mut connected: Vec<(String, f64, String)> = Vec::new(); - for (id, similarity) in scored { - let Some(item) = cp.get_item(&id)? else { - continue; - }; - if shares_content_token(&passage_tokens, &item) { - connected.push((id, similarity, item.title)); - } - } - let Some((_, best_score, title)) = connected.first().cloned() else { - return Ok(None); - }; - Ok(Some(SalienceCandidate { - anchor: anchor_for(chars, passage, config.context_chars), - tier: SalienceTier::Semantic, - explanation: format!("Related to your note \"{title}\" (similarity {best_score:.2})."), - score: best_score, - refs: connected.into_iter().map(|(id, _, _)| id).collect(), - })) -} - -/// Content tokens of a text: lowercased alphanumeric runs longer than three -/// chars that are not stopwords. The same extraction the information gate uses, -/// reused as the semantic tier's lexical-overlap key. -fn content_tokens(text: &str) -> HashSet { - text.split(|ch: char| !ch.is_alphanumeric()) - .filter_map(|token| { - let lower = token.to_ascii_lowercase(); - (lower.len() > 3 && !is_stopword(&lower)).then_some(lower) - }) - .collect() -} - -/// Whether a stored item's title-plus-body shares any content token with the -/// passage. -fn shares_content_token(passage_tokens: &HashSet, item: &Item) -> bool { - let mut text = item.title.clone(); - if let ItemBody::Inline { text: body } = &item.body { - text.push(' '); - text.push_str(body); - } - content_tokens(&text) - .iter() - .any(|token| passage_tokens.contains(token)) -} - -/// True when `needle` occurs in `haystack` with non-alphanumeric (or edge) -/// boundaries on both sides, so "war" does not match inside "warden". Both -/// arguments are already lowercased by the caller. -fn contains_word_bounded(haystack: &str, needle: &str) -> bool { - if needle.is_empty() { - return false; - } - let mut from = 0usize; - while let Some(offset) = haystack[from..].find(needle) { - let at = from + offset; - let before_ok = at == 0 - || !haystack[..at] - .chars() - .next_back() - .map(|ch| ch.is_alphanumeric()) - .unwrap_or(false); - let after = at + needle.len(); - let after_ok = after >= haystack.len() - || !haystack[after..] - .chars() - .next() - .map(|ch| ch.is_alphanumeric()) - .unwrap_or(false); - if before_ok && after_ok { - return true; - } - from = at + needle.len(); - if from > haystack.len() { - break; - } - } - false -} - -/// Rank exact-tier first, then by score descending, then by span start; drop -/// duplicate spans (an exact and semantic hit on the same passage keeps exact); -/// cap to the budget. -fn rank_and_cap(candidates: &mut Vec, max_candidates: usize) { - candidates.sort_by(|left, right| { - tier_rank(left.tier) - .cmp(&tier_rank(right.tier)) - .then_with(|| { - right - .score - .partial_cmp(&left.score) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .then_with(|| left.anchor.position.start.cmp(&right.anchor.position.start)) - }); - let mut seen: HashSet<(u32, u32)> = HashSet::new(); - candidates.retain(|candidate| { - seen.insert(( - candidate.anchor.position.start, - candidate.anchor.position.end, - )) - }); - candidates.truncate(max_candidates); -} - -fn tier_rank(tier: SalienceTier) -> u8 { - match tier { - SalienceTier::Exact => 0, - SalienceTier::Semantic => 1, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use commonplace::{InMemoryBlobStore, IngestInput, ItemKind}; - use rustyred_thg_core::InMemoryGraphStore; - - fn store() -> Commonplace { - Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()) - } - - fn seed( - cp: &mut Commonplace, - title: &str, - text: &str, - ) -> String { - IngestPipeline::default() - .ingest(cp, IngestInput::text(title, text, ItemKind::Doc)) - .expect("ingest seeds an embedded item") - .item - .id - } - - #[test] - fn seeded_tenant_yields_semantic_candidate_with_complete_explanation() { - // A saved note; the page shares its distinctive tokens but never names - // the title verbatim, so only the semantic tier can fire. - let mut cp = store(); - let id = seed( - &mut cp, - "Greek Art Notes", - "marble sculpture athens acropolis parthenon frieze carving", - ); - - let page = "Yesterday was ordinary. \ - The marble sculpture athens acropolis parthenon frieze carving still stands. \ - Nothing else happened."; - let candidates = salience(&cp, page, &SalienceConfig::default()).unwrap(); - - assert_eq!(candidates.len(), 1, "one connected passage"); - let candidate = &candidates[0]; - assert_eq!(candidate.tier, SalienceTier::Semantic); - assert!(candidate.score >= SalienceConfig::default().min_similarity); - assert!( - candidate.explanation.contains("Greek Art Notes"), - "explanation names the connected record: {}", - candidate.explanation - ); - assert!( - candidate.refs.contains(&id), - "refs carry the openable record" - ); - // The anchor's char range selects exactly the quoted passage. - let chars: Vec = page.chars().collect(); - let selected: String = chars - [candidate.anchor.position.start as usize..candidate.anchor.position.end as usize] - .iter() - .collect(); - assert_eq!(selected, candidate.anchor.quote.exact); - assert!(selected.contains("parthenon")); - } - - #[test] - fn exact_tier_matches_verbatim_title_and_ranks_first() { - let mut cp = store(); - // Distinct titles so the page can name one exactly and embed-near another. - let war = seed( - &mut cp, - "Peloponnesian War", - "sparta athens conflict alliance siege", - ); - seed( - &mut cp, - "Botany Field Guide", - "fern spore chlorophyll photosynthesis xylem phloem", - ); - - let page = "The Peloponnesian War reshaped the ancient world forever. \ - Separately, fern spore chlorophyll photosynthesis xylem phloem define plant life."; - let candidates = salience(&cp, page, &SalienceConfig::default()).unwrap(); - - // The exact (verbatim-title) hit sorts ahead of the semantic one. - assert!(!candidates.is_empty()); - assert_eq!(candidates[0].tier, SalienceTier::Exact); - assert_eq!(candidates[0].score, 1.0); - assert!(candidates[0].refs.contains(&war)); - assert!(candidates[0].explanation.contains("Peloponnesian War")); - // Exact-first: no semantic candidate outranks it. - assert!(candidates - .iter() - .take_while(|c| c.tier == SalienceTier::Exact) - .all(|c| c.score == 1.0)); - } - - #[test] - fn no_connection_page_yields_zero_candidates() { - let mut cp = store(); - seed( - &mut cp, - "Greek Art Notes", - "marble sculpture athens acropolis parthenon frieze", - ); - // A page about something entirely unrelated, with no shared tokens and - // no verbatim title: zero candidates, so the overlay renders nothing. - let page = "Quarterly logistics throughput improved. \ - Warehouse forklift routing shortened dwell time across every depot."; - let candidates = salience(&cp, page, &SalienceConfig::default()).unwrap(); - assert!(candidates.is_empty(), "no connection means zero UI"); - } - - #[test] - fn budget_caps_and_dedups() { - let mut cp = store(); - seed( - &mut cp, - "Greek Art Notes", - "marble sculpture athens acropolis parthenon frieze carving", - ); - // Eight passages all sharing the note's tokens; budget of 3 caps them. - let mut page = String::new(); - for _ in 0..8 { - page.push_str( - "The marble sculpture athens acropolis parthenon frieze carving endures here. ", - ); - } - let config = SalienceConfig { - max_candidates: 3, - ..SalienceConfig::default() - }; - let candidates = salience(&cp, &page, &config).unwrap(); - assert_eq!(candidates.len(), 3, "capped to the budget"); - // Distinct spans (dedup by position). - let spans: HashSet<(u32, u32)> = candidates - .iter() - .map(|c| (c.anchor.position.start, c.anchor.position.end)) - .collect(); - assert_eq!(spans.len(), candidates.len(), "no duplicate spans"); - } - - #[test] - fn quiet_config_suppresses_the_semantic_tier() { - // D2-5 / D7-2: a page that only embeds-near a note (no verbatim title) - // yields a candidate in Active but nothing in Quiet. - let mut cp = store(); - seed( - &mut cp, - "Greek Art Notes", - "marble sculpture athens acropolis parthenon frieze carving", - ); - let page = - "The marble sculpture athens acropolis parthenon frieze carving still stands today."; - - let active = salience(&cp, page, &SalienceConfig::active()).unwrap(); - assert_eq!(active.len(), 1, "Active surfaces the semantic connection"); - - let quiet = salience(&cp, page, &SalienceConfig::quiet()).unwrap(); - assert!( - quiet.is_empty(), - "Quiet does not surface semantic connections" - ); - } - - #[test] - fn cache_hits_on_unchanged_page_and_times_the_miss() { - let mut cp = store(); - seed( - &mut cp, - "Greek Art Notes", - "marble sculpture athens acropolis parthenon frieze carving", - ); - let page = - "The marble sculpture athens acropolis parthenon frieze carving still stands today."; - let hash = "blake3:deadbeef"; - let mut cache = SalienceCache::new(); - - // First visit computes and caches (a miss). - let first = - salience_cached(&cp, page, hash, &SalienceConfig::default(), &mut cache).unwrap(); - assert_eq!(first.cache, CacheStatus::Miss); - assert_eq!(first.candidates.len(), 1); - assert_eq!(cache.len(), 1); - - // Revisit the same hash: served from cache, unaffected even though the - // store changed underneath, proving it did not recompute. - seed(&mut cp, "Distraction", "totally unrelated tokens zzz qqq"); - let second = - salience_cached(&cp, page, hash, &SalienceConfig::default(), &mut cache).unwrap(); - assert_eq!(second.cache, CacheStatus::Hit); - assert_eq!(second.elapsed_ms, 0); - assert_eq!( - second.candidates, first.candidates, - "cache returned the prior result" - ); - } - - #[test] - fn candidate_json_matches_the_ts_wire_shape() { - // Wire-parity guard (D2-1): snake_case tier tag, quote/position anchor, - // omitted-empty refs, matching what the TS SalienceCandidate expects. - let candidate = SalienceCandidate { - anchor: SalienceAnchor { - quote: TextQuoteSelector { - exact: "the sculptor of Athens".to_string(), - prefix: Some("was ".to_string()), - suffix: None, - }, - position: TextPositionSelector { start: 4, end: 26 }, - }, - tier: SalienceTier::Exact, - explanation: "names a saved record".to_string(), - score: 1.0, - refs: vec!["item:1".to_string()], - }; - let json = serde_json::to_value(&candidate).unwrap(); - assert_eq!(json["tier"], "exact"); - assert_eq!(json["anchor"]["quote"]["exact"], "the sculptor of Athens"); - assert_eq!(json["anchor"]["quote"]["prefix"], "was "); - assert!( - json["anchor"]["quote"].get("suffix").is_none(), - "absent context omitted" - ); - assert_eq!(json["anchor"]["position"]["start"], 4); - assert_eq!(json["refs"][0], "item:1"); - let back: SalienceCandidate = serde_json::from_value(json).unwrap(); - assert_eq!(back, candidate, "round-trips"); - } -} diff --git a/apps/commonplace-api/src/save_url.rs b/apps/commonplace-api/src/save_url.rs deleted file mode 100644 index 2df29beb..00000000 --- a/apps/commonplace-api/src/save_url.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! `saveUrl`: fetch a page through RustyWeb, then file it through the ingest -//! pipeline. SPEC-COMMONPLACE-SEARCH-STACK B7, surfaced by F4. -//! -//! Save is ingest. The page is fetched and extracted by -//! [`web_consume_to_graph`], and the resulting item is filed, embedded, -//! classified, and linked by [`IngestPipeline`] exactly like any other capture. -//! That is what makes the acceptance criterion hold: after `saveUrl`, a `find` -//! at Corpus scope and the existing `briefing` query both return the page, -//! because it is a real `Item` in the same store and not a special case. -//! -//! ## Why the crawl graph lands in a scratch store first -//! -//! `web_consume_to_graph` is async and takes `&mut S: GraphStore`. The consumer -//! store lives behind a `std::sync::Mutex`, whose guard is not `Send`, so it -//! cannot be held across the fetch await. The fetch therefore writes into a -//! scratch [`InMemoryGraphStore`], and the resulting nodes and edges are merged -//! into the consumer store under a single short lock afterwards. Nothing is -//! discarded: the page node, its extracted facts, and the `CrawlReceipt` all -//! arrive in the same graph the find lanes read. - -use commonplace::{ - BlobStore, Commonplace, EmbeddingGraphStore, IngestBody, IngestInput, IngestPipeline, Residency, -}; -use rustyred_thg_core::{GraphSnapshotSource, GraphStoreResult, InMemoryGraphStore}; -use rustyred_web::browser_engine::{web_consume_to_graph, WebConsumeRequest}; -use rustyred_web::fetch_cascade::{FetchCascade, FetchCascadeOptions}; - -/// Namespace crawl nodes are written under. Matches the RustyWeb default for -/// unverified open-web content. -const SAVE_NAMESPACE: &str = "open_web_unverified"; -const SAVE_USER_AGENT: &str = "CommonPlace/0.1 save-url"; -const SAVE_TIMEOUT_SECONDS: u64 = 20; -const SAVE_MAX_BYTES: usize = 5 * 1024 * 1024; - -/// What `saveUrl` gives back. `collection_name` is the real name the ingest -/// pipeline chose, never a placeholder: the F4 confirmation renders it verbatim. -#[derive(Clone, Debug, PartialEq)] -pub struct SaveUrlReceipt { - pub item_id: String, - pub collection_id: String, - pub collection_name: String, - pub title: String, - pub url: String, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct SaveUrlError { - pub code: String, - pub message: String, -} - -impl SaveUrlError { - fn new(code: &str, message: impl Into) -> Self { - Self { - code: code.to_string(), - message: message.into(), - } - } -} - -impl std::fmt::Display for SaveUrlError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "{}: {}", self.code, self.message) - } -} - -impl std::error::Error for SaveUrlError {} - -/// One fetched page, plus the graph the fetch produced. -pub struct FetchedPage { - pub url: String, - pub title: String, - pub text: String, - /// Crawl graph written by the fetch, to be merged into the consumer store. - pub graph: Option, -} - -/// Where a page comes from. -/// -/// An enum rather than a trait because the live path is async and adding -/// `async-trait` to carry one method would be a dependency for nothing. Tests -/// use [`PageSource::fixture`], production uses [`PageSource::live`]. -pub enum PageSource { - Live(Box), - Fixture(std::collections::BTreeMap), -} - -impl PageSource { - /// Live fetch cascade with the RustyWeb defaults for a single-page save. - pub fn live() -> Result { - let cascade = FetchCascade::new(FetchCascadeOptions::http2_only( - SAVE_USER_AGENT.to_string(), - SAVE_TIMEOUT_SECONDS, - )) - .map_err(|error| SaveUrlError::new("fetch_cascade_unavailable", error.to_string()))?; - Ok(Self::Live(Box::new(cascade))) - } - - /// Offline source: url to (title, text). Used by the acceptance tests so - /// they never reach the network. - pub fn fixture(pages: I) -> Self - where - I: IntoIterator, - U: Into, - T: Into, - X: Into, - { - Self::Fixture( - pages - .into_iter() - .map(|(url, title, text)| (url.into(), (title.into(), text.into()))) - .collect(), - ) - } - - async fn fetch(&self, url: &str, run_id: &str) -> Result { - match self { - Self::Live(cascade) => { - let mut scratch = InMemoryGraphStore::default(); - let request = WebConsumeRequest { - run_id: run_id.to_string(), - url: url.to_string(), - actor_id: String::new(), - namespace: SAVE_NAMESPACE.to_string(), - max_bytes: SAVE_MAX_BYTES, - ingest: true, - respect_robots: true, - }; - let receipt = web_consume_to_graph(&mut scratch, cascade, request) - .await - .map_err(|error| { - SaveUrlError::new("web_consume_failed", format!("{error:?}")) - })?; - let title = if receipt.extract.title.trim().is_empty() { - title_from_url(&receipt.url) - } else { - receipt.extract.title.clone() - }; - Ok(FetchedPage { - url: receipt.url, - title, - text: receipt.extract.text, - graph: Some(scratch), - }) - } - Self::Fixture(pages) => { - let (title, text) = pages.get(url).cloned().ok_or_else(|| { - SaveUrlError::new("page_not_found", format!("no fixture page for {url}")) - })?; - Ok(FetchedPage { - url: url.to_string(), - title, - text, - graph: None, - }) - } - } - } -} - -/// Last path segment of a url, humanized. Only used when the page carried no -/// title of its own. -fn title_from_url(url: &str) -> String { - let trimmed = url - .trim_end_matches('/') - .rsplit('/') - .next() - .unwrap_or(url) - .replace(['-', '_'], " "); - if trimmed.trim().is_empty() { - url.to_string() - } else { - trimmed - } -} - -/// Fetch `url` and file it. Returns the item id and the collection it landed in. -/// -/// The fetch happens before the store is locked; the graph merge and the ingest -/// happen together under one lock, so a reader never sees a page node without -/// its item. -pub async fn save_url( - store: &crate::schema::SharedStore, - source: &PageSource, - url: &str, - run_id: &str, -) -> Result -where - S: EmbeddingGraphStore + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let url = url.trim(); - if url.is_empty() { - return Err(SaveUrlError::new("invalid_url", "saveUrl requires a url")); - } - let page = source.fetch(url, run_id).await?; - - let mut guard = store - .lock() - .map_err(|_| SaveUrlError::new("store_lock_poisoned", "store lock poisoned"))?; - apply_page(&mut guard, page) -} - -/// Merge the crawl graph and file the item. Synchronous, so it is the whole of -/// the locked section. -fn apply_page( - cp: &mut Commonplace, - page: FetchedPage, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - if let Some(graph) = page.graph { - merge_graph(cp, &graph) - .map_err(|error| SaveUrlError::new("graph_merge_failed", format!("{error:?}")))?; - } - let input = IngestInput { - title: page.title.clone(), - body: IngestBody::Link { - url: page.url.clone(), - text: page.text, - }, - source: Some(page.url.clone()), - source_ref: None, - residency: Residency::default(), - tags: Vec::new(), - task: None, - remind_at_ms: None, - due_at_ms: None, - provenance: None, - }; - let receipt = IngestPipeline::default() - .ingest(cp, input) - .map_err(|error| SaveUrlError::new("ingest_failed", format!("{error:?}")))?; - Ok(SaveUrlReceipt { - item_id: receipt.item.id, - collection_id: receipt.collection.id, - collection_name: receipt.collection.name, - title: page.title, - url: page.url, - }) -} - -/// Copy every live node and edge from the scratch crawl graph into the consumer -/// store, so page facts and the crawl receipt are readable by the find lanes. -fn merge_graph(cp: &mut Commonplace, graph: &InMemoryGraphStore) -> GraphStoreResult<()> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let snapshot = graph.graph_snapshot()?; - let store = cp.store_mut(); - for node in snapshot.nodes.into_iter().filter(|node| !node.tombstone) { - store.upsert_node(node)?; - } - for edge in snapshot.edges.into_iter().filter(|edge| !edge.tombstone) { - store.upsert_edge(edge)?; - } - Ok(()) -} diff --git a/apps/commonplace-api/src/schema/find.rs b/apps/commonplace-api/src/schema/find.rs deleted file mode 100644 index 32c4fd1b..00000000 --- a/apps/commonplace-api/src/schema/find.rs +++ /dev/null @@ -1,385 +0,0 @@ -//! GraphQL projection of the composed find (SPEC-COMMONPLACE-SEARCH-STACK B3, -//! surfaced by B7). -//! -//! These types are a thin wire skin over `rustyred_thg_find`. They exist so the -//! Rust shapes stay snake_case internally while the wire stays camelCase, and -//! so the same executor can serve this door and the MCP door in B8 without -//! either one owning the vocabulary. Field names match the pinned TypeScript -//! contract at `packages/block-view-contracts/src/search-stack.ts`. - -use async_graphql::{Enum, InputObject, SimpleObject}; -use rustyred_thg_find::{ - EdgeRef, FindHit, FindRequest, FindResponse, FindResult, FindScope, GraphRelation, Lane, - LaneBudget, -}; - -/// Which match lane produced a hit. -#[derive(Copy, Clone, Debug, Enum, Eq, PartialEq)] -#[graphql(name = "FindLane")] -pub enum FindLaneGql { - Exact, - Lexical, - Semantic, - Structural, -} - -impl From for FindLaneGql { - fn from(lane: Lane) -> Self { - match lane { - Lane::Exact => Self::Exact, - Lane::Lexical => Self::Lexical, - Lane::Semantic => Self::Semantic, - Lane::Structural => Self::Structural, - } - } -} - -impl From for Lane { - fn from(lane: FindLaneGql) -> Self { - match lane { - FindLaneGql::Exact => Self::Exact, - FindLaneGql::Lexical => Self::Lexical, - FindLaneGql::Semantic => Self::Semantic, - FindLaneGql::Structural => Self::Structural, - } - } -} - -/// Scope kinds, in widening order. -#[derive(Copy, Clone, Debug, Enum, Eq, PartialEq)] -#[graphql(name = "FindScopeKind")] -pub enum FindScopeKindGql { - Page, - Session, - Corpus, - Web, -} - -/// How a result stands against what the person already knows. Annotation, never -/// a filter: `Orphan` is an honest answer and the result is still returned. -#[derive(Copy, Clone, Debug, Enum, Eq, PartialEq)] -#[graphql(name = "GraphRelation")] -pub enum GraphRelationGql { - Known, - Extends, - Contradicts, - Orphan, -} - -impl From for GraphRelationGql { - fn from(relation: GraphRelation) -> Self { - match relation { - GraphRelation::Known => Self::Known, - GraphRelation::Extends => Self::Extends, - GraphRelation::Contradicts => Self::Contradicts, - GraphRelation::Orphan => Self::Orphan, - } - } -} - -/// One scope in the widening ladder. GraphQL inputs have no tagged unions, so -/// the kind selects which of the optional id fields is read. -#[derive(Clone, Debug, InputObject)] -pub struct FindScopeInput { - pub kind: FindScopeKindGql, - /// Required when `kind` is `PAGE`. - pub node_id: Option, - /// Required when `kind` is `SESSION`. - pub node_ids: Option>, -} - -impl FindScopeInput { - pub fn into_scope(self) -> Result { - match self.kind { - FindScopeKindGql::Page => self - .node_id - .filter(|id| !id.trim().is_empty()) - .map(FindScope::Page) - .ok_or_else(|| "page scope requires nodeId".to_string()), - FindScopeKindGql::Session => { - let ids = self.node_ids.unwrap_or_default(); - if ids.is_empty() { - Err("session scope requires nodeIds".to_string()) - } else { - Ok(FindScope::Session(ids)) - } - } - FindScopeKindGql::Corpus => Ok(FindScope::Corpus), - FindScopeKindGql::Web => Ok(FindScope::Web), - } - } -} - -/// Byte range inside the document's indexed text property. -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "ByteRange")] -pub struct ByteRangeGql { - pub start: i32, - pub end: i32, -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "FindScopeRef")] -pub struct FindScopeRefGql { - pub kind: FindScopeKindGql, - pub node_id: Option, - pub node_ids: Option>, -} - -impl From<&FindScope> for FindScopeRefGql { - fn from(scope: &FindScope) -> Self { - match scope { - FindScope::Page(id) => Self { - kind: FindScopeKindGql::Page, - node_id: Some(id.clone()), - node_ids: None, - }, - FindScope::Session(ids) => Self { - kind: FindScopeKindGql::Session, - node_id: None, - node_ids: Some(ids.clone()), - }, - FindScope::Corpus => Self { - kind: FindScopeKindGql::Corpus, - node_id: None, - node_ids: None, - }, - FindScope::Web => Self { - kind: FindScopeKindGql::Web, - node_id: None, - node_ids: None, - }, - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "EdgeRef")] -pub struct EdgeRefGql { - pub id: String, - pub from_id: String, - pub to_id: String, - #[graphql(name = "type")] - pub edge_type: String, - pub confidence: Option, -} - -impl From<&EdgeRef> for EdgeRefGql { - fn from(edge: &EdgeRef) -> Self { - Self { - id: edge.id.clone(), - from_id: edge.from_id.clone(), - to_id: edge.to_id.clone(), - edge_type: edge.edge_type.clone(), - confidence: edge.confidence, - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "FindHit")] -pub struct FindHitGql { - pub doc: String, - pub byte_range: ByteRangeGql, - pub lane: FindLaneGql, - pub scope: FindScopeRefGql, - pub snippet: Option, - pub title: Option, - pub source: Option, -} - -impl From<&FindHit> for FindHitGql { - fn from(hit: &FindHit) -> Self { - Self { - doc: hit.doc.clone(), - byte_range: ByteRangeGql { - start: hit.byte_range.start as i32, - end: hit.byte_range.end as i32, - }, - lane: hit.lane.into(), - scope: FindScopeRefGql::from(&hit.scope), - snippet: hit.snippet.clone(), - title: hit.title.clone(), - source: hit.source.clone(), - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "FindResult")] -pub struct FindResultGql { - pub hit: FindHitGql, - pub score: f64, - pub relation: GraphRelationGql, - pub edges: Vec, -} - -impl From<&FindResult> for FindResultGql { - fn from(result: &FindResult) -> Self { - Self { - hit: FindHitGql::from(&result.hit), - score: result.score as f64, - relation: result.relation.into(), - edges: result.edges.iter().map(EdgeRefGql::from).collect(), - } - } -} - -/// Per-lane accounting, so a surface can say which lane went quiet and why. -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "LaneReceipt")] -pub struct LaneReceiptGql { - pub lane: FindLaneGql, - pub seeded: i32, - pub admitted: i32, - pub degraded_reason: Option, -} - -impl From<&LaneBudget> for LaneReceiptGql { - fn from(budget: &LaneBudget) -> Self { - Self { - lane: budget.lane.into(), - seeded: budget.seeded as i32, - admitted: budget.admitted as i32, - degraded_reason: budget.degraded_reason.clone(), - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "FindResponse")] -pub struct FindResponseGql { - pub query: String, - pub results: Vec, - pub lanes: Vec, - pub scopes_searched: Vec, - pub lambda: f64, - /// Stable identity for the retrieval, for provenance and cache keys. - pub retrieval_ref: String, -} - -impl From for FindResponseGql { - fn from(response: FindResponse) -> Self { - Self { - query: response.query, - results: response.results.iter().map(FindResultGql::from).collect(), - lanes: response.lanes.iter().map(LaneReceiptGql::from).collect(), - scopes_searched: response.scopes_searched, - lambda: response.lambda as f64, - retrieval_ref: response.retrieval_ref, - } - } -} - -/// Build the executor request from resolver arguments, applying the same -/// clamps the executor would apply anyway so the response reports what was -/// actually run. -pub fn build_request( - query: String, - scopes: Option>, - lanes: Option>, - k: Option, - lambda: Option, -) -> Result { - let scopes = match scopes { - Some(scopes) if !scopes.is_empty() => scopes - .into_iter() - .map(FindScopeInput::into_scope) - .collect::, _>>()?, - _ => vec![FindScope::Corpus], - }; - let lanes = match lanes { - Some(lanes) if !lanes.is_empty() => lanes.into_iter().map(Lane::from).collect(), - _ => Lane::ALL.to_vec(), - }; - Ok(FindRequest { - query, - scopes, - lanes, - k: k.unwrap_or(10).clamp(1, 200) as usize, - lambda: lambda.unwrap_or(0.7).clamp(0.0, 1.0) as f32, - }) -} - -/// Receipt for `saveUrl`. `collectionName` is the real name the ingest pipeline -/// chose; the F4 confirmation renders it verbatim and never a placeholder. -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "SaveUrlReceipt")] -pub struct SaveUrlReceiptGql { - pub item_id: String, - pub collection_id: String, - pub collection_name: String, - pub title: String, - pub url: String, -} - -impl From for SaveUrlReceiptGql { - fn from(receipt: crate::save_url::SaveUrlReceipt) -> Self { - Self { - item_id: receipt.item_id, - collection_id: receipt.collection_id, - collection_name: receipt.collection_name, - title: receipt.title, - url: receipt.url, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn page_scope_requires_a_node_id() { - let input = FindScopeInput { - kind: FindScopeKindGql::Page, - node_id: None, - node_ids: None, - }; - assert!(input.into_scope().is_err()); - } - - #[test] - fn session_scope_requires_node_ids() { - let input = FindScopeInput { - kind: FindScopeKindGql::Session, - node_id: None, - node_ids: Some(Vec::new()), - }; - assert!(input.into_scope().is_err()); - } - - #[test] - fn scopes_default_to_corpus_and_lanes_to_all() { - let request = build_request("candidate".into(), None, None, None, None).expect("request"); - assert_eq!(request.scopes, vec![FindScope::Corpus]); - assert_eq!(request.lanes.len(), 4); - assert_eq!(request.k, 10); - } - - #[test] - fn k_and_lambda_are_clamped_not_trusted() { - let request = build_request("q".into(), None, None, Some(-5), Some(9.0)).expect("request"); - assert_eq!(request.k, 1); - assert_eq!(request.lambda, 1.0); - let request = - build_request("q".into(), None, None, Some(9_999), Some(-1.0)).expect("request"); - assert_eq!(request.k, 200); - assert_eq!(request.lambda, 0.0); - } - - #[test] - fn lane_round_trips_through_the_wire_enum() { - for lane in Lane::ALL { - assert_eq!(Lane::from(FindLaneGql::from(lane)), lane); - } - } - - #[test] - fn scope_ref_carries_its_ids() { - let session = FindScope::Session(vec!["a".into(), "b".into()]); - let projected = FindScopeRefGql::from(&session); - assert_eq!(projected.kind, FindScopeKindGql::Session); - assert_eq!(projected.node_ids.as_deref().map(<[String]>::len), Some(2)); - } -} diff --git a/apps/commonplace-api/src/schema/mod.rs b/apps/commonplace-api/src/schema/mod.rs deleted file mode 100644 index 8f14f3d8..00000000 --- a/apps/commonplace-api/src/schema/mod.rs +++ /dev/null @@ -1,3353 +0,0 @@ -//! The consumer GraphQL profile (plan unit F3). -//! -//! Exposes the CommonPlace object model as a typed schema: queries for items, -//! collections, and similarity search; mutations for ingest and edit. Every -//! resolver authorizes the request's API key before touching the store, so an -//! invalid key is rejected before any data access. -//! -//! The store is fixed to the in-memory backing here so the seam is fully -//! testable in-process; the identical schema runs over the durable -//! `RedCoreGraphStore` + `DiskObjectStore` backing (both impl the traits this -//! needs) by swapping the type alias, which is the named follow-up for the -//! durable self-hosted binary. - -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::{Arc, Mutex}; - -use async_graphql::{ - Context, EmptySubscription, Enum, Error, InputObject, Json as GqlJson, Object, Result, Schema, - SimpleObject, -}; -use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; -use commonplace::{ - annotation_from_item, Anchor, Annotation, AuthorKind, BlobStore, Collection, CollectionKind, - Commonplace, EmbeddingGraphStore, InMemoryBlobStore, IngestInput, IngestPipeline, Item, - ItemBody, ItemKind, Residency, Resolution, SourceRef, COLLECTION_LABEL, -}; -use rustyred_thg_core::{ - DiskObjectStore, GraphSnapshotSource, InMemoryGraphStore, NodeQuery, RedCoreGraphStore, -}; -use serde_json::{json, Value}; -use theorem_harness_core::GroundedClaim; -use theorem_harness_runtime::{ - run_configured_composed_agent, run_configured_composed_agent_with_claims, - ComposedAgentRunResult, ProviderHeadInvoker, DEFAULT_BINDING_ID, -}; -use yrs::updates::decoder::Decode; -use yrs::{Doc, ReadTxn, StateVector, Transact, Update}; - -pub mod find; -pub mod scatter; - -use crate::auth::{ApiKeyRegistry, ApiKeyToken, Principal}; -use crate::briefing::{briefing as run_briefing, Briefing, BriefingConfig, ConnectedItem}; -use crate::discover::{discover as run_discover, CandidateLink, DiscoverConfig}; -use crate::find::{FindConfig, FindIndexCache}; -use crate::growth::{load_growth_snapshot_from_env, GrowthSnapshotResultGql}; -use crate::organize::{ - organize as run_organize, DailyProgress, OrganizeConfig, OrganizeFiled, OrganizeGroup, - OrganizeItem, OrganizeSnapshot, OrganizedToday, Subtask, Timeframe, -}; -use crate::portability::{self, ExportDocument}; -use crate::publish; -use crate::reconstruction; -use crate::retrieve::{ - answer_from_provenance, retrieve_grounding_measured, AnswerKind, AnswerModel, AskConfig, - AskResult, NoModel, PprExpansionMeasurement, RetrievedItem, -}; -use crate::salience::{salience as run_salience, SalienceCandidate, SalienceConfig, SalienceTier}; -use crate::save_url::{save_url as run_save_url, PageSource}; -use find::{ - build_request as build_find_request, FindLaneGql, FindResponseGql, FindScopeInput, - SaveUrlReceiptGql, -}; -use rustyred_thg_find::{expand as run_expand, scatter as run_scatter}; -use scatter::{build_request as build_scatter_request, parse_aspect_id, ScatterResponseGql}; - -/// The default in-memory store backing (tests + the no-data-dir binary path). -pub type ApiStore = Commonplace; -/// A shared, lockable instance store, generic over the backing. -pub type SharedStore = Arc>>; -/// The in-memory shared store. -pub type InMemoryShared = SharedStore; -/// The durable shared store (RedCore + disk) for a self-hosted instance. -pub type DurableShared = SharedStore; -/// The consumer schema over the in-memory backing (default / tests). -pub type ConsumerSchema = Schema< - Query, - Mutation, - EmptySubscription, ->; -/// The consumer schema over the durable RedCore + disk backing. -pub type DurableSchema = Schema< - Query, - Mutation, - EmptySubscription, ->; - -const PAGE_CRDT_SOURCE: &str = "commonplace.page_crdt_snapshot"; -const PAGE_CRDT_ROLE: &str = "page_crdt_snapshot"; -const PAGE_CRDT_ENCODING: &str = "yjs-update-v1"; -const PAGE_CRDT_MIME: &str = "application/vnd.commonplace.yjs-update-v1"; - -/// An item, in the consumer API shape. -#[derive(SimpleObject)] -pub struct ItemGql { - pub id: String, - pub kind: String, - pub title: String, - pub body_text: Option, - pub blob_hash: Option, - pub mime: Option, - pub source: Option, - pub residency: String, - pub tags: Vec, - pub collections: Vec, - pub classification: Option, - pub status: Option, - pub priority: Option, - pub due_at_ms: Option, - /// Reminder instant in epoch ms (PT-008): explicit or echoed from a - /// natural-language phrase at ingest. - pub remind_at_ms: Option, - pub path: Option, - pub extra: GqlJson, - pub created_at_ms: i64, - pub updated_at_ms: i64, -} - -impl From for ItemGql { - fn from(item: Item) -> Self { - let (body_text, blob_hash, mime) = match item.body { - ItemBody::Inline { text } => (Some(text), None, None), - ItemBody::Blob { - content_hash, mime, .. - } => (None, Some(content_hash), mime), - ItemBody::Empty => (None, None, None), - }; - let path = item - .extra - .get("path") - .or_else(|| item.extra.get("folder_path")) - .and_then(|value| value.as_str()) - .map(str::to_string); - Self { - id: item.id, - kind: item.kind.as_str().to_string(), - title: item.title, - body_text, - blob_hash, - mime, - source: item.source, - residency: item.residency.as_str().to_string(), - tags: item.tags, - collections: item.collections, - classification: item.classification, - status: item.status, - priority: item.priority, - due_at_ms: item.due_at_ms, - remind_at_ms: item.remind_at_ms, - path, - extra: GqlJson(Value::Object(item.extra)), - created_at_ms: item.created_at_ms, - updated_at_ms: item.updated_at_ms, - } - } -} - -/// A collection, in the consumer API shape. -#[derive(SimpleObject)] -pub struct CollectionGql { - pub id: String, - pub name: String, - pub kind: String, - pub identifier: Option, - pub description: Option, - pub start_at_ms: Option, - pub end_at_ms: Option, - pub color: Option, - pub sort_order: Option, - pub feature_flags: GqlJson, - pub created_at_ms: i64, -} - -/// A durable reconstruction stage receipt, owned by Theorem's reconstruction -/// graph and exposed through the CommonPlace GraphQL facade. -#[derive(SimpleObject)] -pub struct ReconstructionStageReceiptGql { - pub id: String, - pub stage: String, - pub generation_stamp: String, - pub payload: GqlJson, -} - -/// A provenance-bearing atom emitted by a durable reconstruction run. -#[derive(SimpleObject)] -pub struct ReconstructionAtomGql { - pub id: String, - pub atom_id: String, - pub provenance: GqlJson, - pub payload: GqlJson, -} - -/// The typed run shape consumed by the CommonPlace reconstruction viewer. -#[derive(SimpleObject)] -pub struct ReconstructionRunGql { - pub id: String, - pub domain: String, - pub subject_id: String, - pub stage_receipts: Vec, - pub atoms: Vec, -} - -#[derive(InputObject)] -pub struct GenerateReconstructionInputGql { - pub domain: String, - pub target: String, - pub budget: Option, -} - -#[derive(SimpleObject)] -pub struct GenerateReconstructionResultGql { - pub run_id: String, - pub scene_package_ref: String, - pub result: GqlJson, -} - -impl From for CollectionGql { - fn from(collection: Collection) -> Self { - Self { - id: collection.id, - name: collection.name, - kind: collection.kind.as_str().to_string(), - identifier: collection.identifier, - description: collection.description, - start_at_ms: collection.start_at_ms, - end_at_ms: collection.end_at_ms, - color: collection.color, - sort_order: collection.sort_order, - feature_flags: GqlJson(json!(collection.feature_flags)), - created_at_ms: collection.created_at_ms, - } - } -} - -#[derive(SimpleObject)] -pub struct PmLabelGql { - pub id: String, - pub name: String, - pub color: Option, -} - -#[derive(SimpleObject)] -pub struct PmStateGql { - pub id: String, - pub name: String, - pub group: String, - pub sort_order: i32, -} - -impl From for PmStateGql { - fn from(item: Item) -> Self { - let group = extra_str(&item, "group").unwrap_or_else(|| "unstarted".to_string()); - let sort_order = extra_i64(&item, "sort_order").unwrap_or(0) as i32; - Self { - id: item.id, - name: item.title, - group, - sort_order, - } - } -} - -#[derive(SimpleObject)] -pub struct PmWorkItemGql { - pub item: ItemGql, - pub sequence_id: Option, - pub state: Option, - pub estimate_point: Option, - pub project_ids: Vec, - pub cycle_ids: Vec, - pub module_ids: Vec, - pub about_ids: Vec, - pub comment_count: i32, - pub worklog_count: i32, - pub total_worklog_duration_ms: i64, -} - -#[derive(SimpleObject)] -pub struct PmProjectGql { - pub collection: CollectionGql, - pub states: Vec, - pub cycles: Vec, - pub modules: Vec, - pub labels: Vec, - pub work_item_count: i32, - pub open_item_count: i32, -} - -#[derive(SimpleObject)] -pub struct PmOverviewGql { - pub projects: Vec, - pub work_items: Vec, - pub stickies: Vec, - pub pages: Vec, -} - -#[derive(SimpleObject)] -pub struct PageCrdtSnapshotGql { - pub page_id: String, - pub item: ItemGql, - pub update_base64: String, - pub encoding: String, - pub blob_hash: String, - pub byte_len: i32, - pub stored_at_ms: i64, -} - -/// A co-annotation thread item in the GraphQL wire shape consumed by -/// `@commonplace/coannotate`. -#[derive(SimpleObject)] -pub struct AnnotationGql { - pub id: String, - pub target_id: Option, - pub author: Option, - pub author_kind: Option, - pub anchor: Option>, - pub body: String, - pub resolved: bool, - pub resolution: Option, - pub created_at_ms: i64, -} - -#[derive(SimpleObject)] -pub struct AnnotationResolutionGql { - pub by: String, - pub receipt: Option, -} - -#[derive(InputObject)] -#[graphql(name = "CreateAnnotationInput")] -pub struct CreateAnnotationInputGql { - pub target_id: String, - pub body: String, - pub anchor: GqlJson, - pub author: Option, - pub author_kind: String, -} - -#[derive(InputObject)] -#[graphql(name = "ReplyAnnotationInput")] -pub struct ReplyAnnotationInputGql { - pub parent_id: String, - pub body: String, - pub author: Option, - pub author_kind: String, -} - -#[derive(InputObject)] -#[graphql(name = "ResolveAnnotationInput")] -pub struct ResolveAnnotationInputGql { - pub id: String, - pub by: String, - pub receipt: Option, -} - -/// A similarity-search hit. -#[derive(SimpleObject)] -pub struct SearchHitGql { - pub item: ItemGql, - pub score: f64, -} - -/// One row in the client-side `embedding_space` table consumed by Embedding Atlas. -#[derive(Clone, SimpleObject)] -pub struct EmbeddingSpaceRowGql { - pub identifier: String, - pub x: f64, - pub y: f64, - /// Integer category id, because Embedding Atlas expects 0-indexed categories. - pub category: i32, - pub category_label: String, - pub text: String, - pub created_ms: i64, - pub community_id: String, - pub epistemic_status: String, -} - -/// The server-side embedding projection plus metadata for the frontend table. -#[derive(SimpleObject)] -pub struct EmbeddingSpaceGql { - pub table: String, - pub projection: String, - pub total: i32, - pub rows: Vec, -} - -/// A nearest-neighbor hit for object-seeded vector navigation. -#[derive(SimpleObject)] -pub struct VectorNeighborGql { - pub row: EmbeddingSpaceRowGql, - pub score: f64, -} - -/// Input for the auto-structuring ingest mutation. -#[derive(InputObject)] -pub struct SourceRefInputGql { - pub source: String, - pub external_id: String, -} - -#[derive(InputObject)] -pub struct CollectorUploadProvenanceInputGql { - pub filename: String, - pub media_type: String, - pub source: String, -} - -#[derive(InputObject)] -pub struct CollectorServiceFactsInputGql { - pub parser: Option, - pub media_type: Option, - pub byte_length: Option, - pub page_count: Option, -} - -#[derive(InputObject)] -pub struct CollectorDocumentProvenanceInputGql { - pub index: i32, - pub document_digest: String, - pub parser_document_id: Option, - pub doc_author: Option, - pub description: Option, - pub published: Option, - pub word_count: Option, - pub token_count_estimate: Option, -} - -#[derive(InputObject)] -pub struct CollectorProvenanceInputGql { - pub kind: String, - pub correlation_id: String, - pub upload: CollectorUploadProvenanceInputGql, - pub service_facts: CollectorServiceFactsInputGql, - pub document: CollectorDocumentProvenanceInputGql, -} - -/// Input for the auto-structuring ingest mutation. -#[derive(InputObject)] -pub struct IngestInputGql { - pub title: String, - pub text: String, - /// One of file/note/link/image/doc, or any custom kind. Defaults to note. - pub kind: Option, - pub tags: Option>, - pub source: Option, - /// Stable collector/source identity. Repeating the same pair updates the - /// existing graph item instead of creating a duplicate. - pub source_ref: Option, - pub residency: Option, - /// Explicit reminder instant (epoch ms). Wins over the server-side - /// natural-language reminder parse (PT-008). - pub remind_at_ms: Option, - /// Explicit due instant (epoch ms). - pub due_at_ms: Option, - /// Typed source/parser audit trail. The resolver binds it to the - /// authenticated API principal and stable source reference before storage. - pub provenance: Option, -} - -#[derive(InputObject)] -pub struct CreateProjectInputGql { - pub name: String, - pub identifier: String, - pub description: Option, - pub color: Option, - pub default_states: Option, -} - -#[derive(InputObject)] -pub struct CreateStateInputGql { - pub project_id: String, - pub name: String, - pub group: String, - pub sort_order: Option, -} - -#[derive(InputObject)] -pub struct CreateWorkItemInputGql { - pub title: String, - pub description: Option, - pub project_id: Option, - pub state_id: Option, - pub priority: Option, - pub due_at_ms: Option, - pub estimate_point: Option, - pub sequence_id: Option, - pub kind: Option, -} - -#[derive(InputObject)] -pub struct CreateCycleInputGql { - pub project_id: String, - pub name: String, - pub start_at_ms: i64, - pub end_at_ms: i64, -} - -#[derive(InputObject)] -pub struct CreateModuleInputGql { - pub project_id: String, - pub name: String, -} - -#[derive(InputObject)] -pub struct CreateCommentInputGql { - pub item_id: String, - pub body: String, - pub author_id: Option, -} - -#[derive(InputObject)] -pub struct LogWorkInputGql { - pub task_id: String, - pub duration_ms: i64, - pub logged_by: Option, - pub description: Option, -} - -#[derive(InputObject)] -pub struct CreateStickyInputGql { - pub owner_id: Option, - pub title: String, - pub description: Option, - pub color: Option, - pub background_color: Option, - pub sort_order: Option, -} - -#[derive(InputObject)] -pub struct CreatePageInputGql { - pub project_id: Option, - pub about_item_id: Option, - pub title: String, - pub body: Option, - pub tags: Option>, -} - -#[derive(InputObject)] -pub struct SavePageInputGql { - pub id: String, - pub title: Option, - pub body: Option, - pub tags: Option>, -} - -#[derive(InputObject)] -pub struct StorePageCrdtSnapshotInputGql { - pub page_id: String, - pub update_base64: String, - pub encoding: Option, -} - -/// How an ask answer was produced. -#[derive(Enum, Copy, Clone, Debug, Eq, PartialEq)] -pub enum AnswerKindGql { - /// Synthesized by a configured generative model. - Model, - /// Extracted from the retrieved items (no generative model configured). - Extractive, - /// No matching items were found. - Empty, -} - -impl From for AnswerKindGql { - fn from(kind: AnswerKind) -> Self { - match kind { - AnswerKind::Model => AnswerKindGql::Model, - AnswerKind::Extractive => AnswerKindGql::Extractive, - AnswerKind::Empty => AnswerKindGql::Empty, - } - } -} - -/// One grounding item behind an answer. -#[derive(SimpleObject)] -pub struct ProvenanceGql { - pub item: ItemGql, - pub score: f64, - /// Which retrieval arms surfaced this item (vector / lexical / graph). - pub arms: Vec, -} - -impl From for ProvenanceGql { - fn from(hit: RetrievedItem) -> Self { - Self { - item: ItemGql::from(hit.item), - score: hit.score, - arms: hit.arms, - } - } -} - -/// Measured contribution of graph expansion beyond flat retrieval. -#[derive(SimpleObject)] -pub struct PprExpansionMeasurementGql { - pub seed_count: i32, - pub flat_candidate_count: i32, - pub ppr_candidate_count: i32, - pub ppr_only_candidate_count: i32, -} - -impl TryFrom for PprExpansionMeasurementGql { - type Error = Error; - - fn try_from(measurement: PprExpansionMeasurement) -> std::result::Result { - Ok(Self { - seed_count: graphql_int_from_usize(measurement.seed_count, "seed_count")?, - flat_candidate_count: graphql_int_from_usize( - measurement.flat_candidate_count, - "flat_candidate_count", - )?, - ppr_candidate_count: graphql_int_from_usize( - measurement.ppr_candidate_count, - "ppr_candidate_count", - )?, - ppr_only_candidate_count: graphql_int_from_usize( - measurement.ppr_only_candidate_count, - "ppr_only_candidate_count", - )?, - }) - } -} - -/// An answer grounded in the user's items, each traceable to its source. -#[derive(SimpleObject)] -pub struct AskResultGql { - pub answer: String, - pub answer_kind: AnswerKindGql, - pub provenance: Vec, - pub ppr_expansion: PprExpansionMeasurementGql, -} - -impl TryFrom for AskResultGql { - type Error = Error; - - fn try_from(result: AskResult) -> std::result::Result { - Ok(Self { - answer: result.answer, - answer_kind: AnswerKindGql::from(result.answer_kind), - provenance: result - .provenance - .into_iter() - .map(ProvenanceGql::from) - .collect(), - ppr_expansion: PprExpansionMeasurementGql::try_from(result.ppr_expansion)?, - }) - } -} - -/// Evidence handed to the composed Theorem agent. -#[derive(Clone, InputObject)] -pub struct TheoremAgentClaimInput { - pub text: String, - pub provenance: String, -} - -/// A published claim from the composed Theorem agent. -#[derive(Clone, SimpleObject)] -pub struct TheoremAgentClaimGql { - pub text: String, - pub provenance: String, -} - -impl From for TheoremAgentClaimGql { - fn from(claim: GroundedClaim) -> Self { - Self { - text: claim.text, - provenance: claim.provenance, - } - } -} - -/// A composed Theorem agent run over the configured API heads. -#[derive(SimpleObject)] -pub struct TheoremAgentRunGql { - pub answer: String, - pub answer_kind: AnswerKindGql, - pub binding_id: String, - pub run_id: String, - pub heads: Vec, - pub claims: Vec, - pub alignment_verdict: GqlJson, - pub evidence_count: i32, -} - -impl TheoremAgentRunGql { - fn from_composed(result: ComposedAgentRunResult, evidence_count: i32) -> Self { - let claims: Vec = result - .published_claims - .iter() - .cloned() - .map(TheoremAgentClaimGql::from) - .collect(); - let answer = theorem_agent_answer(&result, &claims); - Self { - answer_kind: if answer.is_empty() { - AnswerKindGql::Empty - } else { - AnswerKindGql::Model - }, - answer, - binding_id: result.binding_id, - run_id: result.run_id, - heads: result.consensus_head_set, - claims, - alignment_verdict: GqlJson(result.alignment_verdict), - evidence_count, - } - } -} - -fn normalize_theorem_agent_claims(claims: Vec) -> Vec { - claims - .into_iter() - .filter_map(|claim| { - let text = claim.text.trim(); - let provenance = claim.provenance.trim(); - if text.is_empty() || provenance.is_empty() { - None - } else { - Some(GroundedClaim::new(text, provenance)) - } - }) - .collect() -} - -fn theorem_agent_answer( - result: &ComposedAgentRunResult, - claims: &[TheoremAgentClaimGql], -) -> String { - if !claims.is_empty() { - return claims - .iter() - .map(|claim| claim.text.as_str()) - .collect::>() - .join("\n\n") - .trim() - .to_string(); - } - - let Some(receipt) = result.invocation_receipts.last() else { - return String::new(); - }; - if let Some(text) = receipt.payload.get("text").and_then(Value::as_str) { - let text = strip_claims_json(text); - if !text.is_empty() { - return text; - } - } - strip_claims_json(&receipt.output_summary) -} - -fn strip_claims_json(value: &str) -> String { - let lowered = value.to_ascii_lowercase(); - match lowered.find("claims json:") { - Some(marker) => { - let prefix = value[..marker].trim(); - if prefix.is_empty() { - value.trim().to_string() - } else { - prefix.to_string() - } - } - None => value.trim().to_string(), - } -} - -/// An item plus how it connects to the rest of the store. -#[derive(SimpleObject)] -pub struct ConnectedItemGql { - pub item: ItemGql, - pub connections: i32, - pub related: Vec, -} - -impl From for ConnectedItemGql { - fn from(connected: ConnectedItem) -> Self { - Self { - item: ItemGql::from(connected.item), - connections: connected.connections as i32, - related: connected.related.into_iter().map(ItemGql::from).collect(), - } - } -} - -/// Proactive briefing over the store: what is new, what connects, what is open. -#[derive(SimpleObject)] -pub struct BriefingGql { - pub recent: Vec, - pub newly_connected: Vec, - pub open_threads: Vec, -} - -impl From for BriefingGql { - fn from(briefing: Briefing) -> Self { - Self { - recent: briefing.recent.into_iter().map(ItemGql::from).collect(), - newly_connected: briefing - .newly_connected - .into_iter() - .map(ConnectedItemGql::from) - .collect(), - open_threads: briefing - .open_threads - .into_iter() - .map(ItemGql::from) - .collect(), - } - } -} - -/// A proposed connection between two not-yet-linked items. -#[derive(SimpleObject)] -pub struct CandidateLinkGql { - pub a: ItemGql, - pub b: ItemGql, - pub similarity: f64, - pub reason: String, -} - -impl From for CandidateLinkGql { - fn from(link: CandidateLink) -> Self { - Self { - a: ItemGql::from(link.a), - b: ItemGql::from(link.b), - similarity: link.similarity, - reason: link.reason, - } - } -} - -/// A margin-recall salience candidate (D2, HANDOFF-MARGIN-RECALL), in the consumer -/// API shape. The anchor is flattened to the quote plus its resolved character -/// range; `tier` is `"exact"` or `"semantic"`; `refs` are the openable records. -#[derive(SimpleObject)] -pub struct SalienceCandidateGql { - pub quote_exact: String, - pub quote_prefix: Option, - pub quote_suffix: Option, - pub position_start: i32, - pub position_end: i32, - pub tier: String, - pub explanation: String, - pub score: f64, - pub refs: Vec, -} - -impl From for SalienceCandidateGql { - fn from(candidate: SalienceCandidate) -> Self { - Self { - quote_exact: candidate.anchor.quote.exact, - quote_prefix: candidate.anchor.quote.prefix, - quote_suffix: candidate.anchor.quote.suffix, - position_start: candidate.anchor.position.start as i32, - position_end: candidate.anchor.position.end as i32, - tier: match candidate.tier { - SalienceTier::Exact => "exact", - SalienceTier::Semantic => "semantic", - } - .to_string(), - explanation: candidate.explanation, - score: candidate.score, - refs: candidate.refs, - } - } -} - -/// Serialization format for export. -#[derive(Enum, Copy, Clone, Debug, Eq, PartialEq)] -pub enum ExportFormat { - /// Lossless JSON (reimports without loss). - Json, - /// Human-readable markdown (one-way rendering). - Markdown, -} - -/// What an import wrote. -#[derive(SimpleObject)] -pub struct ImportResultGql { - pub imported: i32, - pub collections: i32, -} - -/// A next-best candidate collection for an item. -#[derive(SimpleObject)] -pub struct OrganizeAlternativeGql { - pub collection_id: String, - pub label: String, -} - -/// The engine's classification verdict for an organize item. -#[derive(SimpleObject)] -pub struct OrganizeClassificationGql { - pub target_collection_id: Option, - pub target_collection_label: Option, - pub confidence: f64, - pub alternatives: Vec, -} - -/// A checkbox subtask parsed from a task item's body. -#[derive(SimpleObject)] -pub struct SubtaskGql { - pub text: String, - pub done: bool, -} - -impl From for SubtaskGql { - fn from(subtask: Subtask) -> Self { - Self { - text: subtask.text, - done: subtask.done, - } - } -} - -/// An item in the organize surface, with its classification attached. -#[derive(SimpleObject)] -pub struct OrganizeItemGql { - pub id: String, - pub kind: String, - pub title: String, - pub preview: String, - pub source: String, - /// ISO-8601 UTC of the item's updated_at_ms. - pub arrived_at: String, - pub classification: OrganizeClassificationGql, - pub time_sensitive: bool, - pub expected_action: Option, - /// Checkbox subtasks parsed from the body (empty unless the item is a task). - pub subtasks: Vec, - /// The item's tags (note cards surface these). - pub tags: Vec, -} - -impl From for OrganizeItemGql { - fn from(item: OrganizeItem) -> Self { - let arrived_at = crate::organize::iso_from_ms(item.item.updated_at_ms); - let alternatives = item - .alternatives - .into_iter() - .map(|(collection_id, label)| OrganizeAlternativeGql { - collection_id, - label, - }) - .collect(); - let subtasks = item.subtasks.into_iter().map(SubtaskGql::from).collect(); - Self { - id: item.item.id, - kind: item.kind, - title: item.item.title, - preview: item.preview, - source: item.source, - arrived_at, - classification: OrganizeClassificationGql { - target_collection_id: item.target_collection_id, - target_collection_label: item.target_collection_label, - confidence: item.confidence as f64, - alternatives, - }, - time_sensitive: item.time_sensitive, - expected_action: item.expected_action, - subtasks, - tags: item.tags, - } - } -} - -/// An item the engine filed, plus when it filed it. -#[derive(SimpleObject)] -pub struct OrganizeFiledGql { - pub item: OrganizeItemGql, - pub filed_at: String, -} - -impl From for OrganizeFiledGql { - fn from(filed: OrganizeFiled) -> Self { - Self { - item: OrganizeItemGql::from(filed.item), - filed_at: filed.filed_at, - } - } -} - -/// A filed-into collection with its member count for the timeframe. -#[derive(SimpleObject)] -pub struct OrganizeGroupGql { - pub collection_id: String, - pub label: String, - pub count: i32, -} - -impl From for OrganizeGroupGql { - fn from(group: OrganizeGroup) -> Self { - Self { - collection_id: group.collection_id, - label: group.label, - count: group.count as i32, - } - } -} - -/// What the engine organized in the timeframe, without a human. -#[derive(SimpleObject)] -pub struct OrganizedTodayGql { - pub most_recent: Option, - pub groups: Vec, - pub total_count: i32, -} - -impl From for OrganizedTodayGql { - fn from(today: OrganizedToday) -> Self { - Self { - most_recent: today.most_recent.map(OrganizeFiledGql::from), - groups: today - .groups - .into_iter() - .map(OrganizeGroupGql::from) - .collect(), - total_count: today.total_count as i32, - } - } -} - -/// How much of the timeframe's intake is done vs. total. -#[derive(SimpleObject)] -pub struct DailyProgressGql { - pub timeframe: String, - pub done: i32, - pub total: i32, -} - -impl From for DailyProgressGql { - fn from(progress: DailyProgress) -> Self { - Self { - timeframe: progress.timeframe, - done: progress.done as i32, - total: progress.total as i32, - } - } -} - -/// The full organize snapshot: what needs you, what was organized, and progress. -#[derive(SimpleObject)] -pub struct OrganizeSnapshotGql { - pub needs_you: Vec, - pub organized_today: OrganizedTodayGql, - pub daily_progress: DailyProgressGql, - pub needs_you_ceiling: f64, -} - -impl From for OrganizeSnapshotGql { - fn from(snapshot: OrganizeSnapshot) -> Self { - Self { - needs_you: snapshot - .needs_you - .into_iter() - .map(OrganizeItemGql::from) - .collect(), - organized_today: OrganizedTodayGql::from(snapshot.organized_today), - daily_progress: DailyProgressGql::from(snapshot.daily_progress), - needs_you_ceiling: snapshot.needs_you_ceiling as f64, - } - } -} - -fn principal(ctx: &Context<'_>) -> Result { - let token = ctx - .data_opt::() - .ok_or_else(|| Error::new("missing API key: present a key via the x-api-key header"))?; - let registry = ctx.data::>()?; - registry - .resolve(&token.0) - .cloned() - .ok_or_else(|| Error::new("invalid API key")) -} - -fn collector_provenance_value( - input: &CollectorProvenanceInputGql, - authenticated: &Principal, - source_ref: Option<&SourceRefInputGql>, -) -> Result { - if input.kind != "collector" { - return Err(Error::new("collector provenance kind must be collector")); - } - if !valid_correlation_id(&input.correlation_id) { - return Err(Error::new("collector provenance correlation id is invalid")); - } - bounded_text("collector upload filename", &input.upload.filename, 255)?; - if input.upload.filename.contains(['/', '\\', '\0']) { - return Err(Error::new( - "collector upload filename must be a logical basename", - )); - } - bounded_text("collector upload media type", &input.upload.media_type, 255)?; - bounded_text("collector upload source", &input.upload.source, 2_048)?; - optional_bounded_text( - "collector parser", - input.service_facts.parser.as_deref(), - 128, - )?; - optional_bounded_text( - "collector service media type", - input.service_facts.media_type.as_deref(), - 255, - )?; - non_negative_count("collector byte length", input.service_facts.byte_length)?; - non_negative_count("collector page count", input.service_facts.page_count)?; - - if !(0..32).contains(&input.document.index) { - return Err(Error::new("collector document index is out of range")); - } - if !valid_sha256_digest(&input.document.document_digest) { - return Err(Error::new("collector document digest is invalid")); - } - optional_bounded_text( - "collector parser document id", - input.document.parser_document_id.as_deref(), - 255, - )?; - optional_bounded_text( - "collector document author", - input.document.doc_author.as_deref(), - 512, - )?; - optional_bounded_text( - "collector document description", - input.document.description.as_deref(), - 2_048, - )?; - optional_bounded_text( - "collector document published", - input.document.published.as_deref(), - 128, - )?; - non_negative_count("collector word count", input.document.word_count)?; - non_negative_count( - "collector token count estimate", - input.document.token_count_estimate, - )?; - - let source_ref = source_ref - .ok_or_else(|| Error::new("collector provenance requires a stable source reference"))?; - if input.upload.source != source_ref.source - || !source_ref - .external_id - .ends_with(&input.document.document_digest) - { - return Err(Error::new( - "collector provenance does not match the stable source reference", - )); - } - - Ok(json!({ - "kind": "collector", - "correlationId": input.correlation_id, - "assertedBy": { - "principalId": authenticated.id, - "label": authenticated.label, - }, - "sourceRef": { - "source": source_ref.source, - "externalId": source_ref.external_id, - }, - "upload": { - "filename": input.upload.filename, - "mediaType": input.upload.media_type, - "source": input.upload.source, - }, - "serviceFacts": { - "parser": input.service_facts.parser, - "mediaType": input.service_facts.media_type, - "byteLength": input.service_facts.byte_length, - "pageCount": input.service_facts.page_count, - }, - "document": { - "index": input.document.index, - "documentDigest": input.document.document_digest, - "parserDocumentId": input.document.parser_document_id, - "docAuthor": input.document.doc_author, - "description": input.document.description, - "published": input.document.published, - "wordCount": input.document.word_count, - "tokenCountEstimate": input.document.token_count_estimate, - }, - })) -} - -fn bounded_text(field: &str, value: &str, max_len: usize) -> Result<()> { - if value.trim().is_empty() || value.len() > max_len { - return Err(Error::new(format!( - "{field} must be non-empty and at most {max_len} bytes" - ))); - } - Ok(()) -} - -fn optional_bounded_text(field: &str, value: Option<&str>, max_len: usize) -> Result<()> { - if let Some(value) = value { - bounded_text(field, value, max_len)?; - } - Ok(()) -} - -fn non_negative_count(field: &str, value: Option) -> Result<()> { - if value.is_some_and(|value| value < 0) { - return Err(Error::new(format!("{field} must be non-negative"))); - } - Ok(()) -} - -fn valid_correlation_id(value: &str) -> bool { - let bytes = value.as_bytes(); - (8..=128).contains(&bytes.len()) - && bytes[0].is_ascii_alphanumeric() - && bytes[1..] - .iter() - .all(|byte| byte.is_ascii_alphanumeric() || b"._:-".contains(byte)) -} - -fn valid_sha256_digest(value: &str) -> bool { - value.strip_prefix("sha256:").is_some_and(|digest| { - digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()) - }) -} - -/// The retrieval projection cache. Present on every schema built through the -/// builders below; a schema assembled by hand without one gets a fresh cache -/// per request rather than a refusal, because a cold cache is correct, just -/// slower. -fn find_index(ctx: &Context<'_>) -> Result> { - Ok(ctx - .data_opt::>() - .cloned() - .unwrap_or_else(|| Arc::new(FindIndexCache::new()))) -} -fn shared(ctx: &Context<'_>) -> Result> -where - S: Send + Sync + 'static, - B: Send + Sync + 'static, -{ - ctx.data::>().cloned() -} - -fn store_err(error: rustyred_thg_core::GraphStoreError) -> Error { - Error::new(format!("{error:?}")) -} - -fn remote_graphql_data(response: &Value, field: &str) -> Result> { - if let Some(errors) = response.get("errors").and_then(Value::as_array) { - if let Some(error) = errors.first() { - return Err(Error::new(format!("Theorem reconstruction error: {error}"))); - } - } - response - .get("data") - .and_then(|data| data.get(field)) - .cloned() - .map(|value| if value.is_null() { None } else { Some(value) }) - .ok_or_else(|| Error::new(format!("Theorem reconstruction response omitted {field}"))) -} - -fn reconstruction_run_from_value(value: Option) -> Result> { - let Some(value) = value else { - return Ok(None); - }; - let id = remote_string(&value, "id")?; - let domain = remote_string(&value, "domain")?; - let subject_id = remote_string(&value, "subjectId")?; - let stage_receipts = value - .get("stageReceipts") - .and_then(Value::as_array) - .ok_or_else(|| Error::new("Theorem reconstruction run omitted stageReceipts"))? - .iter() - .map(|receipt| { - Ok(ReconstructionStageReceiptGql { - id: remote_string(receipt, "id")?, - stage: remote_string(receipt, "stage")?, - generation_stamp: remote_string(receipt, "generationStamp")?, - payload: GqlJson(receipt.get("payload").cloned().unwrap_or(Value::Null)), - }) - }) - .collect::>>()?; - let atoms = value - .get("atoms") - .and_then(Value::as_array) - .ok_or_else(|| Error::new("Theorem reconstruction run omitted atoms"))? - .iter() - .map(|atom| { - Ok(ReconstructionAtomGql { - id: remote_string(atom, "id")?, - atom_id: remote_string(atom, "atomId")?, - provenance: GqlJson(atom.get("provenance").cloned().unwrap_or(Value::Null)), - payload: GqlJson(atom.get("payload").cloned().unwrap_or(Value::Null)), - }) - }) - .collect::>>()?; - Ok(Some(ReconstructionRunGql { - id, - domain, - subject_id, - stage_receipts, - atoms, - })) -} - -fn reconstruction_result_from_value(value: Value) -> Result { - Ok(GenerateReconstructionResultGql { - run_id: remote_string(&value, "runId")?, - scene_package_ref: remote_string(&value, "scenePackageRef")?, - result: GqlJson(value.get("result").cloned().unwrap_or(Value::Null)), - }) -} - -fn remote_string(value: &Value, field: &str) -> Result { - value - .get(field) - .and_then(Value::as_str) - .map(str::to_string) - .ok_or_else(|| Error::new(format!("Theorem reconstruction response omitted {field}"))) -} - -fn extra_str(item: &Item, key: &str) -> Option { - item.extra - .get(key) - .and_then(Value::as_str) - .map(str::to_string) -} - -fn extra_i64(item: &Item, key: &str) -> Option { - item.extra.get(key).and_then(Value::as_i64) -} - -fn extra_i32(item: &Item, key: &str) -> Option { - extra_i64(item, key).map(|value| value as i32) -} - -fn graphql_int_from_usize(value: usize, field: &str) -> Result { - i32::try_from(value).map_err(|_| Error::new(format!("{field} exceeds GraphQL Int range"))) -} - -fn embedding_text(item: &Item) -> String { - let text = item.text_for_embedding(); - let compact = text.split_whitespace().collect::>().join(" "); - if compact.len() > 360 { - format!("{}...", compact.chars().take(357).collect::()) - } else { - compact - } -} - -fn embedding_category_label(item: &Item) -> String { - item.classification - .clone() - .filter(|value| !value.trim().is_empty()) - .unwrap_or_else(|| item.kind.as_str().to_string()) -} - -fn embedding_community_id(item: &Item) -> String { - item.collections - .first() - .cloned() - .or_else(|| item.classification.clone()) - .unwrap_or_else(|| item.kind.as_str().to_string()) -} - -fn embedding_status(item: &Item) -> String { - item.status - .clone() - .filter(|value| !value.trim().is_empty()) - .unwrap_or_else(|| item.residency.as_str().to_string()) -} - -fn raw_embedding_projection(embedding: &[f32]) -> (f64, f64) { - let mut x = 0.0_f64; - let mut y = 0.0_f64; - for (idx, value) in embedding.iter().enumerate() { - let angle = (idx as f64 + 1.0) * 2.399_963_229_728_653; - let weight = 1.0 / (1.0 + idx as f64 * 0.015); - let v = *value as f64; - x += v * angle.cos() * weight; - y += v * angle.sin() * weight; - } - (x, y) -} - -fn embedding_space_rows( - cp: &Commonplace, - kind: Option<&str>, - limit: usize, -) -> rustyred_thg_core::GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut projected = Vec::new(); - for item in cp.all_items()? { - if let Some(kind) = kind { - if item.kind.as_str() != kind { - continue; - } - } - let Some(embedding) = cp.resolve_item_embedding(&item)? else { - continue; - }; - let (x, y) = raw_embedding_projection(&embedding); - projected.push((item, x, y)); - } - - if projected.is_empty() { - return Ok(Vec::new()); - } - - let count = projected.len() as f64; - let mean_x = projected.iter().map(|(_, x, _)| x).sum::() / count; - let mean_y = projected.iter().map(|(_, _, y)| y).sum::() / count; - let scale = projected - .iter() - .map(|(_, x, y)| (x - mean_x).abs().max((y - mean_y).abs())) - .fold(0.0_f64, f64::max) - .max(1.0); - - let mut labels: Vec = projected - .iter() - .map(|(item, _, _)| embedding_category_label(item)) - .collect(); - labels.sort(); - labels.dedup(); - - let mut rows = Vec::with_capacity(projected.len().min(limit)); - for (item, x, y) in projected.into_iter().take(limit) { - let category_label = embedding_category_label(&item); - let category = labels.binary_search(&category_label).unwrap_or_default() as i32; - rows.push(EmbeddingSpaceRowGql { - identifier: item.id.clone(), - x: (x - mean_x) / scale, - y: (y - mean_y) / scale, - category, - category_label, - text: embedding_text(&item), - created_ms: item.created_at_ms, - community_id: embedding_community_id(&item), - epistemic_status: embedding_status(&item), - }); - } - Ok(rows) -} - -fn is_terminal_status(status: Option<&str>) -> bool { - match status { - Some(value) => matches!( - value.trim().to_ascii_lowercase().as_str(), - "done" | "closed" | "complete" | "completed" | "cancelled" | "canceled" - ), - None => false, - } -} - -fn default_state_seed() -> [(&'static str, &'static str); 5] { - [ - ("Backlog", "backlog"), - ("Unstarted", "unstarted"), - ("Started", "started"), - ("Completed", "completed"), - ("Cancelled", "cancelled"), - ] -} - -fn identifier_from_name(name: &str) -> String { - let mut ident: String = name - .split_whitespace() - .filter_map(|part| part.chars().find(|ch| ch.is_ascii_alphanumeric())) - .take(4) - .collect::() - .to_ascii_uppercase(); - if ident.is_empty() { - ident = "CP".to_string(); - } - ident -} - -fn next_sequence_id( - cp: &Commonplace, - project_id: Option<&str>, -) -> std::result::Result, Error> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let Some(project_id) = project_id else { - return Ok(None); - }; - let Some(project) = cp.get_collection(project_id).map_err(store_err)? else { - return Ok(None); - }; - let Some(identifier) = project.identifier else { - return Ok(None); - }; - let count = cp.project_work_items(project_id).map_err(store_err)?.len() + 1; - Ok(Some(format!("{identifier}-{count}"))) -} - -fn pm_project_from( - cp: &Commonplace, - collection: Collection, -) -> std::result::Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let states = cp - .project_states(&collection.id) - .map_err(store_err)? - .into_iter() - .map(PmStateGql::from) - .collect(); - let cycles = cp - .project_cycles(&collection.id) - .map_err(store_err)? - .into_iter() - .map(CollectionGql::from) - .collect(); - let modules = cp - .project_modules(&collection.id) - .map_err(store_err)? - .into_iter() - .map(CollectionGql::from) - .collect(); - let labels = cp - .project_labels(&collection.id) - .map_err(store_err)? - .into_iter() - .map(|(tag, color)| PmLabelGql { - id: tag.id, - name: tag.name, - color, - }) - .collect(); - let work_items = cp.project_work_items(&collection.id).map_err(store_err)?; - let open_item_count = work_items - .iter() - .filter(|item| !is_terminal_status(item.status.as_deref())) - .count(); - Ok(PmProjectGql { - collection: CollectionGql::from(collection), - states, - cycles, - modules, - labels, - work_item_count: work_items.len() as i32, - open_item_count: open_item_count as i32, - }) -} - -fn pm_work_item_from( - cp: &Commonplace, - item: Item, -) -> std::result::Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let state = cp - .task_state(&item.id) - .map_err(store_err)? - .map(PmStateGql::from); - let collection_ids = classify_work_item_collections(cp, &item)?; - let about_ids = cp.task_about(&item.id).map_err(store_err)?; - let comments = cp.comments_for(&item.id).map_err(store_err)?; - let worklogs = cp.worklogs_for(&item.id).map_err(store_err)?; - let total_worklog_duration_ms = cp.total_worklog_duration_ms(&item.id).map_err(store_err)?; - Ok(PmWorkItemGql { - sequence_id: extra_str(&item, "sequence_id"), - estimate_point: extra_i32(&item, "estimate_point"), - item: ItemGql::from(item), - state, - project_ids: collection_ids.projects, - cycle_ids: collection_ids.cycles, - module_ids: collection_ids.modules, - about_ids, - comment_count: comments.len() as i32, - worklog_count: worklogs.len() as i32, - total_worklog_duration_ms, - }) -} - -struct WorkItemCollectionIds { - projects: Vec, - cycles: Vec, - modules: Vec, -} - -fn classify_work_item_collections( - cp: &Commonplace, - item: &Item, -) -> std::result::Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let mut projects = Vec::new(); - let mut cycles = Vec::new(); - let mut modules = Vec::new(); - for collection_id in &item.collections { - let Some(collection) = cp.get_collection(collection_id).map_err(store_err)? else { - continue; - }; - match collection.kind { - CollectionKind::Project => projects.push(collection.id), - CollectionKind::Cycle => cycles.push(collection.id), - CollectionKind::Module => modules.push(collection.id), - _ => {} - } - } - Ok(WorkItemCollectionIds { - projects, - cycles, - modules, - }) -} - -fn page_crdt_snapshot_path(page_id: &str) -> String { - format!(".commonplace/pages/{page_id}/content.yjs") -} - -fn latest_page_crdt_snapshot_item( - cp: &Commonplace, - page_id: &str, -) -> rustyred_thg_core::GraphStoreResult> -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - if let Some(item) = cp.item_by_source_ref(PAGE_CRDT_SOURCE, page_id)? { - return Ok(Some(item)); - } - - let mut snapshots: Vec = cp - .items_by_kind(&ItemKind::File)? - .into_iter() - .filter(|item| { - extra_str(item, "content_role").as_deref() == Some(PAGE_CRDT_ROLE) - && extra_str(item, "page_id").as_deref() == Some(page_id) - }) - .collect(); - snapshots.sort_by_key(|item| std::cmp::Reverse(item.updated_at_ms)); - Ok(snapshots.into_iter().next()) -} - -fn page_crdt_snapshot_from( - cp: &Commonplace, - page_id: String, - item: Item, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let (blob_hash, byte_len) = match &item.body { - ItemBody::Blob { - content_hash, - byte_len, - .. - } => (content_hash.clone(), *byte_len), - _ => { - return Err(Error::new( - "page CRDT snapshot item does not reference a blob", - )) - } - }; - let bytes = cp - .read_blob(&item) - .map_err(store_err)? - .ok_or_else(|| Error::new(format!("missing page CRDT blob {blob_hash}")))?; - let encoding = extra_str(&item, "encoding").unwrap_or_else(|| PAGE_CRDT_ENCODING.to_string()); - Ok(PageCrdtSnapshotGql { - page_id, - item: ItemGql::from(item.clone()), - update_base64: BASE64_STANDARD.encode(bytes), - encoding, - blob_hash, - byte_len: byte_len.min(i32::MAX as u64) as i32, - stored_at_ms: item.updated_at_ms, - }) -} - -fn store_page_crdt_snapshot_bytes( - cp: &mut Commonplace, - page_id: &str, - bytes: &[u8], - compacted: bool, -) -> Result -where - S: EmbeddingGraphStore, - B: BlobStore, -{ - let page = cp - .get_item(page_id) - .map_err(store_err)? - .ok_or_else(|| Error::new("page not found"))?; - if page.kind != ItemKind::Doc { - return Err(Error::new("page CRDT snapshots only accept doc pages")); - } - - let content_hash = cp.blobs().put(bytes).map_err(store_err)?; - let mut snapshot = cp - .item_by_source_ref(PAGE_CRDT_SOURCE, page_id) - .map_err(store_err)? - .unwrap_or_else(|| { - Item::new(ItemKind::File, format!("CRDT snapshot for {}", page.title)) - .with_source_ref(SourceRef::new(PAGE_CRDT_SOURCE, page_id.to_string())) - }); - snapshot.title = format!("CRDT snapshot for {}", page.title); - snapshot.body = ItemBody::Blob { - content_hash, - byte_len: bytes.len() as u64, - mime: Some(PAGE_CRDT_MIME.to_string()), - }; - snapshot.source = Some(PAGE_CRDT_SOURCE.to_string()); - snapshot.source_ref = Some(SourceRef::new(PAGE_CRDT_SOURCE, page_id.to_string())); - snapshot.extra.insert("page_id".to_string(), json!(page_id)); - snapshot - .extra - .insert("content_role".to_string(), json!(PAGE_CRDT_ROLE)); - snapshot - .extra - .insert("encoding".to_string(), json!(PAGE_CRDT_ENCODING)); - snapshot - .extra - .insert("path".to_string(), json!(page_crdt_snapshot_path(&page.id))); - snapshot.extra.insert( - "folder_path".to_string(), - json!(format!(".commonplace/pages/{}", page.id)), - ); - snapshot.extra.insert("hidden".to_string(), json!(true)); - if compacted { - snapshot.extra.insert("compacted".to_string(), json!(true)); - } - - let snapshot = cp.put_item(snapshot).map_err(store_err)?; - cp.attach_item(&page.id, &snapshot.id).map_err(store_err)?; - page_crdt_snapshot_from(cp, page.id, snapshot) -} - -fn compact_page_crdt_update_v1(update_v1: &[u8]) -> Result> { - let update = Update::decode_v1(update_v1) - .map_err(|error| Error::new(format!("invalid Yjs update v1: {error}")))?; - let doc = Doc::new(); - doc.transact_mut() - .apply_update(update) - .map_err(|error| Error::new(format!("could not apply Yjs update: {error}")))?; - let compacted = doc - .transact() - .encode_state_as_update_v1(&StateVector::default()); - Ok(compacted) -} - -fn annotation_gql_from(annotation: Annotation) -> Result { - let anchor = annotation - .anchor - .map(|anchor| serde_json::to_value(anchor).map(GqlJson)) - .transpose() - .map_err(|error| Error::new(format!("invalid annotation anchor JSON: {error}")))?; - Ok(AnnotationGql { - id: annotation.id, - target_id: annotation.target_id, - author: annotation.author, - author_kind: annotation - .author_kind - .map(|author_kind| author_kind.as_str().to_string()), - anchor, - body: annotation.body, - resolved: annotation.resolved, - resolution: annotation.resolution.map(AnnotationResolutionGql::from), - created_at_ms: annotation.created_at_ms, - }) -} - -impl From for AnnotationResolutionGql { - fn from(resolution: Resolution) -> Self { - Self { - by: resolution.by, - receipt: resolution.receipt, - } - } -} - -fn annotation_item_gql(item: &Item) -> Result { - annotation_from_item(item) - .ok_or_else(|| Error::new("annotation item did not project")) - .and_then(annotation_gql_from) -} - -fn parse_anchor(value: GqlJson) -> Result { - let GqlJson(value) = value; - serde_json::from_value(value).map_err(|error| Error::new(format!("invalid anchor: {error}"))) -} - -fn parse_author_kind(value: &str) -> Result { - let normalized = value.trim().to_ascii_lowercase(); - AuthorKind::parse(&normalized) - .ok_or_else(|| Error::new("authorKind must be either 'user' or 'head'")) -} - -/// Consumer read API. -pub struct Query(PhantomData (S, B)>); - -#[Object(name = "Query")] -impl Query -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - /// One item by id. - async fn item(&self, ctx: &Context<'_>, id: String) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(cp.get_item(&id).map_err(store_err)?.map(ItemGql::from)) - } - - /// Latest RustyRed-owned Yjs snapshot for a page. The bytes are carried as - /// base64 for GraphQL transport, but live as a content-addressed File blob. - async fn page_crdt_snapshot( - &self, - ctx: &Context<'_>, - page_id: String, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let Some(snapshot) = latest_page_crdt_snapshot_item(&cp, &page_id).map_err(store_err)? - else { - return Ok(None); - }; - page_crdt_snapshot_from(&cp, page_id, snapshot).map(Some) - } - - /// The live, validated Growth projection. Unconfigured or invalid sources - /// return an explicit unavailable result rather than fixture data. - async fn growth_snapshot(&self, ctx: &Context<'_>) -> Result { - principal(ctx)?; - Ok(load_growth_snapshot_from_env().await) - } - - /// Read a Theorem-owned durable reconstruction run. The public facade never - /// reconstructs locally: it forwards to the same GraphQL executor used by - /// the Theorem MCP tool. - async fn reconstruction_run( - &self, - ctx: &Context<'_>, - id: String, - ) -> Result> { - let principal = principal(ctx)?; - if id.trim().is_empty() { - return Err(Error::new("reconstructionRun requires a non-empty id")); - } - let response = reconstruction::execute( - &principal.id, - "graphql_query", - "query($id:String!){ reconstructionRun(id:$id){ id domain subjectId stageReceipts { id stage generationStamp payload } atoms { id atomId provenance payload } } }", - json!({ "id": id }), - ) - .await - .map_err(Error::new)?; - remote_graphql_data(&response, "reconstructionRun").and_then(reconstruction_run_from_value) - } - - /// All items, optionally filtered to a kind. - async fn items(&self, ctx: &Context<'_>, kind: Option) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let items = match kind { - Some(kind) => cp.items_by_kind(&ItemKind::from(kind)).map_err(store_err)?, - None => cp.all_items().map_err(store_err)?, - }; - Ok(items.into_iter().map(ItemGql::from).collect()) - } - - /// Count items without materializing them into the GraphQL response body. - async fn item_count(&self, ctx: &Context<'_>, kind: Option) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let count = match kind { - Some(kind) => cp - .items_by_kind(&ItemKind::from(kind)) - .map_err(store_err)? - .len(), - None => cp.all_items().map_err(store_err)?.len(), - }; - graphql_int_from_usize(count, "item_count") - } - - /// One collection by id. - async fn collection(&self, ctx: &Context<'_>, id: String) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(cp - .get_collection(&id) - .map_err(store_err)? - .map(CollectionGql::from)) - } - - /// All collections. - async fn collections(&self, ctx: &Context<'_>) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let ids: Vec = cp - .store() - .query_nodes(NodeQuery::label(COLLECTION_LABEL).with_limit(usize::MAX)) - .into_iter() - .map(|node| node.id) - .collect(); - let mut collections = Vec::with_capacity(ids.len()); - for id in ids { - if let Some(collection) = cp.get_collection(&id).map_err(store_err)? { - collections.push(CollectionGql::from(collection)); - } - } - Ok(collections) - } - - /// Items in a collection. - async fn collection_items(&self, ctx: &Context<'_>, id: String) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(cp - .collection_items(&id) - .map_err(store_err)? - .into_iter() - .map(ItemGql::from) - .collect()) - } - - /// Plane-parity PM overview over the CommonPlace graph. Projects are typed - /// collections; work items are task/epic items; states, labels, comments, - /// links, worklogs, and stickies remain graph-native nodes/edges. - async fn pm_overview( - &self, - ctx: &Context<'_>, - project_id: Option, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - - let projects = match project_id.as_deref() { - Some(id) => cp - .get_collection(id) - .map_err(store_err)? - .filter(|collection| collection.kind == CollectionKind::Project) - .into_iter() - .collect(), - None => cp.projects().map_err(store_err)?, - }; - let project_gql = projects - .into_iter() - .map(|project| pm_project_from(&cp, project)) - .collect::, _>>()?; - - let work_items = match project_id.as_deref() { - Some(id) => cp.project_work_items(id).map_err(store_err)?, - None => { - let mut items = cp.items_by_kind(&ItemKind::Task).map_err(store_err)?; - items.extend(cp.items_by_kind(&ItemKind::Epic).map_err(store_err)?); - items.sort_by_key(|item| std::cmp::Reverse(item.updated_at_ms)); - items - } - }; - let work_item_gql = work_items - .into_iter() - .map(|item| pm_work_item_from(&cp, item)) - .collect::, _>>()?; - - let mut stickies = cp.items_by_kind(&ItemKind::Sticky).map_err(store_err)?; - if let Some(project_id) = project_id.as_deref() { - stickies.retain(|sticky| extra_str(sticky, "owner_id").as_deref() == Some(project_id)); - } - stickies - .sort_by_key(|sticky| extra_i64(sticky, "sort_order").unwrap_or(sticky.created_at_ms)); - - let mut pages = match project_id.as_deref() { - Some(id) => cp - .collection_items(id) - .map_err(store_err)? - .into_iter() - .filter(|item| item.kind == ItemKind::Doc) - .collect(), - None => cp.items_by_kind(&ItemKind::Doc).map_err(store_err)?, - }; - pages.sort_by_key(|page| std::cmp::Reverse(page.updated_at_ms)); - - Ok(PmOverviewGql { - projects: project_gql, - work_items: work_item_gql, - stickies: stickies.into_iter().map(ItemGql::from).collect(), - pages: pages.into_iter().map(ItemGql::from).collect(), - }) - } - - /// Similarity search over items. - async fn search( - &self, - ctx: &Context<'_>, - query: String, - k: Option, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let k = k.unwrap_or(10).max(1) as usize; - let hits = IngestPipeline::default() - .search(&cp, &query, k) - .map_err(store_err)?; - let mut results = Vec::with_capacity(hits.len()); - for (id, score) in hits { - if let Some(item) = cp.get_item(&id).map_err(store_err)? { - results.push(SearchHitGql { - item: ItemGql::from(item), - score: score as f64, - }); - } - } - Ok(results) - } - - /// Top-level co-annotations for a target object, newest first. - async fn annotations_for_target( - &self, - ctx: &Context<'_>, - target_id: String, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.annotations_for(&target_id) - .map_err(store_err)? - .into_iter() - .map(annotation_gql_from) - .collect() - } - - /// Project stored item embeddings into the `embedding_space` table contract. - async fn embedding_space( - &self, - ctx: &Context<'_>, - kind: Option, - limit: Option, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let limit = limit.unwrap_or(5_000).clamp(1, 50_000) as usize; - let rows = embedding_space_rows(&cp, kind.as_deref(), limit).map_err(store_err)?; - Ok(EmbeddingSpaceGql { - table: "embedding_space".to_string(), - projection: "server:embedding_axes_v1".to_string(), - total: rows.len() as i32, - rows, - }) - } - - /// Nearest embedded items around a stored item, using the RustyRed vector index. - async fn vector_neighbors( - &self, - ctx: &Context<'_>, - item_id: String, - k: Option, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let Some(seed) = cp.get_item(&item_id).map_err(store_err)? else { - return Ok(Vec::new()); - }; - let Some(embedding) = cp.resolve_item_embedding(&seed).map_err(store_err)? else { - return Ok(Vec::new()); - }; - let k = k.unwrap_or(12).clamp(1, 100) as usize; - let hits = IngestPipeline::default() - .search_embedding(&cp, &embedding, k + 1) - .map_err(store_err)?; - let rows = embedding_space_rows(&cp, None, usize::MAX).map_err(store_err)?; - let by_id: HashMap = rows - .into_iter() - .map(|row| (row.identifier.clone(), row)) - .collect(); - let mut out = Vec::with_capacity(k); - for (id, score) in hits { - if id == item_id { - continue; - } - if let Some(row) = by_id.get(&id) { - out.push(VectorNeighborGql { - row: row.clone(), - score: score as f64, - }); - } - if out.len() >= k { - break; - } - } - Ok(out) - } - - /// Ask a question over your store: unified graph + vector + lexical retrieval - /// (reciprocal-rank fusion) with per-item provenance, answered by the - /// configured model or an honest extractive fallback. Each provenance entry - /// is the item a part of the answer is grounded in. - async fn ask( - &self, - ctx: &Context<'_>, - question: String, - k: Option, - ) -> Result { - principal(ctx)?; - let model = ctx.data::>()?.clone(); - let store = shared::(ctx)?; - let config = AskConfig { - k: k.unwrap_or(5).max(1) as usize, - ..AskConfig::default() - }; - let retrieval_question = question.clone(); - let grounding = tokio::task::spawn_blocking(move || -> std::result::Result<_, String> { - let cp = store - .lock() - .map_err(|_| "store lock poisoned".to_string())?; - retrieve_grounding_measured(&*cp, &retrieval_question, &config) - .map_err(|error| format!("{error:?}")) - }) - .await - .map_err(|error| Error::new(format!("ask worker join failed: {error}")))? - .map_err(Error::new)?; - let mut result = answer_from_provenance(model.as_ref(), &question, grounding.provenance); - result.ppr_expansion = grounding.ppr_expansion; - AskResultGql::try_from(result) - } - - /// Run the composed Theorem API agent through the CommonPlace GraphQL edge. - /// The browser sends the user turn here; the resolver calls provider APIs - /// server-side and the agent runtime reaches MCP tools internally. - async fn theorem_agent( - &self, - ctx: &Context<'_>, - task: String, - claims: Option>, - binding_id: Option, - mode: Option, - tenant: Option, - ) -> Result { - principal(ctx)?; - let task = task.trim().to_string(); - if task.is_empty() { - return Err(Error::new("Theorem agent requires a task.")); - } - let _ = mode; - let _ = tenant; - - let binding_id = binding_id - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| DEFAULT_BINDING_ID.to_string()); - let claims = normalize_theorem_agent_claims(claims.unwrap_or_default()); - let evidence_count = claims.len() as i32; - let store = shared::(ctx)?.clone(); - - let result = tokio::task::spawn_blocking(move || -> std::result::Result<_, String> { - let invoker = ProviderHeadInvoker::from_env().map_err(|error| error.to_string())?; - let mut cp = store - .lock() - .map_err(|_| "store lock poisoned".to_string())?; - if claims.is_empty() { - run_configured_composed_agent(cp.store_mut(), &binding_id, &task, &invoker) - } else { - run_configured_composed_agent_with_claims( - cp.store_mut(), - &binding_id, - &task, - claims, - &invoker, - ) - } - .map_err(|error| error.to_string()) - }) - .await - .map_err(|error| Error::new(format!("Theorem agent task join failed: {error}")))? - .map_err(Error::new)?; - - Ok(TheoremAgentRunGql::from_composed(result, evidence_count)) - } - - /// Universal find over the composed retrieval backend: exact, lexical, - /// semantic, and structural lanes, widening through the scopes given, and - /// admitted under the caller's MMR lambda. Every result carries its lane, - /// its scope, its relation to the graph, and the edges supporting that - /// reading. SPEC-COMMONPLACE-SEARCH-STACK B3 and B7. - async fn find( - &self, - ctx: &Context<'_>, - query: String, - scopes: Option>, - lanes: Option>, - k: Option, - lambda: Option, - ) -> Result { - principal(ctx)?; - let request = build_find_request(query, scopes, lanes, k, lambda).map_err(Error::new)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let response = find_index(ctx)? - .with(&cp, &FindConfig::default(), |index| { - rustyred_thg_find::find(&index.context(), &request) - }) - .map_err(store_err)?; - Ok(FindResponseGql::from(response)) - } - - /// Scatter a query into at most eight named aspects, with the compiled - /// force-graph scene. SPEC-COMMONPLACE-SEARCH-STACK B5 and B7. - async fn scatter( - &self, - ctx: &Context<'_>, - query: String, - scopes: Option>, - k: Option, - lambda: Option, - ) -> Result { - principal(ctx)?; - let request = build_scatter_request(query, scopes, k, lambda).map_err(Error::new)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let response = find_index(ctx)? - .with(&cp, &FindConfig::default(), |index| { - run_scatter(&index.context(), &request) - }) - .map_err(store_err)?; - Ok(ScatterResponseGql::from(response)) - } - - /// Re-scatter inside one aspect. The returned aspects are drawn from that - /// aspect's neighborhood, not from the whole corpus. - async fn expand( - &self, - ctx: &Context<'_>, - query: String, - aspect_id: String, - scopes: Option>, - k: Option, - lambda: Option, - ) -> Result { - principal(ctx)?; - let aspect = parse_aspect_id(aspect_id).map_err(Error::new)?; - let request = build_scatter_request(query, scopes, k, lambda).map_err(Error::new)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let response = find_index(ctx)? - .with(&cp, &FindConfig::default(), |index| { - run_expand(&index.context(), &request, &aspect) - }) - .map_err(store_err)?; - Ok(ScatterResponseGql::from(response)) - } - - /// Proactive briefing: recent, newly-connected, and open-thread items - /// surfaced from the store without being asked. - async fn briefing( - &self, - ctx: &Context<'_>, - recent_limit: Option, - connected_limit: Option, - open_limit: Option, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let config = BriefingConfig { - recent_limit: recent_limit.unwrap_or(10).max(1) as usize, - connected_limit: connected_limit.unwrap_or(10).max(1) as usize, - open_limit: open_limit.unwrap_or(10).max(1) as usize, - ..BriefingConfig::default() - }; - let briefing = run_briefing(&*cp, &config).map_err(store_err)?; - Ok(BriefingGql::from(briefing)) - } - - /// Discovery: propose ranked candidate links between items that are - /// semantically similar but not yet connected. - async fn discover( - &self, - ctx: &Context<'_>, - min_similarity: Option, - max_results: Option, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let config = DiscoverConfig { - min_similarity: min_similarity.unwrap_or(0.5), - max_results: max_results.unwrap_or(20).max(1) as usize, - ..DiscoverConfig::default() - }; - let links = run_discover(&*cp, &config).map_err(store_err)?; - Ok(links.into_iter().map(CandidateLinkGql::from).collect()) - } - - /// Salience (D2, HANDOFF-MARGIN-RECALL): the passages of `page_text` that - /// connect back to the tenant's stored knowledge, as margin-recall candidates. - /// `mode` selects the dial policy (`"quiet"` = exact tier only + higher bar; - /// anything else = Active, both tiers); `min_similarity` / `max_candidates` - /// override the preset. The Off position is enforced by the caller (the - /// pipeline is simply not invoked), so reaching this resolver already means - /// recall is on for the origin. - async fn salience( - &self, - ctx: &Context<'_>, - page_text: String, - mode: Option, - min_similarity: Option, - max_candidates: Option, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let mut config = match mode.as_deref() { - Some("quiet") => SalienceConfig::quiet(), - _ => SalienceConfig::active(), - }; - if let Some(min_similarity) = min_similarity { - config.min_similarity = min_similarity; - } - if let Some(max_candidates) = max_candidates { - config.max_candidates = max_candidates.max(1) as usize; - } - let candidates = run_salience(&*cp, &page_text, &config).map_err(store_err)?; - Ok(candidates - .into_iter() - .map(SalienceCandidateGql::from) - .collect()) - } - - /// Organize: the daily triage surface. Partitions the items that arrived in - /// the timeframe into what the engine filed confidently (`organizedToday`) - /// and what still needs a human (`needsYou`, low-confidence or - /// time-sensitive), using the engine's classification signal for both, and - /// reports `dailyProgress` over the intake. - async fn organize( - &self, - ctx: &Context<'_>, - needs_you_ceiling: Option, - timeframe: Option, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let config = OrganizeConfig { - needs_you_ceiling: needs_you_ceiling.unwrap_or(0.58) as f32, - timeframe: Timeframe::from(timeframe.as_deref().unwrap_or("day")), - ..OrganizeConfig::default() - }; - let pipeline = IngestPipeline::default(); - let snapshot = run_organize(&*cp, &pipeline, &config).map_err(store_err)?; - Ok(OrganizeSnapshotGql::from(snapshot)) - } - - /// Export the whole store: lossless JSON (default) or human-readable - /// markdown. The JSON output reimports via `importItems` without loss. - async fn export(&self, ctx: &Context<'_>, format: Option) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let output = match format.unwrap_or(ExportFormat::Json) { - ExportFormat::Json => portability::export_json(&*cp).map_err(store_err)?, - ExportFormat::Markdown => portability::export_markdown(&*cp).map_err(store_err)?, - }; - Ok(output) - } - - /// Resolve a published block by its stable alias for the public host - /// (HANDOFF-PUBLISH D2/D3). The API key is the trusted-caller gate; `viewer` - /// is the signed-in visitor's principal id, used only for private grants. - /// Returns a designed status (`ok`/`gone`/`forbidden`/`not_found`). - async fn published_block( - &self, - ctx: &Context<'_>, - alias: String, - #[graphql(desc = "Signed-in visitor principal id, for private grants.")] viewer: Option< - String, - >, - #[graphql(desc = "Count this resolution as a view (owner counter).")] count_view: Option< - bool, - >, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(publish::resolve_alias_status( - &mut cp, - &alias, - viewer.as_deref(), - count_view.unwrap_or(true), - )) - } - - /// Resolve a permanent, version-addressed block by its content hash. A - /// version URL that was ever public resolves forever (HANDOFF-PUBLISH D1). - async fn published_block_version( - &self, - ctx: &Context<'_>, - version_hash: String, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(publish::resolve_version(&cp, &version_hash).ok()) - } - - /// Aliases of currently-public, live blocks for the public sitemap (P3.2). - /// Unlisted and private blocks are excluded. - async fn public_aliases(&self, ctx: &Context<'_>) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(publish::public_aliases(&cp)) - } -} - -/// Consumer write API. -pub struct Mutation(PhantomData (S, B)>); - -#[Object(name = "Mutation")] -impl Mutation -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - /// Generate through Theorem's reconstruction executor and return its - /// durable run reference for subsequent `reconstructionRun` reads. - async fn reconstruct( - &self, - ctx: &Context<'_>, - input: GenerateReconstructionInputGql, - ) -> Result { - let principal = principal(ctx)?; - if input.domain.trim().is_empty() || input.target.trim().is_empty() { - return Err(Error::new( - "reconstruct requires non-empty domain and target", - )); - } - let response = reconstruction::execute( - &principal.id, - "graphql_mutate", - "mutation($input: GenerateReconstructionInput!){ reconstruct(input:$input){ runId scenePackageRef result } }", - json!({ - "input": { - "domain": input.domain, - "target": input.target, - "budget": input.budget, - } - }), - ) - .await - .map_err(Error::new)?; - let value = remote_graphql_data(&response, "reconstruct")? - .ok_or_else(|| Error::new("Theorem reconstruction response omitted reconstruct"))?; - reconstruction_result_from_value(value) - } - - /// Auto-structuring ingest: embed, classify, file, link, resolve entities. - /// Save the page at `url`: fetch it through RustyWeb, then file it through - /// the ingest pipeline. Returns the filed item and the real collection it - /// landed in. SPEC-COMMONPLACE-SEARCH-STACK B7, surfaced by F4. - async fn save_url(&self, ctx: &Context<'_>, url: String) -> Result { - let principal = principal(ctx)?; - let store = shared::(ctx)?; - // Tests inject an offline page source through schema data; production - // builds the live fetch cascade. - let source = match ctx.data_opt::>() { - Some(source) => source.clone(), - None => Arc::new(PageSource::live().map_err(|error| Error::new(error.to_string()))?), - }; - let run_id = format!("save-url:{}", principal.id); - run_save_url(&store, source.as_ref(), &url, &run_id) - .await - .map(SaveUrlReceiptGql::from) - .map_err(|error| Error::new(error.to_string())) - } - - async fn ingest(&self, ctx: &Context<'_>, input: IngestInputGql) -> Result { - let authenticated = principal(ctx)?; - let provenance = input - .provenance - .as_ref() - .map(|provenance| { - collector_provenance_value(provenance, &authenticated, input.source_ref.as_ref()) - }) - .transpose()?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let kind = input.kind.map(ItemKind::from).unwrap_or(ItemKind::Note); - let mut request = IngestInput::text(input.title, input.text, kind); - if let Some(tags) = input.tags { - request = request.with_tags(tags); - } - if let Some(source) = input.source { - request = request.with_source(source); - } - if let Some(source_ref) = input.source_ref { - request = - request.with_source_ref(SourceRef::new(source_ref.source, source_ref.external_id)); - } - if let Some(residency) = input.residency { - request = request.with_residency(Residency::from(residency)); - } - if let Some(provenance) = provenance { - request = request.with_provenance(provenance); - } - // Explicit scalar instants win over the server-side reminder parse. - request.remind_at_ms = input.remind_at_ms; - request.due_at_ms = input.due_at_ms; - let receipt = IngestPipeline::default() - .ingest(&mut cp, request) - .map_err(store_err)?; - Ok(ItemGql::from(receipt.item)) - } - - /// Create a plain note item (no auto-structuring). - async fn put_note( - &self, - ctx: &Context<'_>, - title: String, - text: String, - tags: Option>, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let mut item = Item::note(title, text); - if let Some(tags) = tags { - item = item.with_tags(tags); - } - Ok(ItemGql::from(cp.put_item(item).map_err(store_err)?)) - } - - /// Create a top-level co-annotation against an object/page/file target. - async fn create_annotation( - &self, - ctx: &Context<'_>, - input: CreateAnnotationInputGql, - ) -> Result { - principal(ctx)?; - let author_kind = parse_author_kind(&input.author_kind)?; - let anchor = parse_anchor(input.anchor)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let item = cp - .create_annotation( - &input.target_id, - input.author.as_deref(), - author_kind, - input.body, - &anchor, - ) - .map_err(store_err)?; - annotation_item_gql(&item) - } - - /// Reply to an annotation thread. - async fn reply_annotation( - &self, - ctx: &Context<'_>, - input: ReplyAnnotationInputGql, - ) -> Result { - principal(ctx)?; - let author_kind = parse_author_kind(&input.author_kind)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let item = cp - .reply_to_annotation( - &input.parent_id, - input.author.as_deref(), - author_kind, - input.body, - ) - .map_err(store_err)?; - annotation_item_gql(&item) - } - - /// Resolve an annotation with a head/commit/user receipt. - async fn resolve_annotation( - &self, - ctx: &Context<'_>, - input: ResolveAnnotationInputGql, - ) -> Result> { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.resolve_annotation(&input.id, &input.by, input.receipt.as_deref()) - .map_err(store_err)? - .as_ref() - .map(annotation_item_gql) - .transpose() - } - - /// Edit an existing item's title, tags, residency, status, due date, or - /// reminder (in place by id). - #[allow(clippy::too_many_arguments)] - async fn edit_item( - &self, - ctx: &Context<'_>, - id: String, - title: Option, - tags: Option>, - residency: Option, - #[graphql(desc = "Task/work status token (e.g. open, done).")] status: Option, - #[graphql(desc = "Due instant in epoch ms; pass -1 to clear the due date.")] - due_at_ms: Option, - #[graphql(desc = "Reminder instant in epoch ms; pass -1 to clear the reminder.")] - remind_at_ms: Option, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let mut item = cp - .get_item(&id) - .map_err(store_err)? - .ok_or_else(|| Error::new("item not found"))?; - if let Some(title) = title { - item.title = title; - } - if let Some(tags) = tags { - item.tags = tags; - } - if let Some(residency) = residency { - item.residency = Residency::from(residency); - } - if let Some(status) = status { - item.status = Some(status); - } - match due_at_ms { - Some(value) if value < 0 => item.due_at_ms = None, - Some(value) => item.due_at_ms = Some(value), - None => {} - } - match remind_at_ms { - Some(value) if value < 0 => item.remind_at_ms = None, - Some(value) => item.remind_at_ms = Some(value), - None => {} - } - Ok(ItemGql::from(cp.put_item(item).map_err(store_err)?)) - } - - /// Create a manual collection. - async fn create_collection(&self, ctx: &Context<'_>, name: String) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(CollectionGql::from( - cp.create_collection(name, commonplace::CollectionKind::Manual) - .map_err(store_err)?, - )) - } - - /// Create a first-class PM project collection, optionally with the default - /// Plane-style workflow states. - async fn create_pm_project( - &self, - ctx: &Context<'_>, - input: CreateProjectInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let identifier = if input.identifier.trim().is_empty() { - identifier_from_name(&input.name) - } else { - input.identifier.trim().to_ascii_uppercase() - }; - let mut project = cp - .create_project(input.name.trim(), identifier) - .map_err(store_err)?; - if let Some(description) = input.description { - project.description = Some(description); - } - if let Some(color) = input.color { - project.color = Some(color); - } - project = cp.put_collection(project).map_err(store_err)?; - if input.default_states.unwrap_or(true) { - for (index, (name, group)) in default_state_seed().into_iter().enumerate() { - cp.create_state(&project.id, name, group, index as i64) - .map_err(store_err)?; - } - } - pm_project_from(&cp, project) - } - - /// Create a workflow state under a PM project. - async fn create_pm_state( - &self, - ctx: &Context<'_>, - input: CreateStateInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let sort_order = input.sort_order.unwrap_or(0) as i64; - let state = cp - .create_state(&input.project_id, input.name, input.group, sort_order) - .map_err(store_err)?; - Ok(PmStateGql::from(state)) - } - - /// Create a task or epic work item, with optional project/state assignment. - async fn create_work_item( - &self, - ctx: &Context<'_>, - input: CreateWorkItemInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let kind = match input.kind.as_deref() { - Some("epic") => ItemKind::Epic, - _ => ItemKind::Task, - }; - let mut item = - Item::new(kind, input.title).with_text(input.description.unwrap_or_default()); - if let Some(priority) = input.priority { - item = item.with_priority(priority); - } - if let Some(due_at_ms) = input.due_at_ms { - item = item.with_due_at(due_at_ms); - } - if let Some(estimate_point) = input.estimate_point { - item = item.with_extra("estimate_point", json!(estimate_point)); - } - let sequence_id = match input.sequence_id { - Some(value) if !value.trim().is_empty() => Some(value), - _ => next_sequence_id(&cp, input.project_id.as_deref())?, - }; - if let Some(sequence_id) = sequence_id { - item = item.with_extra("sequence_id", json!(sequence_id)); - } - let mut item = cp.put_item(item).map_err(store_err)?; - if let Some(project_id) = input.project_id { - cp.add_to_project(&item.id, &project_id) - .map_err(store_err)?; - } - if let Some(state_id) = input.state_id { - cp.set_task_state(&item.id, &state_id).map_err(store_err)?; - item = cp - .get_item(&item.id) - .map_err(store_err)? - .ok_or_else(|| Error::new("work item disappeared after state update"))?; - } - pm_work_item_from(&cp, item) - } - - /// Assign an existing work item to a project. - async fn add_work_item_to_project( - &self, - ctx: &Context<'_>, - item_id: String, - project_id: String, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.add_to_project(&item_id, &project_id) - .map_err(store_err)?; - let item = cp - .get_item(&item_id) - .map_err(store_err)? - .ok_or_else(|| Error::new("work item not found"))?; - pm_work_item_from(&cp, item) - } - - /// Assign an existing work item to a cycle. - async fn add_work_item_to_cycle( - &self, - ctx: &Context<'_>, - item_id: String, - cycle_id: String, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.add_to_cycle(&item_id, &cycle_id).map_err(store_err)?; - let item = cp - .get_item(&item_id) - .map_err(store_err)? - .ok_or_else(|| Error::new("work item not found"))?; - pm_work_item_from(&cp, item) - } - - /// Assign an existing work item to a module. - async fn add_work_item_to_module( - &self, - ctx: &Context<'_>, - item_id: String, - module_id: String, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.add_to_module(&item_id, &module_id).map_err(store_err)?; - let item = cp - .get_item(&item_id) - .map_err(store_err)? - .ok_or_else(|| Error::new("work item not found"))?; - pm_work_item_from(&cp, item) - } - - /// Assign the current workflow state for a work item. - async fn set_work_item_state( - &self, - ctx: &Context<'_>, - item_id: String, - state_id: String, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.set_task_state(&item_id, &state_id).map_err(store_err)?; - let item = cp - .get_item(&item_id) - .map_err(store_err)? - .ok_or_else(|| Error::new("work item not found"))?; - pm_work_item_from(&cp, item) - } - - /// Create a project-scoped colored label, reusing CommonPlace tags. - async fn scope_project_label( - &self, - ctx: &Context<'_>, - project_id: String, - name: String, - color: Option, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let tag = cp - .scope_label_to_project(&project_id, &name, color.clone()) - .map_err(store_err)?; - Ok(PmLabelGql { - id: tag.id, - name: tag.name, - color, - }) - } - - /// Create a dated cycle under a project. - async fn create_pm_cycle( - &self, - ctx: &Context<'_>, - input: CreateCycleInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let cycle = cp - .create_cycle( - &input.project_id, - input.name, - input.start_at_ms, - input.end_at_ms, - ) - .map_err(store_err)?; - Ok(CollectionGql::from(cycle)) - } - - /// Create a module under a project. - async fn create_pm_module( - &self, - ctx: &Context<'_>, - input: CreateModuleInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let module = cp - .create_module(&input.project_id, input.name) - .map_err(store_err)?; - Ok(CollectionGql::from(module)) - } - - /// Add a comment to an item. - async fn create_work_item_comment( - &self, - ctx: &Context<'_>, - input: CreateCommentInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let comment = cp - .create_comment(&input.item_id, input.author_id, input.body) - .map_err(store_err)?; - Ok(ItemGql::from(comment)) - } - - /// Log time against a task. - async fn log_work(&self, ctx: &Context<'_>, input: LogWorkInputGql) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let worklog = cp - .log_work( - &input.task_id, - input.duration_ms, - input.logged_by, - input.description, - ) - .map_err(store_err)?; - Ok(ItemGql::from(worklog)) - } - - /// Create a sticky note. - async fn create_sticky( - &self, - ctx: &Context<'_>, - input: CreateStickyInputGql, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let sticky = cp - .create_sticky( - input.owner_id, - input.title, - input.description.unwrap_or_default(), - input.color, - input.background_color, - input.sort_order, - ) - .map_err(store_err)?; - Ok(ItemGql::from(sticky)) - } - - /// Create a graph-native project page. Pages are `Doc` items, optionally - /// scoped to a project collection and ABOUT-linked from a work item. - async fn create_page(&self, ctx: &Context<'_>, input: CreatePageInputGql) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let title = input.title.trim().to_string(); - if title.is_empty() { - return Err(Error::new("page title is required")); - } - let mut page = Item::new(ItemKind::Doc, title).with_text(input.body.unwrap_or_default()); - if let Some(tags) = input.tags { - page = page.with_tags(tags); - } - let page = cp.put_item(page).map_err(store_err)?; - if let Some(project_id) = input.project_id { - cp.add_to_project(&page.id, &project_id) - .map_err(store_err)?; - } - if let Some(about_item_id) = input.about_item_id { - cp.link_about(&about_item_id, &page.id).map_err(store_err)?; - } - cp.get_item(&page.id) - .map_err(store_err)? - .map(ItemGql::from) - .ok_or_else(|| Error::new("page disappeared after create")) - } - - /// Save a project page's editable fields in place. - async fn save_page(&self, ctx: &Context<'_>, input: SavePageInputGql) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let mut item = cp - .get_item(&input.id) - .map_err(store_err)? - .ok_or_else(|| Error::new("page not found"))?; - if item.kind != ItemKind::Doc { - return Err(Error::new("savePage only accepts doc items")); - } - if let Some(title) = input.title { - let title = title.trim().to_string(); - if title.is_empty() { - return Err(Error::new("page title is required")); - } - item.title = title; - } - if let Some(body) = input.body { - item = item.with_text(body); - } - if let Some(tags) = input.tags { - item.tags = tags; - } - Ok(ItemGql::from(cp.put_item(item).map_err(store_err)?)) - } - - /// Persist a full Yjs page snapshot as RustyRed-owned data. The snapshot is - /// a File item whose body points at the content-addressed blob store and is - /// attached back to the page, so Hocuspocus is transport, not storage. - async fn store_page_crdt_snapshot( - &self, - ctx: &Context<'_>, - input: StorePageCrdtSnapshotInputGql, - ) -> Result { - principal(ctx)?; - let bytes = BASE64_STANDARD - .decode(input.update_base64.trim()) - .map_err(|error| Error::new(format!("invalid base64 Yjs update: {error}")))?; - let encoding = input - .encoding - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(PAGE_CRDT_ENCODING) - .to_string(); - if encoding != PAGE_CRDT_ENCODING { - return Err(Error::new(format!( - "unsupported page CRDT encoding: {encoding}" - ))); - } - - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - store_page_crdt_snapshot_bytes(&mut cp, &input.page_id, &bytes, false) - } - - /// Compact the latest page CRDT snapshot with Rust `yrs`, then write the - /// canonical full-state update back through the same RustyRed File blob. - async fn compact_page_crdt_snapshot( - &self, - ctx: &Context<'_>, - page_id: String, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let snapshot = latest_page_crdt_snapshot_item(&cp, &page_id) - .map_err(store_err)? - .ok_or_else(|| Error::new("page CRDT snapshot not found"))?; - let bytes = cp - .read_blob(&snapshot) - .map_err(store_err)? - .ok_or_else(|| Error::new("page CRDT snapshot blob not found"))?; - let compacted = compact_page_crdt_update_v1(&bytes)?; - store_page_crdt_snapshot_bytes(&mut cp, &page_id, &compacted, true) - } - - /// Add an item to a collection. - async fn add_to_collection( - &self, - ctx: &Context<'_>, - item_id: String, - collection_id: String, - ) -> Result { - principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - cp.add_to_collection(&item_id, &collection_id) - .map_err(store_err)?; - Ok(true) - } - - /// Import a JSON export document (from `export`), recreating items and - /// collections with their original ids so memberships survive. - async fn import_items(&self, ctx: &Context<'_>, data: String) -> Result { - principal(ctx)?; - let document: ExportDocument = serde_json::from_str(&data) - .map_err(|error| Error::new(format!("invalid export JSON: {error}")))?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - let summary = portability::import(&mut cp, &document).map_err(store_err)?; - Ok(ImportResultGql { - imported: summary.items as i32, - collections: summary.collections as i32, - }) - } - - /// Publish (or re-publish) an artifact to a live URL (HANDOFF-PUBLISH D1). - /// Runs the L1 shape floor; on refusal the outcome carries the conformance - /// result rather than erroring, so the publish moment can name the failure. - async fn publish( - &self, - ctx: &Context<'_>, - #[graphql(desc = "The origin artifact item id to publish.")] origin_id: String, - #[graphql(desc = "Visibility: PUBLIC, UNLISTED (default), or PRIVATE.")] visibility: Option< - publish::Visibility, - >, - ) -> Result { - let principal = principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - Ok(publish::publish_block_outcome( - &mut cp, - &origin_id, - visibility.unwrap_or(publish::Visibility::Unlisted), - &principal.id, - )) - } - - /// Unpublish by alias: the alias returns the designed gone state; the block - /// survives in the graph (HANDOFF-PUBLISH D1). - async fn unpublish(&self, ctx: &Context<'_>, alias: String) -> Result { - let principal = principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - publish::unpublish_block(&mut cp, &alias, &principal.id).map_err(|e| match e { - publish::PublishError::Forbidden => Error::new("not authorized to modify this block"), - _ => Error::new("published block not found"), - })?; - Ok(true) - } - - /// Change a published block's visibility; takes effect on next request - /// (HANDOFF-PUBLISH D3). - async fn set_block_visibility( - &self, - ctx: &Context<'_>, - alias: String, - visibility: publish::Visibility, - ) -> Result { - let principal = principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - publish::set_visibility(&mut cp, &alias, &principal.id, visibility).map_err( - |e| match e { - publish::PublishError::Forbidden => { - Error::new("not authorized to modify this block") - } - _ => Error::new("published block not found"), - }, - )?; - Ok(true) - } - - /// The doorway: reference (or fork) a published block into the caller's - /// space, with origin provenance (HANDOFF-PUBLISH D5). Returns the new item id. - async fn reference_block( - &self, - ctx: &Context<'_>, - alias: String, - #[graphql(desc = "Make an owned divergent copy instead of a reference.")] fork: Option< - bool, - >, - ) -> Result { - let principal = principal(ctx)?; - let store = shared::(ctx)?; - let mut cp = store - .lock() - .map_err(|_| Error::new("store lock poisoned"))?; - publish::reference_block(&mut cp, &alias, &principal.id, fork.unwrap_or(false)) - .map_err(|_| Error::new("published block not found")) - } -} - -/// Build the consumer schema over an instance store and its key registry, with -/// no generative answer model (ask uses the extractive fallback). -pub fn build_schema( - store: SharedStore, - registry: Arc, -) -> Schema, Mutation, EmptySubscription> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - build_schema_with_model(store, registry, Arc::new(NoModel)) -} - -/// Build the schema with a specific answer model (for example local Gemma via -/// an OpenAI-compatible endpoint) for generative answers behind the same -/// retrieval. -/// [`build_schema`] with an explicit page source for `saveUrl`. -/// -/// Production leaves this unset and the resolver builds the live fetch cascade. -/// The acceptance tests inject an offline source so they never reach the -/// network. -pub fn build_schema_with_page_source( - store: SharedStore, - registry: Arc, - page_source: Arc, -) -> Schema, Mutation, EmptySubscription> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - Schema::build(Query(PhantomData), Mutation(PhantomData), EmptySubscription) - .data(store) - .data(registry) - .data(Arc::new(NoModel) as Arc) - .data(page_source) - .data(Arc::new(FindIndexCache::new())) - .finish() -} -pub fn build_schema_with_model( - store: SharedStore, - registry: Arc, - model: Arc, -) -> Schema, Mutation, EmptySubscription> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - Schema::build(Query(PhantomData), Mutation(PhantomData), EmptySubscription) - .data(store) - .data(registry) - .data(model) - .data(Arc::new(FindIndexCache::new())) - .finish() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn graphql_int_from_usize_rejects_overflow() { - let error = graphql_int_from_usize(i32::MAX as usize + 1, "item_count") - .expect_err("overflow should be rejected"); - assert!(error - .message - .contains("item_count exceeds GraphQL Int range")); - } - - #[test] - fn ask_result_gql_rejects_overflowing_ppr_counts() { - let result = AskResultGql::try_from(AskResult { - answer: "ok".to_string(), - answer_kind: AnswerKind::Empty, - provenance: Vec::new(), - ppr_expansion: PprExpansionMeasurement { - seed_count: usize::MAX, - flat_candidate_count: 0, - ppr_candidate_count: 0, - ppr_only_candidate_count: 0, - }, - }); - let error = match result { - Ok(_) => panic!("overflow should be rejected"), - Err(error) => error, - }; - assert!(error - .message - .contains("seed_count exceeds GraphQL Int range")); - } -} diff --git a/apps/commonplace-api/src/schema/scatter.rs b/apps/commonplace-api/src/schema/scatter.rs deleted file mode 100644 index f29b6075..00000000 --- a/apps/commonplace-api/src/schema/scatter.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! GraphQL projection of the two-layer scatter (SPEC-COMMONPLACE-SEARCH-STACK -//! B5, surfaced by B7). -//! -//! ## Deviation: the scene rides inline, not behind a URL -//! -//! B7 asks for "the `SceneRef` for the compiled scene". The gateway's `SceneRef` -//! is `{ sceneId, url }` because that gateway serves `/scene/{id}` as rendered -//! HTML. `commonplace-api` has no such route, and the CommonPlace renderer -//! (`scene-host/renderers/ConstellationRenderer.tsx`) takes a `ScenePackageV2` -//! directly as a prop. Minting a URL for a route that does not exist would be a -//! placeholder, and making the client fetch it would add a round trip to get -//! back the object it already had. So `SceneRef` here carries the content -//! address plus the package itself. The id is still a stable handle for caching -//! and provenance. - -use async_graphql::{Json, SimpleObject}; -use rustyred_thg_find::{AspectId, AspectNode, FindScope, ScatterRequest, ScatterResponse}; -use serde_json::Value; - -use super::find::{FindHitGql, GraphRelationGql}; - -/// Inter-aspect similarity edge. -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "AspectEdge")] -pub struct AspectEdgeGql { - pub target: String, - pub weight: f64, -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "AspectNode")] -pub struct AspectNodeGql { - pub id: String, - pub label: String, - pub seed_hits: Vec, - pub relation: GraphRelationGql, - pub edges: Vec, -} - -impl From<&AspectNode> for AspectNodeGql { - fn from(aspect: &AspectNode) -> Self { - Self { - id: aspect.id.clone(), - label: aspect.label.clone(), - seed_hits: aspect.seed_hits.iter().map(FindHitGql::from).collect(), - relation: aspect.relation.into(), - edges: aspect - .edges - .iter() - .map(|(target, weight)| AspectEdgeGql { - target: target.clone(), - weight: *weight as f64, - }) - .collect(), - } - } -} - -/// The compiled scene: a content address plus the `ScenePackageV2` itself. -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "SceneRef")] -pub struct SceneRefGql { - pub scene_id: String, - /// The full `ScenePackageV2`, serialized. Its projection is `force_graph`. - pub package: Json, -} - -#[derive(Clone, Debug, SimpleObject)] -#[graphql(name = "ScatterResponse")] -pub struct ScatterResponseGql { - pub query: String, - pub aspects: Vec, - pub lambda: f64, - /// Which labeler named these aspects: `deterministic` or the model id. - pub labeler: String, - pub scopes_searched: Vec, - /// Set when this response is the expansion of an aspect. - pub expanded_from: Option, - /// `None` only when scene compilation refused, and then `sceneRefusal` says - /// why. Never a fabricated empty scene. - pub scene: Option, - pub scene_refusal: Option, - pub scatter_ref: String, -} - -impl From for ScatterResponseGql { - fn from(response: ScatterResponse) -> Self { - let scene = response.scene.as_ref().and_then(|package| { - serde_json::to_value(package).ok().map(|value| SceneRefGql { - scene_id: package.id.clone(), - package: Json(value), - }) - }); - Self { - query: response.query, - aspects: response.aspects.iter().map(AspectNodeGql::from).collect(), - lambda: response.lambda as f64, - labeler: response.labeler, - scopes_searched: response.scopes_searched, - expanded_from: response.expanded_from, - scene, - scene_refusal: response.scene_refusal, - scatter_ref: response.scatter_ref, - } - } -} - -/// Build the scatter request, clamping `k` to the hard aspect cap. -pub fn build_request( - query: String, - scopes: Option>, - k: Option, - lambda: Option, -) -> Result { - let scopes = match scopes { - Some(scopes) if !scopes.is_empty() => scopes - .into_iter() - .map(super::find::FindScopeInput::into_scope) - .collect::, _>>()?, - _ => vec![FindScope::Corpus], - }; - let defaults = ScatterRequest::default(); - Ok(ScatterRequest { - query, - scopes, - k: k.unwrap_or(defaults.k as i32) - .clamp(1, rustyred_thg_find::MAX_ASPECTS as i32) as usize, - lambda: lambda.unwrap_or(defaults.lambda as f64).clamp(0.0, 1.0) as f32, - ..defaults - }) -} - -/// Aspect id from a GraphQL argument, rejected early when blank so the executor -/// is never asked to expand nothing. -pub fn parse_aspect_id(aspect_id: String) -> Result { - let trimmed = aspect_id.trim(); - if trimmed.is_empty() { - Err("expand requires an aspectId".to_string()) - } else { - Ok(trimmed.to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn scatter_k_is_clamped_to_the_aspect_cap() { - let request = build_request("q".into(), None, Some(50), None).expect("request"); - assert_eq!(request.k, rustyred_thg_find::MAX_ASPECTS); - let request = build_request("q".into(), None, Some(0), None).expect("request"); - assert_eq!(request.k, 1); - } - - #[test] - fn scatter_lambda_is_clamped() { - let request = build_request("q".into(), None, None, Some(4.0)).expect("request"); - assert_eq!(request.lambda, 1.0); - } - - #[test] - fn blank_aspect_id_is_refused() { - assert!(parse_aspect_id(" ".into()).is_err()); - assert_eq!(parse_aspect_id(" a1 ".into()).expect("id"), "a1"); - } -} diff --git a/apps/commonplace-api/src/serve.rs b/apps/commonplace-api/src/serve.rs deleted file mode 100644 index f3951d35..00000000 --- a/apps/commonplace-api/src/serve.rs +++ /dev/null @@ -1,1599 +0,0 @@ -//! Shared HTTP serving surface for the CommonPlace API. -//! -//! The standalone binary and the desktop embedder both serve the same GraphQL -//! contract. The binary uses environment-driven configuration; the desktop uses -//! [`serve_loopback`] with a durable local data directory and graceful shutdown. -//! -//! Alongside `/graphql`, both routers expose the canonical capture seam: -//! `POST /ingest/capture` accepts JSON or multipart [`CaptureEnvelope`] values, -//! while legacy multipart `POST /ingest/blob` adapts into that same path. -//! `GET /blob/{hash}` returns captured bytes. All routes share one API-key -//! registry and accept either `x-api-key` or its Bearer equivalent. - -use std::collections::BTreeMap; -use std::future::Future; -use std::net::SocketAddr; -use std::path::Path; -use std::sync::{mpsc::SyncSender, Arc}; - -use async_graphql::http::GraphiQLSource; -use async_graphql::{EmptySubscription, Request, Schema}; -use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; -use axum::extract::{ - DefaultBodyLimit, FromRequest, Multipart, Path as AxumPath, Request as AxumRequest, State, -}; -use axum::http::{header, HeaderMap, StatusCode}; -use axum::response::{Html, IntoResponse, Response}; -use axum::routing::{get, post}; -use axum::{Json, Router}; -use commonplace::{ - content_hash, BlobStore, EmbeddingGraphStore, IngestBody, IngestInput, IngestPipeline, Item, - ItemBody, ItemKind, ObjectAction, ObjectActionReceipt, ObjectQuery, ObjectSet, Residency, - SourceRef, ViewDescriptor, -}; -use rustyred_thg_core::{GraphSnapshotSource, NodeRecord}; -use serde::{Deserialize, Serialize}; -use tower_http::cors::CorsLayer; - -use crate::voice::{Transcriber, Voice}; -use crate::{ - answer_model_from_env, build_schema, build_schema_with_model, in_memory_store, redcore_store, - AnswerModel, ApiKeyRegistry, ApiKeyToken, Mutation, Principal, Query, SharedStore, -}; - -/// PT-017: cap multipart capture bodies (photo/file/voice) at 32MB. -const BLOB_BODY_LIMIT_BYTES: usize = 32 * 1024 * 1024; -const MAX_CAPTURE_ATTACHMENTS: usize = 32; - -struct AppState -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - schema: Schema, Mutation, EmptySubscription>, - registry: Arc, - store: SharedStore, -} - -impl Clone for AppState -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - fn clone(&self) -> Self { - Self { - schema: self.schema.clone(), - registry: Arc::clone(&self.registry), - store: Arc::clone(&self.store), - } - } -} - -pub fn build_router(store: SharedStore, registry: Arc) -> Router -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let schema = build_schema(Arc::clone(&store), Arc::clone(®istry)); - build_public_router_from_schema(schema, registry, store) -} - -pub fn build_router_with_model( - store: SharedStore, - registry: Arc, - model: Arc, -) -> Router -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let schema = build_schema_with_model(Arc::clone(&store), Arc::clone(®istry), model); - build_public_router_from_schema(schema, registry, store) -} - -fn build_public_router_from_schema( - schema: Schema, Mutation, EmptySubscription>, - registry: Arc, - store: SharedStore, -) -> Router -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let state = AppState { - schema, - registry, - store, - }; - Router::new() - .route("/healthz", get(healthz)) - .route("/graphql", get(graphiql).post(graphql_handler::)) - .merge(blob_routes::()) - .merge(object_routes::()) - .layer(CorsLayer::permissive()) - .with_state(state) -} - -fn build_loopback_router_from_schema( - schema: Schema, Mutation, EmptySubscription>, - registry: Arc, - store: SharedStore, -) -> Router -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let state = AppState { - schema, - registry, - store, - }; - Router::new() - .route("/healthz", get(healthz)) - .route("/graphql", post(graphql_handler::)) - .merge(blob_routes::()) - .merge(object_routes::()) - .with_state(state) -} - -/// The PT-017 blob capture routes, shared by the public and loopback routers. -fn blob_routes() -> Router> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - Router::new() - .route( - "/ingest/capture", - post(ingest_capture_handler::) - .layer(DefaultBodyLimit::max(BLOB_BODY_LIMIT_BYTES)), - ) - .route( - "/ingest/blob", - post(ingest_blob_handler::).layer(DefaultBodyLimit::max(BLOB_BODY_LIMIT_BYTES)), - ) - .route("/blob/{hash}", get(blob_get_handler::)) - .route("/capabilities", get(capabilities_handler::)) - .route("/mobile/catalog", get(mobile_catalog_handler::)) - .route("/tts", post(tts_handler::)) -} - -#[derive(Serialize)] -struct NativeCapabilities { - voice_capture: bool, - voice_readback: bool, - chat_attachments: bool, - chat_url: Option, - web_search: bool, - push_registration_url: Option, - expo_project_id: Option, - capability_catalog: bool, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct NativeCatalogEntry { - id: String, - kind: String, - name: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, -} - -#[derive(Serialize)] -struct NativeCapabilityCatalog { - plugins: Vec, - skills: Vec, -} - -/// Safe capability discovery for native clients. Provider names and secrets -/// stay server-side; the client only learns whether an affordance is real. -async fn capabilities_handler( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - Ok(Json(NativeCapabilities { - voice_capture: Transcriber::from_env().is_enabled(), - voice_readback: Voice::from_env().is_ok(), - // This must only be enabled when the configured hosted ACP route - // consumes file and image content parts instead of dropping them. - chat_attachments: env_flag("COMMONPLACE_CHAT_ATTACHMENTS"), - chat_url: env_value("COMMONPLACE_CHAT_URL"), - web_search: env_flag("COMMONPLACE_WEB_SEARCH"), - push_registration_url: env_value("COMMONPLACE_PUSH_REGISTRATION_URL"), - expo_project_id: env_value("COMMONPLACE_EXPO_PROJECT_ID"), - capability_catalog: true, - })) -} - -/// Installed or configured plugins and skills available to the connected -/// runtime. The normalized response lets native clients select an exact id -/// instead of relying on an unvalidated name typed into the prompt. -async fn mobile_catalog_handler( - State(state): State>, - headers: HeaderMap, -) -> Result, StatusCode> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - let store = state - .store - .lock() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let mut entries = BTreeMap::<(String, String), NativeCatalogEntry>::new(); - for kind in ["plugin", "skill"] { - for item in store - .items_by_kind(&ItemKind::from(kind.to_string())) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - { - if item - .extra - .get("enabled") - .and_then(serde_json::Value::as_bool) - == Some(false) - { - continue; - } - let description = item - .extra - .get("description") - .and_then(serde_json::Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - .or_else(|| match &item.body { - ItemBody::Inline { text } if !text.trim().is_empty() => { - Some(text.trim().to_string()) - } - _ => None, - }); - let entry = NativeCatalogEntry { - id: item.id, - kind: kind.to_string(), - name: item.title, - description, - }; - entries.insert((entry.kind.clone(), entry.id.clone()), entry); - } - } - for entry in configured_catalog_entries() { - entries.insert((entry.kind.clone(), entry.id.clone()), entry); - } - let mut plugins = Vec::new(); - let mut skills = Vec::new(); - for entry in entries.into_values() { - match entry.kind.as_str() { - "plugin" => plugins.push(entry), - "skill" => skills.push(entry), - _ => {} - } - } - plugins.sort_by(|left, right| left.name.cmp(&right.name).then(left.id.cmp(&right.id))); - skills.sort_by(|left, right| left.name.cmp(&right.name).then(left.id.cmp(&right.id))); - Ok(Json(NativeCapabilityCatalog { plugins, skills })) -} - -fn configured_catalog_entries() -> Vec { - let Some(raw) = env_value("COMMONPLACE_MOBILE_CATALOG_JSON") else { - return Vec::new(); - }; - serde_json::from_str::>(&raw) - .unwrap_or_default() - .into_iter() - .filter(|entry| { - !entry.id.trim().is_empty() - && !entry.name.trim().is_empty() - && matches!(entry.kind.as_str(), "plugin" | "skill") - }) - .collect() -} - -fn env_value(name: &str) -> Option { - std::env::var(name) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) -} - -fn env_flag(name: &str) -> bool { - env_value(name).is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")) -} - -/// The block-view object-model seam over HTTP (SPEC-OBJECT-CONTRACT-V2). The web -/// `HttpBlockHost` reaches the same `query_object_set` / `emit_object_action` / -/// registry the Rust `CommonplaceBlockHost` uses, so a surface renders live from -/// the substrate with nothing above the `BlockHost` seam changing. -fn object_routes() -> Router> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - Router::new() - .route("/objects/query", post(objects_query_handler::)) - .route("/objects/action", post(objects_action_handler::)) - .route("/objects/views", get(objects_views_handler::)) -} - -async fn objects_query_handler( - State(state): State>, - headers: HeaderMap, - Json(query): Json, -) -> Result, StatusCode> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - let store = state - .store - .lock() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let set = store - .query_object_set(query) - .map_err(|_| StatusCode::BAD_REQUEST)?; - Ok(Json(set)) -} - -async fn objects_action_handler( - State(state): State>, - headers: HeaderMap, - Json(action): Json, -) -> Result, StatusCode> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - let actor = headers - .get("x-actor-id") - .and_then(|value| value.to_str().ok()) - .map(str::to_string); - let mut store = state - .store - .lock() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let receipt = store - .emit_object_action(action, actor) - .map_err(|_| StatusCode::BAD_REQUEST)?; - Ok(Json(receipt)) -} - -async fn objects_views_handler( - State(state): State>, - headers: HeaderMap, -) -> Result>, StatusCode> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - let store = state - .store - .lock() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let registry = store - .load_view_registry() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(registry.descriptors().to_vec())) -} - -async fn healthz() -> &'static str { - "ok" -} - -async fn graphiql() -> impl IntoResponse { - Html(GraphiQLSource::build().endpoint("/graphql").finish()) -} - -/// Resolve either supported spelling of the same API key. -/// -/// When callers send both forms they must agree. Refusing an ambiguous request -/// avoids authenticating one credential while a proxy or log records another. -fn presented_api_key(headers: &HeaderMap) -> Option<&str> { - let direct = headers - .get("x-api-key") - .and_then(|value| value.to_str().ok()) - .map(str::trim) - .filter(|value| !value.is_empty()); - let bearer = headers - .get(header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(|value| { - let (scheme, token) = value.trim().split_once(' ')?; - scheme - .eq_ignore_ascii_case("bearer") - .then_some(token.trim()) - }) - .filter(|value| !value.is_empty()); - - match (direct, bearer) { - (Some(left), Some(right)) if left == right => Some(left), - (Some(_), Some(_)) => None, - (Some(key), None) | (None, Some(key)) => Some(key), - (None, None) => None, - } -} - -/// The API-key gate shared by GraphQL, capture, blob, and object routes. -fn authorize(state: &AppState, headers: &HeaderMap) -> Result -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - presented_api_key(headers) - .and_then(|key| state.registry.resolve(key)) - .cloned() - .ok_or(StatusCode::FORBIDDEN) -} - -async fn graphql_handler( - State(state): State>, - headers: HeaderMap, - req: GraphQLRequest, -) -> Result -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - let key = presented_api_key(&headers).ok_or(StatusCode::FORBIDDEN)?; - - let request: Request = req.into_inner().data(ApiKeyToken(key.to_string())); - Ok(state.schema.execute(request).await.into()) -} - -macro_rules! flexible_string_enum { - ( - $(#[$meta:meta])* - pub enum $name:ident { - $($variant:ident => $value:literal),+ $(,)? - } - ) => { - $(#[$meta])* - #[derive(Clone, Debug, PartialEq, Eq)] - pub enum $name { - $($variant,)+ - Other(String), - } - - impl $name { - fn as_str(&self) -> &str { - match self { - $(Self::$variant => $value,)+ - Self::Other(value) => value, - } - } - } - - impl Serialize for $name { - fn serialize( - &self, - serializer: Serializer, - ) -> Result - where - Serializer: serde::Serializer, - { - serializer.serialize_str(self.as_str()) - } - } - - impl<'de> Deserialize<'de> for $name { - fn deserialize( - deserializer: Deserializer, - ) -> Result - where - Deserializer: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - let normalized = value.trim().to_ascii_lowercase(); - Ok(match normalized.as_str() { - $($value => Self::$variant,)+ - _ => Self::Other(normalized), - }) - } - } - }; -} - -flexible_string_enum! { - /// How a person or agent produced a capture. - pub enum CaptureMethod { - Clipped => "clipped", - Composed => "composed", - Dropped => "dropped", - Agent => "agent", - Screen => "screen", - Voice => "voice", - } -} - -flexible_string_enum! { - /// The client-facing object category requested for a capture. - pub enum ObjectType { - Source => "source", - Note => "note", - Clip => "clip", - Screen => "screen", - File => "file", - } -} - -flexible_string_enum! { - /// The surface that submitted a capture. - pub enum CaptureSource { - Clipper => "clipper", - Pet => "pet", - Mobile => "mobile", - Console => "console", - Agent => "agent", - Api => "api", - } -} - -/// An attachment already present in the content-addressed blob store. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AttachmentRef { - #[serde(alias = "blobHash", alias = "content_hash", alias = "contentHash")] - pub blob_hash: String, - #[serde(default, alias = "fileName")] - pub file_name: Option, - #[serde(default)] - pub mime: Option, -} - -/// The single capture contract shared by every capture surface. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CaptureEnvelope { - #[serde(alias = "clientId")] - pub client_id: String, - #[serde(default)] - pub title: Option, - #[serde(default)] - pub body: String, - #[serde(alias = "objectType")] - pub object_type: ObjectType, - #[serde(alias = "captureMethod")] - pub capture_method: CaptureMethod, - pub source: CaptureSource, - #[serde(alias = "capturedAt")] - pub captured_at: String, - #[serde(default, alias = "sourceUrl")] - pub source_url: Option, - #[serde(default, alias = "kindHint")] - pub kind_hint: Option, - #[serde(default)] - pub properties: BTreeMap, - #[serde(default)] - pub attachments: Vec, - #[serde(default, alias = "idempotencyKey")] - pub idempotency_key: Option, -} - -/// ItemGql-compatible capture receipt plus capture-specific retry metadata. -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct CaptureIngestResponse { - id: String, - kind: String, - title: String, - body_text: Option, - blob_hash: Option, - mime: Option, - source: Option, - residency: String, - tags: Vec, - collections: Vec, - classification: Option, - status: Option, - priority: Option, - due_at_ms: Option, - remind_at_ms: Option, - path: Option, - extra: serde_json::Value, - created_at_ms: i64, - updated_at_ms: i64, - created: bool, - client_id: String, -} - -impl CaptureIngestResponse { - fn from_item(item: Item, created: bool, client_id: String) -> Self { - let (body_text, blob_hash, mime) = match &item.body { - ItemBody::Inline { text } => (Some(text.clone()), None, None), - ItemBody::Blob { - content_hash, mime, .. - } => (None, Some(content_hash.clone()), mime.clone()), - ItemBody::Empty => (None, None, None), - }; - let path = item - .extra - .get("path") - .or_else(|| item.extra.get("folder_path")) - .and_then(serde_json::Value::as_str) - .map(str::to_string); - Self { - id: item.id, - kind: item.kind.as_str().to_string(), - title: item.title, - body_text, - blob_hash, - mime, - source: item.source, - residency: item.residency.as_str().to_string(), - tags: item.tags, - collections: item.collections, - classification: item.classification, - status: item.status, - priority: item.priority, - due_at_ms: item.due_at_ms, - remind_at_ms: item.remind_at_ms, - path, - extra: serde_json::Value::Object(item.extra), - created_at_ms: item.created_at_ms, - updated_at_ms: item.updated_at_ms, - created, - client_id, - } - } -} - -#[derive(Clone, Debug)] -struct UploadedAttachment { - bytes: Vec, - file_name: Option, - mime: Option, -} - -#[derive(Clone, Debug)] -struct ResolvedAttachment { - reference: AttachmentRef, - bytes: Vec, -} - -fn capture_item_kind(envelope: &CaptureEnvelope, mime: Option<&str>) -> ItemKind { - if let Some(hint) = envelope - .kind_hint - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - { - return blob_item_kind(Some(hint), mime); - } - ItemKind::from(envelope.object_type.as_str().to_string()) -} - -fn append_hash_component(buffer: &mut Vec, value: &[u8]) { - buffer.extend_from_slice(&(value.len() as u64).to_be_bytes()); - buffer.extend_from_slice(value); -} - -fn capture_marker_id( - tenant: &str, - envelope: &CaptureEnvelope, - uploads: &[UploadedAttachment], - upload_hashes: &[String], -) -> String { - let requested_key = envelope - .idempotency_key - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - .unwrap_or_else(|| { - let mut identity = b"capture-envelope-v1".to_vec(); - append_hash_component(&mut identity, envelope.body.as_bytes()); - append_hash_component( - &mut identity, - envelope.title.as_deref().unwrap_or_default().as_bytes(), - ); - append_hash_component(&mut identity, envelope.object_type.as_str().as_bytes()); - append_hash_component(&mut identity, envelope.capture_method.as_str().as_bytes()); - append_hash_component(&mut identity, envelope.source.as_str().as_bytes()); - append_hash_component(&mut identity, envelope.captured_at.as_bytes()); - append_hash_component( - &mut identity, - envelope - .source_url - .as_deref() - .unwrap_or_default() - .as_bytes(), - ); - append_hash_component( - &mut identity, - envelope.kind_hint.as_deref().unwrap_or_default().as_bytes(), - ); - for (key, value) in &envelope.properties { - append_hash_component(&mut identity, key.as_bytes()); - append_hash_component(&mut identity, value.as_bytes()); - } - for attachment in &envelope.attachments { - append_hash_component(&mut identity, attachment.blob_hash.as_bytes()); - append_hash_component( - &mut identity, - attachment - .file_name - .as_deref() - .unwrap_or_default() - .as_bytes(), - ); - append_hash_component( - &mut identity, - attachment.mime.as_deref().unwrap_or_default().as_bytes(), - ); - } - for (upload, upload_hash) in uploads.iter().zip(upload_hashes) { - append_hash_component(&mut identity, upload_hash.as_bytes()); - append_hash_component( - &mut identity, - upload.file_name.as_deref().unwrap_or_default().as_bytes(), - ); - append_hash_component( - &mut identity, - upload.mime.as_deref().unwrap_or_default().as_bytes(), - ); - } - content_hash(&identity) - }); - let effective_key = - content_hash(format!("{tenant}\0{}\0{requested_key}", envelope.client_id).as_bytes()); - format!( - "capture-idempotency:{}", - effective_key.trim_start_matches("sha256:") - ) -} - -fn existing_capture( - state: &AppState, - marker_id: &str, - client_id: &str, -) -> Result, (StatusCode, String)> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let cp = state.store.lock().map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "store lock poisoned".to_string(), - ) - })?; - let Some(marker) = cp.store().get_node(marker_id) else { - let recovered = cp - .item_by_source_ref("commonplace:capture-idempotency", marker_id) - .map_err(internal_store_error)?; - return Ok(recovered - .map(|item| CaptureIngestResponse::from_item(item, false, client_id.to_string()))); - }; - let target_id = marker - .properties - .get("target_id") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "capture idempotency marker has no target".to_string(), - ) - })?; - let item = cp - .get_item(target_id) - .map_err(internal_store_error)? - .ok_or_else(|| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "capture idempotency target is missing".to_string(), - ) - })?; - Ok(Some(CaptureIngestResponse::from_item( - item, - false, - client_id.to_string(), - ))) -} - -fn internal_store_error(error: impl std::fmt::Debug) -> (StatusCode, String) { - eprintln!("capture store failed: {error:?}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - "capture store failed".to_string(), - ) -} - -async fn ingest_capture_handler( - State(state): State>, - request: AxumRequest, -) -> Result, (StatusCode, String)> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let headers = request.headers().clone(); - let principal = - authorize(&state, &headers).map_err(|status| (status, "invalid API key".to_string()))?; - let content_type = headers - .get(header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .unwrap_or_default() - .to_ascii_lowercase(); - - let (envelope, uploads) = if content_type.starts_with("application/json") { - let Json(envelope) = Json::::from_request(request, &state) - .await - .map_err(|error| (StatusCode::BAD_REQUEST, error.to_string()))?; - (envelope, Vec::new()) - } else if content_type.starts_with("multipart/form-data") { - let mut multipart = Multipart::from_request(request, &state) - .await - .map_err(|error| (StatusCode::BAD_REQUEST, error.to_string()))?; - parse_capture_multipart(&mut multipart).await? - } else { - return Err(( - StatusCode::UNSUPPORTED_MEDIA_TYPE, - "Content-Type must be application/json or multipart/form-data".to_string(), - )); - }; - - ingest_capture_inner(&state, &principal.id, envelope, uploads) - .await - .map(Json) -} - -async fn parse_capture_multipart( - multipart: &mut Multipart, -) -> Result<(CaptureEnvelope, Vec), (StatusCode, String)> { - let mut envelope = None; - let mut uploads = Vec::new(); - while let Some(field) = multipart.next_field().await.map_err(|error| { - ( - StatusCode::BAD_REQUEST, - format!("invalid multipart body: {error}"), - ) - })? { - match field.name().unwrap_or_default() { - "capture" => { - let bytes = field.bytes().await.map_err(bad_field)?; - envelope = Some(serde_json::from_slice(&bytes).map_err(|error| { - ( - StatusCode::BAD_REQUEST, - format!("invalid capture envelope: {error}"), - ) - })?); - } - "file" => { - let file_name = field.file_name().map(str::to_string); - let mime = field.content_type().map(str::to_string); - let bytes = field.bytes().await.map_err(bad_field)?.to_vec(); - if !bytes.is_empty() { - uploads.push(UploadedAttachment { - bytes, - file_name, - mime, - }); - } - } - _ => {} - } - } - let envelope = envelope.ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - "multipart field 'capture' is required".to_string(), - ) - })?; - Ok((envelope, uploads)) -} - -async fn ingest_capture_inner( - state: &AppState, - tenant: &str, - mut envelope: CaptureEnvelope, - uploads: Vec, -) -> Result -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - if envelope.body.trim().is_empty() && !uploads.is_empty() { - envelope.body.clear(); - } - validate_capture_with_uploads(&envelope, &uploads)?; - let upload_hashes = uploads - .iter() - .map(|upload| content_hash(&upload.bytes)) - .collect::>(); - let marker_id = capture_marker_id(tenant, &envelope, &uploads, &upload_hashes); - if let Some(existing) = existing_capture(state, &marker_id, &envelope.client_id)? { - return Ok(existing); - } - - let mut attachments = { - let cp = state.store.lock().map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "store lock poisoned".to_string(), - ) - })?; - let mut resolved = Vec::with_capacity(envelope.attachments.len() + uploads.len()); - for reference in &envelope.attachments { - let bytes = cp - .blobs() - .get(&reference.blob_hash) - .map_err(internal_store_error)? - .ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - format!("attachment blob not found: {}", reference.blob_hash), - ) - })?; - resolved.push(ResolvedAttachment { - reference: reference.clone(), - bytes: if resolved.is_empty() { - bytes - } else { - Vec::new() - }, - }); - } - for (upload, expected_hash) in uploads.into_iter().zip(upload_hashes) { - let blob_hash = cp - .blobs() - .put(&upload.bytes) - .map_err(internal_store_error)?; - if blob_hash != expected_hash { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "blob store returned a non-canonical content hash".to_string(), - )); - } - resolved.push(ResolvedAttachment { - reference: AttachmentRef { - blob_hash, - file_name: upload.file_name, - mime: upload.mime, - }, - bytes: if resolved.is_empty() { - upload.bytes - } else { - Vec::new() - }, - }); - } - resolved - }; - - let primary = attachments.first(); - let primary_mime = primary.and_then(|attachment| attachment.reference.mime.as_deref()); - let kind = capture_item_kind(&envelope, primary_mime); - let is_audio = primary_mime.is_some_and(|mime| mime.starts_with("audio/")) - || matches!(&kind, ItemKind::Other(name) if name == "audio"); - if is_audio { - if let Some(primary) = primary { - let transcriber = Transcriber::from_env(); - if transcriber.is_enabled() { - match transcriber - .transcribe(&primary.bytes, primary.reference.mime.as_deref()) - .await - { - Ok(Some(transcript)) => { - envelope.body = merge_caption( - (!envelope.body.trim().is_empty()).then(|| envelope.body.clone()), - &transcript, - ); - } - Ok(None) => {} - Err(error) => eprintln!("voice transcription failed: {error}"), - } - } - } - } - - let title = envelope - .title - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - .or_else(|| { - attachments - .first() - .and_then(|attachment| attachment.reference.file_name.clone()) - }) - .unwrap_or_else(|| "Capture".to_string()); - let body = match attachments.first() { - Some(attachment) => IngestBody::Binary { - bytes: attachment.bytes.clone(), - mime: attachment.reference.mime.clone(), - kind, - text: (!envelope.body.trim().is_empty()).then(|| envelope.body.clone()), - }, - None => IngestBody::Text { - text: envelope.body.clone(), - kind, - }, - }; - let input = IngestInput { - title, - body, - source: envelope.source_url.clone(), - source_ref: Some(SourceRef::new( - "commonplace:capture-idempotency", - marker_id.clone(), - )), - residency: Residency::Local, - tags: envelope - .properties - .get("tags") - .map(|tags| { - tags.split(',') - .map(str::trim) - .filter(|tag| !tag.is_empty()) - .map(str::to_string) - .collect() - }) - .unwrap_or_default(), - task: None, - remind_at_ms: None, - due_at_ms: None, - provenance: None, - }; - - let mut cp = state.store.lock().map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "store lock poisoned".to_string(), - ) - })?; - if let Some(marker) = cp.store().get_node(&marker_id) { - let target_id = marker - .properties - .get("target_id") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "capture idempotency marker has no target".to_string(), - ) - })?; - let item = cp - .get_item(target_id) - .map_err(internal_store_error)? - .ok_or_else(|| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "capture idempotency target is missing".to_string(), - ) - })?; - return Ok(CaptureIngestResponse::from_item( - item, - false, - envelope.client_id, - )); - } - - let mut item = IngestPipeline::default() - .without_content_core() - .ingest(&mut cp, input) - .map_err(internal_store_error)? - .item; - item.extra.insert( - "capture_client_id".to_string(), - serde_json::json!(envelope.client_id), - ); - item.extra.insert( - "capture_method".to_string(), - serde_json::json!(envelope.capture_method.as_str()), - ); - item.extra.insert( - "capture_source".to_string(), - serde_json::json!(envelope.source.as_str()), - ); - item.extra.insert( - "capture_object_type".to_string(), - serde_json::json!(envelope.object_type.as_str()), - ); - item.extra.insert( - "captured_at".to_string(), - serde_json::json!(envelope.captured_at), - ); - item.extra.insert( - "capture_properties".to_string(), - serde_json::to_value(&envelope.properties).map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("capture properties failed: {error}"), - ) - })?, - ); - let attachment_refs = attachments - .drain(..) - .map(|attachment| attachment.reference) - .collect::>(); - item.extra.insert( - "capture_attachments".to_string(), - serde_json::to_value(attachment_refs).map_err(|error| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("capture attachments failed: {error}"), - ) - })?, - ); - let item = cp.put_item(item).map_err(internal_store_error)?; - cp.store_mut() - .upsert_node(NodeRecord::new( - marker_id, - ["CaptureIdempotency"], - serde_json::json!({ - "tenant": tenant, - "target_id": item.id, - }), - )) - .map_err(internal_store_error)?; - - Ok(CaptureIngestResponse::from_item( - item, - true, - envelope.client_id, - )) -} - -fn validate_capture_with_uploads( - envelope: &CaptureEnvelope, - uploads: &[UploadedAttachment], -) -> Result<(), (StatusCode, String)> { - if envelope.body.trim().is_empty() && envelope.attachments.is_empty() && uploads.is_empty() { - return Err(( - StatusCode::BAD_REQUEST, - "body or an attachment is required".to_string(), - )); - } - if envelope.client_id.trim().is_empty() { - return Err((StatusCode::BAD_REQUEST, "client_id is required".to_string())); - } - if envelope - .attachments - .len() - .checked_add(uploads.len()) - .is_none_or(|count| count > MAX_CAPTURE_ATTACHMENTS) - { - return Err(( - StatusCode::PAYLOAD_TOO_LARGE, - format!("capture supports at most {MAX_CAPTURE_ATTACHMENTS} attachments"), - )); - } - for attachment in &envelope.attachments { - let digest = attachment - .blob_hash - .strip_prefix("sha256:") - .filter(|value| { - value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) - }); - if digest.is_none() { - return Err(( - StatusCode::BAD_REQUEST, - "attachment blob_hash must be sha256:<64 hex characters>".to_string(), - )); - } - } - chrono::DateTime::parse_from_rfc3339(&envelope.captured_at).map_err(|_| { - ( - StatusCode::BAD_REQUEST, - "captured_at must be RFC3339".to_string(), - ) - })?; - Ok(()) -} - -/// The item kind for a blob capture: an explicit `kind` hint wins -/// (image|file|audio, or any custom kind); otherwise inferred from the mime. -fn blob_item_kind(hint: Option<&str>, mime: Option<&str>) -> ItemKind { - if let Some(hint) = hint.map(str::trim).filter(|value| !value.is_empty()) { - return ItemKind::from(hint.to_ascii_lowercase()); - } - match mime { - Some(mime) if mime.starts_with("image/") => ItemKind::Image, - Some(mime) if mime.starts_with("audio/") => ItemKind::Other("audio".to_string()), - _ => ItemKind::File, - } -} - -/// Multipart `POST /ingest/blob`: fields `title` (text), `kind` (optional -/// image|file|audio hint), `tags` (optional comma-separated), `text` (optional -/// caption/body), `file` (binary). Stores the bytes via the BlobStore and runs -/// the ingest pipeline, so the capture classifies/files/links like any other. -async fn ingest_blob_handler( - State(state): State>, - headers: HeaderMap, - mut multipart: Multipart, -) -> Result, (StatusCode, String)> -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - let principal = - authorize(&state, &headers).map_err(|status| (status, "invalid API key".to_string()))?; - - let mut title: Option = None; - let mut kind_hint: Option = None; - let mut tags: Vec = Vec::new(); - let mut caption: Option = None; - let mut file_bytes: Option> = None; - let mut file_name: Option = None; - let mut mime: Option = None; - - while let Some(field) = multipart.next_field().await.map_err(|error| { - ( - StatusCode::BAD_REQUEST, - format!("invalid multipart body: {error}"), - ) - })? { - let name = field.name().unwrap_or_default().to_string(); - match name.as_str() { - "title" => { - title = Some(field.text().await.map_err(bad_field)?); - } - "kind" => { - kind_hint = Some(field.text().await.map_err(bad_field)?); - } - "tags" => { - let raw = field.text().await.map_err(bad_field)?; - tags = raw - .split(',') - .map(str::trim) - .filter(|tag| !tag.is_empty()) - .map(str::to_string) - .collect(); - } - "text" => { - let raw = field.text().await.map_err(bad_field)?; - if !raw.trim().is_empty() { - caption = Some(raw); - } - } - "file" => { - file_name = field.file_name().map(str::to_string); - mime = field.content_type().map(str::to_string); - file_bytes = Some(field.bytes().await.map_err(bad_field)?.to_vec()); - } - _ => {} - } - } - - let bytes = file_bytes - .filter(|bytes| !bytes.is_empty()) - .ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - "multipart field 'file' with content is required".to_string(), - ) - })?; - let title = title - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .or(file_name.clone()) - .unwrap_or_else(|| "Capture".to_string()); - let mut legacy_key_bytes = Vec::new(); - legacy_key_bytes.extend_from_slice(title.as_bytes()); - legacy_key_bytes.push(0); - legacy_key_bytes.extend_from_slice(caption.as_deref().unwrap_or_default().as_bytes()); - legacy_key_bytes.push(0); - legacy_key_bytes.extend_from_slice(&bytes); - if kind_hint.is_none() { - kind_hint = match mime.as_deref() { - Some(value) if value.starts_with("image/") => Some("image".to_string()), - Some(value) if value.starts_with("audio/") => Some("audio".to_string()), - _ => None, - }; - } - let envelope = CaptureEnvelope { - client_id: "mobile-legacy".to_string(), - title: Some(title), - body: caption.unwrap_or_default(), - object_type: ObjectType::File, - capture_method: CaptureMethod::Composed, - source: CaptureSource::Mobile, - captured_at: chrono::Utc::now().to_rfc3339(), - source_url: None, - kind_hint, - properties: BTreeMap::from([("tags".to_string(), tags.join(","))]), - attachments: Vec::new(), - idempotency_key: Some(content_hash(&legacy_key_bytes)), - }; - let upload = UploadedAttachment { - bytes, - file_name, - mime, - }; - ingest_capture_inner(&state, &principal.id, envelope, vec![upload]) - .await - .map(Json) -} - -fn bad_field(error: axum::extract::multipart::MultipartError) -> (StatusCode, String) { - ( - StatusCode::BAD_REQUEST, - format!("invalid multipart field: {error}"), - ) -} - -/// Fold a fresh transcript into any caption the user already typed. -fn merge_caption(existing: Option, transcript: &str) -> String { - match existing { - Some(text) if !text.trim().is_empty() => format!("{text}\n\n{transcript}"), - _ => transcript.to_string(), - } -} - -/// Read-back request: the answer text and an optional per-call voice override. -#[derive(Deserialize)] -struct TtsRequest { - text: String, - #[serde(default)] - voice: Option, -} - -/// `POST /tts`: synthesize speech from text through the env-configured provider -/// (ElevenLabs by default, or a self-hosted Kokoro node). The provider key stays -/// on the server; the client only ever sees audio bytes. -async fn tts_handler( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers).map_err(|status| (status, "invalid API key".to_string()))?; - - let trimmed = request.text.trim(); - if trimmed.is_empty() { - return Err((StatusCode::BAD_REQUEST, "text is required".to_string())); - } - // Cap read-back length so a runaway answer cannot fan out a large TTS bill. - let text: String = trimmed.chars().take(5000).collect(); - - let voice = Voice::from_env().map_err(|error| (StatusCode::SERVICE_UNAVAILABLE, error))?; - let speech = voice - .synthesize(&text, request.voice.as_deref()) - .await - .map_err(|error| (StatusCode::BAD_GATEWAY, error))?; - - Ok(([(header::CONTENT_TYPE, speech.mime)], speech.bytes).into_response()) -} - -/// `GET /blob/{hash}`: the raw bytes at a content hash, served with the mime -/// recorded on the item that references the blob (fallback octet-stream). -async fn blob_get_handler( - State(state): State>, - headers: HeaderMap, - AxumPath(hash): AxumPath, -) -> Result -where - S: EmbeddingGraphStore + GraphSnapshotSource + Send + Sync + 'static, - B: BlobStore + Send + Sync + 'static, -{ - authorize(&state, &headers)?; - - let cp = state - .store - .lock() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let bytes = cp - .blobs() - .get(&hash) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .ok_or(StatusCode::NOT_FOUND)?; - let mime = cp - .all_items() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .into_iter() - .find_map(|item| { - if let ItemBody::Blob { - content_hash, mime, .. - } = &item.body - { - if content_hash == &hash { - return mime.clone(); - } - } - item.extra - .get("capture_attachments") - .and_then(serde_json::Value::as_array) - .and_then(|attachments| { - attachments.iter().find_map(|attachment| { - (attachment - .get("blob_hash") - .and_then(serde_json::Value::as_str) - == Some(hash.as_str())) - .then(|| { - attachment - .get("mime") - .and_then(serde_json::Value::as_str) - .map(str::to_string) - }) - .flatten() - }) - }) - }) - .unwrap_or_else(|| "application/octet-stream".to_string()); - drop(cp); - - Ok(([(header::CONTENT_TYPE, mime)], bytes).into_response()) -} - -pub async fn run_from_env() -> Result<(), String> { - let api_key = std::env::var("COMMONPLACE_API_KEY").unwrap_or_else(|_| "dev-key".to_string()); - let instance = - std::env::var("COMMONPLACE_INSTANCE_ID").unwrap_or_else(|_| "default".to_string()); - let registry = Arc::new(ApiKeyRegistry::new().with_key(api_key, instance)); - let port: u16 = std::env::var("PORT") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(50090); - let model = answer_model_from_env(); - - let app = match std::env::var("COMMONPLACE_DATA_DIR") { - Ok(dir) if !dir.trim().is_empty() => { - let store = redcore_store(&dir).map_err(|error| { - format!("commonplace-api open durable store at {dir}: {error:?}") - })?; - build_router_with_model(store, registry, Arc::clone(&model)) - } - _ => build_router_with_model(in_memory_store(), registry, model), - }; - - let listener = tokio::net::TcpListener::bind(("::", port)) - .await - .map_err(|error| format!("commonplace-api bind [::]:{port}: {error}"))?; - println!("commonplace-api listening on [::]:{port}"); - axum::serve(listener, app) - .await - .map_err(|error| format!("commonplace-api serve: {error}")) -} - -async fn prepare_loopback_server( - addr: SocketAddr, - data_dir: impl AsRef, - api_key: impl Into, - instance: impl Into, -) -> Result<(Router, tokio::net::TcpListener), String> { - let data_dir = data_dir.as_ref(); - let store = redcore_store(data_dir).map_err(|error| { - format!( - "commonplace-api open durable store at {}: {error:?}", - data_dir.display() - ) - })?; - let registry = Arc::new(ApiKeyRegistry::new().with_key(api_key.into(), instance.into())); - let app = build_loopback_router_from_schema( - build_schema_with_model( - Arc::clone(&store), - Arc::clone(®istry), - answer_model_from_env(), - ), - registry, - store, - ); - let listener = tokio::net::TcpListener::bind(addr) - .await - .map_err(|error| format!("commonplace-api bind {addr}: {error}"))?; - Ok((app, listener)) -} - -pub async fn serve_loopback( - addr: SocketAddr, - data_dir: impl AsRef, - api_key: impl Into, - instance: impl Into, - shutdown: impl Future + Send + 'static, -) -> Result<(), String> { - let (app, listener) = prepare_loopback_server(addr, data_dir, api_key, instance).await?; - axum::serve(listener, app) - .with_graceful_shutdown(shutdown) - .await - .map_err(|error| format!("commonplace-api serve: {error}")) -} - -pub async fn serve_loopback_with_ready( - addr: SocketAddr, - data_dir: impl AsRef, - api_key: impl Into, - instance: impl Into, - ready: SyncSender>, - shutdown: impl Future + Send + 'static, -) -> Result<(), String> { - match prepare_loopback_server(addr, data_dir, api_key, instance).await { - Ok((app, listener)) => { - let _ = ready.send(Ok(())); - axum::serve(listener, app) - .with_graceful_shutdown(shutdown) - .await - .map_err(|error| format!("commonplace-api serve: {error}")) - } - Err(error) => { - let _ = ready.send(Err(error.clone())); - Err(error) - } - } -} - -#[cfg(test)] -mod tests { - use std::time::{Duration, SystemTime, UNIX_EPOCH}; - - use axum::http::{header, StatusCode}; - use reqwest::Method; - use tokio::sync::oneshot; - - use super::prepare_loopback_server; - - #[tokio::test] - async fn loopback_router_does_not_allow_cross_origin_graphql_preflight() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let data_dir = std::env::current_dir() - .expect("cwd") - .join("target") - .join(format!("loopback-cors-{unique}")); - std::fs::create_dir_all(&data_dir).expect("create data dir"); - - let (app, listener) = prepare_loopback_server( - ([127, 0, 0, 1], 0).into(), - &data_dir, - "loopback-test-key", - "default", - ) - .await - .expect("prepare loopback server"); - let port = listener.local_addr().expect("listener addr").port(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await - .expect("serve loopback test server"); - }); - - tokio::time::sleep(Duration::from_millis(50)).await; - let client = reqwest::Client::new(); - let response = client - .request(Method::OPTIONS, format!("http://127.0.0.1:{port}/graphql")) - .header(header::ORIGIN, "https://evil.example") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") - .header( - header::ACCESS_CONTROL_REQUEST_HEADERS, - "x-api-key,content-type", - ) - .send() - .await - .expect("send preflight"); - assert!( - response - .headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_none(), - "loopback GraphQL must not opt into cross-origin browser access" - ); - - let get = client - .get(format!("http://127.0.0.1:{port}/graphql")) - .send() - .await - .expect("send graphiql probe"); - assert_eq!(get.status(), StatusCode::METHOD_NOT_ALLOWED); - - let _ = shutdown_tx.send(()); - let _ = server.await; - let _ = std::fs::remove_dir_all(&data_dir); - } -} diff --git a/apps/commonplace-api/src/voice.rs b/apps/commonplace-api/src/voice.rs deleted file mode 100644 index d9f203c6..00000000 --- a/apps/commonplace-api/src/voice.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! Voice seam: server-side speech-to-text (STT) and text-to-speech (TTS). -//! -//! Both providers are chosen from the environment so the provider key never -//! reaches the mobile bundle (a phone app is trivially decompiled; secrets stay -//! server-side). STT is OFF by default: audio captures are stored as-is unless -//! `COMMONPLACE_STT_PROVIDER` turns it on. TTS defaults to ElevenLabs and can be -//! pointed at a self-hosted Kokoro node with one env var, no client change. -//! -//! Env (real values live in a gitignored .env / keychain, never in code): -//! COMMONPLACE_STT_PROVIDER = whisper | elevenlabs (unset = off) -//! WHISPER_STT_URL (default http://127.0.0.1:8080/v1/audio/transcriptions) -//! WHISPER_STT_MODEL (default whisper-1) -//! ELEVENLABS_STT_MODEL (default scribe_v1) -//! COMMONPLACE_TTS_PROVIDER = elevenlabs | kokoro (default elevenlabs) -//! ELEVENLABS_API_KEY -- referenced by name only -//! ELEVENLABS_TTS_VOICE_ID (default 21m00Tcm4TlvDq8ikWAM, a stock voice) -//! ELEVENLABS_TTS_MODEL (default eleven_turbo_v2_5) -//! KOKORO_TTS_URL (default http://127.0.0.1:8081/v1/audio/speech) -//! KOKORO_TTS_VOICE (default bm_george) -//! KOKORO_TTS_MODEL (default kokoro) - -use std::env; -use std::time::Duration; - -use serde_json::{json, Value}; - -const ELEVENLABS_STT_URL: &str = "https://api.elevenlabs.io/v1/speech-to-text"; -const ELEVENLABS_TTS_BASE: &str = "https://api.elevenlabs.io/v1/text-to-speech"; -const DEFAULT_WHISPER_URL: &str = "http://127.0.0.1:8080/v1/audio/transcriptions"; -const DEFAULT_KOKORO_URL: &str = "http://127.0.0.1:8081/v1/audio/speech"; -const DEFAULT_ELEVENLABS_VOICE: &str = "21m00Tcm4TlvDq8ikWAM"; -const HTTP_TIMEOUT: Duration = Duration::from_secs(30); - -/// Synthesized speech bytes plus the mime the client should play them as. -pub struct Speech { - pub bytes: Vec, - pub mime: String, -} - -/// Speech-to-text provider resolved from the environment. -pub enum Transcriber { - Disabled, - Whisper { url: String, model: String }, - ElevenLabs { key: String, model: String }, -} - -impl Transcriber { - pub fn from_env() -> Self { - match env::var("COMMONPLACE_STT_PROVIDER").ok().as_deref() { - Some("whisper") => Transcriber::Whisper { - url: env::var("WHISPER_STT_URL") - .unwrap_or_else(|_| DEFAULT_WHISPER_URL.to_string()), - model: env::var("WHISPER_STT_MODEL").unwrap_or_else(|_| "whisper-1".to_string()), - }, - Some("elevenlabs") => match env::var("ELEVENLABS_API_KEY") { - Ok(key) if !key.is_empty() => Transcriber::ElevenLabs { - key, - model: env::var("ELEVENLABS_STT_MODEL") - .unwrap_or_else(|_| "scribe_v1".to_string()), - }, - _ => Transcriber::Disabled, - }, - _ => Transcriber::Disabled, - } - } - - pub fn is_enabled(&self) -> bool { - !matches!(self, Transcriber::Disabled) - } - - /// Transcribe audio bytes to text. `Ok(None)` when disabled or empty result. - pub async fn transcribe( - &self, - bytes: &[u8], - mime: Option<&str>, - ) -> Result, String> { - let name = file_name_for(mime); - match self { - Transcriber::Disabled => Ok(None), - Transcriber::Whisper { url, model } => { - let form = reqwest::multipart::Form::new() - .part("file", audio_part(bytes, name, mime)?) - .text("model", model.clone()) - .text("response_format", "json"); - let resp = client()? - .post(url) - .multipart(form) - .send() - .await - .map_err(|error| error.to_string())?; - parse_text_json(resp).await - } - Transcriber::ElevenLabs { key, model } => { - let form = reqwest::multipart::Form::new() - .part("file", audio_part(bytes, name, mime)?) - .text("model_id", model.clone()); - let resp = client()? - .post(ELEVENLABS_STT_URL) - .header("xi-api-key", key) - .multipart(form) - .send() - .await - .map_err(|error| error.to_string())?; - parse_text_json(resp).await - } - } - } -} - -/// Text-to-speech provider resolved from the environment. -pub enum Voice { - ElevenLabs { - key: String, - voice: String, - model: String, - }, - Kokoro { - url: String, - voice: String, - model: String, - }, -} - -impl Voice { - /// Resolve the configured provider. Errs when ElevenLabs is selected but no - /// key is present, so the `/tts` route fails loud instead of silently. - pub fn from_env() -> Result { - match env::var("COMMONPLACE_TTS_PROVIDER") - .ok() - .as_deref() - .unwrap_or("elevenlabs") - { - "kokoro" => Ok(Voice::Kokoro { - url: env::var("KOKORO_TTS_URL").unwrap_or_else(|_| DEFAULT_KOKORO_URL.to_string()), - voice: env::var("KOKORO_TTS_VOICE").unwrap_or_else(|_| "bm_george".to_string()), - model: env::var("KOKORO_TTS_MODEL").unwrap_or_else(|_| "kokoro".to_string()), - }), - _ => { - let key = env::var("ELEVENLABS_API_KEY") - .map_err(|_| "ELEVENLABS_API_KEY is not set".to_string())?; - if key.is_empty() { - return Err("ELEVENLABS_API_KEY is empty".to_string()); - } - Ok(Voice::ElevenLabs { - key, - voice: env::var("ELEVENLABS_TTS_VOICE_ID") - .unwrap_or_else(|_| DEFAULT_ELEVENLABS_VOICE.to_string()), - model: env::var("ELEVENLABS_TTS_MODEL") - .unwrap_or_else(|_| "eleven_turbo_v2_5".to_string()), - }) - } - } - } - - pub async fn synthesize( - &self, - text: &str, - voice_override: Option<&str>, - ) -> Result { - match self { - Voice::ElevenLabs { key, voice, model } => { - let voice_id = voice_override.unwrap_or(voice); - let resp = client()? - .post(format!("{ELEVENLABS_TTS_BASE}/{voice_id}")) - .header("xi-api-key", key) - .json(&elevenlabs_tts_body(text, model)) - .send() - .await - .map_err(|error| error.to_string())?; - collect_audio(resp).await - } - Voice::Kokoro { url, voice, model } => { - let voice_name = voice_override.unwrap_or(voice); - let resp = client()? - .post(url) - .json(&kokoro_tts_body(text, voice_name, model)) - .send() - .await - .map_err(|error| error.to_string())?; - collect_audio(resp).await - } - } - } -} - -fn client() -> Result { - reqwest::Client::builder() - .timeout(HTTP_TIMEOUT) - .build() - .map_err(|error| error.to_string()) -} - -fn audio_part( - bytes: &[u8], - name: &'static str, - mime: Option<&str>, -) -> Result { - let part = reqwest::multipart::Part::bytes(bytes.to_vec()).file_name(name); - match mime { - Some(mime) => part.mime_str(mime).map_err(|error| error.to_string()), - None => Ok(part), - } -} - -/// A filename with an extension the STT services accept, derived from the mime. -fn file_name_for(mime: Option<&str>) -> &'static str { - match mime { - Some(mime) if mime.contains("wav") => "audio.wav", - Some(mime) if mime.contains("mp3") || mime.contains("mpeg") => "audio.mp3", - Some(mime) if mime.contains("webm") => "audio.webm", - Some(mime) if mime.contains("m4a") || mime.contains("mp4") || mime.contains("aac") => { - "audio.m4a" - } - // expo-audio HIGH_QUALITY records m4a on iOS; safe default. - _ => "audio.m4a", - } -} - -fn elevenlabs_tts_body(text: &str, model: &str) -> Value { - json!({ "text": text, "model_id": model }) -} - -fn kokoro_tts_body(input: &str, voice: &str, model: &str) -> Value { - json!({ "model": model, "input": input, "voice": voice, "response_format": "mp3" }) -} - -async fn parse_text_json(resp: reqwest::Response) -> Result, String> { - let status = resp.status(); - let body = resp.text().await.map_err(|error| error.to_string())?; - if !status.is_success() { - return Err(format!( - "transcription HTTP {status}: {}", - truncate(&body, 200) - )); - } - let value: Value = serde_json::from_str(&body).map_err(|error| error.to_string())?; - let text = value - .get("text") - .and_then(Value::as_str) - .unwrap_or_default() - .trim() - .to_string(); - Ok((!text.is_empty()).then_some(text)) -} - -async fn collect_audio(resp: reqwest::Response) -> Result { - let status = resp.status(); - let mime = resp - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .map(str::to_string); - let bytes = resp.bytes().await.map_err(|error| error.to_string())?; - if !status.is_success() { - return Err(format!( - "tts HTTP {status}: {}", - truncate(&String::from_utf8_lossy(&bytes), 200) - )); - } - let mime = mime - .filter(|mime| mime.starts_with("audio/")) - .unwrap_or_else(|| "audio/mpeg".to_string()); - Ok(Speech { - bytes: bytes.to_vec(), - mime, - }) -} - -fn truncate(text: &str, max: usize) -> String { - let mut chars = text.chars(); - let truncated: String = chars.by_ref().take(max).collect(); - if chars.next().is_some() { - format!("{truncated}...") - } else { - text.to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn file_name_tracks_mime_extension() { - assert_eq!(file_name_for(Some("audio/wav")), "audio.wav"); - assert_eq!(file_name_for(Some("audio/mpeg")), "audio.mp3"); - assert_eq!(file_name_for(Some("audio/m4a")), "audio.m4a"); - assert_eq!(file_name_for(Some("audio/webm")), "audio.webm"); - // Unknown / absent mime falls back to the iOS expo-audio default. - assert_eq!(file_name_for(None), "audio.m4a"); - assert_eq!(file_name_for(Some("application/octet-stream")), "audio.m4a"); - } - - #[test] - fn tts_bodies_match_provider_wire_shapes() { - let el = elevenlabs_tts_body("hello", "eleven_turbo_v2_5"); - assert_eq!(el["text"], "hello"); - assert_eq!(el["model_id"], "eleven_turbo_v2_5"); - - let ko = kokoro_tts_body("hello", "bm_george", "kokoro"); - assert_eq!(ko["input"], "hello"); - assert_eq!(ko["voice"], "bm_george"); - assert_eq!(ko["model"], "kokoro"); - assert_eq!(ko["response_format"], "mp3"); - } - - #[test] - fn truncate_bounds_error_snippets() { - assert_eq!(truncate("short", 200), "short"); - assert_eq!(truncate("abcdef", 3), "abc..."); - assert_eq!(truncate("ééé", 2), "éé..."); - } -} diff --git a/apps/commonplace-api/tests/capture_live.rs b/apps/commonplace-api/tests/capture_live.rs deleted file mode 100644 index 20dbcd53..00000000 --- a/apps/commonplace-api/tests/capture_live.rs +++ /dev/null @@ -1,539 +0,0 @@ -//! SPEC-THEOREM-CAPTURE-2.0 D0: prove the shipped capture surface over HTTP -//! before extending it. - -use std::sync::Arc; - -use commonplace_api::{in_memory_store, serve::build_router, ApiKeyRegistry, InMemoryShared}; -use reqwest::multipart::{Form, Part}; -use serde_json::json; -use tokio::sync::oneshot; - -const KEY: &str = "capture-live-key"; - -async fn spawn_router() -> ( - String, - InMemoryShared, - oneshot::Sender<()>, - tokio::task::JoinHandle<()>, -) { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "capture-live")); - let store = in_memory_store(); - let app = build_router(Arc::clone(&store), registry); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .expect("bind ephemeral capture-live port"); - let port = listener.local_addr().expect("capture-live address").port(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await - .expect("serve capture-live router"); - }); - ( - format!("http://127.0.0.1:{port}"), - store, - shutdown_tx, - server, - ) -} - -#[tokio::test] -async fn running_capture_surface_round_trips_item_and_blob() { - let (base, _store, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - let bytes = b"capture 2.0 live smoke bytes".to_vec(); - - let health = client - .get(format!("{base}/healthz")) - .send() - .await - .expect("GET /healthz"); - assert_eq!(health.status(), reqwest::StatusCode::OK); - - let auth_probe = json!({ - "client_id": "auth-probe", - "body": "authorization must run before ingestion", - "object_type": "note", - "capture_method": "agent", - "source": "api", - "captured_at": "2026-07-28T00:00:00Z" - }); - let missing_key = client - .post(format!("{base}/ingest/capture")) - .json(&auth_probe) - .send() - .await - .expect("POST capture without API key"); - assert_eq!(missing_key.status(), reqwest::StatusCode::FORBIDDEN); - let ambiguous_key = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .bearer_auth("different-key") - .json(&auth_probe) - .send() - .await - .expect("POST capture with conflicting API keys"); - assert_eq!(ambiguous_key.status(), reqwest::StatusCode::FORBIDDEN); - - let capture = client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .multipart( - Form::new().text("title", "D0 live capture").part( - "file", - Part::bytes(bytes.clone()) - .file_name("capture.txt") - .mime_str("text/plain") - .expect("text/plain mime"), - ), - ) - .send() - .await - .expect("POST /ingest/blob"); - assert_eq!(capture.status(), reqwest::StatusCode::OK); - let item: serde_json::Value = capture.json().await.expect("capture ItemGql"); - let id = item["id"].as_str().expect("non-empty capture id"); - assert!(!id.is_empty()); - let kind = item["kind"].as_str().expect("capture kind"); - let blob_hash = item["blobHash"].as_str().expect("capture blob hash"); - - let query = client - .post(format!("{base}/objects/query")) - .header("x-api-key", KEY) - .json(&json!({ - "types": [kind], - "where": { - "kind": "eq", - "field": "id", - "value": id - }, - "live": false - })) - .send() - .await - .expect("POST /objects/query"); - assert_eq!(query.status(), reqwest::StatusCode::OK); - let set: serde_json::Value = query.json().await.expect("capture ObjectSet"); - assert!( - set["objects"] - .as_array() - .is_some_and(|objects| objects.iter().any(|object| object["id"] == id)), - "captured item resolves through /objects/query: {set}" - ); - - let blob = client - .get(format!("{base}/blob/{blob_hash}")) - .header("x-api-key", KEY) - .send() - .await - .expect("GET /blob/{hash}"); - assert_eq!(blob.status(), reqwest::StatusCode::OK); - assert_eq!(blob.bytes().await.expect("capture blob bytes"), bytes); - - let _ = shutdown.send(()); - server.await.expect("capture server task"); -} - -#[tokio::test] -async fn canonical_json_capture_is_flexible_authenticated_and_idempotent() { - let (base, store, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - let capture = json!({ - "client_id": "local-json-contract", - "title": "Canonical capture", - "body": "The same markdown must survive a retry.", - "object_type": "note", - "capture_method": " Wormhole ", - "source": "api", - "captured_at": "2026-07-27T16:30:00-04:00", - "source_url": "https://example.com/capture-contract", - "properties": { - "topic": "capture", - "tags": "contract,live" - } - }); - - let first = client - .post(format!("{base}/ingest/capture")) - .bearer_auth(KEY) - .header(reqwest::header::CONTENT_TYPE, "Application/JSON") - .body(capture.to_string()) - .send() - .await - .expect("POST JSON capture with Bearer auth"); - assert_eq!(first.status(), reqwest::StatusCode::OK); - let first: serde_json::Value = first.json().await.expect("first capture receipt"); - assert_eq!(first["created"], true); - assert_eq!(first["clientId"], "local-json-contract"); - assert_eq!(first["extra"]["capture_method"], "wormhole"); - let id = first["id"].as_str().expect("first capture id"); - { - let mut cp = store.lock().expect("capture test store"); - let marker_id = cp - .get_item(id) - .expect("read captured item") - .expect("captured item exists") - .source_ref - .expect("capture source reference") - .external_id; - let mut marker = cp - .store() - .get_node(&marker_id) - .expect("idempotency marker") - .clone(); - marker.tombstone = true; - cp.store_mut() - .upsert_node(marker) - .expect("tombstone idempotency marker"); - } - - let retry = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .json(&capture) - .send() - .await - .expect("retry JSON capture with x-api-key"); - assert_eq!(retry.status(), reqwest::StatusCode::OK); - let retry: serde_json::Value = retry.json().await.expect("retry capture receipt"); - assert_eq!(retry["id"], id); - assert_eq!(retry["created"], false); - - let mut retitled = capture.clone(); - retitled["title"] = json!("Canonical capture, retitled"); - let retitled = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .json(&retitled) - .send() - .await - .expect("same body with distinct semantic metadata"); - assert_eq!(retitled.status(), reqwest::StatusCode::OK); - let retitled: serde_json::Value = retitled.json().await.expect("retitled capture receipt"); - assert_ne!(retitled["id"], id); - assert_eq!(retitled["created"], true); - - let query = client - .post(format!("{base}/objects/query")) - .header("x-api-key", KEY) - .json(&json!({ - "types": ["note"], - "where": { - "kind": "eq", - "field": "id", - "value": id - }, - "live": false - })) - .send() - .await - .expect("query canonical capture"); - assert_eq!(query.status(), reqwest::StatusCode::OK); - let set: serde_json::Value = query.json().await.expect("canonical ObjectSet"); - assert_eq!(set["objects"][0]["id"], id); - assert_eq!( - set["objects"][0]["properties"]["capture_method"], - "wormhole" - ); - - let mut other_surface = capture.clone(); - other_surface["client_id"] = json!("local-other-surface"); - let distinct = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .json(&other_surface) - .send() - .await - .expect("same content from a distinct surface"); - assert_eq!(distinct.status(), reqwest::StatusCode::OK); - let distinct: serde_json::Value = distinct.json().await.expect("distinct capture receipt"); - assert_ne!(distinct["id"], id); - assert_eq!(distinct["created"], true); - - let invalid_reference = json!({ - "client_id": "invalid-blob-reference", - "body": "Reject malformed content-addressed references.", - "object_type": "note", - "capture_method": "agent", - "source": "api", - "captured_at": "2026-07-27T16:30:00-04:00", - "attachments": [{ - "blob_hash": "../../not-a-content-hash" - }] - }); - let invalid = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .json(&invalid_reference) - .send() - .await - .expect("POST capture with malformed blob hash"); - assert_eq!(invalid.status(), reqwest::StatusCode::BAD_REQUEST); - assert_eq!( - invalid.text().await.expect("invalid blob response"), - "attachment blob_hash must be sha256:<64 hex characters>" - ); - - let _ = shutdown.send(()); - server.await.expect("capture server task"); -} - -#[tokio::test] -async fn canonical_multipart_and_legacy_replay_share_the_capture_core() { - let (base, _store, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - let pdf = b"%PDF-1.7\ncapture contract fixture\n%%EOF".to_vec(); - let secondary = b"secondary plain-text attachment".to_vec(); - let envelope = json!({ - "client_id": "local-pet-drop", - "title": "Dropped fixture.pdf", - "body": "", - "object_type": "file", - "capture_method": "dropped", - "source": "pet", - "captured_at": "2026-07-27T20:31:00Z", - "properties": {} - }); - - let dropped = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .multipart( - Form::new() - .part( - "capture", - Part::text(envelope.to_string()) - .mime_str("application/json") - .expect("application/json mime"), - ) - .part( - "file", - Part::bytes(pdf.clone()) - .file_name("fixture.pdf") - .mime_str("application/pdf") - .expect("application/pdf mime"), - ) - .part( - "file", - Part::bytes(secondary.clone()) - .file_name("secondary.txt") - .mime_str("text/plain") - .expect("text/plain mime"), - ), - ) - .send() - .await - .expect("POST canonical multipart capture"); - assert_eq!(dropped.status(), reqwest::StatusCode::OK); - let dropped: serde_json::Value = dropped.json().await.expect("drop receipt"); - assert_eq!(dropped["extra"]["capture_method"], "dropped"); - assert_eq!(dropped["extra"]["capture_source"], "pet"); - let dropped_id = dropped["id"].as_str().expect("drop id"); - let hash = dropped["blobHash"].as_str().expect("drop blob hash"); - let secondary_hash = dropped["extra"]["capture_attachments"][1]["blob_hash"] - .as_str() - .expect("secondary blob hash"); - let bytes = client - .get(format!("{base}/blob/{hash}")) - .header("x-api-key", KEY) - .send() - .await - .expect("read dropped blob"); - assert_eq!(bytes.status(), reqwest::StatusCode::OK); - assert_eq!(bytes.bytes().await.expect("dropped blob bytes"), pdf); - let secondary_blob = client - .get(format!("{base}/blob/{secondary_hash}")) - .header("x-api-key", KEY) - .send() - .await - .expect("read secondary blob"); - assert_eq!(secondary_blob.status(), reqwest::StatusCode::OK); - assert_eq!( - secondary_blob.headers()[reqwest::header::CONTENT_TYPE], - "text/plain" - ); - assert_eq!( - secondary_blob - .bytes() - .await - .expect("secondary attachment bytes"), - secondary - ); - - let referenced = json!({ - "client_id": "local-reference-only", - "title": "Referenced fixture", - "object_type": "file", - "capture_method": "agent", - "source": "api", - "captured_at": "2026-07-27T20:32:00Z", - "attachments": [{ - "blob_hash": hash, - "file_name": "fixture.pdf", - "mime": "application/pdf" - }] - }); - let referenced = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .json(&referenced) - .send() - .await - .expect("POST attachment-only reference without body"); - assert_eq!(referenced.status(), reqwest::StatusCode::OK); - - let too_many_attachments = json!({ - "client_id": "too-many-references", - "object_type": "file", - "capture_method": "agent", - "source": "api", - "captured_at": "2026-07-27T20:32:00Z", - "attachments": (0..33) - .map(|_| json!({ "blob_hash": hash })) - .collect::>() - }); - let too_many = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .json(&too_many_attachments) - .send() - .await - .expect("POST capture above attachment-count bound"); - assert_eq!(too_many.status(), reqwest::StatusCode::PAYLOAD_TOO_LARGE); - - let repeat_drop = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .multipart( - Form::new() - .part( - "capture", - Part::text(envelope.to_string()) - .mime_str("application/json") - .expect("application/json mime"), - ) - .part( - "file", - Part::bytes(pdf.clone()) - .file_name("fixture.pdf") - .mime_str("application/pdf") - .expect("application/pdf mime"), - ) - .part( - "file", - Part::bytes(secondary.clone()) - .file_name("secondary.txt") - .mime_str("text/plain") - .expect("text/plain mime"), - ), - ) - .send() - .await - .expect("repeat canonical multipart capture"); - assert_eq!(repeat_drop.status(), reqwest::StatusCode::OK); - let repeat_drop: serde_json::Value = repeat_drop.json().await.expect("repeat drop receipt"); - assert_eq!(repeat_drop["id"], dropped_id); - assert_eq!(repeat_drop["created"], false); - - let distinct_drop = client - .post(format!("{base}/ingest/capture")) - .header("x-api-key", KEY) - .multipart( - Form::new() - .part( - "capture", - Part::text(envelope.to_string()) - .mime_str("application/json") - .expect("application/json mime"), - ) - .part( - "file", - Part::bytes(b"%PDF-1.7\ndifferent fixture\n%%EOF".to_vec()) - .file_name("fixture.pdf") - .mime_str("application/pdf") - .expect("application/pdf mime"), - ) - .part( - "file", - Part::bytes(secondary) - .file_name("secondary.txt") - .mime_str("text/plain") - .expect("text/plain mime"), - ), - ) - .send() - .await - .expect("distinct canonical multipart capture"); - assert_eq!(distinct_drop.status(), reqwest::StatusCode::OK); - let distinct_drop: serde_json::Value = - distinct_drop.json().await.expect("distinct drop receipt"); - assert_ne!(distinct_drop["id"], dropped_id); - assert_eq!(distinct_drop["created"], true); - - let boundary = "capture-live-fixed-boundary"; - let legacy_bytes = format!( - "--{boundary}\r\n\ - Content-Disposition: form-data; name=\"title\"\r\n\r\n\ - Recorded legacy request\r\n\ - --{boundary}\r\n\ - Content-Disposition: form-data; name=\"kind\"\r\n\r\n\ - file\r\n\ - --{boundary}\r\n\ - Content-Disposition: form-data; name=\"file\"; filename=\"recorded.txt\"\r\n\ - Content-Type: text/plain\r\n\r\n\ - byte-identical legacy body\r\n\ - --{boundary}--\r\n" - ) - .into_bytes(); - let send_legacy = || { - client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .header( - reqwest::header::CONTENT_TYPE, - format!("multipart/form-data; boundary={boundary}"), - ) - .body(legacy_bytes.clone()) - }; - let first = send_legacy() - .send() - .await - .expect("first recorded legacy request"); - assert_eq!(first.status(), reqwest::StatusCode::OK); - let first: serde_json::Value = first.json().await.expect("first legacy receipt"); - assert_eq!(first["created"], true); - let replay = send_legacy() - .send() - .await - .expect("byte-identical legacy replay"); - assert_eq!(replay.status(), reqwest::StatusCode::OK); - let replay: serde_json::Value = replay.json().await.expect("legacy replay receipt"); - assert_eq!(replay["id"], first["id"]); - assert_eq!(replay["created"], false); - - let inferred_image = client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .multipart( - Form::new().text("title", "Legacy image").part( - "file", - Part::bytes(vec![0x89, b'P', b'N', b'G']) - .file_name("legacy.png") - .mime_str("image/png") - .expect("image/png mime"), - ), - ) - .send() - .await - .expect("POST legacy image without kind hint"); - assert_eq!(inferred_image.status(), reqwest::StatusCode::OK); - let inferred_image: serde_json::Value = - inferred_image.json().await.expect("legacy image receipt"); - assert_eq!(inferred_image["kind"], "image"); - - let _ = shutdown.send(()); - server.await.expect("capture server task"); -} diff --git a/apps/commonplace-api/tests/collector_source_ref_acceptance.rs b/apps/commonplace-api/tests/collector_source_ref_acceptance.rs deleted file mode 100644 index 95d75bcd..00000000 --- a/apps/commonplace-api/tests/collector_source_ref_acceptance.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! FK7 acceptance for the collector's stable source identity. -//! -//! The Express peer sends parsed text through GraphQL. Repeating the same -//! source reference must update one graph item so a retry cannot duplicate -//! user content. - -use std::sync::Arc; - -use commonplace_api::{in_memory_store, serve::build_router, ApiKeyRegistry}; -use serde_json::{json, Value}; -use tokio::sync::oneshot; - -const KEY: &str = "collector-source-ref-key"; - -async fn spawn_router() -> (String, oneshot::Sender<()>, tokio::task::JoinHandle<()>) { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "collector-test")); - let app = build_router(in_memory_store(), registry); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .expect("bind ephemeral port"); - let port = listener.local_addr().expect("local addr").port(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await - .expect("serve collector source-ref test"); - }); - (format!("http://127.0.0.1:{port}"), shutdown_tx, server) -} - -async fn graphql(client: &reqwest::Client, base: &str, body: Value) -> Value { - let response = client - .post(format!("{base}/graphql")) - .header("x-api-key", KEY) - .json(&body) - .send() - .await - .expect("send GraphQL request"); - assert_eq!(response.status(), reqwest::StatusCode::OK); - let payload: Value = response.json().await.expect("GraphQL JSON"); - assert!( - payload.get("errors").is_none(), - "GraphQL request must succeed: {payload}" - ); - payload -} - -#[tokio::test] -async fn repeated_collector_source_ref_updates_one_graph_item() { - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - let mutation = r#" - mutation Ingest($input: IngestInputGql!) { - ingest(input: $input) { - id - title - bodyText - source - extra - } - } - "#; - let document_digest = format!("sha256:{}", "a".repeat(64)); - let source_ref = json!({ - "source": "upload://workspace-42/research.txt", - "externalId": format!( - "collector:sha256:batch:document:0:{document_digest}" - ) - }); - let first_provenance = json!({ - "kind": "collector", - "correlationId": "express-request-first", - "upload": { - "filename": "research.txt", - "mediaType": "text/plain", - "source": "upload://workspace-42/research.txt" - }, - "serviceFacts": { - "parser": "commonplace-text-v1", - "byteLength": 17, - "pageCount": 1 - }, - "document": { - "index": 0, - "documentDigest": document_digest, - "docAuthor": "First Author" - } - }); - - let first = graphql( - &client, - &base, - json!({ - "query": mutation, - "variables": { - "input": { - "title": "Research", - "text": "first parsed body", - "kind": "doc", - "source": "collector://first-pass", - "sourceRef": source_ref, - "provenance": first_provenance - } - } - }), - ) - .await; - let first_id = first["data"]["ingest"]["id"] - .as_str() - .expect("first item id") - .to_string(); - assert_eq!(first["data"]["ingest"]["source"], "collector://first-pass"); - assert_eq!( - first["data"]["ingest"]["extra"]["provenance"]["document"]["docAuthor"], - "First Author" - ); - assert_eq!( - first["data"]["ingest"]["extra"]["provenance"]["assertedBy"]["principalId"], - "collector-test" - ); - - let second = graphql( - &client, - &base, - json!({ - "query": mutation, - "variables": { - "input": { - "title": "Research revised", - "text": "second parsed body", - "kind": "doc", - "source": "collector://second-pass", - "sourceRef": source_ref, - "provenance": { - "kind": "collector", - "correlationId": "express-request-second", - "upload": { - "filename": "research.txt", - "mediaType": "text/plain", - "source": "upload://workspace-42/research.txt" - }, - "serviceFacts": { - "parser": "commonplace-text-v2", - "byteLength": 18, - "pageCount": 1 - }, - "document": { - "index": 0, - "documentDigest": document_digest, - "docAuthor": "Second Author", - "description": "Revised parse" - } - } - } - } - }), - ) - .await; - assert_eq!(second["data"]["ingest"]["id"], first_id); - assert_eq!(second["data"]["ingest"]["bodyText"], "second parsed body"); - assert_eq!( - second["data"]["ingest"]["source"], - "collector://second-pass" - ); - assert_eq!( - second["data"]["ingest"]["extra"]["provenance"]["correlationId"], - "express-request-second" - ); - assert_eq!( - second["data"]["ingest"]["extra"]["provenance"]["document"]["description"], - "Revised parse" - ); - - let listed = graphql( - &client, - &base, - json!({ "query": "query Items { items { id } }" }), - ) - .await; - assert_eq!(listed["data"]["items"].as_array().expect("items").len(), 1); - - let _ = shutdown.send(()); - let _ = server.await; -} diff --git a/apps/commonplace-api/tests/durable_http_acceptance.rs b/apps/commonplace-api/tests/durable_http_acceptance.rs deleted file mode 100644 index 2e7b221e..00000000 --- a/apps/commonplace-api/tests/durable_http_acceptance.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Durable backing for the HTTP surface: items written over HTTP survive a -//! restart of the `commonplace-api` binary. -//! -//! Spawns the real binary with `COMMONPLACE_DATA_DIR` set, ingests over HTTP, -//! kills it, restarts it over the same dir, and reads the item back over HTTP. -//! A process restart is the faithful test: `RedCoreGraphStore` persists at -//! commit time and its data-dir lock is process-scoped, so durability is proven -//! across processes (an in-process reopen is not a valid restart). - -use std::path::Path; -use std::process::{Child, Command, Stdio}; - -const KEY: &str = "durable-key"; - -fn port() -> u16 { - 51000 + (std::process::id() % 2000) as u16 -} - -fn spawn_server(dir: &Path, port: u16) -> Child { - Command::new(env!("CARGO_BIN_EXE_commonplace-api")) - .env("COMMONPLACE_DATA_DIR", dir) - .env("COMMONPLACE_API_KEY", KEY) - .env("PORT", port.to_string()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("spawn commonplace-api") -} - -fn curl(args: &[&str]) -> String { - let output = Command::new("curl") - .args(args) - .stderr(Stdio::null()) - .output() - .expect("run curl"); - String::from_utf8_lossy(&output.stdout).to_string() -} - -fn wait_healthy(port: u16) { - // curl's own retry loop waits for the bind (no foreground sleep needed). - curl(&[ - "-s", - "--retry", - "40", - "--retry-connrefused", - "--retry-delay", - "1", - &format!("http://127.0.0.1:{port}/healthz"), - ]); -} - -fn graphql(port: u16, query: &str) -> String { - let body = serde_json::json!({ "query": query }).to_string(); - curl(&[ - "-s", - "-X", - "POST", - &format!("http://127.0.0.1:{port}/graphql"), - "-H", - "content-type: application/json", - "-H", - &format!("x-api-key: {KEY}"), - "--data", - &body, - ]) -} - -fn stop(mut child: Child) { - let _ = child.kill(); - let _ = child.wait(); -} - -#[test] -fn http_binary_persists_items_across_restart() { - let port = port(); - let dir = std::env::temp_dir().join(format!("commonplace-http-restart-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - - // Session 1: write over HTTP, then kill the server. - let server = spawn_server(&dir, port); - wait_healthy(port); - let write = graphql( - port, - r#"mutation { putNote(title: "Durable http binary", text: "rust ownership notes") { id } }"#, - ); - assert!(write.contains("\"id\""), "putNote over HTTP: {write}"); - stop(server); - - // Session 2: restart over the same dir, read over HTTP. - let server = spawn_server(&dir, port); - wait_healthy(port); - let list = graphql(port, r#"query { items { id title } }"#); - stop(server); - - let _ = std::fs::remove_dir_all(&dir); - assert!( - list.contains("Durable http binary"), - "item persisted across an HTTP server restart: {list}" - ); -} - -// KNOWN GAP (core lane): the full auto-structuring `ingest` write-sequence does -// NOT survive an abrupt process restart, even with AofAlways -- yet it survives -// an in-process reopen, and `put_note`/`editItem` survive a restart. Suspected -// RedCore AOF-flush nuance for the multi-write ingest pattern under abrupt stop -// (rustyred-thg-core). Un-ignore when core fixes it; this asserts the target. -#[test] -#[ignore = "blocked: RedCore loses an ingest write-sequence across an abrupt process restart (core lane)"] -fn ingest_survives_restart_blocked_on_redcore() { - let port = port() + 1; - let dir = std::env::temp_dir().join(format!( - "commonplace-http-ingest-restart-{}", - std::process::id() - )); - let _ = std::fs::remove_dir_all(&dir); - - let server = spawn_server(&dir, port); - wait_healthy(port); - let write = graphql( - port, - r#"mutation { ingest(input: { title: "Durable ingest", text: "rust ownership notes", kind: "doc" }) { id } }"#, - ); - assert!(write.contains("\"id\""), "ingest over HTTP: {write}"); - stop(server); - - // DIAGNOSTIC: what is on disk after the SIGKILL, before respawn? - let aof = - std::fs::read_to_string(dir.join("graph").join("graph.aof.current")).unwrap_or_default(); - eprintln!("POST-SIGKILL AOF lines = {}", aof.lines().count()); - eprintln!( - "POST-SIGKILL manifest = {}", - std::fs::read_to_string(dir.join("graph").join("manifest.json")).unwrap_or_default() - ); - - let server = spawn_server(&dir, port); - wait_healthy(port); - let list = graphql(port, r#"query { items { id title } }"#); - stop(server); - - let _ = std::fs::remove_dir_all(&dir); - assert!( - list.contains("Durable ingest"), - "ingested item should persist across an HTTP server restart: {list}" - ); -} diff --git a/apps/commonplace-api/tests/durable_mcp_acceptance.rs b/apps/commonplace-api/tests/durable_mcp_acceptance.rs deleted file mode 100644 index b3bf7f0e..00000000 --- a/apps/commonplace-api/tests/durable_mcp_acceptance.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Durable backing: items written over MCP survive a process restart. -//! -//! Proves the `COMMONPLACE_DATA_DIR` durable path of `commonplace-mcp`: a note -//! written in one process is read back by a fresh process over the same data -//! dir (RedCoreGraphStore + DiskObjectStore), so a self-hosted instance is a -//! real durable store, not an ephemeral demo. - -use std::io::{Read, Write}; -use std::path::Path; -use std::process::{Command, Stdio}; - -fn run_session(dir: &Path, requests: &[&str]) -> String { - let mut child = Command::new(env!("CARGO_BIN_EXE_commonplace-mcp")) - .env("COMMONPLACE_DATA_DIR", dir) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .expect("spawn commonplace-mcp"); - { - let mut stdin = child.stdin.take().expect("stdin"); - for request in requests { - writeln!(stdin, "{request}").expect("write request"); - } - } // drop stdin -> EOF -> process exits, store flushes + drops. - let mut out = String::new(); - child - .stdout - .take() - .expect("stdout") - .read_to_string(&mut out) - .expect("read stdout"); - let _ = child.wait(); - out -} - -#[test] -fn mcp_persists_items_across_restart() { - let dir = std::env::temp_dir().join(format!( - "commonplace-mcp-durable-{}-{}", - std::process::id(), - env!("CARGO_PKG_NAME") - )); - let _ = std::fs::remove_dir_all(&dir); - - // Session 1: a model writes a note durably. - let first = run_session( - &dir, - &[ - r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#, - r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"put_note","arguments":{"title":"Durable note","text":"survives a restart"}}}"#, - ], - ); - assert!(first.contains("Durable note"), "write session: {first}"); - - // Session 2: a fresh process over the same data dir reads it back. - let second = run_session( - &dir, - &[ - r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#, - r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_items","arguments":{}}}"#, - ], - ); - assert!( - second.contains("Durable note"), - "the note persisted across a process restart: {second}" - ); - - let _ = std::fs::remove_dir_all(&dir); -} diff --git a/apps/commonplace-api/tests/f3_api_acceptance.rs b/apps/commonplace-api/tests/f3_api_acceptance.rs deleted file mode 100644 index 1d2780f2..00000000 --- a/apps/commonplace-api/tests/f3_api_acceptance.rs +++ /dev/null @@ -1,782 +0,0 @@ -//! F3 acceptance: the interoperability API seam. -//! -//! Plan acceptance (COMMONPLACE-CONSUMER-LOOP.md, F3): -//! "an external client with a URL and a key reads and writes items; pointing it -//! at a different instance URL connects to that instance's data; an invalid key -//! is rejected." -//! -//! Verified in-process against the live schema (the HTTP layer in main.rs is a -//! thin header -> request-data shim over the same schema.execute path). - -use std::sync::Arc; - -use async_graphql::{Request, Variables}; -use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; -use commonplace_api::{build_schema, in_memory_store, ApiKeyRegistry, ApiKeyToken, ConsumerSchema}; -use serde_json::json; -use yrs::{Doc, ReadTxn, StateVector, Text, Transact}; - -fn instance_with_key(key: &str) -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(key, "instance")); - build_schema(in_memory_store(), registry) -} - -#[tokio::test] -async fn client_with_key_reads_and_writes_items() { - let key = "valid-key"; - let schema = instance_with_key(key); - - // Write: auto-structuring ingest through the API. - let mutation = r#"mutation { - ingest(input: { title: "Ownership", text: "rust ownership and borrowing", kind: "doc" }) { - id - kind - title - collections - path - } - }"#; - let response = schema - .execute(Request::new(mutation).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "ingest errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let id = data["ingest"]["id"].as_str().unwrap().to_string(); - assert!(!id.is_empty()); - assert_eq!(data["ingest"]["kind"], "doc"); - assert!(!data["ingest"]["collections"].as_array().unwrap().is_empty()); - - // Read it back by id. - let query = format!(r#"query {{ item(id: "{id}") {{ id title kind }} }}"#); - let response = schema - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "read errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - assert_eq!(data["item"]["title"], "Ownership"); - - // Similarity search returns the item. - let search = - r#"query { search(query: "rust ownership borrowing", k: 5) { item { id } score } }"#; - let response = schema - .execute(Request::new(search).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "search errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let hits = data["search"].as_array().unwrap(); - assert!( - hits.iter() - .any(|hit| hit["item"]["id"] == serde_json::json!(id)), - "ingested item is searchable through the API" - ); -} - -#[tokio::test] -async fn embedding_space_and_vector_neighbors_are_exposed() { - let key = "valid-key"; - let schema = instance_with_key(key); - - let mutation = r#"mutation { - rust: ingest(input: { title: "Rust ownership", text: "borrow checker lifetimes ownership", kind: "doc" }) { id } - swift: ingest(input: { title: "Swift data model", text: "observable state persistence mobile data", kind: "doc" }) { id } - }"#; - let response = schema - .execute(Request::new(mutation).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "ingest errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let rust_id = data["rust"]["id"].as_str().unwrap().to_string(); - let swift_id = data["swift"]["id"].as_str().unwrap().to_string(); - - let atlas = r#"query { - embeddingSpace { - table - projection - total - rows { - identifier - x - y - category - categoryLabel - text - createdMs - communityId - epistemicStatus - } - } - }"#; - let response = schema - .execute(Request::new(atlas).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "embeddingSpace errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - assert_eq!(data["embeddingSpace"]["table"], "embedding_space"); - assert_eq!( - data["embeddingSpace"]["projection"], - "server:embedding_axes_v1" - ); - let rows = data["embeddingSpace"]["rows"].as_array().unwrap(); - assert!(rows.len() >= 2); - assert!(rows - .iter() - .any(|row| row["identifier"].as_str() == Some(rust_id.as_str()))); - assert!(rows - .iter() - .any(|row| row["identifier"].as_str() == Some(swift_id.as_str()))); - for row in rows { - assert!(row["x"].as_f64().is_some()); - assert!(row["y"].as_f64().is_some()); - assert!(row["category"].as_i64().is_some()); - assert!(row["createdMs"].as_i64().is_some()); - assert!(row["text"].as_str().unwrap().len() > 3); - } - - let neighbors = format!( - r#"query {{ vectorNeighbors(itemId: "{rust_id}", k: 4) {{ row {{ identifier }} score }} }}"# - ); - let response = schema - .execute(Request::new(neighbors).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "vectorNeighbors errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let hits = data["vectorNeighbors"].as_array().unwrap(); - assert!( - hits.iter() - .all(|hit| hit["row"]["identifier"] != serde_json::json!(rust_id)), - "seed item must not be returned as its own neighbor" - ); - assert!( - hits.iter() - .any(|hit| hit["row"]["identifier"] == serde_json::json!(swift_id)), - "second embedded item should be reachable as a vector neighbor" - ); -} - -#[tokio::test] -async fn invalid_or_missing_key_is_rejected() { - let schema = instance_with_key("good-key"); - let query = r#"query { items { id } }"#; - - // Wrong key. - let response = schema - .execute(Request::new(query).data(ApiKeyToken("wrong-key".to_string()))) - .await; - assert!( - !response.errors.is_empty(), - "an invalid key must be rejected" - ); - - // No key at all. - let response = schema.execute(Request::new(query)).await; - assert!( - !response.errors.is_empty(), - "a missing key must be rejected" - ); - - // A write with a bad key must also be rejected and must not mutate. - let mutation = r#"mutation { putNote(title: "sneaky", text: "x") { id } }"#; - let response = schema - .execute(Request::new(mutation).data(ApiKeyToken("wrong-key".to_string()))) - .await; - assert!( - !response.errors.is_empty(), - "an unauthorized write must be rejected" - ); -} - -#[tokio::test] -async fn different_instances_have_separate_data() { - let key = "shared-key"; - let instance_a = instance_with_key(key); - let instance_b = instance_with_key(key); - - // Write to instance A only. - let mutation = r#"mutation { putNote(title: "A only", text: "lives in A") { id } }"#; - let response = instance_a - .execute(Request::new(mutation).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - - // Instance B (a different instance URL) has its own, empty dataset. - let query = r#"query { items { id title } }"#; - let response = instance_b - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - let data = response.data.into_json().unwrap(); - assert_eq!( - data["items"].as_array().unwrap().len(), - 0, - "instance B does not see instance A's data" - ); - - // Instance A has exactly the one item. - let response = instance_a - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - let data = response.data.into_json().unwrap(); - let items = data["items"].as_array().unwrap(); - assert_eq!(items.len(), 1); - assert_eq!(items[0]["title"], "A only"); -} - -#[tokio::test] -async fn annotations_round_trip_through_graphql_contract() { - let key = "annotation-key"; - let schema = instance_with_key(key); - - let response = schema - .execute( - Request::new(r#"mutation { putNote(title: "Target", text: "annotate me") { id } }"#) - .data(ApiKeyToken(key.to_string())), - ) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let target_id = data["putNote"]["id"].as_str().unwrap().to_string(); - - let create = r#"mutation Create($input: CreateAnnotationInput!) { - createAnnotation(input: $input) { - id - targetId - author - authorKind - anchor - body - resolved - resolution { by receipt } - createdAtMs - } - }"#; - let response = schema - .execute( - Request::new(create) - .variables(Variables::from_json(json!({ - "input": { - "targetId": target_id, - "body": "padding wraps the header", - "anchor": { "kind": "file_line", "path": "src/App.tsx", "line": 42, "column": 9 }, - "author": "head:claude", - "authorKind": "head" - } - }))) - .data(ApiKeyToken(key.to_string())), - ) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let created = &data["createAnnotation"]; - let annotation_id = created["id"].as_str().unwrap().to_string(); - assert_eq!(created["targetId"], json!(target_id)); - assert_eq!(created["author"], "head:claude"); - assert_eq!(created["authorKind"], "head"); - assert_eq!(created["anchor"]["kind"], "file_line"); - assert_eq!(created["anchor"]["line"], 42); - assert_eq!(created["resolved"], false); - assert!(created["createdAtMs"].as_i64().unwrap() > 0); - - let reply = r#"mutation Reply($input: ReplyAnnotationInput!) { - replyAnnotation(input: $input) { - id - targetId - authorKind - anchor - body - resolved - createdAtMs - } - }"#; - let response = schema - .execute( - Request::new(reply) - .variables(Variables::from_json(json!({ - "input": { - "parentId": annotation_id, - "body": "on it", - "author": "user:travis", - "authorKind": "user" - } - }))) - .data(ApiKeyToken(key.to_string())), - ) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["replyAnnotation"]["targetId"], json!(annotation_id)); - assert_eq!(data["replyAnnotation"]["authorKind"], "user"); - assert!(data["replyAnnotation"]["anchor"].is_null()); - - let list = r#"query List($targetId: String!) { - annotationsForTarget(targetId: $targetId) { - id - targetId - authorKind - anchor - body - resolved - } - }"#; - let response = schema - .execute( - Request::new(list) - .variables(Variables::from_json(json!({ "targetId": target_id }))) - .data(ApiKeyToken(key.to_string())), - ) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let annotations = data["annotationsForTarget"].as_array().unwrap(); - assert_eq!(annotations.len(), 1); - assert_eq!(annotations[0]["id"], json!(annotation_id)); - - let resolve = r#"mutation Resolve($input: ResolveAnnotationInput!) { - resolveAnnotation(input: $input) { - id - resolved - resolution { by receipt } - } - }"#; - let response = schema - .execute( - Request::new(resolve) - .variables(Variables::from_json(json!({ - "input": { - "id": annotation_id, - "by": "commit", - "receipt": "abc123" - } - }))) - .data(ApiKeyToken(key.to_string())), - ) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["resolveAnnotation"]["resolved"], true); - assert_eq!(data["resolveAnnotation"]["resolution"]["by"], "commit"); - assert_eq!(data["resolveAnnotation"]["resolution"]["receipt"], "abc123"); -} - -#[tokio::test] -async fn growth_snapshot_is_a_typed_authenticated_query() { - let key = "growth-key"; - let schema = instance_with_key(key); - let query = r#"query { - growthSnapshot { - available - message - snapshot { - schemaVersion - source - tenantSlug - xp { total byContext { leaf xp } } - card { stats { form } bundle { manifest { ownerPublicFingerprint } faceSvg } } - } - } - }"#; - let response = schema - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "Growth query errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let result = &data["growthSnapshot"]; - if result["available"].as_bool() == Some(true) { - assert_eq!(result["snapshot"]["schemaVersion"], 1); - assert_eq!(result["snapshot"]["source"], "live"); - } else { - assert!(result["snapshot"].is_null()); - assert!(result["message"] - .as_str() - .is_some_and(|message| !message.is_empty())); - } - - let unauthorized = schema.execute(Request::new(query)).await; - assert!( - !unauthorized.errors.is_empty(), - "Growth query requires an API key" - ); -} - -#[tokio::test] -async fn pm_overview_exposes_project_state_and_work_items() { - let key = "pm-key"; - let schema = instance_with_key(key); - - let mutation = r##"mutation { - createPmProject(input: { - name: "CommonPlace" - identifier: "CP" - color: "#2D5F6B" - defaultStates: true - }) { - collection { id name kind identifier featureFlags } - states { id name group sortOrder } - openItemCount - } - }"##; - let response = schema - .execute(Request::new(mutation).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let project_id = data["createPmProject"]["collection"]["id"] - .as_str() - .unwrap() - .to_string(); - let backlog_state = data["createPmProject"]["states"][0]["id"] - .as_str() - .unwrap() - .to_string(); - assert_eq!(data["createPmProject"]["collection"]["kind"], "project"); - assert_eq!(data["createPmProject"]["collection"]["identifier"], "CP"); - assert_eq!( - data["createPmProject"]["collection"]["featureFlags"]["states"], - true - ); - - let create_item = format!( - r#"mutation {{ - createWorkItem(input: {{ - title: "Wire PM overview" - description: "Graph-native Plane parity" - projectId: "{project_id}" - stateId: "{backlog_state}" - priority: "high" - estimatePoint: 3 - }}) {{ - sequenceId - state {{ id name group }} - item {{ id kind title status priority extra }} - projectIds - commentCount - totalWorklogDurationMs - }} - }}"# - ); - let response = schema - .execute(Request::new(create_item).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let item_id = data["createWorkItem"]["item"]["id"] - .as_str() - .unwrap() - .to_string(); - assert_eq!(data["createWorkItem"]["sequenceId"], "CP-1"); - assert_eq!(data["createWorkItem"]["state"]["name"], "Backlog"); - assert_eq!(data["createWorkItem"]["item"]["status"], "Backlog"); - assert_eq!(data["createWorkItem"]["item"]["priority"], "high"); - assert_eq!(data["createWorkItem"]["item"]["extra"]["estimate_point"], 3); - assert_eq!(data["createWorkItem"]["projectIds"][0], project_id); - - let create_page = format!( - r#"mutation {{ - createPage(input: {{ - projectId: "{project_id}" - aboutItemId: "{item_id}" - title: "PM design notes" - body: "The page and the task share one graph." - tags: ["plane-parity"] - }}) {{ id kind title bodyText collections tags }} - }}"# - ); - let response = schema - .execute(Request::new(create_page).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let page_id = data["createPage"]["id"].as_str().unwrap().to_string(); - assert_eq!(data["createPage"]["kind"], "doc"); - assert_eq!(data["createPage"]["collections"][0], project_id); - assert_eq!(data["createPage"]["tags"][0], "plane-parity"); - - let save_page = format!( - r#"mutation {{ - savePage(input: {{ - id: "{page_id}" - title: "PM design notes v2" - body: "Updated through the Pages contract." - }}) {{ id title bodyText }} - }}"# - ); - let response = schema - .execute(Request::new(save_page).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["savePage"]["title"], "PM design notes v2"); - assert_eq!( - data["savePage"]["bodyText"], - "Updated through the Pages contract." - ); - - let planning = format!( - r#"mutation {{ - cycle: createPmCycle(input: {{ - projectId: "{project_id}" - name: "June cycle" - startAtMs: 1720000000000 - endAtMs: 1720600000000 - }}) {{ id name kind }} - module: createPmModule(input: {{ - projectId: "{project_id}" - name: "Data views" - }}) {{ id name kind }} - }}"# - ); - let response = schema - .execute(Request::new(planning).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let cycle_id = data["cycle"]["id"].as_str().unwrap().to_string(); - let module_id = data["module"]["id"].as_str().unwrap().to_string(); - assert_eq!(data["cycle"]["kind"], "cycle"); - assert_eq!(data["module"]["kind"], "module"); - - let mutations = format!( - r#"mutation {{ - createWorkItemComment(input: {{ - itemId: "{item_id}" - body: "Ready for review." - authorId: "member:travis" - }}) {{ id kind title bodyText }} - logWork(input: {{ - taskId: "{item_id}" - durationMs: 1800000 - loggedBy: "member:travis" - description: "API contract test" - }}) {{ id kind extra }} - addWorkItemToCycle(itemId: "{item_id}", cycleId: "{cycle_id}") {{ - cycleIds - }} - addWorkItemToModule(itemId: "{item_id}", moduleId: "{module_id}") {{ - moduleIds - }} - }}"# - ); - let response = schema - .execute(Request::new(mutations).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["addWorkItemToCycle"]["cycleIds"][0], cycle_id); - assert_eq!(data["addWorkItemToModule"]["moduleIds"][0], module_id); - - let query = format!( - r#"query {{ - pmOverview(projectId: "{project_id}") {{ - projects {{ - collection {{ id name kind }} - states {{ name group }} - cycles {{ id name kind }} - modules {{ id name kind }} - workItemCount - openItemCount - }} - workItems {{ - sequenceId - cycleIds - moduleIds - commentCount - worklogCount - totalWorklogDurationMs - aboutIds - item {{ id title status }} - }} - pages {{ id kind title bodyText }} - }} - }}"# - ); - let response = schema - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["pmOverview"]["projects"][0]["workItemCount"], 1); - assert_eq!(data["pmOverview"]["projects"][0]["openItemCount"], 1); - assert_eq!( - data["pmOverview"]["projects"][0]["cycles"][0]["id"], - cycle_id - ); - assert_eq!( - data["pmOverview"]["projects"][0]["modules"][0]["id"], - module_id - ); - assert_eq!(data["pmOverview"]["workItems"][0]["sequenceId"], "CP-1"); - assert_eq!(data["pmOverview"]["workItems"][0]["cycleIds"][0], cycle_id); - assert_eq!( - data["pmOverview"]["workItems"][0]["moduleIds"][0], - module_id - ); - assert_eq!(data["pmOverview"]["workItems"][0]["commentCount"], 1); - assert_eq!(data["pmOverview"]["workItems"][0]["worklogCount"], 1); - assert_eq!( - data["pmOverview"]["workItems"][0]["totalWorklogDurationMs"], - 1_800_000 - ); - assert_eq!(data["pmOverview"]["workItems"][0]["aboutIds"][0], page_id); - assert_eq!(data["pmOverview"]["pages"][0]["id"], page_id); - assert_eq!( - data["pmOverview"]["pages"][0]["title"], - "PM design notes v2" - ); -} - -#[tokio::test] -async fn page_crdt_snapshots_are_owned_by_rustyred_files() { - let key = "valid-key"; - let schema = instance_with_key(key); - - let create_page = r#"mutation { - createPage(input: { title: "Collaborative page", body: "" }) { - id - kind - } - }"#; - let response = schema - .execute(Request::new(create_page).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let page_id = data["createPage"]["id"].as_str().unwrap().to_string(); - - let update_bytes = { - let doc = Doc::new(); - let text = doc.get_or_insert_text("content"); - let mut txn = doc.transact_mut(); - text.insert(&mut txn, 0, "CommonPlace collaborative text"); - drop(txn); - let update = doc - .transact() - .encode_state_as_update_v1(&StateVector::default()); - update - }; - let update_base64 = BASE64_STANDARD.encode(&update_bytes); - let store_snapshot = format!( - r#"mutation {{ - storePageCrdtSnapshot(input: {{ - pageId: "{page_id}" - updateBase64: "{update_base64}" - encoding: "yjs-update-v1" - }}) {{ - pageId - updateBase64 - encoding - byteLen - blobHash - item {{ - id - kind - mime - source - blobHash - extra - }} - }} - }}"# - ); - let response = schema - .execute(Request::new(store_snapshot).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - let snapshot = &data["storePageCrdtSnapshot"]; - assert_eq!(snapshot["pageId"], page_id); - assert_eq!(snapshot["updateBase64"], update_base64); - assert_eq!(snapshot["encoding"], "yjs-update-v1"); - assert!(snapshot["byteLen"].as_i64().unwrap() > 0); - assert_eq!(snapshot["item"]["kind"], "file"); - assert_eq!( - snapshot["item"]["mime"], - "application/vnd.commonplace.yjs-update-v1" - ); - assert_eq!(snapshot["item"]["source"], "commonplace.page_crdt_snapshot"); - assert_eq!( - snapshot["item"]["extra"]["content_role"], - "page_crdt_snapshot" - ); - assert_eq!(snapshot["item"]["extra"]["page_id"], page_id); - assert_eq!(snapshot["item"]["extra"]["hidden"], true); - assert_eq!(snapshot["blobHash"], snapshot["item"]["blobHash"]); - - let query_snapshot = format!( - r#"query {{ - pageCrdtSnapshot(pageId: "{page_id}") {{ - pageId - updateBase64 - encoding - item {{ id kind extra }} - }} - }}"# - ); - let response = schema - .execute(Request::new(query_snapshot).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["pageCrdtSnapshot"]["pageId"], page_id); - assert_eq!(data["pageCrdtSnapshot"]["updateBase64"], update_base64); - assert_eq!( - data["pageCrdtSnapshot"]["item"]["extra"]["path"], - format!(".commonplace/pages/{page_id}/content.yjs") - ); - - let compact = format!( - r#"mutation {{ - compactPageCrdtSnapshot(pageId: "{page_id}") {{ - pageId - updateBase64 - encoding - byteLen - item {{ id kind extra }} - }} - }}"# - ); - let response = schema - .execute(Request::new(compact).data(ApiKeyToken(key.to_string()))) - .await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let data = response.data.into_json().unwrap(); - assert_eq!(data["compactPageCrdtSnapshot"]["pageId"], page_id); - assert_eq!(data["compactPageCrdtSnapshot"]["encoding"], "yjs-update-v1"); - assert_eq!( - data["compactPageCrdtSnapshot"]["item"]["extra"]["compacted"], - true - ); - assert!(!BASE64_STANDARD - .decode( - data["compactPageCrdtSnapshot"]["updateBase64"] - .as_str() - .unwrap() - ) - .unwrap() - .is_empty()); -} diff --git a/apps/commonplace-api/tests/find_acceptance.rs b/apps/commonplace-api/tests/find_acceptance.rs deleted file mode 100644 index 120c56ad..00000000 --- a/apps/commonplace-api/tests/find_acceptance.rs +++ /dev/null @@ -1,351 +0,0 @@ -//! B7 acceptance: the GraphQL find surface and the saveUrl mutation. -//! -//! Spec acceptance (SPEC-COMMONPLACE-SEARCH-STACK-1.0, B7): -//! "authed `find` over a seeded store returns lane-attributed hits; `saveUrl` -//! followed by `find` at Corpus scope surfaces the saved page; the saved item -//! appears in the existing `briefing` query; unauthed requests are rejected by -//! the existing key auth." -//! -//! The save path uses an offline page source, so these tests never reach the -//! network. The seam is real: production builds the live fetch cascade. - -use std::sync::Arc; - -use async_graphql::Request; -use commonplace_api::save_url::PageSource; -use commonplace_api::schema::build_schema_with_page_source; -use commonplace_api::{in_memory_store, ApiKeyRegistry, ApiKeyToken, ConsumerSchema}; - -const KEY: &str = "key"; -const SAVED_URL: &str = "https://example.test/membrane-admission"; - -fn instance() -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - let pages = PageSource::fixture([( - SAVED_URL, - "Membrane admission", - "the membrane admits candidates under a token budget and defers the rest", - )]); - build_schema_with_page_source(in_memory_store(), registry, Arc::new(pages)) -} - -async fn exec(schema: &ConsumerSchema, query: impl Into) -> serde_json::Value { - let response = schema - .execute(Request::new(query).data(ApiKeyToken(KEY.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "gql errors: {:?}", - response.errors - ); - response.data.into_json().unwrap() -} - -async fn exec_unauthed(schema: &ConsumerSchema, query: impl Into) -> Vec { - let response = schema.execute(Request::new(query)).await; - response - .errors - .into_iter() - .map(|error| error.message) - .collect() -} - -const FIND_QUERY: &str = r#" -query Find($q: String!, $lanes: [FindLane!]) { - find(query: $q, lanes: $lanes, k: 20) { - query - lambda - retrievalRef - scopesSearched - lanes { lane seeded admitted degradedReason } - results { - score - relation - edges { id type } - hit { doc lane title source snippet byteRange { start end } scope { kind } } - } - } -} -"#; - -async fn find(schema: &ConsumerSchema, query: &str) -> serde_json::Value { - let request = Request::new(FIND_QUERY) - .variables(async_graphql::Variables::from_json( - serde_json::json!({ "q": query }), - )) - .data(ApiKeyToken(KEY.to_string())); - let response = schema.execute(request).await; - assert!( - response.errors.is_empty(), - "gql errors: {:?}", - response.errors - ); - response.data.into_json().unwrap()["find"].clone() -} - -async fn seed_docs(schema: &ConsumerSchema) { - exec( - schema, - r#"mutation { ingest(input: { title: "Membrane notes", text: "the membrane admits candidates and defers the rest", kind: "doc" }) { id } }"#, - ) - .await; - exec( - schema, - r#"mutation { ingest(input: { title: "Frontier", text: "trigram indexes narrow the scan before verification", kind: "doc" }) { id } }"#, - ) - .await; -} - -#[tokio::test] -async fn authed_find_over_a_seeded_store_returns_lane_attributed_hits() { - let schema = instance(); - seed_docs(&schema).await; - - let found = find(&schema, "candidates").await; - let results = found["results"].as_array().expect("results"); - assert!(!results.is_empty(), "seeded store returned no hits"); - - for result in results { - let lane = result["hit"]["lane"].as_str().expect("lane"); - assert!( - ["EXACT", "LEXICAL", "SEMANTIC", "STRUCTURAL"].contains(&lane), - "unexpected lane {lane}" - ); - assert_eq!(result["hit"]["scope"]["kind"], "CORPUS"); - let relation = result["relation"].as_str().expect("relation"); - assert!( - ["KNOWN", "EXTENDS", "CONTRADICTS", "ORPHAN"].contains(&relation), - "unexpected relation {relation}" - ); - assert!(result["edges"].is_array(), "edges must always be present"); - assert!(result["hit"]["byteRange"]["start"].is_number()); - } - - assert_eq!(found["scopesSearched"], serde_json::json!(["corpus"])); - assert_eq!(found["lanes"].as_array().expect("lanes").len(), 4); - assert!(found["retrievalRef"] - .as_str() - .expect("retrievalRef") - .starts_with("find:retrieval:")); -} - -#[tokio::test] -async fn disabling_a_lane_through_graphql_removes_only_that_lanes_hits() { - let schema = instance(); - seed_docs(&schema).await; - - let all = find(&schema, "candidates").await; - let request = Request::new(FIND_QUERY) - .variables(async_graphql::Variables::from_json(serde_json::json!({ - "q": "candidates", - "lanes": ["LEXICAL", "SEMANTIC", "STRUCTURAL"], - }))) - .data(ApiKeyToken(KEY.to_string())); - let response = schema.execute(request).await; - assert!(response.errors.is_empty(), "{:?}", response.errors); - let without_exact = response.data.into_json().unwrap()["find"].clone(); - - let lanes_of = |value: &serde_json::Value| -> Vec { - value["results"] - .as_array() - .expect("results") - .iter() - .map(|result| result["hit"]["lane"].as_str().unwrap_or_default().to_string()) - .collect() - }; - assert!(lanes_of(&all).iter().any(|lane| lane == "EXACT")); - assert!(!lanes_of(&without_exact).iter().any(|lane| lane == "EXACT")); - - let exact_receipt = without_exact["lanes"] - .as_array() - .expect("lanes") - .iter() - .find(|receipt| receipt["lane"] == "EXACT") - .expect("exact receipt") - .clone(); - assert_eq!(exact_receipt["degradedReason"], "lane disabled by request"); -} - -#[tokio::test] -async fn save_url_then_find_at_corpus_scope_surfaces_the_saved_page() { - let schema = instance(); - - let saved = exec( - &schema, - format!( - r#"mutation {{ saveUrl(url: "{SAVED_URL}") {{ itemId collectionId collectionName title url }} }}"# - ), - ) - .await; - let receipt = &saved["saveUrl"]; - let item_id = receipt["itemId"].as_str().expect("itemId").to_string(); - assert_eq!(receipt["url"], SAVED_URL); - assert_eq!(receipt["title"], "Membrane admission"); - - let found = find(&schema, "membrane").await; - let docs: Vec = found["results"] - .as_array() - .expect("results") - .iter() - .map(|result| result["hit"]["doc"].as_str().unwrap_or_default().to_string()) - .collect(); - assert!( - docs.contains(&item_id), - "saved page {item_id} not found at Corpus scope; got {docs:?}" - ); -} - -#[tokio::test] -async fn save_url_returns_the_real_collection_name_from_the_ingest_receipt() { - let schema = instance(); - let saved = exec( - &schema, - format!(r#"mutation {{ saveUrl(url: "{SAVED_URL}") {{ collectionId collectionName }} }}"#), - ) - .await; - let name = saved["saveUrl"]["collectionName"] - .as_str() - .expect("collectionName"); - assert!(!name.trim().is_empty(), "collection name was blank"); - for placeholder in ["placeholder", "unknown", "untitled", "default", "todo"] { - assert!( - !name.to_ascii_lowercase().contains(placeholder), - "collection name {name:?} looks like a placeholder" - ); - } - assert!(!saved["saveUrl"]["collectionId"] - .as_str() - .expect("collectionId") - .is_empty()); -} - -#[tokio::test] -async fn saved_item_appears_in_the_existing_briefing_query() { - let schema = instance(); - let saved = exec( - &schema, - format!(r#"mutation {{ saveUrl(url: "{SAVED_URL}") {{ itemId }} }}"#), - ) - .await; - let item_id = saved["saveUrl"]["itemId"].as_str().expect("itemId"); - - let briefing = exec(&schema, "{ briefing { recent { id title } } }").await; - let recent = briefing["briefing"]["recent"] - .as_array() - .expect("recent") - .iter() - .map(|item| item["id"].as_str().unwrap_or_default().to_string()) - .collect::>(); - assert!( - recent.iter().any(|id| id == item_id), - "saved item missing from briefing; got {recent:?}" - ); -} - -#[tokio::test] -async fn save_url_refuses_a_page_it_cannot_fetch() { - let schema = instance(); - let errors = schema - .execute( - Request::new(r#"mutation { saveUrl(url: "https://example.test/absent") { itemId } }"#) - .data(ApiKeyToken(KEY.to_string())), - ) - .await - .errors - .into_iter() - .map(|error| error.message) - .collect::>(); - assert!( - errors.iter().any(|message| message.contains("page_not_found")), - "expected a named fetch failure, got {errors:?}" - ); -} - -#[tokio::test] -async fn unauthed_find_is_rejected_by_key_auth() { - let schema = instance(); - let errors = exec_unauthed(&schema, r#"{ find(query: "candidates") { query } }"#).await; - assert!( - errors.iter().any(|message| message.contains("API key")), - "expected a key auth refusal, got {errors:?}" - ); -} - -#[tokio::test] -async fn unauthed_save_url_is_rejected_by_key_auth() { - let schema = instance(); - let errors = exec_unauthed( - &schema, - format!(r#"mutation {{ saveUrl(url: "{SAVED_URL}") {{ itemId }} }}"#), - ) - .await; - assert!( - errors.iter().any(|message| message.contains("API key")), - "expected a key auth refusal, got {errors:?}" - ); -} - -#[tokio::test] -async fn scatter_never_exceeds_eight_aspects_and_carries_its_scene() { - let schema = instance(); - seed_docs(&schema).await; - - let scattered = exec( - &schema, - r#"{ scatter(query: "membrane", k: 50) { aspects { id label relation edges { target weight } } lambda labeler scatterRef sceneRefusal scene { sceneId } } }"#, - ) - .await; - let response = &scattered["scatter"]; - let aspects = response["aspects"].as_array().expect("aspects"); - assert!(aspects.len() <= 8, "scatter returned {} aspects", aspects.len()); - for aspect in aspects { - let label = aspect["label"].as_str().expect("label"); - assert!(!label.trim().is_empty(), "aspect label was blank"); - } - // The scene either compiled or said why. Never a fabricated empty scene. - assert!( - response["scene"].is_object() || response["sceneRefusal"].is_string(), - "scatter returned neither a scene nor a refusal" - ); - assert!(response["scatterRef"].as_str().is_some()); -} - -#[tokio::test] -async fn expand_refuses_a_blank_aspect_id() { - let schema = instance(); - let errors = schema - .execute( - Request::new(r#"{ expand(query: "membrane", aspectId: " ") { scatterRef } }"#) - .data(ApiKeyToken(KEY.to_string())), - ) - .await - .errors - .into_iter() - .map(|error| error.message) - .collect::>(); - assert!( - errors.iter().any(|message| message.contains("aspectId")), - "expected a named refusal, got {errors:?}" - ); -} - -#[tokio::test] -async fn page_scope_without_a_node_id_is_refused() { - let schema = instance(); - let errors = schema - .execute( - Request::new( - r#"{ find(query: "membrane", scopes: [{ kind: PAGE }]) { retrievalRef } }"#, - ) - .data(ApiKeyToken(KEY.to_string())), - ) - .await - .errors - .into_iter() - .map(|error| error.message) - .collect::>(); - assert!( - errors.iter().any(|message| message.contains("nodeId")), - "expected a named refusal, got {errors:?}" - ); -} diff --git a/apps/commonplace-api/tests/i1_ask_acceptance.rs b/apps/commonplace-api/tests/i1_ask_acceptance.rs deleted file mode 100644 index 7a36656f..00000000 --- a/apps/commonplace-api/tests/i1_ask_acceptance.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! I1 acceptance: ask over your store. -//! -//! Plan acceptance (COMMONPLACE-CONSUMER-LOOP.md, I1): -//! "a natural-language question returns an answer grounded in the user's items, -//! each claim traceable to the item it came from." -//! -//! Exercised through the live GraphQL schema with the default (no-model) answer -//! seam, so the answer is the honest extractive fallback over fused -//! graph+vector+lexical retrieval, with provenance back to the source items. - -use std::sync::Arc; - -use async_graphql::Request; -use commonplace_api::{ - build_schema, build_schema_with_model, in_memory_store, AnswerModel, ApiKeyRegistry, - ApiKeyToken, ConsumerSchema, RetrievedItem, -}; - -fn instance(key: &str) -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(key, "instance")); - build_schema(in_memory_store(), registry) -} - -fn instance_with_model(key: &str, model: Arc) -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(key, "instance")); - build_schema_with_model(in_memory_store(), registry, model) -} - -struct StaticAnswerModel; - -impl AnswerModel for StaticAnswerModel { - fn synthesize(&self, question: &str, context: &[RetrievedItem]) -> Option { - (!context.is_empty()).then(|| format!("model answer for {question}")) - } -} - -async fn ingest(schema: &ConsumerSchema, key: &str, title: &str, text: &str) { - let mutation = format!( - r#"mutation {{ ingest(input: {{ title: "{title}", text: "{text}", kind: "doc" }}) {{ id }} }}"# - ); - let response = schema - .execute(Request::new(mutation).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "ingest errors: {:?}", - response.errors - ); -} - -#[tokio::test] -async fn ask_uses_configured_model_when_available() { - let key = "key"; - let schema = instance_with_model(key, Arc::new(StaticAnswerModel)); - ingest( - &schema, - key, - "Local Gemma", - "Gemma synthesizes answers over grounded CommonPlace notes.", - ) - .await; - - let query = r#"query { - ask(question: "does the model answer", k: 1) { - answer - answerKind - provenance { item { title } } - } - }"#; - let response = schema - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "ask errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let ask = &data["ask"]; - - assert_eq!(ask["answer"], "model answer for does the model answer"); - assert_eq!(ask["answerKind"], "MODEL"); - assert_eq!(ask["provenance"][0]["item"]["title"], "Local Gemma"); -} - -#[tokio::test] -async fn ask_returns_a_grounded_answer_with_traceable_provenance() { - let key = "key"; - let schema = instance(key); - - // Populate the store: three related rust items plus an unrelated distractor. - ingest( - &schema, - key, - "Rust ownership", - "rust ownership and borrowing rules govern memory safety", - ) - .await; - ingest( - &schema, - key, - "Rust borrow checker", - "the borrow checker enforces ownership and borrowing in rust programs", - ) - .await; - ingest( - &schema, - key, - "Rust lifetimes", - "lifetimes annotate how long references borrow in rust code", - ) - .await; - ingest( - &schema, - key, - "Postgres indexing", - "btree and gin indexes speed up postgres query planning", - ) - .await; - - let query = r#"query { - ask(question: "how does rust ownership and borrowing work", k: 3) { - answer - answerKind - provenance { item { id title } score arms } - } - }"#; - let response = schema - .execute(Request::new(query).data(ApiKeyToken(key.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "ask errors: {:?}", - response.errors - ); - let data = response.data.into_json().unwrap(); - let ask = &data["ask"]; - - // An answer was produced, extractively (no generative model configured). - assert!( - !ask["answer"].as_str().unwrap().is_empty(), - "non-empty answer" - ); - assert_eq!(ask["answerKind"], "EXTRACTIVE"); - - // It is grounded in items, each traceable by id. - let provenance = ask["provenance"].as_array().unwrap(); - assert!( - !provenance.is_empty(), - "answer is grounded in at least one item" - ); - for entry in provenance { - assert!( - !entry["item"]["id"].as_str().unwrap().is_empty(), - "each provenance entry traces to an item id" - ); - assert!( - !entry["arms"].as_array().unwrap().is_empty(), - "each provenance entry records which retrieval arms surfaced it" - ); - } - - // The top grounding item is a rust item, not the unrelated distractor. - let top_title = provenance[0]["item"]["title"].as_str().unwrap(); - assert!( - top_title.starts_with("Rust"), - "top provenance should be a rust item, got {top_title}" - ); - assert!( - provenance - .iter() - .any(|entry| entry["item"]["title"].as_str().unwrap().starts_with("Rust")), - "rust items are surfaced for a rust question" - ); -} - -#[tokio::test] -async fn ask_requires_a_valid_key() { - let schema = instance("good"); - let query = r#"query { ask(question: "anything") { answer } }"#; - - let response = schema.execute(Request::new(query)).await; - assert!(!response.errors.is_empty(), "ask without a key is rejected"); - - let response = schema - .execute(Request::new(query).data(ApiKeyToken("bad".to_string()))) - .await; - assert!( - !response.errors.is_empty(), - "ask with an invalid key is rejected" - ); -} - -#[tokio::test] -async fn theorem_agent_is_exposed_through_graphql_auth_and_validates_task_before_provider() { - let schema = instance("good"); - let query = r#"query { - theoremAgent(task: " ", bindingId: "agent:test") { - answer - answerKind - bindingId - } - }"#; - - let response = schema.execute(Request::new(query)).await; - assert!( - !response.errors.is_empty(), - "theoremAgent without a key is rejected" - ); - - let response = schema - .execute(Request::new(query).data(ApiKeyToken("good".to_string()))) - .await; - assert!( - !response.errors.is_empty(), - "empty task is rejected before provider invocation" - ); - assert!( - response.errors[0].message.contains("requires a task"), - "unexpected error: {:?}", - response.errors - ); -} diff --git a/apps/commonplace-api/tests/i2_briefing_acceptance.rs b/apps/commonplace-api/tests/i2_briefing_acceptance.rs deleted file mode 100644 index d159e0ab..00000000 --- a/apps/commonplace-api/tests/i2_briefing_acceptance.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! I2 acceptance: proactive surfacing (briefing). -//! -//! Plan acceptance (COMMONPLACE-CONSUMER-LOOP.md, I2): -//! "a briefing call returns recent and newly-connected items and open threads -//! drawn from the user's store." - -use std::sync::Arc; - -use async_graphql::Request; -use commonplace_api::{build_schema, in_memory_store, ApiKeyRegistry, ApiKeyToken, ConsumerSchema}; - -const KEY: &str = "key"; - -fn instance() -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - build_schema(in_memory_store(), registry) -} - -async fn exec(schema: &ConsumerSchema, query: impl Into) -> serde_json::Value { - let response = schema - .execute(Request::new(query).data(ApiKeyToken(KEY.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "gql errors: {:?}", - response.errors - ); - response.data.into_json().unwrap() -} - -#[tokio::test] -async fn briefing_surfaces_recent_connected_and_open_threads() { - let schema = instance(); - - // Two closely-related docs (auto-structured + linked via SIMILAR_TO). - exec( - &schema, - r#"mutation { ingest(input: { title: "Rust ownership", text: "rust ownership and borrowing govern memory safety", kind: "doc" }) { id } }"#, - ) - .await; - exec( - &schema, - r#"mutation { ingest(input: { title: "Rust borrowing", text: "rust ownership and borrowing rules govern memory safety in rust", kind: "doc" }) { id } }"#, - ) - .await; - - // A raw capture in no collection -> an open thread by being unfiled. - exec( - &schema, - r#"mutation { putNote(title: "Quick capture", text: "a raw thought to sort later") { id } }"#, - ) - .await; - - // An open-tagged note that we then FILE into a collection: still an open - // thread by tag, proving the tag path is independent of being unfiled. - let todo = exec( - &schema, - r#"mutation { putNote(title: "Todo item", text: "finish the report", tags: ["open"]) { id } }"#, - ) - .await; - let todo_id = todo["putNote"]["id"].as_str().unwrap().to_string(); - let tasks = exec( - &schema, - r#"mutation { createCollection(name: "Tasks") { id } }"#, - ) - .await; - let tasks_id = tasks["createCollection"]["id"] - .as_str() - .unwrap() - .to_string(); - exec( - &schema, - format!( - r#"mutation {{ addToCollection(itemId: "{todo_id}", collectionId: "{tasks_id}") }}"# - ), - ) - .await; - - let data = exec( - &schema, - r#"query { - briefing(recentLimit: 10, connectedLimit: 10, openLimit: 10) { - recent { id title } - newlyConnected { item { id title } connections related { id } } - openThreads { id title collections } - } - }"#, - ) - .await; - let briefing = &data["briefing"]; - - // Recent: non-empty, drawn from the store. - let recent = briefing["recent"].as_array().unwrap(); - assert!(!recent.is_empty(), "briefing surfaces recent items"); - - // Newly connected: the linked rust docs appear with a connection count. - let connected = briefing["newlyConnected"].as_array().unwrap(); - assert!( - !connected.is_empty(), - "briefing surfaces newly-connected items" - ); - let top = &connected[0]; - assert!( - top["item"]["title"].as_str().unwrap().starts_with("Rust"), - "the most-connected item is one of the linked rust docs" - ); - assert!(top["connections"].as_i64().unwrap() >= 1); - assert!( - !top["related"].as_array().unwrap().is_empty(), - "connected item lists its neighbors" - ); - - // Open threads: the unfiled capture AND the open-tagged (but filed) todo; - // the filed-and-untagged rust docs are NOT open threads. - let open_titles: Vec = briefing["openThreads"] - .as_array() - .unwrap() - .iter() - .map(|item| item["title"].as_str().unwrap().to_string()) - .collect(); - assert!( - open_titles.contains(&"Quick capture".to_string()), - "unfiled capture is an open thread" - ); - assert!( - open_titles.contains(&"Todo item".to_string()), - "open-tagged item is an open thread" - ); - assert!( - !open_titles.contains(&"Rust ownership".to_string()), - "filed, untagged items are not open threads" - ); - - // The open-tagged todo is genuinely filed (open by tag, not by being unfiled). - let todo_entry = briefing["openThreads"] - .as_array() - .unwrap() - .iter() - .find(|item| item["title"] == "Todo item") - .unwrap(); - assert!( - !todo_entry["collections"].as_array().unwrap().is_empty(), - "the open-tagged item is filed yet still surfaced as open" - ); -} diff --git a/apps/commonplace-api/tests/i4_discovery_acceptance.rs b/apps/commonplace-api/tests/i4_discovery_acceptance.rs deleted file mode 100644 index ac68f8ff..00000000 --- a/apps/commonplace-api/tests/i4_discovery_acceptance.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! I4 acceptance: discovery. -//! -//! Plan acceptance (COMMONPLACE-CONSUMER-LOOP.md, I4): -//! "a discovery call returns candidate links between items that are not yet -//! connected, ranked." - -use std::sync::Arc; - -use async_graphql::Request; -use commonplace::{DeterministicEmbedder, IngestInput, IngestPipeline}; -use commonplace_api::{ - build_schema, in_memory_store, ApiKeyRegistry, ApiKeyToken, ConsumerSchema, InMemoryShared, -}; - -const KEY: &str = "key"; - -async fn discover(schema: &ConsumerSchema, min: f64) -> serde_json::Value { - let query = format!( - r#"query {{ discover(minSimilarity: {min}, maxResults: 20) {{ a {{ id title }} b {{ id title }} similarity reason }} }}"# - ); - let response = schema - .execute(Request::new(query).data(ApiKeyToken(KEY.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "discover errors: {:?}", - response.errors - ); - response.data.into_json().unwrap() -} - -fn is_rust_pair(link: &serde_json::Value, a: &str, b: &str) -> bool { - let la = link["a"]["id"].as_str().unwrap(); - let lb = link["b"]["id"].as_str().unwrap(); - (la == a && lb == b) || (la == b && lb == a) -} - -#[tokio::test] -async fn discover_proposes_similar_but_unlinked_pairs_then_excludes_connected() { - let store: InMemoryShared = in_memory_store(); - - // Pre-populate with an artificially HIGH similarity threshold so the items - // are embedded and indexed but the auto-linker writes NO SIMILAR_TO edges: - // a latent connection for discovery to surface. - let (rust_a, rust_b); - { - let mut cp = store.lock().unwrap(); - let pipeline = - IngestPipeline::new(DeterministicEmbedder::default()).with_similarity_threshold(0.99); - let a = pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust ownership", - "rust ownership and borrowing govern memory safety", - ), - ) - .unwrap(); - let b = pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust borrowing", - "rust ownership and borrowing rules govern memory safety in rust", - ), - ) - .unwrap(); - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Postgres indexing", - "btree and gin indexes speed up postgres query planning", - ), - ) - .unwrap(); - rust_a = a.item.id.clone(); - rust_b = b.item.id.clone(); - } - - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - let schema = build_schema(store.clone(), registry); - - // The two rust docs are similar but not yet linked -> proposed, ranked. - let data = discover(&schema, 0.3).await; - let links = data["discover"].as_array().unwrap(); - assert!(!links.is_empty(), "discovery returns candidate links"); - assert!( - links - .iter() - .any(|link| is_rust_pair(link, &rust_a, &rust_b)), - "the similar-but-unlinked rust pair is proposed" - ); - // Ranked by similarity, each above the floor, each with a reason. - let mut previous = f64::INFINITY; - for link in links { - let similarity = link["similarity"].as_f64().unwrap(); - assert!(similarity >= 0.3, "respects the min-similarity floor"); - assert!( - similarity <= previous + 1e-9, - "results are ranked by similarity desc" - ); - previous = similarity; - assert!(!link["reason"].as_str().unwrap().is_empty()); - } - - // Connect the pair directly; discovery must no longer propose it. - { - let mut cp = store.lock().unwrap(); - cp.add_similarity(&rust_a, &rust_b, 1.0).unwrap(); - } - let data = discover(&schema, 0.3).await; - let links = data["discover"].as_array().unwrap(); - assert!( - !links - .iter() - .any(|link| is_rust_pair(link, &rust_a, &rust_b)), - "a now-connected pair is no longer proposed" - ); -} diff --git a/apps/commonplace-api/tests/mcp_stdio_acceptance.rs b/apps/commonplace-api/tests/mcp_stdio_acceptance.rs deleted file mode 100644 index e79862b6..00000000 --- a/apps/commonplace-api/tests/mcp_stdio_acceptance.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! AG3 acceptance: MCP access over the store. -//! -//! Plan acceptance (COMMONPLACE-CONSUMER-LOOP.md, AG3): -//! "an external model connects over MCP and reads and writes items." -//! -//! Spawns the `commonplace-mcp` binary and drives it over stdio as an MCP client -//! would: initialize, list tools, write an item (put_note), then read it back -//! (list_items). The binary speaks newline-delimited JSON-RPC 2.0 (the MCP stdio -//! transport). - -use std::io::{Read, Write}; -use std::process::{Command, Stdio}; - -#[test] -fn external_model_reads_and_writes_items_over_mcp() { - let mut child = Command::new(env!("CARGO_BIN_EXE_commonplace-mcp")) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn commonplace-mcp"); - - let requests = [ - r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}"#, - r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#, - r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#, - r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"put_note","arguments":{"title":"From a model","text":"written over MCP","tags":["mcp"]}}}"#, - r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_items","arguments":{}}}"#, - ]; - { - let mut stdin = child.stdin.take().expect("stdin"); - for request in requests { - writeln!(stdin, "{request}").expect("write request"); - } - } // drop stdin -> EOF -> the server's read loop ends. - - let mut out = String::new(); - child - .stdout - .take() - .expect("stdout") - .read_to_string(&mut out) - .expect("read stdout"); - let mut err = String::new(); - child - .stderr - .take() - .expect("stderr") - .read_to_string(&mut err) - .expect("read stderr"); - let status = child.wait().expect("wait for child"); - assert!( - status.success(), - "commonplace-mcp exited with {status}; stderr: {err}; stdout: {out}" - ); - - // initialize answered with protocol version + server identity. - assert!( - out.contains("\"protocolVersion\""), - "initialize result: {out}" - ); - assert!(out.contains("commonplace-mcp"), "serverInfo present: {out}"); - // tools/list advertises read + write item tools. - assert!( - out.contains("put_note") && out.contains("get_item") && out.contains("search"), - "tools/list advertises the item tools: {out}" - ); - // The write happened and a subsequent read shows the item back (round-trip). - assert!( - out.contains("From a model"), - "an item written over MCP is read back over MCP: {out}" - ); - // The notification produced no response; every emitted line is JSON-RPC. - let response_lines: Vec<&str> = out.lines().filter(|line| !line.trim().is_empty()).collect(); - assert_eq!( - response_lines.len(), - 4, - "4 requests with ids -> 4 responses, the notification none: {out}" - ); - for line in response_lines { - assert!( - line.contains("\"jsonrpc\""), - "each response is a JSON-RPC envelope: {line}" - ); - } -} diff --git a/apps/commonplace-api/tests/mobile_wiring_acceptance.rs b/apps/commonplace-api/tests/mobile_wiring_acceptance.rs deleted file mode 100644 index 161a2d05..00000000 --- a/apps/commonplace-api/tests/mobile_wiring_acceptance.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Record 007: authenticated mobile bootstrap and exact capability catalog. - -use std::sync::Arc; - -use commonplace::{Item, ItemKind}; -use commonplace_api::{in_memory_store, serve::build_router, ApiKeyRegistry}; -use tokio::sync::oneshot; - -const KEY: &str = "mobile-wiring-key"; - -async fn spawn_router() -> (String, oneshot::Sender<()>, tokio::task::JoinHandle<()>) { - let store = in_memory_store(); - { - let mut commonplace = store.lock().expect("mobile catalog store lock"); - commonplace - .put_item( - Item::new(ItemKind::from("plugin".to_string()), "Theorems Harness") - .with_text("Grounded theorem runtime and review tools."), - ) - .expect("seed plugin"); - commonplace - .put_item( - Item::new(ItemKind::from("skill".to_string()), "Planning theorem") - .with_text("Create an executable theorem plan."), - ) - .expect("seed skill"); - } - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "mobile")); - let app = build_router(store, registry); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .expect("bind ephemeral port"); - let port = listener.local_addr().expect("local addr").port(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await - .expect("serve mobile wiring test server"); - }); - (format!("http://127.0.0.1:{port}"), shutdown_tx, server) -} - -#[tokio::test] -async fn mobile_bootstrap_and_catalog_are_authenticated_and_normalized() { - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - - let unauthenticated = client - .get(format!("{base}/mobile/catalog")) - .send() - .await - .expect("unauthenticated catalog request"); - assert_eq!(unauthenticated.status(), reqwest::StatusCode::FORBIDDEN); - - let capabilities = client - .get(format!("{base}/capabilities")) - .header("x-api-key", KEY) - .send() - .await - .expect("capabilities request"); - assert_eq!(capabilities.status(), reqwest::StatusCode::OK); - let capabilities: serde_json::Value = capabilities.json().await.expect("capabilities json"); - assert_eq!(capabilities["capability_catalog"], true); - assert!(capabilities.get("chat_url").is_some()); - assert!(capabilities.get("push_registration_url").is_some()); - assert!(capabilities.get("expo_project_id").is_some()); - - let catalog = client - .get(format!("{base}/mobile/catalog")) - .header("x-api-key", KEY) - .send() - .await - .expect("catalog request"); - assert_eq!(catalog.status(), reqwest::StatusCode::OK); - let catalog: serde_json::Value = catalog.json().await.expect("catalog json"); - let plugin = catalog["plugins"] - .as_array() - .expect("plugins array") - .iter() - .find(|entry| entry["name"] == "Theorems Harness") - .expect("seed plugin in catalog"); - assert_eq!(plugin["kind"], "plugin"); - assert_eq!( - plugin["description"], - "Grounded theorem runtime and review tools." - ); - let skill = catalog["skills"] - .as_array() - .expect("skills array") - .iter() - .find(|entry| entry["name"] == "Planning theorem") - .expect("seed skill in catalog"); - assert_eq!(skill["kind"], "skill"); - - let _ = shutdown.send(()); - let _ = server.await; -} diff --git a/apps/commonplace-api/tests/objects_http_acceptance.rs b/apps/commonplace-api/tests/objects_http_acceptance.rs deleted file mode 100644 index 558b304e..00000000 --- a/apps/commonplace-api/tests/objects_http_acceptance.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! SPEC-OBJECT-CONTRACT-V2: the block-view object model over HTTP. -//! -//! `POST /objects/action` (create) files an item, `POST /objects/query` returns -//! it as an ObjectSet, and `GET /objects/views` hydrates the seed registry — all -//! behind the same `x-api-key` gate as `/graphql`. This is the wire the web -//! `HttpBlockHost` rides: the SurfaceRenderer above the BlockHost seam is -//! unchanged whether the host is in-memory or this live substrate. - -use std::sync::Arc; -use std::{ - path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, -}; - -use commonplace_api::{in_memory_store, redcore_store, serve::build_router, ApiKeyRegistry}; -use serde_json::json; -use tokio::sync::oneshot; - -const KEY: &str = "objects-key"; - -struct TestDataDir(PathBuf); - -impl TestDataDir { - fn new() -> Self { - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock") - .as_nanos(); - let path = std::env::temp_dir().join(format!( - "commonplace-api-cn4-{}-{suffix}", - std::process::id() - )); - Self(path) - } - - fn path(&self) -> &Path { - &self.0 - } -} - -impl Drop for TestDataDir { - fn drop(&mut self) { - if self - .0 - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with("commonplace-api-cn4-")) - { - let _ = std::fs::remove_dir_all(&self.0); - } - } -} - -async fn spawn_router() -> (String, oneshot::Sender<()>, tokio::task::JoinHandle<()>) { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - let app = build_router(in_memory_store(), registry); - spawn_app(app).await -} - -async fn spawn_app( - app: axum::Router, -) -> (String, oneshot::Sender<()>, tokio::task::JoinHandle<()>) { - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .expect("bind ephemeral port"); - let port = listener.local_addr().expect("local addr").port(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await - .expect("serve objects test server"); - }); - (format!("http://127.0.0.1:{port}"), shutdown_tx, server) -} - -#[tokio::test] -async fn canvas_and_chat_work_survive_object_seam_restart() { - let data_dir = TestDataDir::new(); - let registry = || Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - - let first_store = redcore_store(data_dir.path()).expect("open first durable store"); - let (base, shutdown, server) = spawn_app(build_router(first_store, registry())).await; - let client = reqwest::Client::new(); - - for (type_ref, props) in [ - ( - "canvas", - json!({ - "id": "canvas.default", - "title": "Canvas", - "persistence_kind": "canvas-work-v1", - "graph": { - "id": "canvas.default", - "title": "Canvas", - "tenant": "authenticated-object-seam", - "placements": [{ - "canvasId": "canvas.default", - "objectId": "note.restart", - "x": 40, - "y": 80, - "width": 240, - "height": 120 - }], - "groups": [], - "connections": [], - "objects": [{ - "id": "note.restart", - "type": "note", - "title": "Survives" - }] - } - }), - ), - ( - "chat-thread", - json!({ - "id": "chat-thread:restart", - "title": "Durable transcript", - "projectId": "chat-project:default", - "sessionId": "runtime-hint-only", - "sessionResumable": false, - "updatedAt": 1, - "messages": [ - { "id": "message-1", "role": "user", "text": "Persist this" }, - { "id": "message-2", "role": "assistant", "text": "Persisted" } - ], - "persistence_kind": "chat-transcript-v1" - }), - ), - ] { - let response = client - .post(format!("{base}/objects/action")) - .header("x-api-key", KEY) - .json(&json!({ "kind": "create", "type": type_ref, "props": props })) - .send() - .await - .expect("persist work object"); - assert_eq!(response.status(), reqwest::StatusCode::OK); - } - - let _ = shutdown.send(()); - let _ = server.await; - - let reopened_store = redcore_store(data_dir.path()).expect("reopen durable store"); - let (base, shutdown, server) = spawn_app(build_router(reopened_store, registry())).await; - let response = client - .post(format!("{base}/objects/query")) - .header("x-api-key", KEY) - .json(&json!({ "types": ["canvas", "chat-thread"], "page": { "limit": 10 } })) - .send() - .await - .expect("query reopened work"); - assert_eq!(response.status(), reqwest::StatusCode::OK); - let set: serde_json::Value = response.json().await.expect("reopened set json"); - let objects = set["objects"].as_array().expect("reopened objects"); - let canvas = objects - .iter() - .find(|object| object["id"] == "canvas.default") - .expect("canvas survived"); - assert_eq!( - canvas["properties"]["graph"]["placements"][0]["objectId"], - "note.restart" - ); - let thread = objects - .iter() - .find(|object| object["id"] == "chat-thread:restart") - .expect("thread survived"); - assert_eq!(thread["properties"]["messages"][1]["text"], "Persisted"); - assert_eq!(thread["properties"]["sessionResumable"], false); - - let _ = shutdown.send(()); - let _ = server.await; -} - -#[tokio::test] -async fn object_model_round_trips_over_http() { - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - - // Create an object through the action endpoint. - let create = client - .post(format!("{base}/objects/action")) - .header("x-api-key", KEY) - .json(&json!({ - "kind": "create", - "type": "task", - "props": { "title": "Wired via HTTP", "status": "todo" } - })) - .send() - .await - .expect("post action"); - assert_eq!(create.status(), reqwest::StatusCode::OK); - let receipt: serde_json::Value = create.json().await.expect("receipt json"); - assert_eq!(receipt["status"], "applied"); - assert_eq!(receipt["action_kind"], "create"); - - // Query it back as an ObjectSet. - let query = client - .post(format!("{base}/objects/query")) - .header("x-api-key", KEY) - .json(&json!({ "types": ["task"], "live": true })) - .send() - .await - .expect("post query"); - assert_eq!(query.status(), reqwest::StatusCode::OK); - let set: serde_json::Value = query.json().await.expect("set json"); - let objects = set["objects"].as_array().expect("objects array"); - assert!( - objects - .iter() - .any(|object| object["properties"]["title"] == "Wired via HTTP"), - "the created task returns in the ObjectSet: {set}" - ); - - // Views hydrate from the seed registry (matches v1 default_commonplace). - let views = client - .get(format!("{base}/objects/views")) - .header("x-api-key", KEY) - .send() - .await - .expect("get views"); - assert_eq!(views.status(), reqwest::StatusCode::OK); - let descriptors: serde_json::Value = views.json().await.expect("views json"); - let ids: Vec<&str> = descriptors - .as_array() - .expect("views array") - .iter() - .filter_map(|descriptor| descriptor["id"].as_str()) - .collect(); - assert!(ids.contains(&"table"), "seed registry over HTTP: {ids:?}"); - - // The gate holds: no key -> 403. - let unauth = client - .post(format!("{base}/objects/query")) - .json(&json!({ "types": ["task"] })) - .send() - .await - .expect("unauth request"); - assert_eq!(unauth.status(), reqwest::StatusCode::FORBIDDEN); - - let _ = shutdown.send(()); - let _ = server.await; -} - -#[tokio::test] -async fn layout_objects_round_trip_with_stable_ids_over_http() { - // B6b: surface / region / view-instance persist through /objects with pinned - // ids and ordered CONTAINS (move), then query returns the tree. - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - - for (type_ref, id, props) in [ - ( - "surface", - "console-chat", - json!({ "id": "console-chat", "name": "Chat", "kind": "chat", "active": true }), - ), - ( - "region", - "chat.region-editor", - json!({ "id": "chat.region-editor", "kind": "editor", "size": 100 }), - ), - ( - "view-instance", - "chat.vi-surface", - json!({ - "id": "chat.vi-surface", - "title": "Chat", - "descriptor_id": "chat.surface", - "query": { "types": ["thread"] } - }), - ), - ] { - let create = client - .post(format!("{base}/objects/action")) - .header("x-api-key", KEY) - .json(&json!({ "kind": "create", "type": type_ref, "props": props })) - .send() - .await - .expect("create layout object"); - assert_eq!(create.status(), reqwest::StatusCode::OK); - let receipt: serde_json::Value = create.json().await.expect("receipt"); - assert_eq!(receipt["status"], "applied"); - assert_eq!(receipt["target_ids"][0], id); - } - - for (child, parent, order) in [ - ("chat.region-editor", "console-chat", 1.0), - ("chat.vi-surface", "chat.region-editor", 1.0), - ] { - let move_action = client - .post(format!("{base}/objects/action")) - .header("x-api-key", KEY) - .json(&json!({ - "kind": "move", - "id": child, - "new_parent": parent, - "order": order - })) - .send() - .await - .expect("move contains"); - assert_eq!(move_action.status(), reqwest::StatusCode::OK); - } - - let query = client - .post(format!("{base}/objects/query")) - .header("x-api-key", KEY) - .json(&json!({ - "types": ["surface", "region", "view-instance"], - "traverse": [{ "edge": "CONTAINS", "dir": "out" }] - })) - .send() - .await - .expect("query layout"); - assert_eq!(query.status(), reqwest::StatusCode::OK); - let set: serde_json::Value = query.json().await.expect("set json"); - let objects = set["objects"].as_array().expect("objects"); - assert_eq!(objects.len(), 3); - let surface = objects - .iter() - .find(|object| object["id"] == "console-chat") - .expect("surface"); - assert_eq!(surface["type"], "surface"); - assert_eq!(surface["properties"]["name"], "Chat"); - assert_eq!( - surface["relations"]["CONTAINS"], - json!(["chat.region-editor"]) - ); - let region = objects - .iter() - .find(|object| object["id"] == "chat.region-editor") - .expect("region"); - assert_eq!( - region["relations"]["CONTAINS"], - json!(["chat.vi-surface"]) - ); - - let _ = shutdown.send(()); - let _ = server.await; -} diff --git a/apps/commonplace-api/tests/organize_acceptance.rs b/apps/commonplace-api/tests/organize_acceptance.rs deleted file mode 100644 index 2b78050a..00000000 --- a/apps/commonplace-api/tests/organize_acceptance.rs +++ /dev/null @@ -1,404 +0,0 @@ -//! Organize acceptance: the daily triage partition. -//! -//! Proves that `organize` partitions the items that arrived in a timeframe into -//! what the engine filed confidently ("organized today") and what still needs a -//! human ("needs you"), reusing the engine's classification signal (cosine of an -//! item's stored embedding to each collection's label embedding) for both, and -//! that `daily_progress` reconciles with the partition. -//! -//! The DeterministicEmbedder is text-seeded, so items that share tokens cluster. -//! We build two confident clusters (so two collections exist with label -//! embeddings) and one deliberately token-disjoint item whose cosine to every -//! collection falls below the ceiling. - -use commonplace::{ - mentions::MENTION_CANDIDATE_KIND, Commonplace, DeterministicEmbedder, InMemoryBlobStore, - IngestInput, IngestPipeline, -}; -use commonplace_api::{organize, OrganizeConfig, Timeframe}; -use rustyred_thg_core::InMemoryGraphStore; - -fn store() -> Commonplace { - Commonplace::new(InMemoryGraphStore::new(), InMemoryBlobStore::new()) -} - -// A higher-dimensional deterministic embedder: 16-bucket hashing collapses -// token-disjoint text into similar dense vectors, so we seed at 256 dims where -// distinct tokens land in distinct buckets and cosine separates clusters -// cleanly. `classify_item` only compares the STORED vectors, so the pipeline -// `organize` is called with does not need this embedder. -fn seed_pipeline() -> IngestPipeline { - IngestPipeline::new(DeterministicEmbedder::new(256)) -} - -#[test] -fn organize_partitions_confident_from_needs_you() { - let mut cp = store(); - let pipeline = seed_pipeline(); - - // Cluster 1 (rust): three docs sharing strong tokens -> one auto collection, - // each classifies confidently against that collection's label embedding. - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust ownership", - "rust ownership borrowing memory safety lifetime compiler", - ), - ) - .unwrap(); - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust borrowing", - "rust ownership borrowing memory safety lifetime rules", - ), - ) - .unwrap(); - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust lifetimes", - "rust ownership borrowing memory safety lifetime annotations", - ), - ) - .unwrap(); - - // Cluster 2 (cooking): a second confident collection, disjoint from rust. - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Pasta recipe", - "cooking pasta tomato garlic basil olive simmer sauce", - ), - ) - .unwrap(); - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Risotto recipe", - "cooking pasta tomato garlic basil olive simmer broth", - ), - ) - .unwrap(); - - // The ambiguous item: token-disjoint from both clusters. We file it into the - // existing collections (threshold 0) so it does NOT create its own collection - // that would later classify against itself at cosine ~1.0. Its stored - // embedding is still far from every collection's label embedding. - let loose = seed_pipeline().with_collection_threshold(0.0); - let ambiguous = loose - .ingest( - &mut cp, - IngestInput::note( - "Quarterly tax question", - "wonder whether quarterly estimated payment deadline shifted weekend holiday", - ), - ) - .unwrap(); - let ambiguous_id = ambiguous.item.id.clone(); - - // Six intake items are stored (five cluster docs plus the ambiguous one); - // the mention detector also creates two system candidates for the repeated - // "Rust ownership" reference. Those candidates stay out of organize intake. - let items = cp.all_items().unwrap(); - let intake_items: Vec<_> = items - .iter() - .filter(|item| item.kind.as_str() != MENTION_CANDIDATE_KIND) - .collect(); - let item_titles: Vec<&str> = intake_items - .iter() - .map(|item| item.title.as_str()) - .collect(); - assert_eq!( - intake_items.len(), - 6, - "five cluster docs plus the ambiguous one, got {item_titles:?}" - ); - assert_eq!( - items - .iter() - .filter(|item| item.kind.as_str() == MENTION_CANDIDATE_KIND) - .count(), - 2, - "the two repeated ownership references become confirmation candidates" - ); - - let config = OrganizeConfig { - needs_you_ceiling: 0.58, - timeframe: Timeframe::Day, - needs_you_cap: 24, - now_ms: now_after_all(&cp), - }; - let snapshot = organize(&cp, &IngestPipeline::default(), &config).unwrap(); - - // needs_you holds the ambiguous item, below the ceiling, with a target and - // populated alternatives (two collections exist). - let needs = &snapshot.needs_you; - let ambiguous_entry = needs - .iter() - .find(|item| item.item.id == ambiguous_id) - .expect("ambiguous item is surfaced as needing you"); - assert!( - ambiguous_entry.confidence < config.needs_you_ceiling, - "ambiguous item confidence {} must be below the ceiling {}", - ambiguous_entry.confidence, - config.needs_you_ceiling - ); - assert!( - ambiguous_entry.target_collection_id.is_some(), - "ambiguous item still gets a best-collection target" - ); - assert!( - !ambiguous_entry.alternatives.is_empty(), - "two collections exist, so the ambiguous item has a next-best alternative" - ); - - // The confident rust/cooking docs are filed (organized today), NOT in needs_you. - let needs_titles: Vec<&str> = needs.iter().map(|item| item.item.title.as_str()).collect(); - assert!( - !needs_titles.contains(&"Rust ownership"), - "a confidently-filed item is not in needs_you" - ); - - let today = &snapshot.organized_today; - assert!( - today.total_count >= 5, - "the five confident cluster docs are organized today (got {})", - today.total_count - ); - let most_recent = today - .most_recent - .as_ref() - .expect("organized_today surfaces a most-recent filed item"); - assert!( - !most_recent.filed_at.is_empty() && most_recent.filed_at.ends_with('Z'), - "filed_at is an ISO-8601 UTC string" - ); - assert!( - !today.groups.is_empty(), - "filed items group under their target collections" - ); - - // daily_progress reconciles: done == organized_today.total_count, and - // done + (below-ceiling needs_you) == total intake. - assert_eq!(snapshot.daily_progress.timeframe, "day"); - assert_eq!(snapshot.daily_progress.done, today.total_count); - let below_ceiling_needs = needs - .iter() - .filter(|item| item.confidence < config.needs_you_ceiling) - .count(); - assert_eq!( - snapshot.daily_progress.done + below_ceiling_needs, - snapshot.daily_progress.total, - "done + below-ceiling needs_you reconciles with total intake" - ); - assert_eq!(snapshot.needs_you_ceiling, 0.58); -} - -#[test] -fn needs_you_truncates_at_cap() { - let mut cp = store(); - - // Five token-disjoint notes with no collection ever created confidently: - // each carries an embedding but there is no matching collection, so each is - // low-confidence and lands in needs_you. - for token in ["alpha", "bravo", "charlie", "delta", "echo"] { - // Each note seeds its own auto collection at ingest, but we file via a - // zero-threshold pipeline into the FIRST collection so no per-item - // self-matching collection is created after the first. - let loose = seed_pipeline().with_collection_threshold(0.0); - loose - .ingest( - &mut cp, - IngestInput::note( - format!("{token} note"), - format!("{token} {token} unique body"), - ), - ) - .unwrap(); - } - - let config = OrganizeConfig { - needs_you_ceiling: 0.99, // force everything below the ceiling - timeframe: Timeframe::Day, - needs_you_cap: 2, - now_ms: now_after_all(&cp), - }; - let snapshot = organize(&cp, &IngestPipeline::default(), &config).unwrap(); - - assert_eq!( - snapshot.needs_you.len(), - 2, - "needs_you is truncated to the cap of 2" - ); - // The full intake still counts beyond the cap. - assert_eq!(snapshot.daily_progress.total, 5); -} - -#[test] -fn singleton_seed_surfaces_in_needs_you() { - // The F2 ingest path files EVERY item at capture, minting a fresh singleton - // collection for anything novel. Confidence-to-best-collection would read - // ~1.0 for that self-seeded bucket, so the item would wrongly look "filed". - // organize measures confidence against ESTABLISHED collections (>1 member), - // so a lonely self-seeded item correctly surfaces in needs_you. - let mut cp = store(); - let pipeline = seed_pipeline(); - - // An established cluster: two rust docs share one collection (2 members). - pipeline - .ingest( - &mut cp, - IngestInput::document("Rust A", "rust ownership borrowing memory safety lifetime"), - ) - .unwrap(); - pipeline - .ingest( - &mut cp, - IngestInput::document("Rust B", "rust ownership borrowing memory safety compiler"), - ) - .unwrap(); - - // A novel item: token-disjoint, so the default threshold mints its OWN - // singleton collection (cosine ~1.0 to that singleton's label embedding). - let novel = pipeline - .ingest( - &mut cp, - IngestInput::note( - "Garden plan", - "tomato seedlings watering schedule greenhouse compost", - ), - ) - .unwrap(); - let novel_id = novel.item.id.clone(); - - let config = OrganizeConfig { - needs_you_ceiling: 0.58, - timeframe: Timeframe::Day, - needs_you_cap: 24, - now_ms: now_after_all(&cp), - }; - let snapshot = organize(&cp, &IngestPipeline::default(), &config).unwrap(); - - // The novel item is in needs_you despite cosine ~1.0 to its own singleton. - let entry = snapshot - .needs_you - .iter() - .find(|item| item.item.id == novel_id) - .expect("a novel self-seeded item surfaces in needs_you, not filed"); - assert!( - entry.confidence < config.needs_you_ceiling, - "confidence is measured against established collections, not the item's own singleton (got {})", - entry.confidence - ); - - // The established cluster is filed (organized today), not in needs_you. - assert!( - snapshot.organized_today.total_count >= 2, - "the established rust cluster is organized today (got {})", - snapshot.organized_today.total_count - ); - assert!( - !snapshot - .needs_you - .iter() - .any(|item| item.item.title == "Rust A"), - "an established-cluster doc is filed, not in needs_you" - ); -} - -#[test] -fn task_kind_carries_subtasks() { - let mut cp = store(); - let pipeline = seed_pipeline(); - - // Two clustered docs establish a collection (so the surface has a real - // classification baseline, as the daily-driver path always does). - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust ownership", - "rust ownership borrowing memory safety lifetime compiler", - ), - ) - .unwrap(); - pipeline - .ingest( - &mut cp, - IngestInput::document( - "Rust borrowing", - "rust ownership borrowing memory safety lifetime rules", - ), - ) - .unwrap(); - - // A checkbox note, token-disjoint from the rust cluster so it stays in - // needs_you (below the ceiling) and does not mint a self-matching singleton - // (filed into the existing collection via the zero-threshold pipeline). - // Its body has three subtasks, the first done. - let loose = seed_pipeline().with_collection_threshold(0.0); - let task = loose - .ingest( - &mut cp, - IngestInput::note( - "Offsite checklist", - "- [x] book the room\n- [ ] send the agenda\n* [ ] invite finance", - ), - ) - .unwrap(); - let task_id = task.item.id.clone(); - - let config = OrganizeConfig { - needs_you_ceiling: 0.58, - timeframe: Timeframe::Day, - needs_you_cap: 24, - now_ms: now_after_all(&cp), - }; - let snapshot = organize(&cp, &IngestPipeline::default(), &config).unwrap(); - - let entry = snapshot - .needs_you - .iter() - .find(|item| item.item.id == task_id) - .expect("the checkbox note surfaces in needs_you"); - - assert_eq!( - entry.kind, "task", - "an item with checkbox subtasks derives kind=task (got {})", - entry.kind - ); - assert_eq!( - entry.subtasks.len(), - 3, - "three checkbox lines parse into three subtasks" - ); - assert_eq!( - entry.subtasks.iter().filter(|s| s.done).count(), - 1, - "exactly one subtask is done" - ); - // Order preserved: first done, then the two open ones in document order. - assert!(entry.subtasks[0].done, "the first subtask is done"); - assert_eq!(entry.subtasks[0].text, "book the room"); - assert_eq!(entry.subtasks[1].text, "send the agenda"); - assert!(!entry.subtasks[1].done); - assert_eq!(entry.subtasks[2].text, "invite finance"); - assert!(!entry.subtasks[2].done); -} - -/// A "now" just after every item's `updated_at_ms` so all arrive within the day. -fn now_after_all(cp: &Commonplace) -> i64 { - cp.all_items() - .unwrap() - .iter() - .map(|item| item.updated_at_ms) - .max() - .unwrap_or(0) - + 1 -} diff --git a/apps/commonplace-api/tests/pt008_remind_acceptance.rs b/apps/commonplace-api/tests/pt008_remind_acceptance.rs deleted file mode 100644 index 57cf2a64..00000000 --- a/apps/commonplace-api/tests/pt008_remind_acceptance.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! PT-008 acceptance: `remind_at_ms` through the consumer GraphQL profile. -//! -//! Record 004 (docs/records/004-mobile-app.md): "commonplace-api additive: -//! remind_at_ms scalar (model+GQL+ingest NL date parse echo), edit_item gains -//! status/due/remind". Verified in-process against the live schema, the same -//! way the F3 acceptance drives it. - -use std::sync::Arc; - -use async_graphql::Request; -use commonplace_api::{build_schema, in_memory_store, ApiKeyRegistry, ApiKeyToken, ConsumerSchema}; - -const KEY: &str = "remind-key"; - -fn instance() -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - build_schema(in_memory_store(), registry) -} - -async fn execute(schema: &ConsumerSchema, query: &str) -> serde_json::Value { - let response = schema - .execute(Request::new(query).data(ApiKeyToken(KEY.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "graphql errors for {query}: {:?}", - response.errors - ); - response.data.into_json().unwrap() -} - -#[tokio::test] -async fn ingest_echoes_natural_language_reminder_over_graphql() { - let schema = instance(); - - let data = execute( - &schema, - r#"mutation { - ingest(input: { title: "Call mom", text: "call mom friday 9am" }) { - id - remindAtMs - bodyText - } - }"#, - ) - .await; - - let remind = data["ingest"]["remindAtMs"] - .as_i64() - .expect("remindAtMs echoed from the phrase"); - assert!(remind > 0, "reminder is a real epoch instant: {remind}"); - // Echo, not rewrite: the captured text is intact. - assert_eq!(data["ingest"]["bodyText"], "call mom friday 9am"); - - // The scalar round-trips through the item query. - let id = data["ingest"]["id"].as_str().unwrap(); - let read = execute(&schema, &format!(r#"query {{ item(id: "{id}") {{ remindAtMs }} }}"#)).await; - assert_eq!(read["item"]["remindAtMs"].as_i64(), Some(remind)); -} - -#[tokio::test] -async fn explicit_ingest_scalars_win_over_the_parser() { - let schema = instance(); - - let data = execute( - &schema, - r#"mutation { - ingest(input: { - title: "Call mom", - text: "call mom friday 9am", - remindAtMs: 1234567, - dueAtMs: 7654321 - }) { - remindAtMs - dueAtMs - } - }"#, - ) - .await; - assert_eq!(data["ingest"]["remindAtMs"].as_i64(), Some(1_234_567)); - assert_eq!(data["ingest"]["dueAtMs"].as_i64(), Some(7_654_321)); -} - -#[tokio::test] -async fn edit_item_roundtrips_status_due_and_remind() { - let schema = instance(); - - let data = execute( - &schema, - r#"mutation { - putNote(title: "Follow up", text: "loop back with the vendor") { id } - }"#, - ) - .await; - let id = data["putNote"]["id"].as_str().unwrap().to_string(); - - // Set all three new scalars. - let edited = execute( - &schema, - &format!( - r#"mutation {{ - editItem(id: "{id}", status: "open", dueAtMs: 1900000000000, remindAtMs: 1890000000000) {{ - status - dueAtMs - remindAtMs - }} - }}"# - ), - ) - .await; - assert_eq!(edited["editItem"]["status"], "open"); - assert_eq!(edited["editItem"]["dueAtMs"].as_i64(), Some(1_900_000_000_000)); - assert_eq!( - edited["editItem"]["remindAtMs"].as_i64(), - Some(1_890_000_000_000) - ); - - // Read back through the item query. - let read = execute( - &schema, - &format!(r#"query {{ item(id: "{id}") {{ status dueAtMs remindAtMs }} }}"#), - ) - .await; - assert_eq!(read["item"]["status"], "open"); - assert_eq!(read["item"]["dueAtMs"].as_i64(), Some(1_900_000_000_000)); - assert_eq!(read["item"]["remindAtMs"].as_i64(), Some(1_890_000_000_000)); - - // -1 clears due and remind, leaves status alone. - let cleared = execute( - &schema, - &format!( - r#"mutation {{ - editItem(id: "{id}", dueAtMs: -1, remindAtMs: -1) {{ - status - dueAtMs - remindAtMs - }} - }}"# - ), - ) - .await; - assert_eq!(cleared["editItem"]["status"], "open"); - assert!(cleared["editItem"]["dueAtMs"].is_null()); - assert!(cleared["editItem"]["remindAtMs"].is_null()); -} - -#[tokio::test] -async fn plain_captures_have_no_reminder() { - let schema = instance(); - let data = execute( - &schema, - r#"mutation { - ingest(input: { title: "Groceries", text: "milk eggs bread" }) { remindAtMs } - }"#, - ) - .await; - assert!(data["ingest"]["remindAtMs"].is_null()); -} diff --git a/apps/commonplace-api/tests/pt017_blob_capture_acceptance.rs b/apps/commonplace-api/tests/pt017_blob_capture_acceptance.rs deleted file mode 100644 index 7ede7a8b..00000000 --- a/apps/commonplace-api/tests/pt017_blob_capture_acceptance.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! PT-017 (server half) acceptance: the multipart blob capture seam. -//! -//! Record 004 (docs/records/004-mobile-app.md): multipart `POST /ingest/blob` -//! stores bytes via the BlobStore and runs the ingest pipeline (right ItemKind, -//! blob_hash, mime), and `GET /blob/{hash}` serves the bytes back with the -//! item's mime, both behind the same `x-api-key` gate as `/graphql`. -//! -//! Runs the real router over an ephemeral loopback port (multipart needs the -//! HTTP layer, not just schema.execute). - -use std::sync::Arc; - -use commonplace_api::{in_memory_store, serve::build_router, ApiKeyRegistry}; -use reqwest::multipart::{Form, Part}; -use tokio::sync::oneshot; - -const KEY: &str = "blob-key"; - -async fn spawn_router() -> (String, oneshot::Sender<()>, tokio::task::JoinHandle<()>) { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - let app = build_router(in_memory_store(), registry); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .expect("bind ephemeral port"); - let port = listener.local_addr().expect("local addr").port(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await - .expect("serve blob test server"); - }); - (format!("http://127.0.0.1:{port}"), shutdown_tx, server) -} - -fn capture_form(bytes: Vec) -> Form { - Form::new() - .text("title", "Receipt photo") - .text("kind", "image") - .text("tags", "receipts, finance") - .text("text", "remind me tomorrow at 9 to expense this") - .part( - "file", - Part::bytes(bytes) - .file_name("receipt.png") - .mime_str("image/png") - .expect("mime"), - ) -} - -#[tokio::test] -async fn blob_capture_files_an_item_and_serves_bytes_back() { - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - let bytes = b"png-ish bytes for the blob store".to_vec(); - - // Capture: authenticated multipart POST. - let response = client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .multipart(capture_form(bytes.clone())) - .send() - .await - .expect("post capture"); - assert_eq!(response.status(), reqwest::StatusCode::OK); - let body: serde_json::Value = response.json().await.expect("json body"); - - assert!(!body["id"].as_str().unwrap_or_default().is_empty()); - assert_eq!(body["kind"], "image"); - assert_eq!(body["title"], "Receipt photo"); - assert_eq!(body["mime"], "image/png"); - let blob_hash = body["blobHash"].as_str().expect("blobHash on receipt"); - assert!(blob_hash.starts_with("sha256:"), "content address: {blob_hash}"); - assert!( - !body["classification"].as_str().unwrap_or_default().is_empty(), - "ingest classified the capture: {body}" - ); - assert!(!body["collections"].as_array().unwrap().is_empty()); - let tags: Vec<&str> = body["tags"] - .as_array() - .unwrap() - .iter() - .filter_map(|tag| tag.as_str()) - .collect(); - assert_eq!(tags, vec!["receipts", "finance"]); - assert!( - body["remindAtMs"].as_i64().unwrap_or_default() > 0, - "caption reminder phrase echoed: {body}" - ); - assert!(body["createdAtMs"].as_i64().unwrap_or_default() > 0); - - // Render path: GET /blob/{hash} returns the bytes with the item's mime. - let blob = client - .get(format!("{base}/blob/{blob_hash}")) - .header("x-api-key", KEY) - .send() - .await - .expect("get blob"); - assert_eq!(blob.status(), reqwest::StatusCode::OK); - assert_eq!( - blob.headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()), - Some("image/png") - ); - assert_eq!(blob.bytes().await.expect("blob bytes").to_vec(), bytes); - - // Unknown hash is a 404. - let missing = client - .get(format!("{base}/blob/sha256:{}", "0".repeat(64))) - .header("x-api-key", KEY) - .send() - .await - .expect("get missing blob"); - assert_eq!(missing.status(), reqwest::StatusCode::NOT_FOUND); - - let _ = shutdown.send(()); - let _ = server.await; -} - -#[tokio::test] -async fn blob_routes_reject_missing_or_invalid_keys() { - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - - let no_key = client - .post(format!("{base}/ingest/blob")) - .multipart(capture_form(b"bytes".to_vec())) - .send() - .await - .expect("post without key"); - assert_eq!(no_key.status(), reqwest::StatusCode::FORBIDDEN); - - let bad_key = client - .get(format!("{base}/blob/sha256:{}", "0".repeat(64))) - .header("x-api-key", "wrong-key") - .send() - .await - .expect("get with wrong key"); - assert_eq!(bad_key.status(), reqwest::StatusCode::FORBIDDEN); - - let _ = shutdown.send(()); - let _ = server.await; -} - -#[tokio::test] -async fn blob_capture_requires_a_file_and_infers_kind_from_mime() { - let (base, shutdown, server) = spawn_router().await; - let client = reqwest::Client::new(); - - // No file field: 400. - let no_file = client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .multipart(Form::new().text("title", "Nothing here")) - .send() - .await - .expect("post without file"); - assert_eq!(no_file.status(), reqwest::StatusCode::BAD_REQUEST); - - // Audio kind hint lands as an audio item (voice capture path). - let voice = client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .multipart( - Form::new().text("kind", "audio").part( - "file", - Part::bytes(b"m4a bytes".to_vec()) - .file_name("memo.m4a") - .mime_str("audio/mp4") - .expect("mime"), - ), - ) - .send() - .await - .expect("post voice capture"); - assert_eq!(voice.status(), reqwest::StatusCode::OK); - let body: serde_json::Value = voice.json().await.expect("json body"); - assert_eq!(body["kind"], "audio"); - // No title field: the filename stands in. - assert_eq!(body["title"], "memo.m4a"); - assert_eq!(body["mime"], "audio/mp4"); - - // No kind hint: mime infers the kind. - let inferred = client - .post(format!("{base}/ingest/blob")) - .header("x-api-key", KEY) - .multipart( - Form::new().text("title", "Scan").part( - "file", - Part::bytes(b"pdf bytes".to_vec()) - .file_name("scan.pdf") - .mime_str("application/pdf") - .expect("mime"), - ), - ) - .send() - .await - .expect("post file capture"); - assert_eq!(inferred.status(), reqwest::StatusCode::OK); - let body: serde_json::Value = inferred.json().await.expect("json body"); - assert_eq!(body["kind"], "file"); - assert_eq!(body["mime"], "application/pdf"); - - let _ = shutdown.send(()); - let _ = server.await; -} diff --git a/apps/commonplace-api/tests/x2_portability_acceptance.rs b/apps/commonplace-api/tests/x2_portability_acceptance.rs deleted file mode 100644 index c60252f8..00000000 --- a/apps/commonplace-api/tests/x2_portability_acceptance.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! X2 acceptance: import and export (no lock-in). -//! -//! Plan acceptance (COMMONPLACE-CONSUMER-LOOP.md, X2): -//! "a markdown or JSON export reimports without loss." -//! -//! Exports an instance to JSON, imports into a fresh instance, and asserts the -//! items round-trip without loss (kind/title/body/tags/collections), plus a -//! human-readable markdown export. - -use std::collections::BTreeMap; -use std::sync::Arc; - -use async_graphql::Request; -use commonplace_api::{build_schema, in_memory_store, ApiKeyRegistry, ApiKeyToken, ConsumerSchema}; - -const KEY: &str = "key"; - -fn instance() -> ConsumerSchema { - let registry = Arc::new(ApiKeyRegistry::new().with_key(KEY, "instance")); - build_schema(in_memory_store(), registry) -} - -async fn exec(schema: &ConsumerSchema, query: impl Into) -> serde_json::Value { - let response = schema - .execute(Request::new(query).data(ApiKeyToken(KEY.to_string()))) - .await; - assert!( - response.errors.is_empty(), - "gql errors: {:?}", - response.errors - ); - response.data.into_json().unwrap() -} - -/// A comparable signature of the store's items (id -> normalized fields). -async fn item_signatures(schema: &ConsumerSchema) -> BTreeMap { - let data = exec( - schema, - r#"query { items { id kind title bodyText tags collections } }"#, - ) - .await; - let mut map = BTreeMap::new(); - for item in data["items"].as_array().unwrap() { - let id = item["id"].as_str().unwrap().to_string(); - let mut tags: Vec = item["tags"] - .as_array() - .unwrap() - .iter() - .map(|t| t.as_str().unwrap().to_string()) - .collect(); - tags.sort(); - let mut collections: Vec = item["collections"] - .as_array() - .unwrap() - .iter() - .map(|c| c.as_str().unwrap().to_string()) - .collect(); - collections.sort(); - map.insert( - id, - serde_json::json!({ - "kind": item["kind"], "title": item["title"], - "bodyText": item["bodyText"], "tags": tags, "collections": collections, - }), - ); - } - map -} - -#[tokio::test] -async fn json_export_reimports_without_loss() { - let source = instance(); - - // Populate: auto-structured docs (collections + similarity), a tagged note, - // and a note filed into a manual collection. - exec(&source, r#"mutation { ingest(input: { title: "Rust ownership", text: "rust ownership and borrowing govern memory safety", kind: "doc" }) { id } }"#).await; - exec(&source, r#"mutation { ingest(input: { title: "Rust borrowing", text: "rust ownership and borrowing rules govern memory safety in rust", kind: "doc" }) { id } }"#).await; - exec(&source, r#"mutation { putNote(title: "A loose thought", text: "sort later", tags: ["open"]) { id } }"#).await; - let coll = exec( - &source, - r#"mutation { createCollection(name: "Manual") { id } }"#, - ) - .await; - let coll_id = coll["createCollection"]["id"].as_str().unwrap().to_string(); - let filed = exec( - &source, - r#"mutation { putNote(title: "Filed note", text: "lives in manual") { id } }"#, - ) - .await; - let filed_id = filed["putNote"]["id"].as_str().unwrap().to_string(); - exec( - &source, - format!( - r#"mutation {{ addToCollection(itemId: "{filed_id}", collectionId: "{coll_id}") }}"# - ), - ) - .await; - - let before = item_signatures(&source).await; - assert!(before.len() >= 4, "source has items"); - - // Export JSON. - let export = exec(&source, r#"query { export(format: JSON) }"#).await; - let json = export["export"].as_str().unwrap().to_string(); - assert!(json.contains("Rust ownership")); - - // Import into a fresh instance. - let target = instance(); - assert!( - item_signatures(&target).await.is_empty(), - "target starts empty" - ); - let import = exec( - &target, - format!( - r#"mutation {{ importItems(data: {}) {{ imported collections }} }}"#, - serde_json::Value::String(json.clone()) - ), - ) - .await; - assert!(import["importItems"]["imported"].as_i64().unwrap() >= 4); - assert!(import["importItems"]["collections"].as_i64().unwrap() >= 1); - - // The target's items match the source's exactly (id-keyed, no loss). - let after = item_signatures(&target).await; - assert_eq!(after, before, "every item round-trips without loss"); - - // Memberships survived (the filed note is still in its collection). - assert!( - after[&filed_id]["collections"] - .as_array() - .unwrap() - .iter() - .any(|c| c == &serde_json::json!(coll_id)), - "collection membership preserved across export/import" - ); -} - -#[tokio::test] -async fn markdown_export_is_human_readable() { - let schema = instance(); - exec(&schema, r#"mutation { ingest(input: { title: "Coffee notes", text: "pour over technique and grind size", kind: "doc" }) { id } }"#).await; - let export = exec(&schema, r#"query { export(format: MARKDOWN) }"#).await; - let markdown = export["export"].as_str().unwrap(); - assert!( - markdown.starts_with("# CommonPlace export"), - "markdown header: {markdown}" - ); - assert!( - markdown.contains("Coffee notes"), - "markdown includes the item: {markdown}" - ); -} diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 4930989e..8aec7091 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -40,6 +40,18 @@ API key, optional gateway URL (scenes) and harness node URL (rooms). ## Hosted mobile wiring +The surviving hosted node is +`https://commonplace-api-production.up.railway.app`; its health endpoint is +`https://commonplace-api-production.up.railway.app/healthz`. Save that node URL +and its API key under Account, or set the secret-free build default: + +```bash +EXPO_PUBLIC_COMMONPLACE_CLOUD_URL=https://commonplace-api-production.up.railway.app +``` + +Account values stored in SecureStore take precedence over the build default. +The canonical server source is `Travis-Gilbert/Theorem/apps/commonplace-api`. + The connected commonplace-api node exposes a secret-free `/capabilities` bootstrap and the authenticated `/mobile/catalog`. The app discovers the hosted ACP chat URL, attachment and web-search support, exact plugin/skill identities, diff --git a/apps/mobile/nativewind-env.d.ts b/apps/mobile/nativewind-env.d.ts index a13e3136..1625edf4 100644 --- a/apps/mobile/nativewind-env.d.ts +++ b/apps/mobile/nativewind-env.d.ts @@ -1 +1,3 @@ /// + +declare module '*.css'; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 53e7f536..9852e8f1 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -5,6 +5,7 @@ "dependencies": { "@assistant-ui/react-native": "^0.1.30", "@commonplace/block-view": "file:../../packages/block-view", + "@commonplace/mobile-contracts": "file:../../packages/mobile-contracts", "@expo-google-fonts/ibm-plex-sans": "^0.4.1", "@expo-google-fonts/inter": "^0.4.2", "@expo-google-fonts/jetbrains-mono": "^0.4.1", diff --git a/apps/mobile/src/api/client.ts b/apps/mobile/src/api/client.ts index e6e89830..17673d69 100644 --- a/apps/mobile/src/api/client.ts +++ b/apps/mobile/src/api/client.ts @@ -1,3 +1,5 @@ +import type { AttentionItem, ContractDescriptor } from '@commonplace/mobile-contracts'; + import { readInstanceSettings } from './instance'; export class GqlError extends Error { @@ -10,6 +12,28 @@ export class GqlError extends Error { } } +export async function instanceJson(path: string, init: RequestInit = {}): Promise { + const settings = await readInstanceSettings(); + const headers = new Headers(init.headers); + if (settings.apiKey) headers.set('x-api-key', settings.apiKey); + if (init.body && !headers.has('content-type')) headers.set('content-type', 'application/json'); + const response = await fetch(`${settings.url.replace(/\/$/, '')}${path}`, { + ...init, + headers, + }); + if (!response.ok) throw new GqlError(`HTTP ${response.status}`, response.status); + return (await response.json()) as T; +} + +export type MobileCapabilities = { + web_search: boolean; + contracts: ContractDescriptor[]; +}; + +export const fetchMobileCapabilities = () => instanceJson('/capabilities'); + +export const fetchMobileAttention = () => instanceJson('/mobile/attention'); + /** Minimal GraphQL-over-fetch client against the configured instance. */ export async function gql(query: string, variables?: Record): Promise { const settings = await readInstanceSettings(); diff --git a/apps/mobile/src/api/harness.ts b/apps/mobile/src/api/harness.ts index 69ba0404..b54fe920 100644 --- a/apps/mobile/src/api/harness.ts +++ b/apps/mobile/src/api/harness.ts @@ -4,7 +4,13 @@ * tenant passed explicitly; run on a trusted network or behind a proxy. */ import EventSource from 'react-native-sse'; +import type { + RemoteSurfaceToken, + ResumePage, + SnapshotWithTail, +} from '@commonplace/mobile-contracts'; +import { instanceJson } from './client'; import { readInstanceSettings } from './instance'; export type RoomSummary = { @@ -25,6 +31,40 @@ export type RoomMessage = { [k: string]: unknown; }; +export type SurfaceTokenRequest = { + runId: string; + surfaceId: string; + surfaceKind?: RemoteSurfaceToken['surfaceKind']; + allowedMode: RemoteSurfaceToken['allowedMode']; + deviceId: string; + ttlSeconds?: number; +}; + +export type SurfaceTokenResponse = { + token: RemoteSurfaceToken; + sealed: { + ciphertext: string; + nonce: string; + }; +}; + +export function fetchRunEvents(runId: string, afterSeq = 0, limit?: number): Promise { + const query = new URLSearchParams({ after_seq: String(afterSeq) }); + if (limit !== undefined) query.set('limit', String(limit)); + return instanceJson( + `/mobile/runs/${encodeURIComponent(runId)}/events?${query.toString()}`, + ); +} + +export const fetchRunSnapshot = (runId: string) => + instanceJson(`/mobile/runs/${encodeURIComponent(runId)}/snapshot`); + +export const mintRemoteSurfaceToken = (request: SurfaceTokenRequest) => + instanceJson('/mobile/remote-surface/token', { + method: 'POST', + body: JSON.stringify(request), + }); + async function harnessBase(): Promise<{ base: string; tenant: string; actorId: string } | null> { const s = await readInstanceSettings(); if (!s.harnessUrl) return null; diff --git a/apps/mobile/src/api/types.ts b/apps/mobile/src/api/types.ts index 3c9cb726..8c3a7b13 100644 --- a/apps/mobile/src/api/types.ts +++ b/apps/mobile/src/api/types.ts @@ -1,4 +1,21 @@ -/** Mirrors apps/commonplace-api/src/schema.rs (async-graphql camelCase). */ +/** Mirrors Theorem/apps/commonplace-api/src/schema.rs (async-graphql camelCase). */ + +export type { + ApprovalRequest, + AttentionItem, + ContextLens, + ContractDescriptor, + ControlLease, + FieldDefinition, + GraphNeighborhood, + ObjectType, + RemoteSurfaceToken, + ResumePage, + RunEvent, + SemanticFrame, + SnapshotWithTail, + ViewDefinition, +} from '@commonplace/mobile-contracts'; export type ItemGql = { id: string; diff --git a/docs/architecture/api-topology.md b/docs/architecture/api-topology.md index b75e5f87..ad518b84 100644 --- a/docs/architecture/api-topology.md +++ b/docs/architecture/api-topology.md @@ -1,6 +1,6 @@ # API topology — the one map an agent should read before touching data flow -Status: authoritative map, 2026-07-06. If you are about to ask "which API does the +Status: authoritative map, 2026-08-22. If you are about to ask "which API does the frontend call / where does agent-written state live / MCP vs HTTP", read this first. ## The one-paragraph answer @@ -15,6 +15,13 @@ agents**, not a second database: it carries `graphql_query` / `graphql_mutate` tool-calls onto the **same store**. Two doors, one store — so what an agent writes through the MCP is readable by the site through `commonplace-api`. +The canonical source tree is `Travis-Gilbert/Theorem/apps/commonplace-api`. Its +surviving deployment is the Railway service at +`https://commonplace-api-production.up.railway.app`; the prior Fly deployment is +undeployed and retired. CommonPlace contains no second `apps/commonplace-api` +source tree. Railway must be relinked to the Theorem source before its next +source build. + ``` Agents ──tools/call──▶ rustyred-thg-mcp ──┐ ├──▶ rustyred-thg-core (ONE store) diff --git a/package.json b/package.json index cf614831..7086586a 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,8 @@ "desktop:build": "npm --prefix apps/desktop run tauri -- build", "collab:dev": "npm --prefix apps/commonplace-collab run dev", "collab:start": "npm --prefix apps/commonplace-collab run start", - "api:dev": "cargo run --manifest-path apps/commonplace-api/Cargo.toml --bin commonplace-api", - "api:test": "cargo test --manifest-path apps/commonplace-api/Cargo.toml", - "api:dev:theorem": "cargo run --manifest-path ../Theorem/apps/commonplace-api/Cargo.toml --bin commonplace-api", - "api:test:theorem": "cargo test --manifest-path ../Theorem/apps/commonplace-api/Cargo.toml", + "api:dev": "cargo run --manifest-path ../Theorem/apps/commonplace-api/Cargo.toml --bin commonplace-api", + "api:test": "cargo test --manifest-path ../Theorem/apps/commonplace-api/Cargo.toml", "check": "node scripts/update-rustyred-source.mjs --check && cargo test --workspace && npm --prefix packages/block-view-contracts run check", "check:rust": "cargo test --workspace" }, diff --git a/packages/mobile-contracts/README.md b/packages/mobile-contracts/README.md new file mode 100644 index 00000000..1382c088 --- /dev/null +++ b/packages/mobile-contracts/README.md @@ -0,0 +1,16 @@ +# @commonplace/mobile-contracts + +This package is the CommonPlace-side boundary for the mobile kernel contracts: +Object Schema v2, Run Event v1, and Remote Surface v1. + +`mobile-contract-source.json` pins the exact Theorem source revision. The update +workflow fetches ts-rs output and canonical JSON fixtures from that revision, +runs this package's conformance check, and opens a CommonPlace pull request. + +Do not edit declarations under `src/generated/` by hand. Change the Rust wire +types in `Travis-Gilbert/Theorem`, regenerate there, and let the dispatch +pipeline synchronize this package from the source pin. + +The root and module barrels are generated from the synchronized file inventory +by `npm run generate`; `npm run check` refuses barrel drift before typechecking +the shared JSON fixtures. diff --git a/packages/mobile-contracts/fixtures/approval_request.json b/packages/mobile-contracts/fixtures/approval_request.json new file mode 100644 index 00000000..fcd55c05 --- /dev/null +++ b/packages/mobile-contracts/fixtures/approval_request.json @@ -0,0 +1,11 @@ +{ + "approvalId": "approval:fixture", + "createdAt": "2026-08-22T12:00:00Z", + "expiresAt": "2026-08-22T12:05:00Z", + "preview": "Send the prepared message", + "risk": "high", + "runId": "harnessrun:fixture", + "seq": 2, + "status": "open", + "summary": "Approve the fixture action" +} diff --git a/packages/mobile-contracts/fixtures/attention_item.json b/packages/mobile-contracts/fixtures/attention_item.json new file mode 100644 index 00000000..dd5d6afd --- /dev/null +++ b/packages/mobile-contracts/fixtures/attention_item.json @@ -0,0 +1,8 @@ +{ + "attentionId": "attention:approval:fixture", + "createdAt": "2026-08-22T12:00:00Z", + "risk": "high", + "runId": "harnessrun:fixture", + "source": "approval-request", + "summary": "Approve the fixture action" +} diff --git a/packages/mobile-contracts/fixtures/context_lens.json b/packages/mobile-contracts/fixtures/context_lens.json new file mode 100644 index 00000000..c76b4e10 --- /dev/null +++ b/packages/mobile-contracts/fixtures/context_lens.json @@ -0,0 +1,32 @@ +{ + "lensId": "lens:fixture", + "permittedObjectTypes": [ + "schema:object-type:Travis-Gilbert:project" + ], + "query": "current mobile project", + "relationTypes": [ + "related-to" + ], + "relevance": { + "kind": "ppr", + "params": { + "alpha": 0.85 + } + }, + "requiredProvenance": true, + "scope": { + "projectSlug": "mobile-contract-distribution", + "surface": "mobile", + "tenantSlug": "Travis-Gilbert" + }, + "seedObjectIds": [ + "record:fixture" + ], + "sensitiveFieldPolicy": "redact", + "summarizationStrategy": "extractive", + "timeWindow": { + "start": "2026-08-01T00:00:00Z" + }, + "tokenBudget": 4096, + "traversalDepth": 1 +} diff --git a/packages/mobile-contracts/fixtures/contract_catalog.json b/packages/mobile-contracts/fixtures/contract_catalog.json new file mode 100644 index 00000000..195948ef --- /dev/null +++ b/packages/mobile-contracts/fixtures/contract_catalog.json @@ -0,0 +1,14 @@ +[ + { + "name": "object-schema", + "version": "object-schema-v2" + }, + { + "name": "run-event", + "version": "run-event-v1" + }, + { + "name": "remote-surface", + "version": "remote-surface-v1" + } +] diff --git a/packages/mobile-contracts/fixtures/control_lease.json b/packages/mobile-contracts/fixtures/control_lease.json new file mode 100644 index 00000000..bd0b0b7d --- /dev/null +++ b/packages/mobile-contracts/fixtures/control_lease.json @@ -0,0 +1,10 @@ +{ + "acquiredAt": "2026-08-22T12:00:00Z", + "expiresAt": "2026-08-22T12:05:00Z", + "holderDevice": "device:fixture", + "leaseId": "lease:fixture", + "mode": "observe", + "released": false, + "runId": "harnessrun:fixture", + "surfaceId": "surface:fixture" +} diff --git a/packages/mobile-contracts/fixtures/field_definition.json b/packages/mobile-contracts/fixtures/field_definition.json new file mode 100644 index 00000000..91be1ba1 --- /dev/null +++ b/packages/mobile-contracts/fixtures/field_definition.json @@ -0,0 +1,16 @@ +{ + "default": "untitled", + "description": "Fixture title field", + "display": { + "component": "text-input" + }, + "fieldType": { + "kind": "text" + }, + "indexed": true, + "key": "title", + "label": "Title", + "required": true, + "sensitivity": "normal", + "system": false +} diff --git a/packages/mobile-contracts/fixtures/graph_neighborhood.json b/packages/mobile-contracts/fixtures/graph_neighborhood.json new file mode 100644 index 00000000..131d18f6 --- /dev/null +++ b/packages/mobile-contracts/fixtures/graph_neighborhood.json @@ -0,0 +1,33 @@ +{ + "edges": [ + { + "confidence": 0.93, + "edgeId": "edge:fixture", + "label": "Related to", + "provenanceRef": "receipt:fixture", + "relationType": "related-to", + "sourceNodeId": "record:fixture", + "targetNodeId": "record:related" + } + ], + "focusObjectId": "record:fixture", + "hops": 1, + "nodes": [ + { + "confidence": 1.0, + "label": "Fixture project", + "nodeId": "record:fixture", + "objectTypeId": "schema:object-type:Travis-Gilbert:project", + "x": 0.0, + "y": 0.0 + }, + { + "confidence": 0.93, + "label": "Related project", + "nodeId": "record:related", + "objectTypeId": "schema:object-type:Travis-Gilbert:project", + "x": 1.0, + "y": 0.0 + } + ] +} diff --git a/packages/mobile-contracts/fixtures/object_type.json b/packages/mobile-contracts/fixtures/object_type.json new file mode 100644 index 00000000..158d7f82 --- /dev/null +++ b/packages/mobile-contracts/fixtures/object_type.json @@ -0,0 +1,43 @@ +{ + "contentAnchor": "sha256:object-type-fixture", + "description": "A fixture project type", + "enforcement": "observe", + "fields": [ + { + "description": "Fixture title field", + "display": { + "component": "text-input" + }, + "fieldType": { + "kind": "text" + }, + "indexed": true, + "key": "title", + "label": "Title", + "required": true, + "sensitivity": "normal", + "system": false + } + ], + "labelPlural": "Projects", + "labelSingular": "Project", + "namePlural": "projects", + "nameSingular": "project", + "objectTypeId": "schema:object-type:Travis-Gilbert:project", + "relations": [ + { + "cardinality": "many", + "fieldKey": "related_project_rel", + "inverseLabel": "Related projects", + "relationId": "relation:fixture", + "required": false, + "semantic": "related-to", + "sourceObjectTypeId": "schema:object-type:Travis-Gilbert:project", + "system": false, + "targetObjectTypeId": "schema:object-type:Travis-Gilbert:project" + } + ], + "retired": false, + "system": false, + "tenantId": "Travis-Gilbert" +} diff --git a/packages/mobile-contracts/fixtures/remote_surface_token.json b/packages/mobile-contracts/fixtures/remote_surface_token.json new file mode 100644 index 00000000..25d14f83 --- /dev/null +++ b/packages/mobile-contracts/fixtures/remote_surface_token.json @@ -0,0 +1,11 @@ +{ + "allowedMode": "observe", + "deviceId": "device:fixture", + "expiresAt": "2026-08-22T12:05:00Z", + "maxRisk": "low", + "runId": "harnessrun:fixture", + "surfaceId": "surface:fixture", + "surfaceKind": "browser", + "tenant": "Travis-Gilbert", + "user": "user:fixture" +} diff --git a/packages/mobile-contracts/fixtures/resume_page.json b/packages/mobile-contracts/fixtures/resume_page.json new file mode 100644 index 00000000..86edb8b3 --- /dev/null +++ b/packages/mobile-contracts/fixtures/resume_page.json @@ -0,0 +1,21 @@ +{ + "cursor": { + "afterSeq": 0, + "runId": "harnessrun:fixture" + }, + "events": [ + { + "at": "2026-08-22T12:00:00Z", + "idempotencyKey": "event-fixture-1", + "kind": "message", + "payload": { + "text": "hello from the fixture" + }, + "runId": "harnessrun:fixture", + "seq": 1, + "sourceEventType": "MESSAGE.ADDED", + "stateHashAfter": "sha256:state-1" + } + ], + "latestSeq": 1 +} diff --git a/packages/mobile-contracts/fixtures/run_event.json b/packages/mobile-contracts/fixtures/run_event.json new file mode 100644 index 00000000..cb2bff51 --- /dev/null +++ b/packages/mobile-contracts/fixtures/run_event.json @@ -0,0 +1,12 @@ +{ + "at": "2026-08-22T12:00:00Z", + "idempotencyKey": "event-fixture-1", + "kind": "message", + "payload": { + "text": "hello from the fixture" + }, + "runId": "harnessrun:fixture", + "seq": 1, + "sourceEventType": "MESSAGE.ADDED", + "stateHashAfter": "sha256:state-1" +} diff --git a/packages/mobile-contracts/fixtures/semantic_frame.json b/packages/mobile-contracts/fixtures/semantic_frame.json new file mode 100644 index 00000000..263e0511 --- /dev/null +++ b/packages/mobile-contracts/fixtures/semantic_frame.json @@ -0,0 +1,24 @@ +{ + "actionResult": "navigation-complete", + "agentCursor": { + "x": 420.5, + "y": 180.25 + }, + "focusedElement": "actionable:search", + "proposedAction": "click", + "rationale": "Open the selected result", + "requiresApproval": false, + "scroll": { + "x": 0.0, + "y": 640.0 + }, + "seq": 7, + "targetId": "actionable:result-1", + "title": "Fixture page", + "url": "https://example.com/fixture", + "viewport": { + "deviceScale": 2.0, + "height": 844.0, + "width": 390.0 + } +} diff --git a/packages/mobile-contracts/fixtures/snapshot_with_tail.json b/packages/mobile-contracts/fixtures/snapshot_with_tail.json new file mode 100644 index 00000000..c1d9c256 --- /dev/null +++ b/packages/mobile-contracts/fixtures/snapshot_with_tail.json @@ -0,0 +1,25 @@ +{ + "events": [ + { + "at": "2026-08-22T12:00:01Z", + "idempotencyKey": "event-fixture-2", + "kind": "tool-call", + "payload": { + "tool": "graphql_query" + }, + "runId": "harnessrun:fixture", + "seq": 2, + "sourceEventType": "TOOL.CALL.STARTED", + "stateHashAfter": "sha256:state-2" + } + ], + "snapshot": { + "runId": "harnessrun:fixture", + "seq": 1, + "state": { + "status": "running" + }, + "stateHash": "sha256:state-1", + "takenAt": "2026-08-22T12:00:00Z" + } +} diff --git a/packages/mobile-contracts/fixtures/view_definition.json b/packages/mobile-contracts/fixtures/view_definition.json new file mode 100644 index 00000000..02d3dafc --- /dev/null +++ b/packages/mobile-contracts/fixtures/view_definition.json @@ -0,0 +1,15 @@ +{ + "layout": { + "density": "comfortable" + }, + "name": "Fixture projects", + "origin": "human", + "renderer": "table", + "target": { + "kind": "object-type", + "object_type_id": "schema:object-type:Travis-Gilbert:project" + }, + "temporary": false, + "viewId": "view:fixture", + "viewKind": "table" +} diff --git a/packages/mobile-contracts/mobile-contract-source.json b/packages/mobile-contracts/mobile-contract-source.json new file mode 100644 index 00000000..0db6f695 --- /dev/null +++ b/packages/mobile-contracts/mobile-contract-source.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "source": { + "repo": "Travis-Gilbert/Theorem", + "ref": "codex/mobile-contract-distribution-theorem", + "sha": "148132430859bdf38ac51738430319826fd98925", + "url": "https://github.com/Travis-Gilbert/Theorem/tree/148132430859bdf38ac51738430319826fd98925" + }, + "contracts": { + "objectSchema": "object-schema-v2", + "runEvent": "run-event-v1", + "remoteSurface": "remote-surface-v1" + }, + "updatedAt": "2026-08-22T19:42:31Z", + "notes": [ + "This file is the CommonPlace-side source pin for generated mobile contracts.", + "GitHub Actions updates the pin and generated files from Theorem repository_dispatch events and opens a PR.", + "The TypeScript declarations are generated from the pinned Rust source and must never be edited by hand." + ] +} diff --git a/packages/mobile-contracts/package.json b/packages/mobile-contracts/package.json new file mode 100644 index 00000000..b90c2377 --- /dev/null +++ b/packages/mobile-contracts/package.json @@ -0,0 +1,28 @@ +{ + "name": "@commonplace/mobile-contracts", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Generated Object Schema v2, Run Event v1, and Remote Surface v1 contracts.", + "exports": { + ".": "./src/index.ts", + "./object-schema": "./src/object-schema.ts", + "./run-event": "./src/run-event.ts", + "./remote-surface": "./src/remote-surface.ts", + "./source": "./mobile-contract-source.json", + "./mobile-contract-source.json": "./mobile-contract-source.json" + }, + "files": [ + "src", + "fixtures", + "mobile-contract-source.json", + "README.md" + ], + "scripts": { + "generate": "node scripts/generate-barrels.mjs", + "check": "node scripts/generate-barrels.mjs --check && npx --yes -p typescript@6.0.3 tsc --noEmit -p tsconfig.json" + }, + "devDependencies": { + "typescript": "~6.0.3" + } +} diff --git a/packages/mobile-contracts/scripts/generate-barrels.mjs b/packages/mobile-contracts/scripts/generate-barrels.mjs new file mode 100644 index 00000000..e4e8dfd3 --- /dev/null +++ b/packages/mobile-contracts/scripts/generate-barrels.mjs @@ -0,0 +1,54 @@ +import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const generatedRoot = join(packageRoot, 'src', 'generated', 'mobile-kernel-contract'); +const checkOnly = process.argv.includes('--check'); +const header = '// Generated from synchronized ts-rs bindings. Do not edit by hand.\n'; + +function typeNames(directory) { + return readdirSync(directory) + .filter((entry) => entry.endsWith('.ts')) + .map((entry) => entry.slice(0, -3)) + .sort(); +} + +function exportsFor(names, sourceDirectory = '') { + return names + .map( + (name) => + `export type { ${name} } from './generated/mobile-kernel-contract/${sourceDirectory}${name}';`, + ) + .join('\n'); +} + +const moduleBarrels = [ + ['object-schema.ts', 'object_schema'], + ['run-event.ts', 'run_event'], + ['remote-surface.ts', 'remote_surface'], +]; +const outputs = new Map( + moduleBarrels.map(([filename, sourceDirectory]) => [ + join(packageRoot, 'src', filename), + `${header}${exportsFor(typeNames(join(generatedRoot, sourceDirectory)), `${sourceDirectory}/`)}\n`, + ]), +); + +outputs.set( + join(packageRoot, 'src', 'index.ts'), + `${header}${exportsFor(typeNames(generatedRoot))}\nexport * from './object-schema';\nexport * from './remote-surface';\nexport * from './run-event';\n`, +); + +const drifted = []; +for (const [filename, expected] of outputs) { + if (checkOnly) { + if (readFileSync(filename, 'utf8') !== expected) drifted.push(filename); + } else { + writeFileSync(filename, expected); + } +} + +if (drifted.length > 0) { + throw new Error(`Generated mobile-contract barrel drift: ${drifted.join(', ')}`); +} diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractDescriptor.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractDescriptor.ts new file mode 100644 index 00000000..219511e1 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractDescriptor.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * One entry of the versioned protocol negotiation surface (spec §5.2). + * + * The Mobile Gateway advertises these from its capabilities bootstrap so a + * client can refuse to speak a contract revision it does not understand. + */ +export type ContractDescriptor = { +/** + * Stable contract family name (`object-schema`, `run-event`, + * `remote-surface`). + */ +name: string, +/** + * Full versioned identifier, e.g. `object-schema-v2`. + */ +version: string, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractValidation.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractValidation.ts new file mode 100644 index 00000000..123e05b0 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/ContractValidation.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Validation result shared by every contract gate in this crate. + * + * Mirrors `scene_contract::SceneValidation`: `ok` plus an optional reason + * string a client can surface verbatim. + */ +export type ContractValidation = { ok: boolean, reason?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/AgentSchemaAction.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/AgentSchemaAction.ts new file mode 100644 index 00000000..b8008b68 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/AgentSchemaAction.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Agent action classes over the schema and their gating (the spec §7.4 + * table). + */ +export type AgentSchemaAction = "create-record" | "add-relationship" | "create-temporary-view" | "propose-schema-change" | "apply-schema-change" | "delete-or-change-field-meaning"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Cardinality.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Cardinality.ts new file mode 100644 index 00000000..a60f908c --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Cardinality.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Relation cardinality, matching the register's `Cardinality`. + */ +export type Cardinality = "one" | "many"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextLens.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextLens.ts new file mode 100644 index 00000000..09cdeaf7 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextLens.ts @@ -0,0 +1,48 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LensScope } from "./LensScope"; +import type { RelevancePolicy } from "./RelevancePolicy"; +import type { SensitiveFieldPolicy } from "./SensitiveFieldPolicy"; +import type { TimeWindow } from "./TimeWindow"; + +/** + * Context is a lens, not a data dump (spec §7.3). All nine policies are + * present on the wire; `validate_context_lens` enforces the ones structure + * alone cannot. + */ +export type ContextLens = { lensId: string, scope?: LensScope | null, +/** + * Query or seed objects. + */ +query?: string | null, seedObjectIds: Array, +/** + * Permitted object types. + */ +permittedObjectTypes: Array, +/** + * Relation types and traversal depth. + */ +relationTypes: Array, traversalDepth: number, +/** + * Time window. + */ +timeWindow: TimeWindow, +/** + * Relevance or PPR policy. + */ +relevance: RelevancePolicy, +/** + * Token budget. + */ +tokenBudget: number, +/** + * Sensitive-field policy. + */ +sensitiveFieldPolicy: SensitiveFieldPolicy, +/** + * Required provenance. + */ +requiredProvenance: boolean, +/** + * Summarization strategy. + */ +summarizationStrategy: string, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextSnapshotRef.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextSnapshotRef.ts new file mode 100644 index 00000000..5185183d --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ContextSnapshotRef.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Reference to the versioned context snapshot a lens resolved into + * (spec §7.3). Both forms exist (spec §16 q4 recommendation): a + * content-addressed blob for immutability plus a first-class record pointing + * at it for queryability. The run stores this reference, not only the + * expanded prompt. + */ +export type ContextSnapshotRef = { snapshotRecordId: string, contentHash: string, lensId: string, resolvedAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Enforcement.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Enforcement.ts new file mode 100644 index 00000000..6364d353 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Enforcement.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Conformance enforcement rides along from the register unchanged + * (spec §7.7 rule 2). + */ +export type Enforcement = "observe" | "warn" | "reject"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldDefinition.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldDefinition.ts new file mode 100644 index 00000000..e713bb4b --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldDefinition.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; +import type { FieldSensitivity } from "./FieldSensitivity"; +import type { FieldWireType } from "./FieldWireType"; + +/** + * Scalar type, validation, indexing, sensitivity, default, and display + * behavior for one field (spec §6.2). `key`, `label`, `field_type`, + * `required`, and `system` bind 1:1 to the register's `FieldSpec`; the rest + * are wire-level presentation the register carries in `extensions`. + */ +export type FieldDefinition = { key: string, label: string, description?: string | null, fieldType: FieldWireType, required: boolean, system: boolean, sensitivity: FieldSensitivity, indexed?: boolean | null, default?: JsonValue | null, display: { [key in string]: JsonValue }, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldSensitivity.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldSensitivity.ts new file mode 100644 index 00000000..af0da39c --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldSensitivity.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Sensitivity marker a field carries so a `ContextLens` sensitive-field + * policy has something to act on (spec §6.2, §7.3). + */ +export type FieldSensitivity = "normal" | "sensitive"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldWireType.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldWireType.ts new file mode 100644 index 00000000..f35e5970 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/FieldWireType.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Cardinality } from "./Cardinality"; + +/** + * Wire mirror of the register's `FieldType` tagged union + * (`rustyred-thg-schema::declared::FieldType`), kept serde-compatible so a + * register field round-trips through this contract without translation. + */ +export type FieldWireType = { "kind": "text" } | { "kind": "long_text" } | { "kind": "integer" } | { "kind": "number" } | { "kind": "boolean" } | { "kind": "timestamp" } | { "kind": "date" } | { "kind": "uuid" } | { "kind": "json" } | { "kind": "enum", variants: Array, } | { "kind": "vector", dim: number, } | { "kind": "geometry" } | { "kind": "relation", target_object_type_id: string, cardinality: Cardinality, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphEdge.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphEdge.ts new file mode 100644 index 00000000..ac845f3c --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphEdge.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Typed graph edge. Edge labels describe the actual relation; long-press + * provenance resolves through `provenance_ref` (spec §7.5). + */ +export type GraphEdge = { edgeId: string, sourceNodeId: string, targetNodeId: string, label: string, relationType: string, confidence?: number | null, provenanceRef?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNeighborhood.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNeighborhood.ts new file mode 100644 index 00000000..8f9b3050 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNeighborhood.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GraphEdge } from "./GraphEdge"; +import type { GraphNode } from "./GraphNode"; + +/** + * A local neighborhood around one record — never a global graph of the + * entire space (spec §3.5). Generated HTML scenes may remain one renderer of + * this data; they are never the only graph contract (spec §7.1 rule 6). + */ +export type GraphNeighborhood = { focusObjectId: string, hops: number, nodes?: Array, edges?: Array, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNode.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNode.ts new file mode 100644 index 00000000..e6f6f891 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/GraphNode.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Typed graph node for Graph Explore (spec §7.1 rule 6, §17.4). + */ +export type GraphNode = { nodeId: string, label: string, objectTypeId?: string | null, confidence?: number | null, x?: number | null, y?: number | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/LensScope.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/LensScope.ts new file mode 100644 index 00000000..e5c9132c --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/LensScope.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Scope vocabulary shared with the harness `ContextScope`, so lens + * resolution and the web's scope resolution go through one resolver + * (spec §7.7 rule 4). + */ +export type LensScope = { tenantSlug: string, projectSlug?: string | null, sessionId?: string | null, surface?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ObjectType.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ObjectType.ts new file mode 100644 index 00000000..28a06d7e --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ObjectType.ts @@ -0,0 +1,21 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Enforcement } from "./Enforcement"; +import type { FieldDefinition } from "./FieldDefinition"; +import type { RelationDefinition } from "./RelationDefinition"; + +/** + * Person, Project, Place, Decision, Repository, Meeting, Research Question, + * Medication, or custom (spec §6.2). Custom object types use exactly this + * machinery — an object type never encodes one fixed screen (spec §7.1 + * rules 1-2). + */ +export type ObjectType = { +/** + * Register identifier: `schema:object-type:{tenant}:{name}`. + */ +objectTypeId: string, +/** + * Register content anchor (`stable_hash` over the declared input); the + * content-addressed binding of spec §7.7 rule 1. + */ +contentAnchor: string, tenantId?: string | null, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, fields: Array, relations: Array, enforcement: Enforcement, system: boolean, retired: boolean, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Record.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Record.ts new file mode 100644 index 00000000..a86309d9 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/Record.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; + +/** + * Instance of an object type (spec §6.2). + */ +export type Record = { recordId: string, objectTypeId: string, schemaRevisionId?: string | null, values: { [key in string]: JsonValue }, provenanceRefs: Array, updatedAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelationDefinition.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelationDefinition.ts new file mode 100644 index 00000000..90483683 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelationDefinition.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Cardinality } from "./Cardinality"; + +/** + * Wire projection of a register `FieldType::Relation` field + * (spec §6.2): source type, target type, cardinality, inverse label, and + * semantic meaning. + */ +export type RelationDefinition = { relationId: string, sourceObjectTypeId: string, targetObjectTypeId: string, cardinality: Cardinality, +/** + * Register field key this relation projects from (e.g. `{edge}_rel`). + */ +fieldKey?: string | null, inverseLabel?: string | null, semantic?: string | null, +/** + * Carried through from the register `FieldSpec` this relation projects + * from. Without it a required relation is indistinguishable from an + * optional one on the client, and the claimed 1:1 register binding would + * silently drop enforcement. + */ +required: boolean, system: boolean, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelevancePolicy.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelevancePolicy.ts new file mode 100644 index 00000000..ff8d9f1f --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/RelevancePolicy.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; + +/** + * Relevance policy the resolver applies (e.g. `ppr`), with its parameters. + */ +export type RelevancePolicy = { kind: string, params?: { [key in string]: JsonValue }, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaActionPolicy.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaActionPolicy.ts new file mode 100644 index 00000000..98cbb279 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaActionPolicy.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SchemaActionPolicy = "automatic" | "policy-controlled" | "explicit-approval" | "always-approval"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChange.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChange.ts new file mode 100644 index 00000000..dfa4b7c3 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChange.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SchemaChangeKind } from "./SchemaChangeKind"; + +export type SchemaChange = { kind: SchemaChangeKind, objectTypeId: string, fieldKey?: string | null, detail?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChangeKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChangeKind.ts new file mode 100644 index 00000000..4313f970 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaChangeKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SchemaChangeKind = "create-object-type" | "create-field" | "create-relation" | "modify-field-meaning" | "delete-field" | "retire-object-type"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposal.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposal.ts new file mode 100644 index 00000000..a054d151 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposal.ts @@ -0,0 +1,55 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FieldDefinition } from "./FieldDefinition"; +import type { ObjectType } from "./ObjectType"; +import type { SchemaProposalStatus } from "./SchemaProposalStatus"; +import type { ViewDefinition } from "./ViewDefinition"; + +/** + * What an agent-produced schema proposal must show the user (spec §7.4): + * the proposed change, why it is needed, similar existing fields or types, + * existing records affected, migration behavior, reversibility, and a + * preview of resulting views. This is what prevents synonymous type sprawl. + */ +export type SchemaProposal = { +/** + * Stable proposal hash minted by the register's proposal surface. + */ +proposalId: string, tenantId?: string | null, +/** + * The proposed object type (a register draft: empty id and anchor until + * declared), when the proposal creates a type. + */ +proposedObjectType?: ObjectType | null, +/** + * The proposed field, when the proposal extends an existing type. + */ +proposedField?: FieldDefinition | null, targetObjectTypeId?: string | null, +/** + * Why the change is needed. + */ +rationale: string, +/** + * Similar existing fields or types the user should compare against. + */ +similarExisting: Array, +/** + * Existing records affected. + */ +affectedRecordCount: number, +/** + * Migration behavior. + */ +migration: string, +/** + * Whether the change can be reversed. + */ +reversible: boolean, +/** + * Preview of resulting views. + */ +viewPreviews: Array, status: SchemaProposalStatus, +/** + * Compare-and-set anchor for acceptance: promotion must name the anchor + * it was reviewed against. + */ +expectedContentAnchor?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposalStatus.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposalStatus.ts new file mode 100644 index 00000000..7352e22c --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaProposalStatus.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Proposal lifecycle mapped onto the register's draft → promote flow + * (spec §7.7 rule 2): a proposal is a draft; approval is promotion; a second + * proposal pipeline is a defect. + */ +export type SchemaProposalStatus = "draft" | "promoted" | "rejected"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaRevision.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaRevision.ts new file mode 100644 index 00000000..b6e6cea0 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SchemaRevision.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SchemaChange } from "./SchemaChange"; + +/** + * Versions model changes and records migrations (spec §6.2). Binds to the + * register's `DeclaredSchemaVersion` / `ObjectTypeRevision` graph. + */ +export type SchemaRevision = { +/** + * Register identifier: `schema:model-version:{tenant}:{seq}:{anchor}`. + */ +revisionId: string, contentAnchor: string, sequence?: number | null, parentRevisionId?: string | null, changes: Array, migration?: string | null, declaredAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SensitiveFieldPolicy.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SensitiveFieldPolicy.ts new file mode 100644 index 00000000..5dda774a --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/SensitiveFieldPolicy.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * How much of the sensitive surface a lens exposes (spec §7.3). + */ +export type SensitiveFieldPolicy = "exclude" | "redact" | "include"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/TimeWindow.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/TimeWindow.ts new file mode 100644 index 00000000..33d2eeb8 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/TimeWindow.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Time window a lens admits; either bound may be open. + */ +export type TimeWindow = { start?: string | null, end?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewDefinition.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewDefinition.ts new file mode 100644 index 00000000..5eab4d91 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewDefinition.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; +import type { ViewKind } from "./ViewKind"; +import type { ViewOrigin } from "./ViewOrigin"; +import type { ViewTarget } from "./ViewTarget"; + +/** + * List, table, board, calendar, timeline, map, graph, or scene presentation + * of a type (spec §6.2). One record serves every surface — an agent-minted + * view that appears on one surface and not the other is a defect + * (spec §7.7 rule 3). `renderer` binds to the `commonplace` + * `ViewDescriptor` renderer vocabulary. + */ +export type ViewDefinition = { viewId: string, name: string, viewKind: ViewKind, target: ViewTarget, renderer?: string | null, +/** + * Agents may create temporary views without changing data (spec §7.1 + * rule 4). + */ +temporary: boolean, origin: ViewOrigin, layout: { [key in string]: JsonValue }, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewKind.ts new file mode 100644 index 00000000..c31504c5 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewKind.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * View presentations (spec §6.2, §7.2). A `Project` may have all of them at + * once — they are records pointing at the same type. + */ +export type ViewKind = "list" | "table" | "board" | "calendar" | "timeline" | "map" | "graph" | "scene"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewOrigin.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewOrigin.ts new file mode 100644 index 00000000..62c5ee0c --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewOrigin.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ViewOrigin = "builtin" | "human" | "agent"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewTarget.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewTarget.ts new file mode 100644 index 00000000..aef5db3a --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/object_schema/ViewTarget.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; + +/** + * A view points at a type or a query over types; it never mutates the type + * (spec §7.1 rule 3). + */ +export type ViewTarget = { "kind": "object-type", object_type_id: string, } | { "kind": "query", query: JsonValue, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlLease.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlLease.ts new file mode 100644 index 00000000..c27b3f02 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlLease.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ControlMode } from "./ControlMode"; + +/** + * Whether the agent or the user currently owns input (spec §6.1, §9.6). + */ +export type ControlLease = { leaseId: string, runId: string, surfaceId: string, mode: ControlMode, +/** + * Device holding the lease during takeover; device identity lives here + * and in the surface token, never in node tenancy (spec §16 q3). + */ +holderDevice?: string | null, acquiredAt?: string | null, expiresAt?: string | null, released: boolean, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlMode.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlMode.ts new file mode 100644 index 00000000..da9e96bf --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/ControlMode.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Control state must be painfully clear: exactly one input owner at a time + * (spec §9.6). + */ +export type ControlMode = "observe" | "approve" | "take-over" | "return-to-agent"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/InputOwner.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/InputOwner.ts new file mode 100644 index 00000000..7012f24b --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/InputOwner.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type InputOwner = "agent" | "user" | "transition"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Point.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Point.ts new file mode 100644 index 00000000..21f4501d --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Point.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Point = { x: number, y: number, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceToken.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceToken.ts new file mode 100644 index 00000000..f779dec1 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceToken.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RiskLevel } from "../run_event/RiskLevel"; +import type { ControlMode } from "./ControlMode"; +import type { SurfaceKind } from "./SurfaceKind"; + +/** + * Short-lived, scoped capability the phone holds instead of infrastructure + * credentials (spec §9.9, §10.1). + */ +export type RemoteSurfaceToken = { user: string, tenant: string, runId: string, surfaceId: string, surfaceKind: SurfaceKind, allowedMode: ControlMode, expiresAt: string, deviceId: string, maxRisk: RiskLevel, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceTransport.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceTransport.ts new file mode 100644 index 00000000..d916c3a5 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/RemoteSurfaceTransport.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; +import type { TransportKind } from "./TransportKind"; + +export type RemoteSurfaceTransport = { kind: TransportKind, params?: { [key in string]: JsonValue }, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SemanticFrame.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SemanticFrame.ts new file mode 100644 index 00000000..86284be7 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SemanticFrame.ts @@ -0,0 +1,30 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Point } from "./Point"; +import type { Viewport } from "./Viewport"; + +/** + * One frame of semantic browser state: the things the pixels cannot carry + * (spec §9.4). Native overlays are drawn from these fields. + */ +export type SemanticFrame = { seq?: number | null, url: string, title: string, viewport: Viewport, scroll: Point, agentCursor?: Point | null, +/** + * Stable actionable ID of the currently focused element + * (`pilot-core` `InteractiveElement.element_id`). + */ +focusedElement?: string | null, +/** + * Stable actionable ID of the semantic target of the proposed action. + */ +targetId?: string | null, proposedAction?: string | null, +/** + * Why the agent chose the action. + */ +rationale?: string | null, requiresApproval?: boolean | null, actionResult?: string | null, +/** + * Content-addressed before-state reference. + */ +beforeRef?: string | null, +/** + * Content-addressed after-state reference. + */ +afterRef?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceChannel.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceChannel.ts new file mode 100644 index 00000000..23ed3096 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceChannel.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * The three channels and the truth each carries (spec §9.2). + */ +export type SurfaceChannel = "video" | "semantic-stream" | "receipt"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceFrame.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceFrame.ts new file mode 100644 index 00000000..a3880254 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceFrame.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * One frame of the periodic-frames observation transport. Frame bytes ride a + * content-addressed reference or inline base64 — never both absent. + */ +export type SurfaceFrame = { seq: number, at?: string | null, +/** + * Encoding of the frame payload, e.g. `image/png`. + */ +encoding: string, width: number, height: number, frameRef?: string | null, dataBase64?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceKind.ts new file mode 100644 index 00000000..53a9597a --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/SurfaceKind.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * What the surface renders. v1 ships browser-only (Servo); the screen must + * not care what the target is, so the kind stays open (spec §9.7). + */ +export type SurfaceKind = "browser" | "desktop" | "ide" | "terminal" | "application" | string; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/TransportKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/TransportKind.ts new file mode 100644 index 00000000..2839fd30 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/TransportKind.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Transport phasing (spec §9.8): earliest observation may ship periodic + * compressed frames; interactive takeover requires WebRTC. Both sit behind + * this one descriptor so the prototype transport cannot infect the product + * model. + */ +export type TransportKind = "periodic-frames" | "web-rtc"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Viewport.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Viewport.ts new file mode 100644 index 00000000..662eb026 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/remote_surface/Viewport.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Viewport = { width: number, height: number, +/** + * Device pixels per CSS pixel, matching `pilot-core`'s coordinate + * transform seam. + */ +deviceScale: number, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Actor.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Actor.ts new file mode 100644 index 00000000..8ae66878 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Actor.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActorKind } from "./ActorKind"; + +export type Actor = { kind: ActorKind, id: string, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ActorKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ActorKind.ts new file mode 100644 index 00000000..b13512bb --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ActorKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActorKind = "agent" | "user"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalRequest.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalRequest.ts new file mode 100644 index 00000000..ea897a2b --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalRequest.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ApprovalStatus } from "./ApprovalStatus"; +import type { RiskLevel } from "./RiskLevel"; + +/** + * Something requiring the user (spec §6.1, §10.4). + * + * Most approvals bind to a run; a schema-proposal approval (spec §7.4, + * §16 q6) or a harness agency-proposal approval binds to a proposal + * instead, so both bindings are optional and at least one must be present. + */ +export type ApprovalRequest = { approvalId: string, runId?: string | null, proposalId?: string | null, seq?: number | null, summary: string, preview?: string | null, risk: RiskLevel, status: ApprovalStatus, createdAt?: string | null, decidedAt?: string | null, expiresAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalStatus.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalStatus.ts new file mode 100644 index 00000000..9bdfef90 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ApprovalStatus.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ApprovalStatus = "open" | "approved" | "rejected" | "expired"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Artifact.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Artifact.ts new file mode 100644 index 00000000..62343662 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Artifact.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; +import type { ArtifactKind } from "./ArtifactKind"; + +export type Artifact = { artifactId: string, runId?: string | null, kind: ArtifactKind, label?: string | null, +/** + * Content-addressed reference into RustyRed blob storage; artifact bytes + * never travel inline on this contract. + */ +contentRef: string, mediaType?: string | null, byteLen?: number | null, metadata: { [key in string]: JsonValue }, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ArtifactKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ArtifactKind.ts new file mode 100644 index 00000000..1d3fd14a --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ArtifactKind.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Artifact kinds (spec §6.1). + */ +export type ArtifactKind = "file" | "diff" | "screenshot" | "video" | "report" | "test-result" | "pull-request" | "captured-page" | "generated-object" | string; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionItem.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionItem.ts new file mode 100644 index 00000000..212e4f32 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionItem.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AttentionSource } from "./AttentionSource"; +import type { RiskLevel } from "./RiskLevel"; + +/** + * One entry of the Home "waiting on the user" projection (spec §3.1, §10.4). + */ +export type AttentionItem = { attentionId: string, +/** + * Absent only for approval-sourced items that bind to a proposal rather + * than a run (spec §16 q6). + */ +runId?: string | null, source: AttentionSource, +/** + * Present only when the source is an approval request; risk rides the + * `ApprovalRequest` and the client never classifies it (spec §10.4). + */ +risk?: RiskLevel | null, summary: string, createdAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionSource.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionSource.ts new file mode 100644 index 00000000..a4624e7e --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/AttentionSource.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Attention sources (spec §10.4): an `AttentionItem` is derivable from + * exactly these four, and nothing else invents membership. + */ +export type AttentionSource = "approval-request" | "request-for-input" | "failure-requiring-decision" | "login-takeover"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/CapabilityGrant.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/CapabilityGrant.ts new file mode 100644 index 00000000..e2aea7ad --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/CapabilityGrant.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * What a run may access (spec §6.1, §10.1): only the secrets and MCP grants + * it needs, never durable infrastructure credentials. + */ +export type CapabilityGrant = { grantId: string, capability: string, scope?: string | null, expiresAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/EventCursor.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/EventCursor.ts new file mode 100644 index 00000000..bbff0ea8 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/EventCursor.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Resume cursor: clients resume with "events after sequence N" (spec §8.1 + * rule 3). + */ +export type EventCursor = { runId: string, afterSeq: number, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTarget.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTarget.ts new file mode 100644 index 00000000..47121ccf --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTarget.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExecutionTargetKind } from "./ExecutionTargetKind"; +import type { MachineClass } from "./MachineClass"; + +export type ExecutionTarget = { kind: ExecutionTargetKind, machineClass: MachineClass, +/** + * Control-plane identifier of the concrete machine, when one is bound. + */ +nodeId?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTargetKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTargetKind.ts new file mode 100644 index 00000000..2aa113b3 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ExecutionTargetKind.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Where a run executes (spec §6.1). + */ +export type ExecutionTargetKind = "servo-browser" | "interactive-desktop" | "code-workspace" | "shell-machine" | "research-worker"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/MachineClass.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/MachineClass.ts new file mode 100644 index 00000000..c4ef5c78 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/MachineClass.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Which class of remote computer a run executes on (spec §8.4). + */ +export type MachineClass = "personal-node" | "ephemeral-run"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Receipt.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Receipt.ts new file mode 100644 index 00000000..e9179038 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Receipt.ts @@ -0,0 +1,21 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; +import type { Actor } from "./Actor"; +import type { ReceiptAction } from "./ReceiptAction"; + +/** + * What actually happened (spec §6.1, §9.2 channel table, §10.2). + */ +export type Receipt = { receiptId: string, runId?: string | null, action: ReceiptAction, actor: Actor, +/** + * Content-addressed before-state evidence. + */ +beforeRef?: string | null, +/** + * Content-addressed after-state evidence. + */ +afterRef?: string | null, at?: string | null, +/** + * Optional rollback information (spec §10.2). + */ +rollback?: JsonValue | null, detail: { [key in string]: JsonValue }, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ReceiptAction.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ReceiptAction.ts new file mode 100644 index 00000000..c49ca1a8 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ReceiptAction.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Consequential-action classes the one receipt model covers (spec §10.2). + */ +export type ReceiptAction = "browser-action" | "file-write" | "shell-command" | "record-edit" | "outbound-message" | "form-submission" | "schema-change" | string; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ResumePage.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ResumePage.ts new file mode 100644 index 00000000..14c38677 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/ResumePage.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EventCursor } from "./EventCursor"; +import type { RunEvent } from "./RunEvent"; + +/** + * One page of the gateway's "resume events after sequence N" operation + * (spec §5.2). `latest_seq` lets a client see how far behind the page is. + */ +export type ResumePage = { cursor: EventCursor, events?: Array, latestSeq: number, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RiskLevel.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RiskLevel.ts new file mode 100644 index 00000000..bbf279f3 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RiskLevel.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Risk level carried by an approval (spec §10.4): the client never + * classifies risk itself. + */ +export type RiskLevel = "low" | "medium" | "high"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Run.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Run.ts new file mode 100644 index 00000000..463d5103 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Run.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CapabilityGrant } from "./CapabilityGrant"; +import type { ExecutionTarget } from "./ExecutionTarget"; +import type { RunStatus } from "./RunStatus"; + +/** + * One bounded execution (spec §6.1). + */ +export type Run = { runId: string, sessionId: string, target: ExecutionTarget, status: RunStatus, +/** + * Runs carry idempotency tokens (spec §8.1 rule 4); the harness SDK + * enforces them on append and cancel. + */ +idempotencyToken?: string | null, capabilityGrants: Array, createdAt?: string | null, +/** + * Highest event sequence persisted for this run. + */ +lastEventSeq: number, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEvent.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEvent.ts new file mode 100644 index 00000000..832627a2 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEvent.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; +import type { RunEventKind } from "./RunEventKind"; + +/** + * Ordered, durable event (spec §6.1, §8.1 rules 2-3). + * + * `seq` is the harness log's gapless, strictly monotonic per-run sequence; + * `state_hash_after` is the SDK state hash that anchors snapshot identity + * (spec §8.1 rule 8). + */ +export type RunEvent = { runId: string, seq: number, kind: RunEventKind, +/** + * The originating harness event type string, preserved so no adapter is + * lossy (e.g. `RUN.CREATED`). + */ +sourceEventType?: string | null, at?: string | null, payload: { [key in string]: JsonValue }, stateHashAfter?: string | null, idempotencyKey?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEventKind.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEventKind.ts new file mode 100644 index 00000000..3febb8f5 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunEventKind.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Wire event kinds (spec §6.1). `Other` carries any harness event type that + * has no 1:1 surface kind, verbatim and lossless. + */ +export type RunEventKind = "message" | "plan" | "tool-call" | "shell-command" | "browser-action" | "artifact" | "warning" | "request-for-input" | "completion" | string; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStateSnapshot.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStateSnapshot.ts new file mode 100644 index 00000000..705146d3 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStateSnapshot.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../../serde_json/JsonValue"; + +/** + * Compact run-state snapshot at a sequence boundary. The SDK's state hash + * anchors snapshot identity (spec §8.1 rule 8). + */ +export type RunStateSnapshot = { runId: string, seq: number, stateHash: string, +/** + * Content-addressed reference to the serialized state, when spilled. + */ +snapshotRef?: string | null, state?: JsonValue | null, takenAt?: string | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStatus.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStatus.ts new file mode 100644 index 00000000..1da987ca --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/RunStatus.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RunStatus = "pending" | "running" | "waiting-on-user" | "completed" | "failed" | "cancelled"; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Session.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Session.ts new file mode 100644 index 00000000..051c39f6 --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/Session.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ContextSnapshotRef } from "../object_schema/ContextSnapshotRef"; + +/** + * Long-lived continuity context (spec §6.1). + * + * Harness sessions are identified by `(agent_id, tenant)`; the product-level + * `session_id` is minted by the control plane. Durable thread, message, and + * session records live on the server and in RustyRed — the client cache is + * never the system of record (spec §8.1 rule 7). + */ +export type Session = { sessionId: string, tenant: string, agentId?: string | null, title?: string | null, createdAt?: string | null, +/** + * Reference to the versioned context snapshot resolved when the session + * began (spec §7.3): the run stores the reference, not only the expanded + * prompt. + */ +contextSnapshot?: ContextSnapshotRef | null, }; diff --git a/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/SnapshotWithTail.ts b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/SnapshotWithTail.ts new file mode 100644 index 00000000..bcc31dbc --- /dev/null +++ b/packages/mobile-contracts/src/generated/mobile-kernel-contract/run_event/SnapshotWithTail.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RunEvent } from "./RunEvent"; +import type { RunStateSnapshot } from "./RunStateSnapshot"; + +/** + * Snapshot-plus-tail late join (spec §8.1 rule 8): a late-joining client + * requests a compact state snapshot at N plus the events after N, never a + * full-log replay. + */ +export type SnapshotWithTail = { snapshot: RunStateSnapshot, events?: Array, }; diff --git a/packages/mobile-contracts/src/generated/serde_json/JsonValue.ts b/packages/mobile-contracts/src/generated/serde_json/JsonValue.ts new file mode 100644 index 00000000..320a852f --- /dev/null +++ b/packages/mobile-contracts/src/generated/serde_json/JsonValue.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type JsonValue = number | string | boolean | Array | { [key in string]: JsonValue } | null; diff --git a/packages/mobile-contracts/src/index.ts b/packages/mobile-contracts/src/index.ts new file mode 100644 index 00000000..55494a52 --- /dev/null +++ b/packages/mobile-contracts/src/index.ts @@ -0,0 +1,6 @@ +// Generated from synchronized ts-rs bindings. Do not edit by hand. +export type { ContractDescriptor } from './generated/mobile-kernel-contract/ContractDescriptor'; +export type { ContractValidation } from './generated/mobile-kernel-contract/ContractValidation'; +export * from './object-schema'; +export * from './remote-surface'; +export * from './run-event'; diff --git a/packages/mobile-contracts/src/object-schema.ts b/packages/mobile-contracts/src/object-schema.ts new file mode 100644 index 00000000..efc9e138 --- /dev/null +++ b/packages/mobile-contracts/src/object-schema.ts @@ -0,0 +1,29 @@ +// Generated from synchronized ts-rs bindings. Do not edit by hand. +export type { AgentSchemaAction } from './generated/mobile-kernel-contract/object_schema/AgentSchemaAction'; +export type { Cardinality } from './generated/mobile-kernel-contract/object_schema/Cardinality'; +export type { ContextLens } from './generated/mobile-kernel-contract/object_schema/ContextLens'; +export type { ContextSnapshotRef } from './generated/mobile-kernel-contract/object_schema/ContextSnapshotRef'; +export type { Enforcement } from './generated/mobile-kernel-contract/object_schema/Enforcement'; +export type { FieldDefinition } from './generated/mobile-kernel-contract/object_schema/FieldDefinition'; +export type { FieldSensitivity } from './generated/mobile-kernel-contract/object_schema/FieldSensitivity'; +export type { FieldWireType } from './generated/mobile-kernel-contract/object_schema/FieldWireType'; +export type { GraphEdge } from './generated/mobile-kernel-contract/object_schema/GraphEdge'; +export type { GraphNeighborhood } from './generated/mobile-kernel-contract/object_schema/GraphNeighborhood'; +export type { GraphNode } from './generated/mobile-kernel-contract/object_schema/GraphNode'; +export type { LensScope } from './generated/mobile-kernel-contract/object_schema/LensScope'; +export type { ObjectType } from './generated/mobile-kernel-contract/object_schema/ObjectType'; +export type { Record } from './generated/mobile-kernel-contract/object_schema/Record'; +export type { RelationDefinition } from './generated/mobile-kernel-contract/object_schema/RelationDefinition'; +export type { RelevancePolicy } from './generated/mobile-kernel-contract/object_schema/RelevancePolicy'; +export type { SchemaActionPolicy } from './generated/mobile-kernel-contract/object_schema/SchemaActionPolicy'; +export type { SchemaChange } from './generated/mobile-kernel-contract/object_schema/SchemaChange'; +export type { SchemaChangeKind } from './generated/mobile-kernel-contract/object_schema/SchemaChangeKind'; +export type { SchemaProposal } from './generated/mobile-kernel-contract/object_schema/SchemaProposal'; +export type { SchemaProposalStatus } from './generated/mobile-kernel-contract/object_schema/SchemaProposalStatus'; +export type { SchemaRevision } from './generated/mobile-kernel-contract/object_schema/SchemaRevision'; +export type { SensitiveFieldPolicy } from './generated/mobile-kernel-contract/object_schema/SensitiveFieldPolicy'; +export type { TimeWindow } from './generated/mobile-kernel-contract/object_schema/TimeWindow'; +export type { ViewDefinition } from './generated/mobile-kernel-contract/object_schema/ViewDefinition'; +export type { ViewKind } from './generated/mobile-kernel-contract/object_schema/ViewKind'; +export type { ViewOrigin } from './generated/mobile-kernel-contract/object_schema/ViewOrigin'; +export type { ViewTarget } from './generated/mobile-kernel-contract/object_schema/ViewTarget'; diff --git a/packages/mobile-contracts/src/remote-surface.ts b/packages/mobile-contracts/src/remote-surface.ts new file mode 100644 index 00000000..d586dfbe --- /dev/null +++ b/packages/mobile-contracts/src/remote-surface.ts @@ -0,0 +1,13 @@ +// Generated from synchronized ts-rs bindings. Do not edit by hand. +export type { ControlLease } from './generated/mobile-kernel-contract/remote_surface/ControlLease'; +export type { ControlMode } from './generated/mobile-kernel-contract/remote_surface/ControlMode'; +export type { InputOwner } from './generated/mobile-kernel-contract/remote_surface/InputOwner'; +export type { Point } from './generated/mobile-kernel-contract/remote_surface/Point'; +export type { RemoteSurfaceToken } from './generated/mobile-kernel-contract/remote_surface/RemoteSurfaceToken'; +export type { RemoteSurfaceTransport } from './generated/mobile-kernel-contract/remote_surface/RemoteSurfaceTransport'; +export type { SemanticFrame } from './generated/mobile-kernel-contract/remote_surface/SemanticFrame'; +export type { SurfaceChannel } from './generated/mobile-kernel-contract/remote_surface/SurfaceChannel'; +export type { SurfaceFrame } from './generated/mobile-kernel-contract/remote_surface/SurfaceFrame'; +export type { SurfaceKind } from './generated/mobile-kernel-contract/remote_surface/SurfaceKind'; +export type { TransportKind } from './generated/mobile-kernel-contract/remote_surface/TransportKind'; +export type { Viewport } from './generated/mobile-kernel-contract/remote_surface/Viewport'; diff --git a/packages/mobile-contracts/src/run-event.ts b/packages/mobile-contracts/src/run-event.ts new file mode 100644 index 00000000..1a05f1a4 --- /dev/null +++ b/packages/mobile-contracts/src/run-event.ts @@ -0,0 +1,25 @@ +// Generated from synchronized ts-rs bindings. Do not edit by hand. +export type { Actor } from './generated/mobile-kernel-contract/run_event/Actor'; +export type { ActorKind } from './generated/mobile-kernel-contract/run_event/ActorKind'; +export type { ApprovalRequest } from './generated/mobile-kernel-contract/run_event/ApprovalRequest'; +export type { ApprovalStatus } from './generated/mobile-kernel-contract/run_event/ApprovalStatus'; +export type { Artifact } from './generated/mobile-kernel-contract/run_event/Artifact'; +export type { ArtifactKind } from './generated/mobile-kernel-contract/run_event/ArtifactKind'; +export type { AttentionItem } from './generated/mobile-kernel-contract/run_event/AttentionItem'; +export type { AttentionSource } from './generated/mobile-kernel-contract/run_event/AttentionSource'; +export type { CapabilityGrant } from './generated/mobile-kernel-contract/run_event/CapabilityGrant'; +export type { EventCursor } from './generated/mobile-kernel-contract/run_event/EventCursor'; +export type { ExecutionTarget } from './generated/mobile-kernel-contract/run_event/ExecutionTarget'; +export type { ExecutionTargetKind } from './generated/mobile-kernel-contract/run_event/ExecutionTargetKind'; +export type { MachineClass } from './generated/mobile-kernel-contract/run_event/MachineClass'; +export type { Receipt } from './generated/mobile-kernel-contract/run_event/Receipt'; +export type { ReceiptAction } from './generated/mobile-kernel-contract/run_event/ReceiptAction'; +export type { ResumePage } from './generated/mobile-kernel-contract/run_event/ResumePage'; +export type { RiskLevel } from './generated/mobile-kernel-contract/run_event/RiskLevel'; +export type { Run } from './generated/mobile-kernel-contract/run_event/Run'; +export type { RunEvent } from './generated/mobile-kernel-contract/run_event/RunEvent'; +export type { RunEventKind } from './generated/mobile-kernel-contract/run_event/RunEventKind'; +export type { RunStateSnapshot } from './generated/mobile-kernel-contract/run_event/RunStateSnapshot'; +export type { RunStatus } from './generated/mobile-kernel-contract/run_event/RunStatus'; +export type { Session } from './generated/mobile-kernel-contract/run_event/Session'; +export type { SnapshotWithTail } from './generated/mobile-kernel-contract/run_event/SnapshotWithTail'; diff --git a/packages/mobile-contracts/tests/conformance.ts b/packages/mobile-contracts/tests/conformance.ts new file mode 100644 index 00000000..37a69ba7 --- /dev/null +++ b/packages/mobile-contracts/tests/conformance.ts @@ -0,0 +1,109 @@ +import approvalRequestFixture from '../fixtures/approval_request.json'; +import attentionItemFixture from '../fixtures/attention_item.json'; +import contextLensFixture from '../fixtures/context_lens.json'; +import contractCatalogFixture from '../fixtures/contract_catalog.json'; +import controlLeaseFixture from '../fixtures/control_lease.json'; +import fieldDefinitionFixture from '../fixtures/field_definition.json'; +import graphNeighborhoodFixture from '../fixtures/graph_neighborhood.json'; +import objectTypeFixture from '../fixtures/object_type.json'; +import remoteSurfaceTokenFixture from '../fixtures/remote_surface_token.json'; +import resumePageFixture from '../fixtures/resume_page.json'; +import runEventFixture from '../fixtures/run_event.json'; +import semanticFrameFixture from '../fixtures/semantic_frame.json'; +import snapshotWithTailFixture from '../fixtures/snapshot_with_tail.json'; +import viewDefinitionFixture from '../fixtures/view_definition.json'; + +import type { + ApprovalRequest, + AttentionItem, + ContextLens, + ContractDescriptor, + ControlLease, + FieldDefinition, + GraphNeighborhood, + ObjectType, + RelationDefinition, + RemoteSurfaceToken, + ResumePage, + RunEvent, + SemanticFrame, + SnapshotWithTail, + ViewDefinition, +} from '../src/index'; + +function expectType(_value: T): void {} + +function asRunEvent( + event: Omit & { kind: string }, +): RunEvent { + return { + ...event, + kind: event.kind as RunEvent['kind'], + }; +} + +function asFieldDefinition( + field: Omit & { + fieldType: object; + sensitivity: string; + }, +): FieldDefinition { + return { + ...field, + fieldType: field.fieldType as FieldDefinition['fieldType'], + sensitivity: field.sensitivity as FieldDefinition['sensitivity'], + }; +} + +expectType(asRunEvent(runEventFixture)); +expectType({ + ...resumePageFixture, + events: resumePageFixture.events.map(asRunEvent), +}); +expectType({ + ...snapshotWithTailFixture, + events: snapshotWithTailFixture.events.map(asRunEvent), +}); +expectType({ + ...attentionItemFixture, + source: attentionItemFixture.source as AttentionItem['source'], + risk: attentionItemFixture.risk as NonNullable, +}); +expectType({ + ...approvalRequestFixture, + risk: approvalRequestFixture.risk as ApprovalRequest['risk'], + status: approvalRequestFixture.status as ApprovalRequest['status'], +}); +expectType({ + ...remoteSurfaceTokenFixture, + allowedMode: remoteSurfaceTokenFixture.allowedMode as RemoteSurfaceToken['allowedMode'], + maxRisk: remoteSurfaceTokenFixture.maxRisk as RemoteSurfaceToken['maxRisk'], + surfaceKind: remoteSurfaceTokenFixture.surfaceKind as RemoteSurfaceToken['surfaceKind'], +}); +expectType(semanticFrameFixture); +expectType({ + ...controlLeaseFixture, + mode: controlLeaseFixture.mode as ControlLease['mode'], +}); +expectType(asFieldDefinition(fieldDefinitionFixture)); +expectType({ + ...objectTypeFixture, + enforcement: objectTypeFixture.enforcement as ObjectType['enforcement'], + fields: objectTypeFixture.fields.map(asFieldDefinition), + relations: objectTypeFixture.relations.map((relation): RelationDefinition => ({ + ...relation, + cardinality: relation.cardinality as RelationDefinition['cardinality'], + })), +}); +expectType({ + ...viewDefinitionFixture, + origin: viewDefinitionFixture.origin as ViewDefinition['origin'], + target: viewDefinitionFixture.target as ViewDefinition['target'], + viewKind: viewDefinitionFixture.viewKind as ViewDefinition['viewKind'], +}); +expectType({ + ...contextLensFixture, + sensitiveFieldPolicy: contextLensFixture.sensitiveFieldPolicy as ContextLens['sensitiveFieldPolicy'], +}); +expectType(graphNeighborhoodFixture); +expectType(contractCatalogFixture); diff --git a/packages/mobile-contracts/tsconfig.json b/packages/mobile-contracts/tsconfig.json new file mode 100644 index 00000000..45be3671 --- /dev/null +++ b/packages/mobile-contracts/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowArbitraryExtensions": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + }, + "include": [ + "src/**/*.ts", + "tests/**/*.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ce7a084..6dca4054 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -623,6 +623,9 @@ importers: '@commonplace/block-view': specifier: file:../../packages/block-view version: file:packages/block-view(react@19.2.3) + '@commonplace/mobile-contracts': + specifier: file:../../packages/mobile-contracts + version: link:../../packages/mobile-contracts '@expo-google-fonts/ibm-plex-sans': specifier: ^0.4.1 version: 0.4.1 @@ -925,6 +928,12 @@ importers: specifier: ^4.1.6 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/mobile-contracts: + devDependencies: + typescript: + specifier: ~6.0.3 + version: 6.0.3 + packages/model-canvas: dependencies: '@commonplace/canvas-substrate':