diff --git a/CHANGELOG.md b/CHANGELOG.md index 7147f904..42c6a9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,63 @@ ### Added +- `warp-core` now has an external contract proof fixture for the v0.1.0 local + contract-host path. The fixture installs a generated-style package with a + mutation, conflict-capable mutation, and QueryView query; submits non-trivial + canonical vars; executes only through scheduler-owned ticks; observes a + bounded contract reading; retains reading payload and receipt evidence + through `echo-cas` semantic coordinates; and replays witnessed submission + history to the same observed intent outcome. The fixture keeps application + nouns inside the test package and generated payload shape, not in Echo core. +- `echo-cas` semantic retention now supports bounded byte-range lookup through + `RetainedBlobIndex::load_range(...)`. Range lookup requires the exact + semantic coordinate, enforces the caller's byte budget, and returns typed + `RetentionError` variants for missing coordinates, missing content, + over-budget requests, or out-of-bounds ranges. Content-hash lookup remains a + byte lookup only; semantic success still requires coordinate match. +- `echo-cas` semantic retention now fails closed when the same + `SemanticBlobCoordinate` is retained with different bytes. Retaining the same + coordinate with the same content is idempotent, while conflicting content + returns `RetentionError::SemanticCoordinateConflict`. Bounded range lookup now + proves the semantic coordinate exists before reporting range-budget errors. +- `echo-cas` now provides a local semantic retention index above the + content-only blob store. `RetainedBlobIndex` maps + `SemanticBlobCoordinate` values to retained descriptors for contract + artifacts, receipts, witnesses, reading payloads, reading envelopes, and + observer artifacts while preserving the rule that `BlobHash` names bytes + only. Retained blobs can be loaded by content hash or exact semantic + coordinate, equal bytes under different semantic coordinates do not alias, + and missing coordinates or missing bytes return typed `RetentionError` + variants instead of fake successful reads. +- `warp-core` QueryView readings now carry a `QueryReadingIdentity` in + `ReadingEnvelope`. The identity binds query id, domain-separated vars digest, + resolved basis digest, requested aperture digest, observer plan, and + installed contract evidence when present, while keeping payload bytes in + `ObservationPayload::QueryBytes`. Tests prove reading identity changes when + query vars, query id, causal basis, schema/observer plan, or budget changes. + Over-budget reads still obstruct with `BudgetExceeded`; residual readings + remain explicit posture rather than fake complete payloads. +- `warp-core` query reading identity now excludes observation freshness + metadata from the basis digest. The same QueryView against the same resolved + commit keeps a stable `QueryReadingIdentity` even if unrelated runtime-owned + tick progress changes the observation freshness watermark. +- `warp-core` now publishes observation artifacts under observation contract + version 3 and `echo:observation-artifact:v3` because contract evidence and + query reading identity are now part of the canonical reading envelope hashed + into observation artifacts. +- `echo-wasm-abi` now reports `ABI_VERSION` 11 for the expanded + `ReadingEnvelope` response shape. Legacy retained reading envelopes that omit + the new optional contract/query identity fields decode those fields as + `None`. +- `warp-core` now attaches contract package evidence to installed contract + readings and receipt correlations. Installed QueryView readings carry + package id, package name/version, artifact hash, schema hash, codec identity, + registry version, query op id, and operation kind in the `ReadingEnvelope`. + Installed mutation receipt correlations copy the same package evidence from + ticketed runtime ingress after scheduler-owned execution. Built-in reads and + non-package observers can still leave this evidence empty. The evidence is + metadata only: it does not grant tick authority, mutate state, replace + semantic reading identity, or act as a CAS lookup key. - `warp-core` now connects the installed contract package boundary to the witnessed intent pipeline. Package-supported canonical EINT mutation ids can be staged through ticketed runtime ingress only after Echo has witnessed the diff --git a/Cargo.lock b/Cargo.lock index d836a511..1a4a651c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2055,6 +2055,7 @@ dependencies = [ "bytemuck", "bytes", "ciborium", + "echo-cas", "echo-dry-tests", "echo-registry-api", "echo-runtime-schema", diff --git a/README.md b/README.md index abe111b4..fca599cb 100644 --- a/README.md +++ b/README.md @@ -3,403 +3,113 @@

-ECHO + ECHO

-

- A deterministic WARP runtime for witnessed causal history, bounded optics, and holographic readings. -

-

-Docs • -Architecture • -There Is No Graph • -Continuum • -WSC / Verkle / IPA • -warp-core + A deterministic WARP runtime for witnessed causal history, bounded readings, and replayable evidence.

-Determinism CI -CI -Platforms + Docs + · + Architecture + · + There Is No Graph + · + Continuum + · + warp-core

-# Echo - -Echo is a real-time, deterministic -[WARP optic](https://github.com/flyingrobots/aion) that participates in the -[Continuum protocol](https://github.com/flyingrobots/continuum): a protocol for -exchanging shared, witnessed causal history between distributed peers. - -Unlike traditional runtimes that model state as a mutable object graph, Echo -treats witnessed causal history as the immutable source of truth. State is not -the substrate; it is materialized on demand as a lawful, witnessed, replayable -reading over that history. Graph-shaped state is a reading. Files are readings. -Build outputs are readings. Debugger views are readings. Echo exists to govern -the integrity of those views without making any one view the territory. - -Echo does this through WARP optics: structured, law-named processes for -observing, transforming, importing, and retaining causal history. - -- **Ticking** is an optic that advances a worldline by applying a deterministic - batch of canonical intents. -- **Braiding** is an optic that merges two or more concurrent strands of - history under explicit settlement law. -- **Querying** is an optic that materializes a bounded reading for an observer. -- **Admission** is an optic that lawfully incorporates transported history from - a remote peer. - -Application-authored optics do not create ticks or `TickReceipt` values. -Authored surfaces may declare retained consequence obligations that Echo must -satisfy, including receipt obligations, but only Echo's trusted runtime and -scheduler own tick boundaries and receipt emission. - -Because transformations go through lawful optics, Echo can emit computational -holograms: compact, evidence-bearing boundary artifacts that name the causal -basis, law, aperture, identity, posture, witnesses, and retained support needed -to replay or audit a computation. For ticked execution, that support includes -the initial basis and ordered tick receipts or provenance payloads, so Echo does -not need to retain every intermediate materialized state as authoritative truth. - -Echo operates in a deterministic cycle: it admits canonical intents, schedules -work, settles speculative paths, emits evidence-bearing receipts for every -transition, serves bounded observations to clients, and retains the minimal -artifacts needed for replay or verification. - -You can use Echo to write deterministic real-time simulations, verifiable build -systems, and other applications that demand strong auditability. To get -started, read [Writing An Echo Application](#writing-an-echo-application) and -[Application Contract Hosting](docs/architecture/application-contract-hosting.md). - -## Developer Experience - -Echo is a participant in a larger architecture for provable determinism. Most -applications do not call Echo with application objects directly. They: - -```mermaid -flowchart LR - contract["Author GraphQL contract"] - wesley["Compile with Wesley"] - helpers["Use generated helpers"] - intents["Submit canonical EINT intents"] - runtime["Echo-owned tick, scheduling, and settlement"] - readings["Observe ReadingEnvelope-backed results"] - - contract --> wesley --> helpers --> intents - intents --> runtime --> readings -``` - -Echo handles causal admission, scheduling, receipts, witnesses, retention, -replay, and bounded observations. The application owns domain semantics. -Wesley bridges the two by turning authored contracts into generated Echo-facing -surfaces. - -### 1. Declare A Contract - -The developer journey starts with GraphQL SDL that names the application's -data, operations, readings, and law metadata. GraphQL is the authoring surface, -not Echo's runtime language. Data types define the nouns; operation metadata, -footprints, policies, capabilities, and constraints define the verbs and laws -that govern those nouns. - -### 2. Compile With Wesley - -The [Wesley](https://github.com/flyingrobots/wesley) compiler lowers the -authored contract into generated artifacts that Echo can verify and host: - -- type-safe codecs and operation helpers; -- registry metadata, operation ids, and artifact identity; -- footprint certificates and runtime-facing contract metadata. - -The checked-in Echo path is Rust-first through -[`echo-wesley-gen`](crates/echo-wesley-gen/README.md). TypeScript and browser -generation should follow the same contract identity, registry, -artifact-verification, and footprint-honesty rules instead of inventing a -parallel Echo API. - -### 3. Submit Intents - -Applications do not mutate Echo state directly, and they do not decide when -Echo ticks. They submit canonical EINT bytes through `dispatch_intent(...)`. -That call is ingress: it gives Echo causal input and returns dispatch evidence. -It is not an application-owned tick command or a domain mutation RPC. - -Echo owns the tick boundary, pending-set order, scheduler, and settlement law. -When Echo reaches a runtime-owned tick boundary, it evaluates pending intents -against installed contract law, footprints, and scheduler constraints, then -emits receipts for what was admitted, staged, pluralized, conflicted, or -obstructed. - -The application may receive ingress evidence, admission evidence, reading -evidence, and later tick receipts through observation/correlation surfaces. It -must not treat `dispatch_intent(...)` as "run this now." - -A host may run Echo on a fixed wall-clock cadence, but wall-clock frequency is -host/runtime-owner policy. The semantic tick remains a logical scheduler-owned -coordinate. - -### 4. Observe Readings - -Clients do not ask Echo for "the state." They ask for a bounded reading from an -explicit basis under an observer plan. - -Generated query helpers build `ObservationRequest` values. Echo returns an -`ObservationArtifact` containing payload bytes and a `ReadingEnvelope` naming -the basis, observer, witness references, budget posture, rights posture, and -whether the reading is complete, residual, plural, or obstructed. Application -code decodes the payload only after inspecting that evidence. - -## Reader Paths - -- **Write an app:** start with - [Writing An Echo Application](#writing-an-echo-application), then read - [Application Contract Hosting](docs/architecture/application-contract-hosting.md). -- **Understand the model:** read [WARP And Continuum](#warp-and-continuum), - [Core Ontology](#core-ontology), and - [There Is No Graph](docs/architecture/there-is-no-graph.md). -- **Generate contracts:** use - [echo-wesley-gen](crates/echo-wesley-gen/README.md) with a GraphQL SDL - contract. -- **Hack the runtime:** start with [Core Crates](#core-crates), then run the - [Quick Start](#quick-start) checks. -- **Follow retained readings and proofs:** read - [WSC, Verkle, IPA, And Retained Readings](docs/architecture/wsc-verkle-ipa-retained-readings.md). - -## Why It Exists - -Traditional systems pretend there is one mutable global state: - -```text -program + state -> mutated state -``` - -That model leaks. It turns concurrency into locks, collaboration into merge -pain, debugging into archaeology, and generated artifacts into "trust me, this -script probably ran." +> Echo owns time, admission, scheduling, and witnessed causal history so +> application code can stay focused on domain semantics. -Echo follows the WARP model instead: +# Echo -```text -causal basis + optic law + support obligations -> witnessed reading +**Echo** is a deterministic runtime for building applications on witnessed +causal history instead of mutable in-memory state. -reading + intent + admission law -> witnessed suffix +Traditional runtimes treat "current state" as the source of truth. Echo does +not. In Echo, the durable territory is admitted causal history: submissions, +frontiers, receipts, witnesses, retained artifacts, and replayable evidence. +State-like things such as files, graphs, UI models, build outputs, debugger +snapshots, and query results are **readings** over that history. -witnessed suffix + optic -> new reading -``` +Application code does not mutate Echo state directly, and it does not decide +when Echo ticks. Applications submit canonical intents. Echo admits, schedules, +settles, and executes them at runtime-owned tick boundaries, then emits receipts +and evidence-carrying observations. -The result is not "no state." State-like values still exist everywhere. The -difference is authority: materialized state is a chart, cache, viewport, or -hologram. It is not the territory. - -## WARP And Continuum - -WARP is the runtime/optic model used here. A WARP optic is a bounded, -law-governed participant over causal history. It can observe, admit, retain, -reveal, import, materialize, or verify readings, but it does not own a canonical -global graph. - -Echo implements the paper vocabulary with sharper typed boundaries: -`LawWitness`, `AdmissionTicket`, `TicketedRuntimeIngress`, `TickReceipt`, -`ReadingEnvelope`, graph facts, retained readings, and retained artifacts are -not interchangeable. The implementation map lives at -[WARP Optic Implementation Map](docs/design/warp-optic-implementation-map.md). - -Continuum is the compatibility layer between WARP participants. It is not Echo, -not "the Echo protocol," and not a second runtime that owns the truth. It is the -shared transport vocabulary for exchanging enough causal evidence for another -optic to produce a compatible local reading: - -- causal suffixes; -- coordinates and frontiers; -- witnesses, receipts, and support obligations; -- hologram and reading boundaries; -- optic, rule, schema, and artifact identifiers. - -Echo is one Continuum-speaking WARP participant. `git-warp`, Wesley, Graft, -WARPDrive, `warp-ttd`, and application tools such as `jedit` can also be WARP -participants when they exchange witnessed causal structure instead of pretending -to pass around a privileged graph object. - -The payload is not "the graph." The payload is the causal suffix, coordinate, -support, and witness material needed for another optic to construct its own -lawful reading. - -## Core Ontology - -| Concept | Meaning in Echo | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| **Causal history** | The witnessed substrate: admitted transitions, frontiers, receipts, witnesses, and retained boundary artifacts. | -| **WARP optic** | A bounded, law-named operation over causal history. It may admit, observe, retain, reveal, import, or materialize. | -| **Reading** | An observer-relative artifact emitted from a coordinate, aperture, and projection law. | -| **Hologram** | A witnessed output carrying enough basis, law, aperture, evidence, identity, and posture to recreate the claim at its declared level. | -| **Witness** | Evidence that a transition or reading followed from a named basis under a named law. | -| **Shell** | A retained boundary artifact such as a tick patch, suffix bundle, provenance payload, or checkpoint base. | -| **AdmissionTicket** | Lawful admission evidence. It is not execution and not a `TickReceipt`. | -| **TickReceipt** | Scheduler-owned execution outcome evidence for a committed tick. | -| **ReadIdentity** | The semantic question a retained payload answers. It is intentionally separate from the CAS byte hash. | - -The front-door architecture note is -[There Is No Graph](docs/architecture/there-is-no-graph.md). - -## What Echo Owns - -Echo owns the generic hot runtime path: - -- canonical intent ingress; -- deterministic scheduling and footprint checks; -- rewrite settlement; -- worldline and provenance retention; -- replayable tick patches; -- Merkle commitments over state and patch boundaries; -- observation artifacts and `ReadingEnvelope` metadata; -- WASM/session boundaries for browser and host integration; -- `echo-cas` retention for bytes, witnesses, receipts, and cached readings. - -Echo does **not** own application nouns. - -Names like `ReplaceRange`, `JeditBuffer`, `CounterIncrement`, -`RenameSymbol`, or `GraftProjection` belong in authored contracts, -Wesley-generated code, application adapters, or fixtures. They must not become -Echo substrate APIs. - -## WARP Runtime Flow - -Echo's hot path is deliberately boring: - -1. External callers submit canonical intent bytes. -2. Inbox sequencing derives content identity and canonical pending order. -3. Rules propose candidate rewrites with explicit footprints. -4. The scheduler admits a deterministic independent subset. -5. The engine applies admitted rewrites. -6. Echo emits receipts, tick patches, provenance, and hashes. -7. Observation services resolve coordinates and return readings. -8. Retention stores the bytes and witness material needed for replay or - obstruction. - -The point is not to mutate a global graph. The point is to admit and observe -witnessed causal structure through explicit laws. - -## Application Contracts - -Applications talk to Echo through generated contracts, not app-specific runtime -APIs. - -Wesley is the compiler optic for those contracts. Application authors describe -their domain operations and readings in GraphQL SDL; Wesley lowers that -authored contract into generated helpers, registries, codecs, operation ids, -artifact metadata, and footprint certificates. Echo then hosts the generated -contract through generic dispatch and observation boundaries. - -Wesley exists because Echo's runtime boundary is intentionally generic. Echo -should not learn what `increment`, `ReplaceRange`, `CounterValue`, or -`JeditBuffer` mean. Generated Wesley code gives applications a typed surface -while preserving Echo's substrate rule: - -```text -Application nouns live in contracts. -Echo receives canonical intents and returns witnessed readings. -``` +## When To Use Echo -The current shape is: - -```text -Application UI - -> application adapter / Wesley-generated contract client - -> canonical operation variables - -> EINT intent bytes - -> host-owned Echo ingress: dispatch_intent(...) - -> accepted / witnessed ingress evidence - -> later runtime-owned scheduler tick / settlement - -> Echo receipts and witness refs - -> Echo observe(...) - -> ReadingEnvelope + payload bytes - -> generated/application decoding - -> UI -``` +| Need | Echo Provides | Example Use Cases | +| ------------------------------ | ---------------------------------------------------------- | ---------------------------------------------------- | +| Deterministic execution | Runtime-owned ticks, admission, scheduling, and settlement | Simulations, engines, structured editors | +| Replayability and auditability | Witnessed causal history, receipts, and retained artifacts | Build systems, compliance tools, versioned pipelines | +| Evidence-carrying reads | Payloads wrapped in `ReadingEnvelope` evidence | Debugging, time travel, proof-carrying data | +| Law-governed collaboration | Intent submission over shared causal history | Multi-user structured editing | +| Causal transport | Witnessed suffix import/export instead of state sync | Multi-runtime and peer-to-peer systems | +| Retained readings | Content retention plus semantic lookup | Audit, forensics, replay, "what happened?" analysis | -The application layer submits canonical bytes and reads observations. It does -not own the scheduler lifecycle or decide when Echo ticks; host/runtime policy -owns that authority. +## Philosophy: There Is No Graph -This is why a serious text editor such as `jedit` can own its rope model, -buffer law, edit-group law, checkpoint policy, and UI behavior while Echo stays -generic. Echo hosts the generated contract, verifies artifact metadata, admits -intents, emits readings, and retains bytes. It does not become a text editor. +Echo is not a graph database. It is not a mutable state server. It is a +deterministic WARP runtime over witnessed causal history. -See [Application Contract Hosting](docs/architecture/application-contract-hosting.md). +| Dimension | Traditional Runtime | Echo | +| --------------- | -------------------------------------- | ----------------------------------- | +| Source of truth | Mutable in-memory state | Witnessed causal history | +| Change model | Direct mutation | Canonical intent submission | +| Time authority | Application callbacks, events, threads | Trusted runtime scheduler | +| Read model | "Give me current state" | Bounded reading from explicit basis | +| Read result | Bare payload | Payload plus `ReadingEnvelope` | +| Distribution | Replicate state | Exchange witnessed causal suffixes | -## Writing An Echo Application +**In Echo, causal history is primary. Everything else is derived.** -The normal authoring loop is contract-first: +Read [There Is No Graph](docs/architecture/there-is-no-graph.md) for the deeper +model. -1. Author a GraphQL SDL contract in the application repo. -2. Declare operation/read names with `@wes_op`. -3. Declare deterministic access footprints with `@wes_footprint` when the - operation mutates or observes application state. -4. Run `echo-wesley-gen` to generate Rust contract helpers. -5. Have the host verify the generated registry/artifact metadata. -6. Use generated helpers to pack EINT intent bytes and submit them to Echo - ingress. -7. Let Echo's runtime-owned tick cycle admit work, emit receipts, retain - witnesses, and make observations available. -8. Use generated query helpers to build observation requests, then decode and - present the returned reading in the application after inspecting its - `ReadingEnvelope`. +## How It Works -The end-to-end shape is: +1. Author your domain model as a GraphQL contract. +2. Compile it with Wesley into generated helpers, codecs, and contract + artifacts. +3. Submit canonical intents through Echo's generic ingress boundary. +4. Echo owns admission, scheduling, ticks, settlement, and execution. +5. Observe results as `ObservationArtifact`s with `ReadingEnvelope` evidence. ```mermaid -flowchart TD - contract["counter.graphql"] - gen["echo-wesley-gen"] - generated["generated.rs"] - - verify["verify_contract_artifact(...)"] - pack["pack_increment_intent(...)"] - dispatch["dispatch_intent(...)"] - tick["Echo-owned tick / settlement"] - - request["counter_value_observation_request(...)"] - observe["observe(...)"] - envelope["inspect ReadingEnvelope"] - - subgraph compile["Compile Contract"] - contract --> gen --> generated - end - - subgraph write["Admit Intent"] - generated --> verify --> pack --> dispatch --> tick - end - - subgraph read["Observe Reading"] - generated --> request --> observe --> envelope - end - - tick -. "receipts and witness refs" .-> observe -``` - -A tiny contract looks like this: +sequenceDiagram + participant Dev + participant App + participant Wesley + participant Echo + + Dev->>Dev: Author GraphQL contract + Dev->>Wesley: Compile with echo-wesley-gen + Wesley-->>Dev: Generated helpers + contract artifacts + App->>Echo: Submit canonical intent + Echo-->>App: DispatchResponse with ingress evidence + Echo->>Echo: Runtime-owned admission, scheduling, tick + App->>Echo: Send ObservationRequest + Echo-->>App: ObservationArtifact + ReadingEnvelope +``` + +## Contracts And Boundaries + +Echo core is intentionally generic. Application nouns belong in authored +contracts and generated adapters, not in the runtime kernel. + +- You define nouns, operations, and queries in GraphQL. +- You use Wesley directives such as `@wes_op` and `@wes_footprint` to describe + operation identity and deterministic footprint claims. +- Wesley generates type-safe helpers, codecs, registry metadata, and host + adapters. +- Echo verifies and hosts those artifacts through stable generic boundaries. ```graphql -directive @wes_op(name: String!) on FIELD_DEFINITION -directive @wes_footprint( - reads: [String!] - writes: [String!] -) on FIELD_DEFINITION - -type CounterValue { - value: Int! -} - -input IncrementInput { - amount: Int! -} - -type Query { - counterValue: CounterValue! @wes_op(name: "counterValue") -} - type Mutation { increment(input: IncrementInput!): CounterValue! @wes_op(name: "increment") @@ -407,283 +117,114 @@ type Mutation { } ``` -Generate the Rust contract surface: - -```bash -cargo run -p echo-wesley-gen -- --schema counter.graphql --out generated.rs -``` - -Application code should use generated helpers rather than hand-rolling Echo -wire bytes. Dispatch submits canonical input to Echo; it does not command Echo -to tick. Conceptually: - -```rust -let intent = generated::pack_increment_intent( - &generated::__echo_wesley_generated::IncrementVars { - input: generated::IncrementInput { amount: 1 }, - }, -)?; - -let dispatch_response = echo_wasm_abi::kernel_port::KernelPort::dispatch_intent( - &mut kernel, - &intent, -)?; -``` - -Echo's runtime owns the tick cadence, scheduler, settlement, and receipt -emission. For reads, generated query helpers build `ObservationRequest` values. -Echo returns an `ObservationArtifact` containing payload bytes plus a -`ReadingEnvelope`; the application should inspect that envelope before treating -the reading as complete. - -Current checked-in generation is Rust-first. TypeScript/browser generation -should follow the same contract identity, registry, artifact-verification, and -footprint-honesty rules rather than inventing a separate Echo API. - -### Boundary Vocabulary - -- **GraphQL SDL contract:** the application-owned declaration of types, - operations, reads, and metadata. -- **Wesley:** the compiler optic that lowers the contract into generated Echo - helpers and registry metadata. -- **EINT:** Echo's canonical intent envelope. Generated helpers pack operation - variables into this shape. -- **ObservationRequest:** the generic Echo read request produced by generated - query helpers. -- **ReadingEnvelope:** the evidence wrapper around a returned reading. It names - basis, observer, projection, witness references, and whether the reading is - complete, residual, obstructed, or otherwise limited. -- **Artifact verification:** the host check that a generated contract registry - matches the expected schema, codec, registry version, and certificate - posture. - -## What Not To Put In Echo - -Echo is generic substrate. Keep application semantics above the generated -contract boundary. - -Do not add: - -- app-specific runtime APIs such as `replace_range(...)`, - `increment_counter(...)`, `rename_symbol(...)`, or `save_buffer(...)`; -- application-owned structs as core Echo state; -- GraphQL execution as Echo's runtime language; -- hand-rolled EINT packing in product code when generated helpers exist; -- jedit, Graft, Wesley, Continuum, or `git-warp` ownership inside Echo core. - -The operational anchor is: - -```text -big ontology claim: there is no privileged graph -runtime consequence: Echo stores witnessed causal history and serves readings -through explicit dispatch and observation boundaries -``` - -## jedit Boundary - -`jedit` is expected to be a serious Echo consumer, not an Echo submodule. - -`jedit` owns: - -- rope model and buffer semantics; -- edit group law; -- dirty state and checkpoint policy; -- editor UI and user interaction policy; -- the external text GraphQL contract. - -Wesley owns: - -- compiling that external GraphQL contract into generated helpers; -- carrying contract identity, schema identity, operation ids, registry - metadata, and footprint certificates. +## Core Guarantees + +- **Runtime-owned time**: application code cannot tick Echo or choose scheduler + boundaries. +- **Deterministic execution**: ticks, admission, handler dispatch, and + settlement are scheduler-owned. +- **Evidence-first observations**: readings carry basis, observer, witness, + budget, rights, and residual posture. +- **Replayable history**: submissions, receipts, witnesses, and retained + artifacts are structured for audit and replay. +- **Domain separation**: Echo core stays generic; application semantics live in + contracts. +- **Continuum-oriented transport**: Echo is built for witnessed causal suffix + exchange, not blind state synchronization. + +## Determinism, Ticks, And The Scheduler + +Echo enforces determinism by narrowing every application action into explicit, +canonical evidence before the scheduler can act on it: + +- application input enters as canonical EINT bytes, not ad hoc callbacks; +- Wesley-generated contract metadata names operation ids, codecs, and + footprint claims; +- Echo-owned admission decides whether submitted work can become scheduler + work; +- the scheduler drains eligible work in deterministic order under explicit + conflict and footprint rules; +- handlers run only during scheduler-owned ticks; +- every committed tick emits receipt evidence that can be replayed and checked. + +`dispatch_intent(...)` is ingress. It is not "run this now." A host may run Echo +on a fixed wall-clock cadence or in an until-idle loop, but that cadence is +trusted runtime policy. The semantic tick is a logical scheduler-owned +coordinate, not application timing. + +When a tick is attempted, Echo treats it as failure-atomic scheduler work: +lawful conflicts or obstructions become receipt evidence, while internal +runtime faults roll back uncommitted writes and quarantine the affected lane +instead of silently retrying forever. + +## What Echo Owns vs. What You Own Echo owns: -- generic contract hosting; -- intent admission and scheduling; -- receipts, witnesses, and retained bytes; -- contract-aware readings and `ReadingEnvelope` posture. - -Echo tests may use generated `jedit` Wesley output as a fixture. Echo should not -author the `jedit` contract or grow text-editor APIs. - -## Retained Readings: WSC, Verkle, IPA, CAS - -Echo's retained-reading direction is: - -```text -WSC = canonical columnar bytes for a reading or checkpoint -Verkle = authenticated commitment/index over those bytes -IPA = compact proof mechanism for opening bounded apertures -echo-cas = content-addressed byte retention -``` - -Short version: - -```text -WSC gives us the table. -Verkle gives us the root. -IPA gives us the aperture proof. -echo-cas stores the bytes. -``` - -Current reality: - -- `warp-core` has WSC writing, validation, and borrowed view support. -- `echo-cas` stores opaque bytes by `BLAKE3(bytes)`. -- retained reading identity is intentionally separate from CAS byte identity. +- causal history, frontiers, and runtime coordinates; +- admission, scheduling, ticks, and settlement; +- receipts, witnesses, reading envelopes, and retained artifacts; +- bounded observation machinery; +- generic contract hosting and suffix import/export surfaces. -Future direction: +You own: -- WSC-backed retained readings and checkpoints; -- Verkle or equivalent authenticated indexes over WSC coordinates; -- IPA or equivalent compact opening proofs for proof-carrying apertures; -- bounded reads that can verify selected rows, chunks, or ranges without - materializing the full retained reading. - -This is future proof infrastructure, not a new ontology. WSC is not truth. -Verkle is not truth. IPA is not storage. CAS is not semantic identity. - -See [WSC, Verkle, IPA, And Retained Readings](docs/architecture/wsc-verkle-ipa-retained-readings.md). - -## Current Reality - -Works today: - -- Rust contract generation from GraphQL SDL through `echo-wesley-gen`; -- generated registry metadata and operation descriptors; -- generated footprint certificate constants for `@wes_footprint`; -- host-side contract artifact verification through `echo-registry-api`; -- generic EINT dispatch and observation plumbing; -- WSC writing, validation, inspection, and borrowed views in `warp-core`; -- content-addressed byte retention in `echo-cas`; -- docs and Method backlog tracking for active contract-hosting work. - -Designed or in progress: - -- TypeScript/browser generator parity; -- generated `jedit` contract fixtures as Echo integration evidence; -- contract-aware receipts and readings with full application identity; -- WSC-backed retained readings and checkpoints; -- Verkle or equivalent authenticated retained-reading indexes; -- IPA or equivalent proof-carrying aperture openings; -- full Continuum interchange across Echo, `git-warp`, Wesley, Graft, - WARPDrive, and `warp-ttd`. - -## Determinism Posture - -Echo is built around exact replay and cross-platform convergence. - -The runtime treats nondeterminism as an input discipline problem: - -- no ambient wall-clock time in admitted simulation law; -- no unseeded randomness inside ticks; -- platform-sensitive math is pinned behind deterministic representations; -- canonical CBOR is used at ABI boundaries; -- footprint declarations constrain parallel work; -- receipts and patches carry the evidence needed for replay and audit. - -The slogan is not "parallelism is safe because we hope so." The rule is: - -```text -parallel work is admitted only when the runtime can prove the admitted subset -is lawful for the current basis. -``` +- domain semantics; +- product policy and UI; +- authored GraphQL contracts; +- generated contract helpers and host integrations. -## Core Crates - -| Crate | Role | -| -------------------------- | -------------------------------------------------------------------------------------------------------- | -| `warp-core` | Hot runtime kernel: worldlines, scheduling, settlement, observation, WSC, receipts, and core WARP state. | -| `echo-wasm-abi` | Canonical host/runtime DTOs, `KernelPort`, canonical CBOR helpers, observation and dispatch surfaces. | -| `warp-wasm` | Browser/JavaScript boundary around the runtime kernel. | -| `warp-cli` | Native CLI for WSC inspection, validation, and runtime support tooling. | -| `echo-registry-api` | Minimal generic registry boundary for generated application contracts. | -| `echo-wesley-gen` | Wesley-to-Echo Rust generator for generated DTOs, op ids, registry metadata, and contract helpers. | -| `echo-cas` | Content-addressed byte store. It stores bytes; typed identity lives above it. | -| `echo-ttd` / `ttd-browser` | Time-travel/debugging protocol surfaces and browser bridges. | -| `echo-dind-*` | Cross-platform determinism harnesses and evidence tooling. | - -## Quick Start - -### Hacking Echo - -Install hooks and check the current Method view: +## Quick Start For Contributors ```bash make hooks -cargo xtask method status --json -``` - -Run a fast runtime slice: - -```bash +cargo xtask method status cargo xtask test-slice warp-core-smoke -``` - -Run focused generated-contract checks: - -```bash -cargo test -p echo-wesley-gen -cargo test -p echo-registry-api -``` - -Build the docs: - -```bash -pnpm docs:build -``` - -Run the determinism harness: - -```bash cargo xtask dind run ``` -### Generating A Contract +## Benchmarks And Gates -Generate a Rust contract surface from GraphQL SDL: +Echo treats determinism and performance as executable claims, not aspirations. +CI includes deterministic math guards, materialization determinism, DIND replay +checks, decoder security tests, reproducible WASM builds, rustdoc warnings, +clippy lanes, and criterion-based performance regression gates. -```bash -cargo run -p echo-wesley-gen -- --schema counter.graphql --out generated.rs -``` - -Generate to stdout while iterating: +Scheduler benchmarks live in +[Scheduler Performance](docs/benchmarks/scheduler-performance-warp-core.md). +Run them locally with: ```bash -cargo run -p echo-wesley-gen -- --schema counter.graphql +cargo bench -p warp-benches ``` -### Inspecting WSC +## Key Crates -Inspect a WSC snapshot: +- `warp-core`: deterministic runtime kernel +- `echo-wasm-abi`: public ABI and wire DTOs +- `echo-wesley-gen`: Wesley contract helper generator +- `echo-cas`: content-addressed retention and semantic lookup +- `echo-ttd`: time-travel and playback tooling -```bash -export SNAPSHOT=/path/to/state.wsc +## Onramps -cargo run -p warp-cli -- inspect "$SNAPSHOT" -cargo run -p warp-cli -- inspect "$SNAPSHOT" --tree -cargo run -p warp-cli -- verify "$SNAPSHOT" -cargo run -p warp-cli -- --format json verify "$SNAPSHOT" -``` +- Building applications: + [Application Contract Hosting](docs/architecture/application-contract-hosting.md) +- Understanding the model: + [There Is No Graph](docs/architecture/there-is-no-graph.md) +- Core runtime details: [warp-core spec](docs/spec/warp-core.md) +- Causal transport: [Continuum Transport](docs/architecture/continuum-transport.md) +- Documentation map: [Docs](docs/index.md) -## Documentation Map +## Status -- [Docs index](docs/index.md) -- [Current bearing](docs/BEARING.md) -- [Runtime model](docs/architecture/outline.md) -- [There Is No Graph](docs/architecture/there-is-no-graph.md) -- [Continuum Transport](docs/architecture/continuum-transport.md) -- [Application Contract Hosting](docs/architecture/application-contract-hosting.md) -- [echo-wesley-gen CLI](crates/echo-wesley-gen/README.md) -- [WSC, Verkle, IPA, And Retained Readings](docs/architecture/wsc-verkle-ipa-retained-readings.md) -- [warp-core spec](docs/spec/warp-core.md) -- [WASM ABI contract](docs/spec/SPEC-0009-wasm-abi.md) -- [Theory map](docs/theory/THEORY.md) -- [Contributor workflow](docs/workflows.md) +Echo has a working deterministic kernel, installed contract hosting, witnessed +intent submission, scheduler-owned execution, observation envelopes, semantic +retention, suffix transport surfaces, and playback tooling. ---- +The current `v0.1.0` goal is narrower and practical: make Echo a usable local +deterministic contract host. Ongoing work focuses on durable submission +persistence, product-facing intent outcome APIs, reference host loops, retained +evidence polish, release-grade quickstarts, and deeper Continuum integration. -

-Built by FLYING•ROBOTS. -

+Built by [FLYING ROBOTS](https://github.com/flyingrobots). diff --git a/crates/echo-cas/src/lib.rs b/crates/echo-cas/src/lib.rs index 8f15eca4..425a018b 100644 --- a/crates/echo-cas/src/lib.rs +++ b/crates/echo-cas/src/lib.rs @@ -22,7 +22,12 @@ #![forbid(unsafe_code)] mod memory; +mod retention; pub use memory::MemoryTier; +pub use retention::{ + RetainedBlob, RetainedBlobDescriptor, RetainedBlobIndex, RetainedBlobRange, RetainedBlobRole, + RetentionError, SemanticBlobCoordinate, +}; use std::sync::Arc; diff --git a/crates/echo-cas/src/retention.rs b/crates/echo-cas/src/retention.rs new file mode 100644 index 00000000..c6dfac46 --- /dev/null +++ b/crates/echo-cas/src/retention.rs @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Semantic retention coordinates above the content-only CAS layer. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use thiserror::Error; + +use crate::{blob_hash, BlobHash, BlobStore}; + +/// Semantic role of a retained Echo blob. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RetainedBlobRole { + /// Generated contract artifact bytes. + ContractArtifact, + /// Scheduler receipt or receipt-adjacent material. + ContractReceipt, + /// Witness material. + Witness, + /// Reading payload bytes. + ReadingPayload, + /// Encoded reading envelope bytes. + ReadingEnvelope, + /// Generated observer artifact bytes. + ObserverArtifact, +} + +/// Semantic coordinate for a retained contract blob. +/// +/// CAS identity remains content-only. This coordinate names the question the +/// retained bytes answer: contract namespace, schema, artifact, role, and a +/// caller-supplied semantic digest for the specific receipt, witness, reading, +/// or artifact coordinate. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SemanticBlobCoordinate { + /// Contract or package namespace. + pub namespace: String, + /// Hex-encoded schema hash for the contract family. + pub schema_hash_hex: String, + /// Hex-encoded package/artifact hash. + pub artifact_hash_hex: String, + /// Retained blob role. + pub role: RetainedBlobRole, + /// Domain-separated semantic coordinate digest owned by the caller. + pub semantic_digest: [u8; 32], +} + +/// Descriptor for retained bytes under a semantic coordinate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RetainedBlobDescriptor { + /// Semantic coordinate that names the retained bytes. + pub coordinate: SemanticBlobCoordinate, + /// Content-only CAS hash for the retained bytes. + pub content_hash: BlobHash, + /// Retained byte length. + pub byte_len: u64, +} + +/// Retained bytes plus their descriptor. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RetainedBlob { + /// Semantic descriptor. + pub descriptor: RetainedBlobDescriptor, + /// Retained content bytes. + pub bytes: Arc<[u8]>, +} + +/// Bounded byte range loaded through a semantic coordinate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RetainedBlobRange { + /// Semantic descriptor for the source blob. + pub descriptor: RetainedBlobDescriptor, + /// Starting byte offset in the source blob. + pub offset: u64, + /// Bounded retained bytes. + pub bytes: Arc<[u8]>, +} + +/// Typed semantic-retention lookup failures. +#[derive(Clone, Debug, PartialEq, Eq, Error)] +pub enum RetentionError { + /// No descriptor exists for the semantic coordinate. + #[error("missing semantic retention coordinate: {coordinate:?}")] + MissingSemanticCoordinate { + /// Requested semantic coordinate. + coordinate: SemanticBlobCoordinate, + }, + /// The semantic descriptor exists, but its content bytes are not retained locally. + #[error("missing retained blob content: {content_hash}")] + MissingBlob { + /// Missing content hash. + content_hash: BlobHash, + }, + /// The requested bounded range exceeds the caller's byte budget. + #[error("retained blob range exceeds budget: requested {requested_bytes}, max {max_bytes}")] + RangeExceedsBudget { + /// Requested range length. + requested_bytes: u64, + /// Caller-provided byte budget. + max_bytes: u64, + }, + /// The requested range is outside the retained blob. + #[error( + "retained blob range is out of bounds: offset {offset}, len {len}, blob len {byte_len}" + )] + RangeOutOfBounds { + /// Requested start offset. + offset: u64, + /// Requested range length. + len: u64, + /// Retained blob length. + byte_len: u64, + }, + /// A semantic coordinate already names different retained bytes. + #[error( + "semantic retention coordinate conflict: {coordinate:?} already names {existing_content_hash}, not {new_content_hash}" + )] + SemanticCoordinateConflict { + /// Conflicting semantic coordinate. + coordinate: Box, + /// Existing content hash for the coordinate. + existing_content_hash: BlobHash, + /// Newly supplied content hash. + new_content_hash: BlobHash, + }, +} + +/// In-memory semantic index over a [`BlobStore`]. +/// +/// This index does not change CAS hashing. It records which content-only blob +/// answers a specific semantic coordinate and pins retained bytes as local +/// retention roots. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RetainedBlobIndex { + descriptors: BTreeMap, +} + +impl RetainedBlobIndex { + /// Retains bytes under a semantic coordinate and pins the content hash. + pub fn retain( + &mut self, + store: &mut S, + coordinate: SemanticBlobCoordinate, + bytes: &[u8], + ) -> Result { + let content_hash = blob_hash(bytes); + if let Some(existing) = self.descriptors.get(&coordinate) { + if existing.content_hash != content_hash || existing.byte_len != bytes.len() as u64 { + return Err(RetentionError::SemanticCoordinateConflict { + coordinate: Box::new(coordinate), + existing_content_hash: existing.content_hash, + new_content_hash: content_hash, + }); + } + if !store.has(&existing.content_hash) { + let restored_hash = store.put(bytes); + debug_assert_eq!(restored_hash, existing.content_hash); + } + store.pin(&existing.content_hash); + return Ok(existing.clone()); + } + + let content_hash = store.put(bytes); + store.pin(&content_hash); + let descriptor = RetainedBlobDescriptor { + coordinate: coordinate.clone(), + content_hash, + byte_len: bytes.len() as u64, + }; + self.descriptors.insert(coordinate, descriptor.clone()); + Ok(descriptor) + } + + /// Returns the descriptor for a semantic coordinate. + #[must_use] + pub fn descriptor( + &self, + coordinate: &SemanticBlobCoordinate, + ) -> Option<&RetainedBlobDescriptor> { + self.descriptors.get(coordinate) + } + + /// Loads retained bytes by content hash only. + /// + /// This is byte lookup, not semantic authority. Call [`Self::load`] when the + /// caller needs proof that the bytes answer a specific semantic coordinate. + pub fn load_by_hash( + &self, + store: &S, + content_hash: BlobHash, + ) -> Result, RetentionError> { + store + .get(&content_hash) + .ok_or(RetentionError::MissingBlob { content_hash }) + } + + /// Loads retained bytes only when the semantic coordinate is indexed and + /// the content bytes are still present locally. + pub fn load( + &self, + store: &S, + coordinate: &SemanticBlobCoordinate, + ) -> Result { + let descriptor = self.descriptors.get(coordinate).cloned().ok_or_else(|| { + RetentionError::MissingSemanticCoordinate { + coordinate: coordinate.clone(), + } + })?; + let bytes = self.load_by_hash(store, descriptor.content_hash)?; + Ok(RetainedBlob { descriptor, bytes }) + } + + /// Loads a bounded byte range through an exact semantic coordinate. + /// + /// This is retained-payload lookup, not a streaming subscription surface. + /// The semantic coordinate must match first, then the requested range must + /// fit inside the caller-provided byte budget. + pub fn load_range( + &self, + store: &S, + coordinate: &SemanticBlobCoordinate, + offset: u64, + len: u64, + max_bytes: u64, + ) -> Result { + let retained = self.load(store, coordinate)?; + if len > max_bytes { + return Err(RetentionError::RangeExceedsBudget { + requested_bytes: len, + max_bytes, + }); + } + let end = offset + .checked_add(len) + .ok_or(RetentionError::RangeOutOfBounds { + offset, + len, + byte_len: retained.descriptor.byte_len, + })?; + if end > retained.descriptor.byte_len { + return Err(RetentionError::RangeOutOfBounds { + offset, + len, + byte_len: retained.descriptor.byte_len, + }); + } + let start = usize::try_from(offset).map_err(|_| RetentionError::RangeOutOfBounds { + offset, + len, + byte_len: retained.descriptor.byte_len, + })?; + let end = usize::try_from(end).map_err(|_| RetentionError::RangeOutOfBounds { + offset, + len, + byte_len: retained.descriptor.byte_len, + })?; + + Ok(RetainedBlobRange { + descriptor: retained.descriptor, + offset, + bytes: Arc::from(&retained.bytes[start..end]), + }) + } +} diff --git a/crates/echo-cas/tests/semantic_retention.rs b/crates/echo-cas/tests/semantic_retention.rs new file mode 100644 index 00000000..9f6a93ac --- /dev/null +++ b/crates/echo-cas/tests/semantic_retention.rs @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Semantic retention tests above the content-only CAS layer. + +use echo_cas::{ + blob_hash, BlobHash, BlobStore, CasError, MemoryTier, RetainedBlobIndex, RetainedBlobRole, + RetentionError, SemanticBlobCoordinate, +}; +use std::sync::Arc; + +#[derive(Default)] +struct CountingStore { + inner: MemoryTier, + put_calls: usize, +} + +impl BlobStore for CountingStore { + fn put(&mut self, bytes: &[u8]) -> BlobHash { + self.put_calls += 1; + self.inner.put(bytes) + } + + fn put_verified(&mut self, expected: BlobHash, bytes: &[u8]) -> Result<(), CasError> { + self.inner.put_verified(expected, bytes) + } + + fn get(&self, hash: &BlobHash) -> Option> { + self.inner.get(hash) + } + + fn has(&self, hash: &BlobHash) -> bool { + self.inner.has(hash) + } + + fn pin(&mut self, hash: &BlobHash) { + self.inner.pin(hash); + } + + fn unpin(&mut self, hash: &BlobHash) { + self.inner.unpin(hash); + } +} + +fn coordinate(role: RetainedBlobRole, semantic_seed: u8) -> SemanticBlobCoordinate { + SemanticBlobCoordinate { + namespace: "contract:toy-counter".to_owned(), + schema_hash_hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + .to_owned(), + artifact_hash_hex: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_owned(), + role, + semantic_digest: [semantic_seed; 32], + } +} + +#[test] +fn retained_contract_artifact_loads_by_hash_and_semantic_coordinate() -> Result<(), String> { + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let bytes = b"generated contract artifact bytes"; + let coord = coordinate(RetainedBlobRole::ContractArtifact, 1); + + let descriptor = index + .retain(&mut blobs, coord.clone(), bytes) + .map_err(|err| format!("retain failed: {err:?}"))?; + + assert_eq!(descriptor.content_hash, blob_hash(bytes)); + assert_eq!(descriptor.byte_len, bytes.len() as u64); + let by_hash = index + .load_by_hash(&blobs, descriptor.content_hash) + .map_err(|err| format!("load by hash failed: {err:?}"))?; + assert_eq!(by_hash.as_ref(), bytes); + let retained = index + .load(&blobs, &coord) + .map_err(|err| format!("load by semantic coordinate failed: {err:?}"))?; + assert_eq!(retained.descriptor, descriptor); + assert_eq!(retained.bytes.as_ref(), bytes); + Ok(()) +} + +#[test] +fn same_bytes_under_different_semantic_coordinates_do_not_alias() -> Result<(), String> { + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let bytes = b"same retained bytes"; + let artifact = coordinate(RetainedBlobRole::ContractArtifact, 2); + let reading = coordinate(RetainedBlobRole::ReadingPayload, 3); + + let artifact_descriptor = index + .retain(&mut blobs, artifact.clone(), bytes) + .map_err(|err| format!("artifact retain failed: {err:?}"))?; + let reading_descriptor = index + .retain(&mut blobs, reading.clone(), bytes) + .map_err(|err| format!("reading retain failed: {err:?}"))?; + + assert_eq!( + artifact_descriptor.content_hash, + reading_descriptor.content_hash + ); + assert_ne!( + artifact_descriptor.coordinate, + reading_descriptor.coordinate + ); + let loaded_artifact = index + .load(&blobs, &artifact) + .map_err(|err| format!("artifact semantic load failed: {err:?}"))?; + let loaded_reading = index + .load(&blobs, &reading) + .map_err(|err| format!("reading semantic load failed: {err:?}"))?; + assert_eq!(loaded_artifact.descriptor, artifact_descriptor); + assert_eq!(loaded_reading.descriptor, reading_descriptor); + Ok(()) +} + +#[test] +fn missing_semantic_coordinate_returns_typed_obstruction() -> Result<(), String> { + let blobs = MemoryTier::new(); + let index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ReadingEnvelope, 4); + + let err = index + .load(&blobs, &coord) + .err() + .ok_or_else(|| "missing semantic coordinate unexpectedly loaded".to_owned())?; + + assert_eq!( + err, + RetentionError::MissingSemanticCoordinate { coordinate: coord } + ); + Ok(()) +} + +#[test] +fn semantic_lookup_reads_bounded_byte_range_under_budget() -> Result<(), String> { + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ReadingPayload, 5); + index + .retain(&mut blobs, coord.clone(), b"abcdefghijklmnopqrstuvwxyz") + .map_err(|err| format!("retain failed: {err:?}"))?; + + let range = index + .load_range(&blobs, &coord, 4, 6, 6) + .map_err(|err| format!("bounded range lookup failed: {err:?}"))?; + + assert_eq!(range.offset, 4); + assert_eq!(range.bytes.as_ref(), b"efghij"); + assert_eq!(range.descriptor.coordinate, coord); + Ok(()) +} + +#[test] +fn semantic_range_lookup_returns_budget_obstruction() -> Result<(), String> { + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ReadingPayload, 6); + index + .retain(&mut blobs, coord.clone(), b"bounded payload") + .map_err(|err| format!("retain failed: {err:?}"))?; + + let err = index + .load_range(&blobs, &coord, 0, 8, 4) + .err() + .ok_or_else(|| "over-budget range unexpectedly loaded".to_owned())?; + + assert_eq!( + err, + RetentionError::RangeExceedsBudget { + requested_bytes: 8, + max_bytes: 4, + } + ); + Ok(()) +} + +#[test] +fn semantic_lookup_requires_exact_coordinate_even_when_content_hash_matches() -> Result<(), String> +{ + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ReadingPayload, 7); + let wrong = coordinate(RetainedBlobRole::ReadingPayload, 8); + let descriptor = index + .retain(&mut blobs, coord, b"same content") + .map_err(|err| format!("retain failed: {err:?}"))?; + + assert_eq!( + index + .load_by_hash(&blobs, descriptor.content_hash) + .map_err(|err| format!("load by content hash failed: {err:?}"))? + .as_ref(), + b"same content" + ); + let err = index + .load(&blobs, &wrong) + .err() + .ok_or_else(|| "wrong semantic coordinate unexpectedly loaded".to_owned())?; + + assert_eq!( + err, + RetentionError::MissingSemanticCoordinate { coordinate: wrong } + ); + Ok(()) +} + +#[test] +fn same_semantic_coordinate_and_content_retain_idempotently() -> Result<(), String> { + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ContractReceipt, 9); + let bytes = b"stable receipt material"; + + let first = index + .retain(&mut blobs, coord.clone(), bytes) + .map_err(|err| format!("first retain failed: {err:?}"))?; + let second = index + .retain(&mut blobs, coord.clone(), bytes) + .map_err(|err| format!("second retain failed: {err:?}"))?; + + assert_eq!(first, second); + assert_eq!(blobs.len(), 1); + assert_eq!(blobs.pinned_count(), 1); + assert_eq!( + index + .load(&blobs, &coord) + .map_err(|err| format!("load after idempotent retain failed: {err:?}"))? + .bytes + .as_ref(), + bytes + ); + Ok(()) +} + +#[test] +fn idempotent_retain_skips_store_put_when_content_is_present() -> Result<(), String> { + let mut blobs = CountingStore::default(); + let mut index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ContractReceipt, 12); + let bytes = b"stable retained bytes"; + + index + .retain(&mut blobs, coord.clone(), bytes) + .map_err(|err| format!("first retain failed: {err:?}"))?; + assert_eq!(blobs.put_calls, 1); + + index + .retain(&mut blobs, coord, bytes) + .map_err(|err| format!("idempotent retain failed: {err:?}"))?; + + assert_eq!(blobs.put_calls, 1); + Ok(()) +} + +#[test] +fn same_semantic_coordinate_with_different_content_is_rejected() -> Result<(), String> { + let mut blobs = MemoryTier::new(); + let mut index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ContractReceipt, 10); + let original = b"original receipt material"; + let conflicting = b"conflicting receipt material"; + let descriptor = index + .retain(&mut blobs, coord.clone(), original) + .map_err(|err| format!("original retain failed: {err:?}"))?; + + let err = index + .retain(&mut blobs, coord.clone(), conflicting) + .err() + .ok_or_else(|| "conflicting semantic retain unexpectedly succeeded".to_owned())?; + + assert_eq!( + err, + RetentionError::SemanticCoordinateConflict { + coordinate: Box::new(coord.clone()), + existing_content_hash: descriptor.content_hash, + new_content_hash: blob_hash(conflicting), + } + ); + assert_eq!( + index + .load(&blobs, &coord) + .map_err(|err| format!("load after conflicting retain failed: {err:?}"))? + .bytes + .as_ref(), + original + ); + Ok(()) +} + +#[test] +fn missing_semantic_coordinate_takes_precedence_over_range_budget() -> Result<(), String> { + let blobs = MemoryTier::new(); + let index = RetainedBlobIndex::default(); + let coord = coordinate(RetainedBlobRole::ReadingPayload, 11); + + let err = index + .load_range(&blobs, &coord, 0, 8, 4) + .err() + .ok_or_else(|| "missing semantic range unexpectedly loaded".to_owned())?; + + assert_eq!( + err, + RetentionError::MissingSemanticCoordinate { coordinate: coord } + ); + Ok(()) +} diff --git a/crates/echo-wasm-abi/src/kernel_port.rs b/crates/echo-wasm-abi/src/kernel_port.rs index bad63f04..9081d216 100644 --- a/crates/echo-wasm-abi/src/kernel_port.rs +++ b/crates/echo-wasm-abi/src/kernel_port.rs @@ -10,7 +10,7 @@ //! //! # ABI Version //! -//! The current ABI version is [`ABI_VERSION`] (10). All response types are +//! The current ABI version is [`ABI_VERSION`] (11). All response types are //! CBOR-encoded using the canonical rules defined in `docs/spec/js-cbor-mapping.md`. //! Breaking changes to response shapes or error codes require a bump to the //! ABI version. @@ -40,7 +40,7 @@ use serde::{ /// /// Increment when response types, error codes, or method signatures change /// in a backward-incompatible way. -pub const ABI_VERSION: u32 = 10; +pub const ABI_VERSION: u32 = 11; fn deserialize_opaque_id<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error> where @@ -1627,6 +1627,55 @@ pub enum ReadingResidualPosture { Obstructed, } +/// Installed contract operation kind carried by receipt and reading evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ContractOperationKind { + /// Generated mutation operation. + Mutation, + /// Generated query operation. + Query, +} + +/// Contract package identity attached to receipt and reading evidence. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ContractEvidenceIdentity { + /// Deterministic installed package id. + pub package_id: Vec, + /// Runtime package name chosen by the host. + pub package_name: String, + /// Runtime package version chosen by the host. + pub package_version: String, + /// Hex-encoded generated package artifact hash. + pub artifact_hash_hex: String, + /// Registry codec identity verified at install time. + pub codec_id: String, + /// Registry version verified at install time. + pub registry_version: u32, + /// Hex-encoded authored schema hash verified at install time. + pub schema_sha256_hex: String, + /// Generated operation/query id handled by this package. + pub op_id: u32, + /// Generated operation kind. + pub op_kind: ContractOperationKind, +} + +/// Stable identity of the QueryView question answered by a reading. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct QueryReadingIdentity { + /// Stable digest over query id, vars, basis, observer plan, budget, rights, + /// and installed contract evidence when present. + pub reading_id: Vec, + /// Generated query operation id. + pub query_id: u32, + /// Domain-separated digest of canonical query vars bytes. + pub vars_digest: Vec, + /// Digest of the resolved causal basis and basis posture. + pub basis_digest: Vec, + /// Digest of the requested local read aperture: budget and rights posture. + pub aperture_digest: Vec, +} + /// Reading-envelope metadata carried by every observation artifact. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ReadingEnvelope { @@ -1636,6 +1685,13 @@ pub struct ReadingEnvelope { pub observer_instance: Option, /// Native observer basis used by the reading. pub observer_basis: ReadingObserverBasis, + /// Installed contract package identity, when this reading came from a + /// generated contract observer. + #[serde(default)] + pub contract: Option, + /// Stable query reading identity, when this envelope answered QueryView. + #[serde(default)] + pub query_identity: Option, /// Witnesses or shell references that support the reading. pub witness_refs: Vec, /// Read-side parent/strand basis posture. diff --git a/crates/echo-wasm-abi/src/lib.rs b/crates/echo-wasm-abi/src/lib.rs index 6b56cff6..4a590222 100644 --- a/crates/echo-wasm-abi/src/lib.rs +++ b/crates/echo-wasm-abi/src/lib.rs @@ -449,6 +449,11 @@ mod tests { out } + #[test] + fn kernel_port_abi_version_tracks_reading_envelope_contract_fields() { + assert_eq!(kernel_port::ABI_VERSION, 11); + } + #[test] fn test_pack_unpack_round_trip() { let op_id = 12345; @@ -975,6 +980,7 @@ mod tests { WorldlineTick, }; use alloc::boxed::Box; + use ciborium::value::Value; let reference = crate::kernel_port::ProvenanceRef { worldline_id: WorldlineId::from_bytes([1; 32]), @@ -1006,6 +1012,8 @@ mod tests { }, observer_instance: None, observer_basis: ReadingObserverBasis::CommitBoundary, + contract: None, + query_identity: None, witness_refs: vec![ReadingWitnessRef::ResolvedCommit { reference }], parent_basis_posture: ObservationBasisPosture::Worldline, budget_posture: ReadingBudgetPosture::UnboundedOneShot, @@ -1018,6 +1026,23 @@ mod tests { let decoded: OpticReadingEnvelope = decode_cbor(&encode_cbor(&envelope).unwrap()).unwrap(); assert_eq!(decoded, envelope); + let mut legacy_reading_value = + decode_value(&encode_cbor(&envelope.reading).unwrap()).unwrap(); + assert!(matches!(&legacy_reading_value, Value::Map(_))); + if let Value::Map(entries) = &mut legacy_reading_value { + entries.retain(|(key, _)| { + !matches!( + key, + Value::Text(text) if text == "contract" || text == "query_identity" + ) + }); + } + let legacy_reading: ReadingEnvelope = + decode_cbor(&encode_value(&legacy_reading_value).unwrap()).unwrap(); + assert_eq!(legacy_reading, envelope.reading); + assert_eq!(legacy_reading.contract, None); + assert_eq!(legacy_reading.query_identity, None); + let optic_result = ObserveOpticResult::Reading(Box::new(OpticReading { envelope: envelope.reading.clone(), read_identity: envelope.read_identity.clone(), diff --git a/crates/echo-wesley-gen/tests/generation.rs b/crates/echo-wesley-gen/tests/generation.rs index e22e06f0..e2b9b9b0 100644 --- a/crates/echo-wesley-gen/tests/generation.rs +++ b/crates/echo-wesley-gen/tests/generation.rs @@ -316,6 +316,8 @@ mod tests { }, observer_instance: None, observer_basis: ReadingObserverBasis::QueryView, + contract: None, + query_identity: None, witness_refs: vec![ReadingWitnessRef::EmptyFrontier { worldline_id, state_root, diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 2b5e1d27..abb0537a 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -32,6 +32,7 @@ serde = { version = "1.0", features = ["derive"] } serde-value = "0.7" proptest = { version = "1.5" } echo-dry-tests = { workspace = true } +echo-cas = { workspace = true } [features] default = [] diff --git a/crates/warp-core/src/contract_registry.rs b/crates/warp-core/src/contract_registry.rs index d9d330f3..ee22d2a9 100644 --- a/crates/warp-core/src/contract_registry.rs +++ b/crates/warp-core/src/contract_registry.rs @@ -46,6 +46,53 @@ impl InstalledContractPackageId { } } +/// Installed contract operation kind carried by evidence surfaces. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ContractOperationKind { + /// Generated mutation operation. + Mutation, + /// Generated query operation. + Query, +} + +impl From for OpKind { + fn from(kind: ContractOperationKind) -> Self { + match kind { + ContractOperationKind::Mutation => OpKind::Mutation, + ContractOperationKind::Query => OpKind::Query, + } + } +} + +/// Contract package identity attached to receipts and readings. +/// +/// This is evidence metadata. It does not authorize execution, does not grant +/// query rights, and does not make CAS hashes semantic. It names the installed +/// package boundary that supplied the mutation handler or query observer. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ContractEvidenceIdentity { + /// Deterministic installed package id. + pub package_id: InstalledContractPackageId, + /// Runtime package name chosen by the host. + pub package_name: String, + /// Runtime package version chosen by the host. + pub package_version: String, + /// Hex-encoded generated package artifact hash. + pub artifact_hash_hex: String, + /// Registry codec identity verified at install time. + pub codec_id: String, + /// Registry version verified at install time. + pub registry_version: u32, + /// Hex-encoded authored schema hash verified at install time. + pub schema_sha256_hex: String, + /// Generated operation/query id handled by this package. + pub op_id: u32, + /// Generated operation kind. + pub op_kind: ContractOperationKind, +} + /// Host-owned package identity supplied when installing generated contract code. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ContractPackageIdentity<'a> { @@ -100,6 +147,28 @@ pub struct InstalledContractPackageRecord { pub query_op_ids: Vec, } +impl InstalledContractPackageRecord { + /// Builds receipt/reading evidence for one operation installed by this package. + #[must_use] + pub fn evidence_identity( + &self, + op_id: u32, + op_kind: ContractOperationKind, + ) -> ContractEvidenceIdentity { + ContractEvidenceIdentity { + package_id: self.package_id, + package_name: self.package_name.clone(), + package_version: self.package_version.clone(), + artifact_hash_hex: self.artifact_hash_hex.clone(), + codec_id: self.registry_info.codec_id.to_owned(), + registry_version: self.registry_info.registry_version, + schema_sha256_hex: self.registry_info.schema_sha256_hex.to_owned(), + op_id, + op_kind, + } + } +} + /// Error returned when installing a generated contract package fails. #[derive(Debug, Error)] pub enum InstalledContractPackageError<'a> { diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 8c795adf..a6444d5b 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -432,6 +432,8 @@ pub struct TicketedRuntimeIngressRecord { pub ingress_id: Hash, /// Resolved semantic writer-head target. pub head_key: WriterHeadKey, + /// Installed contract package evidence for generated contract work. + pub contract: Option, } /// Result of staging a ticketed invocation into runtime ingress. @@ -467,6 +469,8 @@ pub struct ReceiptCorrelationRecord { pub ingress_id: Hash, /// Writer head that committed the ingress batch. pub head_key: WriterHeadKey, + /// Installed contract package evidence for generated contract work. + pub contract: Option, /// Runtime cycle stamp that produced the receipt. pub commit_global_tick: GlobalTick, /// Worldline frontier tick after the scheduler-owned commit. @@ -1462,6 +1466,16 @@ impl WorldlineRuntime { submission_id: Hash, ticket: &OpticAdmissionTicket, envelope: IngressEnvelope, + ) -> Result { + self.ingest_ticketed_invocation_inner(submission_id, ticket, envelope, None) + } + + fn ingest_ticketed_invocation_inner( + &mut self, + submission_id: Hash, + ticket: &OpticAdmissionTicket, + envelope: IngressEnvelope, + contract: Option, ) -> Result { let Some(submission) = self.witnessed_submissions.get(&submission_id) else { return Err(RuntimeError::UnknownIntentSubmission(submission_id)); @@ -1507,6 +1521,7 @@ impl WorldlineRuntime { ticket_digest: ticket.ticket_digest, ingress_id, head_key, + contract, }; self.ticketed_runtime_ingress .insert(ticketed_ingress_id, record.clone()); @@ -1533,21 +1548,18 @@ impl WorldlineRuntime { #[cfg(feature = "native_rule_bootstrap")] pub fn ingest_installed_contract_invocation( &mut self, - authority: &TicketedRuntimeIngressAuthority, + _authority: &TicketedRuntimeIngressAuthority, engine: &Engine, submission_id: Hash, ticket: &OpticAdmissionTicket, envelope: IngressEnvelope, ) -> Result { let op_id = installed_contract_mutation_op_id(&envelope)?; - if engine - .installed_contract_mutation_package_id(op_id) - .is_none() - { - return Err(RuntimeError::UnsupportedInstalledContractMutation { op_id }); - } + let contract = engine + .installed_contract_mutation_evidence(op_id) + .ok_or(RuntimeError::UnsupportedInstalledContractMutation { op_id })?; - self.ingest_ticketed_invocation(authority, submission_id, ticket, envelope) + self.ingest_ticketed_invocation_inner(submission_id, ticket, envelope, Some(contract)) } /// Resolves an ingress envelope to a specific writer head and stores it in that inbox. @@ -1767,6 +1779,7 @@ impl WorldlineRuntime { ticket_digest: ticketed_ingress.ticket_digest, ingress_id, head_key: context.head_key, + contract: ticketed_ingress.contract.clone(), commit_global_tick: context.commit_global_tick, worldline_tick_after: context.worldline_tick_after, tick_receipt_digest: context.tick_receipt_digest, @@ -4587,6 +4600,7 @@ mod tests { ticket_digest, ingress_id, head_key: head_a, + contract: None, }; runtime .ticketed_runtime_ingress diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 8717c998..8ad5a20c 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -1193,6 +1193,16 @@ impl Engine { self.contract_query_observers.get(&query_id) } + pub(crate) fn contract_query_observer_package_evidence( + &self, + query_id: u32, + ) -> Option { + let package_id = self.contract_query_observer_packages.get(&query_id)?; + self.installed_contract_packages + .get(package_id) + .map(|record| record.evidence_identity(query_id, crate::ContractOperationKind::Query)) + } + /// Installs a generated contract package through the package registry /// boundary. /// @@ -1319,6 +1329,19 @@ impl Engine { self.contract_mutation_handlers.get(&op_id) } + /// Returns contract evidence for the installed package that owns a mutation op id. + #[cfg(feature = "native_rule_bootstrap")] + #[must_use] + pub fn installed_contract_mutation_evidence( + &self, + op_id: u32, + ) -> Option { + let package_id = self.contract_mutation_handlers.get(&op_id)?; + self.installed_contract_packages + .get(package_id) + .map(|record| record.evidence_identity(op_id, crate::ContractOperationKind::Mutation)) + } + /// Returns the package id that installed a query operation id. #[cfg(feature = "native_rule_bootstrap")] #[must_use] diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 04f6069e..c4891983 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -191,8 +191,9 @@ pub use contract_host::{ eint_op_id, eint_vars_for_op, matches_eint_op, runtime_ingress_eint_read_footprint, }; pub use contract_registry::{ - ContractMutationHandler, ContractPackageIdentity, InstalledContractPackage, - InstalledContractPackageError, InstalledContractPackageId, InstalledContractPackageRecord, + ContractEvidenceIdentity, ContractMutationHandler, ContractOperationKind, + ContractPackageIdentity, InstalledContractPackage, InstalledContractPackageError, + InstalledContractPackageId, InstalledContractPackageRecord, }; pub use dynamic_binding::{ BoundNodeRef, ClosureMemberBinding, DirectSlotBinding, DynamicBindingError, @@ -254,9 +255,9 @@ pub use observation::{ ObservationCoordinate, ObservationError, ObservationFrame, ObservationPayload, ObservationProjection, ObservationProjectionKind, ObservationReadBudget, ObservationRequest, ObservationRights, ObservationService, ObserverInstanceId, ObserverInstanceRef, ObserverPlanId, - ReadingBudgetPosture, ReadingEnvelope, ReadingObserverBasis, ReadingObserverPlan, - ReadingResidualPosture, ReadingRightsPosture, ReadingWitnessRef, ResolvedObservationCoordinate, - WorldlineSnapshot, + QueryReadingIdentity, ReadingBudgetPosture, ReadingEnvelope, ReadingObserverBasis, + ReadingObserverPlan, ReadingResidualPosture, ReadingRightsPosture, ReadingWitnessRef, + ResolvedObservationCoordinate, WorldlineSnapshot, }; pub use optic::{ AdmissionLawId, AdmittedIntent, AttachmentDescentPolicy, BraidId, CapabilityPosture, diff --git a/crates/warp-core/src/observation.rs b/crates/warp-core/src/observation.rs index e10374b3..3a87e493 100644 --- a/crates/warp-core/src/observation.rs +++ b/crates/warp-core/src/observation.rs @@ -21,6 +21,7 @@ use thiserror::Error; use crate::attachment::{AttachmentOwner, AttachmentPlane}; use crate::clock::{GlobalTick, WorldlineTick}; +use crate::contract_registry::{ContractEvidenceIdentity, ContractOperationKind}; use crate::coordinator::WorldlineRuntime; use crate::engine_impl::Engine; use crate::ident::Hash; @@ -36,8 +37,11 @@ use crate::strand::{StrandId, StrandRevalidationState}; use crate::tick_patch::SlotId; use crate::worldline::WorldlineId; -const OBSERVATION_VERSION: u32 = 2; -const OBSERVATION_ARTIFACT_DOMAIN: &[u8] = b"echo:observation-artifact:v2\0"; +const OBSERVATION_VERSION: u32 = 3; +const OBSERVATION_ARTIFACT_DOMAIN: &[u8] = b"echo:observation-artifact:v3\0"; +const QUERY_READING_IDENTITY_DOMAIN: &[u8] = b"echo:query-reading-identity:v1\0"; +const QUERY_READING_BASIS_DOMAIN: &[u8] = b"echo:query-reading-basis:v1\0"; +const QUERY_READING_APERTURE_DOMAIN: &[u8] = b"echo:query-reading-aperture:v1\0"; const OPTIC_OBSERVATION_WITNESS_SET_DOMAIN: &[u8] = b"echo:optic-observation-witness-set:v1\0"; const OPTIC_LIVE_TAIL_WITNESS_SET_DOMAIN: &[u8] = b"echo:optic-live-tail-witness-set:v1\0"; const OPTIC_METADATA_APERTURE_MIN_BYTES: u64 = 128; @@ -841,6 +845,38 @@ impl ReadingResidualPosture { } } +/// Stable identity of the QueryView question answered by a reading. +/// +/// The identity names the question and requested bound, not the returned bytes. +/// Payload bytes still live in [`ObservationPayload::QueryBytes`], and retained +/// payloads still need a separate semantic retention descriptor. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct QueryReadingIdentity { + /// Stable digest over query id, vars, basis, observer plan, budget, rights, + /// and installed contract evidence when present. + pub reading_id: Hash, + /// Generated query operation id. + pub query_id: u32, + /// Domain-separated digest of canonical query vars bytes. + pub vars_digest: Hash, + /// Digest of the resolved causal basis and basis posture. + pub basis_digest: Hash, + /// Digest of the requested local read aperture: budget and rights posture. + pub aperture_digest: Hash, +} + +impl QueryReadingIdentity { + fn to_abi(&self) -> abi::QueryReadingIdentity { + abi::QueryReadingIdentity { + reading_id: self.reading_id.to_vec(), + query_id: self.query_id, + vars_digest: self.vars_digest.to_vec(), + basis_digest: self.basis_digest.to_vec(), + aperture_digest: self.aperture_digest.to_vec(), + } + } +} + /// Reading-envelope metadata carried by every observation artifact. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ReadingEnvelope { @@ -850,6 +886,11 @@ pub struct ReadingEnvelope { pub observer_instance: Option, /// Native observer basis used by the reading. pub observer_basis: ReadingObserverBasis, + /// Installed contract package identity, when this reading came from a + /// generated contract observer. + pub contract: Option, + /// Stable identity of the QueryView question answered by this reading. + pub query_identity: Option, /// Witnesses or shell references that support the reading. pub witness_refs: Vec, /// Read-side parent/strand basis posture. @@ -871,6 +912,11 @@ impl ReadingEnvelope { .as_ref() .map(ObserverInstanceRef::to_abi), observer_basis: self.observer_basis.to_abi(), + contract: self.contract.as_ref().map(contract_evidence_to_abi), + query_identity: self + .query_identity + .as_ref() + .map(QueryReadingIdentity::to_abi), witness_refs: self .witness_refs .iter() @@ -884,6 +930,35 @@ impl ReadingEnvelope { } } +fn contract_evidence_to_abi(evidence: &ContractEvidenceIdentity) -> abi::ContractEvidenceIdentity { + abi::ContractEvidenceIdentity { + package_id: evidence.package_id.as_bytes().to_vec(), + package_name: evidence.package_name.clone(), + package_version: evidence.package_version.clone(), + artifact_hash_hex: evidence.artifact_hash_hex.clone(), + codec_id: evidence.codec_id.clone(), + registry_version: evidence.registry_version, + schema_sha256_hex: evidence.schema_sha256_hex.clone(), + op_id: evidence.op_id, + op_kind: match evidence.op_kind { + ContractOperationKind::Mutation => abi::ContractOperationKind::Mutation, + ContractOperationKind::Query => abi::ContractOperationKind::Query, + }, + } +} + +fn query_vars_digest(vars_bytes: &[u8]) -> Result { + let digest = echo_wasm_abi::query_vars_digest_v1(vars_bytes); + digest.as_slice().try_into().map_err(|_| { + ObservationError::CodecFailure("query vars digest length was not 32 bytes".to_owned()) + }) +} + +fn push_len_prefixed(hasher: &mut Hasher, bytes: &[u8]) { + hasher.update(&(bytes.len() as u64).to_le_bytes()); + hasher.update(bytes); +} + /// Minimal frontier/head observation payload. #[derive(Clone, Debug, PartialEq, Eq)] pub struct HeadObservation { @@ -1106,91 +1181,97 @@ impl ObservationService { let resolved = Self::resolve_coordinate(runtime, provenance, engine, &request)?; let parent_basis_posture = Self::basis_posture(runtime, provenance, worldline_id, request.coordinate.at)?; - let (payload, observer_plan, residual_posture) = match (&request.frame, &request.projection) - { - (ObservationFrame::CommitBoundary, ObservationProjection::Head) => ( - ObservationPayload::Head(HeadObservation { - worldline_tick: resolved.resolved_worldline_tick, - commit_global_tick: resolved.commit_global_tick, - state_root: resolved.state_root, - commit_hash: resolved.commit_hash, - }), - request.observer_plan.clone(), - ReadingResidualPosture::Complete, - ), - (ObservationFrame::CommitBoundary, ObservationProjection::Snapshot) => ( - ObservationPayload::Snapshot(WorldlineSnapshot { - worldline_tick: resolved.resolved_worldline_tick, - commit_global_tick: resolved.commit_global_tick, - state_root: resolved.state_root, - commit_hash: resolved.commit_hash, - }), - request.observer_plan.clone(), - ReadingResidualPosture::Complete, - ), - ( - ObservationFrame::RecordedTruth, - ObservationProjection::TruthChannels { channels }, - ) => { - let entry = provenance - .entry(worldline_id, resolved.resolved_worldline_tick) - .map_err(|_| ObservationError::ObservationUnavailable { - worldline_id, - at: request.coordinate.at, - })?; - let outputs = match channels { - Some(filter) => entry - .outputs - .into_iter() - .filter(|(channel, _)| filter.contains(channel)) - .collect(), - None => entry.outputs, - }; - ( - ObservationPayload::TruthChannels(outputs), + let (payload, observer_plan, contract, residual_posture) = + match (&request.frame, &request.projection) { + (ObservationFrame::CommitBoundary, ObservationProjection::Head) => ( + ObservationPayload::Head(HeadObservation { + worldline_tick: resolved.resolved_worldline_tick, + commit_global_tick: resolved.commit_global_tick, + state_root: resolved.state_root, + commit_hash: resolved.commit_hash, + }), request.observer_plan.clone(), + None, ReadingResidualPosture::Complete, - ) - } - ( - ObservationFrame::QueryView, - ObservationProjection::Query { - query_id, - vars_bytes, - }, - ) => { - let observer = engine.contract_query_observer(*query_id).ok_or( - ObservationError::UnsupportedQuery { - query_id: *query_id, + ), + (ObservationFrame::CommitBoundary, ObservationProjection::Snapshot) => ( + ObservationPayload::Snapshot(WorldlineSnapshot { + worldline_tick: resolved.resolved_worldline_tick, + commit_global_tick: resolved.commit_global_tick, + state_root: resolved.state_root, + commit_hash: resolved.commit_hash, + }), + request.observer_plan.clone(), + None, + ReadingResidualPosture::Complete, + ), + ( + ObservationFrame::RecordedTruth, + ObservationProjection::TruthChannels { channels }, + ) => { + let entry = provenance + .entry(worldline_id, resolved.resolved_worldline_tick) + .map_err(|_| ObservationError::ObservationUnavailable { + worldline_id, + at: request.coordinate.at, + })?; + let outputs = match channels { + Some(filter) => entry + .outputs + .into_iter() + .filter(|(channel, _)| filter.contains(channel)) + .collect(), + None => entry.outputs, + }; + ( + ObservationPayload::TruthChannels(outputs), + request.observer_plan.clone(), + None, + ReadingResidualPosture::Complete, + ) + } + ( + ObservationFrame::QueryView, + ObservationProjection::Query { + query_id, + vars_bytes, }, - )?; - let result = (observer.observe)(ContractQueryObserverContext { - query_id: *query_id, - vars_bytes, - request: &request, - resolved: &resolved, - runtime, - provenance, - }) - .map_err(|source| { - ObservationError::ContractQueryObserverFailed { + ) => { + let observer = engine.contract_query_observer(*query_id).ok_or( + ObservationError::UnsupportedQuery { + query_id: *query_id, + }, + )?; + let contract = engine.contract_query_observer_package_evidence(*query_id); + let result = (observer.observe)(ContractQueryObserverContext { query_id: *query_id, - source, - } - })?; - ( - ObservationPayload::QueryBytes(result.bytes), - observer.observer_plan(), - result.residual_posture, - ) - } - _ => unreachable!("validity matrix must reject unsupported combinations"), - }; + vars_bytes, + request: &request, + resolved: &resolved, + runtime, + provenance, + }) + .map_err(|source| { + ObservationError::ContractQueryObserverFailed { + query_id: *query_id, + source, + } + })?; + ( + ObservationPayload::QueryBytes(result.bytes), + observer.observer_plan(), + contract, + result.residual_posture, + ) + } + _ => unreachable!("validity matrix must reject unsupported combinations"), + }; let reading = Self::reading_envelope( &resolved, parent_basis_posture, &request, observer_plan, + contract, residual_posture, &payload, )?; @@ -1936,15 +2017,25 @@ impl ObservationService { parent_basis_posture: ObservationBasisPosture, request: &ObservationRequest, observer_plan: ReadingObserverPlan, + contract: Option, residual_posture: ReadingResidualPosture, payload: &ObservationPayload, ) -> Result { let witness_refs = Self::witness_refs(resolved, request.frame); let budget_posture = Self::budget_posture(request.budget, payload, witness_refs.len())?; + let query_identity = Self::query_reading_identity( + resolved, + parent_basis_posture.clone(), + request, + &observer_plan, + contract.as_ref(), + )?; Ok(ReadingEnvelope { observer_plan, observer_instance: request.observer_instance.clone(), observer_basis: Self::observer_basis(request.frame), + contract, + query_identity, witness_refs, parent_basis_posture, budget_posture, @@ -1953,6 +2044,90 @@ impl ObservationService { }) } + fn query_reading_identity( + resolved: &ResolvedObservationCoordinate, + parent_basis_posture: ObservationBasisPosture, + request: &ObservationRequest, + observer_plan: &ReadingObserverPlan, + contract: Option<&ContractEvidenceIdentity>, + ) -> Result, ObservationError> { + let ObservationProjection::Query { + query_id, + vars_bytes, + } = &request.projection + else { + return Ok(None); + }; + + let vars_digest = query_vars_digest(vars_bytes)?; + let basis_digest = Self::query_basis_digest(resolved, &parent_basis_posture)?; + let aperture_digest = Self::query_aperture_digest(request.budget, request.rights)?; + let observer_plan_bytes = echo_wasm_abi::encode_cbor(&observer_plan.to_abi()) + .map_err(|err| ObservationError::CodecFailure(err.to_string()))?; + let contract_bytes = contract + .map(contract_evidence_to_abi) + .map(|evidence| echo_wasm_abi::encode_cbor(&evidence)) + .transpose() + .map_err(|err| ObservationError::CodecFailure(err.to_string()))?; + + let mut hasher = Hasher::new(); + hasher.update(QUERY_READING_IDENTITY_DOMAIN); + hasher.update(&query_id.to_le_bytes()); + hasher.update(&vars_digest); + hasher.update(&basis_digest); + hasher.update(&aperture_digest); + push_len_prefixed(&mut hasher, &observer_plan_bytes); + match contract_bytes { + Some(bytes) => { + hasher.update(&[1]); + push_len_prefixed(&mut hasher, &bytes); + } + None => { + hasher.update(&[0]); + } + } + + Ok(Some(QueryReadingIdentity { + reading_id: hasher.finalize().into(), + query_id: *query_id, + vars_digest, + basis_digest, + aperture_digest, + })) + } + + fn query_basis_digest( + resolved: &ResolvedObservationCoordinate, + parent_basis_posture: &ObservationBasisPosture, + ) -> Result { + let mut resolved_basis = resolved.to_abi(); + resolved_basis.observed_after_global_tick = None; + let resolved_bytes = echo_wasm_abi::encode_cbor(&resolved_basis) + .map_err(|err| ObservationError::CodecFailure(err.to_string()))?; + let posture_bytes = echo_wasm_abi::encode_cbor(&parent_basis_posture.to_abi()) + .map_err(|err| ObservationError::CodecFailure(err.to_string()))?; + let mut hasher = Hasher::new(); + hasher.update(QUERY_READING_BASIS_DOMAIN); + push_len_prefixed(&mut hasher, &resolved_bytes); + push_len_prefixed(&mut hasher, &posture_bytes); + Ok(hasher.finalize().into()) + } + + fn query_aperture_digest( + budget: ObservationReadBudget, + rights: ObservationRights, + ) -> Result { + let budget_bytes = echo_wasm_abi::encode_cbor(&budget.to_abi()) + .map_err(|err| ObservationError::CodecFailure(err.to_string()))?; + let rights_bytes = echo_wasm_abi::encode_cbor(&rights.to_abi()) + .map_err(|err| ObservationError::CodecFailure(err.to_string()))?; + let mut hasher = Hasher::new(); + hasher.update(QUERY_READING_APERTURE_DOMAIN); + push_len_prefixed(&mut hasher, &budget_bytes); + push_len_prefixed(&mut hasher, &rights_bytes); + Ok(hasher.finalize().into()) + } + fn budget_posture( budget: ObservationReadBudget, payload: &ObservationPayload, @@ -2258,6 +2433,24 @@ mod tests { WorldlineState, }; + fn observation_artifact_hash_with_domain( + artifact: &ObservationArtifact, + domain: &[u8], + ) -> Hash { + let input = abi::ObservationHashInput { + resolved: artifact.resolved.to_abi(), + reading: artifact.reading.to_abi(), + frame: artifact.frame.to_abi(), + projection: artifact.projection.to_abi(), + payload: artifact.payload.to_abi(), + }; + let bytes = echo_wasm_abi::encode_cbor(&input).unwrap(); + let mut hasher = Hasher::new(); + hasher.update(domain); + hasher.update(&bytes); + hasher.finalize().into() + } + fn wl(n: u8) -> WorldlineId { WorldlineId::from_bytes([n; 32]) } @@ -3280,6 +3473,159 @@ mod tests { assert_ne!(baseline.artifact_hash, different_query.artifact_hash); assert_ne!(baseline.artifact_hash, different_basis.artifact_hash); assert_ne!(baseline.artifact_hash, different_schema.artifact_hash); + assert_eq!(baseline.resolved.observation_version, 3); + assert_eq!( + baseline.artifact_hash, + observation_artifact_hash_with_domain(&baseline, b"echo:observation-artifact:v3\0") + ); + assert_ne!( + baseline.artifact_hash, + observation_artifact_hash_with_domain(&baseline, b"echo:observation-artifact:v2\0") + ); + + let baseline_identity = baseline + .reading + .query_identity + .as_ref() + .expect("contract QueryView reading must carry query identity"); + assert_eq!(baseline_identity.query_id, 9_001); + assert_eq!( + baseline_identity.vars_digest.as_slice(), + echo_wasm_abi::query_vars_digest_v1(b"counter=read") + ); + assert_ne!( + baseline_identity.reading_id, + different_vars + .reading + .query_identity + .as_ref() + .expect("different vars reading must carry query identity") + .reading_id + ); + assert_ne!( + baseline_identity.reading_id, + different_query + .reading + .query_identity + .as_ref() + .expect("different query reading must carry query identity") + .reading_id + ); + assert_ne!( + baseline_identity.reading_id, + different_basis + .reading + .query_identity + .as_ref() + .expect("different basis reading must carry query identity") + .reading_id + ); + assert_ne!( + baseline_identity.reading_id, + different_schema + .reading + .query_identity + .as_ref() + .expect("different schema reading must carry query identity") + .reading_id + ); + + let mut bounded_request = query_request( + worldline_id, + ObservationAt::Frontier, + 9_001, + b"counter=read".to_vec(), + ); + bounded_request.budget = ObservationReadBudget::Bounded { + max_payload_bytes: 512, + max_witness_refs: 1, + }; + let bounded = + ObservationService::observe(&runtime, &provenance, &engine, bounded_request).unwrap(); + assert_ne!( + baseline_identity.reading_id, + bounded + .reading + .query_identity + .as_ref() + .expect("bounded reading must carry query identity") + .reading_id + ); + } + + #[test] + fn contract_query_identity_ignores_observation_freshness() { + let (mut engine, mut runtime, provenance, worldline_id) = one_commit_fixture(); + engine + .register_contract_query_observer(ContractQueryObserver::new( + 9_001, + authored_query_plan(90, 91), + complete_query_observer, + )) + .unwrap(); + + let baseline = ObservationService::observe( + &runtime, + &provenance, + &engine, + query_request( + worldline_id, + ObservationAt::Frontier, + 9_001, + b"counter=read".to_vec(), + ), + ) + .unwrap(); + runtime.advance_global_tick().unwrap(); + let after_unrelated_progress = ObservationService::observe( + &runtime, + &provenance, + &engine, + query_request( + worldline_id, + ObservationAt::Frontier, + 9_001, + b"counter=read".to_vec(), + ), + ) + .unwrap(); + + assert_eq!( + baseline.resolved.commit_hash, + after_unrelated_progress.resolved.commit_hash + ); + assert_ne!( + baseline.resolved.observed_after_global_tick, + after_unrelated_progress.resolved.observed_after_global_tick + ); + assert_eq!( + baseline + .reading + .query_identity + .as_ref() + .expect("baseline query identity") + .reading_id, + after_unrelated_progress + .reading + .query_identity + .as_ref() + .expect("later query identity") + .reading_id + ); + assert_eq!( + baseline + .reading + .query_identity + .as_ref() + .expect("baseline query identity") + .basis_digest, + after_unrelated_progress + .reading + .query_identity + .as_ref() + .expect("later query identity") + .basis_digest + ); } #[test] diff --git a/crates/warp-core/src/optic/tests.rs b/crates/warp-core/src/optic/tests.rs index f4e9fa2f..cb5e0a81 100644 --- a/crates/warp-core/src/optic/tests.rs +++ b/crates/warp-core/src/optic/tests.rs @@ -120,6 +120,8 @@ fn reading_envelope() -> ReadingEnvelope { }, observer_instance: None, observer_basis: ReadingObserverBasis::CommitBoundary, + contract: None, + query_identity: None, witness_refs: vec![ReadingWitnessRef::ResolvedCommit { reference: provenance(1, 2), }], diff --git a/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs b/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs index a43ae0dd..5d5e1c12 100644 --- a/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs +++ b/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs @@ -4,24 +4,30 @@ #![cfg(all(feature = "native_rule_bootstrap", feature = "host_test"))] #![allow(clippy::expect_used, clippy::panic)] +use echo_cas::{MemoryTier, RetainedBlobIndex, RetainedBlobRole, SemanticBlobCoordinate}; use echo_registry_api::{ ArgDef, ContractArtifactVerificationPolicy, ObjectDef, OpDef, OpKind, RegistryInfo, RegistryProvider, }; use warp_core::{ - make_head_id, make_intent_kind, make_node_id, make_type_id, ContractMutationHandler, - ContractPackageIdentity, Engine, EngineBuilder, GraphStore, GraphView, InboxPolicy, + make_head_id, make_intent_kind, make_node_id, make_type_id, AuthoredObserverPlan, + ContractMutationHandler, ContractPackageIdentity, ContractQueryObserver, + ContractQueryObserverResult, Engine, EngineBuilder, GraphStore, GraphView, InboxPolicy, IngressEnvelope, IngressSubmissionGeneration, IngressTarget, IntentOutcomeDecision, - IntentOutcomeObservation, IntentSubmissionDisposition, NodeId, NodeRecord, + IntentOutcomeObservation, IntentSubmissionDisposition, NodeId, NodeRecord, ObservationAt, + ObservationCoordinate, ObservationFrame, ObservationPayload, ObservationProjection, + ObservationReadBudget, ObservationRequest, ObservationService, ObserverPlanId, OpticAdmissionTicket, OpticArtifactHandle, PatternGraph, PlaybackMode, ProvenanceService, - RuntimeError, SchedulerCoordinator, SchedulerKind, TickDelta, TickReceiptRejection, - TicketedRuntimeIngressAuthority, WorldlineId, WorldlineRuntime, WorldlineState, WriterHead, - WriterHeadKey, OPTIC_ADMISSION_TICKET_KIND, OPTIC_ARTIFACT_HANDLE_KIND, + ReadingBudgetPosture, ReceiptCorrelationRecord, RuntimeError, SchedulerCoordinator, + SchedulerKind, TickDelta, TickReceiptRejection, TicketedRuntimeIngressAuthority, WorldlineId, + WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, OPTIC_ADMISSION_TICKET_KIND, + OPTIC_ARTIFACT_HANDLE_KIND, }; const SCHEMA_SHA256_HEX: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const MUTATION_OP_ID: u32 = 1001; const CONFLICT_OP_ID: u32 = 1002; +const QUERY_OP_ID: u32 = 1003; const UNKNOWN_OP_ID: u32 = 9999; const MUTATION_VARS: &[u8] = b"amount=42"; const CONFLICT_VARS_A: &[u8] = b"amount=1"; @@ -64,6 +70,15 @@ static OPS: &[OpDef] = &[ directives_json: "{}", footprint_certificate: None, }, + OpDef { + kind: OpKind::Query, + name: "counterWindow", + op_id: QUERY_OP_ID, + args: INCREMENT_ARGS, + result_ty: "CounterWindow", + directives_json: "{}", + footprint_certificate: None, + }, ]; struct StaticRegistry; @@ -239,6 +254,26 @@ fn conflict_rule() -> warp_core::RewriteRule { } } +fn query_observer() -> ContractQueryObserver { + ContractQueryObserver::new(QUERY_OP_ID, query_observer_plan(), |context| { + let mut bytes = b"window:".to_vec(); + bytes.extend_from_slice(context.vars_bytes); + bytes.extend_from_slice(b":value=42"); + Ok(ContractQueryObserverResult::complete(bytes)) + }) +} + +fn query_observer_plan() -> AuthoredObserverPlan { + AuthoredObserverPlan { + plan_id: ObserverPlanId::from_bytes([0x51; 32]), + artifact_hash: [0x52; 32], + schema_hash: [0x53; 32], + state_schema_hash: [0x54; 32], + update_law_hash: [0x55; 32], + emission_law_hash: [0x56; 32], + } +} + fn install_contract(engine: &mut Engine) { static REGISTRY: StaticRegistry = StaticRegistry; engine @@ -256,7 +291,7 @@ fn install_contract(engine: &mut Engine) { rule: conflict_rule(), }, ], - query_observers: vec![], + query_observers: vec![query_observer()], }) .expect("contract package should install"); } @@ -343,6 +378,78 @@ fn eint_envelope(worldline_id: WorldlineId, op_id: u32, vars: &[u8]) -> IngressE ) } +fn query_request(worldline_id: WorldlineId, vars: &[u8]) -> ObservationRequest { + let mut request = ObservationRequest::builtin_one_shot( + ObservationCoordinate { + worldline_id, + at: ObservationAt::Frontier, + }, + ObservationFrame::QueryView, + ObservationProjection::Query { + query_id: QUERY_OP_ID, + vars_bytes: vars.to_vec(), + }, + ) + .expect("query request should build"); + request.budget = ObservationReadBudget::Bounded { + max_payload_bytes: 128, + max_witness_refs: 1, + }; + request +} + +fn semantic_coordinate( + contract: &warp_core::ContractEvidenceIdentity, + role: RetainedBlobRole, + semantic_digest: [u8; 32], +) -> SemanticBlobCoordinate { + SemanticBlobCoordinate { + namespace: contract.package_name.clone(), + schema_hash_hex: contract.schema_sha256_hex.clone(), + artifact_hash_hex: contract.artifact_hash_hex.clone(), + role, + semantic_digest, + } +} + +fn push_len_prefixed_bytes(out: &mut Vec, bytes: &[u8]) { + out.extend_from_slice(&(bytes.len() as u64).to_le_bytes()); + out.extend_from_slice(bytes); +} + +fn receipt_correlation_material(correlation: &ReceiptCorrelationRecord) -> Vec { + let mut bytes = b"echo:test:contract-receipt-correlation:v1\0".to_vec(); + bytes.extend_from_slice(&correlation.ticketed_ingress_id); + bytes.extend_from_slice(&correlation.submission_id); + bytes.extend_from_slice(&correlation.ticket_digest); + bytes.extend_from_slice(&correlation.ingress_id); + bytes.extend_from_slice(correlation.head_key.worldline_id.as_bytes()); + bytes.extend_from_slice(correlation.head_key.head_id.as_bytes()); + bytes.extend_from_slice(&correlation.commit_global_tick.as_u64().to_le_bytes()); + bytes.extend_from_slice(&correlation.worldline_tick_after.as_u64().to_le_bytes()); + bytes.extend_from_slice(&correlation.tick_receipt_digest); + bytes.extend_from_slice(&correlation.commit_hash); + match &correlation.contract { + Some(contract) => { + bytes.push(1); + bytes.extend_from_slice(contract.package_id.as_bytes()); + push_len_prefixed_bytes(&mut bytes, contract.package_name.as_bytes()); + push_len_prefixed_bytes(&mut bytes, contract.package_version.as_bytes()); + push_len_prefixed_bytes(&mut bytes, contract.artifact_hash_hex.as_bytes()); + push_len_prefixed_bytes(&mut bytes, contract.codec_id.as_bytes()); + bytes.extend_from_slice(&contract.registry_version.to_le_bytes()); + push_len_prefixed_bytes(&mut bytes, contract.schema_sha256_hex.as_bytes()); + bytes.extend_from_slice(&contract.op_id.to_le_bytes()); + bytes.push(match contract.op_kind { + warp_core::ContractOperationKind::Mutation => 1, + warp_core::ContractOperationKind::Query => 2, + }); + } + None => bytes.push(0), + } + bytes +} + #[test] fn installed_contract_mutation_dispatches_only_through_ticketed_scheduler_tick() { let (mut runtime, mut engine, worldline_id, head) = pipeline_runtime(); @@ -406,6 +513,17 @@ fn installed_contract_mutation_dispatches_only_through_ticketed_scheduler_tick() assert!(runtime .receipt_correlation_for_submission(&submission) .is_some()); + let correlation = runtime + .receipt_correlation_for_submission(&submission) + .expect("receipt correlation should exist"); + let contract = correlation + .contract + .as_ref() + .expect("installed mutation receipt correlation must carry contract evidence"); + assert_eq!(contract.op_id, MUTATION_OP_ID); + assert_eq!(contract.op_kind, warp_core::ContractOperationKind::Mutation); + assert_eq!(contract.schema_sha256_hex, SCHEMA_SHA256_HEX); + assert_eq!(contract.package_name, "toy-counter"); assert_eq!( runtime.observe_intent_outcome(&submission), @@ -769,6 +887,171 @@ fn witnessed_submission_replay_preserves_generation_continuity() { )); } +#[test] +fn external_contract_fixture_proves_mutation_query_retention_and_replay() { + let (mut runtime, mut engine, worldline_id, _head) = pipeline_runtime(); + let envelope = eint_envelope(worldline_id, MUTATION_OP_ID, MUTATION_VARS); + let ticket = admission_ticket(31); + let submission = match runtime + .submit_intent(envelope.clone()) + .expect("external fixture submission should be witnessed") + { + IntentSubmissionDisposition::Accepted { submission_id, .. } => submission_id, + IntentSubmissionDisposition::Duplicate { .. } => { + panic!("first external fixture submission must not be duplicate") + } + }; + let replay_records = runtime.witnessed_submission_replay_records(); + + runtime + .ingest_installed_contract_invocation( + &ticketed_authority(), + &engine, + submission, + &ticket, + envelope.clone(), + ) + .expect("external fixture ticketed ingress should stage"); + let mut provenance = provenance_for(&runtime); + SchedulerCoordinator::super_tick(&mut runtime, &mut provenance, &mut engine) + .expect("external fixture scheduler-owned tick should commit"); + let outcome = runtime.observe_intent_outcome(&submission); + assert!(matches!( + outcome, + IntentOutcomeObservation::Decided { + decision: IntentOutcomeDecision::Applied { .. }, + .. + } + )); + + let query_vars = b"start=0;len=8"; + let reading = ObservationService::observe( + &runtime, + &provenance, + &engine, + query_request(worldline_id, query_vars), + ) + .expect("external fixture QueryView reading should observe"); + let query_payload = match &reading.payload { + ObservationPayload::QueryBytes(bytes) => bytes.clone(), + other => panic!("external fixture expected QueryBytes, got {other:?}"), + }; + assert_eq!(query_payload, b"window:start=0;len=8:value=42"); + match reading.reading.budget_posture { + ReadingBudgetPosture::Bounded { + max_payload_bytes, + payload_bytes, + max_witness_refs, + witness_refs, + } => { + assert_eq!(max_payload_bytes, 128); + assert!(payload_bytes >= query_payload.len() as u64); + assert!(payload_bytes <= max_payload_bytes); + assert_eq!(max_witness_refs, 1); + assert_eq!(witness_refs, 1); + } + other @ ReadingBudgetPosture::UnboundedOneShot => { + panic!("external fixture expected bounded reading posture, got {other:?}"); + } + } + + let mut blobs = MemoryTier::new(); + let mut retained = RetainedBlobIndex::default(); + let query_identity = reading + .reading + .query_identity + .as_ref() + .expect("external fixture reading must carry query identity"); + let query_contract = reading + .reading + .contract + .as_ref() + .expect("external fixture reading must carry contract evidence"); + let reading_coord = semantic_coordinate( + query_contract, + RetainedBlobRole::ReadingPayload, + query_identity.reading_id, + ); + let reading_descriptor = retained + .retain(&mut blobs, reading_coord.clone(), &query_payload) + .expect("retained reading payload should index"); + assert_eq!( + retained + .load_range(&blobs, &reading_coord, 7, 13, 13) + .expect("retained bounded reading range should load") + .bytes + .as_ref(), + b"start=0;len=8" + ); + + let correlation = runtime + .receipt_correlation_for_submission(&submission) + .expect("external fixture receipt correlation should exist"); + let receipt_contract = correlation + .contract + .as_ref() + .expect("external fixture receipt must carry contract evidence"); + let receipt_coord = semantic_coordinate( + receipt_contract, + RetainedBlobRole::ContractReceipt, + correlation.tick_receipt_digest, + ); + let receipt_material = receipt_correlation_material(correlation); + let receipt_descriptor = retained + .retain(&mut blobs, receipt_coord.clone(), &receipt_material) + .expect("retained receipt correlation material should index"); + + assert_eq!( + retained + .load(&blobs, &reading_coord) + .expect("retained reading payload should load") + .descriptor, + reading_descriptor + ); + assert_eq!( + retained + .load(&blobs, &receipt_coord) + .expect("retained receipt correlation material should load") + .descriptor, + receipt_descriptor + ); + assert_eq!( + retained + .load(&blobs, &receipt_coord) + .expect("retained receipt correlation material should load") + .bytes + .as_ref(), + receipt_material.as_slice() + ); + + let (mut replayed_runtime, mut replayed_engine, _worldline_id, _head) = pipeline_runtime(); + replayed_runtime + .replay_witnessed_submissions(replay_records) + .expect("external fixture replay should import witnessed submissions"); + replayed_runtime + .ingest_installed_contract_invocation( + &ticketed_authority(), + &replayed_engine, + submission, + &ticket, + envelope, + ) + .expect("external fixture replay ticketed ingress should stage"); + let mut replayed_provenance = provenance_for(&replayed_runtime); + SchedulerCoordinator::super_tick( + &mut replayed_runtime, + &mut replayed_provenance, + &mut replayed_engine, + ) + .expect("external fixture replay tick should commit"); + + assert_eq!( + replayed_runtime.observe_intent_outcome(&submission), + outcome, + "external fixture replay must reproduce the same observed outcome" + ); +} + #[test] fn installed_contract_pipeline_replays_to_same_receipt_and_outcome() { let (mut original_runtime, mut original_engine, worldline_id, _head) = pipeline_runtime(); diff --git a/crates/warp-core/tests/installed_contract_registry_tests.rs b/crates/warp-core/tests/installed_contract_registry_tests.rs index 85d0e229..51ccc227 100644 --- a/crates/warp-core/tests/installed_contract_registry_tests.rs +++ b/crates/warp-core/tests/installed_contract_registry_tests.rs @@ -296,6 +296,23 @@ fn installed_contract_package_binds_supported_mutation_and_query() -> Result<(), observed.payload, ObservationPayload::QueryBytes(b"value=42".to_vec()) ); + let contract = observed + .reading + .contract + .as_ref() + .ok_or("installed query reading must carry contract evidence")?; + assert_eq!(contract.package_id, record.package_id); + assert_eq!(contract.package_name, "toy-counter"); + assert_eq!(contract.package_version, "0.1.0"); + assert_eq!( + contract.artifact_hash_hex, + package_identity().artifact_hash_hex + ); + assert_eq!(contract.schema_sha256_hex, SCHEMA_SHA256_HEX); + assert_eq!(contract.codec_id, "cbor-canon-v1"); + assert_eq!(contract.registry_version, 1); + assert_eq!(contract.op_id, QUERY_OP_ID); + assert_eq!(contract.op_kind, warp_core::ContractOperationKind::Query); Ok(()) } diff --git a/crates/warp-wasm/README.md b/crates/warp-wasm/README.md index 4681773a..cde08d39 100644 --- a/crates/warp-wasm/README.md +++ b/crates/warp-wasm/README.md @@ -13,7 +13,7 @@ See the repository root `README.md` for the full overview. Echo’s deterministic wire protocol can be used from JavaScript/TypeScript in web-based tools and playgrounds. - Exposes the current observation-first and intent-shaped control surface - (`ABI_VERSION` 10 in `echo-wasm-abi`): `observe(...)` is the only public + (`ABI_VERSION` 11 in `echo-wasm-abi`): `observe(...)` is the only public world-state read export, `scheduler_status()` is the read-only scheduler metadata export, and `dispatch_intent(...)` is application intent ingress. Accepted application ingress returns witnessed submission identity in addition diff --git a/crates/warp-wasm/src/lib.rs b/crates/warp-wasm/src/lib.rs index 6b26fa1a..491a2eb4 100644 --- a/crates/warp-wasm/src/lib.rs +++ b/crates/warp-wasm/src/lib.rs @@ -812,6 +812,8 @@ mod init_tests { }, observer_instance: None, observer_basis: ReadingObserverBasis::CommitBoundary, + contract: None, + query_identity: None, witness_refs: vec![ReadingWitnessRef::EmptyFrontier { worldline_id: WorldlineId::from_bytes([9; 32]), state_root: head.state_root.clone(), diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs index 69c3019b..1cf03630 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -795,6 +795,8 @@ impl WarpKernel { observer_plan: request.observer_plan.clone(), observer_instance: request.observer_instance.clone(), observer_basis: AbiReadingObserverBasis::QueryView, + contract: None, + query_identity: None, witness_refs: Vec::new(), parent_basis_posture: echo_wasm_abi::kernel_port::ObservationBasisPosture::Worldline, diff --git a/docs/BEARING.md b/docs/BEARING.md index 048b6c23..4273a1a1 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -80,14 +80,13 @@ without moving application nouns into Echo core. current replay records prove deterministic import shape, not persistence. - Product-facing clients do not yet have polished ABI/helper surfaces for per-intent applied/rejected semantics. -- Contract-aware receipt and reading identities are not yet strong enough for - serious external consumers: package identity, schema identity, operation - identity, basis, vars, aperture, and residual posture need explicit evidence - where the current generic surfaces are too broad. -- Contract artifacts, witness material, and bounded reading payloads are not yet - retained through a generic semantic lookup layer above `echo-cas`. -- External consumer proof fixtures, especially `jedit`, have not yet proven the - generic host path with application-owned contracts and generated artifacts. +- Contract-aware obstruction taxonomy and product-facing error surfaces still + need release-grade stabilization. +- The semantic retention layer is local and in-memory; durable retained + artifact, witness, receipt, and reading recovery remains future work. +- Generic external contract proof exists, but a serious application-owned + consumer proof, especially `jedit`, still needs to prove the host path with + generated artifacts outside Echo core. ## Doctrine @@ -207,58 +206,63 @@ AdmissionTicket + witnessed submission -> ticketed runtime ingress paper-level privacy/runtime policy concepts. The local contract-host pipeline does not yet implement that full social lane model. -## Next Five Slices +## Recently Completed Slice Batch 1. **Contract-Aware Receipts And Readings** - Make scheduler receipt correlation and QueryView readings honest about the - installed contract package they came from. The first slice should inventory - existing identity inputs and add only the missing generic ones: package - identity, schema hash, artifact hash, operation/query id, basis, vars digest, - aperture or residual posture, and witness refs where needed. - - Do not add consumer-specific receipt fields or duplicate identities already - covered by `intent_id`, `ReceiptCorrelationRecord`, or `ReadingEnvelope`. + Installed QueryView readings and installed mutation receipt correlations + now carry contract package evidence: package id, schema hash, artifact hash, + codec identity, operation/query id, and operation kind. 2. **Contract Reading Identity And Bounded Payloads** - Harden generated-query readings so the identity names the question actually - answered, and bounded observers can return only the requested aperture or a - typed residual/budget posture. This is the read-side counterpart to the - installed intent pipeline. - - Do not canonicalize text-editor state in Echo core and do not let CAS content - hashes stand in for semantic reading identity. + QueryView readings now carry `QueryReadingIdentity`, binding query id, vars + digest, resolved basis digest, requested aperture digest, observer plan, and + installed contract evidence when present. 3. **Contract Artifact Retention In `echo-cas`** - Add minimal generic retention for contract artifacts, receipt material, - witness refs, reading envelopes, and reading payloads. CAS remains - content-only; semantic lookup keys live above it and include contract/schema - coordinates. - - Do not build distributed storage, proof-carrying retention, or app-specific - indexes in this slice. + `echo-cas` now has a local semantic retention index above content-only + blobs for contract artifacts, receipts, witnesses, reading payloads, + reading envelopes, and observer artifacts. 4. **Contract Retention And Semantic Lookup Seams** - Add the host seam needed for bounded observers and large retained payloads: - contract payloads can refer to retained fragments, observers can read bounded - retained ranges under budget, and unavailable retention returns typed - obstruction instead of fake success. - - Do not redefine wormholes, make full materialization canonical, or collapse - retention identity into raw CAS hashes. + Semantic retention lookup now supports bounded byte ranges under caller + budget while requiring exact semantic coordinate match. 5. **External Contract Proof Fixture** - Prove the generic host path with an externally owned Wesley-compiled contract - fixture, with `jedit` as the serious consumer shape. The fixture may exercise - text-like operations and readings, but Echo core must remain generic and free - of text, rope, buffer, editor, or Graft-specific APIs. + The installed contract pipeline now has a generic external-consumer-shaped + proof covering mutation, QueryView reading, retained evidence, and replay + without application nouns in Echo core. + +## Next Candidate Slices + +1. **Durable Witnessed Submission Persistence** + + Accepted-but-not-yet-ticked submissions should survive restart without + becoming half-accepted, uncorrelatable history. + +2. **Product-Facing Intent Outcome API** + + Wrap the current core outcome observation into a developer-facing local API + that preserves the authority boundary and does not tick synchronously. + +3. **Reference Trusted Runtime Host Loop** + + Provide a boring host-owned loop that owns tick cadence, runs until idle, and + exposes app-safe submit/observe/query surfaces. + +4. **Local Replay/DIND Proof For Contract Path** + + Turn the local replay fixture into a release-gate proof over package, + submissions, scheduler policy, receipts, readings, and retained evidence. + +5. **Release-Grade Quickstart** - Do not build the product UI, author the `jedit` contract in Echo, or add a - special `jedit` ABI. + Make the first clean-checkout contract-host flow executable end to end with + documented commands. Direct `native_rule_bootstrap` registration remains an internal fixture and transitional engine-test path. Contract-host proofs that need package identity, diff --git a/docs/design/v0.1.0-contract-artifact-retention-in-echo-cas.md b/docs/design/v0.1.0-contract-artifact-retention-in-echo-cas.md new file mode 100644 index 00000000..e49ad874 --- /dev/null +++ b/docs/design/v0.1.0-contract-artifact-retention-in-echo-cas.md @@ -0,0 +1,53 @@ + + + +# v0.1.0 Contract Artifact Retention In echo-cas + +Status: implemented local boundary. + +## Claim + +`echo-cas` remains a content-addressed byte store, but Echo needs a local +semantic index above CAS for contract artifacts, receipts, witnesses, reading +payloads, reading envelopes, and observer artifacts. + +## Boundary + +`RetainedBlobIndex` records a `SemanticBlobCoordinate` to +`RetainedBlobDescriptor` mapping. The descriptor carries: + +- semantic coordinate; +- content-only `BlobHash`; +- byte length. + +The semantic coordinate carries: + +- contract/package namespace; +- schema hash; +- artifact hash; +- retained blob role; +- caller-owned semantic digest. + +The index stores bytes through a generic `BlobStore` and pins the resulting +content hash as a local retention root. It does not change the CAS hash policy. + +## Invariants + +- CAS hash names bytes only. +- Semantic coordinate names the question those bytes answer. +- Equal bytes under different semantic coordinates do not alias. +- Re-retaining the same semantic coordinate with identical bytes is + idempotent. +- Re-retaining the same semantic coordinate with different bytes returns + `RetentionError::SemanticCoordinateConflict`. +- Missing semantic coordinates return `RetentionError::MissingSemanticCoordinate`. +- Missing content bytes return `RetentionError::MissingBlob`. +- Retention is local; this is not distributed storage or proof-carrying + retention. + +## Witnesses + +The semantic retention tests prove contract artifact bytes can load by content +hash and semantic coordinate, equal bytes under different coordinates stay +separate, and missing semantic coordinates return typed obstruction instead of +fake empty success. diff --git a/docs/design/v0.1.0-contract-aware-receipts-and-readings.md b/docs/design/v0.1.0-contract-aware-receipts-and-readings.md new file mode 100644 index 00000000..789a5341 --- /dev/null +++ b/docs/design/v0.1.0-contract-aware-receipts-and-readings.md @@ -0,0 +1,57 @@ + + + +# v0.1.0 Contract-Aware Receipts And Readings + +Status: implemented local boundary. + +## Claim + +Installed contract receipts and QueryView readings must name the contract +package boundary that produced them. + +This is evidence metadata, not authority. It does not grant tick control, query +rights, mutation rights, or retention. It only says which installed package, +schema, artifact, codec, operation id, and operation kind supplied the mutation +handler or query observer. + +## Boundary + +`ContractEvidenceIdentity` is attached only when work or reads pass through the +installed contract package boundary: + +- installed mutation ingress records carry mutation package evidence; +- scheduler receipt correlations copy that mutation evidence from ticketed + runtime ingress; +- installed QueryView readings carry query package evidence in the + `ReadingEnvelope`; +- built-in observations and directly registered observers may leave the + contract evidence field empty. + +The evidence object names: + +- installed package id; +- package name and version; +- package artifact hash; +- registry codec id and version; +- schema hash; +- operation/query id; +- mutation/query kind. + +## Non-Authority Rules + +- Application dispatch still does not tick. +- A receipt correlation is not a `TickReceipt`. +- A `ReadingEnvelope` is not execution evidence. +- Contract evidence is not a CAS lookup key. +- Contract evidence is not a substitute for semantic reading identity. + +## Witnesses + +The installed package registry test proves that a supported generated query +returns `QueryBytes` whose `ReadingEnvelope` carries contract evidence matching +the installed package record. + +The installed intent pipeline test proves that a scheduler-owned receipt +correlation for a generated mutation carries mutation package evidence after +ticketed runtime ingress and scheduler-owned execution. diff --git a/docs/design/v0.1.0-contract-reading-identity-and-bounded-payloads.md b/docs/design/v0.1.0-contract-reading-identity-and-bounded-payloads.md new file mode 100644 index 00000000..6c953f6b --- /dev/null +++ b/docs/design/v0.1.0-contract-reading-identity-and-bounded-payloads.md @@ -0,0 +1,50 @@ + + + +# v0.1.0 Contract Reading Identity And Bounded Payloads + +Status: implemented local boundary. + +## Claim + +QueryView readings need a stable identity for the question answered by the +reading. The identity must be separate from returned payload bytes and separate +from raw CAS content hashes. + +## Boundary + +`QueryReadingIdentity` is carried in `ReadingEnvelope` for QueryView readings. +It names: + +- generated query operation id; +- domain-separated digest of canonical query vars bytes; +- digest of the resolved causal basis and basis posture; +- digest of the requested local aperture, represented by budget and rights + posture; +- stable `reading_id` binding those inputs to the observer plan and installed + contract evidence when present. + +The identity is not the payload. Payload bytes remain +`ObservationPayload::QueryBytes`, and retention still requires a separate +semantic descriptor above CAS. + +## Bounded Payload Rule + +The existing observation budget remains the local bound: + +- over-budget payloads return `ObservationError::BudgetExceeded`; +- bounded successful readings report `ReadingBudgetPosture::Bounded`; +- observers may report `ReadingResidualPosture::Residual` when the bounded + payload is intentionally partial. + +## Non-Goals + +- Do not implement the full observer-rights lattice. +- Do not make CAS hashes semantic reading identities. +- Do not require streaming subscriptions. +- Do not add application nouns to Echo core. + +## Witnesses + +The observation identity test proves the `QueryReadingIdentity` changes when +query vars, query id, basis, schema/observer plan, or budget changes. diff --git a/docs/design/v0.1.0-contract-retention-and-semantic-lookup-seams.md b/docs/design/v0.1.0-contract-retention-and-semantic-lookup-seams.md new file mode 100644 index 00000000..ced3b1b6 --- /dev/null +++ b/docs/design/v0.1.0-contract-retention-and-semantic-lookup-seams.md @@ -0,0 +1,42 @@ + + + +# v0.1.0 Contract Retention And Semantic Lookup Seams + +Status: implemented local boundary. + +## Claim + +Retained contract payloads need semantic lookup and bounded byte-range access +above raw CAS identity. A content hash can load bytes, but it does not prove +those bytes answer a specific query, receipt, witness, or artifact coordinate. + +## Boundary + +`RetainedBlobIndex::load(...)` requires an exact `SemanticBlobCoordinate`. +`RetainedBlobIndex::load_range(...)` adds a bounded range seam over the same +coordinate: + +```text +semantic coordinate ++ offset ++ length ++ caller byte budget +-> bounded retained bytes or typed retention error +``` + +This is retained-payload lookup, not a streaming subscription surface. + +## Invariants + +- `BlobHash` lookup remains content-only byte lookup. +- Semantic lookup requires exact coordinate match. +- Equal bytes under different semantic coordinates do not alias. +- Bounded range lookup enforces caller byte budget before returning bytes. +- Out-of-range reads return typed retention errors. +- Missing local retention returns typed retention errors, not empty success. + +## Witnesses + +The semantic retention tests prove bounded range lookup, budget obstruction, and +coordinate mismatch behavior even when the underlying content hash matches. diff --git a/docs/design/v0.1.0-external-contract-proof-fixture.md b/docs/design/v0.1.0-external-contract-proof-fixture.md new file mode 100644 index 00000000..93f94bfe --- /dev/null +++ b/docs/design/v0.1.0-external-contract-proof-fixture.md @@ -0,0 +1,50 @@ + + + +# v0.1.0 External Contract Proof Fixture + +Status: implemented local proof fixture. + +## Claim + +Echo needs one consumer-shaped proof that the generic contract-host path works +without importing application nouns into core. + +## Fixture Shape + +The proof fixture is intentionally generic. It installs a generated-style +contract package with: + +- one mutation operation; +- one conflict-capable mutation operation; +- one QueryView query operation; +- non-trivial canonical vars; +- generated-style mutation rules and read-only query observer; +- scheduler-owned execution only; +- retained reading payload and receipt-correlation evidence through `echo-cas`; +- local replay to the same observed outcome. + +The fixture uses toy contract labels only inside the test package and generated +payloads. It does not add editor, text, jedit, Graft, or application-specific +APIs to `warp-core`. + +## Invariants + +- Submission is witnessed before execution. +- Ticketed installed-contract ingress stages without ticking. +- Mutation handlers run only during `SchedulerCoordinator::super_tick(...)`. +- Query observers are read-only and return bounded `QueryBytes`. +- Reading evidence carries contract evidence and query reading identity. +- Receipt evidence carries contract evidence. +- Retained reading payload and receipt-correlation evidence use semantic + coordinates above CAS. +- Replay reproduces the same observed intent outcome. + +## Witnesses + +`external_contract_fixture_proves_mutation_query_retention_and_replay` proves +mutation, query, retention, and replay in one generic fixture. + +Existing installed pipeline tests continue to prove unsupported mutation +rejection, footprint conflict rejection without hidden retry, and witnessed +submission replay shape. diff --git a/docs/design/v0.1.0-release-plan.md b/docs/design/v0.1.0-release-plan.md index 6c46831f..61b4880b 100644 --- a/docs/design/v0.1.0-release-plan.md +++ b/docs/design/v0.1.0-release-plan.md @@ -97,18 +97,24 @@ which vars and aperture? which outcome, obstruction, or residual posture? ``` -Missing pieces: +Implemented local pieces: -- contract-aware receipt identity; -- contract-aware reading identity; - explicit package, schema, and artifact identity in relevant evidence surfaces; -- clean distinction between `intent_id`, `submission_id`, `ticket_digest`, - `TickReceipt`, and reading identity; +- contract-aware installed mutation receipt correlations; +- contract-aware QueryView reading envelopes; +- clean local distinction between `submission_id`, `ticket_digest`, + receipt-correlation evidence, payload bytes, and reading identity; +- tests proving local identity changes when contract, schema, operation, query, + basis, vars, or aperture changes. + +Remaining pieces: + +- product-facing contract-aware receipt identity and outcome API polish; - contract-aware obstruction taxonomy, including unsupported operation, unsupported query, admission obstruction, runtime fault, missing retention, stale basis, residual reading, and budget exceeded; -- tests proving identity changes when contract, schema, operation, query, basis, - vars, or aperture changes. +- release-grade retained `TickReceipt` and reading-envelope material beyond the + current local correlation/payload fixture. ### 2. Bounded Query Readings @@ -120,14 +126,18 @@ I read this basis, through this query, under this aperture and budget, and here is either the bounded payload or the residual or obstruction posture. ``` -Missing pieces: +Implemented local pieces: - stable reading identity over query id, basis, vars, aperture, and observer plan; - bounded payload behavior; +- proof that QueryView observers remain read-only and do not tick or mutate. + +Remaining pieces: + - residual posture when budget or aperture limits prevent a full answer; - typed obstruction for stale, unsupported, or unavailable reads; -- proof that QueryView observers remain read-only and do not tick or mutate. +- product-facing helper/API polish around the bounded reading evidence. This is not the full WARP observer-rights/revelation lattice. `v0.1.0` needs bounded, read-only, honestly identified local query readings. Rights-governed @@ -167,15 +177,26 @@ Retention invariants: - missing material returns obstruction, not empty success; - cache hit is not evidence unless the semantic coordinate matches. -Missing pieces: +Implemented local pieces: -- semantic contract artifact refs above `echo-cas`; -- retained reading refs; -- retained receipt and witness refs; -- missing-retention obstruction; +- `RetainedBlobIndex` provides a semantic coordinate index above content-only + CAS; +- exact semantic coordinate loads and content-hash byte lookup; +- retained blob roles for artifacts, receipts, witnesses, reading payloads, + reading envelopes, and observer artifacts; +- bounded `load_range` with budget and typed errors; +- same semantic coordinate plus different content is rejected as a conflict; - no fake cache hits; - no raw CAS hash pretending to be semantic reading identity. +Remaining pieces: + +- first-class retained artifact, receipt, reading, and witness refs in core/ABI; +- missing-retention obstruction integrated into observation and product APIs; +- durable recovery, disk persistence, garbage collection, and rehydration; +- release-grade retained `TickReceipt` and reading-envelope material beyond the + current local fixture. + ### 4. Durable Witnessed Submission Persistence Current witnessed submission replay shape is useful, but `v0.1.0` should not @@ -328,30 +349,30 @@ Missing pieces: ## Feature Checklist -| Feature | Needed for `v0.1.0`? | Current status | -| :---------------------------------------------------- | :--------------------------- | :------------------------------------------------------------------ | -| Deterministic scheduler-owned execution | Yes | Mostly complete | -| Application cannot tick | Yes | Complete | -| Witnessed intent submission | Yes | Partial: in-memory/replay shape exists; durable persistence missing | -| Admission evidence through ticket | Yes | Complete for local pipeline | -| Ticketed runtime ingress | Yes | Complete | -| Installed contract package registry | Yes | Complete locally | -| Installed mutation handler dispatch | Yes | Complete locally | -| Intent outcome observation | Yes | Complete core surface; product API polish missing | -| Contract-aware receipts | Yes | Missing | -| Contract-aware readings | Yes | Missing | -| QueryView observer bridge | Yes | Complete core surface | -| Wesley query and mutation host helpers | Yes | Complete for current seam | -| Bounded reading identity and payloads | Yes | Missing | -| Local retention for artifacts, readings, and receipts | Yes | Missing | -| Durable accepted-submission recovery | Yes | Missing | -| App-safe WASM, Node, and browser API | Yes, if shipping JS packages | Conditional on published artifact set | -| Reference trusted host loop | Yes | Missing | -| External consumer proof fixture | Yes | Missing | -| Quickstart/docs that actually work | Yes | Partial | -| Versioned contract/API compatibility | Yes | Missing/release work | -| Versioned crates/packages/release notes | Yes | Missing/release work | -| CI release gate for v0.1 path | Yes | Partial | +| Feature | Needed for `v0.1.0`? | Current status | +| :---------------------------------------------------- | :--------------------------- | :-------------------------------------------------------------------------------------------- | +| Deterministic scheduler-owned execution | Yes | Mostly complete | +| Application cannot tick | Yes | Complete | +| Witnessed intent submission | Yes | Partial: in-memory/replay shape exists; durable persistence missing | +| Admission evidence through ticket | Yes | Complete for local pipeline | +| Ticketed runtime ingress | Yes | Complete | +| Installed contract package registry | Yes | Complete locally | +| Installed mutation handler dispatch | Yes | Complete locally | +| Intent outcome observation | Yes | Complete core surface; product API polish missing | +| Contract-aware receipts | Yes | Complete local correlation proof; product API/taxonomy polish remains | +| Contract-aware readings | Yes | Complete local QueryView proof | +| QueryView observer bridge | Yes | Complete core surface | +| Wesley query and mutation host helpers | Yes | Complete for current seam | +| Bounded reading identity and payloads | Yes | Complete local proof | +| Local retention for artifacts, readings, and receipts | Yes | `RetainedBlobIndex` boundary complete; retained refs/obstructions and durable recovery remain | +| Durable accepted-submission recovery | Yes | Missing | +| App-safe WASM, Node, and browser API | Yes, if shipping JS packages | Conditional on published artifact set | +| Reference trusted host loop | Yes | Missing | +| External consumer proof fixture | Yes | Complete generic fixture; serious app-owned fixture remains | +| Quickstart/docs that actually work | Yes | Partial | +| Versioned contract/API compatibility | Yes | Missing/release work | +| Versioned crates/packages/release notes | Yes | Missing/release work | +| CI release gate for v0.1 path | Yes | Partial | ## Explicit Non-Goals For v0.1.0 diff --git a/docs/method/backlog/v0.1.0/KERNEL_contract-aware-receipts-and-readings.md b/docs/method/backlog/v0.1.0/KERNEL_contract-aware-receipts-and-readings.md index 6aa71d04..5f9f54ff 100644 --- a/docs/method/backlog/v0.1.0/KERNEL_contract-aware-receipts-and-readings.md +++ b/docs/method/backlog/v0.1.0/KERNEL_contract-aware-receipts-and-readings.md @@ -3,7 +3,7 @@ # Contract-Aware Receipts And Readings -Status: v0.1.0 release blocker. +Status: implemented local boundary. Depends on: diff --git a/docs/method/backlog/v0.1.0/KERNEL_contract-reading-identity-and-bounded-payloads.md b/docs/method/backlog/v0.1.0/KERNEL_contract-reading-identity-and-bounded-payloads.md index 19ee379b..d150f027 100644 --- a/docs/method/backlog/v0.1.0/KERNEL_contract-reading-identity-and-bounded-payloads.md +++ b/docs/method/backlog/v0.1.0/KERNEL_contract-reading-identity-and-bounded-payloads.md @@ -3,7 +3,7 @@ # Contract Reading Identity And Bounded Payloads -Status: v0.1.0 release blocker. +Status: implemented local boundary. Depends on: diff --git a/docs/method/backlog/v0.1.0/PLATFORM_contract-artifact-retention-in-echo-cas.md b/docs/method/backlog/v0.1.0/PLATFORM_contract-artifact-retention-in-echo-cas.md index d44484c0..907b4e60 100644 --- a/docs/method/backlog/v0.1.0/PLATFORM_contract-artifact-retention-in-echo-cas.md +++ b/docs/method/backlog/v0.1.0/PLATFORM_contract-artifact-retention-in-echo-cas.md @@ -3,7 +3,7 @@ # Contract Artifact Retention In echo-cas -Status: v0.1.0 release blocker. +Status: implemented local boundary. Depends on: diff --git a/docs/method/backlog/v0.1.0/PLATFORM_contract-retention-and-semantic-lookup-seams.md b/docs/method/backlog/v0.1.0/PLATFORM_contract-retention-and-semantic-lookup-seams.md index 40f14545..30936ae6 100644 --- a/docs/method/backlog/v0.1.0/PLATFORM_contract-retention-and-semantic-lookup-seams.md +++ b/docs/method/backlog/v0.1.0/PLATFORM_contract-retention-and-semantic-lookup-seams.md @@ -3,7 +3,7 @@ # Contract Retention And Semantic Lookup Seams -Status: v0.1.0 release blocker. +Status: implemented local boundary. Depends on: diff --git a/docs/method/backlog/v0.1.0/PLATFORM_external-contract-proof-fixture.md b/docs/method/backlog/v0.1.0/PLATFORM_external-contract-proof-fixture.md index 4207b32b..6dad8cf7 100644 --- a/docs/method/backlog/v0.1.0/PLATFORM_external-contract-proof-fixture.md +++ b/docs/method/backlog/v0.1.0/PLATFORM_external-contract-proof-fixture.md @@ -3,7 +3,7 @@ # External Contract Proof Fixture -Status: v0.1.0 release blocker. +Status: implemented local proof fixture. Depends on: diff --git a/docs/spec/SPEC-0009-wasm-abi-v3.md b/docs/spec/SPEC-0009-wasm-abi-v3.md index 3cceaf23..d7ae50e6 100644 --- a/docs/spec/SPEC-0009-wasm-abi-v3.md +++ b/docs/spec/SPEC-0009-wasm-abi-v3.md @@ -232,7 +232,7 @@ to: | `payload` | tagged union | Head, snapshot, recorded truth, or query bytes | `artifact_hash` is computed as -`blake3("echo:observation-artifact:v2\0" || canonical_cbor(hash_input))`. +`blake3("echo:observation-artifact:v3\0" || canonical_cbor(hash_input))`. ### ResolvedObservationCoordinate