From 34191c02901b9a8877555032e616fd6843904cb7 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 12 Jul 2026 11:02:37 -0700 Subject: [PATCH 1/7] Remove retired browser and legacy API surfaces --- CHANGELOG.md | 25 +- README.md | 179 ++--- advanced.ts | 101 +-- bin/cli/commands/optic.ts | 2 +- browser.ts | 44 -- diagnostics.ts | 50 +- docs/migrations/v19/README.md | 151 ++-- docs/topics/api/README.md | 100 +-- docs/topics/getting-started.md | 233 +++--- docs/topics/git-substrate.md | 18 +- docs/topics/observers.md | 1 - docs/topics/optic-reads.md | 1 - docs/topics/querying.md | 184 ++--- docs/topics/reference.md | 626 +++++----------- docs/topics/strands.md | 92 +-- docs/topics/sync.md | 21 +- eslint.config.ts | 39 - examples/README.md | 18 - examples/bounded-reads.ts | 38 - examples/observers.ts | 44 -- examples/optics.ts | 44 -- index.ts | 3 +- jsr.json | 5 - legacy.ts | 479 ------------ package.json | 12 +- rootCompatibility.ts | 50 -- scripts/check-source-backed-reference.ts | 3 - .../conformance/btrProvenanceBoundary.test.ts | 26 +- .../btrSigningBytesOwnership.test.ts | 29 +- .../snapshotPublicApiSurface.test.ts | 14 +- .../v18BoundedMemoryLargeGraphGate.test.ts | 2 +- .../v18CoordinateOpticPublicPath.test.ts | 14 +- .../v18FirstUseOpticsHonesty.test.ts | 8 +- test/fixtures/mergeConflictCorpus.ts | 2 +- test/type-check/consumer.ts | 682 ------------------ test/type-check/tsconfig.json | 1 - test/type-check/v19-first-use.ts | 2 +- test/type-check/v19-subpaths.ts | 38 +- test/unit/cli/optic.test.ts | 30 +- test/unit/domain/WarpApp.facade.test.ts | 6 +- test/unit/domain/WarpCore.apiSurface.test.ts | 12 +- .../domain/WarpCore.effectPipeline.test.ts | 31 +- test/unit/domain/WarpCore.emit.test.ts | 58 +- test/unit/domain/WarpFacade.test.ts | 218 +++--- .../unit/domain/WarpGraph.public-sync.test.ts | 2 +- .../domain/WarpGraph.strands.compare.test.ts | 81 ++- test/unit/domain/WarpWorldline.test.ts | 102 +-- .../GitWarpReceiptEnvelopeBoundary.test.ts | 56 +- test/unit/domain/legacy.exports.test.ts | 648 ----------------- .../publicApiExecutableExamples.test.ts | 44 +- .../publicReadApiExecutablePaths.test.ts | 7 +- .../publicReadingSurface.behavior.test.ts | 24 +- .../publicStrandAndRuntimeSeams.test.ts | 28 +- .../services/merge/TtdMergeInspector.test.ts | 2 +- .../services/query/BoundedSupportRule.test.ts | 40 +- .../services/query/CausalIndexPlan.test.ts | 15 +- .../query/SupportFragmentPlan.test.ts | 34 +- test/unit/index.exports.test.ts | 21 - .../jsr-publish-root-graph-shape.test.ts | 20 - .../openwarpgraph-composition-root.test.ts | 36 +- .../scripts/public-api-aperture-noun.test.ts | 11 +- .../scripts/public-api-facade-split.test.ts | 46 -- .../scripts/release-artifact-command.test.ts | 51 +- .../scripts/root-export-line-budget.test.ts | 6 - .../scripts/v18-package-surface-audit.test.ts | 110 --- .../scripts/v19-public-api-boundary.test.ts | 78 +- .../scripts/worldline-api-deprecation.test.ts | 105 --- test/unit/v7-guards.test.ts | 22 +- tsconfig.publish.json | 2 - tsconfig.src.json | 2 - vitest.config.ts | 2 +- 71 files changed, 1252 insertions(+), 4079 deletions(-) delete mode 100644 browser.ts delete mode 100644 examples/README.md delete mode 100644 examples/bounded-reads.ts delete mode 100644 examples/observers.ts delete mode 100644 examples/optics.ts delete mode 100644 legacy.ts delete mode 100644 rootCompatibility.ts delete mode 100644 test/type-check/consumer.ts delete mode 100644 test/unit/domain/legacy.exports.test.ts delete mode 100644 test/unit/index.exports.test.ts delete mode 100644 test/unit/scripts/jsr-publish-root-graph-shape.test.ts delete mode 100644 test/unit/scripts/public-api-facade-split.test.ts delete mode 100644 test/unit/scripts/v18-package-surface-audit.test.ts delete mode 100644 test/unit/scripts/worldline-api-deprecation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 96892d86b..8b49806f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,24 +26,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Added the v19 `openWarp()` product opener plus `Warp` and `Timeline` - facade handles to the package root only; the browser root remains outside - the v19 facade surface. -- Moved graph-shaped, worldline-shaped, optic-shaped, witness-shaped, and - diagnostic compatibility exports out of the package root into explicit - `legacy`, `storage`, `advanced`, and `diagnostics` subpaths. The package root - now rejects those nouns through the v19 public API boundary audit. + facade handles to the package root. +- Moved supported storage, formal WARP, and operator inspection exports out of + the package root into the explicit `storage`, `advanced`, and `diagnostics` + subpaths. The package root now rejects graph-first compatibility nouns + through the v19 public API boundary audit. - Added visible-state scope helpers to the `diagnostics` subpath so materialized-state inspection has an explicit non-legacy import path. -- Deprecated the entire graph-first legacy API. `legacy` remains migration-only - and is no longer presented as a valid first-use path. - Moved receipt canonical JSON and ORSet/full-state wire encoding out of domain types and storage adapters into infrastructure codec modules; `ORSet` no longer exposes `serialize()` or `deserialize()`. -- Raised the coverage ratchet from `92.10%` to `92.56%` after adding targeted - coverage for bounded query node paging and memory-budget rejection paths. +- Raised the coverage ratchet from `92.10%` to `92.62%` after adding targeted + coverage for bounded query node paging and memory-budget rejection paths and + removing retired compatibility surfaces. - Upgraded `@git-stunts/git-cas` to `^6.1.0` so Git-backed state caches can use the library's crash-safe `RootSet` retention API. +### Removed + +- Removed the `browser` and `legacy` package entrypoints, the graph-first + compatibility barrel, and their compatibility-only examples and tests. +- Removed the v18 graph-first package exports rather than carrying a deprecated + second application API through v19. + ### Fixed - Git-backed state-cache payload trees are now anchored through a graph-scoped diff --git a/README.md b/README.md index 538fb4d6c..bb6694024 100644 --- a/README.md +++ b/README.md @@ -58,41 +58,47 @@ This is the public contract new application code should start from. The deprecated v18 graph-first API remains available only for migration. ```typescript -import { openWarp, intent, reading } from "@git-stunts/git-warp"; -import { GitStorageAdapter } from "@git-stunts/git-warp/storage"; -import GitPlumbing from "@git-stunts/plumbing"; +import { openWarp, intent, reading } from '@git-stunts/git-warp'; +import { GitStorageAdapter } from '@git-stunts/git-warp/storage'; +import GitPlumbing from '@git-stunts/plumbing'; -const plumbing = new GitPlumbing({ cwd: "." }); +const plumbing = new GitPlumbing({ cwd: '.' }); const warp = await openWarp({ storage: new GitStorageAdapter({ plumbing }), - writer: "agent-1", + writer: 'agent-1', }); -const events = await warp.timeline("events"); - -await events.write(intent.node.add({ - subject: "user:alice", -})); - -const write = await events.write(intent.property.set({ - subject: "user:alice", - key: "role", - value: "admin", -})); - -const role = await events.read(reading.property({ - subject: "user:alice", - key: "role", -})); +const events = await warp.timeline('events'); + +await events.write( + intent.node.add({ + subject: 'user:alice', + }) +); + +const write = await events.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) +); + +const role = await events.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) +); console.log(role.value); console.log(role.receipt); ``` -The v18 graph-first API remains only under `@git-stunts/git-warp/legacy`. That -entrypoint is deprecated and migration-only. Do not start new application code -on `openWarpWorldline()`, `openWarpGraph()`, `WarpApp`, `WarpCore`, -`GitGraphAdapter`, patch builders, or graph operation creators. +The v18 graph-first API is not exported in v19. Migrate uses of +`openWarpWorldline()`, `openWarpGraph()`, `WarpApp`, `WarpCore`, +`GitGraphAdapter`, patch builders, and graph operation creators before +upgrading. ### Bounded Reads @@ -111,11 +117,11 @@ through advanced/diagnostic surfaces, not the package root.

For the Nerds™: Optics

-> In category theory, an **optic** from a whole $(S, T)$ to a part $(A, B)$ is an element of a coend — an existential package over a *residual* object $M$: +> In category theory, an **optic** from a whole $(S, T)$ to a part $(A, B)$ is an element of a coend — an existential package over a _residual_ object $M$: > > $$\mathrm{Optic}\big((S,T),\,(A,B)\big) \;=\; \int^{M} \mathcal{C}\!\left(S,\; M \otimes A\right) \times \mathcal{C}\!\left(M \otimes B,\; T\right)$$ > -> That is a focus $S \to M \otimes A$ paired with a put-back $M \otimes B \to T$, where $M$ — everything you *didn't* look at — is sealed inside the existential and never named. +> That is a focus $S \to M \otimes A$ paired with a put-back $M \otimes B \to T$, where $M$ — everything you _didn't_ look at — is sealed inside the existential and never named. > > git-warp's read model is the read-only half. The whole $S$ is a **coordinate** (a reading over causal history); the part $A$ is the bounded question (`.node()`, `.prop()`, a traversal); the residual $M$ is the rest of history you deliberately do not materialize. The **bounded support rule** is that $M$ made small and explicit, and `prepareOpticBasis()` is the witness that a lawful factorization $S \to M \otimes A$ exists at the chosen coordinate. No witness ⇒ `E_OPTIC_NO_BOUNDED_BASIS`: the runtime refuses the read rather than collapsing $M$ back into the whole graph. > @@ -125,11 +131,10 @@ through advanced/diagnostic surfaces, not the package root. ### Bounded reads in practice -The root API should express bounded questions as readings and report the -support actually used through receipts. The older `openWarpGraph()` and -`openWarpWorldline()` examples are deprecated migration material, not a second -application path. They live under `@git-stunts/git-warp/legacy` only so -existing consumers can pay down old imports deliberately. +The root API expresses bounded questions as readings and reports the support +actually used through receipts. The removed `openWarpGraph()` and +`openWarpWorldline()` paths are migration source material, not a second +application API. See the [v19 API reflection](docs/topics/api/), the [v19 migration guide](docs/migrations/v19/), and @@ -174,32 +179,32 @@ mindmap Checkpoint ~ ``` -| Concept | Status | One-line meaning | -|---|---|---| -| **Patch** | shipped | An appended claim about a bounded causal site; never rewritten. | -| **Worldline** | transition | The named causal history a writer commits to and readers observe. | -| **Live strand** | transition | A strand over a live parent basis plus its own owned overlay divergence. | -| **Braid** | target | A composition of lanes; common-basis braid validation is not yet shipped. | -| **Coordinate** | transition | A comparable read point: a causal basis plus a ceiling. | -| **Frontier** | transition | The causal basis — which history has been observed (writer tips / version vectors). | -| **Ceiling** | shipped | The upper replay boundary on a coordinate or lane. | -| **Tick** | transition | One atomic admitted history step on a lane. | -| **ProjectionHandle** | shipped | A pinned read handle returned by `live()` and `seek()`. | -| **Observer** | transition | A read surface that answers a question through an aperture. | -| **Aperture** | transition | The `{ match, expose, redact }` policy bounding what an observer sees. | -| **Optic** | transition | A first-class runtime read-intent noun used by fluent optic reads; native Continuum transport remains future work. | -| **Optic basis** | shipped | Verified bounded evidence (`prepareOpticBasis()`) that an optic read is answerable without full materialization. | -| **Bounded support rule** | transition | The smallest causally sufficient support set to answer an optic honestly. | -| **Causal index** | transition | A rebuildable acceleration structure that finds support without whole-graph discovery. | -| **Support fragment** | transition | A cached partial materialization keyed by support and coordinate (cache storage pending). | -| **Materialization plan** | target | The plan deciding receipts vs. indexes vs. fragments vs. replay vs. full state. | -| **Witness** | target | Minimal information sufficient to justify a local change result. | -| **TickReceipt** | shipped | The operational envelope recording what happened for one admitted step. | -| **GraphDiff** | transition | A first-class "what changed between these coordinates?" result. | +| Concept | Status | One-line meaning | +| ------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **Patch** | shipped | An appended claim about a bounded causal site; never rewritten. | +| **Worldline** | transition | The named causal history a writer commits to and readers observe. | +| **Live strand** | transition | A strand over a live parent basis plus its own owned overlay divergence. | +| **Braid** | target | A composition of lanes; common-basis braid validation is not yet shipped. | +| **Coordinate** | transition | A comparable read point: a causal basis plus a ceiling. | +| **Frontier** | transition | The causal basis — which history has been observed (writer tips / version vectors). | +| **Ceiling** | shipped | The upper replay boundary on a coordinate or lane. | +| **Tick** | transition | One atomic admitted history step on a lane. | +| **ProjectionHandle** | shipped | A pinned read handle returned by `live()` and `seek()`. | +| **Observer** | transition | A read surface that answers a question through an aperture. | +| **Aperture** | transition | The `{ match, expose, redact }` policy bounding what an observer sees. | +| **Optic** | transition | A first-class runtime read-intent noun used by fluent optic reads; native Continuum transport remains future work. | +| **Optic basis** | shipped | Verified bounded evidence (`prepareOpticBasis()`) that an optic read is answerable without full materialization. | +| **Bounded support rule** | transition | The smallest causally sufficient support set to answer an optic honestly. | +| **Causal index** | transition | A rebuildable acceleration structure that finds support without whole-graph discovery. | +| **Support fragment** | transition | A cached partial materialization keyed by support and coordinate (cache storage pending). | +| **Materialization plan** | target | The plan deciding receipts vs. indexes vs. fragments vs. replay vs. full state. | +| **Witness** | target | Minimal information sufficient to justify a local change result. | +| **TickReceipt** | shipped | The operational envelope recording what happened for one admitted step. | +| **GraphDiff** | transition | A first-class "what changed between these coordinates?" result. | | **Witnessed suffix admission shell** | transition | Observer-readable import/export envelope for a transported suffix; live Echo/git-warp network exchange remains Continuum stack work. | -| **WarpStateSnapshot** | shipped | A persisted materialized graph state at a coordinate. | -| **WarpStateCache** | shipped | The owning system for persisted and in-memory snapshot reuse. | -| **Checkpoint** | transition | A pinned snapshot protected from ordinary eviction. | +| **WarpStateSnapshot** | shipped | A persisted materialized graph state at a coordinate. | +| **WarpStateCache** | shipped | The owning system for persisted and in-memory snapshot reuse. | +| **Checkpoint** | transition | A pinned snapshot protected from ordinary eviction. | The topic pages own the current explanations for these nouns. Exact API, CLI, schema, and error inventories should be generated or coverage-checked rather @@ -217,7 +222,7 @@ Writers can make independent changes without a central coordinator. When history

For the Nerds™ — Convergence is a join-semilattice

-> Deterministic multi-writer merge is the statement that materialized state forms a **join-semilattice** $(L, \sqcup)$: a partial order with a least upper bound for any pair. Each writer's patches push state *up* the order, and merge is the **join** $\sqcup$ — idempotent, commutative, and associative: +> Deterministic multi-writer merge is the statement that materialized state forms a **join-semilattice** $(L, \sqcup)$: a partial order with a least upper bound for any pair. Each writer's patches push state _up_ the order, and merge is the **join** $\sqcup$ — idempotent, commutative, and associative: > > $$x \sqcup x = x \qquad x \sqcup y = y \sqcup x \qquad (x \sqcup y) \sqcup z = x \sqcup (y \sqcup z)$$ > @@ -246,13 +251,13 @@ The runtime is designed so reads stay scoped. It avoids the “just materialize

For the Nerds™ — A causal cone is a down-set

-> History is a **poset** $(H, \preceq)$ under causal precedence (Git's parent edges give the Hasse diagram). A **coordinate** picks a *consistent cut*: a down-closed set, i.e. a **down-set / order ideal** +> History is a **poset** $(H, \preceq)$ under causal precedence (Git's parent edges give the Hasse diagram). A **coordinate** picks a _consistent cut_: a down-closed set, i.e. a **down-set / order ideal** > > $$\downarrow\! C \;=\; \{\, x \in H \mid x \preceq c \text{ for some } c \in C \,\}.$$ > > A **frontier** is that cut's boundary — an **antichain** of writer tips, the maximal elements of the ideal. > -> A read's **causal cone** $D(v) = {\downarrow}\{v\}$ is the down-set generated by $v$: everything that could have influenced it, and nothing that couldn't. Because the poset is well-founded, $D(v)$ is finite even when $H$ is unbounded — which is *why* a bounded read can exist at all. The **bounded support rule** is then just the claim "this optic's answer factors through $D(v)$, so materializing the rest of $H \setminus D(v)$ is provably unnecessary." +> A read's **causal cone** $D(v) = {\downarrow}\{v\}$ is the down-set generated by $v$: everything that could have influenced it, and nothing that couldn't. Because the poset is well-founded, $D(v)$ is finite even when $H$ is unbounded — which is _why_ a bounded read can exist at all. The **bounded support rule** is then just the claim "this optic's answer factors through $D(v)$, so materializing the rest of $H \setminus D(v)$ is provably unnecessary."
@@ -265,22 +270,21 @@ The broader worldline-wide direction is still narrower than the doctrine: live s ## API surface The v19 application entry point is the root intent/timeline/reading/receipt -surface. The v18 `openWarpWorldline()` and `openWarpGraph()` entry points are -deprecated and available only from `@git-stunts/git-warp/legacy` for migration. -Treat every legacy import as removal debt. New application code should use -root readings and receipts; new operator code should use explicit diagnostic -APIs instead of the graph-first compatibility bag. +surface. The v18 `openWarpWorldline()` and `openWarpGraph()` package entry +points are removed. New application code uses root readings and receipts; new +operator code uses explicit diagnostic APIs instead of a graph-first +compatibility bag. ## Architectural moments Internally, the runtime still has four architectural moments: -|Moment|Capabilities|What it does| -|---|---|---| -|Commitment|`patches`, `strands`, `comparison`|Admits claims into frontier-relative truth| -|Folding|`checkpoint`|Re-expresses admitted history as operational artifacts| -|Revelation|`query`, `subscriptions`, `provenance`|Exposes admitted truth under bounded rights| -|Governance|`sync`|Transports and admits remote suffixes| +| Moment | Capabilities | What it does | +| ---------- | -------------------------------------- | ------------------------------------------------------ | +| Commitment | `patches`, `strands`, `comparison` | Admits claims into frontier-relative truth | +| Folding | `checkpoint` | Re-expresses admitted history as operational artifacts | +| Revelation | `query`, `subscriptions`, `provenance` | Exposes admitted truth under bounded rights | +| Governance | `sync` | Transports and admits remote suffixes | Deprecated migration code may still encounter the old graph-first names for these moments. Do not introduce those names in new root-level application code. @@ -307,7 +311,7 @@ Each writer appends patch commits under `refs/warp//writers/`. ## Continuum -`git-warp` is one runtime in the [Continuum](https://github.com/flyingrobots/continuum) stack. Continuum is **not** a runtime, database, or storage engine — it is the shared **boundary protocol** that lets independent runtimes, apps, debuggers, and agents exchange *witnessed history* instead of copying one database or scraping UIs. +`git-warp` is one runtime in the [Continuum](https://github.com/flyingrobots/continuum) stack. Continuum is **not** a runtime, database, or storage engine — it is the shared **boundary protocol** that lets independent runtimes, apps, debuggers, and agents exchange _witnessed history_ instead of copying one database or scraping UIs. Continuum's model shift is the same one git-warp is built on: @@ -324,22 +328,22 @@ In the stack, **git-warp and Echo own runtime truth**, while Continuum owns the ## When to use it -|Use case|Fit| -|---|---| -|Offline-first multi-writer convergence|Strong| -|Agent/tool substrate with causal history|Strong| -|Graph semantics without inventing merge law|Strong| -|Speculative lanes for what-if exploration|Strong| -|High-throughput real-time execution|Use Echo instead| -|General-purpose OLTP|Use Postgres| -|Full-text search / analytics|Use purpose-built engines| -|Time-travel debugging UI|Use warp-ttd on top of git-warp| +| Use case | Fit | +| ------------------------------------------- | ------------------------------- | +| Offline-first multi-writer convergence | Strong | +| Agent/tool substrate with causal history | Strong | +| Graph semantics without inventing merge law | Strong | +| Speculative lanes for what-if exploration | Strong | +| High-throughput real-time execution | Use Echo instead | +| General-purpose OLTP | Use Postgres | +| Full-text search / analytics | Use purpose-built engines | +| Time-travel debugging UI | Use warp-ttd on top of git-warp | ## Runtime posture -Legacy docs may still describe `openWarpWorldline()`, worldline reads, +The v19 migration guide records removed graph-first names, worldline reads, coordinates, reified optics, and observer apertures for migration context. They -are not the first-use application path. `GitWarpWitnessedSuffixAdmissionShell` +are not package-level application paths. `GitWarpWitnessedSuffixAdmissionShell` is a transition runtime envelope, not proof of live network exchange. Native Continuum witnesshood, remote optic transport, common-basis braid validation, live Echo/git-warp suffix exchange, support-fragment cache storage, and @@ -350,12 +354,11 @@ their own docs say otherwise. - [Topics](docs/topics/README.md) — task-oriented documentation map - [v19 API reflection](docs/topics/api/) — current public API direction -- [v19 migration guide](docs/migrations/v19/) — deprecated legacy API paydown +- [v19 migration guide](docs/migrations/v19/) — graph-first API replacement - [Optic reads](docs/topics/optic-reads.md), [Observers](docs/topics/observers.md), and [Querying](docs/topics/querying.md) — read-model context - [Strands](docs/topics/strands.md), [Sync](docs/topics/sync.md), and [CLI](docs/topics/cli.md) — runtime and operator paths - [Git substrate](docs/topics/git-substrate.md), [Content and CAS](docs/topics/content-and-cas.md), and [Continuum boundary](docs/topics/continuum-boundary.md) — substrate and boundary explanations - [Operations](docs/operations/) and [Troubleshooting](docs/topics/troubleshooting.md) — maintenance and recovery workflows -- [Examples](examples/) — runnable read-model snippets - [Architecture](ARCHITECTURE.md) — hexagonal layers and admission kernel --- @@ -424,9 +427,9 @@ Migration tools and diagnostic surfaces exist. See [Operations](docs/operations/ Yes. Graph history lives under WARP refs such as `refs/warp//writers/`, separate from branch refs and the checked-out worktree. -### Where can I get help or see examples? +### Where can I get help? -[examples/](examples/), [docs/topics/](docs/topics/README.md), [Issues](https://github.com/git-stunts/git-warp/issues). +[docs/topics/](docs/topics/README.md), [Issues](https://github.com/git-stunts/git-warp/issues). --- diff --git a/advanced.ts b/advanced.ts index 598efd76a..f74b6b6a3 100644 --- a/advanced.ts +++ b/advanced.ts @@ -6,64 +6,75 @@ * lands through v19. */ -export { - BoundedSupportRule, - CausalIndexPlan, - composeWormholes, - createWormhole, - deserializeWormhole, - LiveSelector, - Observer, - ObserverAccumulation, - ObserverBasis, - ObserverEmission, - ObserverPlan, - ObserverReadingEnvelope, - openAperture, - Optic, - OpticAperturePosture, - OpticBasisPosture, - OpticCoordinatePosture, - OpticSupportRule, - ProjectionHandle, - RejectedZKWormhole, - replayWormhole, - serializeWormhole, - StrandSelector, - SupportFragmentPlan, - VerifiedZKWormhole, - verifyZKWormhole, - WarpWorldlineCoordinate, - WarpWorldlineOpticBasis, - WorldlineSelector, - ZKWormholeEdge, - ZKWormholeProofVerifierPort, -} from './legacy.ts'; -export * from './src/continuumExports.ts'; +export { default as BoundedSupportRule } from './src/domain/services/query/BoundedSupportRule.ts'; export type { - Aperture, - ApertureOpeningVerificationResult, BoundedSupportDirection, BoundedSupportKind, BoundedSupportRuleFields, BoundedSupportSurface, +} from './src/domain/services/query/BoundedSupportRule.ts'; +export { default as CausalIndexPlan } from './src/domain/services/query/CausalIndexPlan.ts'; +export type { CausalIndexFamily, CausalIndexPlanFields, CausalIndexPlanPosture, - ObserverConfig, - ObserverPlanFields, +} from './src/domain/services/query/CausalIndexPlan.ts'; +export { + composeWormholes, + createWormhole, + deserializeWormhole, + replayWormhole, + serializeWormhole, +} from './src/application/WormholeServiceDefaults.ts'; +export { default as LiveSelector } from './src/domain/types/LiveSelector.ts'; +export { default as Observer } from './src/domain/services/query/Observer.ts'; +export { default as ObserverAccumulation } from './src/domain/services/query/ObserverAccumulation.ts'; +export { default as ObserverBasis } from './src/domain/services/query/ObserverBasis.ts'; +export { default as ObserverEmission } from './src/domain/services/query/ObserverEmission.ts'; +export { default as ObserverPlan } from './src/domain/services/query/ObserverPlan.ts'; +export type { ObserverPlanFields } from './src/domain/services/query/ObserverPlan.ts'; +export { default as ObserverReadingEnvelope } from './src/domain/services/query/ObserverReadingEnvelope.ts'; +export type { ObserverReadingEnvelopeBudget, ObserverReadingEnvelopeFields, - OpticAperturePostureValue, - OpticBasisPostureValue, +} from './src/domain/services/query/ObserverReadingEnvelope.ts'; +export { + openAperture, + verifyZKWormhole, +} from './src/domain/services/wormhole/ZKWormholeService.ts'; +export { default as Optic } from './src/domain/services/optic/Optic.ts'; +export type { OpticContextValue, - OpticCoordinatePostureValue, OpticFields, OpticPostureFields, - OpticSupportRuleValue, +} from './src/domain/services/optic/Optic.ts'; +export { default as OpticAperturePosture } from './src/domain/services/optic/OpticAperturePosture.ts'; +export type { OpticAperturePostureValue } from './src/domain/services/optic/OpticAperturePosture.ts'; +export { default as OpticBasisPosture } from './src/domain/services/optic/OpticBasisPosture.ts'; +export type { OpticBasisPostureValue } from './src/domain/services/optic/OpticBasisPosture.ts'; +export { default as OpticCoordinatePosture } from './src/domain/services/optic/OpticCoordinatePosture.ts'; +export type { OpticCoordinatePostureValue } from './src/domain/services/optic/OpticCoordinatePosture.ts'; +export { default as OpticSupportRule } from './src/domain/services/optic/OpticSupportRule.ts'; +export type { OpticSupportRuleValue } from './src/domain/services/optic/OpticSupportRule.ts'; +export { default as ProjectionHandle } from './src/domain/services/ProjectionHandle.ts'; +export { default as RejectedZKWormhole } from './src/domain/services/wormhole/RejectedZKWormhole.ts'; +export { default as StrandSelector } from './src/domain/types/StrandSelector.ts'; +export { default as SupportFragmentPlan } from './src/domain/services/query/SupportFragmentPlan.ts'; +export type { SupportFragmentMaterializationPosture, SupportFragmentPlanFields, - WarpWorldlineCoordinateFrontierEntry, - ZKWormholeEdgeFields, +} from './src/domain/services/query/SupportFragmentPlan.ts'; +export { default as VerifiedZKWormhole } from './src/domain/services/wormhole/VerifiedZKWormhole.ts'; +export { default as WarpWorldlineCoordinate } from './src/domain/WarpWorldlineCoordinate.ts'; +export type { WarpWorldlineCoordinateFrontierEntry } from './src/domain/WarpWorldlineCoordinate.ts'; +export { default as WarpWorldlineOpticBasis } from './src/domain/WarpWorldlineOpticBasis.ts'; +export { default as WorldlineSelector } from './src/domain/types/WorldlineSelector.ts'; +export { default as ZKWormholeEdge } from './src/domain/services/wormhole/ZKWormholeEdge.ts'; +export type { ZKWormholeEdgeFields } from './src/domain/services/wormhole/ZKWormholeEdge.ts'; +export type { + ApertureOpeningVerificationResult, ZKWormholeVerificationResult, -} from './legacy.ts'; +} from './src/domain/services/wormhole/ZKWormholeVerificationResult.ts'; +export { default as ZKWormholeProofVerifierPort } from './src/ports/ZKWormholeProofVerifierPort.ts'; +export type { Aperture, ObserverConfig } from './src/domain/types/Aperture.ts'; +export * from './src/continuumExports.ts'; diff --git a/bin/cli/commands/optic.ts b/bin/cli/commands/optic.ts index 3b1703d28..7384b7718 100644 --- a/bin/cli/commands/optic.ts +++ b/bin/cli/commands/optic.ts @@ -1,4 +1,4 @@ -import { openWarpWorldline } from '../../../legacy.ts'; +import { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; import QueryError from '../../../src/domain/errors/QueryError.ts'; import type ReadIdentity from '../../../src/domain/services/optic/ReadIdentity.ts'; import type WorldlineOptic from '../../../src/domain/services/optic/WorldlineOptic.ts'; diff --git a/browser.ts b/browser.ts deleted file mode 100644 index 82f180fb2..000000000 --- a/browser.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Browser entry point for @git-stunts/git-warp. - * - * Re-exports only browser-safe code without exposing the legacy graph-shaped - * compatibility API from the browser root. Use `MemoryStorageAdapter` from the - * storage subpath and `WebCryptoAdapter` here for in-browser operation. - * - * @module browser - * - * @example - * ```js - * import { WebCryptoAdapter, generateWriterId } from '@git-stunts/git-warp/browser'; - * import { MemoryStorageAdapter } from '@git-stunts/git-warp/storage'; - * import { sha1sync } from '@git-stunts/git-warp/sha1sync'; - * - * const adapter = new MemoryStorageAdapter({ hash: sha1sync }); - * const crypto = new WebCryptoAdapter(); - * ``` - */ - -import { installDefaultRuntimeHostBrowserPorts } from './src/application/RuntimeHostBrowserDefaults.ts'; - -installDefaultRuntimeHostBrowserPorts(); - -export { default as WebCryptoAdapter } from './src/infrastructure/adapters/WebCryptoAdapter.ts'; - -// CRDT primitives -export { default as VersionVector } from './src/domain/crdt/VersionVector.ts'; -export { default as SyncSecret } from './src/domain/services/sync/SyncSecret.ts'; -export type { SyncRateLimitConfig } from './src/domain/services/sync/SyncRateLimiter.ts'; - -// Errors -export { default as WarpError } from './src/domain/errors/WarpError.ts'; -export { - EncryptionError, - ForkError, - QueryError, - StorageError, - TraversalError, - SyncError, -} from './src/domain/errors/index.ts'; - -// Utilities -export { generateWriterId } from './src/domain/utils/WriterId.ts'; diff --git a/diagnostics.ts b/diagnostics.ts index b5c586f72..c9369d21e 100644 --- a/diagnostics.ts +++ b/diagnostics.ts @@ -5,37 +5,37 @@ * first-use code does not learn substrate nouns before it needs them. */ +export { default as BisectService } from './src/domain/services/BisectService.ts'; +export { default as CommitDagTraversalService } from './src/domain/services/dag/CommitDagTraversalService.ts'; +export { default as ContentAttachmentProjection } from './src/domain/services/ContentAttachmentProjection.ts'; export { - BisectService, - CommitDagTraversalService, - ContentAttachmentProjection, exportCoordinateComparisonFact, exportCoordinateTransferPlanFact, - GraphDiff, - GraphOpAlgebraProjection, - QueryBuilder, - TtdMergeBranch, - TtdMergeFootprint, - TtdMergeInspection, - TtdMergeInspector, - TtdMergeLoweringWitness, - TtdMergeObstructionWitness, - TtdMergePolicyRequirement, -} from './legacy.ts'; +} from './src/domain/services/CoordinateFactExport.ts'; +export { default as GraphDiff } from './src/domain/services/comparison/GraphDiff.ts'; +export type { GraphDiffFields } from './src/domain/services/comparison/GraphDiff.ts'; +export type { GraphDiffOptions } from './src/domain/capabilities/ComparisonCapability.ts'; +export { default as GraphOpAlgebraProjection } from './src/domain/services/GraphOpAlgebraProjection.ts'; +export { default as QueryBuilder } from './src/domain/services/query/QueryBuilder.ts'; +export { default as TtdMergeBranch } from './src/domain/services/merge/TtdMergeBranch.ts'; +export type { TtdMergeBranchFields } from './src/domain/services/merge/TtdMergeBranch.ts'; +export { default as TtdMergeFootprint } from './src/domain/services/merge/TtdMergeFootprint.ts'; +export type { TtdMergeFootprintFields } from './src/domain/services/merge/TtdMergeFootprint.ts'; +export { default as TtdMergeInspection } from './src/domain/services/merge/TtdMergeInspection.ts'; +export type { TtdMergeInspectionFields } from './src/domain/services/merge/TtdMergeInspection.ts'; +export type { TtdMergeInspectionDomain } from './src/domain/services/merge/TtdMergeInspectionDomain.ts'; +export { default as TtdMergeInspector } from './src/domain/services/merge/TtdMergeInspector.ts'; export type { - GraphDiffFields, - GraphDiffOptions, - TtdMergeBranchFields, - TtdMergeFootprintFields, - TtdMergeInspectionDomain, - TtdMergeInspectionFields, - TtdMergeLoweringSurface, - TtdMergeLoweringWitnessFields, TtdMergeObjectBranchInput, TtdMergeObjectInspectionInput, - TtdMergeObstructionWitnessFields, - TtdMergePolicyRequirementFields, -} from './legacy.ts'; +} from './src/domain/services/merge/TtdMergeInspector.ts'; +export type { TtdMergeLoweringSurface } from './src/domain/services/merge/TtdMergeLoweringSurface.ts'; +export { default as TtdMergeLoweringWitness } from './src/domain/services/merge/TtdMergeLoweringWitness.ts'; +export type { TtdMergeLoweringWitnessFields } from './src/domain/services/merge/TtdMergeLoweringWitness.ts'; +export { default as TtdMergeObstructionWitness } from './src/domain/services/merge/TtdMergeObstructionWitness.ts'; +export type { TtdMergeObstructionWitnessFields } from './src/domain/services/merge/TtdMergeObstructionWitness.ts'; +export { default as TtdMergePolicyRequirement } from './src/domain/services/merge/TtdMergePolicyRequirement.ts'; +export type { TtdMergePolicyRequirementFields } from './src/domain/services/merge/TtdMergePolicyRequirement.ts'; export { normalizeVisibleStateScope, nodeIdInVisibleStateScope, diff --git a/docs/migrations/v19/README.md b/docs/migrations/v19/README.md index 8cb3891fe..6a033e3c0 100644 --- a/docs/migrations/v19/README.md +++ b/docs/migrations/v19/README.md @@ -3,7 +3,7 @@ This migration plan covers consumers moving from v18 or earlier public surfaces to the planned v19 public API. It is intentionally explicit because v19 is a major-version boundary: root exports become small and application -oriented, while graph-shaped compatibility surfaces move out of root. +oriented, while graph-shaped compatibility surfaces are removed. The migration target is: @@ -18,11 +18,7 @@ Consumers should stop importing graph substrate from package root. Application code should move to: ```typescript -import { - openWarp, - intent, - reading, -} from '@git-stunts/git-warp'; +import { openWarp, intent, reading } from '@git-stunts/git-warp'; ``` Storage adapters move to: @@ -31,37 +27,31 @@ Storage adapters move to: import { GitStorageAdapter } from '@git-stunts/git-warp/storage'; ``` -Compatibility, diagnostics, and expert WARP terms move to explicit subpaths: +Diagnostics and expert WARP terms move to explicit subpaths: ```text -@git-stunts/git-warp/legacy @git-stunts/git-warp/diagnostics @git-stunts/git-warp/advanced ``` ## Subpath Policy -| Subpath | Contract | -| --- | --- | -| Root | first-use public API; no graph substrate | -| `storage` | supported persistence adapters | -| `advanced` | stable formal WARP concepts for expert use | -| `diagnostics` | operator and inspection tools | -| `legacy` | deprecated compatibility only; migration bridge | +| Subpath | Contract | +| ------------- | ------------------------------------------ | +| Root | first-use public API; no graph substrate | +| `storage` | supported persistence adapters | +| `advanced` | stable formal WARP concepts for expert use | +| `diagnostics` | operator and inspection tools | -Do not use `legacy` for new application code. It exists only to let old -consumers upgrade deliberately instead of rewriting everything in one commit. -Every `legacy` import should be treated as debt with a removal plan. +The former `browser` and `legacy` subpaths do not exist in v19. Consumers must +remove those imports before upgrading. ## Root Import Migration Before: ```typescript -import { - GitGraphAdapter, - openWarpWorldline, -} from '@git-stunts/git-warp'; +import { GitGraphAdapter, openWarpWorldline } from '@git-stunts/git-warp'; import GitPlumbing from '@git-stunts/plumbing'; const persistence = new GitGraphAdapter({ @@ -101,9 +91,7 @@ Before: ```typescript await events.commit((patch) => { - patch - .addNode('user:alice') - .setProperty('user:alice', 'role', 'admin'); + patch.addNode('user:alice').setProperty('user:alice', 'role', 'admin'); }); ``` @@ -117,7 +105,7 @@ const receipt = await events.write( subject: 'user:alice', key: 'role', value: 'admin', - }), + }) ); switch (receipt.outcome) { @@ -141,9 +129,7 @@ is `timeline.write(intent)`. Before: ```typescript -const role = await events - .live() - .getNodeProps('user:alice'); +const role = await events.live().getNodeProps('user:alice'); ``` or: @@ -151,11 +137,7 @@ or: ```typescript await events.prepareOpticBasis(); const coordinate = await events.coordinate(); -const role = await coordinate - .optic() - .node('user:alice') - .prop('role') - .read(); +const role = await coordinate.optic().node('user:alice').prop('role').read(); ``` After: @@ -167,7 +149,7 @@ const result = await events.read( reading.property({ subject: 'user:alice', key: 'role', - }), + }) ); result.value; @@ -199,7 +181,7 @@ const historical = await events.at(tick).read( reading.property({ subject: 'user:alice', key: 'role', - }), + }) ); ``` @@ -223,11 +205,13 @@ After: ```typescript const draft = await timeline.draft('try-admin-role'); -await draft.write(intent.property.set({ - subject: 'user:alice', - key: 'role', - value: 'admin', -})); +await draft.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) +); const preview = await timeline.previewJoin(draft, { policy: 'deterministic', @@ -243,30 +227,30 @@ and joins first. ## Symbol Disposition Table -| v18 or earlier symbol | v19 path | Notes | -| --- | --- | --- | -| `openWarpWorldline()` | root `openWarp().timeline(name)` | preferred application opener | -| `WarpWorldline` | root `Timeline` | public handle rename | -| `GitGraphAdapter` | `storage` `GitStorageAdapter` | graph name removed | -| `InMemoryGraphAdapter` | `storage` `MemoryStorageAdapter` | graph name removed | -| `GraphPersistencePort` | root `WarpStorage` for app options; `legacy` for the old port | public storage contract is facade-shaped | -| `commit((patch) => ...)` | `timeline.write(intent.*)` | receipt-returning | -| `PatchBuilder` | deprecated `legacy` | replace with intent builders | -| `PatchSession` | deprecated `legacy` | replace with receipt-returning writes | -| `createNodeAdd()` | deprecated `legacy` | use intent builders | -| `createEdgeAdd()` | deprecated `legacy` | use intent builders | -| `createPropSet()` | deprecated `legacy` | use intent builders | -| `openWarpGraph()` | deprecated `legacy` | replace diagnostics with explicit diagnostic APIs | -| `WarpApp` | deprecated `legacy` | no root default export in v19 | -| `WarpCore` | deprecated `legacy` | replace diagnostics with explicit diagnostic APIs | -| `GraphNode` | deprecated `legacy` | no root export | -| `GraphDiff` | `diagnostics` | operator-facing comparison | -| `Optic` | `advanced` | readings are root | -| `Coordinate` | `advanced` or receipt fields | ticks are root | -| `Observer` | `advanced` | readings are first-use root | -| `Strand` | `advanced` | drafts are first-use root | -| `Braid` | `advanced` | joins are first-use root | -| Continuum evidence nouns | `advanced` or `diagnostics` | receipt evidence stays root-facing | +| v18 or earlier symbol | v19 path | Notes | +| ------------------------ | ---------------------------------- | ------------------------------------------------- | +| `openWarpWorldline()` | root `openWarp().timeline(name)` | preferred application opener | +| `WarpWorldline` | root `Timeline` | public handle rename | +| `GitGraphAdapter` | `storage` `GitStorageAdapter` | graph name removed | +| `InMemoryGraphAdapter` | `storage` `MemoryStorageAdapter` | graph name removed | +| `GraphPersistencePort` | root `WarpStorage` for app options | old graph-shaped port removed from public API | +| `commit((patch) => ...)` | `timeline.write(intent.*)` | receipt-returning | +| `PatchBuilder` | removed | replace with intent builders | +| `PatchSession` | removed | replace with receipt-returning writes | +| `createNodeAdd()` | removed | use intent builders | +| `createEdgeAdd()` | removed | use intent builders | +| `createPropSet()` | removed | use intent builders | +| `openWarpGraph()` | removed | replace diagnostics with explicit diagnostic APIs | +| `WarpApp` | removed | no root default export in v19 | +| `WarpCore` | removed | replace diagnostics with explicit diagnostic APIs | +| `GraphNode` | removed | no public export | +| `GraphDiff` | `diagnostics` | operator-facing comparison | +| `Optic` | `advanced` | readings are root | +| `Coordinate` | `advanced` or receipt fields | ticks are root | +| `Observer` | `advanced` | readings are first-use root | +| `Strand` | `advanced` | drafts are first-use root | +| `Braid` | `advanced` | joins are first-use root | +| Continuum evidence nouns | `advanced` or `diagnostics` | receipt evidence stays root-facing | ## Receipt Outcome Migration @@ -276,11 +260,13 @@ thrown exception. After, consumers should switch on receipt outcomes: ```typescript -const receipt = await timeline.write(intent.property.set({ - subject: 'user:alice', - key: 'role', - value: 'admin', -})); +const receipt = await timeline.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) +); switch (receipt.outcome) { case 'accepted': @@ -318,39 +304,26 @@ classes or operation fields, not in `receipt.outcome`. 5. Move `seek()`/coordinate-first call sites to `tick()` and `at(tick)`. 6. Move diagnostics, materialization, and graph diff code to explicit `diagnostics` APIs. -7. Move remaining graph-shaped code to `legacy` and file follow-up removal - issues. -8. Remove all root imports of graph-shaped symbols. +7. Remove remaining graph-shaped package imports. ## Compatibility Window The v19 line should not keep old and new APIs side by side in root. That would make root a mixed contract again. -Compatibility should be explicit and visibly deprecated: - -```typescript -import { - openWarpGraph, - GitGraphAdapter, -} from '@git-stunts/git-warp/legacy'; -``` - -That import tells reviewers and migration tools that the call site is deprecated -debt and still needs paydown. It is not a second supported product path. +There is no graph-first compatibility subpath in v19. Keep a consumer on v18 +until its graph-shaped imports have been migrated. ## Validation Plan The v19 migration should land with checks that enforce the new boundary: - root export audit rejects graph-shaped public symbols; -- browser export audit mirrors the root first-use API; - consumer typecheck covers `openWarp`, `intent`, `reading`, `Timeline`, `Receipt`, and storage subpath imports; -- legacy typecheck proves old symbols still exist only under deprecated - `legacy`; -- docs reference generator records root, storage, advanced, diagnostics, and - legacy surfaces separately; +- package-boundary tests reject the removed `browser` and `legacy` subpaths; +- docs reference generator records root, storage, advanced, and diagnostics + surfaces separately; - README quick start uses no graph-shaped root import. ## Related Reading diff --git a/docs/topics/api/README.md b/docs/topics/api/README.md index 84715a531..14dfb3141 100644 --- a/docs/topics/api/README.md +++ b/docs/topics/api/README.md @@ -1,6 +1,6 @@ # v19 Public API Reflection -This note records the v19 public API line before implementation. The target is +This note records the implemented v19 public API line. The target is not to make `git-warp` less precise. The target is to stop making first-use application code learn substrate language before it can do useful work. @@ -63,11 +63,7 @@ and writer before they understand the simpler timeline model. The preferred first-use imports are: ```typescript -import { - openWarp, - intent, - reading, -} from '@git-stunts/git-warp'; +import { openWarp, intent, reading } from '@git-stunts/git-warp'; ``` Storage adapters belong behind an explicit storage subpath: @@ -89,8 +85,8 @@ Braid Hologram ``` -Those terms should live in advanced, diagnostics, legacy, or internal surfaces, -not in the first-use root API. +Those terms should live in advanced, diagnostics, or internal surfaces, not in +the first-use root API. ## Root Shape @@ -110,16 +106,18 @@ const warp = await openWarp({ const timeline = await warp.timeline('events'); -await timeline.write(intent.node.add({ - subject: 'user:alice', -})); +await timeline.write( + intent.node.add({ + subject: 'user:alice', + }) +); const write = await timeline.write( intent.property.set({ subject: 'user:alice', key: 'role', value: 'admin', - }), + }) ); switch (write.outcome) { @@ -138,7 +136,7 @@ const role = await timeline.read( reading.property({ subject: 'user:alice', key: 'role', - }), + }) ); console.log(role.value); @@ -149,10 +147,12 @@ The read path should return a result object, not a naked value. Provenance is the normal path, not an afterthought: ```typescript -const result = await timeline.read(reading.property({ - subject: 'user:alice', - key: 'role', -})); +const result = await timeline.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) +); result.value; result.receipt; @@ -216,10 +216,12 @@ The builder output should be a runtime-backed value, not loose shape trust. The public API should lead with `reading`. ```typescript -await timeline.read(reading.property({ - subject: 'user:alice', - key: 'role', -})); +await timeline.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) +); ``` The formal model remains: @@ -301,11 +303,13 @@ Public speculative work reads like this: ```typescript const draft = await timeline.draft('try-admin-role'); -await draft.write(intent.property.set({ - subject: 'user:alice', - key: 'role', - value: 'admin', -})); +await draft.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) +); const preview = await timeline.previewJoin(draft, { policy: 'deterministic', @@ -333,7 +337,7 @@ const roleAtTick = await timeline.at(tick).read( reading.property({ subject: 'user:alice', key: 'role', - }), + }) ); ``` @@ -376,44 +380,42 @@ Use explicit subpaths: @git-stunts/git-warp/storage @git-stunts/git-warp/advanced @git-stunts/git-warp/diagnostics -@git-stunts/git-warp/legacy ``` The boundaries mean different things: -| Surface | Meaning | -| --- | --- | -| Root | first-use product API | -| `storage` | supported persistence adapters | -| `advanced` | formal WARP nouns for expert use | +| Surface | Meaning | +| ------------- | ------------------------------------------------------- | +| Root | first-use product API | +| `storage` | supported persistence adapters | +| `advanced` | formal WARP nouns for expert use | | `diagnostics` | inspection, materialization, replay, and operator tools | -| `legacy` | deprecated compatibility only, with removal pressure | -Do not turn `advanced` into a junk drawer. If a symbol exists only because old -code still imports it, it belongs in deprecated `legacy`, not `advanced`. +Do not turn `advanced` into a junk drawer. Symbols that exist only for removed +graph-first consumers are not part of the v19 package boundary. ## Migration Map Each old root symbol needs one explicit disposition: -| v18 root symbol | v19 disposition | -| --- | --- | -| `openWarpWorldline()` | `openWarp().timeline(name)` | -| `GitGraphAdapter` | `GitStorageAdapter` from `storage` | -| `InMemoryGraphAdapter` | `MemoryStorageAdapter` from `storage` | -| `commit((patch) => ...)` | `timeline.write(intent.*)` | -| `coordinate()` | `tick()` publicly, `Coordinate` in advanced/evidence | -| `optic()` | `timeline.read(reading.*)` or `advanced` | -| `openWarpGraph()` | deprecated `legacy`; replace diagnostics with explicit diagnostic APIs | -| `PatchBuilder` | deprecated `legacy` | -| `GraphDiff` | `diagnostics` | -| graph op creators | deprecated `legacy` only | +| v18 root symbol | v19 disposition | +| ------------------------ | ---------------------------------------------------------- | +| `openWarpWorldline()` | `openWarp().timeline(name)` | +| `GitGraphAdapter` | `GitStorageAdapter` from `storage` | +| `InMemoryGraphAdapter` | `MemoryStorageAdapter` from `storage` | +| `commit((patch) => ...)` | `timeline.write(intent.*)` | +| `coordinate()` | `tick()` publicly, `Coordinate` in advanced/evidence | +| `optic()` | `timeline.read(reading.*)` or `advanced` | +| `openWarpGraph()` | removed; replace diagnostics with explicit diagnostic APIs | +| `PatchBuilder` | removed; use intent builders | +| `GraphDiff` | `diagnostics` | +| graph op creators | removed; use intent builders | The compatibility story should be honest: ```text Root is clean. -Legacy is deprecated and temporary. +Graph-first compatibility exports are gone. Diagnostics are for operators. Advanced is for formal WARP work. ``` diff --git a/docs/topics/getting-started.md b/docs/topics/getting-started.md index dde29b547..72da0efc8 100644 --- a/docs/topics/getting-started.md +++ b/docs/topics/getting-started.md @@ -1,21 +1,7 @@ -# Deprecated v18 Compatibility Walkthrough +# Getting Started -This guide documents the deprecated v18 graph-first compatibility flow. Do not -use it as the starting point for new application code. New work should target -the v19 intent/timeline/reading/receipt API described in -[v19 Public API](api/). - -1. install the package -2. open a worldline -3. write a first patch -4. read current state -5. pin a historical view -6. sync the worldline history through Git refs - -If you already know you want broader builder patterns, jump to -[Querying](querying.md). If you want bounded reads first, jump to -[Optic reads](optic-reads.md). If you want substrate internals, trust, replay, or -performance details, jump to [Git substrate](git-substrate.md). +The v19 application API opens timelines, writes intents, and returns receipts. +It does not expose the graph-first compatibility API from earlier releases. ## Install @@ -23,167 +9,116 @@ performance details, jump to [Git substrate](git-substrate.md). npm install @git-stunts/git-warp @git-stunts/plumbing ``` -## Open a Deprecated Worldline - -This walkthrough uses a collaborative security audit worldline only to show how -existing v18 consumers migrate safely. History matters here because the team -will revise findings over time and later inspect earlier states. +## Open WARP ```typescript -import { GitGraphAdapter, openWarpWorldline } from '@git-stunts/git-warp/legacy'; +import { openWarp } from '@git-stunts/git-warp'; +import { GitStorageAdapter } from '@git-stunts/git-warp/storage'; import GitPlumbing from '@git-stunts/plumbing'; -const plumbing = new GitPlumbing({ cwd: './security-repo' }); -const persistence = new GitGraphAdapter({ plumbing }); - -const audit = await openWarpWorldline({ - persistence, - worldlineName: 'security-audit', - writerId: 'local', +const warp = await openWarp({ + storage: new GitStorageAdapter({ + plumbing: new GitPlumbing({ cwd: './security-repo' }), + }), + writer: 'local', }); -// audit is a frozen Worldline-first handle over "security-audit" -``` - -Use a unique `writerId` per machine or clone in real deployments. The tutorial -uses `local` to keep the example readable, but production graphs should use a -stable unique id such as a hostname, device id, or UUID. - -> **Deprecated compatibility:** `openWarpWorldline()`, `openWarpGraph()`, -> `WarpApp.open()`, and `WarpCore.open()` are migration-only surfaces. New -> application code should not start here. -## Write the first patch - -```typescript -const patch1 = await audit.commit((p) => { - p.addNode('service:auth') - .setProperty('service:auth', 'name', 'Auth service') - .addNode('finding:oauth-state-mismatch') - .setProperty('finding:oauth-state-mismatch', 'title', 'OAuth state mismatch') - .setProperty('finding:oauth-state-mismatch', 'severity', 'critical') - .setProperty('finding:oauth-state-mismatch', 'status', 'open') - .addEdge('finding:oauth-state-mismatch', 'service:auth', 'affects'); -}); -// patch1 = 'abc123...' // patch commit SHA +const audit = await warp.timeline('security-audit'); ``` -`audit.commit(...)` commits once after the callback finishes. It writes one WARP -patch commit under `refs/warp/security-audit/writers/local`; it does not create -a normal source-tree commit on your checked-out branch. - -## See where the graph lives - -Your source tree and your graph history share one Git repository, but they live on different refs. - -```mermaid -flowchart TB - repo["security-repo/.git"] --> main["refs/heads/main
code and docs"] - repo --> warp["refs/warp/security-audit/writers/local
graph patches"] - main --> code["src/
README.md
package.json"] - warp --> p1["P1
lamport=1"] -``` +Use a stable writer identity for each independent clone or process that writes +to the same timeline. -That is the core trick: graph history is stored in Git without taking over normal branch history. - -## Write a second patch +## Write Intents ```typescript -const patch2 = await audit.commit((p) => { - p.setProperty('finding:oauth-state-mismatch', 'severity', 'high') - .setProperty('finding:oauth-state-mismatch', 'status', 'triaged'); -}); -// patch2 = 'def456...' +import { intent } from '@git-stunts/git-warp'; + +await audit.write( + intent.node.add({ + subject: 'finding:oauth-state-mismatch', + }) +); + +const receipt = await audit.write( + intent.property.set({ + subject: 'finding:oauth-state-mismatch', + key: 'severity', + value: 'critical', + }) +); + +console.log(receipt.outcome); +console.log(receipt.patchSha); ``` -Now the live graph says the finding is triaged, but the earlier state still exists in history. +Every call writes one intent and returns a `WriteReceipt`. Treat +`receipt.outcome` as the settlement result instead of treating a returned SHA +as the only success signal. -## Read current state - -The first-use read shape is exact id-only query. When a checkpoint-tail basis is -available, this path uses the bounded exact-read provider and reports a -checkpoint-tail read identity instead of a fake whole-graph hash. Broader -property, wildcard, traversal, and observer reads remain `transitional`; see -[Optic reads](optic-reads.md) before treating them as large-graph safe. +## Read A Bounded Value ```typescript -// Create a live read handle over current worldline history -const worldline = audit.live(); - -const finding = await worldline.query() - .match('finding:oauth-state-mismatch') - .select(['id']) - .run(); -// finding = { -// stateHash: 'checkpoint-tail-query:{...read identity...}', -// nodes: [{ id: 'finding:oauth-state-mismatch' }], -// } +import { reading } from '@git-stunts/git-warp'; + +const severity = await audit.read( + reading.property({ + subject: 'finding:oauth-state-mismatch', + key: 'severity', + }) +); + +const exists = await audit.read( + reading.node.exists({ + subject: 'finding:oauth-state-mismatch', + }) +); + +console.log(severity.value, severity.receipt); +console.log(exists.value, exists.receipt); ``` -## Read earlier history +Readings ask bounded questions. The receipt records how the runtime supported +the answer. -Because WARP state is history-aware, you can pin a historical coordinate and read what the graph looked like before the second patch landed. +## Work In A Draft ```typescript -const beforeTriage = await audit.seek({ - source: { - kind: 'coordinate', - frontier: { local: patch2 }, - ceiling: 1, - }, -}); - -const findingBeforeTriage = await beforeTriage.query() - .match('finding:oauth-state-mismatch') - .select(['id']) - .run(); -// findingBeforeTriage.nodes = [{ id: 'finding:oauth-state-mismatch' }] -``` - -`ceiling: 1` means "show me the graph after the first patch only." The second patch still exists in history, but this pinned worldline ignores it. +const draft = await audit.draft('review-severity'); -## Add a filtered read aperture +await draft.write( + intent.property.set({ + subject: 'finding:oauth-state-mismatch', + key: 'severity', + value: 'high', + }) +); -An `Aperture` defines what is visible. An `Observer` applies that aperture over a worldline. +const preview = await audit.previewJoin(draft); +const joined = await audit.join(draft); -```typescript -const externalAperture = { - match: ['finding:*', 'service:*'], - redact: ['exploitSteps', 'internalNotes'], -}; - -const externalView = await audit.observer('external-review', externalAperture); -// externalView is an Observer handle scoped by the aperture above +console.log(preview.receipt); +console.log(joined.receipt); ``` -## Sync the worldline through Git - -In the common case, your graph travels with Git. The part people miss is that WARP refs are not always covered by default branch refspecs, so show them explicitly while you are learning: +Draft writes stay separate until joined. `previewJoin` and `join` are separate +methods so there is no boolean dry-run mode. -```bash -# Fetch graph refs for this graph explicitly -git fetch origin 'refs/warp/security-audit/*:refs/warp/security-audit/*' - -# Push graph refs for this graph explicitly -git push origin 'refs/warp/security-audit/*:refs/warp/security-audit/*' -``` +## Git Storage -In a real repo, you will usually automate that with Git config or team tooling so you do not type those refspecs by hand forever. +WARP history lives under `refs/warp/**`, separate from source branches such as +`refs/heads/main`. Writing a timeline does not create a source-tree commit on +the checked-out branch. -## What you learned +## Removed Imports -- writes become WARP patch commits under `refs/warp/...` -- source-tree history and graph history stay separate -- `WarpWorldline.commit()` creates and commits patches -- `WarpWorldline.live()` provides live reads, traversal, and query builders -- historical reads are pinned by coordinate, not reconstructed in app code -- Git transports the graph, but you may need explicit WARP refspecs +The `@git-stunts/git-warp/browser` and `@git-stunts/git-warp/legacy` subpaths +are not part of v19. Migrate graph-first consumers before upgrading. -## Next steps +## Next Steps -- [Optic reads](optic-reads.md): bounded read contract and current cost posture -- [Querying](querying.md): builder patterns for Worldlines, observers, and strands -- [Strands](strands.md): durable speculative lanes and braids -- [Git substrate](git-substrate.md): substrate internals, trust, replay, and performance -- [Content and CAS](content-and-cas.md): content attachments and encrypted CAS payloads -- [CLI](cli.md): operator workflows from the terminal -- [Sync](sync.md): explicit WARP refspecs and sync diagnostics +- [v19 Public API](api/) +- [Querying](querying.md) +- [Content and CAS](content-and-cas.md) +- [Git substrate](git-substrate.md) +- [v19 migration guide](../migrations/v19/) diff --git a/docs/topics/git-substrate.md b/docs/topics/git-substrate.md index aad20c5ae..578563b3a 100644 --- a/docs/topics/git-substrate.md +++ b/docs/topics/git-substrate.md @@ -3,19 +3,13 @@ Use this page when you need to understand what lives in Git and why replay is deterministic. -## Deprecated Compatibility Roots +## Package boundary -The v18 graph-first roots are deprecated and migration-only: - -- `openWarpWorldline()` for application code and agent workflows; -- `openWarpGraph()` for diagnostics, sync, checkpoints, provenance, - comparison, strands, migration, and compatibility; -- `WarpApp` and `WarpCore` for legacy facade compatibility and substrate - tooling. - -New application code should not start with these APIs. Use them only to migrate -existing graph-first consumers or to keep old diagnostic tooling running while -it moves to explicit diagnostic surfaces. +The graph composition roots described by older releases are runtime internals in +v19, not deprecated package exports. Application code starts with `openWarp()`; +storage, formal machinery, and operator inspection use the explicit `storage`, +`advanced`, and `diagnostics` subpaths. Keep substrate tooling behind those +supported boundaries instead of importing `src/domain` modules. ## WARP refs diff --git a/docs/topics/observers.md b/docs/topics/observers.md index 135d31dda..ae2431dde 100644 --- a/docs/topics/observers.md +++ b/docs/topics/observers.md @@ -54,4 +54,3 @@ encryption when stored bytes need protection at rest; see - [Optic reads](optic-reads.md) - [Querying](querying.md) - [Content and CAS](content-and-cas.md) -- Example: [`examples/observers.ts`](../../examples/observers.ts) diff --git a/docs/topics/optic-reads.md b/docs/topics/optic-reads.md index 66b3e3c14..23a5fb150 100644 --- a/docs/topics/optic-reads.md +++ b/docs/topics/optic-reads.md @@ -89,4 +89,3 @@ only to the extent that the composed support rule remains bounded. - [Querying](querying.md) - [Observers](observers.md) - [Continuum boundary](continuum-boundary.md) -- Example: [`examples/optics.ts`](../../examples/optics.ts) diff --git a/docs/topics/querying.md b/docs/topics/querying.md index 983a16185..d9729cd80 100644 --- a/docs/topics/querying.md +++ b/docs/topics/querying.md @@ -1,152 +1,90 @@ -# Deprecated v18 Querying Compatibility +# Querying -Use this page only while migrating v18 graph-first consumers. New product code, -agent workflows, and local-first tools should target the v19 -intent/timeline/reading/receipt API. +The v19 root API exposes bounded application readings. It deliberately does +not expose the broad graph query, worldline, or materialization handles from +earlier releases. -The main decision is not which method exists. The main decision is which read -surface owns the question. +## Choose The Surface -## Choose the read surface +| Need | Surface | +| ---------------------------------------------------- | ------------------------------------ | +| Read one known property | `reading.property(...)` | +| Check whether one known node exists | `reading.node.exists(...)` | +| Inspect comparison or visible-state diagnostics | `diagnostics` subpath | +| Work with formal optics, observers, or support plans | `advanced` subpath | +| Run operator-oriented graph commands | `git warp query`, `path`, or `optic` | -| Need | Use | Why | -| --- | --- | --- | -| Write and read live admitted history | `openWarpWorldline()` | Deprecated compatibility surface. | -| Read one known entity or property with bounded evidence | Optic read | Fails closed when the checkpoint-tail basis is unavailable. | -| Filter visibility for a role, tenant, or review surface | Observer | Applies an aperture over a worldline or projection. | -| Ask broader graph questions | Query builder | Supports match, predicates, traversal, selection, aggregation, and support plans. | -| Read an earlier coordinate | `seek(...)` or `graph.query.worldline({ source })` | Pins the read to a coordinate or ceiling. | -| Read speculative work | Strand source | Keeps proposed work out of live truth. | -| Inspect provenance or replay internals | Substrate capability | Diagnostic and operator path. | +The `advanced` and `diagnostics` subpaths expose expert components, not a +second graph-first application facade. -## Open a Deprecated Worldline +## Read A Property ```typescript -import { - GitGraphAdapter, - openWarpGraph, - openWarpWorldline, -} from '@git-stunts/git-warp/legacy'; -import GitPlumbing from '@git-stunts/plumbing'; - -const plumbing = new GitPlumbing({ cwd: './team-repo' }); -const persistence = new GitGraphAdapter({ plumbing }); - -const team = await openWarpWorldline({ - persistence, - worldlineName: 'team', - writerId: 'alice', -}); -``` - -Use `openWarpWorldline()` only while paying down v18 compatibility code. Use -`openWarpGraph()` only when legacy tooling intentionally needs lower-level -capability namespaces such as sync, provenance, checkpoints, comparison, or -strands. - -## Write live truth +import { openWarp, reading } from '@git-stunts/git-warp'; +import { GitStorageAdapter } from '@git-stunts/git-warp/storage'; -```typescript -const patchSha = await team.commit((p) => { - p.addNode('task:auth') - .setProperty('task:auth', 'title', 'Implement OAuth2') - .setProperty('task:auth', 'status', 'in-progress'); +const warp = await openWarp({ + storage: new GitStorageAdapter({ plumbing }), + writer: 'alice', }); -``` - -The callback builds one WARP patch. The patch lands under WARP refs, not under -ordinary source-tree branch refs. +const team = await warp.timeline('team'); -## Read live truth +const status = await team.read( + reading.property({ + subject: 'task:auth', + key: 'status', + }) +); -```typescript -const worldline = team.live(); - -const tasks = await worldline.query() - .match('task:*') - .select(['id', 'props']) - .run(); +console.log(status.value); +console.log(status.receipt); ``` -The query builder supports match predicates, property filters, incoming and -outgoing traversal, selection, aggregation, support planning, and execution. -Broad wildcard and traversal shapes can still have provider caveats, so do not -claim every query is a bounded optic read. - -For exact id-only reads with checkpoint-tail evidence, use -[Optic reads](optic-reads.md). - -## Read through an observer +## Check Node Existence ```typescript -const publicAperture = { - match: ['task:*', 'user:*'], - redact: ['email', 'ssn'], -}; - -const publicView = await worldline.observer('public-users', publicAperture); -const users = await publicView.query().match('user:*').run(); +const task = await team.read( + reading.node.exists({ + subject: 'task:auth', + }) +); + +console.log(task.value); +console.log(task.receipt.outcome); ``` -Observers are read-only filtered views. Redaction changes what a selected read -path returns; it is not encryption. Use [Content and CAS](content-and-cas.md) -when stored bytes need protection at rest. +## Unsupported Root Queries -## Read a historical coordinate +The root does not currently provide wildcard matching, arbitrary traversal, +aggregation, historical coordinate selection, or draft reads. Do not recover +those APIs by importing internal modules. Use the operator CLI or an explicit +expert surface where one exists, and keep application code on readings. -```typescript -const historical = await team.seek({ - source: { - kind: 'coordinate', - frontier: { alice: 'patch-tip-sha' }, - ceiling: 12, - }, -}); +This boundary is intentional: a broad materialization call must not masquerade +as a bounded read. -const taskAtTick12 = await historical.getNodeProps('task:auth'); -``` - -Coordinates pin the read. They do not move Git `HEAD`. +## Diagnostics -## Read a strand +`@git-stunts/git-warp/diagnostics` exports operator-oriented components such as +`GraphDiff`, `QueryBuilder`, `TtdMergeInspector`, and visible-state scope +helpers. Callers are responsible for supplying their required runtime context. -```typescript -const graph = await openWarpGraph({ - persistence, - graphName: 'team', - writerId: 'alice', -}); +## Advanced Read Machinery -const reviewLane = graph.query.worldline({ - source: { kind: 'strand', strandId: 'review-auth' }, -}); - -const reviewTask = await reviewLane.getNodeProps('task:auth'); -``` +`@git-stunts/git-warp/advanced` exports formal concepts such as `Optic`, +`Observer`, `BoundedSupportRule`, and `CausalIndexPlan`. These are useful for +proof-oriented or runtime-integration work; they are not first-use query verbs. -Use [Strands](strands.md) for the full speculative-lane workflow, including -braids, comparison, diagnostic materialization, and transfer planning. - -## Explain a conflict - -When you need to explain why a visible value won, inspect provenance: - -```typescript -const patchShas = await graph.provenance.patchesFor('task:auth'); - -for (const patchSha of patchShas) { - const patch = await graph.provenance.loadPatchBySha(patchSha); - console.log(patchSha, patch.ops.length); -} -``` +## Removed Compatibility Surface -Use this for diagnostics and conflict UX. Do not build a second graph runtime -above provenance output. +The graph-first `openWarpWorldline()` and `openWarpGraph()` package exports are +removed in v19. Keep an unmigrated consumer on v18 until it can use readings, +the CLI, or an explicit expert integration. -## See also +## See Also +- [Getting started](getting-started.md) +- [v19 Public API](api/) - [Optic reads](optic-reads.md) -- [Observers](observers.md) - [Strands](strands.md) -- [Git substrate](git-substrate.md) -- [Sync](sync.md) +- [v19 migration guide](../migrations/v19/) diff --git a/docs/topics/reference.md b/docs/topics/reference.md index a552ee873..95b37d91b 100644 --- a/docs/topics/reference.md +++ b/docs/topics/reference.md @@ -11,20 +11,16 @@ public API export, CLI command, package entrypoint, or public error class. | npm bin | `warp-graph` | `./dist/bin/warp-graph.js` | `package.json#L23` | | npm bin | `git-warp` | `./bin/git-warp` | `package.json#L24` | | npm export | `.` | `types=./dist/index.d.ts; import=./dist/index.js; default=./dist/index.js` | `package.json#L27` | -| npm export | `./browser` | `types=./dist/browser.d.ts; import=./dist/browser.js; default=./dist/browser.js` | `package.json#L32` | -| npm export | `./storage` | `types=./dist/storage.d.ts; import=./dist/storage.js; default=./dist/storage.js` | `package.json#L37` | -| npm export | `./advanced` | `types=./dist/advanced.d.ts; import=./dist/advanced.js; default=./dist/advanced.js` | `package.json#L42` | -| npm export | `./diagnostics` | `types=./dist/diagnostics.d.ts; import=./dist/diagnostics.js; default=./dist/diagnostics.js` | `package.json#L47` | -| npm export | `./legacy` | `types=./dist/legacy.d.ts; import=./dist/legacy.js; default=./dist/legacy.js` | `package.json#L52` | -| npm export | `./sha1sync` | `types=./dist/src/infrastructure/adapters/sha1sync.d.ts; import=./dist/src/infrastructure/adapters/sha1sync.js; default=./dist/src/infrastructure/adapters/sha1sync.js` | `package.json#L57` | -| npm export | `./package.json` | `./package.json` | `package.json#L62` | +| npm export | `./storage` | `types=./dist/storage.d.ts; import=./dist/storage.js; default=./dist/storage.js` | `package.json#L32` | +| npm export | `./advanced` | `types=./dist/advanced.d.ts; import=./dist/advanced.js; default=./dist/advanced.js` | `package.json#L37` | +| npm export | `./diagnostics` | `types=./dist/diagnostics.d.ts; import=./dist/diagnostics.js; default=./dist/diagnostics.js` | `package.json#L42` | +| npm export | `./sha1sync` | `types=./dist/src/infrastructure/adapters/sha1sync.d.ts; import=./dist/src/infrastructure/adapters/sha1sync.js; default=./dist/src/infrastructure/adapters/sha1sync.js` | `package.json#L47` | +| npm export | `./package.json` | `./package.json` | `package.json#L52` | | JSR export | `.` | `./index.ts` | `jsr.json#L8` | -| JSR export | `./browser` | `./browser.ts` | `jsr.json#L9` | -| JSR export | `./storage` | `./storage.ts` | `jsr.json#L10` | -| JSR export | `./advanced` | `./advanced.ts` | `jsr.json#L11` | -| JSR export | `./diagnostics` | `./diagnostics.ts` | `jsr.json#L12` | -| JSR export | `./legacy` | `./legacy.ts` | `jsr.json#L13` | -| JSR export | `./sha1sync` | `./src/infrastructure/adapters/sha1sync.ts` | `jsr.json#L14` | +| JSR export | `./storage` | `./storage.ts` | `jsr.json#L9` | +| JSR export | `./advanced` | `./advanced.ts` | `jsr.json#L10` | +| JSR export | `./diagnostics` | `./diagnostics.ts` | `jsr.json#L11` | +| JSR export | `./sha1sync` | `./src/infrastructure/adapters/sha1sync.ts` | `jsr.json#L12` | ## Root API export surface @@ -34,86 +30,86 @@ First-use product API: `openWarp`, `intent`, `reading`, timelines, and receipts. | Module | Kind | Source | | --- | --- | --- | -| `./src/domain/memory/index.ts` | export * | `index.ts#L55` | +| `./src/domain/memory/index.ts` | export * | `index.ts#L58` | ### Value exports Source: `index.ts`. Count: 73. ```text -AlfredOperationPolicyAdapter @ index.ts#L68 -AuditError @ index.ts#L71 -BunHttpAdapter @ index.ts#L102 -canonicalEmissionJson @ index.ts#L125 -canonicalObservationJson @ index.ts#L126 -CasContentEncryptionPolicy @ index.ts#L62 -checkAborted @ index.ts#L104 -ChunkEffectSink @ index.ts#L136 -ConsoleEffectSink @ index.ts#L135 -ConsoleLogger @ index.ts#L96 -ContinuumArtifactAuthorityError @ index.ts#L72 -createDeliveryObservation @ index.ts#L122 -createEffectEmission @ index.ts#L117 -createExternalizationPolicy @ index.ts#L129 -createTickReceipt @ index.ts#L108 -createTimeoutSignal @ index.ts#L104 -CryptoPort @ index.ts#L98 -DELIVERY_MODES @ index.ts#L118 -DELIVERY_OUTCOMES @ index.ts#L119 -DenoHttpAdapter @ index.ts#L103 -DraftTimeline @ index.ts#L19 -EffectPipeline @ index.ts#L115 -EffectSinkPort @ index.ts#L113 -EncryptionError @ index.ts#L73 -ForkError @ index.ts#L74 -HealthCheckService @ index.ts#L93 -HealthStatus @ index.ts#L93 -HttpServerPort @ index.ts#L99 -IndexError @ index.ts#L75 -INSPECT_LENS @ index.ts#L132 -intent @ index.ts#L22 -Intent @ index.ts#L23 -JoinReceipt @ index.ts#L24 -JoinResult @ index.ts#L25 -LIVE_LENS @ index.ts#L130 -LoggerPort @ index.ts#L94 -LogLevel @ index.ts#L96 -MemoryBudgetError @ index.ts#L76 -MultiplexSink @ index.ts#L114 -NodeCryptoAdapter @ index.ts#L100 -NoOpEffectSink @ index.ts#L134 -NoOpLogger @ index.ts#L95 -NoopOperationPolicyAdapter @ index.ts#L69 -openWarp @ index.ts#L18 -OperationAbortedError @ index.ts#L77 -OperationPolicyExhaustedError @ index.ts#L78 -OperationPolicyPort @ index.ts#L57 -OperationPolicyTimeoutError @ index.ts#L79 -PatchError @ index.ts#L80 -QueryError @ index.ts#L81 -reading @ index.ts#L26 -Reading @ index.ts#L27 -ReadingResult @ index.ts#L28 -ReadReceipt @ index.ts#L29 -REPLAY_LENS @ index.ts#L131 -SchemaUnsupportedError @ index.ts#L82 -ShardCorruptionError @ index.ts#L83 -ShardLoadError @ index.ts#L84 -ShardValidationError @ index.ts#L85 -StorageError @ index.ts#L86 -StrandError @ index.ts#L87 -SyncError @ index.ts#L88 -SyncSecret @ index.ts#L105 -TICK_RECEIPT_OP_TYPES @ index.ts#L109 -TICK_RECEIPT_RESULT_TYPES @ index.ts#L110 -tickReceiptCanonicalJson @ index.ts#L112 -Timeline @ index.ts#L21 -TraversalError @ index.ts#L89 -Warp @ index.ts#L20 -WebCryptoAdapter @ index.ts#L101 -WormholeError @ index.ts#L90 -WriteReceipt @ index.ts#L30 -WriterError @ index.ts#L97 +AlfredOperationPolicyAdapter @ index.ts#L71 +AuditError @ index.ts#L74 +BunHttpAdapter @ index.ts#L108 +canonicalEmissionJson @ index.ts#L129 +canonicalObservationJson @ index.ts#L130 +CasContentEncryptionPolicy @ index.ts#L65 +checkAborted @ index.ts#L110 +ChunkEffectSink @ index.ts#L140 +ConsoleEffectSink @ index.ts#L139 +ConsoleLogger @ index.ts#L102 +ContinuumArtifactAuthorityError @ index.ts#L75 +createDeliveryObservation @ index.ts#L127 +createEffectEmission @ index.ts#L123 +createExternalizationPolicy @ index.ts#L133 +createTickReceipt @ index.ts#L114 +createTimeoutSignal @ index.ts#L110 +CryptoPort @ index.ts#L104 +DELIVERY_MODES @ index.ts#L124 +DELIVERY_OUTCOMES @ index.ts#L125 +DenoHttpAdapter @ index.ts#L109 +DraftTimeline @ index.ts#L18 +EffectPipeline @ index.ts#L121 +EffectSinkPort @ index.ts#L119 +EncryptionError @ index.ts#L76 +ForkError @ index.ts#L77 +HealthCheckService @ index.ts#L97 +HealthStatus @ index.ts#L98 +HttpServerPort @ index.ts#L105 +IndexError @ index.ts#L78 +INSPECT_LENS @ index.ts#L136 +intent @ index.ts#L21 +Intent @ index.ts#L22 +JoinReceipt @ index.ts#L23 +JoinResult @ index.ts#L24 +LIVE_LENS @ index.ts#L134 +LoggerPort @ index.ts#L100 +LogLevel @ index.ts#L102 +MemoryBudgetError @ index.ts#L79 +MultiplexSink @ index.ts#L120 +NodeCryptoAdapter @ index.ts#L106 +NoOpEffectSink @ index.ts#L138 +NoOpLogger @ index.ts#L101 +NoopOperationPolicyAdapter @ index.ts#L72 +openWarp @ index.ts#L17 +OperationAbortedError @ index.ts#L80 +OperationPolicyExhaustedError @ index.ts#L81 +OperationPolicyPort @ index.ts#L60 +OperationPolicyTimeoutError @ index.ts#L82 +PatchError @ index.ts#L83 +QueryError @ index.ts#L84 +reading @ index.ts#L25 +Reading @ index.ts#L26 +ReadingResult @ index.ts#L27 +ReadReceipt @ index.ts#L28 +REPLAY_LENS @ index.ts#L135 +SchemaUnsupportedError @ index.ts#L85 +ShardCorruptionError @ index.ts#L86 +ShardLoadError @ index.ts#L87 +ShardValidationError @ index.ts#L88 +StorageError @ index.ts#L89 +StrandError @ index.ts#L90 +SyncError @ index.ts#L91 +SyncSecret @ index.ts#L111 +TICK_RECEIPT_OP_TYPES @ index.ts#L115 +TICK_RECEIPT_RESULT_TYPES @ index.ts#L116 +tickReceiptCanonicalJson @ index.ts#L118 +Timeline @ index.ts#L20 +TraversalError @ index.ts#L92 +Warp @ index.ts#L19 +WebCryptoAdapter @ index.ts#L107 +WormholeError @ index.ts#L93 +WriteReceipt @ index.ts#L29 +WriterError @ index.ts#L103 ``` ### Type exports @@ -121,38 +117,38 @@ WriterError @ index.ts#L97 Source: `index.ts`. Count: 32. ```text -CasContentEncryptionDiagnostics @ index.ts#L64 -CasContentEncryptionScheme @ index.ts#L65 -CasResolvedVaultKeyOptions @ index.ts#L66 -EdgeIntentFields @ index.ts#L33 -EdgePropertyIntentFields @ index.ts#L34 -IntentBuilders @ index.ts#L40 -IntentDescriptor @ index.ts#L35 -IntentKind @ index.ts#L36 +CasContentEncryptionDiagnostics @ index.ts#L67 +CasContentEncryptionScheme @ index.ts#L68 +CasResolvedVaultKeyOptions @ index.ts#L69 +EdgeIntentFields @ index.ts#L32 +EdgePropertyIntentFields @ index.ts#L33 +IntentBuilders @ index.ts#L39 +IntentDescriptor @ index.ts#L34 +IntentKind @ index.ts#L35 JoinMode @ index.ts#L41 -JoinOptions @ index.ts#L43 -JoinPolicy @ index.ts#L43 -JoinReceiptOptions @ index.ts#L41 -JoinReceiptOutcome @ index.ts#L41 -JoinResultOptions @ index.ts#L42 -NodeIntentFields @ index.ts#L37 -NodeReadingFields @ index.ts#L45 -OpenWarpOptions @ index.ts#L31 -OperationPolicyExecuteOptions @ index.ts#L59 -OperationRetryDecision @ index.ts#L60 -PropertyIntentFields @ index.ts#L38 -PropertyReadingFields @ index.ts#L46 -ReadingBuilders @ index.ts#L50 -ReadingDescriptor @ index.ts#L47 -ReadingKind @ index.ts#L48 -ReadingResultOptions @ index.ts#L51 -ReadingValue @ index.ts#L51 -ReadReceiptOptions @ index.ts#L52 -ReadReceiptOutcome @ index.ts#L52 -ReceiptOutcome @ index.ts#L53 -SyncRateLimitConfig @ index.ts#L106 -WarpStorage @ index.ts#L31 -WriteReceiptOptions @ index.ts#L53 +JoinOptions @ index.ts#L46 +JoinPolicy @ index.ts#L46 +JoinReceiptOptions @ index.ts#L42 +JoinReceiptOutcome @ index.ts#L43 +JoinResultOptions @ index.ts#L45 +NodeIntentFields @ index.ts#L36 +NodeReadingFields @ index.ts#L48 +OpenWarpOptions @ index.ts#L30 +OperationPolicyExecuteOptions @ index.ts#L62 +OperationRetryDecision @ index.ts#L63 +PropertyIntentFields @ index.ts#L37 +PropertyReadingFields @ index.ts#L49 +ReadingBuilders @ index.ts#L53 +ReadingDescriptor @ index.ts#L50 +ReadingKind @ index.ts#L51 +ReadingResultOptions @ index.ts#L54 +ReadingValue @ index.ts#L54 +ReadReceiptOptions @ index.ts#L55 +ReadReceiptOutcome @ index.ts#L55 +ReceiptOutcome @ index.ts#L56 +SyncRateLimitConfig @ index.ts#L112 +WarpStorage @ index.ts#L30 +WriteReceiptOptions @ index.ts#L56 ``` ## Storage export surface @@ -192,44 +188,44 @@ Formal WARP and Continuum concepts for expert use; not first-use root API. | Module | Kind | Source | | --- | --- | --- | -| `./src/continuumExports.ts` | export * | `advanced.ts#L42` | +| `./src/continuumExports.ts` | export * | `advanced.ts#L80` | ### Value exports Source: `advanced.ts`. Count: 31. ```text -BoundedSupportRule @ advanced.ts#L10 -CausalIndexPlan @ advanced.ts#L11 -composeWormholes @ advanced.ts#L12 -createWormhole @ advanced.ts#L13 -deserializeWormhole @ advanced.ts#L14 -LiveSelector @ advanced.ts#L15 -Observer @ advanced.ts#L16 -ObserverAccumulation @ advanced.ts#L17 -ObserverBasis @ advanced.ts#L18 -ObserverEmission @ advanced.ts#L19 -ObserverPlan @ advanced.ts#L20 -ObserverReadingEnvelope @ advanced.ts#L21 -openAperture @ advanced.ts#L22 -Optic @ advanced.ts#L23 -OpticAperturePosture @ advanced.ts#L24 -OpticBasisPosture @ advanced.ts#L25 -OpticCoordinatePosture @ advanced.ts#L26 -OpticSupportRule @ advanced.ts#L27 -ProjectionHandle @ advanced.ts#L28 -RejectedZKWormhole @ advanced.ts#L29 -replayWormhole @ advanced.ts#L30 -serializeWormhole @ advanced.ts#L31 -StrandSelector @ advanced.ts#L32 -SupportFragmentPlan @ advanced.ts#L33 -VerifiedZKWormhole @ advanced.ts#L34 -verifyZKWormhole @ advanced.ts#L35 -WarpWorldlineCoordinate @ advanced.ts#L36 -WarpWorldlineOpticBasis @ advanced.ts#L37 -WorldlineSelector @ advanced.ts#L38 -ZKWormholeEdge @ advanced.ts#L39 -ZKWormholeProofVerifierPort @ advanced.ts#L40 +BoundedSupportRule @ advanced.ts#L9 +CausalIndexPlan @ advanced.ts#L16 +composeWormholes @ advanced.ts#L23 +createWormhole @ advanced.ts#L24 +deserializeWormhole @ advanced.ts#L25 +LiveSelector @ advanced.ts#L29 +Observer @ advanced.ts#L30 +ObserverAccumulation @ advanced.ts#L31 +ObserverBasis @ advanced.ts#L32 +ObserverEmission @ advanced.ts#L33 +ObserverPlan @ advanced.ts#L34 +ObserverReadingEnvelope @ advanced.ts#L36 +openAperture @ advanced.ts#L42 +Optic @ advanced.ts#L45 +OpticAperturePosture @ advanced.ts#L51 +OpticBasisPosture @ advanced.ts#L53 +OpticCoordinatePosture @ advanced.ts#L55 +OpticSupportRule @ advanced.ts#L57 +ProjectionHandle @ advanced.ts#L59 +RejectedZKWormhole @ advanced.ts#L60 +replayWormhole @ advanced.ts#L26 +serializeWormhole @ advanced.ts#L27 +StrandSelector @ advanced.ts#L61 +SupportFragmentPlan @ advanced.ts#L62 +VerifiedZKWormhole @ advanced.ts#L67 +verifyZKWormhole @ advanced.ts#L43 +WarpWorldlineCoordinate @ advanced.ts#L68 +WarpWorldlineOpticBasis @ advanced.ts#L70 +WorldlineSelector @ advanced.ts#L71 +ZKWormholeEdge @ advanced.ts#L72 +ZKWormholeProofVerifierPort @ advanced.ts#L78 ``` ### Type exports @@ -237,31 +233,31 @@ ZKWormholeProofVerifierPort @ advanced.ts#L40 Source: `advanced.ts`. Count: 25. ```text -Aperture @ advanced.ts#L44 -ApertureOpeningVerificationResult @ advanced.ts#L45 -BoundedSupportDirection @ advanced.ts#L46 -BoundedSupportKind @ advanced.ts#L47 -BoundedSupportRuleFields @ advanced.ts#L48 -BoundedSupportSurface @ advanced.ts#L49 -CausalIndexFamily @ advanced.ts#L50 -CausalIndexPlanFields @ advanced.ts#L51 -CausalIndexPlanPosture @ advanced.ts#L52 -ObserverConfig @ advanced.ts#L53 -ObserverPlanFields @ advanced.ts#L54 -ObserverReadingEnvelopeBudget @ advanced.ts#L55 -ObserverReadingEnvelopeFields @ advanced.ts#L56 -OpticAperturePostureValue @ advanced.ts#L57 -OpticBasisPostureValue @ advanced.ts#L58 -OpticContextValue @ advanced.ts#L59 -OpticCoordinatePostureValue @ advanced.ts#L60 -OpticFields @ advanced.ts#L61 -OpticPostureFields @ advanced.ts#L62 -OpticSupportRuleValue @ advanced.ts#L63 +Aperture @ advanced.ts#L79 +ApertureOpeningVerificationResult @ advanced.ts#L75 +BoundedSupportDirection @ advanced.ts#L11 +BoundedSupportKind @ advanced.ts#L12 +BoundedSupportRuleFields @ advanced.ts#L13 +BoundedSupportSurface @ advanced.ts#L14 +CausalIndexFamily @ advanced.ts#L18 +CausalIndexPlanFields @ advanced.ts#L19 +CausalIndexPlanPosture @ advanced.ts#L20 +ObserverConfig @ advanced.ts#L79 +ObserverPlanFields @ advanced.ts#L35 +ObserverReadingEnvelopeBudget @ advanced.ts#L38 +ObserverReadingEnvelopeFields @ advanced.ts#L39 +OpticAperturePostureValue @ advanced.ts#L52 +OpticBasisPostureValue @ advanced.ts#L54 +OpticContextValue @ advanced.ts#L47 +OpticCoordinatePostureValue @ advanced.ts#L56 +OpticFields @ advanced.ts#L48 +OpticPostureFields @ advanced.ts#L49 +OpticSupportRuleValue @ advanced.ts#L58 SupportFragmentMaterializationPosture @ advanced.ts#L64 SupportFragmentPlanFields @ advanced.ts#L65 -WarpWorldlineCoordinateFrontierEntry @ advanced.ts#L66 -ZKWormholeEdgeFields @ advanced.ts#L67 -ZKWormholeVerificationResult @ advanced.ts#L68 +WarpWorldlineCoordinateFrontierEntry @ advanced.ts#L69 +ZKWormholeEdgeFields @ advanced.ts#L73 +ZKWormholeVerificationResult @ advanced.ts#L76 ``` ## Diagnostics export surface @@ -273,24 +269,24 @@ Operator, inspection, comparison, and replay tools. Source: `diagnostics.ts`. Count: 18. ```text -BisectService @ diagnostics.ts#L9 -CommitDagTraversalService @ diagnostics.ts#L10 -ContentAttachmentProjection @ diagnostics.ts#L11 +BisectService @ diagnostics.ts#L8 +CommitDagTraversalService @ diagnostics.ts#L9 +ContentAttachmentProjection @ diagnostics.ts#L10 exportCoordinateComparisonFact @ diagnostics.ts#L12 exportCoordinateTransferPlanFact @ diagnostics.ts#L13 -GraphDiff @ diagnostics.ts#L14 -GraphOpAlgebraProjection @ diagnostics.ts#L15 +GraphDiff @ diagnostics.ts#L15 +GraphOpAlgebraProjection @ diagnostics.ts#L18 nodeIdInVisibleStateScope @ diagnostics.ts#L41 normalizeVisibleStateScope @ diagnostics.ts#L40 -QueryBuilder @ diagnostics.ts#L16 +QueryBuilder @ diagnostics.ts#L19 scopeMaterializedState @ diagnostics.ts#L42 -TtdMergeBranch @ diagnostics.ts#L17 -TtdMergeFootprint @ diagnostics.ts#L18 -TtdMergeInspection @ diagnostics.ts#L19 -TtdMergeInspector @ diagnostics.ts#L20 -TtdMergeLoweringWitness @ diagnostics.ts#L21 -TtdMergeObstructionWitness @ diagnostics.ts#L22 -TtdMergePolicyRequirement @ diagnostics.ts#L23 +TtdMergeBranch @ diagnostics.ts#L20 +TtdMergeFootprint @ diagnostics.ts#L22 +TtdMergeInspection @ diagnostics.ts#L24 +TtdMergeInspector @ diagnostics.ts#L27 +TtdMergeLoweringWitness @ diagnostics.ts#L33 +TtdMergeObstructionWitness @ diagnostics.ts#L35 +TtdMergePolicyRequirement @ diagnostics.ts#L37 ``` ### Type exports @@ -298,260 +294,22 @@ TtdMergePolicyRequirement @ diagnostics.ts#L23 Source: `diagnostics.ts`. Count: 14. ```text -GraphDiffFields @ diagnostics.ts#L26 -GraphDiffOptions @ diagnostics.ts#L27 -TtdMergeBranchFields @ diagnostics.ts#L28 -TtdMergeFootprintFields @ diagnostics.ts#L29 -TtdMergeInspectionDomain @ diagnostics.ts#L30 -TtdMergeInspectionFields @ diagnostics.ts#L31 +GraphDiffFields @ diagnostics.ts#L16 +GraphDiffOptions @ diagnostics.ts#L17 +TtdMergeBranchFields @ diagnostics.ts#L21 +TtdMergeFootprintFields @ diagnostics.ts#L23 +TtdMergeInspectionDomain @ diagnostics.ts#L26 +TtdMergeInspectionFields @ diagnostics.ts#L25 TtdMergeLoweringSurface @ diagnostics.ts#L32 -TtdMergeLoweringWitnessFields @ diagnostics.ts#L33 -TtdMergeObjectBranchInput @ diagnostics.ts#L34 -TtdMergeObjectInspectionInput @ diagnostics.ts#L35 +TtdMergeLoweringWitnessFields @ diagnostics.ts#L34 +TtdMergeObjectBranchInput @ diagnostics.ts#L29 +TtdMergeObjectInspectionInput @ diagnostics.ts#L30 TtdMergeObstructionWitnessFields @ diagnostics.ts#L36 -TtdMergePolicyRequirementFields @ diagnostics.ts#L37 +TtdMergePolicyRequirementFields @ diagnostics.ts#L38 VisibleStateScope @ diagnostics.ts#L45 VisibleStateScopePrefixFilter @ diagnostics.ts#L46 ``` -## Legacy export surface - -Deprecated compatibility-only imports for migration paydown. - -### Export modules - -| Module | Kind | Source | -| --- | --- | --- | -| `./src/domain/graph/publicGraphSubstrate.ts` | export * | `legacy.ts#L222` | -| `./src/domain/memory/index.ts` | export * | `legacy.ts#L223` | -| `./src/continuumExports.ts` | export * | `legacy.ts#L224` | - -### Value exports - -Source: `legacy.ts`. Count: 161. - -```text -AlfredOperationPolicyAdapter @ legacy.ts#L229 -ApertureOpeningProof @ legacy.ts#L408 -AuditError @ legacy.ts#L233 -BisectService @ legacy.ts#L274 -BitmapIndexBuilder @ legacy.ts#L268 -BitmapIndexReader @ legacy.ts#L269 -BlobStoragePort @ legacy.ts#L288 -BoundedSupportRule @ legacy.ts#L331 -BTR @ legacy.ts#L397 -buildWarpStateIndex @ legacy.ts#L364 -BunHttpAdapter @ legacy.ts#L298 -canonicalEmissionJson @ legacy.ts#L417 -canonicalObservationJson @ legacy.ts#L419 -CasContentEncryptionPolicy @ legacy.ts#L227 -CausalIndexPlan @ legacy.ts#L332 -checkAborted @ legacy.ts#L305 -ChunkEffectSink @ legacy.ts#L428 -CommitDagTraversalService @ legacy.ts#L273 -compareVisibleState @ legacy.ts#L368 -composeWormholes @ legacy.ts#L404 -computeStateHash @ legacy.ts#L365 -computeTranslationCost @ legacy.ts#L345 -ConsoleEffectSink @ legacy.ts#L427 -ConsoleLogger @ legacy.ts#L281 -CONTENT_PROPERTY_KEY @ legacy.ts#L360 -ContentAttachmentProjection @ legacy.ts#L430 -ContinuumArtifactAuthorityError @ legacy.ts#L234 -CoordinateSelector @ legacy.ts#L329 -createBlobValue @ legacy.ts#L354 -createBTR @ legacy.ts#L398 -createDeliveryObservation @ legacy.ts#L418 -createEdgeAdd @ legacy.ts#L350 -createEdgeTombstone @ legacy.ts#L351 -createEffectEmission @ legacy.ts#L416 -createExternalizationPolicy @ legacy.ts#L420 -createInlineValue @ legacy.ts#L353 -createNodeAdd @ legacy.ts#L348 -createNodeTombstone @ legacy.ts#L349 -createPropSet @ legacy.ts#L352 -createStateReader @ legacy.ts#L367 -createTickReceipt @ legacy.ts#L388 -createTimeoutSignal @ legacy.ts#L306 -createV18BoundedMemoryCapabilityReport @ legacy.ts#L385 -createWormhole @ legacy.ts#L403 -CryptoPort @ legacy.ts#L290 -decodeEdgePropKey @ legacy.ts#L358 -DELIVERY_MODES @ legacy.ts#L421 -DELIVERY_OUTCOMES @ legacy.ts#L422 -DenoHttpAdapter @ legacy.ts#L299 -deserializeWormhole @ legacy.ts#L407 -EffectPipeline @ legacy.ts#L415 -EffectSinkPort @ legacy.ts#L413 -encodeEdgePropKey @ legacy.ts#L357 -EncryptionError @ legacy.ts#L235 -exportCoordinateComparisonFact @ legacy.ts#L383 -exportCoordinateTransferPlanFact @ legacy.ts#L384 -ForkError @ legacy.ts#L236 -GitGraphAdapter @ legacy.ts#L264 -GraphDiff @ legacy.ts#L369 -GraphNode @ legacy.ts#L266 -GraphOpAlgebraProjection @ legacy.ts#L267 -GraphPersistencePort @ legacy.ts#L275 -HealthCheckService @ legacy.ts#L271 -HealthStatus @ legacy.ts#L272 -HttpServerPort @ legacy.ts#L291 -ImmutableBytes @ legacy.ts#L377 -IndexError @ legacy.ts#L237 -IndexRebuildService @ legacy.ts#L270 -IndexStoragePort @ legacy.ts#L276 -InMemoryBlobStorageAdapter @ legacy.ts#L289 -InMemoryGraphAdapter @ legacy.ts#L265 -INSPECT_LENS @ legacy.ts#L425 -isEdgePropKey @ legacy.ts#L359 -LIVE_LENS @ legacy.ts#L423 -LiveSelector @ legacy.ts#L328 -LoggerPort @ legacy.ts#L279 -LogLevel @ legacy.ts#L282 -MemoryBudgetError @ legacy.ts#L238 -MultiplexSink @ legacy.ts#L414 -NodeCryptoAdapter @ legacy.ts#L294 -NoOpEffectSink @ legacy.ts#L426 -NoOpLogger @ legacy.ts#L280 -NoopOperationPolicyAdapter @ legacy.ts#L230 -normalizeVisibleStateScope @ legacy.ts#L381 -Observer @ legacy.ts#L335 -ObserverAccumulation @ legacy.ts#L336 -ObserverBasis @ legacy.ts#L337 -ObserverEmission @ legacy.ts#L338 -ObserverPlan @ legacy.ts#L339 -ObserverReadingEnvelope @ legacy.ts#L340 -openAperture @ legacy.ts#L410 -openWarpGraph @ legacy.ts#L310 -openWarpWorldline @ legacy.ts#L313 -OperationAbortedError @ legacy.ts#L239 -OperationPolicyExhaustedError @ legacy.ts#L231 -OperationPolicyPort @ legacy.ts#L225 -OperationPolicyTimeoutError @ legacy.ts#L231 -Optic @ legacy.ts#L317 -OpticAperturePosture @ legacy.ts#L318 -OpticBasisPosture @ legacy.ts#L319 -OpticCoordinatePosture @ legacy.ts#L320 -OpticSupportRule @ legacy.ts#L321 -PatchBuilder @ legacy.ts#L341 -PatchError @ legacy.ts#L240 -PatchSession @ legacy.ts#L342 -ProjectionHandle @ legacy.ts#L322 -projectState @ legacy.ts#L366 -ProvenanceIndex @ legacy.ts#L344 -ProvenancePayload @ legacy.ts#L394 -QueryBuilder @ legacy.ts#L334 -QueryError @ legacy.ts#L241 -RejectedApertureOpening @ legacy.ts#L408 -RejectedZKWormhole @ legacy.ts#L408 -REPLAY_LENS @ legacy.ts#L424 -replayBTR @ legacy.ts#L400 -replayWormhole @ legacy.ts#L405 -SchemaUnsupportedError @ legacy.ts#L242 -scopeMaterializedState @ legacy.ts#L382 -SeekCachePort @ legacy.ts#L285 -serializeWormhole @ legacy.ts#L406 -ShardCorruptionError @ legacy.ts#L243 -ShardLoadError @ legacy.ts#L244 -ShardValidationError @ legacy.ts#L245 -SnapshotORSet @ legacy.ts#L378 -SnapshotVersionVector @ legacy.ts#L379 -SnapshotWarpState @ legacy.ts#L380 -StorageError @ legacy.ts#L246 -StrandError @ legacy.ts#L247 -StrandSelector @ legacy.ts#L330 -SupportFragmentPlan @ legacy.ts#L333 -SyncError @ legacy.ts#L248 -SyncSecret @ legacy.ts#L429 -TICK_RECEIPT_OP_TYPES @ legacy.ts#L390 -TICK_RECEIPT_RESULT_TYPES @ legacy.ts#L391 -tickReceiptCanonicalJson @ legacy.ts#L389 -TraversalError @ legacy.ts#L249 -TtdMergeBranch @ legacy.ts#L370 -TtdMergeFootprint @ legacy.ts#L371 -TtdMergeInspection @ legacy.ts#L372 -TtdMergeInspector @ legacy.ts#L373 -TtdMergeLoweringWitness @ legacy.ts#L374 -TtdMergeObstructionWitness @ legacy.ts#L375 -TtdMergePolicyRequirement @ legacy.ts#L376 -VerifiedApertureOpening @ legacy.ts#L409 -VerifiedZKWormhole @ legacy.ts#L409 -verifyBTR @ legacy.ts#L399 -verifyZKWormhole @ legacy.ts#L410 -WarpApp @ legacy.ts#L325 -WarpCore @ legacy.ts#L326 -WarpOpenOptions @ legacy.ts#L309 -WarpStateIndexBuilder @ legacy.ts#L363 -WarpWorldline @ legacy.ts#L314 -WarpWorldlineCoordinate @ legacy.ts#L315 -WarpWorldlineOpticBasis @ legacy.ts#L316 -WebCryptoAdapter @ legacy.ts#L295 -WorldlineSelector @ legacy.ts#L327 -WormholeError @ legacy.ts#L250 -Writer @ legacy.ts#L343 -WriterError @ legacy.ts#L302 -ZKWormholeEdge @ legacy.ts#L409 -ZKWormholeProofVerifierPort @ legacy.ts#L410 -``` - -### Type exports - -Source: `legacy.ts`. Count: 51. - -```text -Aperture @ legacy.ts#L434 -ApertureOpeningProofFields @ legacy.ts#L463 -ApertureOpeningVerificationResult @ legacy.ts#L463 -BoundedSupportDirection @ legacy.ts#L449 -BoundedSupportKind @ legacy.ts#L450 -BoundedSupportRuleFields @ legacy.ts#L451 -BoundedSupportSurface @ legacy.ts#L452 -CasContentEncryptionDiagnostics @ legacy.ts#L228 -CasContentEncryptionScheme @ legacy.ts#L228 -CasResolvedVaultKeyOptions @ legacy.ts#L228 -CasVaultResolutionWitness @ legacy.ts#L228 -CausalIndexFamily @ legacy.ts#L453 -CausalIndexPlanFields @ legacy.ts#L454 -CausalIndexPlanPosture @ legacy.ts#L455 -GraphDiffFields @ legacy.ts#L459 -GraphDiffOptions @ legacy.ts#L458 -ObserverConfig @ legacy.ts#L435 -ObserverPlanFields @ legacy.ts#L460 -ObserverReadingEnvelopeBudget @ legacy.ts#L461 -ObserverReadingEnvelopeFields @ legacy.ts#L462 -OperationPolicyExecuteOptions @ legacy.ts#L226 -OperationRetryDecision @ legacy.ts#L226 -OperationRetryObserver @ legacy.ts#L226 -OpticAperturePostureValue @ legacy.ts#L442 -OpticBasisPostureValue @ legacy.ts#L443 -OpticContextValue @ legacy.ts#L444 -OpticCoordinatePostureValue @ legacy.ts#L445 -OpticFields @ legacy.ts#L446 -OpticPostureFields @ legacy.ts#L447 -OpticSupportRuleValue @ legacy.ts#L448 -PropValue @ legacy.ts#L436 -SnapshotPropValue @ legacy.ts#L437 -SupportFragmentMaterializationPosture @ legacy.ts#L456 -SupportFragmentPlanFields @ legacy.ts#L457 -SyncRateLimitConfig @ legacy.ts#L438 -TtdMergeBranchFields @ legacy.ts#L466 -TtdMergeFootprintFields @ legacy.ts#L467 -TtdMergeInspectionDomain @ legacy.ts#L468 -TtdMergeInspectionFields @ legacy.ts#L469 -TtdMergeLoweringSurface @ legacy.ts#L470 -TtdMergeLoweringWitnessFields @ legacy.ts#L471 -TtdMergeObjectBranchInput @ legacy.ts#L472 -TtdMergeObjectInspectionInput @ legacy.ts#L473 -TtdMergeObstructionWitnessFields @ legacy.ts#L474 -TtdMergePolicyRequirementFields @ legacy.ts#L475 -WarpKernelPort @ legacy.ts#L439 -WarpWorldlineCoordinateFrontierEntry @ legacy.ts#L465 -WarpWorldlineOpenOptions @ legacy.ts#L440 -WarpWorldlinePatchBuild @ legacy.ts#L441 -ZKWormholeEdgeFields @ legacy.ts#L464 -ZKWormholeVerificationResult @ legacy.ts#L464 -``` - ## CLI command registry | Command | Handler | Source | diff --git a/docs/topics/strands.md b/docs/topics/strands.md index fc7dc1522..2d218f7a7 100644 --- a/docs/topics/strands.md +++ b/docs/topics/strands.md @@ -1,98 +1,26 @@ # Strands -This page documents deprecated v18 strand controls. Use it for migration and -diagnostic context, not as a first-use public API guide. +Strands are currently an operator workflow exposed through the CLI. The v19 +package does not publish the former graph-first strand capability bag. A strand is not a Git branch or worktree. It is a WARP coordinate plus an overlay patch log. That makes it useful for review lanes, proposed changes, experiments, and agent work that should remain outside admitted live truth. -## Create a speculative lane +## Strand model -```typescript -const graph = await openWarpGraph({ - persistence, - graphName: 'team', - writerId: 'alice', -}); - -const strand = await graph.strands.createStrand({ - strandId: 'review-auth', - owner: 'alice', - scope: 'OAuth review', -}); -``` - -Use `openWarpGraph()` for legacy strand controls only while migrating. New -application reads and writes should not start with `openWarpWorldline()`. - -## Patch a strand - -```typescript -await graph.strands.patchStrand('review-auth', (p) => { - p.setProperty('task:auth', 'status', 'ready-for-review'); -}); -``` - -The patch lands on the strand overlay, not the live writer chain. - -## Read a strand - -Read a strand through the same projection model as other pinned sources: - -```typescript -const reviewLane = graph.query.worldline({ - source: { kind: 'strand', strandId: 'review-auth' }, -}); - -const reviewTask = await reviewLane.getNodeProps('task:auth'); -``` - -This keeps application reads on a projection handle instead of requiring custom -strand replay code in the caller. - -## Braid strands - -Braids let one strand read support overlays from other strands. Use them when a -review lane needs to see related speculative work without collapsing it into -live truth. - -```typescript -await graph.strands.braidStrand('review-auth', { - braidedStrandIds: ['peer-review'], - writable: true, -}); -``` +A strand patch lands on the strand overlay, not the live writer chain. Braids +let one strand read support overlays from other strands when a review lane needs +related speculative work without collapsing it into live truth. The current implementation records pinned braid support overlays. It is real runtime behavior, but common-basis braid validation and live holographic braid realization remain future architecture. Keep docs precise: shipped braids are pinned overlays, not a general distributed merge protocol. -## Compare and transfer - -Use comparison when a strand needs review before transfer: - -```typescript -const diff = await graph.comparison.diff({ - from: { kind: 'strand', strandId: 'review-auth' }, - to: 'live', -}); -``` - Transfer planning belongs with comparison and governance workflows. Do not -model strand transfer as a Git branch merge. - -## Inspect diagnostically - -`materializeStrand()` is an inspection primitive: - -```typescript -const state = await graph.strands.materializeStrand('review-auth'); -``` - -Use it for diagnostics, receipts, and review evidence. Do not make -materialized strand state the normal application read path. +model strand transfer as a Git branch merge. Materialization is for diagnostics, +receipts, and review evidence, not the normal application read path. ## CLI workflow @@ -106,6 +34,10 @@ git warp strand transfer-plan review-auth --repo ./team-repo --into live git warp strand drop review-auth --repo ./team-repo ``` +There is intentionally no equivalent v19 TypeScript example. Importing +`src/domain/WarpGraph.ts` would couple application code to an internal +composition root that can change without package-level compatibility promises. + ## See also - [Querying](querying.md) diff --git a/docs/topics/sync.md b/docs/topics/sync.md index b22895f22..557153527 100644 --- a/docs/topics/sync.md +++ b/docs/topics/sync.md @@ -22,23 +22,10 @@ operators do not rely on memory. ## Programmatic sync -Deprecated operator code can use the lower-level graph capability bag when it -intentionally needs sync status, requests, or serve endpoints: - -```typescript -const graph = await openWarpGraph({ - persistence, - graphName: 'team', - writerId: 'alice', -}); - -const status = await graph.sync.status(); -const request = await graph.sync.createSyncRequest(); -``` - -Do not use `openWarpWorldline()` for new application writes and reads. Reach for -`graph.sync` only when maintaining legacy transport, governance, or diagnostic -tooling. +The v19 package does not expose the former graph capability bag. There is no +supported programmatic sync surface yet. Use the CLI for operator workflows +instead of importing runtime internals. A future embedded sync API must earn an +explicit package boundary of its own. ## CLI sync diff --git a/eslint.config.ts b/eslint.config.ts index 2049a2954..39554dd85 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -49,7 +49,6 @@ export default tseslint.config( "packages/**/node_modules/**", "coverage/**", "dist/**", - "examples/html/assets/**", "src/infrastructure/adapters/wesley/generated/**", "scripts/**", ".claude/**", @@ -817,42 +816,4 @@ export default tseslint.config( }, }, - // ── Example scripts ──────────────────────────────────────────────────────── - { - files: ["examples/**/*.ts"], - languageOptions: { - ecmaVersion: 2022, - sourceType: "module", - parser: tseslint.parser, - globals: { - process: "readonly", - Buffer: "readonly", - console: "readonly", - setTimeout: "readonly", - clearTimeout: "readonly", - performance: "readonly", - global: "readonly", - }, - }, - rules: { - "no-console": "off", - "max-lines-per-function": "off", - "complexity": "off", - "no-unused-vars": "off", - }, - }, - - // ── Browser globals for HTML example assets ──────────────────────────────── - { - files: ["examples/html/assets/**/*.ts"], - languageOptions: { - globals: { - window: "readonly", - document: "readonly", - localStorage: "readonly", - CustomEvent: "readonly", - Viz: "readonly", - }, - }, - }, ); diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index c65dc078e..000000000 --- a/examples/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Deprecated v18 Compatibility Examples - -Runnable snippets for the deprecated v18 graph-first compatibility surface. -Each file pairs with a topic page under [`docs/topics/`](../docs/topics/). -Do not copy these examples into new application code. - -| Example | Shows | Topic | -| --- | --- | --- | -| [`optics.ts`](optics.ts) | A bounded coordinate read through an optic | [Optic reads](../docs/topics/optic-reads.md) | -| [`observers.ts`](observers.ts) | Bounding visibility with an aperture | [Observers](../docs/topics/observers.md) | -| [`bounded-reads.ts`](bounded-reads.ts) | `comparison.diff()` and `materializeSlice()` | [Optic reads](../docs/topics/optic-reads.md) | - -Each example exports an `async` function that takes the `cwd` of a Git -repository. They use the deprecated migration-only -`@git-stunts/git-warp/legacy` surface with a `@git-stunts/plumbing`-backed -`GitGraphAdapter`, so run them against a real repo only for migration testing. -The Lamport ceilings in `bounded-reads.ts` are placeholders — substitute values -from your own history. diff --git a/examples/bounded-reads.ts b/examples/bounded-reads.ts deleted file mode 100644 index dcc91cd18..000000000 --- a/examples/bounded-reads.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Deprecated v18 compatibility example: bounded reads that never fold the - * whole graph. - * - * Pairs with docs/topics/optic-reads.md. These use the legacy - * openWarpGraph() surface; materializeSlice() is a bounded causal-cone read. - * Illustrative: run against a real @git-stunts/plumbing-backed Git repository - * only for migration testing. - */ -import { GitGraphAdapter, openWarpGraph } from '@git-stunts/git-warp/legacy'; -import GitPlumbing from '@git-stunts/plumbing'; - -export async function boundedReads(cwd: string): Promise { - const plumbing = new GitPlumbing({ cwd }); - const persistence = new GitGraphAdapter({ plumbing }); - - const graph = await openWarpGraph({ - persistence, - graphName: 'events', - writerId: 'agent-1', - }); - - // What changed between two live Lamport ceilings — a frozen GraphDiff, - // not a query().match('*') wildcard scan. - const diff = await graph.comparison.diff({ - from: 120, - to: 135, - targetId: 'user:alice', - }); - void diff.nodes.added; - void diff.nodeProperties.changed; - - // One entity's backward causal cone, replayed on its own. Loads only the - // cone's patches — never the whole graph. - const slice = await graph.provenance.materializeSlice('user:alice'); - void slice.state; - void slice.patchCount; -} diff --git a/examples/observers.ts b/examples/observers.ts deleted file mode 100644 index 1549856fb..000000000 --- a/examples/observers.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Deprecated v18 compatibility example: bounding visibility with an observer - * and aperture. - * - * Pairs with docs/topics/observers.md. Illustrative: run against a real - * @git-stunts/plumbing-backed Git repository only for migration testing. - */ -import { - GitGraphAdapter, - openWarpWorldline, - type SnapshotPropValue, -} from '@git-stunts/git-warp/legacy'; -import GitPlumbing from '@git-stunts/plumbing'; - -type PublicNodeProps = Readonly<{ [key: string]: SnapshotPropValue }>; - -export async function readThroughPublicAperture( - cwd: string, -): Promise { - const plumbing = new GitPlumbing({ cwd }); - const persistence = new GitGraphAdapter({ plumbing }); - - const events = await openWarpWorldline({ - persistence, - worldlineName: 'events', - writerId: 'agent-1', - }); - - await events.commit((patch) => { - patch - .addNode('task:auth') - .setProperty('task:auth', 'status', 'open') - .setProperty('task:auth', 'internalNotes', 'do not leak'); - }); - - // The aperture selects which entities are in view and redacts sensitive props. - const publicView = await events.observer('public-review', { - match: ['task:*', 'service:*'], - redact: ['internalNotes'], - }); - - // => { status: 'open' } (internalNotes is redacted) - return await publicView.getNodeProps('task:auth'); -} diff --git a/examples/optics.ts b/examples/optics.ts deleted file mode 100644 index 2ebd27629..000000000 --- a/examples/optics.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Deprecated v18 compatibility example: bounded coordinate reads through an - * optic. - * - * Pairs with docs/topics/optic-reads.md. Illustrative: run against a real - * @git-stunts/plumbing-backed Git repository only for migration testing. - */ -import { - GitGraphAdapter, - openWarpWorldline, - type PropValue, -} from '@git-stunts/git-warp/legacy'; -import GitPlumbing from '@git-stunts/plumbing'; - -type PublicNodePropertyRead = { - readonly nodeId: string; - readonly key: string; - readonly exists: boolean; - readonly value: PropValue | undefined; -}; - -export async function readRoleThroughOptic(cwd: string): Promise { - const plumbing = new GitPlumbing({ cwd }); - const persistence = new GitGraphAdapter({ plumbing }); - - const events = await openWarpWorldline({ - persistence, - worldlineName: 'events', - writerId: 'agent-1', - }); - - await events.commit((patch) => { - patch.addNode('user:alice').setProperty('user:alice', 'role', 'admin'); - }); - - // Verify a bounded basis exists, then read through a captured coordinate. - // prepareOpticBasis() fails closed with E_OPTIC_NO_BOUNDED_BASIS rather than - // materializing the whole graph. - await events.prepareOpticBasis(); - const coordinate = await events.coordinate(); - - // => { nodeId: 'user:alice', key: 'role', exists: true, value: 'admin', readIdentity: ... } - return await coordinate.optic().node('user:alice').prop('role').read(); -} diff --git a/index.ts b/index.ts index 3c30afbe4..23ef7c34b 100644 --- a/index.ts +++ b/index.ts @@ -4,8 +4,7 @@ * Public v19 application boundary. * * Root users should write intents, read timelines, and keep receipts. The - * graph-first compatibility surface is deprecated and isolated under - * `@git-stunts/git-warp/legacy` for migration-only use. + * The graph-first compatibility surface is no longer publicly exported. * Storage adapters live under `@git-stunts/git-warp/storage`; formal read, * evidence, and support machinery lives under `@git-stunts/git-warp/advanced`; * operator inspection tools live under `@git-stunts/git-warp/diagnostics`. diff --git a/jsr.json b/jsr.json index faee12407..c4fc4d23d 100644 --- a/jsr.json +++ b/jsr.json @@ -6,22 +6,17 @@ }, "exports": { ".": "./index.ts", - "./browser": "./browser.ts", "./storage": "./storage.ts", "./advanced": "./advanced.ts", "./diagnostics": "./diagnostics.ts", - "./legacy": "./legacy.ts", "./sha1sync": "./src/infrastructure/adapters/sha1sync.ts" }, "publish": { "include": [ "index.ts", - "browser.ts", "storage.ts", "advanced.ts", "diagnostics.ts", - "legacy.ts", - "rootCompatibility.ts", "src/**/*.ts", "src/**/*.d.ts", "README.md", diff --git a/legacy.ts b/legacy.ts deleted file mode 100644 index ae3a7dd4d..000000000 --- a/legacy.ts +++ /dev/null @@ -1,479 +0,0 @@ -/** - * @module - * - * Deprecated graph-first compatibility surface for v18-era code. - * - * Do not use this entrypoint for new application code. It exists only as a - * migration bridge while consumers move to root intent, reading, timeline, and - * receipt nouns plus the `storage`, `advanced`, and `diagnostics` subpaths. - * - * @deprecated The entire `@git-stunts/git-warp/legacy` module is deprecated - * and migration-only. New code must not import from it. - */ - -import GitGraphAdapter from './src/infrastructure/adapters/GitGraphAdapter.ts'; -import GraphNode from './src/domain/entities/GraphNode.ts'; -import BitmapIndexBuilder from './src/domain/services/index/BitmapIndexBuilder.ts'; -import BitmapIndexReader from './src/domain/services/index/BitmapIndexReader.ts'; -import IndexRebuildService from './src/domain/services/index/IndexRebuildService.ts'; -import HealthCheckService, { HealthStatus } from './src/domain/services/HealthCheckService.ts'; -import CommitDagTraversalService from './src/domain/services/dag/CommitDagTraversalService.ts'; -import GraphPersistencePort from './src/ports/GraphPersistencePort.ts'; -import type WarpKernelPort from './src/ports/WarpKernelPort.ts'; -import IndexStoragePort from './src/ports/IndexStoragePort.ts'; -import LoggerPort from './src/ports/LoggerPort.ts'; -import SeekCachePort from './src/ports/SeekCachePort.ts'; -import InMemoryGraphAdapter from './src/infrastructure/adapters/InMemoryGraphAdapter.ts'; -import NoOpLogger from './src/infrastructure/adapters/NoOpLogger.ts'; -import ConsoleLogger, { LogLevel } from './src/infrastructure/adapters/ConsoleLogger.ts'; -import WriterError from './src/domain/errors/WriterError.ts'; -import BlobStoragePort from './src/ports/BlobStoragePort.ts'; -import InMemoryBlobStorageAdapter from './src/domain/utils/defaultBlobStorage.ts'; -import CryptoPort from './src/ports/CryptoPort.ts'; -import HttpServerPort from './src/ports/HttpServerPort.ts'; -import NodeCryptoAdapter from './src/infrastructure/adapters/NodeCryptoAdapter.ts'; -import WebCryptoAdapter from './src/infrastructure/adapters/WebCryptoAdapter.ts'; -import BunHttpAdapter from './src/infrastructure/adapters/BunHttpAdapter.ts'; -import DenoHttpAdapter from './src/infrastructure/adapters/DenoHttpAdapter.ts'; -import { checkAborted, createTimeoutSignal } from './src/domain/utils/cancellation.ts'; - -// Multi-writer graph support (WARP) -import WarpCore from './src/domain/WarpCore.ts'; -import WarpApp from './src/domain/WarpApp.ts'; -import { - createNodeAdd, - createNodeTombstone, - createEdgeAdd, - createEdgeTombstone, - createPropSet, - createInlineValue, - createBlobValue, - createV18BoundedMemoryCapabilityReport, -} from './rootCompatibility.ts'; -import QueryBuilder from './src/domain/services/query/QueryBuilder.ts'; -import BoundedSupportRule from './src/domain/services/query/BoundedSupportRule.ts'; -import CausalIndexPlan from './src/domain/services/query/CausalIndexPlan.ts'; -import SupportFragmentPlan from './src/domain/services/query/SupportFragmentPlan.ts'; -import Observer from './src/domain/services/query/Observer.ts'; -import ObserverAccumulation from './src/domain/services/query/ObserverAccumulation.ts'; -import ObserverBasis from './src/domain/services/query/ObserverBasis.ts'; -import ObserverEmission from './src/domain/services/query/ObserverEmission.ts'; -import ObserverPlan from './src/domain/services/query/ObserverPlan.ts'; -import ObserverReadingEnvelope from './src/domain/services/query/ObserverReadingEnvelope.ts'; -import ProjectionHandle from './src/domain/services/ProjectionHandle.ts'; -import WorldlineSelector from './src/domain/types/WorldlineSelector.ts'; -import LiveSelector from './src/domain/types/LiveSelector.ts'; -import CoordinateSelector from './src/domain/types/CoordinateSelector.ts'; -import StrandSelector from './src/domain/types/StrandSelector.ts'; -import { computeTranslationCost } from './src/domain/services/TranslationCost.ts'; -import { - encodeEdgePropKey, - decodeEdgePropKey, - isEdgePropKey, - CONTENT_PROPERTY_KEY, -} from './src/domain/services/KeyCodec.ts'; -import { - createTickReceipt, - OP_TYPES as TICK_RECEIPT_OP_TYPES, - RESULT_TYPES as TICK_RECEIPT_RESULT_TYPES, -} from './src/domain/types/TickReceipt.ts'; -import { - tickReceiptCanonicalJson, - canonicalEmissionJson, - canonicalObservationJson, -} from './src/infrastructure/codecs/ReceiptJsonCodec.ts'; - -// Provenance payload (HOLOGRAM) -import ProvenancePayload from './src/domain/services/provenance/ProvenancePayload.ts'; - -// Boundary Transition Records (HOLOGRAM) -import { - createBTR, - verifyBTR, - replayBTR, -} from './src/application/provenance/BtrOperations.ts'; -import { BTR } from './src/domain/services/provenance/BTR.ts'; - -// Wormhole compression (HOLOGRAM) -import { - createWormhole, - composeWormholes, - replayWormhole, - serializeWormhole, - deserializeWormhole, -} from './src/application/WormholeServiceDefaults.ts'; -import ApertureOpeningProof from './src/domain/services/wormhole/ApertureOpeningProof.ts'; -import RejectedApertureOpening from './src/domain/services/wormhole/RejectedApertureOpening.ts'; -import RejectedZKWormhole from './src/domain/services/wormhole/RejectedZKWormhole.ts'; -import VerifiedApertureOpening from './src/domain/services/wormhole/VerifiedApertureOpening.ts'; -import VerifiedZKWormhole from './src/domain/services/wormhole/VerifiedZKWormhole.ts'; -import ZKWormholeEdge from './src/domain/services/wormhole/ZKWormholeEdge.ts'; -import { openAperture, verifyZKWormhole } from './src/domain/services/wormhole/ZKWormholeService.ts'; - -import BisectService from './src/domain/services/BisectService.ts'; -import EffectSinkPort from './src/ports/EffectSinkPort.ts'; -import { MultiplexSink } from './src/domain/services/MultiplexSink.ts'; -import { EffectPipeline } from './src/domain/services/EffectPipeline.ts'; -import { - createEffectEmission, - DELIVERY_MODES, - DELIVERY_OUTCOMES, -} from './src/domain/types/EffectEmission.ts'; -import { - createDeliveryObservation, -} from './src/domain/types/DeliveryObservation.ts'; -import { - createExternalizationPolicy, - LIVE_LENS, - REPLAY_LENS, - INSPECT_LENS, -} from './src/domain/types/ExternalizationPolicy.ts'; -import { NoOpEffectSink } from './src/infrastructure/adapters/NoOpEffectSink.ts'; -import { ConsoleEffectSink } from './src/infrastructure/adapters/ConsoleEffectSink.ts'; -import { ChunkEffectSink } from './src/infrastructure/adapters/ChunkEffectSink.ts'; -import SyncSecret from './src/domain/services/sync/SyncSecret.ts'; -import ZKWormholeProofVerifierPort from './src/ports/ZKWormholeProofVerifierPort.ts'; -import ContentAttachmentProjection from './src/domain/services/ContentAttachmentProjection.ts'; -import GraphOpAlgebraProjection from './src/domain/services/GraphOpAlgebraProjection.ts'; -import { openWarpGraph } from './src/domain/WarpGraph.ts'; -import WarpWorldline, { openWarpWorldline } from './src/domain/WarpWorldline.ts'; -import { WarpOpenOptions } from './src/domain/warp/RuntimeHostBoot.ts'; -import WarpWorldlineCoordinate from './src/domain/WarpWorldlineCoordinate.ts'; -import WarpWorldlineOpticBasis from './src/domain/WarpWorldlineOpticBasis.ts'; -import Optic from './src/domain/services/optic/Optic.ts'; -import OpticAperturePosture from './src/domain/services/optic/OpticAperturePosture.ts'; -import OpticBasisPosture from './src/domain/services/optic/OpticBasisPosture.ts'; -import OpticCoordinatePosture from './src/domain/services/optic/OpticCoordinatePosture.ts'; -import OpticSupportRule from './src/domain/services/optic/OpticSupportRule.ts'; -import { PatchBuilder } from './src/domain/services/PatchBuilder.ts'; -import { PatchSession } from './src/domain/warp/PatchSession.ts'; -import { Writer } from './src/domain/warp/Writer.ts'; -import { ProvenanceIndex } from './src/domain/services/provenance/ProvenanceIndex.ts'; -import WarpStateIndexBuilder, { buildWarpStateIndex } from './src/domain/services/index/WarpStateIndexBuilder.ts'; -import { computeStateHash, projectState } from './src/domain/services/state/StateSerializer.ts'; -import { createStateReader } from './src/domain/services/state/StateReader.ts'; -import { compareVisibleState } from './src/domain/services/comparison/VisibleStateComparison.ts'; -import GraphDiff from './src/domain/services/comparison/GraphDiff.ts'; -import TtdMergeBranch from './src/domain/services/merge/TtdMergeBranch.ts'; -import TtdMergeFootprint from './src/domain/services/merge/TtdMergeFootprint.ts'; -import TtdMergeInspection from './src/domain/services/merge/TtdMergeInspection.ts'; -import TtdMergeInspector from './src/domain/services/merge/TtdMergeInspector.ts'; -import TtdMergeLoweringWitness from './src/domain/services/merge/TtdMergeLoweringWitness.ts'; -import TtdMergeObstructionWitness from './src/domain/services/merge/TtdMergeObstructionWitness.ts'; -import TtdMergePolicyRequirement from './src/domain/services/merge/TtdMergePolicyRequirement.ts'; -import ImmutableBytes from './src/domain/services/snapshot/ImmutableBytes.ts'; -import SnapshotORSet from './src/domain/services/snapshot/SnapshotORSet.ts'; -import SnapshotVersionVector from './src/domain/services/snapshot/SnapshotVersionVector.ts'; -import SnapshotWarpState from './src/domain/services/snapshot/SnapshotWarpState.ts'; -import type { PropValue } from './src/domain/types/PropValue.ts'; -import type { Aperture, ObserverConfig } from './src/domain/types/Aperture.ts'; -import type { SnapshotPropValue } from './src/domain/services/snapshot/SnapshotPropValue.ts'; -import type { SyncRateLimitConfig } from './src/domain/services/sync/SyncRateLimiter.ts'; -import type { WarpWorldlineOpenOptions, WarpWorldlinePatchBuild } from './src/domain/WarpWorldline.ts'; -import type { - OpticContextValue, - OpticFields, - OpticPostureFields, -} from './src/domain/services/optic/Optic.ts'; -import type { OpticAperturePostureValue } from './src/domain/services/optic/OpticAperturePosture.ts'; -import type { OpticBasisPostureValue } from './src/domain/services/optic/OpticBasisPosture.ts'; -import type { OpticCoordinatePostureValue } from './src/domain/services/optic/OpticCoordinatePosture.ts'; -import type { OpticSupportRuleValue } from './src/domain/services/optic/OpticSupportRule.ts'; -import type { ApertureOpeningProofFields } from './src/domain/services/wormhole/ApertureOpeningProof.ts'; -import type { ZKWormholeEdgeFields } from './src/domain/services/wormhole/ZKWormholeEdge.ts'; -import type { ApertureOpeningVerificationResult, ZKWormholeVerificationResult } from './src/domain/services/wormhole/ZKWormholeVerificationResult.ts'; -import type { WarpWorldlineCoordinateFrontierEntry } from './src/domain/WarpWorldlineCoordinate.ts'; -import type { GraphDiffOptions } from './src/domain/capabilities/ComparisonCapability.ts'; -import type { GraphDiffFields } from './src/domain/services/comparison/GraphDiff.ts'; -import type { ObserverPlanFields } from './src/domain/services/query/ObserverPlan.ts'; -import type { - ObserverReadingEnvelopeBudget, - ObserverReadingEnvelopeFields, -} from './src/domain/services/query/ObserverReadingEnvelope.ts'; -import type { - BoundedSupportDirection, - BoundedSupportKind, - BoundedSupportRuleFields, - BoundedSupportSurface, -} from './src/domain/services/query/BoundedSupportRule.ts'; -import type { - CausalIndexFamily, - CausalIndexPlanFields, - CausalIndexPlanPosture, -} from './src/domain/services/query/CausalIndexPlan.ts'; -import type { - SupportFragmentMaterializationPosture, - SupportFragmentPlanFields, -} from './src/domain/services/query/SupportFragmentPlan.ts'; -import { - normalizeVisibleStateScope, - scopeMaterializedState, -} from './src/domain/services/VisibleStateScope.ts'; -import { - exportCoordinateComparisonFact, - exportCoordinateTransferPlanFact, -} from './src/domain/services/CoordinateFactExport.ts'; -import { installDefaultRuntimeHostNodePorts } from './src/application/RuntimeHostNodeDefaults.ts'; - -// Idempotent: the default installers replace runtime resolver slots with -// closures over frozen singleton adapters; repeated legacy imports do not stack. -installDefaultRuntimeHostNodePorts(); - -export * from './src/domain/graph/publicGraphSubstrate.ts'; -export * from './src/domain/memory/index.ts'; -export * from './src/continuumExports.ts'; -export { default as OperationPolicyPort } from './src/ports/OperationPolicyPort.ts'; -export type { OperationPolicyExecuteOptions, OperationRetryDecision, OperationRetryObserver } from './src/ports/OperationPolicyPort.ts'; -export { default as CasContentEncryptionPolicy } from './src/infrastructure/adapters/CasContentEncryptionPolicy.ts'; -export type { CasContentEncryptionDiagnostics, CasContentEncryptionScheme, CasResolvedVaultKeyOptions, CasVaultResolutionWitness } from './src/infrastructure/adapters/CasContentEncryptionPolicy.ts'; -export { default as AlfredOperationPolicyAdapter } from './src/infrastructure/adapters/AlfredOperationPolicyAdapter.ts'; -export { default as NoopOperationPolicyAdapter } from './src/infrastructure/adapters/NoopOperationPolicyAdapter.ts'; -export { OperationPolicyExhaustedError, OperationPolicyTimeoutError } from './src/domain/errors/index.ts'; -export { - AuditError, - ContinuumArtifactAuthorityError, - EncryptionError, - ForkError, - IndexError, - MemoryBudgetError, - OperationAbortedError, - PatchError, - QueryError, - SchemaUnsupportedError, - ShardCorruptionError, - ShardLoadError, - ShardValidationError, - StorageError, - StrandError, - SyncError, - TraversalError, - WormholeError, -} from './src/domain/errors/index.ts'; - -import type { TtdMergeBranchFields } from './src/domain/services/merge/TtdMergeBranch.ts'; -import type { TtdMergeFootprintFields } from './src/domain/services/merge/TtdMergeFootprint.ts'; -import type { TtdMergeInspectionFields } from './src/domain/services/merge/TtdMergeInspection.ts'; -import type { TtdMergeInspectionDomain } from './src/domain/services/merge/TtdMergeInspectionDomain.ts'; -import type { TtdMergeLoweringSurface } from './src/domain/services/merge/TtdMergeLoweringSurface.ts'; -import type { TtdMergeLoweringWitnessFields } from './src/domain/services/merge/TtdMergeLoweringWitness.ts'; -import type { TtdMergeObjectBranchInput, TtdMergeObjectInspectionInput } from './src/domain/services/merge/TtdMergeInspector.ts'; -import type { TtdMergeObstructionWitnessFields } from './src/domain/services/merge/TtdMergeObstructionWitness.ts'; -import type { TtdMergePolicyRequirementFields } from './src/domain/services/merge/TtdMergePolicyRequirement.ts'; - -export { - GitGraphAdapter, - InMemoryGraphAdapter, - GraphNode, - GraphOpAlgebraProjection, - BitmapIndexBuilder, - BitmapIndexReader, - IndexRebuildService, - HealthCheckService, - HealthStatus, - CommitDagTraversalService, - BisectService, - GraphPersistencePort, - IndexStoragePort, - - // Logging infrastructure - LoggerPort, - NoOpLogger, - ConsoleLogger, - LogLevel, - - // Seek cache (RECALL) - SeekCachePort, - - // Port contracts - BlobStoragePort, - InMemoryBlobStorageAdapter, - CryptoPort, - HttpServerPort, - - // Crypto adapters - NodeCryptoAdapter, - WebCryptoAdapter, - - // HTTP adapters - BunHttpAdapter, - DenoHttpAdapter, - - // Error types for integrity failure handling - WriterError, - - // Cancellation utilities - checkAborted, - createTimeoutSignal, - - // Deprecated v18 graph composition root. - WarpOpenOptions, - openWarpGraph, - - // Deprecated v18 worldline compatibility handle. - openWarpWorldline, - WarpWorldline, - WarpWorldlineCoordinate, - WarpWorldlineOpticBasis, - Optic, - OpticAperturePosture, - OpticBasisPosture, - OpticCoordinatePosture, - OpticSupportRule, - ProjectionHandle, - - // Deprecated v18 facades and patch/query helpers. - WarpApp, - WarpCore, - WorldlineSelector, - LiveSelector, - CoordinateSelector, - StrandSelector, - BoundedSupportRule, - CausalIndexPlan, - SupportFragmentPlan, - QueryBuilder, - Observer, - ObserverAccumulation, - ObserverBasis, - ObserverEmission, - ObserverPlan, - ObserverReadingEnvelope, - PatchBuilder, - PatchSession, - Writer, - ProvenanceIndex, - computeTranslationCost, - - // WARP type creators - createNodeAdd, - createNodeTombstone, - createEdgeAdd, - createEdgeTombstone, - createPropSet, - createInlineValue, - createBlobValue, - - // Key codec utilities (BULKHEAD) - encodeEdgePropKey, - decodeEdgePropKey, - isEdgePropKey, - CONTENT_PROPERTY_KEY, - - // State indexing & hashing - WarpStateIndexBuilder, - buildWarpStateIndex, - computeStateHash, - projectState, - createStateReader, - compareVisibleState, - GraphDiff, - TtdMergeBranch, - TtdMergeFootprint, - TtdMergeInspection, - TtdMergeInspector, - TtdMergeLoweringWitness, - TtdMergeObstructionWitness, - TtdMergePolicyRequirement, - ImmutableBytes, - SnapshotORSet, - SnapshotVersionVector, - SnapshotWarpState, - normalizeVisibleStateScope, - scopeMaterializedState, - exportCoordinateComparisonFact, - exportCoordinateTransferPlanFact, - createV18BoundedMemoryCapabilityReport, - - // Tick receipts (LIGHTHOUSE) - createTickReceipt, - tickReceiptCanonicalJson, - TICK_RECEIPT_OP_TYPES, - TICK_RECEIPT_RESULT_TYPES, - - // Provenance payload (HOLOGRAM) - ProvenancePayload, - - // Boundary Transition Records (HOLOGRAM) - BTR, - createBTR, - verifyBTR, - replayBTR, - - // Wormhole compression (HOLOGRAM) - createWormhole, - composeWormholes, - replayWormhole, - serializeWormhole, - deserializeWormhole, - ApertureOpeningProof, RejectedApertureOpening, RejectedZKWormhole, - VerifiedApertureOpening, VerifiedZKWormhole, ZKWormholeEdge, - openAperture, verifyZKWormhole, ZKWormholeProofVerifierPort, - - // Effect emission & delivery observation - EffectSinkPort, - MultiplexSink, - EffectPipeline, - createEffectEmission, - canonicalEmissionJson, - createDeliveryObservation, - canonicalObservationJson, - createExternalizationPolicy, - DELIVERY_MODES, - DELIVERY_OUTCOMES, - LIVE_LENS, - REPLAY_LENS, - INSPECT_LENS, - NoOpEffectSink, - ConsoleEffectSink, - ChunkEffectSink, - SyncSecret, - ContentAttachmentProjection, -}; - -export type { - Aperture, - ObserverConfig, - PropValue, - SnapshotPropValue, - SyncRateLimitConfig, - WarpKernelPort, - WarpWorldlineOpenOptions, - WarpWorldlinePatchBuild, - OpticAperturePostureValue, - OpticBasisPostureValue, - OpticContextValue, - OpticCoordinatePostureValue, - OpticFields, - OpticPostureFields, - OpticSupportRuleValue, - BoundedSupportDirection, - BoundedSupportKind, - BoundedSupportRuleFields, - BoundedSupportSurface, - CausalIndexFamily, - CausalIndexPlanFields, - CausalIndexPlanPosture, - SupportFragmentMaterializationPosture, - SupportFragmentPlanFields, - GraphDiffOptions, - GraphDiffFields, - ObserverPlanFields, - ObserverReadingEnvelopeBudget, - ObserverReadingEnvelopeFields, - ApertureOpeningProofFields, ApertureOpeningVerificationResult, - ZKWormholeEdgeFields, ZKWormholeVerificationResult, - WarpWorldlineCoordinateFrontierEntry, - TtdMergeBranchFields, - TtdMergeFootprintFields, - TtdMergeInspectionDomain, - TtdMergeInspectionFields, - TtdMergeLoweringSurface, - TtdMergeLoweringWitnessFields, - TtdMergeObjectBranchInput, - TtdMergeObjectInspectionInput, - TtdMergeObstructionWitnessFields, - TtdMergePolicyRequirementFields, -}; - -// Deprecated default export retained only for old migration callers. -export default WarpApp; diff --git a/package.json b/package.json index 2f9f862a5..bfa9acb1f 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,6 @@ "import": "./dist/index.js", "default": "./dist/index.js" }, - "./browser": { - "types": "./dist/browser.d.ts", - "import": "./dist/browser.js", - "default": "./dist/browser.js" - }, "./storage": { "types": "./dist/storage.d.ts", "import": "./dist/storage.js", @@ -49,11 +44,6 @@ "import": "./dist/diagnostics.js", "default": "./dist/diagnostics.js" }, - "./legacy": { - "types": "./dist/legacy.d.ts", - "import": "./dist/legacy.js", - "default": "./dist/legacy.js" - }, "./sha1sync": { "types": "./dist/src/infrastructure/adapters/sha1sync.d.ts", "import": "./dist/src/infrastructure/adapters/sha1sync.js", @@ -75,7 +65,7 @@ "access": "public" }, "scripts": { - "build": "tsc -p tsconfig.publish.json", + "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.publish.json", "lint": "sh -c 'eslint . \"$@\" && npm run lint:test-law && npm run lint:source-size && npm run lint:cas-invariants' --", "lint:ratchet": "sh scripts/lint-ratchet.sh", "lint:md": "markdownlint \"**/*.md\" --ignore node_modules --ignore \"**/node_modules/**\"", diff --git a/rootCompatibility.ts b/rootCompatibility.ts deleted file mode 100644 index 03dd5e079..000000000 --- a/rootCompatibility.ts +++ /dev/null @@ -1,50 +0,0 @@ -import createBoundedMemoryCapabilityReport - from './src/domain/memory/createBoundedMemoryCapabilityReport.ts'; -import type { PropValue } from './src/domain/types/PropValue.ts'; - -export function createNodeAdd(node: string): { type: 'NodeAdd'; node: string } { - return { type: 'NodeAdd' as const, node }; -} - -export function createNodeTombstone(node: string): { type: 'NodeTombstone'; node: string } { - return { type: 'NodeTombstone' as const, node }; -} - -export function createEdgeAdd( - from: string, - to: string, - label: string, -): { type: 'EdgeAdd'; from: string; to: string; label: string } { - return { type: 'EdgeAdd' as const, from, to, label }; -} - -export function createEdgeTombstone( - from: string, - to: string, - label: string, -): { type: 'EdgeTombstone'; from: string; to: string; label: string } { - return { type: 'EdgeTombstone' as const, from, to, label }; -} - -type PropSetValue = { type: 'inline'; value: PropValue } | { type: 'blob'; oid: string }; - -export function createPropSet( - node: string, - key: string, - value: PropSetValue, -): { type: 'PropSet'; node: string; key: string; value: PropSetValue } { - return { type: 'PropSet' as const, node, key, value }; -} - -export function createInlineValue(value: PropValue): { type: 'inline'; value: PropValue } { - return { type: 'inline' as const, value }; -} - -export function createBlobValue(oid: string): { type: 'blob'; oid: string } { - return { type: 'blob' as const, oid }; -} - -/** - * @deprecated Use createBoundedMemoryCapabilityReport. - */ -export const createV18BoundedMemoryCapabilityReport = createBoundedMemoryCapabilityReport; diff --git a/scripts/check-source-backed-reference.ts b/scripts/check-source-backed-reference.ts index 9c934747b..248542408 100644 --- a/scripts/check-source-backed-reference.ts +++ b/scripts/check-source-backed-reference.ts @@ -239,7 +239,6 @@ function generate(): string { const storageSource = new SourceText('storage.ts'); const advancedSource = new SourceText('advanced.ts'); const diagnosticsSource = new SourceText('diagnostics.ts'); - const legacySource = new SourceText('legacy.ts'); const packageBins = captureObjectEntries(packageSource, 'bin'); const packageExports = captureExportEntries(packageSource, 'exports').filter((item) => item.name.startsWith('.')); const jsrExports = captureExportEntries(jsrSource, 'exports').filter((item) => item.name.startsWith('.')); @@ -269,8 +268,6 @@ function generate(): string { '', ...exportSurface('Diagnostics export surface', diagnosticsSource, 'Operator, inspection, comparison, and replay tools.'), '', - ...exportSurface('Legacy export surface', legacySource, 'Deprecated compatibility-only imports for migration paydown.'), - '', '## CLI command registry', '', table(['Command', 'Handler', 'Source'], commands.map((item) => [`\`${item.name}\``, `\`${item.detail}\``, `\`${item.source}\``])), diff --git a/test/conformance/btrProvenanceBoundary.test.ts b/test/conformance/btrProvenanceBoundary.test.ts index 75cec610a..2aa2103cd 100644 --- a/test/conformance/btrProvenanceBoundary.test.ts +++ b/test/conformance/btrProvenanceBoundary.test.ts @@ -1,14 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { - BTR, - createBTR, - NodeCryptoAdapter, - ProvenancePayload, - replayBTR, - verifyBTR, -} from '../../legacy.ts'; +import { createBTR, replayBTR, verifyBTR } from '../../src/application/provenance/BtrOperations.ts'; +import { BTR } from '../../src/domain/services/provenance/BTR.ts'; +import ProvenancePayload from '../../src/domain/services/provenance/ProvenancePayload.ts'; import BtrCodecAdapter from '../../src/infrastructure/adapters/BtrCodecAdapter.ts'; +import NodeCryptoAdapter from '../../src/infrastructure/adapters/NodeCryptoAdapter.ts'; +import defaultCodec from '../../src/infrastructure/codecs/CborCodec.ts'; import { createEmptyState, createSamplePatches } from '../helpers/warpGraphTestUtils.ts'; const crypto = new NodeCryptoAdapter(); @@ -51,15 +48,17 @@ describe('BTR provenance boundary repair contract', () => { timestamp, crypto, btrCodec, + stateCodec: defaultCodec, }); const decoded = decodeRecord(btrCodec.encodeRecord(record)); const verification = await verifyBTR(decoded, key, { crypto, btrCodec, + stateCodec: defaultCodec, verifyReplay: true, }); - const replayed = await replayBTR(decoded, { crypto }); + const replayed = await replayBTR(decoded, { crypto, stateCodec: defaultCodec }); expect(decoded.P).toHaveLength(3); expect(decoded.kappa).toBe(record.kappa); @@ -68,8 +67,10 @@ describe('BTR provenance boundary repair contract', () => { expect(replayed.h_out).toBe(record.h_out); expect(replayed.state.hasNodeRecord('node-a')).toBe(true); expect(replayed.state.hasNodeRecord('node-b')).toBe(true); - expect(replayed.state.getNodeProp('node-a', 'name')?.value) - .toEqual({ type: 'inline', value: 'Alice' }); + expect(replayed.state.getNodeProp('node-a', 'name')?.value).toEqual({ + type: 'inline', + value: 'Alice', + }); }); it('rejects semantic tampering after a valid BTR was signed', async () => { @@ -78,11 +79,13 @@ describe('BTR provenance boundary repair contract', () => { timestamp, crypto, btrCodec, + stateCodec: defaultCodec, }); const verification = await verifyBTR(tamperTimestamp(record), key, { crypto, btrCodec, + stateCodec: defaultCodec, verifyReplay: true, }); @@ -96,6 +99,7 @@ describe('BTR provenance boundary repair contract', () => { timestamp, crypto, btrCodec, + stateCodec: defaultCodec, }); expect(await verifyBTR(record, key, { crypto })).toMatchObject({ diff --git a/test/conformance/btrSigningBytesOwnership.test.ts b/test/conformance/btrSigningBytesOwnership.test.ts index 3d6dbbcbd..f66660bbf 100644 --- a/test/conformance/btrSigningBytesOwnership.test.ts +++ b/test/conformance/btrSigningBytesOwnership.test.ts @@ -1,14 +1,12 @@ import { describe, expect, it } from 'vitest'; -import { - createBTR, - NodeCryptoAdapter, - ProvenancePayload, - verifyBTR, -} from '../../legacy.ts'; +import { createBTR, verifyBTR } from '../../src/application/provenance/BtrOperations.ts'; import BtrCodecAdapter from '../../src/infrastructure/adapters/BtrCodecAdapter.ts'; import BtrSigningBytes from '../../src/domain/services/provenance/BtrSigningBytes.ts'; import type BtrSigningEnvelope from '../../src/domain/services/provenance/BtrSigningEnvelope.ts'; +import ProvenancePayload from '../../src/domain/services/provenance/ProvenancePayload.ts'; +import NodeCryptoAdapter from '../../src/infrastructure/adapters/NodeCryptoAdapter.ts'; +import defaultCodec from '../../src/infrastructure/codecs/CborCodec.ts'; import CryptoPort from '../../src/ports/CryptoPort.ts'; import { createEmptyState, createSamplePatches } from '../helpers/warpGraphTestUtils.ts'; @@ -32,7 +30,7 @@ class CapturingCryptoPort extends CryptoPort { override async hmac( algorithm: string, hmacKey: string | Uint8Array, - data: string | Uint8Array, + data: string | Uint8Array ): Promise { this.#hmacInputs.push(copyData(data)); return await this.#delegate.hmac(algorithm, hmacKey, data); @@ -46,7 +44,7 @@ class CapturingCryptoPort extends CryptoPort { class AlteredSigningBytesCodec extends BtrCodecAdapter { override signingBytes(envelope: BtrSigningEnvelope): BtrSigningBytes { return BtrSigningBytes.fromCanonicalBtrSigningEncoder( - appendZero(super.signingBytes(envelope).copyBytes()), + appendZero(super.signingBytes(envelope).copyBytes()) ); } } @@ -94,6 +92,7 @@ describe('BTR signing-byte ownership behavior', () => { timestamp, crypto, btrCodec, + stateCodec: defaultCodec, }); const signingBytes = btrCodec.signingBytes(record.envelope); @@ -107,6 +106,7 @@ describe('BTR signing-byte ownership behavior', () => { timestamp, crypto, btrCodec, + stateCodec: defaultCodec, }); const signingBytes = btrCodec.signingBytes(record.envelope); const exposed = signingBytes.copyBytes(); @@ -124,6 +124,7 @@ describe('BTR signing-byte ownership behavior', () => { timestamp, crypto: capturingCrypto, btrCodec, + stateCodec: defaultCodec, }); const hmacInputs = capturingCrypto.hmacInputs(); const hmacInput = hmacInputs[0]; @@ -141,10 +142,12 @@ describe('BTR signing-byte ownership behavior', () => { timestamp, crypto, btrCodec, + stateCodec: defaultCodec, }); const verification = await verifyBTR(record, key, { crypto, btrCodec: new AlteredSigningBytesCodec(), + stateCodec: defaultCodec, }); expect(verification.valid).toBe(false); @@ -152,9 +155,11 @@ describe('BTR signing-byte ownership behavior', () => { }); it('rejects raw runtime construction outside the canonical encoder path', () => { - expect(() => Reflect.construct( - runtimeSigningBytesConstructor(), - [new Uint8Array([1]), Symbol('raw-signing-bytes')], - )).toThrow('BtrSigningBytes must be created by the canonical BTR signing encoder'); + expect(() => + Reflect.construct(runtimeSigningBytesConstructor(), [ + new Uint8Array([1]), + Symbol('raw-signing-bytes'), + ]) + ).toThrow('BtrSigningBytes must be created by the canonical BTR signing encoder'); }); }); diff --git a/test/conformance/snapshotPublicApiSurface.test.ts b/test/conformance/snapshotPublicApiSurface.test.ts index b46334df7..81585caf3 100644 --- a/test/conformance/snapshotPublicApiSurface.test.ts +++ b/test/conformance/snapshotPublicApiSurface.test.ts @@ -1,12 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { - WarpCore, - ImmutableBytes, - SnapshotORSet, - SnapshotVersionVector, - SnapshotWarpState, - type SnapshotPropValue, -} from '../../legacy.ts'; +import WarpCore from '../../src/domain/WarpCore.ts'; +import ImmutableBytes from '../../src/domain/services/snapshot/ImmutableBytes.ts'; +import SnapshotORSet from '../../src/domain/services/snapshot/SnapshotORSet.ts'; +import type { SnapshotPropValue } from '../../src/domain/services/snapshot/SnapshotPropValue.ts'; +import SnapshotVersionVector from '../../src/domain/services/snapshot/SnapshotVersionVector.ts'; +import SnapshotWarpState from '../../src/domain/services/snapshot/SnapshotWarpState.ts'; type PublicPropBag = Readonly<{ [key: string]: SnapshotPropValue }>; type PublicVisibleEdge = { diff --git a/test/conformance/v18BoundedMemoryLargeGraphGate.test.ts b/test/conformance/v18BoundedMemoryLargeGraphGate.test.ts index a91c41401..0b323a13d 100644 --- a/test/conformance/v18BoundedMemoryLargeGraphGate.test.ts +++ b/test/conformance/v18BoundedMemoryLargeGraphGate.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { openWarpWorldline } from '../../legacy.ts'; +import { openWarpWorldline } from '../../src/domain/WarpWorldline.ts'; import MemoryBudgetError from '../../src/domain/errors/MemoryBudgetError.ts'; import BoundedQueryReadModel from '../../src/domain/services/query/BoundedQueryReadModel.ts'; import InMemoryGraphAdapter from '../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; diff --git a/test/conformance/v18CoordinateOpticPublicPath.test.ts b/test/conformance/v18CoordinateOpticPublicPath.test.ts index c7f9d0906..e50e481fe 100644 --- a/test/conformance/v18CoordinateOpticPublicPath.test.ts +++ b/test/conformance/v18CoordinateOpticPublicPath.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { openWarpWorldline } from '../../legacy.ts'; +import { openWarpWorldline } from '../../src/domain/WarpWorldline.ts'; import InMemoryGraphAdapter from '../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import { openRuntimeHostProduct } from '../../src/domain/warp/RuntimeHostProduct.ts'; import type { PatchBuilder } from '../../src/domain/services/PatchBuilder.ts'; @@ -21,7 +21,9 @@ async function openWorldlineWithOperatorBasis(options: { persistence, graphName: options.worldlineName, writerId: 'app', - ...(options.onDeleteWithData === undefined ? {} : { onDeleteWithData: options.onDeleteWithData }), + ...(options.onDeleteWithData === undefined + ? {} + : { onDeleteWithData: options.onDeleteWithData }), }); await runtime.patch(options.seed); await runtime.materialize(); @@ -30,7 +32,9 @@ async function openWorldlineWithOperatorBasis(options: { persistence, worldlineName: options.worldlineName, writerId: 'app', - ...(options.onDeleteWithData === undefined ? {} : { onDeleteWithData: options.onDeleteWithData }), + ...(options.onDeleteWithData === undefined + ? {} + : { onDeleteWithData: options.onDeleteWithData }), }); } @@ -184,7 +188,9 @@ describe('v18 coordinate optic public path', () => { field: 'nodeId', }, }); - await expect(async () => coordinate.optic().node(NODE_ID).prop('').read()).rejects.toMatchObject({ + await expect(async () => + coordinate.optic().node(NODE_ID).prop('').read() + ).rejects.toMatchObject({ code: 'E_OPTIC_FAILURE_SCHEMA', context: { field: 'propertyKey', diff --git a/test/conformance/v18FirstUseOpticsHonesty.test.ts b/test/conformance/v18FirstUseOpticsHonesty.test.ts index 0ea583579..a80b47ff2 100644 --- a/test/conformance/v18FirstUseOpticsHonesty.test.ts +++ b/test/conformance/v18FirstUseOpticsHonesty.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { openWarpWorldline } from '../../legacy.ts'; +import { openWarpWorldline } from '../../src/domain/WarpWorldline.ts'; import { openRuntimeHostProduct } from '../../src/domain/warp/RuntimeHostProduct.ts'; import InMemoryGraphAdapter from '../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import type CommitMessageCodecPort from '../../src/ports/CommitMessageCodecPort.ts'; @@ -29,7 +29,9 @@ class FirstUseOpticsTrapAdapter extends InMemoryGraphAdapter { readonly patchSha: string; readonly commitMessageCodec: CommitMessageCodecPort; }): Promise { - const patchMessage = options.commitMessageCodec.decodePatch(await this.showNode(options.patchSha)); + const patchMessage = options.commitMessageCodec.decodePatch( + await this.showNode(options.patchSha) + ); this._forbiddenPatchBlobOids.add(patchMessage.patchOid); await this._forbidCheckpointStateBlobReads(options); this._forbidWrites = true; @@ -94,7 +96,7 @@ class FirstUseOpticsTrapAdapter extends InMemoryGraphAdapter { readonly commitMessageCodec: CommitMessageCodecPort; }): Promise { const checkpointMessage = options.commitMessageCodec.decodeCheckpoint( - await this.showNode(options.checkpointSha), + await this.showNode(options.checkpointSha) ); const rootTreeOids = await this.readTreeOids(checkpointMessage.indexOid); const stateTreeOid = rootTreeOids['state']; diff --git a/test/fixtures/mergeConflictCorpus.ts b/test/fixtures/mergeConflictCorpus.ts index 12acf85fe..241018190 100644 --- a/test/fixtures/mergeConflictCorpus.ts +++ b/test/fixtures/mergeConflictCorpus.ts @@ -96,7 +96,7 @@ const variants: readonly CorpusVariant[] = Object.freeze([ { slug: 'type-surface', primaryFile: 'index.ts', - secondaryFile: 'test/type-check/consumer.ts', + secondaryFile: 'test/type-check/v19-consumer.ts', writers: Object.freeze(['surface-a', 'surface-b']), }, { diff --git a/test/type-check/consumer.ts b/test/type-check/consumer.ts deleted file mode 100644 index 4c648b1ef..000000000 --- a/test/type-check/consumer.ts +++ /dev/null @@ -1,682 +0,0 @@ -/** - * Consumer smoke test -- compile-only. - * - * Exercises the current package-root API surface of @git-stunts/git-warp. - * This file is not executed; the TypeScript compiler is the test. - */ - -import WarpAppDefault, { - GitGraphAdapter, - InMemoryGraphAdapter, - GraphNode, - BitmapIndexBuilder, - BitmapIndexReader, - IndexRebuildService, - HealthCheckService, - HealthStatus, - CommitDagTraversalService, - BisectService, - GraphPersistencePort, - IndexStoragePort, - LoggerPort, - NoOpLogger, - ConsoleLogger, - LogLevel, - SeekCachePort, - BlobStoragePort, - InMemoryBlobStorageAdapter, - CryptoPort, - HttpServerPort, - NodeCryptoAdapter, - WebCryptoAdapter, - BunHttpAdapter, - DenoHttpAdapter, - AuditError, - EncryptionError, - PatchError, - ForkError, - IndexError, - QueryError, - SchemaUnsupportedError, - ShardLoadError, - ShardCorruptionError, - ShardValidationError, - StorageError, - TraversalError, - OperationAbortedError, - SyncError, - StrandError, - WormholeError, - WriterError, - checkAborted, - createTimeoutSignal, - openWarpWorldline, - openWarpGraph, - WarpOpenOptions, - WarpApp, - WarpCore, - WarpWorldline, - WarpWorldlineCoordinate, - WarpWorldlineOpticBasis, - ContinuumEvidencePosture, - Optic, - OpticAperturePosture, - OpticBasisPosture, - OpticCoordinatePosture, - OpticSupportRule, - ProjectionHandle, - WorldlineSelector, - LiveSelector, - CoordinateSelector, - StrandSelector, - QueryBuilder, - Observer, - PatchBuilder, - PatchSession, - Writer, - ProvenanceIndex, - computeTranslationCost, - createNodeAdd, - createNodeTombstone, - createEdgeAdd, - createEdgeTombstone, - createPropSet, - createInlineValue, - createBlobValue, - encodeEdgePropKey, - decodeEdgePropKey, - isEdgePropKey, - CONTENT_PROPERTY_KEY, - WarpStateIndexBuilder, - buildWarpStateIndex, - computeStateHash, - projectState, - createStateReader, - compareVisibleState, - GraphDiff, - ImmutableBytes, - SnapshotORSet, - SnapshotVersionVector, - SnapshotWarpState, - SyncSecret, - normalizeVisibleStateScope, - scopeMaterializedState, - exportCoordinateComparisonFact, - exportCoordinateTransferPlanFact, - createTickReceipt, - tickReceiptCanonicalJson, - TICK_RECEIPT_OP_TYPES, - TICK_RECEIPT_RESULT_TYPES, - ProvenancePayload, - BTR, - createBTR, - verifyBTR, - replayBTR, - createWormhole, - composeWormholes, - replayWormhole, - serializeWormhole, - deserializeWormhole, - EffectSinkPort, - MultiplexSink, - EffectPipeline, - createEffectEmission, - canonicalEmissionJson, - createDeliveryObservation, - canonicalObservationJson, - createExternalizationPolicy, - DELIVERY_MODES, - DELIVERY_OUTCOMES, - LIVE_LENS, - REPLAY_LENS, - INSPECT_LENS, - NoOpEffectSink, - ConsoleEffectSink, - ChunkEffectSink, - type Aperture, - type ObserverConfig, - type PropValue, - type SnapshotPropValue, - type SyncRateLimitConfig, - type WarpKernelPort, - type WarpWorldlineOpenOptions, - type WarpWorldlinePatchBuild, - type OpticContextValue, - type OpticFields, - type OpticPostureFields, - type GraphDiffOptions, - type WarpWorldlineCoordinateFrontierEntry, -} from '../../legacy.ts'; - -import { - WarpError as BrowserWarpError, - VersionVector as BrowserVersionVector, - generateWriterId as browserGenerateWriterId, -} from '../../browser.ts'; - -type PublicPropBag = Readonly<{ [key: string]: SnapshotPropValue }>; -type PublicVisibleEdge = Readonly<{ - from: string; - to: string; - label: string; - props: PublicPropBag; -}>; - -declare const persistence: GraphPersistencePort; -const kernelPersistence: WarpKernelPort = persistence; -declare const indexStorage: IndexStoragePort; -declare const logger: LoggerPort; -declare const crypto: CryptoPort; -declare const seekCache: SeekCachePort; -declare const httpPort: HttpServerPort; -declare const liveState: Parameters[0]; -declare const btrCodecOptions: Parameters[2]; -declare const btrVerifyOptions: Parameters[2]; - -const sameAppCtor: typeof WarpAppDefault = WarpApp; -const parsedOpenOptions = new WarpOpenOptions({ - persistence, - graphName: 'consumer-open-options', - writerId: 'writer-open-options', -}); - -const exportedRuntimeSurface = [ - GitGraphAdapter, - InMemoryGraphAdapter, - GraphNode, - BitmapIndexBuilder, - BitmapIndexReader, - IndexRebuildService, - HealthCheckService, - HealthStatus, - CommitDagTraversalService, - BisectService, - GraphPersistencePort, - IndexStoragePort, - LoggerPort, - NoOpLogger, - ConsoleLogger, - LogLevel, - SeekCachePort, - BlobStoragePort, - InMemoryBlobStorageAdapter, - CryptoPort, - HttpServerPort, - NodeCryptoAdapter, - WebCryptoAdapter, - BunHttpAdapter, - DenoHttpAdapter, - AuditError, - EncryptionError, - PatchError, - ForkError, - IndexError, - QueryError, - SchemaUnsupportedError, - ShardLoadError, - ShardCorruptionError, - ShardValidationError, - StorageError, - TraversalError, - OperationAbortedError, - SyncError, - StrandError, - WormholeError, - WriterError, - WarpOpenOptions, - WarpWorldline, - openWarpGraph, - WarpApp, - WarpCore, - ProjectionHandle, - WorldlineSelector, - LiveSelector, - CoordinateSelector, - StrandSelector, - QueryBuilder, - Observer, - PatchBuilder, - PatchSession, - Writer, - ProvenanceIndex, - WarpStateIndexBuilder, - ImmutableBytes, - SnapshotORSet, - SnapshotVersionVector, - SnapshotWarpState, - WarpWorldlineCoordinate, - WarpWorldlineOpticBasis, - Optic, - OpticAperturePosture, - OpticBasisPosture, - OpticCoordinatePosture, - OpticSupportRule, - ProvenancePayload, - BTR, - EffectSinkPort, - MultiplexSink, - EffectPipeline, - NoOpEffectSink, - ConsoleEffectSink, - ChunkEffectSink, -] as const; - -const exportedFunctionSurface = [ - checkAborted, - createTimeoutSignal, - openWarpWorldline, - computeTranslationCost, - createNodeAdd, - createNodeTombstone, - createEdgeAdd, - createEdgeTombstone, - createPropSet, - createInlineValue, - createBlobValue, - encodeEdgePropKey, - decodeEdgePropKey, - isEdgePropKey, - buildWarpStateIndex, - computeStateHash, - projectState, - createStateReader, - compareVisibleState, - normalizeVisibleStateScope, - scopeMaterializedState, - exportCoordinateComparisonFact, - exportCoordinateTransferPlanFact, - createTickReceipt, - tickReceiptCanonicalJson, - createBTR, - verifyBTR, - replayBTR, - createWormhole, - composeWormholes, - replayWormhole, - serializeWormhole, - deserializeWormhole, - createEffectEmission, - canonicalEmissionJson, - createDeliveryObservation, - canonicalObservationJson, - createExternalizationPolicy, -] as const; - -const exportedConstantSurface = [ - CONTENT_PROPERTY_KEY, - TICK_RECEIPT_OP_TYPES, - TICK_RECEIPT_RESULT_TYPES, - DELIVERY_MODES, - DELIVERY_OUTCOMES, - LIVE_LENS, - REPLAY_LENS, - INSPECT_LENS, -] as const; - -void sameAppCtor; -void exportedRuntimeSurface; -void exportedFunctionSurface; -void exportedConstantSurface; -void kernelPersistence; -void parsedOpenOptions; - -const app: WarpApp = await WarpApp.open({ - graphName: 'consumer-test', - persistence, - writerId: 'writer-1', - logger, - crypto, - seekCache, - autoMaterialize: true, - onDeleteWithData: 'reject', - trust: { mode: 'off' }, -}); - -const graph: WarpCore = app.core(); -const graphBag = await openWarpGraph({ - persistence, - graphName: 'consumer-test', - writerId: 'writer-2', - logger, - crypto, - seekCache, - trust: { mode: 'off' }, -}); - -const graphName: string = graph.graphName; -const writerId: string = graph.writerId; -const graphBagName: string = graphBag.graphName; -const graphBagWriter: string = graphBag.writerId; -void graphName; -void writerId; -void graphBagName; -void graphBagWriter; - -const worldlineOptions: WarpWorldlineOpenOptions = { - persistence, - worldlineName: 'consumer-worldline', - writerId: 'writer-worldline', - logger, - crypto, - seekCache, - trust: { mode: 'off' }, -}; -const warpWorldline: WarpWorldline = await openWarpWorldline(worldlineOptions); -const worldlinePatchBuild: WarpWorldlinePatchBuild = (patch) => { - patch.addNode('worldline-node'); -}; -const publicUsersAperture: Aperture = { - match: 'user:*', - expose: ['name'], - redact: ['secret'], -}; -const publicUsersObserverConfig: ObserverConfig = publicUsersAperture; -const worldlinePatchSha: string = await warpWorldline.commit(worldlinePatchBuild); -const worldlineOpticBasis: WarpWorldlineOpticBasis = await warpWorldline.prepareOpticBasis(); -const worldlineCoordinate: WarpWorldlineCoordinate = await warpWorldline.coordinate(); -const worldlineLive: ProjectionHandle = warpWorldline.live(); -const worldlineHistorical: ProjectionHandle = await warpWorldline.seek({ - source: { kind: 'live', ceiling: 1 }, -}); -const appObserver: Observer = await app.observer(publicUsersAperture); -const appNamedApertureObserver: Observer = await app.observer('public-users', publicUsersAperture); -const appAliasApertureObserver: Observer = await app.observer(publicUsersObserverConfig); -const worldlineObserver: Observer = await warpWorldline.observer({ match: '*' }); -const namedApertureObserver: Observer = await warpWorldline.observer('public-users', publicUsersAperture); -const aliasApertureObserver: Observer = await warpWorldline.observer(publicUsersObserverConfig); -const coordinateFrontierEntries: readonly WarpWorldlineCoordinateFrontierEntry[] = - worldlineCoordinate.frontierEntries; -const coordinateSource = worldlineCoordinate.source(); -const coordinateOptic = worldlineCoordinate.optic().node('worldline-node').toOptic(); -const coordinateOpticContext: OpticContextValue = coordinateOptic.toContextValue(); -const coordinateOpticNode = await worldlineCoordinate.optic().node('worldline-node').read(); -const coordinateOpticAlive: boolean = coordinateOpticNode.alive; -const publicOpticPosture: OpticPostureFields = { - coordinatePosture: OpticCoordinatePosture.capturedCoordinate(), - aperturePosture: OpticAperturePosture.defaultFullRead(), - basisPosture: OpticBasisPosture.checkpointTailBasisVerified(), - evidencePosture: ContinuumEvidencePosture.translatedGitWarpEvidence(), -}; -const publicOpticFields: OpticFields = { - ...publicOpticPosture, - target: coordinateOptic.target, - supportRule: OpticSupportRule.exactEntity(), -}; -const publicOptic: Optic = new Optic(publicOpticFields); - -void worldlinePatchSha; -void worldlineOpticBasis; -void coordinateOptic; -void coordinateOpticContext; -void publicOpticPosture; -void publicOpticFields; -void publicOptic; -void coordinateFrontierEntries; -void coordinateSource; -void coordinateOpticAlive; -void worldlineLive; -void worldlineHistorical; -void appObserver; -void appNamedApertureObserver; -void appAliasApertureObserver; -void worldlineObserver; -void namedApertureObserver; -void aliasApertureObserver; - -const materialized: SnapshotWarpState = await graph.materialize(); -const materializedWithReceipts: { state: SnapshotWarpState; receipts: readonly ReturnType[] } = - await graph.materialize({ receipts: true }); -const graphDiffOptions: GraphDiffOptions = { from: 0, to: 1, targetId: 'node-a' }; -const graphDiff: GraphDiff = await graph.diff(graphDiffOptions); -const stateSnapshot: SnapshotWarpState | null = await graph.getStateSnapshot(); -const graphBagStateSnapshot: SnapshotWarpState | null = await graphBag.query.getStateSnapshot(); -const graphBagNodeProps: PublicPropBag | null = await graphBag.query.getNodeProps('node-a'); -const graphBagQueryBuilder: QueryBuilder = graphBag.query.query(); -const graphBagWorldline: ProjectionHandle = graphBag.query.worldline(); -const graphBagObserver: Observer = await graphBag.query.observer({ match: '*' }); -const nodeAlive: SnapshotORSet = materialized.nodeAlive; -const observedFrontier: SnapshotVersionVector = materialized.observedFrontier; -const snapshotValue: SnapshotPropValue | undefined = [...materialized.prop.values()][0]?.value; -const propValue: PropValue = new Uint8Array([1, 2, 3]); - -if (snapshotValue instanceof ImmutableBytes) { - const bytes: Uint8Array = snapshotValue.toUint8Array(); - const byteArray: readonly number[] = snapshotValue.toArray(); - const byteLength: number = snapshotValue.length; - const firstByte: number | undefined = snapshotValue.at(0); - void bytes; - void byteArray; - void byteLength; - void firstByte; -} - -void materializedWithReceipts; -void graphDiff; -void stateSnapshot; -void graphBagStateSnapshot; -void graphBagNodeProps; -void graphBagQueryBuilder; -void graphBagWorldline; -void graphBagObserver; -void nodeAlive; -void observedFrontier; -void propValue; - -const nodeProps: PublicPropBag | null = await graph.getNodeProps('node-a'); -const edgeProps: PublicPropBag | null = await graph.getEdgeProps('node-a', 'node-b', 'knows'); -const edges: PublicVisibleEdge[] = await graph.getEdges(); -const neighbors: Array<{ nodeId: string; label: string; direction: 'outgoing' | 'incoming' }> = - await graph.neighbors('node-a'); -const propertyCount: number = await graph.getPropertyCount(); -const queryBuilder: QueryBuilder = graph.query(); -const observer: Observer = await graph.observer({ match: '*' }); -const worldline: ProjectionHandle = graph.worldline(); - -void nodeProps; -void edgeProps; -void edges; -void neighbors; -void propertyCount; -void queryBuilder; -void observer; -void worldline; - -const patchBuilder: PatchBuilder = await graph.createPatch(); -const chainedPatchBuilder: PatchBuilder = patchBuilder - .addNode('node-a') - .addEdge('node-a', 'node-b', 'knows') - .setProperty('node-a', 'name', 'Alice'); -const builtPatch = patchBuilder.build(); -const committedSha: string = await patchBuilder.commit(); -const patchSha: string = await graph.patch((patch) => { - patch.addNode('node-c'); -}); -const writer: Writer = await graph.writer(); -const patchSession: PatchSession = await writer.beginPatch(); -const patchSessionSha: string = await patchSession.addNode('node-d').commit(); - -void chainedPatchBuilder; -void builtPatch; -void committedSha; -void patchSha; -void patchSessionSha; - -const inlineValue = createInlineValue('value'); -const blobValue = createBlobValue('oid'); -const nodeAdd = createNodeAdd('node-a'); -const nodeTombstone = createNodeTombstone('node-a'); -const edgeAdd = createEdgeAdd('node-a', 'node-b', 'knows'); -const edgeTombstone = createEdgeTombstone('node-a', 'node-b', 'knows'); -const propSet = createPropSet('node-a', 'name', inlineValue); - -void blobValue; -void nodeAdd; -void nodeTombstone; -void edgeAdd; -void edgeTombstone; -void propSet; - -const receipt = createTickReceipt({ - patchSha: 'abc123', - writer: 'writer-1', - lamport: 1, - ops: [{ op: 'NodeAdd', target: 'node-a', result: 'applied' }], -}); -const receiptJson: string = tickReceiptCanonicalJson(receipt); -void receiptJson; - -const payload = ProvenancePayload.identity(); -const payloadEntries = payload.entries(); -const payloadBack: ProvenancePayload = ProvenancePayload.fromEntries(payloadEntries); -const btrRecord = await createBTR(liveState, payloadBack, btrCodecOptions); -const btrVerified: Awaited> = await verifyBTR(btrRecord, 'secret', btrVerifyOptions); -const btrReplayed: Awaited> = await replayBTR(btrRecord, { - crypto, -}); - -void btrVerified; -void btrReplayed; - -const wormhole = await createWormhole({ - persistence, - graphName: 'consumer-test', - fromSha: 'source-sha', - toSha: 'target-sha', -}); -const composedWormhole = await composeWormholes(wormhole, wormhole); -const wormholeState = replayWormhole(composedWormhole); -const serializedWormhole = serializeWormhole(composedWormhole); -const deserializedWormhole = deserializeWormhole(serializedWormhole); - -void wormholeState; -void deserializedWormhole; - -const encodedEdgePropKey: string = encodeEdgePropKey('node-a', 'node-b', 'knows', 'weight'); -const decodedEdgePropKey = decodeEdgePropKey(encodedEdgePropKey); -const edgePropKeyCheck: boolean = isEdgePropKey(encodedEdgePropKey); - -void decodedEdgePropKey; -void edgePropKeyCheck; - -const reader = createStateReader(liveState); -const snapshotReader = createStateReader(materialized); -const readerProps: PublicPropBag | null = reader.getNodeProps('node-a'); -const readerEdges: PublicVisibleEdge[] = reader.getEdges(); -const comparison = compareVisibleState(liveState, liveState, { targetId: 'node-a' }); - -void snapshotReader; -void readerProps; -void readerEdges; -void comparison; - -const indexBuilder = new BitmapIndexBuilder(); -const nodeId: number = indexBuilder.registerNode('sha-a'); -indexBuilder.addEdge('sha-a', 'sha-b'); -const serializedIndex: Record = indexBuilder.serialize(); -const indexReader = new BitmapIndexReader({ storage: indexStorage, strict: true, logger }); -indexReader.setup({ 'meta_ab.cbor': '0123456789012345678901234567890123456789' }); -const indexedId: Promise = indexReader.lookupId('sha-a'); -const parents: Promise = indexReader.getParents('sha-a'); -const children: Promise = indexReader.getChildren('sha-a'); - -void nodeId; -void serializedIndex; -void indexedId; -void parents; -void children; - -const timeoutSignal: AbortSignal = createTimeoutSignal(1000); -checkAborted(timeoutSignal, 'consumer-test'); -const server = await graph.serve({ - port: 3000, - httpPort, - unsafeAllowUnauthenticatedLocalhost: true, -}); -const serverUrl: string = server.url; -await server.close(); -const syncSecret: SyncSecret = SyncSecret.fromString('shared-secret'); -const syncRateLimit: SyncRateLimitConfig = { - capacity: 20, - refillTokensPerSecond: 5, - clock: () => 0, -}; -const authedServer = await graph.serve({ - port: 3001, - httpPort, - auth: { keys: { default: syncSecret }, mode: 'enforce', rateLimit: syncRateLimit }, -}); -await graph.syncWith(authedServer.url, { - auth: { secret: syncSecret, keyId: 'default' }, -}); -await authedServer.close(); - -void serverUrl; -void syncSecret; -void syncRateLimit; - -const browserError: BrowserWarpError = new BrowserWarpError('browser smoke', 'E_BROWSER_SMOKE'); -const browserVector: BrowserVersionVector = BrowserVersionVector.empty(); -const browserWriterId: string = browserGenerateWriterId(); - -void browserError; -void browserVector; -void browserWriterId; - -// Negative checks. - -// @ts-expect-error materialize capability bag is not public on v17 WarpGraph. -const badGraphBagMaterialize = graphBag.materialize; - -// @ts-expect-error nested materialize frontdoor is not public on v17 WarpGraph. -const badGraphBagNestedMaterialize = graphBag.materialize.materialize; - -// @ts-expect-error query readings do not expose materialization. -const badGraphBagQueryMaterialize = graphBag.query.materialize; - -const badWorldlineGraphAlias: WarpWorldlineOpenOptions = { - persistence, - worldlineName: 'consumer-worldline', - writerId: 'writer-worldline', - // @ts-expect-error WarpWorldline open options do not accept graphName. - graphName: 'legacy-graph', -}; - -// @ts-expect-error WarpWorldline open options require worldlineName. -const badWorldlineMissingName: WarpWorldlineOpenOptions = { - persistence, - writerId: 'writer-worldline', -}; - -// @ts-expect-error WarpWorldline does not expose materialization. -const badWarpWorldlineMaterialize = warpWorldline.materialize; - -// @ts-expect-error WarpWorldline does not expose graphName. -const badWarpWorldlineGraphName = warpWorldline.graphName; - -// @ts-expect-error sync auth secrets must be explicit SyncSecret values. -await graph.syncWith(serverUrl, { auth: { secret: 'shared-secret', keyId: 'default' } }); - -// @ts-expect-error sync auth key maps must carry SyncSecret values. -await graph.serve({ port: 3002, httpPort, auth: { keys: { default: 'shared-secret' } } }); - -// @ts-expect-error materialize() does not return a string. -const badMaterialized: string = await graph.materialize(); - -// @ts-expect-error hasNode requires a string node id. -const badHasNode: boolean = await graph.hasNode(42); - -// @ts-expect-error getEdgeProps requires from, to, and label. -await graph.getEdgeProps('node-a', 'node-b'); - -// @ts-expect-error createNodeAdd requires a string node id. -createNodeAdd(42); - -void badGraphBagMaterialize; -void badGraphBagNestedMaterialize; -void badGraphBagQueryMaterialize; -void badWorldlineGraphAlias; -void badWorldlineMissingName; -void badWarpWorldlineMaterialize; -void badWarpWorldlineGraphName; -void badMaterialized; -void badHasNode; diff --git a/test/type-check/tsconfig.json b/test/type-check/tsconfig.json index 6baa47349..beed01c4d 100644 --- a/test/type-check/tsconfig.json +++ b/test/type-check/tsconfig.json @@ -9,7 +9,6 @@ "noUnusedParameters": false }, "include": [ - "consumer.ts", "runtime-declarations.d.ts", "v19-consumer.ts", "v19-first-use.ts", diff --git a/test/type-check/v19-first-use.ts b/test/type-check/v19-first-use.ts index ddc152311..dd051ae3d 100644 --- a/test/type-check/v19-first-use.ts +++ b/test/type-check/v19-first-use.ts @@ -2,7 +2,7 @@ * v19 first-use consumer fixture -- compile-only. * * This fixture intentionally imports only the root first-use verbs and storage - * subpath adapter. Advanced, diagnostic, and legacy nouns are out-of-band. + * subpath adapter. Graph-first compatibility nouns are not public in v19. */ import { intent, openWarp, reading } from '../../index.ts'; diff --git a/test/type-check/v19-subpaths.ts b/test/type-check/v19-subpaths.ts index a8f44b50d..07a311f6a 100644 --- a/test/type-check/v19-subpaths.ts +++ b/test/type-check/v19-subpaths.ts @@ -1,33 +1,13 @@ /** * v19 explicit subpath consumer fixture -- compile-only. * - * Storage, advanced, diagnostics, and legacy imports stay reachable only from - * their named compatibility surfaces. + * Storage, advanced, and diagnostics imports stay reachable only from their + * named expert surfaces. */ -import { - GitStorageAdapter, - MemoryStorageAdapter, - NodeCryptoAdapter, -} from '../../storage.ts'; -import { - GitWarpTickHologram, - Observer, - Optic, - ProjectionHandle, -} from '../../advanced.ts'; -import { - QueryBuilder, - TtdMergeInspector, - normalizeVisibleStateScope, -} from '../../diagnostics.ts'; -import { - InMemoryGraphAdapter, - PatchBuilder, - WarpWorldline, - openWarpGraph, - openWarpWorldline, -} from '../../legacy.ts'; +import { GitStorageAdapter, MemoryStorageAdapter, NodeCryptoAdapter } from '../../storage.ts'; +import { GitWarpTickHologram, Observer, Optic, ProjectionHandle } from '../../advanced.ts'; +import { QueryBuilder, TtdMergeInspector, normalizeVisibleStateScope } from '../../diagnostics.ts'; const storageAdapter: typeof MemoryStorageAdapter = MemoryStorageAdapter; const gitStorageAdapter: typeof GitStorageAdapter = GitStorageAdapter; @@ -38,9 +18,6 @@ const projectionHandle: typeof ProjectionHandle = ProjectionHandle; const tickHologram: typeof GitWarpTickHologram = GitWarpTickHologram; const queryBuilder: typeof QueryBuilder = QueryBuilder; const ttdMergeInspector: typeof TtdMergeInspector = TtdMergeInspector; -const legacyAdapter: typeof InMemoryGraphAdapter = InMemoryGraphAdapter; -const patchBuilder: typeof PatchBuilder = PatchBuilder; -const worldline: typeof WarpWorldline = WarpWorldline; void storageAdapter; void gitStorageAdapter; @@ -52,8 +29,3 @@ void tickHologram; void queryBuilder; void ttdMergeInspector; void normalizeVisibleStateScope; -void legacyAdapter; -void patchBuilder; -void worldline; -void openWarpGraph; -void openWarpWorldline; diff --git a/test/unit/cli/optic.test.ts b/test/unit/cli/optic.test.ts index dea89eb5e..35edf4dd6 100644 --- a/test/unit/cli/optic.test.ts +++ b/test/unit/cli/optic.test.ts @@ -9,12 +9,12 @@ vi.mock('../../../bin/cli/shared.ts', () => ({ listGraphNames: vi.fn(), })); -vi.mock('../../../legacy.ts', () => ({ +vi.mock('../../../src/domain/WarpWorldline.ts', () => ({ openWarpWorldline: vi.fn(), })); const shared = await import('../../../bin/cli/shared.ts'); -const api = await import('../../../legacy.ts'); +const api = await import('../../../src/domain/WarpWorldline.ts'); const createPersistence = shared.createPersistence as ReturnType; const resolveGraphName = shared.resolveGraphName as ReturnType; const listGraphNames = shared.listGraphNames as ReturnType; @@ -45,7 +45,11 @@ const READ_IDENTITY = Object.freeze({ Object.freeze({ path: 'props_12.cbor', oid: 'cccccccccccccccccccccccccccccccccccccccc' }), ]), tailWitnesses: Object.freeze([ - Object.freeze({ sha: 'dddddddddddddddddddddddddddddddddddddddd', writerId: 'alice', lamport: 3 }), + Object.freeze({ + sha: 'dddddddddddddddddddddddddddddddddddddddd', + writerId: 'alice', + lamport: 3, + }), ]), reducerVersion: 'checkpoint-tail-locator', projectionVersion: 'optic-read-foundation', @@ -122,10 +126,12 @@ describe('optic command', () => { it('emits obstruction payloads for bounded-basis failures', async () => { openWarpWorldline.mockResolvedValue({ - prepareOpticBasis: vi.fn().mockRejectedValue(new QueryError('No bounded basis.', { - code: 'E_OPTIC_NO_BOUNDED_BASIS', - context: { reason: 'missing-checkpoint' }, - })), + prepareOpticBasis: vi.fn().mockRejectedValue( + new QueryError('No bounded basis.', { + code: 'E_OPTIC_NO_BOUNDED_BASIS', + context: { reason: 'missing-checkpoint' }, + }) + ), coordinate: vi.fn(), }); @@ -163,10 +169,12 @@ describe('optic command', () => { coordinate: vi.fn().mockResolvedValue({ optic: () => ({ node: () => ({ - read: vi.fn().mockRejectedValue(new QueryError('Shard unavailable.', { - code: 'E_OPTIC_NO_BOUNDED_BASIS', - context: { reason: 'checkpoint-shard-unavailable' }, - })), + read: vi.fn().mockRejectedValue( + new QueryError('Shard unavailable.', { + code: 'E_OPTIC_NO_BOUNDED_BASIS', + context: { reason: 'checkpoint-shard-unavailable' }, + }) + ), }), }), }), diff --git a/test/unit/domain/WarpApp.facade.test.ts b/test/unit/domain/WarpApp.facade.test.ts index f0321b07b..0e8866873 100644 --- a/test/unit/domain/WarpApp.facade.test.ts +++ b/test/unit/domain/WarpApp.facade.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; -import WarpApp, { InMemoryGraphAdapter, WarpCore } from '../../../legacy.ts'; +import WarpApp from '../../../src/domain/WarpApp.ts'; +import WarpCore from '../../../src/domain/WarpCore.ts'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; describe('WarpApp facade', () => { it('exposes a curated app surface with an explicit core escape hatch', async () => { @@ -20,7 +22,7 @@ describe('WarpApp facade', () => { expect(typeof app.createStrand).toBe('function'); expect(typeof app.core).toBe('function'); - const appAny = (app); + const appAny = app; expect((appAny as any).materialize).toBeUndefined(); expect((appAny as any).materializeCoordinate).toBeUndefined(); expect((appAny as any).getNodes).toBeUndefined(); diff --git a/test/unit/domain/WarpCore.apiSurface.test.ts b/test/unit/domain/WarpCore.apiSurface.test.ts index 40840d496..fabc5fb8b 100644 --- a/test/unit/domain/WarpCore.apiSurface.test.ts +++ b/test/unit/domain/WarpCore.apiSurface.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { InMemoryGraphAdapter } from '../../../legacy.ts'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import WarpCore from '../../../src/domain/WarpCore.ts'; describe('WarpCore API surface', () => { @@ -29,8 +29,14 @@ describe('WarpCore API surface', () => { it('keeps effect accessors on the prototype surface', () => { const effectPipeline = Object.getOwnPropertyDescriptor(WarpCore.prototype, 'effectPipeline'); const effectEmissions = Object.getOwnPropertyDescriptor(WarpCore.prototype, 'effectEmissions'); - const deliveryObservations = Object.getOwnPropertyDescriptor(WarpCore.prototype, 'deliveryObservations'); - const externalizationPolicy = Object.getOwnPropertyDescriptor(WarpCore.prototype, 'externalizationPolicy'); + const deliveryObservations = Object.getOwnPropertyDescriptor( + WarpCore.prototype, + 'deliveryObservations' + ); + const externalizationPolicy = Object.getOwnPropertyDescriptor( + WarpCore.prototype, + 'externalizationPolicy' + ); expect(typeof effectPipeline?.get).toBe('function'); expect(typeof effectPipeline?.set).toBe('function'); diff --git a/test/unit/domain/WarpCore.effectPipeline.test.ts b/test/unit/domain/WarpCore.effectPipeline.test.ts index f4a478706..f41a99fa4 100644 --- a/test/unit/domain/WarpCore.effectPipeline.test.ts +++ b/test/unit/domain/WarpCore.effectPipeline.test.ts @@ -1,13 +1,10 @@ import { describe, it, expect } from 'vitest'; -import { - WarpCore, - InMemoryGraphAdapter, - EffectPipeline, - MultiplexSink, - NoOpEffectSink, - LIVE_LENS, - REPLAY_LENS, -} from '../../../legacy.ts'; +import WarpCore from '../../../src/domain/WarpCore.ts'; +import { EffectPipeline } from '../../../src/domain/services/EffectPipeline.ts'; +import { MultiplexSink } from '../../../src/domain/services/MultiplexSink.ts'; +import { LIVE_LENS, REPLAY_LENS } from '../../../src/domain/types/ExternalizationPolicy.ts'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; +import { NoOpEffectSink } from '../../../src/infrastructure/adapters/NoOpEffectSink.ts'; async function openCore(extra = {}): Promise { return await WarpCore.open({ @@ -32,7 +29,7 @@ describe('WarpCore — effect pipeline (host-domain infra)', () => { const mux = new MultiplexSink(); mux.addSink(new NoOpEffectSink()); const pipeline = new EffectPipeline({ - sink: ((mux) as any), + sink: mux as any, lens: LIVE_LENS, }); @@ -57,7 +54,7 @@ describe('WarpCore — effect pipeline (host-domain infra)', () => { const mux = new MultiplexSink(); mux.addSink(new NoOpEffectSink()); core.effectPipeline = new EffectPipeline({ - sink: ((mux) as any), + sink: mux as any, lens: LIVE_LENS, }); @@ -113,8 +110,12 @@ describe('WarpCore — effect pipeline (host-domain infra)', () => { externalizationPolicy: LIVE_LENS, }); - const pipeline = (core.effectPipeline as EffectPipeline); - const result = await pipeline.emit('notification', { text: 'hi' }, { id: 'emit-1', timestamp: 42 }); + const pipeline = core.effectPipeline as EffectPipeline; + const result = await pipeline.emit( + 'notification', + { text: 'hi' }, + { id: 'emit-1', timestamp: 42 } + ); expect(result.emission.kind).toBe('notification'); expect(result.observations).toHaveLength(1); @@ -126,7 +127,7 @@ describe('WarpCore — effect pipeline (host-domain infra)', () => { externalizationPolicy: LIVE_LENS, }); - const pipeline = (core.effectPipeline as EffectPipeline); + const pipeline = core.effectPipeline as EffectPipeline; await pipeline.emit('a', 1, { id: 'emit-a', timestamp: 1 }); await pipeline.emit('b', 2, { id: 'emit-b', timestamp: 2 }); await pipeline.emit('c', 3, { id: 'emit-c', timestamp: 3 }); @@ -141,7 +142,7 @@ describe('WarpCore — effect pipeline (host-domain infra)', () => { externalizationPolicy: REPLAY_LENS, }); - const pipeline = (core.effectPipeline as EffectPipeline); + const pipeline = core.effectPipeline as EffectPipeline; const result = await pipeline.emit('test', null, { id: 'emit-replay', timestamp: 0 }); expect(result.observations[0]?.outcome).toBe('suppressed'); diff --git a/test/unit/domain/WarpCore.emit.test.ts b/test/unit/domain/WarpCore.emit.test.ts index f887f15d4..5c8d5417d 100644 --- a/test/unit/domain/WarpCore.emit.test.ts +++ b/test/unit/domain/WarpCore.emit.test.ts @@ -1,19 +1,17 @@ import { describe, it, expect } from 'vitest'; -import { - WarpCore, - InMemoryGraphAdapter, -} from '../../../legacy.ts'; +import WarpCore from '../../../src/domain/WarpCore.ts'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import { EFFECT_NODE_PREFIX } from '../../../src/domain/services/KeyCodec.ts'; type WarpCoreWired = Awaited>; async function openCore(extra = {}): Promise { - return await WarpCore.open({ + return (await WarpCore.open({ persistence: new InMemoryGraphAdapter(), graphName: 'emit-test', writerId: 'writer-1', ...extra, - }) as WarpCoreWired; + })) as WarpCoreWired; } describe('PatchBuilder.emitEffect() — graph entity behavior', () => { @@ -23,7 +21,7 @@ describe('PatchBuilder.emitEffect() — graph entity behavior', () => { describe('writes effect graph entities', () => { it('creates a node with the effect prefix', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; await core.patch((p) => { effectId = (p as any).emitEffect('notification', { text: 'hello' }); }); @@ -37,60 +35,68 @@ describe('PatchBuilder.emitEffect() — graph entity behavior', () => { it('sets kind property on the effect node', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; await core.patch((p) => { effectId = (p as any).emitEffect('diagnostic', null); }); await core.materialize(); const props = await core.getNodeProps(effectId); - if (props == null) { throw new Error('props should not be null'); } + if (props == null) { + throw new Error('props should not be null'); + } expect(props['kind']).toBe('diagnostic'); }); it('sets writer property from the patch writerId', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; await core.patch((p) => { effectId = (p as any).emitEffect('test', null); }); await core.materialize(); const props = await core.getNodeProps(effectId); - if (props == null) { throw new Error('props should not be null'); } + if (props == null) { + throw new Error('props should not be null'); + } expect(props['writer']).toBe('writer-1'); }); it('canonically serializes complex payloads', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; await core.patch((p) => { effectId = (p as any).emitEffect('export', { format: 'csv', rows: 100 }); }); await core.materialize(); const props = await core.getNodeProps(effectId); - if (props == null) { throw new Error('props should not be null'); } - const parsed = JSON.parse((props['payload'] as string)); + if (props == null) { + throw new Error('props should not be null'); + } + const parsed = JSON.parse(props['payload'] as string); expect(parsed).toEqual({ format: 'csv', rows: 100 }); }); it('does not set payload property for null payload', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; await core.patch((p) => { effectId = (p as any).emitEffect('ping', null); }); await core.materialize(); const props = await core.getNodeProps(effectId); - if (props == null) { throw new Error('props should not be null'); } + if (props == null) { + throw new Error('props should not be null'); + } expect(props['payload']).toBeUndefined(); }); it('generates unique effect IDs across multiple emits', async () => { const core = await openCore(); - const ids = ([]) as string[]; + const ids = [] as string[]; await core.patch((p) => { ids.push((p as any).emitEffect('a', null)); ids.push((p as any).emitEffect('b', null)); @@ -117,7 +123,7 @@ describe('PatchBuilder.emitEffect() — graph entity behavior', () => { await expect( core.patch((p) => { (p as any).emitEffect('', null); - }), + }) ).rejects.toThrow('emitEffect: kind must be a non-empty string'); }); }); @@ -128,7 +134,7 @@ describe('PatchBuilder.emitEffect() — graph entity behavior', () => { describe('same-patch causality', () => { it('effect and its cause share the same patch', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; const patchSha = await core.patch((p) => { p.addNode('user:alice'); p.setProperty('user:alice', 'name', 'Alice'); @@ -154,24 +160,26 @@ describe('PatchBuilder.emitEffect() — graph entity behavior', () => { describe('determinism', () => { it('does not set a timestamp property', async () => { const core = await openCore(); - let effectId = ('') as string; + let effectId = '' as string; await core.patch((p) => { effectId = (p as any).emitEffect('test', null); }); await core.materialize(); const props = await core.getNodeProps(effectId); - if (props == null) { throw new Error('props should not be null'); } + if (props == null) { + throw new Error('props should not be null'); + } expect(props['timestamp']).toBeUndefined(); }); it('canonical payload serialization is deterministic across instances', async () => { const core1 = await openCore(); - const core2 = await WarpCore.open({ + const core2 = (await WarpCore.open({ persistence: new InMemoryGraphAdapter(), graphName: 'emit-test-2', writerId: 'writer-1', - }) as WarpCoreWired; + })) as WarpCoreWired; const payload = { z: 1, a: 2, m: { b: 3, a: 4 } }; const sharedId = `${EFFECT_NODE_PREFIX}determinism-check`; @@ -188,7 +196,9 @@ describe('PatchBuilder.emitEffect() — graph entity behavior', () => { const props1 = await core1.getNodeProps(sharedId); const props2 = await core2.getNodeProps(sharedId); - if (props1 == null || props2 == null) { throw new Error('props should not be null'); } + if (props1 == null || props2 == null) { + throw new Error('props should not be null'); + } expect(props1['payload']).toBe(props2['payload']); }); }); diff --git a/test/unit/domain/WarpFacade.test.ts b/test/unit/domain/WarpFacade.test.ts index c5aa5eb0e..5a5434030 100644 --- a/test/unit/domain/WarpFacade.test.ts +++ b/test/unit/domain/WarpFacade.test.ts @@ -42,24 +42,6 @@ const FORBIDDEN_ROOT_SUBSTRATE_EXPORTS = Object.freeze([ 'GitWarpTickHologramFields', ]); -const FORBIDDEN_BROWSER_V19_EXPORTS = Object.freeze([ - 'openWarp', - 'Warp', - 'Timeline', - 'intent', - 'Intent', - 'reading', - 'Reading', - 'ReadReceipt', - 'ReadingResult', - 'WriteReceipt', - 'DraftTimeline', - 'JoinReceipt', - 'JoinResult', - 'OpenWarpOptions', - 'WarpStorage', -]); - function exportedNamesFor(path: string): ReadonlySet { const sourceFile = sourceFileFor(path); const exportedNames = new Set(); @@ -85,13 +67,7 @@ function allDeclaredNamesFor(path: string): ReadonlySet { function sourceFileFor(path: string): ts.SourceFile { const sourceText = readFileSync(new URL(`../../../${path}`, import.meta.url), 'utf8'); - return ts.createSourceFile( - path, - sourceText, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS, - ); + return ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); } function collectExportDeclarationNames(statement: ts.Statement, exportedNames: Set): void { @@ -118,11 +94,11 @@ function collectDeclaredStatementName(statement: ts.Statement, declaredNames: Se } if ( - (ts.isClassDeclaration(statement) - || ts.isFunctionDeclaration(statement) - || ts.isInterfaceDeclaration(statement) - || ts.isTypeAliasDeclaration(statement)) - && statement.name !== undefined + (ts.isClassDeclaration(statement) || + ts.isFunctionDeclaration(statement) || + ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement)) && + statement.name !== undefined ) { declaredNames.add(statement.name.text); } @@ -133,7 +109,10 @@ function collectExportedDeclarationName(statement: ts.Statement, exportedNames: return; } const modifiers = ts.getModifiers(statement); - if (modifiers === undefined || !modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) { + if ( + modifiers === undefined || + !modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) + ) { return; } @@ -147,11 +126,11 @@ function collectExportedDeclarationName(statement: ts.Statement, exportedNames: } if ( - (ts.isClassDeclaration(statement) - || ts.isFunctionDeclaration(statement) - || ts.isInterfaceDeclaration(statement) - || ts.isTypeAliasDeclaration(statement)) - && statement.name !== undefined + (ts.isClassDeclaration(statement) || + ts.isFunctionDeclaration(statement) || + ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement)) && + statement.name !== undefined ) { exportedNames.add(statement.name.text); } @@ -175,14 +154,6 @@ describe('v19 Warp facade', () => { expect(timeline.writer).toBe('agent-1'); }); - it('keeps the v19 facade off the browser root', () => { - const browserExports = exportedNamesFor('browser.ts'); - - for (const name of FORBIDDEN_BROWSER_V19_EXPORTS) { - expect(browserExports.has(name)).toBe(false); - } - }); - it('keeps substrate graph, worldline, patch, optic, and hologram names off the root', () => { const rootExports = exportedNamesFor('index.ts'); @@ -212,16 +183,20 @@ describe('v19 Warp facade', () => { }); it('rejects missing storage and blank identities', async () => { - await expect(openWarp({ - // @ts-expect-error runtime validation accepts JavaScript callers. - storage: null, - writer: 'agent-1', - })).rejects.toThrow('openWarp requires storage'); - - await expect(openWarp({ - storage: new MemoryStorageAdapter(), - writer: ' ', - })).rejects.toThrow('openWarp requires non-empty identity fields'); + await expect( + openWarp({ + // @ts-expect-error runtime validation accepts JavaScript callers. + storage: null, + writer: 'agent-1', + }) + ).rejects.toThrow('openWarp requires storage'); + + await expect( + openWarp({ + storage: new MemoryStorageAdapter(), + writer: ' ', + }) + ).rejects.toThrow('openWarp requires non-empty identity fields'); const warp = await openWarp({ storage: new MemoryStorageAdapter(), @@ -232,10 +207,12 @@ describe('v19 Warp facade', () => { }); it('rejects invalid non-empty facade identities before opening timelines', async () => { - await expect(openWarp({ - storage: new MemoryStorageAdapter(), - writer: 'agent 1', - })).rejects.toMatchObject({ code: 'E_INVALID_WRITER_ID' }); + await expect( + openWarp({ + storage: new MemoryStorageAdapter(), + writer: 'agent 1', + }) + ).rejects.toMatchObject({ code: 'E_INVALID_WRITER_ID' }); const openedNames: string[] = []; const openTimeline = async (name: string): Promise => { @@ -243,10 +220,13 @@ describe('v19 Warp facade', () => { return new Timeline({ name, writer: 'agent-1' }); }; - expect(() => new Warp({ - writer: 'agent/1', - openTimeline, - })).toThrow('Invalid writer ID: contains forward slash'); + expect( + () => + new Warp({ + writer: 'agent/1', + openTimeline, + }) + ).toThrow('Invalid writer ID: contains forward slash'); const warp = new Warp({ writer: 'agent-1', @@ -258,15 +238,21 @@ describe('v19 Warp facade', () => { }); expect(openedNames).toEqual([]); - expect(() => new Timeline({ - name: 'bad name', - writer: 'agent-1', - })).toThrow('Invalid graph name: contains space'); - - expect(() => new Timeline({ - name: 'events', - writer: 'x'.repeat(MAX_WRITER_ID_LENGTH + 1), - })).toThrow('Invalid writer ID: exceeds maximum length'); + expect( + () => + new Timeline({ + name: 'bad name', + writer: 'agent-1', + }) + ).toThrow('Invalid graph name: contains space'); + + expect( + () => + new Timeline({ + name: 'events', + writer: 'x'.repeat(MAX_WRITER_ID_LENGTH + 1), + }) + ).toThrow('Invalid writer ID: exceeds maximum length'); }); it('writes public intents and returns accepted write receipts', async () => { @@ -277,11 +263,13 @@ describe('v19 Warp facade', () => { const timeline = await warp.timeline('events'); const nodeReceipt = await timeline.write(intent.node.add({ subject: 'user:alice' })); - const propertyReceipt = await timeline.write(intent.property.set({ - subject: 'user:alice', - key: 'role', - value: 'admin', - })); + const propertyReceipt = await timeline.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) + ); expect(nodeReceipt.outcome).toBe('accepted'); expect(nodeReceipt.intent.kind).toBe('node.add'); @@ -302,9 +290,7 @@ describe('v19 Warp facade', () => { if (!('nodes' in result)) { throw new Error('query result must include nodes'); } - expect(result.nodes).toEqual([ - { id: 'user:alice', props: { role: 'admin' } }, - ]); + expect(result.nodes).toEqual([{ id: 'user:alice', props: { role: 'admin' } }]); }); it('reads public readings and returns resolved read receipts', async () => { @@ -315,19 +301,25 @@ describe('v19 Warp facade', () => { const timeline = await warp.timeline('events'); await timeline.write(intent.node.add({ subject: 'user:alice' })); - await timeline.write(intent.property.set({ - subject: 'user:alice', - key: 'role', - value: 'admin', - })); - - const propertyResult = await timeline.read(reading.property({ - subject: 'user:alice', - key: 'role', - })); - const existsResult = await timeline.read(reading.node.exists({ - subject: 'user:alice', - })); + await timeline.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) + ); + + const propertyResult = await timeline.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ); + const existsResult = await timeline.read( + reading.node.exists({ + subject: 'user:alice', + }) + ); expect(propertyResult).toBeInstanceOf(ReadingResult); expect(propertyResult.receipt).toBeInstanceOf(ReadReceipt); @@ -353,27 +345,35 @@ describe('v19 Warp facade', () => { await timeline.write(intent.node.add({ subject: 'user:alice' })); const draft = await timeline.draft('try-admin-role'); - const draftWrite = await draft.write(intent.property.set({ - subject: 'user:alice', - key: 'role', - value: 'admin', - })); - const beforeJoin = await timeline.read(reading.property({ - subject: 'user:alice', - key: 'role', - })); + const draftWrite = await draft.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'admin', + }) + ); + const beforeJoin = await timeline.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ); const preview = await timeline.previewJoin(draft, { policy: 'deterministic', }); - const afterPreview = await timeline.read(reading.property({ - subject: 'user:alice', - key: 'role', - })); + const afterPreview = await timeline.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ); const joined = await timeline.join(draft); - const afterJoin = await timeline.read(reading.property({ - subject: 'user:alice', - key: 'role', - })); + const afterJoin = await timeline.read( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ); expect(draft).toBeInstanceOf(DraftTimeline); expect(draft.name).toBe('try-admin-role'); diff --git a/test/unit/domain/WarpGraph.public-sync.test.ts b/test/unit/domain/WarpGraph.public-sync.test.ts index 20c941d98..6e7e19366 100644 --- a/test/unit/domain/WarpGraph.public-sync.test.ts +++ b/test/unit/domain/WarpGraph.public-sync.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { openWarpGraph } from '../../../legacy.ts'; +import { openWarpGraph } from '../../../src/domain/WarpGraph.ts'; import { createGitRepo } from '../../helpers/warpGraphTestUtils.ts'; describe('WarpGraph public sync seam', { timeout: 20000 }, () => { diff --git a/test/unit/domain/WarpGraph.strands.compare.test.ts b/test/unit/domain/WarpGraph.strands.compare.test.ts index 40b768f56..dab16a31b 100644 --- a/test/unit/domain/WarpGraph.strands.compare.test.ts +++ b/test/unit/domain/WarpGraph.strands.compare.test.ts @@ -73,12 +73,12 @@ function createMockPersistence() { return commit || { message: '', parents: [] }; }), writeTree: vi.fn(async (entries) => { - const oid = hexSha(2000000 + (++treeCounter)); + const oid = hexSha(2000000 + ++treeCounter); trees.set(oid, entries); return oid; }), commitNodeWithTree: vi.fn(async ({ treeOid, message, parents }) => { - const sha = hexSha(3000000 + (++commitCounter)); + const sha = hexSha(3000000 + ++commitCounter); commits.set(sha, { treeOid, message, parents: parents || [] }); return sha; }), @@ -89,7 +89,7 @@ function createMockPersistence() { return oid; }), commitNode: vi.fn(async ({ message, parents }) => { - const sha = hexSha(1000000 + (++commitCounter)); + const sha = hexSha(1000000 + ++commitCounter); commits.set(sha, { message, parents: parents || [] }); return sha; }), @@ -111,17 +111,13 @@ function createMockPersistence() { * }} options * @returns {Promise} */ -async function simulatePatchCommit(persistence, { - graphName, - writerId, - lamport, - ops, - reads, - writes, - context, -}) { +async function simulatePatchCommit( + persistence, + { graphName, writerId, lamport, ops, reads, writes, context } +) { const { encode } = await import('../../../src/infrastructure/codecs/CborCodec.ts'); - const { encodePatchMessage } = await import('../../../src/infrastructure/adapters/TrailerCommitMessageCodecAdapter.ts'); + const { encodePatchMessage } = + await import('../../../src/infrastructure/adapters/TrailerCommitMessageCodecAdapter.ts'); const { buildWriterRef } = await import('../../../src/domain/utils/RefLayout.ts'); const patch = { @@ -147,18 +143,18 @@ async function simulatePatchCommit(persistence, { } describe('WarpCore strand foundation', () => { - let persistence; - let graph; + let persistence; + let graph; const graphName = 'strands-demo'; beforeEach(async () => { persistence = createMockPersistence(); - graph = ((await WarpCore.open({ + graph = (await WarpCore.open({ persistence, graphName, writerId: 'tester', autoMaterialize: false, - })) as WarpCoreRuntime); + })) as WarpCoreRuntime; }); it('compareStrand reports strand-vs-base divergence as substrate facts', async () => { @@ -255,9 +251,7 @@ describe('WarpCore strand foundation', () => { writerId: 'alice', lamport: 2, context: new Map([['alice', 1]]), - ops: [ - { type: 'PropSet', node: 'n1', key: 'status', value: 'live' }, - ], + ops: [{ type: 'PropSet', node: 'n1', key: 'status', value: 'live' }], reads: ['n1'], writes: ['n1'], }); @@ -340,9 +334,7 @@ describe('WarpCore strand foundation', () => { writerId: 'alice', lamport: 2, context: new Map([['alice', 1]]), - ops: [ - { type: 'PropSet', node: 'n1', key: 'color', value: 'blue' }, - ], + ops: [{ type: 'PropSet', node: 'n1', key: 'color', value: 'blue' }], reads: ['n1'], writes: ['n1'], }); @@ -395,7 +387,9 @@ describe('WarpCore strand foundation', () => { }, }); expect(factExport.canonicalFactJson).toBe(canonicalStringify(factExport.fact)); - await expect(graph._crypto.hash('sha256', factExport.canonicalFactJson)).resolves.toBe(factExport.factDigest); + await expect(graph._crypto.hash('sha256', factExport.canonicalFactJson)).resolves.toBe( + factExport.factDigest + ); }); it('scopes coordinate comparison and transfer planning by node-id prefix without mutating the raw substrate truth', async () => { @@ -421,8 +415,19 @@ describe('WarpCore strand foundation', () => { context: new Map([['alice', 1]]), ops: [ { type: 'NodeAdd', node: 'comparison-artifact:cmp-1', dot: Dot.create('alice', 2) }, - { type: 'PropSet', node: 'comparison-artifact:cmp-1', key: 'kind', value: 'comparison-artifact' }, - { type: 'EdgeAdd', from: 'task:1', to: 'comparison-artifact:cmp-1', label: 'governs', dot: Dot.create('alice', 3) }, + { + type: 'PropSet', + node: 'comparison-artifact:cmp-1', + key: 'kind', + value: 'comparison-artifact', + }, + { + type: 'EdgeAdd', + from: 'task:1', + to: 'comparison-artifact:cmp-1', + label: 'governs', + dot: Dot.create('alice', 3), + }, ], reads: ['task:1', 'comparison-artifact:cmp-1'], writes: ['comparison-artifact:cmp-1', 'task:1\0comparison-artifact:cmp-1\0governs'], @@ -452,12 +457,16 @@ describe('WarpCore strand foundation', () => { expect(scopedComparison.visibleState.changed).toBe(false); expect(scopedComparison.visiblePatchDivergence.leftOnlyCount).toBe(0); expect(scopedComparison.visiblePatchDivergence.rightOnlyCount).toBe(0); - expect(scopedComparison.left.resolved.patchUniverseDigest).toBe(scopedComparison.right.resolved.patchUniverseDigest); + expect(scopedComparison.left.resolved.patchUniverseDigest).toBe( + scopedComparison.right.resolved.patchUniverseDigest + ); const scopedFactExport = exportCoordinateComparisonFact(scopedComparison); expect(scopedFactExport.fact.scope).toEqual(scope); expect(scopedFactExport.canonicalFactJson).toBe(canonicalStringify(scopedFactExport.fact)); - await expect(graph._crypto.hash('sha256', scopedFactExport.canonicalFactJson)).resolves.toBe(scopedFactExport.factDigest); + await expect(graph._crypto.hash('sha256', scopedFactExport.canonicalFactJson)).resolves.toBe( + scopedFactExport.factDigest + ); const scopedTransfer = await graph.planCoordinateTransfer({ source: { kind: 'coordinate', frontier: operationalFrontier }, @@ -514,8 +523,8 @@ describe('WarpCore strand foundation', () => { size: 14, }); persistence._blobs.set( - /** @type {{ oid: string }} */ (strandContentMeta).oid, - Buffer.from('worldline-body'), + /** @type {{ oid: string }} */ strandContentMeta.oid, + Buffer.from('worldline-body') ); const transferPlan = await graph.planStrandTransfer('ws_transfer_live'); @@ -549,9 +558,10 @@ describe('WarpCore strand foundation', () => { key: 'obsolete', value: null, }); - const attachOp = /** @type {import('../../../legacy.ts').VisibleStateTransferOperation & { op: 'attach_node_content', nodeId: string, content: Uint8Array, contentOid: string, mime?: string|null, size?: number|null }} */ ( - transferPlan.ops.find((op) => op.op === 'attach_node_content' && op.nodeId === 'doc:1') - ); + const attachOp = + /** @type {import('../../../src/domain/types/CoordinateComparison.ts').VisibleStateTransferOperation & { op: 'attach_node_content', nodeId: string, content: Uint8Array, contentOid: string, mime?: string|null, size?: number|null }} */ transferPlan.ops.find( + (op) => op.op === 'attach_node_content' && op.nodeId === 'doc:1' + ); expect(attachOp).toMatchObject({ op: 'attach_node_content', nodeId: 'doc:1', @@ -598,7 +608,8 @@ describe('WarpCore strand foundation', () => { }), }); expect(factExport.canonicalFactJson).toBe(canonicalStringify(factExport.fact)); - await expect(graph._crypto.hash('sha256', factExport.canonicalFactJson)).resolves.toBe(factExport.factDigest); + await expect(graph._crypto.hash('sha256', factExport.canonicalFactJson)).resolves.toBe( + factExport.factDigest + ); }); - }); diff --git a/test/unit/domain/WarpWorldline.test.ts b/test/unit/domain/WarpWorldline.test.ts index e65404eb3..0f9907a14 100644 --- a/test/unit/domain/WarpWorldline.test.ts +++ b/test/unit/domain/WarpWorldline.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { openWarpWorldline } from '../../../legacy.ts'; +import { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import WarpWorldline, { type WarpWorldlinePatchBuild } from '../../../src/domain/WarpWorldline.ts'; import ProjectionHandle from '../../../src/domain/services/ProjectionHandle.ts'; @@ -16,7 +16,7 @@ type ObserverCall = { }; function isAperture( - value: Aperture | { readonly source: WorldlineSource } | undefined, + value: Aperture | { readonly source: WorldlineSource } | undefined ): value is Aperture { return value !== undefined && 'match' in value; } @@ -27,11 +27,7 @@ function createObserverBacking(calls: ObserverCall[]): ObserverBacking { getNodes: async () => [], getNodeProps: async () => null, getEdges: async () => [], - observer: async ( - name: string, - config: Aperture, - options: { source: WorldlineSource }, - ) => { + observer: async (name: string, config: Aperture, options: { source: WorldlineSource }) => { calls.push({ name, config, source: options.source }); return createObserver(name, config, calls, options.source); }, @@ -42,7 +38,7 @@ function createObserver( name: string, config: Aperture, calls: ObserverCall[], - source: WorldlineSource, + source: WorldlineSource ): Observer { return new Observer({ name, @@ -58,15 +54,15 @@ function createWorldline(calls: ObserverCall[]): ProjectionHandle { observer: async ( nameOrConfig: string | Aperture, configOrOptions?: Aperture | { readonly source: WorldlineSource }, - maybeOptions?: { readonly source: WorldlineSource }, + maybeOptions?: { readonly source: WorldlineSource } ) => { const name = typeof nameOrConfig === 'string' ? nameOrConfig : 'observer'; - const config = typeof nameOrConfig === 'string' - ? requireAperture(configOrOptions) - : nameOrConfig; - const options = typeof nameOrConfig === 'string' - ? requireObserverSource(maybeOptions) - : requireObserverSource(configOrOptions); + const config = + typeof nameOrConfig === 'string' ? requireAperture(configOrOptions) : nameOrConfig; + const options = + typeof nameOrConfig === 'string' + ? requireObserverSource(maybeOptions) + : requireObserverSource(configOrOptions); calls.push({ name, config, source: options.source }); return createObserver(name, config, calls, options.source); }, @@ -75,7 +71,7 @@ function createWorldline(calls: ObserverCall[]): ProjectionHandle { } function requireAperture( - value: Aperture | { readonly source: WorldlineSource } | undefined, + value: Aperture | { readonly source: WorldlineSource } | undefined ): Aperture { if (isAperture(value)) { return value; @@ -84,7 +80,7 @@ function requireAperture( } function requireObserverSource( - value: Aperture | { readonly source: WorldlineSource } | undefined, + value: Aperture | { readonly source: WorldlineSource } | undefined ): { readonly source: WorldlineSource } { if (value !== undefined && 'source' in value) { return value; @@ -94,14 +90,18 @@ function requireObserverSource( function createHandle( calls: ObserverCall[] = [], - commitPatch: (build: WarpWorldlinePatchBuild) => Promise = async () => 'patch-sha', + commitPatch: (build: WarpWorldlinePatchBuild) => Promise = async () => 'patch-sha' ): WarpWorldline { return new WarpWorldline({ worldlineName: 'events', writerId: 'agent-1', commitPatch, createWorldline: () => createWorldline(calls), - admitIntent: async (descriptor) => ({ admitted: true, sha: 'blob:intent:123', intentId: descriptor.intentId }), + admitIntent: async (descriptor) => ({ + admitted: true, + sha: 'blob:intent:123', + intentId: descriptor.intentId, + }), }); } @@ -143,10 +143,12 @@ describe('WarpWorldline', () => { writerId: 'agent-1', }); - await expect(handle.commit((patch) => { - patch.addNode('user:bob'); - throw new Error('abort worldline commit'); - })).rejects.toThrow('abort worldline commit'); + await expect( + handle.commit((patch) => { + patch.addNode('user:bob'); + throw new Error('abort worldline commit'); + }) + ).rejects.toThrow('abort worldline commit'); await expect(handle.live().hasNode('user:bob')).resolves.toBe(false); }); @@ -209,45 +211,53 @@ describe('WarpWorldline', () => { }); it('rejects empty open identities before returning a handle', async () => { - await expect(openWarpWorldline({ - persistence: new InMemoryGraphAdapter(), - worldlineName: '', - writerId: 'agent-1', - })).rejects.toMatchObject({ + await expect( + openWarpWorldline({ + persistence: new InMemoryGraphAdapter(), + worldlineName: '', + writerId: 'agent-1', + }) + ).rejects.toMatchObject({ code: 'E_WARP_WORLDLINE_IDENTITY', context: { field: 'worldlineName' }, }); }); it('rejects empty writer identity before returning a handle', async () => { - await expect(openWarpWorldline({ - persistence: new InMemoryGraphAdapter(), - worldlineName: 'events', - writerId: '', - })).rejects.toMatchObject({ + await expect( + openWarpWorldline({ + persistence: new InMemoryGraphAdapter(), + worldlineName: 'events', + writerId: '', + }) + ).rejects.toMatchObject({ code: 'E_WARP_WORLDLINE_IDENTITY', context: { field: 'writerId' }, }); }); it('rejects blank writer identity before returning a handle', async () => { - await expect(openWarpWorldline({ - persistence: new InMemoryGraphAdapter(), - worldlineName: 'events', - writerId: ' ', - })).rejects.toMatchObject({ + await expect( + openWarpWorldline({ + persistence: new InMemoryGraphAdapter(), + worldlineName: 'events', + writerId: ' ', + }) + ).rejects.toMatchObject({ code: 'E_WARP_WORLDLINE_IDENTITY', context: { field: 'writerId' }, }); }); it('reports malformed JavaScript writer identity as WarpError', async () => { - await expect(openWarpWorldline({ - persistence: new InMemoryGraphAdapter(), - worldlineName: 'events', - // @ts-expect-error exercising runtime validation for JavaScript callers - writerId: 42, - })).rejects.toMatchObject({ + await expect( + openWarpWorldline({ + persistence: new InMemoryGraphAdapter(), + worldlineName: 'events', + // @ts-expect-error exercising runtime validation for JavaScript callers + writerId: 42, + }) + ).rejects.toMatchObject({ code: 'E_WARP_WORLDLINE_IDENTITY', context: { field: 'writerId' }, }); @@ -322,6 +332,8 @@ describe('WarpWorldline', () => { it('preserves bounded optic failure semantics from the live worldline', () => { const handle = createHandle(); - expect(() => handle.optic()).toThrow('worldline optic requires a checkpoint-tail bounded basis source'); + expect(() => handle.optic()).toThrow( + 'worldline optic requires a checkpoint-tail bounded basis source' + ); }); }); diff --git a/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts b/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts index 84c39226b..f860d055d 100644 --- a/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts +++ b/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { GitWarpReceiptEnvelopeBoundary } from '../../../../legacy.ts'; +import { GitWarpReceiptEnvelopeBoundary } from '../../../../advanced.ts'; import WarpError from '../../../../src/domain/errors/WarpError.ts'; import { TickReceipt } from '../../../../src/domain/types/TickReceipt.ts'; @@ -9,20 +9,24 @@ function makeReceipt(): TickReceipt { patchSha: 'd'.repeat(40), writer: 'writer-a', lamport: 11, - ops: [{ - op: 'NodeAdd', - target: 'node:a', - result: 'applied', - }, { - op: 'PropSet', - target: 'node:a\u0000name', - result: 'superseded', - reason: 'LWW: writer writer-b at lamport 12 wins', - }, { - op: 'EdgeAdd', - target: 'node:a\u0000node:b\u0000knows', - result: 'redundant', - }], + ops: [ + { + op: 'NodeAdd', + target: 'node:a', + result: 'applied', + }, + { + op: 'PropSet', + target: 'node:a\u0000name', + result: 'superseded', + reason: 'LWW: writer writer-b at lamport 12 wins', + }, + { + op: 'EdgeAdd', + target: 'node:a\u0000node:b\u0000knows', + result: 'redundant', + }, + ], }); } @@ -58,14 +62,20 @@ describe('GitWarpReceiptEnvelopeBoundary', () => { }); it('rejects missing or non-receipt carriers at runtime', () => { - expect(() => new GitWarpReceiptEnvelopeBoundary( - // @ts-expect-error runtime guard for JavaScript callers - undefined, - )).toThrow(WarpError); + expect( + () => + new GitWarpReceiptEnvelopeBoundary( + // @ts-expect-error runtime guard for JavaScript callers + undefined + ) + ).toThrow(WarpError); - expect(() => new GitWarpReceiptEnvelopeBoundary({ - // @ts-expect-error runtime guard for JavaScript callers - receipt: undefined, - })).toThrow(WarpError); + expect( + () => + new GitWarpReceiptEnvelopeBoundary({ + // @ts-expect-error runtime guard for JavaScript callers + receipt: undefined, + }) + ).toThrow(WarpError); }); }); diff --git a/test/unit/domain/legacy.exports.test.ts b/test/unit/domain/legacy.exports.test.ts deleted file mode 100644 index 984d3ff7a..000000000 --- a/test/unit/domain/legacy.exports.test.ts +++ /dev/null @@ -1,648 +0,0 @@ -/** - * Tests for the legacy compatibility exports. - * - * Verifies that graph-first migration symbols remain available from - * `legacy.ts`. - */ - -import { describe, it, expect } from 'vitest'; -import * as publicApi from '../../../legacy.ts'; - -// Import everything from the legacy compatibility entry point. -import WarpAppDefault, { - WarpApp, - WarpCore, - // Core classes - GitGraphAdapter, - AttachmentKey, - AttachmentRecord, - AttachmentSchemaVersion, - ContentAttachmentMime, - ContentAttachmentOid, - ContentAttachmentPayload, - ContentAttachmentProjection, - ContentAttachmentRecord, - ContentAttachmentSize, - ContentAttachmentWriteIntent, - EdgeId, - EdgeRecord, - EdgeTypeId, - GraphAttachmentSetOp, - GraphEdgeRecordSetOp, - GraphNodeRecordSetOp, - GraphOpAlgebra, - GraphOpAlgebraProjection, - GraphNode, - NodeId, - NodeRecord, - NodeTypeId, - BitmapIndexBuilder, - BitmapIndexReader, - IndexRebuildService, - HealthCheckService, - HealthStatus, - CommitDagTraversalService, - GraphPersistencePort, - IndexStoragePort, - - // Logging infrastructure - LoggerPort, - NoOpLogger, - ConsoleLogger, - LogLevel, - - // Error types - ForkError, - WormholeError, - IndexError, - ShardLoadError, - ShardCorruptionError, - ShardValidationError, - StorageError, - TraversalError, - OperationAbortedError, - MemoryBudgetError, - Observer, - ObserverPlan, - ObserverReadingEnvelope, - ProjectionHandle, - Optic, - OpticAperturePosture, - OpticBasisPosture, - OpticCoordinatePosture, - OpticSupportRule, - ContinuumArtifactAuthorityError, - - // Cancellation utilities - checkAborted, - createTimeoutSignal, - - // WARP type creators - createNodeAdd, - createNodeTombstone, - createEdgeAdd, - createEdgeTombstone, - createPropSet, - createInlineValue, - createBlobValue, - createStateReader, - compareVisibleState, - GraphDiff, - SupportFragmentPlan, - normalizeVisibleStateScope, - scopeMaterializedState, - ContinuumArtifactAuthority, - ContinuumArtifactDescriptor, - ContinuumArtifactIngestionPolicy, - ContinuumEvidenceAccess, - ContinuumEvidenceClaim, - ContinuumEvidenceCompleteness, - ContinuumEvidenceOrigin, - ContinuumEvidencePosture, - ContinuumEvidenceProofStrength, - ContinuumFamilyId, - ContinuumGeneratedFamilyInventory, - ContinuumGeneratedFamilyInventoryEntry, - ContinuumGeneratedFamilyStatus, - ContinuumReceiptFamilyProjection, - GitWarpReadingEnvelopePayloadFact, - GitWarpReadingEnvelopeSourceFacts, - GitWarpBraidHologram, - GitWarpBraidHologramMember, - GitWarpSuffixTransformHologram, - GitWarpWitnessedSuffixAdmissionOutcome, - GitWarpWitnessedSuffixAdmissionShell, - GitWarpTickHologram, - GitWarpTickPatchReplayCore, - GitWarpTickReceiptShell, - GitWarpTickReceiptWitnessCore, - GitWarpTickWitnessLadder, - GitWarpWitnessedSuffixPatchFact, - GitWarpWitnessedSuffixSourceFacts, - GitWarpReceiptSourceFacts, - createCurrentContinuumGeneratedFamilyInventory, - ContinuumArtifactJsonFileAdapter, - MemoryBudget, - MemoryBudgetLease, - MemoryCapability, - MemoryCapabilityPosture, - MemoryCapabilityReport, - WarpMemoryPool, - WarpMemoryPoolSnapshot, - createV18BoundedMemoryCapabilityReport, - createBoundedMemoryCapabilityReport, - ApertureOpeningProof, - RejectedApertureOpening, - RejectedZKWormhole, - VerifiedApertureOpening, - VerifiedZKWormhole, - ZKWormholeEdge, - openAperture, - verifyZKWormhole, - ZKWormholeProofVerifierPort, -} from '../../../legacy.ts'; - -describe('legacy.ts exports', () => { - describe('default export', () => { - it('exports WarpApp as default', () => { - expect(WarpAppDefault).toBeDefined(); - expect(typeof WarpAppDefault).toBe('function'); - expect(WarpAppDefault).toBe(WarpApp); - expect(WarpAppDefault.name).toBe('WarpApp'); - }); - }); - - describe('runtime exports', () => { - it('exports WarpApp as a named export', () => { - expect(WarpApp).toBeDefined(); - expect(typeof WarpApp).toBe('function'); - expect(WarpApp).toBe(WarpAppDefault); - }); - - it('exports WarpCore as the full plumbing-facing surface', () => { - expect(WarpCore).toBeDefined(); - expect(typeof WarpCore).toBe('function'); - expect(WarpCore.name).toBe('WarpCore'); - }); - - it('does not export WarpRuntime from the public entry point', () => { - expect('WarpRuntime' in publicApi).toBe(false); - }); - - it('does not export WarpGraph as a public compatibility alias', () => { - expect('WarpGraph' in publicApi).toBe(false); - }); - }); - - describe('visible-state helpers', () => { - it('exports normalizeVisibleStateScope', () => { - expect(normalizeVisibleStateScope).toBeDefined(); - expect(typeof normalizeVisibleStateScope).toBe('function'); - }); - - it('exports scopeMaterializedState', () => { - expect(scopeMaterializedState).toBeDefined(); - expect(typeof scopeMaterializedState).toBe('function'); - }); - }); - - describe('core classes', () => { - it('exports ProjectionHandle and not the retired Worldline class alias', () => { - expect(ProjectionHandle).toBeDefined(); - expect(typeof ProjectionHandle).toBe('function'); - expect('Worldline' in publicApi).toBe(false); - }); - - it('exports reified Optic runtime nouns', () => { - expect(Optic).toBeDefined(); - expect(typeof Optic).toBe('function'); - expect(OpticAperturePosture).toBeDefined(); - expect(typeof OpticAperturePosture).toBe('function'); - expect(OpticBasisPosture).toBeDefined(); - expect(typeof OpticBasisPosture).toBe('function'); - expect(OpticCoordinatePosture).toBeDefined(); - expect(typeof OpticCoordinatePosture).toBe('function'); - expect(OpticSupportRule).toBeDefined(); - expect(typeof OpticSupportRule).toBe('function'); - }); - - it('exports Observer', () => { - expect(Observer).toBeDefined(); - expect(typeof Observer).toBe('function'); - expect(Observer.name).toBe('Observer'); - expect(ObserverPlan).toBeDefined(); - expect(typeof ObserverPlan).toBe('function'); - expect(ObserverReadingEnvelope).toBeDefined(); - expect(typeof ObserverReadingEnvelope).toBe('function'); - expect('ObserverView' in publicApi).toBe(false); - }); - - it('exports GitGraphAdapter', () => { - expect(GitGraphAdapter).toBeDefined(); - expect(typeof GitGraphAdapter).toBe('function'); - }); - - it('exports GraphNode', () => { - expect(GraphNode).toBeDefined(); - expect(typeof GraphNode).toBe('function'); - }); - - it('exports graph attachment substrate nouns', () => { - expect(AttachmentKey).toBeDefined(); - expect(typeof AttachmentKey).toBe('function'); - expect(AttachmentRecord).toBeDefined(); - expect(typeof AttachmentRecord).toBe('function'); - expect(AttachmentSchemaVersion).toBeDefined(); - expect(typeof AttachmentSchemaVersion).toBe('function'); - }); - - it('exports content attachment payload nouns', () => { - expect(ContentAttachmentMime).toBeDefined(); - expect(typeof ContentAttachmentMime).toBe('function'); - expect(ContentAttachmentOid).toBeDefined(); - expect(typeof ContentAttachmentOid).toBe('function'); - expect(ContentAttachmentPayload).toBeDefined(); - expect(typeof ContentAttachmentPayload).toBe('function'); - expect(ContentAttachmentProjection).toBeDefined(); - expect(typeof ContentAttachmentProjection).toBe('function'); - expect(ContentAttachmentRecord).toBeDefined(); - expect(typeof ContentAttachmentRecord).toBe('function'); - expect(ContentAttachmentSize).toBeDefined(); - expect(typeof ContentAttachmentSize).toBe('function'); - expect(ContentAttachmentWriteIntent).toBeDefined(); - expect(typeof ContentAttachmentWriteIntent).toBe('function'); - }); - - it('exports graph-op algebra substrate nouns', () => { - expect(GraphAttachmentSetOp).toBeDefined(); - expect(typeof GraphAttachmentSetOp).toBe('function'); - expect(GraphEdgeRecordSetOp).toBeDefined(); - expect(typeof GraphEdgeRecordSetOp).toBe('function'); - expect(GraphNodeRecordSetOp).toBeDefined(); - expect(typeof GraphNodeRecordSetOp).toBe('function'); - expect(GraphOpAlgebra).toBeDefined(); - expect(typeof GraphOpAlgebra).toBe('function'); - expect(GraphOpAlgebraProjection).toBeDefined(); - expect(typeof GraphOpAlgebraProjection).toBe('function'); - }); - - it('exports graph edge substrate nouns', () => { - expect(EdgeId).toBeDefined(); - expect(typeof EdgeId).toBe('function'); - expect(EdgeRecord).toBeDefined(); - expect(typeof EdgeRecord).toBe('function'); - expect(EdgeTypeId).toBeDefined(); - expect(typeof EdgeTypeId).toBe('function'); - }); - - it('exports graph node substrate nouns', () => { - expect(NodeId).toBeDefined(); - expect(typeof NodeId).toBe('function'); - expect(NodeRecord).toBeDefined(); - expect(typeof NodeRecord).toBe('function'); - expect(NodeTypeId).toBeDefined(); - expect(typeof NodeTypeId).toBe('function'); - }); - - it('exports BitmapIndexBuilder', () => { - expect(BitmapIndexBuilder).toBeDefined(); - expect(typeof BitmapIndexBuilder).toBe('function'); - }); - - it('exports BitmapIndexReader', () => { - expect(BitmapIndexReader).toBeDefined(); - expect(typeof BitmapIndexReader).toBe('function'); - }); - - it('exports IndexRebuildService', () => { - expect(IndexRebuildService).toBeDefined(); - expect(typeof IndexRebuildService).toBe('function'); - }); - - it('exports HealthCheckService', () => { - expect(HealthCheckService).toBeDefined(); - expect(typeof HealthCheckService).toBe('function'); - }); - - it('exports HealthStatus enum', () => { - expect(HealthStatus).toBeDefined(); - expect(HealthStatus.HEALTHY).toBe('healthy'); - expect(HealthStatus.DEGRADED).toBe('degraded'); - expect(HealthStatus.UNHEALTHY).toBe('unhealthy'); - }); - - it('exports CommitDagTraversalService', () => { - expect(CommitDagTraversalService).toBeDefined(); - expect(typeof CommitDagTraversalService).toBe('function'); - }); - - }); - - describe('port interfaces', () => { - it('exports GraphPersistencePort', () => { - expect(GraphPersistencePort).toBeDefined(); - expect(typeof GraphPersistencePort).toBe('function'); - }); - - it('exports IndexStoragePort', () => { - expect(IndexStoragePort).toBeDefined(); - expect(typeof IndexStoragePort).toBe('function'); - }); - }); - - describe('logging infrastructure', () => { - it('exports LoggerPort', () => { - expect(LoggerPort).toBeDefined(); - expect(typeof LoggerPort).toBe('function'); - }); - - it('exports NoOpLogger', () => { - expect(NoOpLogger).toBeDefined(); - expect(typeof NoOpLogger).toBe('function'); - }); - - it('exports ConsoleLogger', () => { - expect(ConsoleLogger).toBeDefined(); - expect(typeof ConsoleLogger).toBe('function'); - }); - - it('exports LogLevel enum', () => { - expect(LogLevel).toBeDefined(); - expect(LogLevel.DEBUG).toBeDefined(); - expect(LogLevel.INFO).toBeDefined(); - expect(LogLevel.WARN).toBeDefined(); - expect(LogLevel.ERROR).toBeDefined(); - }); - }); - - describe('error types', () => { - it('exports ForkError', () => { - expect(ForkError).toBeDefined(); - expect(typeof ForkError).toBe('function'); - }); - - it('exports WormholeError', () => { - expect(WormholeError).toBeDefined(); - expect(typeof WormholeError).toBe('function'); - }); - - it('exports IndexError', () => { - expect(IndexError).toBeDefined(); - expect(typeof IndexError).toBe('function'); - }); - - it('exports ShardLoadError', () => { - expect(ShardLoadError).toBeDefined(); - expect(typeof ShardLoadError).toBe('function'); - }); - - it('exports ShardCorruptionError', () => { - expect(ShardCorruptionError).toBeDefined(); - expect(typeof ShardCorruptionError).toBe('function'); - }); - - it('exports ShardValidationError', () => { - expect(ShardValidationError).toBeDefined(); - expect(typeof ShardValidationError).toBe('function'); - }); - - it('exports StorageError', () => { - expect(StorageError).toBeDefined(); - expect(typeof StorageError).toBe('function'); - }); - - it('exports TraversalError', () => { - expect(TraversalError).toBeDefined(); - expect(typeof TraversalError).toBe('function'); - }); - - it('exports OperationAbortedError', () => { - expect(OperationAbortedError).toBeDefined(); - expect(typeof OperationAbortedError).toBe('function'); - }); - - it('exports MemoryBudgetError', () => { - expect(MemoryBudgetError).toBeDefined(); - expect(typeof MemoryBudgetError).toBe('function'); - }); - - it('exports ContinuumArtifactAuthorityError', () => { - expect(ContinuumArtifactAuthorityError).toBeDefined(); - expect(typeof ContinuumArtifactAuthorityError).toBe('function'); - }); - }); - - describe('bounded-memory contract exports', () => { - it('exports the memory budget runtime nouns', () => { - expect(MemoryBudget).toBeDefined(); - expect(typeof MemoryBudget).toBe('function'); - expect(MemoryBudgetLease).toBeDefined(); - expect(typeof MemoryBudgetLease).toBe('function'); - expect(WarpMemoryPool).toBeDefined(); - expect(typeof WarpMemoryPool).toBe('function'); - expect(WarpMemoryPoolSnapshot).toBeDefined(); - expect(typeof WarpMemoryPoolSnapshot).toBe('function'); - }); - - it('exports the memory capability report runtime nouns', () => { - expect(MemoryCapability).toBeDefined(); - expect(typeof MemoryCapability).toBe('function'); - expect(MemoryCapabilityPosture).toBeDefined(); - expect(typeof MemoryCapabilityPosture).toBe('function'); - expect(MemoryCapabilityReport).toBeDefined(); - expect(typeof MemoryCapabilityReport).toBe('function'); - expect(createV18BoundedMemoryCapabilityReport).toBeDefined(); - expect(typeof createV18BoundedMemoryCapabilityReport).toBe('function'); - expect(createV18BoundedMemoryCapabilityReport).toBe(createBoundedMemoryCapabilityReport); - expect(createBoundedMemoryCapabilityReport).toBeDefined(); - expect(typeof createBoundedMemoryCapabilityReport).toBe('function'); - }); - }); - - describe('Continuum boundary artifacts', () => { - it('exports the artifact descriptor classes', () => { - expect(ContinuumArtifactAuthority).toBeDefined(); - expect(ContinuumArtifactDescriptor).toBeDefined(); - expect(ContinuumArtifactIngestionPolicy).toBeDefined(); - expect(ContinuumEvidenceAccess).toBeDefined(); - expect(ContinuumEvidenceClaim).toBeDefined(); - expect(ContinuumEvidenceCompleteness).toBeDefined(); - expect(ContinuumEvidenceOrigin).toBeDefined(); - expect(ContinuumEvidencePosture).toBeDefined(); - expect(ContinuumEvidenceProofStrength).toBeDefined(); - expect(ContinuumFamilyId).toBeDefined(); - expect(ContinuumGeneratedFamilyInventory).toBeDefined(); - expect(ContinuumGeneratedFamilyInventoryEntry).toBeDefined(); - expect(ContinuumGeneratedFamilyStatus).toBeDefined(); - expect(ContinuumReceiptFamilyProjection).toBeDefined(); - expect(GitWarpReadingEnvelopePayloadFact).toBeDefined(); - expect(GitWarpReadingEnvelopeSourceFacts).toBeDefined(); - expect(GitWarpBraidHologram).toBeDefined(); - expect(GitWarpBraidHologramMember).toBeDefined(); - expect(GitWarpSuffixTransformHologram).toBeDefined(); - expect(GitWarpWitnessedSuffixAdmissionOutcome).toBeDefined(); - expect(GitWarpWitnessedSuffixAdmissionShell).toBeDefined(); - expect(GitWarpTickHologram).toBeDefined(); - expect(GitWarpTickPatchReplayCore).toBeDefined(); - expect(GitWarpTickReceiptShell).toBeDefined(); - expect(GitWarpTickReceiptWitnessCore).toBeDefined(); - expect(GitWarpTickWitnessLadder).toBeDefined(); - expect(GitWarpWitnessedSuffixPatchFact).toBeDefined(); - expect(GitWarpWitnessedSuffixSourceFacts).toBeDefined(); - expect(GitWarpReceiptSourceFacts).toBeDefined(); - expect(createCurrentContinuumGeneratedFamilyInventory).toBeDefined(); - expect(ContinuumArtifactJsonFileAdapter).toBeDefined(); - }); - - it('constructs a generated receipt-family descriptor from public exports', () => { - const descriptor = new ContinuumArtifactDescriptor({ - familyId: 'receipt-family', - version: '0.1.0', - sourceSchemaPath: '~/git/continuum/schemas/continuum-receipt-family.graphql', - generatedBy: 'wesley witness-continuum --scope receipt-family', - artifactKind: 'continuum.family.fixture', - authority: 'generated-fixture', - targets: ['typescript'], - }); - - expect(descriptor.familyId).toBeInstanceOf(ContinuumFamilyId); - expect(descriptor.hasGeneratedAuthority()).toBe(true); - }); - - it('exports explicit Continuum evidence posture claims', () => { - const descriptor = new ContinuumArtifactDescriptor({ - familyId: 'receipt-family', - version: '0.1.0', - sourceSchemaPath: '~/git/continuum/schemas/continuum-receipt-family.graphql', - generatedBy: 'wesley witness-continuum --scope receipt-family', - artifactKind: 'continuum.family.fixture', - authority: 'generated-fixture', - targets: ['typescript'], - }); - const claim = new ContinuumEvidenceClaim({ - descriptor, - posture: ContinuumEvidencePosture.translatedGitWarpEvidence(), - }); - - expect(claim.posture).toBeInstanceOf(ContinuumEvidencePosture); - expect(claim.isTranslatedGitWarpEvidence()).toBe(true); - expect(claim.isNativeContinuumEvidence()).toBe(false); - }); - - it('exports the current generated-family readiness inventory', () => { - const inventory = createCurrentContinuumGeneratedFamilyInventory(); - - expect(inventory).toBeInstanceOf(ContinuumGeneratedFamilyInventory); - expect(inventory.requireEntry('receipt-family')).toBeInstanceOf(ContinuumGeneratedFamilyInventoryEntry); - expect(inventory.requireEntry('receipt-family').status).toBeInstanceOf(ContinuumGeneratedFamilyStatus); - expect(inventory.requireEntry('receipt-family').status.isProjectionReady()).toBe(true); - expect(inventory.requireEntry('runtime-boundary-family').status.isProjectionReady()).toBe(false); - }); - }); - - describe('ZK wormhole artifacts', () => { - it('exports cold-tier wormhole proof surfaces', () => { - expect(ApertureOpeningProof).toBeDefined(); - expect(RejectedApertureOpening).toBeDefined(); - expect(RejectedZKWormhole).toBeDefined(); - expect(VerifiedApertureOpening).toBeDefined(); - expect(VerifiedZKWormhole).toBeDefined(); - expect(ZKWormholeEdge).toBeDefined(); - expect(openAperture).toBeDefined(); - expect(verifyZKWormhole).toBeDefined(); - expect(ZKWormholeProofVerifierPort).toBeDefined(); - }); - }); - - describe('cancellation utilities', () => { - it('exports checkAborted', () => { - expect(checkAborted).toBeDefined(); - expect(typeof checkAborted).toBe('function'); - }); - - it('exports createTimeoutSignal', () => { - expect(createTimeoutSignal).toBeDefined(); - expect(typeof createTimeoutSignal).toBe('function'); - }); - }); - - describe('multi-writer graph support (WARP)', () => { - it('exports WarpCore as the public plumbing surface from the main entry point', () => { - expect(WarpCore).toBeDefined(); - expect(typeof WarpCore.open).toBe('function'); - }); - }); - - describe('WARP type creators', () => { - it('exports createNodeAdd', () => { - expect(createNodeAdd).toBeDefined(); - expect(typeof createNodeAdd).toBe('function'); - const op = createNodeAdd('user:alice'); - expect(op).toEqual({ type: 'NodeAdd', node: 'user:alice' }); - }); - - it('exports createNodeTombstone', () => { - expect(createNodeTombstone).toBeDefined(); - expect(typeof createNodeTombstone).toBe('function'); - const op = createNodeTombstone('user:alice'); - expect(op).toEqual({ type: 'NodeTombstone', node: 'user:alice' }); - }); - - it('exports createEdgeAdd', () => { - expect(createEdgeAdd).toBeDefined(); - expect(typeof createEdgeAdd).toBe('function'); - const op = createEdgeAdd('user:alice', 'user:bob', 'follows'); - expect(op).toEqual({ type: 'EdgeAdd', from: 'user:alice', to: 'user:bob', label: 'follows' }); - }); - - it('exports createEdgeTombstone', () => { - expect(createEdgeTombstone).toBeDefined(); - expect(typeof createEdgeTombstone).toBe('function'); - const op = createEdgeTombstone('user:alice', 'user:bob', 'follows'); - expect(op).toEqual({ type: 'EdgeTombstone', from: 'user:alice', to: 'user:bob', label: 'follows' }); - }); - - it('exports createPropSet', () => { - expect(createPropSet).toBeDefined(); - expect(typeof createPropSet).toBe('function'); - const value = createInlineValue('Alice'); - const op = createPropSet('user:alice', 'name', value); - expect(op).toEqual({ type: 'PropSet', node: 'user:alice', key: 'name', value: { type: 'inline', value: 'Alice' } }); - }); - - // Note: createPatch (schema:1) has been removed - use Patch constructor directly - - it('exports createInlineValue', () => { - expect(createInlineValue).toBeDefined(); - expect(typeof createInlineValue).toBe('function'); - const ref = createInlineValue('hello'); - expect(ref).toEqual({ type: 'inline', value: 'hello' }); - }); - - it('exports createBlobValue', () => { - expect(createBlobValue).toBeDefined(); - expect(typeof createBlobValue).toBe('function'); - const ref = createBlobValue('abc123def456'); - expect(ref).toEqual({ type: 'blob', oid: 'abc123def456' }); - }); - - it('exports createStateReader', () => { - expect(createStateReader).toBeDefined(); - expect(typeof createStateReader).toBe('function'); - }); - - it('exports compareVisibleState', () => { - expect(compareVisibleState).toBeDefined(); - expect(typeof compareVisibleState).toBe('function'); - }); - - it('exports GraphDiff', () => { - expect(GraphDiff).toBeDefined(); - expect(typeof GraphDiff).toBe('function'); - }); - - it('exports SupportFragmentPlan', () => { - expect(SupportFragmentPlan).toBeDefined(); - expect(typeof SupportFragmentPlan).toBe('function'); - }); - }); - - describe('usage patterns', () => { - it('supports ESM default and named imports for WarpApp/WarpCore', () => { - // This test verifies the import syntax works - // import WarpApp, { WarpCore } from 'warp'; - expect(WarpAppDefault).toBeDefined(); - expect(WarpApp).toBeDefined(); - expect(WarpCore).toBeDefined(); - expect(WarpAppDefault).toBe(WarpApp); - }); - - it('supports importing all WARP utilities together', () => { - // Verify all the pieces needed for WARP usage are available - expect(WarpApp).toBeDefined(); - expect(WarpCore).toBeDefined(); - expect(createNodeAdd).toBeDefined(); - // Note: createPatch (schema:1) removed - use Patch constructor directly - }); - }); -}); diff --git a/test/unit/domain/publicApiExecutableExamples.test.ts b/test/unit/domain/publicApiExecutableExamples.test.ts index 31ecd9fef..8565b06c6 100644 --- a/test/unit/domain/publicApiExecutableExamples.test.ts +++ b/test/unit/domain/publicApiExecutableExamples.test.ts @@ -1,18 +1,12 @@ import { describe, expect, it } from 'vitest'; -import * as packageRoot from '../../../legacy.ts'; -import { - InMemoryGraphAdapter, - Observer, - WarpApp, - WarpWorldline, - openWarpGraph, - openWarpWorldline, -} from '../../../legacy.ts'; +import { Observer, type Aperture } from '../../../advanced.ts'; +import WarpApp from '../../../src/domain/WarpApp.ts'; +import { openWarpGraph } from '../../../src/domain/WarpGraph.ts'; +import WarpWorldline, { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import { createInMemoryRepo } from '../../helpers/warpGraphTestUtils.ts'; -import type { Aperture } from '../../../legacy.ts'; - const PUBLIC_USERS_APERTURE: Aperture = Object.freeze({ match: 'user:*', expose: ['name', 'role', 'level'], @@ -52,21 +46,8 @@ async function seedGuideGraph(events: WarpWorldline): Promise { }); } -function expectRootExportAbsent(name: string): void { - expect(Object.prototype.hasOwnProperty.call(packageRoot, name)).toBe(false); -} - describe('public API executable examples', () => { - it('keeps package-root examples on public nouns without exporting runtime classes', async () => { - expect(packageRoot.openWarpWorldline).toBe(openWarpWorldline); - expect(packageRoot.openWarpGraph).toBe(openWarpGraph); - expect(packageRoot.WarpApp).toBe(WarpApp); - expect(packageRoot.WarpWorldline).toBe(WarpWorldline); - expect(packageRoot.Observer).toBe(Observer); - expectRootExportAbsent('WarpRuntime'); - expectRootExportAbsent('ObserverView'); - expectRootExportAbsent('WarpGraph'); - + it('opens the internal graph composition root', async () => { const graph = await openWarpGraph({ persistence: new InMemoryGraphAdapter(), graphName: 'public-api-smoke', @@ -195,7 +176,10 @@ describe('public API executable examples', () => { const liveObserver = await events.observer('public-users', PUBLIC_USERS_APERTURE); const historical = await events.seek({ source: { kind: 'live', ceiling: 1 } }); - const historicalObserver = await historical.observer('public-users-at-first-tick', PUBLIC_USERS_APERTURE); + const historicalObserver = await historical.observer( + 'public-users-at-first-tick', + PUBLIC_USERS_APERTURE + ); expect(liveObserver).toBeInstanceOf(Observer); expect(liveObserver.name).toBe('public-users'); @@ -226,8 +210,12 @@ describe('public API executable examples', () => { expect(report.diagnosticNames()).toEqual(['graph-wide-materialization']); expect(report.legacyNames()).toEqual(['legacy-query-arrays']); expect(report.requireCapability('memory-budget-contract').posture.toString()).toBe('safe'); - expect(report.requireCapability('checkpoint-tail-optics').posture.toString()).toBe('transitional'); - expect(report.requireCapability('graph-wide-materialization').posture.toString()).toBe('diagnostic'); + expect(report.requireCapability('checkpoint-tail-optics').posture.toString()).toBe( + 'transitional' + ); + expect(report.requireCapability('graph-wide-materialization').posture.toString()).toBe( + 'diagnostic' + ); expect(report.requireCapability('legacy-query-arrays').posture.toString()).toBe('legacy'); }); diff --git a/test/unit/domain/publicReadApiExecutablePaths.test.ts b/test/unit/domain/publicReadApiExecutablePaths.test.ts index 328fa543a..213c5d6b7 100644 --- a/test/unit/domain/publicReadApiExecutablePaths.test.ts +++ b/test/unit/domain/publicReadApiExecutablePaths.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { openWarpWorldline } from '../../../legacy.ts'; +import { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import { openRuntimeHostProduct } from '../../../src/domain/warp/RuntimeHostProduct.ts'; @@ -19,10 +19,7 @@ describe('public read API executable paths', () => { .setProperty('user:alice', 'secret', 'redacted'); }); await audit.commit((patch) => { - patch - .addNode('user:bob') - .setProperty('user:bob', 'name', 'Bob') - .addNode('internal:salary'); + patch.addNode('user:bob').setProperty('user:bob', 'name', 'Bob').addNode('internal:salary'); }); await expect(audit.live().hasNode('user:bob')).resolves.toBe(true); diff --git a/test/unit/domain/publicReadingSurface.behavior.test.ts b/test/unit/domain/publicReadingSurface.behavior.test.ts index d4f6b890e..86c77b599 100644 --- a/test/unit/domain/publicReadingSurface.behavior.test.ts +++ b/test/unit/domain/publicReadingSurface.behavior.test.ts @@ -1,14 +1,15 @@ import { TextDecoder } from 'node:util'; import { describe, expect, it } from 'vitest'; -import WarpAppDefault, { - InMemoryGraphAdapter, - openWarpGraph, - openWarpWorldline, - WarpApp, - WarpCore, -} from '../../../legacy.ts'; - -function openOptions(graphName: string, writerId: string): { +import WarpApp from '../../../src/domain/WarpApp.ts'; +import WarpCore from '../../../src/domain/WarpCore.ts'; +import { openWarpGraph } from '../../../src/domain/WarpGraph.ts'; +import { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; + +function openOptions( + graphName: string, + writerId: string +): { persistence: InMemoryGraphAdapter; graphName: string; writerId: string; @@ -63,9 +64,7 @@ describe('public reading surfaces', () => { if (!('nodes' in result)) { throw new Error('query result must include nodes'); } - expect(result.nodes).toEqual([ - { id: 'user:alice', props: { role: 'admin' } }, - ]); + expect(result.nodes).toEqual([{ id: 'user:alice', props: { role: 'admin' } }]); }); it('keeps WarpApp curated while WarpCore remains the explicit materialization escape hatch', async () => { @@ -78,7 +77,6 @@ describe('public reading surfaces', () => { }); }); - expect(WarpAppDefault).toBe(WarpApp); expect(app).toBeInstanceOf(WarpApp); expect(app.core()).toBeInstanceOf(WarpCore); expect('materialize' in app).toBe(false); diff --git a/test/unit/domain/publicStrandAndRuntimeSeams.test.ts b/test/unit/domain/publicStrandAndRuntimeSeams.test.ts index e698006c7..c3b150987 100644 --- a/test/unit/domain/publicStrandAndRuntimeSeams.test.ts +++ b/test/unit/domain/publicStrandAndRuntimeSeams.test.ts @@ -1,15 +1,11 @@ import { describe, expect, it, vi } from 'vitest'; -import { - StrandError, - WarpCore, - openWarpGraph, -} from '../../../legacy.ts'; +import WarpCore from '../../../src/domain/WarpCore.ts'; +import { openWarpGraph } from '../../../src/domain/WarpGraph.ts'; +import StrandError from '../../../src/domain/errors/StrandError.ts'; import RuntimeDetachedFactory from '../../../src/domain/warp/RuntimeDetachedFactory.ts'; import RuntimePatchCollector from '../../../src/domain/warp/RuntimePatchCollector.ts'; -import { - openRuntimeHostProduct, -} from '../../../src/domain/warp/RuntimeHostProduct.ts'; +import { openRuntimeHostProduct } from '../../../src/domain/warp/RuntimeHostProduct.ts'; import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import PatchJournalPort from '../../../src/ports/PatchJournalPort.ts'; import CheckpointStorePort from '../../../src/ports/CheckpointStorePort.ts'; @@ -60,9 +56,7 @@ describe('public strand and runtime host seams', () => { owner: 'agent-1', }); await core.patchStrand('review', (patch) => { - patch - .addNode('task:review') - .setProperty('task:review', 'status', 'draft'); + patch.addNode('task:review').setProperty('task:review', 'status', 'draft'); }); const braided = await core.braidStrand('review', { writable: false }); const materialized = await core.materializeStrand('review'); @@ -215,7 +209,7 @@ function createDetachedReadSurface(): DetachedGraphInternalReadSurface { } function requireDetachedOpenOptions( - open: ReturnType>, + open: ReturnType> ): DetachedOpenOptions { expect(open).toHaveBeenCalledTimes(1); const call = open.mock.calls[0]; @@ -237,7 +231,7 @@ class RecordingPatchJournalPort extends PatchJournalPort { scanPatchRange( _writerId: string, _fromSha: string | null, - _toSha: string, + _toSha: string ): WarpStream { return WarpStream.from([]); } @@ -271,18 +265,14 @@ class RecordingIndexStorePort extends IndexStorePort { return {}; } - async decodeShard( - _blobOid: string, - ): Promise { + async decodeShard(_blobOid: string): Promise { throw new RuntimeSeamTestError('decodeShard should not be called'); } } class RuntimeSeamTestError extends Error {} -function requireSinglePatchSha( - patches: ReadonlyArray<{ readonly sha: string }>, -): string { +function requireSinglePatchSha(patches: ReadonlyArray<{ readonly sha: string }>): string { const patch = patches[0]; if (patch === undefined) { throw new RuntimeSeamTestError('expected one strand patch'); diff --git a/test/unit/domain/services/merge/TtdMergeInspector.test.ts b/test/unit/domain/services/merge/TtdMergeInspector.test.ts index 5bc9821f7..45f47cb77 100644 --- a/test/unit/domain/services/merge/TtdMergeInspector.test.ts +++ b/test/unit/domain/services/merge/TtdMergeInspector.test.ts @@ -7,7 +7,7 @@ import { TtdMergeLoweringWitness, TtdMergeObstructionWitness, TtdMergePolicyRequirement, -} from '../../../../../legacy.ts'; +} from '../../../../../diagnostics.ts'; function itemAt(items: readonly T[], index: number): T { const item = items[index]; diff --git a/test/unit/domain/services/query/BoundedSupportRule.test.ts b/test/unit/domain/services/query/BoundedSupportRule.test.ts index fe5d8ec21..f0997322d 100644 --- a/test/unit/domain/services/query/BoundedSupportRule.test.ts +++ b/test/unit/domain/services/query/BoundedSupportRule.test.ts @@ -1,9 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { - BoundedSupportRule, - QueryBuilder, -} from '../../../../../legacy.ts'; +import { BoundedSupportRule } from '../../../../../advanced.ts'; +import { QueryBuilder } from '../../../../../diagnostics.ts'; import QueryError from '../../../../../src/domain/errors/QueryError.ts'; import type { QueryNeighborEntry, @@ -42,9 +40,7 @@ function query(): QueryBuilder { describe('BoundedSupportRule', () => { it('classifies exact node-id queries as entity support', () => { - const rule = query() - .match('user:alice') - .supportRule(); + const rule = query().match('user:alice').supportRule(); expect(rule).toBeInstanceOf(BoundedSupportRule); expect(Object.isFrozen(rule)).toBe(true); @@ -69,10 +65,7 @@ describe('BoundedSupportRule', () => { }); it('classifies wildcard reads as global discovery', () => { - const rule = query() - .match('task:*') - .where({ status: 'todo' }) - .supportRule(); + const rule = query().match('task:*').where({ status: 'todo' }).supportRule(); expect(rule.kind).toBe('global-discovery'); expect(rule.rootNodeIds).toEqual([]); @@ -93,16 +86,21 @@ describe('BoundedSupportRule', () => { }); it('rejects invalid runtime carriers', () => { - expect(() => new BoundedSupportRule({ - surface: 'query', - kind: 'entity', - reason: 'bad max depth', - maxDepth: -1, - })).toThrow(QueryError); + expect( + () => + new BoundedSupportRule({ + surface: 'query', + kind: 'entity', + reason: 'bad max depth', + maxDepth: -1, + }) + ).toThrow(QueryError); - expect(() => BoundedSupportRule.fromQueryPlan( - // @ts-expect-error runtime guard for JavaScript callers - undefined, - )).toThrow(QueryError); + expect(() => + BoundedSupportRule.fromQueryPlan( + // @ts-expect-error runtime guard for JavaScript callers + undefined + ) + ).toThrow(QueryError); }); }); diff --git a/test/unit/domain/services/query/CausalIndexPlan.test.ts b/test/unit/domain/services/query/CausalIndexPlan.test.ts index 84aadf907..1856d875a 100644 --- a/test/unit/domain/services/query/CausalIndexPlan.test.ts +++ b/test/unit/domain/services/query/CausalIndexPlan.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - BoundedSupportRule, - CausalIndexPlan, -} from '../../../../../legacy.ts'; +import { BoundedSupportRule, CausalIndexPlan } from '../../../../../advanced.ts'; import QueryError from '../../../../../src/domain/errors/QueryError.ts'; describe('CausalIndexPlan', () => { @@ -52,9 +49,11 @@ describe('CausalIndexPlan', () => { }); it('rejects invalid runtime carriers', () => { - expect(() => CausalIndexPlan.fromSupportRule( - // @ts-expect-error runtime guard for JavaScript callers - undefined, - )).toThrow(QueryError); + expect(() => + CausalIndexPlan.fromSupportRule( + // @ts-expect-error runtime guard for JavaScript callers + undefined + ) + ).toThrow(QueryError); }); }); diff --git a/test/unit/domain/services/query/SupportFragmentPlan.test.ts b/test/unit/domain/services/query/SupportFragmentPlan.test.ts index 50d38bfd7..8d689acbe 100644 --- a/test/unit/domain/services/query/SupportFragmentPlan.test.ts +++ b/test/unit/domain/services/query/SupportFragmentPlan.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest'; import { BoundedSupportRule, CausalIndexPlan, - QueryBuilder, SupportFragmentPlan, -} from '../../../../../legacy.ts'; +} from '../../../../../advanced.ts'; +import { QueryBuilder } from '../../../../../diagnostics.ts'; import QueryError from '../../../../../src/domain/errors/QueryError.ts'; import type { QueryNeighborEntry, @@ -44,9 +44,7 @@ function query(): QueryBuilder { describe('SupportFragmentPlan', () => { it('creates a cacheable support fragment plan for exact entity reads', () => { - const plan = query() - .match('node:a') - .supportFragmentPlan(); + const plan = query().match('node:a').supportFragmentPlan(); expect(plan).toBeInstanceOf(SupportFragmentPlan); expect(plan.posture).toBe('support-fragment'); @@ -56,7 +54,7 @@ describe('SupportFragmentPlan', () => { expect(plan.canMaterializeSupportFragment()).toBe(true); expect(plan.requiresFullGraphFallback()).toBe(false); expect(plan.fragmentKeyForCoordinate('frontier:demo')).toBe( - 'surface:query/kind:entity/roots:node:a/depth:none/directions:none/indexes:entity-patch@frontier:demo', + 'surface:query/kind:entity/roots:node:a/depth:none/directions:none/indexes:entity-patch@frontier:demo' ); expect(Object.isFrozen(plan)).toBe(true); expect(Object.isFrozen(plan.requiredEntityIds)).toBe(true); @@ -76,9 +74,7 @@ describe('SupportFragmentPlan', () => { }); it('marks wildcard discovery as full-graph fallback instead of a fragment key', () => { - const plan = query() - .match('node:*') - .supportFragmentPlan(); + const plan = query().match('node:*').supportFragmentPlan(); expect(plan.posture).toBe('global-fallback'); expect(plan.canMaterializeSupportFragment()).toBe(false); @@ -96,14 +92,18 @@ describe('SupportFragmentPlan', () => { nodeIds: ['node:b'], }); - expect(() => SupportFragmentPlan.fromSupportAndIndex({ - supportRule, - causalIndexPlan: CausalIndexPlan.fromSupportRule(otherSupportRule), - })).toThrow(QueryError); + expect(() => + SupportFragmentPlan.fromSupportAndIndex({ + supportRule, + causalIndexPlan: CausalIndexPlan.fromSupportRule(otherSupportRule), + }) + ).toThrow(QueryError); - expect(() => SupportFragmentPlan.fromSupportRule( - // @ts-expect-error runtime guard for JavaScript callers - undefined, - )).toThrow(QueryError); + expect(() => + SupportFragmentPlan.fromSupportRule( + // @ts-expect-error runtime guard for JavaScript callers + undefined + ) + ).toThrow(QueryError); }); }); diff --git a/test/unit/index.exports.test.ts b/test/unit/index.exports.test.ts deleted file mode 100644 index bca93c1e4..000000000 --- a/test/unit/index.exports.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import * as api from '../../legacy.ts'; - -describe('public runtime exports', () => { - it('exports the Worldline-first open helper and handle', () => { - expect(api.openWarpWorldline).toBeDefined(); - expect(api.WarpWorldline).toBeDefined(); - expect(api.WarpWorldlineCoordinate).toBeDefined(); - expect(api.WarpWorldlineOpticBasis).toBeDefined(); - expect(api.ProjectionHandle).toBeDefined(); - expect('Worldline' in api).toBe(false); - }); - - it('does not export the retired browser viewer service', () => { - expect('WarpServeService' in api).toBe(false); - }); - - it('does not export the retired browser viewer websocket port', () => { - expect('WebSocketServerPort' in api).toBe(false); - }); -}); diff --git a/test/unit/scripts/jsr-publish-root-graph-shape.test.ts b/test/unit/scripts/jsr-publish-root-graph-shape.test.ts deleted file mode 100644 index 9b07f65ce..000000000 --- a/test/unit/scripts/jsr-publish-root-graph-shape.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; - -import jsrJson from '../../../jsr.json' with { type: 'json' }; - -const ROOT_EXPORT_FILE = '../../../legacy.ts'; -const ROOT_COMPATIBILITY_IMPORT = "from './rootCompatibility.ts'"; -const ROOT_COMPATIBILITY_PUBLISH_ENTRY = 'rootCompatibility.ts'; - -function readRepoFile(relativePath: string): string { - return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'); -} - -describe('JSR publish root graph shape', () => { - it('publishes the root compatibility module imported by the root export', () => { - expect(readRepoFile(ROOT_EXPORT_FILE)).toContain(ROOT_COMPATIBILITY_IMPORT); - expect(jsrJson.publish.include).toContain(ROOT_COMPATIBILITY_PUBLISH_ENTRY); - }); -}); diff --git a/test/unit/scripts/openwarpgraph-composition-root.test.ts b/test/unit/scripts/openwarpgraph-composition-root.test.ts index e07661053..14fa5b8af 100644 --- a/test/unit/scripts/openwarpgraph-composition-root.test.ts +++ b/test/unit/scripts/openwarpgraph-composition-root.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import ts from 'typescript'; import { describe, expect, it } from 'vitest'; -import { openWarpGraph } from '../../../legacy.ts'; +import { openWarpGraph } from '../../../src/domain/WarpGraph.ts'; import { createInMemoryRepo } from '../../helpers/warpGraphTestUtils.ts'; type SourcePaths = { @@ -34,7 +34,7 @@ function parseSource(sourceFilePath: string): ts.SourceFile { sourceFilePath, readFileSync(sourceFilePath, 'utf8'), ts.ScriptTarget.Latest, - true, + true ); } @@ -65,7 +65,11 @@ function hasIdentifierCall(sourceFile: ts.SourceFile, identifier: string): boole return found; } -function hasPropertyCall(sourceFile: ts.SourceFile, objectName: string, propertyName: string): boolean { +function hasPropertyCall( + sourceFile: ts.SourceFile, + objectName: string, + propertyName: string +): boolean { let found = false; function visit(node: ts.Node): void { @@ -74,7 +78,8 @@ function hasPropertyCall(sourceFile: ts.SourceFile, objectName: string, property } if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { const callTarget = node.expression; - found = ts.isIdentifier(callTarget.expression) && + found = + ts.isIdentifier(callTarget.expression) && callTarget.expression.text === objectName && callTarget.name.text === propertyName; } @@ -92,7 +97,8 @@ function exportedFunctionNames(sourceFile: ts.SourceFile): Set { if (!ts.isFunctionDeclaration(node) || node.name === undefined) { return; } - const isExported = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; + const isExported = + node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; if (isExported) { names.add(node.name.text); } @@ -101,14 +107,18 @@ function exportedFunctionNames(sourceFile: ts.SourceFile): Set { return names; } -function exportedFunction(sourceFile: ts.SourceFile, functionName: string): ts.FunctionDeclaration | null { +function exportedFunction( + sourceFile: ts.SourceFile, + functionName: string +): ts.FunctionDeclaration | null { let match: ts.FunctionDeclaration | null = null; sourceFile.forEachChild((node) => { if (match !== null || !ts.isFunctionDeclaration(node) || node.name === undefined) { return; } - const isExported = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; + const isExported = + node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; if (isExported && node.name.text === functionName) { match = node; } @@ -159,7 +169,9 @@ function collectCompositionRootViolations(): string[] { violations.push('RuntimeHost no longer imports the dedicated boot module'); } if (!hasIdentifierCall(runtimeHost, 'resolveRuntimeHostConstructionOptions')) { - violations.push('RuntimeHost no longer delegates option resolution to the dedicated boot module'); + violations.push( + 'RuntimeHost no longer delegates option resolution to the dedicated boot module' + ); } if (!exportedFunctionNames(runtimeHostBoot).has('resolveRuntimeHostConstructionOptions')) { violations.push('RuntimeHostBoot no longer exports the construction-option resolver'); @@ -193,9 +205,13 @@ describe('openWarpGraph compatibility composition root', () => { expect(Object.prototype.hasOwnProperty.call(graph, '_runtime')).toBe(false); expect('_runtime' in graph).toBe(false); - await (await graph.patches.createPatch()).addNode('node:compatibility-composition-root').commit(); + await (await graph.patches.createPatch()) + .addNode('node:compatibility-composition-root') + .commit(); - await expect(graph.query.hasNode('node:compatibility-composition-root')).rejects.toMatchObject({ + await expect( + graph.query.hasNode('node:compatibility-composition-root') + ).rejects.toMatchObject({ code: 'E_NO_STATE', }); } finally { diff --git a/test/unit/scripts/public-api-aperture-noun.test.ts b/test/unit/scripts/public-api-aperture-noun.test.ts index 745a64d87..74ae43016 100644 --- a/test/unit/scripts/public-api-aperture-noun.test.ts +++ b/test/unit/scripts/public-api-aperture-noun.test.ts @@ -1,12 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { - Observer, - computeTranslationCost, -} from '../../../legacy.ts'; -import type { - Aperture, - ObserverConfig, -} from '../../../legacy.ts'; +import { Observer } from '../../../advanced.ts'; +import { computeTranslationCost } from '../../../src/domain/services/TranslationCost.ts'; +import type { Aperture, ObserverConfig } from '../../../advanced.ts'; function acceptAperture(config: Aperture): ObserverConfig { return config; diff --git a/test/unit/scripts/public-api-facade-split.test.ts b/test/unit/scripts/public-api-facade-split.test.ts deleted file mode 100644 index 5c70dfc13..000000000 --- a/test/unit/scripts/public-api-facade-split.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import WarpAppDefault, { - InMemoryGraphAdapter, - WarpApp, - WarpCore, -} from '../../../legacy.ts'; -import * as publicApi from '../../../legacy.ts'; - -function openOptions(graphName: string, writerId: string): Parameters[0] { - return { - persistence: new InMemoryGraphAdapter(), - graphName, - writerId, - }; -} - -describe('public facade split', () => { - it('exports WarpApp as the default compatibility facade without WarpRuntime', () => { - expect(WarpAppDefault).toBe(WarpApp); - expect(publicApi.WarpApp).toBe(WarpApp); - expect(publicApi.WarpCore).toBe(WarpCore); - expect(Object.prototype.hasOwnProperty.call(publicApi, 'WarpRuntime')).toBe(false); - }); - - it('opens WarpApp as a curated facade with an explicit WarpCore escape hatch', async () => { - const app = await WarpApp.open(openOptions('facade-app', 'writer-app')); - - expect(app).toBeInstanceOf(WarpApp); - expect(app.graphName).toBe('facade-app'); - expect(app.writerId).toBe('writer-app'); - expect(app.core()).toBeInstanceOf(WarpCore); - expect('materialize' in app).toBe(false); - expect('getNodes' in app).toBe(false); - expect('query' in app).toBe(false); - }); - - it('opens WarpCore as the explicit compatibility escape hatch', async () => { - const core = await WarpCore.open(openOptions('facade-core', 'writer-core')); - - expect(core).toBeInstanceOf(WarpCore); - expect(core.graphName).toBe('facade-core'); - expect(core.writerId).toBe('writer-core'); - expect(typeof core.createPatch).toBe('function'); - expect(typeof core.materialize).toBe('function'); - }); -}); diff --git a/test/unit/scripts/release-artifact-command.test.ts b/test/unit/scripts/release-artifact-command.test.ts index 7c8bcf548..985d0e8ac 100644 --- a/test/unit/scripts/release-artifact-command.test.ts +++ b/test/unit/scripts/release-artifact-command.test.ts @@ -1,4 +1,8 @@ -import { spawnSync, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from 'node:child_process'; +import { + spawnSync, + type SpawnSyncOptionsWithStringEncoding, + type SpawnSyncReturns, +} from 'node:child_process'; import { describe, expect, it } from 'vitest'; const COMMAND_TIMEOUT_MS = 120_000; @@ -6,7 +10,7 @@ const COMMAND_TIMEOUT_MS = 120_000; type CommandRunner = ( command: string, args: readonly string[], - options: SpawnSyncOptionsWithStringEncoding, + options: SpawnSyncOptionsWithStringEncoding ) => SpawnSyncReturns; type SpawnCall = { @@ -18,9 +22,13 @@ type SpawnCall = { type PackSectionName = 'Tarball Contents' | 'Tarball Details'; -const defaultCommandRunner: CommandRunner = (command, args, options) => spawnSync(command, [...args], options); +const defaultCommandRunner: CommandRunner = (command, args, options) => + spawnSync(command, [...args], options); -function runNpmCommand(args: readonly string[], runner: CommandRunner = defaultCommandRunner): string { +function runNpmCommand( + args: readonly string[], + runner: CommandRunner = defaultCommandRunner +): string { const result = runner('npm', [...args], { encoding: 'utf8', timeout: COMMAND_TIMEOUT_MS, @@ -94,21 +102,25 @@ describe('release artifact command evidence', () => { runNpmCommand(['--version'], recordingRunner); - expect(calls).toEqual([{ - command: 'npm', - args: ['--version'], - timeout: COMMAND_TIMEOUT_MS, - killSignal: 'SIGKILL', - }]); + expect(calls).toEqual([ + { + command: 'npm', + args: ['--version'], + timeout: COMMAND_TIMEOUT_MS, + killSignal: 'SIGKILL', + }, + ]); }); it('parses decorated npm pack section headers', () => { - const entries = packEntries([ - 'npm notice === Tarball Contents ===', - 'npm notice 1.2kB dist/index.js', - 'npm notice === Tarball Details ===', - 'npm notice name: @git-stunts/git-warp', - ].join('\n')); + const entries = packEntries( + [ + 'npm notice === Tarball Contents ===', + 'npm notice 1.2kB dist/index.js', + 'npm notice === Tarball Details ===', + 'npm notice name: @git-stunts/git-warp', + ].join('\n') + ); expect(entries).toEqual(new Set(['dist/index.js'])); }); @@ -118,7 +130,12 @@ describe('release artifact command evidence', () => { expect(entries.has('dist/index.js')).toBe(true); expect(entries.has('dist/index.d.ts')).toBe(true); - expect(entries.has('dist/browser.js')).toBe(true); + expect(entries.has('dist/browser.js')).toBe(false); + expect(entries.has('dist/browser.d.ts')).toBe(false); + expect(entries.has('dist/legacy.js')).toBe(false); + expect(entries.has('dist/legacy.d.ts')).toBe(false); + expect(entries.has('dist/rootCompatibility.js')).toBe(false); + expect(entries.has('dist/rootCompatibility.d.ts')).toBe(false); expect(entries.has('dist/bin/warp-graph.js')).toBe(true); expect(entries.has('bin/git-warp')).toBe(true); expect(entries.has('README.md')).toBe(true); diff --git a/test/unit/scripts/root-export-line-budget.test.ts b/test/unit/scripts/root-export-line-budget.test.ts index 02edc030a..9bdfcd9d1 100644 --- a/test/unit/scripts/root-export-line-budget.test.ts +++ b/test/unit/scripts/root-export-line-budget.test.ts @@ -18,10 +18,4 @@ describe('root export line budget', () => { expect(lines).toBeLessThanOrEqual(SOURCE_LINE_LIMIT); }); - - it('keeps the legacy compatibility barrel below the source line cap', () => { - const lines = lineCount(readRepoFile('../../../legacy.ts')); - - expect(lines).toBeLessThanOrEqual(SOURCE_LINE_LIMIT); - }); }); diff --git a/test/unit/scripts/v18-package-surface-audit.test.ts b/test/unit/scripts/v18-package-surface-audit.test.ts deleted file mode 100644 index 0df05d79c..000000000 --- a/test/unit/scripts/v18-package-surface-audit.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; - -function repoPath(relativePath: string): string { - return fileURLToPath(new URL(`../../../${relativePath}`, import.meta.url)); -} - -function readText(relativePath: string): string { - return readFileSync(repoPath(relativePath), 'utf8'); -} - -function lineCount(source: string): number { - return source.split('\n').length; -} - -const packageJson = readText('package.json'); -const jsrJson = readText('jsr.json'); -const tsconfigPublish = readText('tsconfig.publish.json'); -const readme = readText('README.md'); -const indexSource = readText('index.ts'); -const legacySource = readText('legacy.ts'); -const generatedIndexDeclarationPath = repoPath('dist/index.d.ts'); - -function moduleDoc(source: string, sourceName: string): string { - const terminator = source.indexOf('*/'); - if (terminator === -1) { - throw new Error(`${sourceName} is missing its package module JSDoc block`); - } - return source.slice(0, terminator + '*/'.length); -} - -const indexModuleDoc = moduleDoc(indexSource, 'index.ts'); -const legacyModuleDoc = moduleDoc(legacySource, 'legacy.ts'); - -describe('v18 package surface audit', () => { - it('positions the registry package around the v19 API boundary', () => { - expect(packageJson).toContain( - '"description": "Git-native causal history runtime for intent writes, timeline reads, and receipts."', - ); - expect(packageJson).not.toContain('"worldline"'); - expect(packageJson).toContain('"local-first"'); - expect(packageJson).toContain('"crdt"'); - }); - - it('keeps npm and JSR root exports pointed at the public barrel', () => { - expect(packageJson).toContain('"types": "./dist/index.d.ts"'); - expect(packageJson).toContain('"import": "./dist/index.js"'); - expect(packageJson).toContain('"default": "./dist/index.js"'); - expect(jsrJson).toContain('".": "./index.ts"'); - }); - - it('keeps the legacy root declaration monolith retired', () => { - expect(existsSync(repoPath('index.d.ts'))).toBe(false); - expect(packageJson).not.toContain('"types": "./index.d.ts"'); - expect(tsconfigPublish).toContain('"declaration": true'); - if (existsSync(generatedIndexDeclarationPath)) { - expect(lineCount(readFileSync(generatedIndexDeclarationPath, 'utf8'))).toBeLessThanOrEqual(500); - } - }); - - it('moves the Worldline-first opener, handle, and option types to legacy', () => { - expect(indexSource).not.toContain('import WarpWorldline, { openWarpWorldline }'); - expect(legacySource).toContain('import WarpWorldline, { openWarpWorldline }'); - expect(legacySource).toContain('openWarpWorldline,'); - expect(legacySource).toContain('WarpWorldline,'); - expect(legacySource).toContain('ProjectionHandle,'); - expect(legacySource).not.toMatch(/^\s+Worldline,$/m); - expect(legacySource).toContain('WarpWorldlineOpenOptions,'); - expect(legacySource).toContain('WarpWorldlinePatchBuild,'); - }); - - it('keeps package hover docs on the v19 boundary story', () => { - expect(indexModuleDoc).toContain('Public v19 application boundary'); - expect(indexModuleDoc).toContain('write intents, read timelines, and keep receipts'); - expect(indexModuleDoc).toContain('@git-stunts/git-warp/legacy'); - expect(indexModuleDoc).toContain('@git-stunts/git-warp/storage'); - expect(indexModuleDoc).toContain('@git-stunts/git-warp/advanced'); - expect(indexModuleDoc).toContain('@git-stunts/git-warp/diagnostics'); - expect(indexModuleDoc).not.toContain('openWarpWorldline'); - expect(indexModuleDoc).not.toContain('WarpApp.open('); - expect(indexModuleDoc).not.toContain('app.materialize('); - }); - - it('keeps legacy hover docs deprecated and example-free', () => { - expect(legacyModuleDoc).toContain('@deprecated'); - expect(legacyModuleDoc).toContain('migration-only'); - expect(legacyModuleDoc).not.toContain('@example'); - expect(legacyModuleDoc).not.toContain('openWarpWorldline'); - expect(legacyModuleDoc).not.toContain("events.commit((patch) =>"); - expect(legacyModuleDoc).not.toContain('events.live().getNodeProps'); - expect(legacyModuleDoc).not.toContain('WarpApp.open('); - expect(legacyModuleDoc).not.toContain('app.createPatch('); - expect(legacyModuleDoc).not.toContain('app.materialize('); - }); - - it('keeps legacy default export compatibility explicit', () => { - expect(indexSource).not.toContain('export default WarpApp;'); - expect(legacySource).toContain('export default WarpApp;'); - expect(legacySource).toContain('Deprecated default export retained only for old migration callers'); - }); - - it('keeps the README from teaching copyable legacy API setup', () => { - expect(readme).not.toContain('import { openWarpGraph } from "@git-stunts/git-warp/legacy";'); - expect(readme).not.toContain('const graph = await openWarpGraph('); - expect(readme).not.toContain('const events = await openWarpWorldline('); - expect(readme).toContain('The v18 graph-first API remains only under `@git-stunts/git-warp/legacy`.'); - expect(readme).toContain('Treat every legacy import as removal debt.'); - }); -}); diff --git a/test/unit/scripts/v19-public-api-boundary.test.ts b/test/unit/scripts/v19-public-api-boundary.test.ts index 517fdfd6a..8c5c613e4 100644 --- a/test/unit/scripts/v19-public-api-boundary.test.ts +++ b/test/unit/scripts/v19-public-api-boundary.test.ts @@ -1,10 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -import { - extractJsExports, - parseExportBlock, -} from '../../../scripts/check-dts-surface.ts'; +import { extractJsExports, parseExportBlock } from '../../../scripts/check-dts-surface.ts'; const REPO_ROOT = new URL('../../../', import.meta.url); @@ -76,44 +73,61 @@ function hasForbiddenVocabulary(name: string): boolean { return false; } - return name.includes('Graph') - || name.includes('Worldline') - || name.includes('Strand') - || name.includes('Optic') - || name.includes('Hologram') - || name.includes('Witness') - || name.includes('Braid') - || name.includes('Wormhole') - || name.includes('Projection') - || name.includes('Observer') - || name.includes('Query') - || name.includes('Coordinate') - || name.includes('Selector') - || name === 'WarpApp' - || name === 'WarpCore' - || name === 'PatchBuilder' - || name === 'PatchSession' - || name.startsWith('createNode') - || name.startsWith('createEdge') - || name.startsWith('createProp') - || name === 'createInlineValue' - || name === 'createBlobValue' - || name === 'decodeEdgePropKey' - || name === 'encodeEdgePropKey' - || name === 'isEdgePropKey' - || GRAPH_SUBSTRATE_NOUNS.has(name); + return ( + name.includes('Graph') || + name.includes('Worldline') || + name.includes('Strand') || + name.includes('Optic') || + name.includes('Hologram') || + name.includes('Witness') || + name.includes('Braid') || + name.includes('Wormhole') || + name.includes('Projection') || + name.includes('Observer') || + name.includes('Query') || + name.includes('Coordinate') || + name.includes('Selector') || + name === 'WarpApp' || + name === 'WarpCore' || + name === 'PatchBuilder' || + name === 'PatchSession' || + name.startsWith('createNode') || + name.startsWith('createEdge') || + name.startsWith('createProp') || + name === 'createInlineValue' || + name === 'createBlobValue' || + name === 'decodeEdgePropKey' || + name === 'encodeEdgePropKey' || + name === 'isEdgePropKey' || + GRAPH_SUBSTRATE_NOUNS.has(name) + ); } function forbiddenExportsFrom(exportNames: readonly string[]): string[] { return sorted(exportNames.filter((name) => hasForbiddenVocabulary(name))); } +function packageExportNames(relativePath: string): string[] { + const value: unknown = JSON.parse(readFileSync(new URL(relativePath, REPO_ROOT), 'utf8')); + if (!isRecord(value) || !isRecord(value['exports'])) { + throw new Error(`${relativePath} must contain an exports object`); + } + return sorted(Object.keys(value['exports'])); +} + +function isRecord(value: unknown): value is Readonly> { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + describe('v19 public API boundary', () => { it('keeps graph, optic, worldline, witness, and diagnostic nouns out of package root', () => { expect(forbiddenExportsFrom(collectSourceExports('index.ts'))).toEqual([]); }); - it('keeps graph, optic, worldline, witness, and diagnostic nouns out of browser root', () => { - expect(forbiddenExportsFrom(collectSourceExports('browser.ts'))).toEqual([]); + it('does not publish retired browser or legacy compatibility entrypoints', () => { + expect(packageExportNames('package.json')).not.toContain('./browser'); + expect(packageExportNames('package.json')).not.toContain('./legacy'); + expect(packageExportNames('jsr.json')).not.toContain('./browser'); + expect(packageExportNames('jsr.json')).not.toContain('./legacy'); }); }); diff --git a/test/unit/scripts/worldline-api-deprecation.test.ts b/test/unit/scripts/worldline-api-deprecation.test.ts deleted file mode 100644 index ec283c09c..000000000 --- a/test/unit/scripts/worldline-api-deprecation.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; -import { describe, expect, it } from 'vitest'; - -const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); - -function readRepoFile(relativePath: string): string { - return readFileSync(`${repoRoot}${relativePath}`, 'utf8'); -} - -function parseRepoFile(relativePath: string): ts.SourceFile { - return ts.createSourceFile( - relativePath, - readRepoFile(relativePath), - ts.ScriptTarget.Latest, - true, - ); -} - -function exportedFunction(sourceFile: ts.SourceFile, functionName: string): ts.FunctionDeclaration | null { - let match: ts.FunctionDeclaration | null = null; - - sourceFile.forEachChild((node) => { - if (match !== null || !ts.isFunctionDeclaration(node) || node.name === undefined) { - return; - } - const exported = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; - if (exported && node.name.text === functionName) { - match = node; - } - }); - - return match; -} - -function jsDocTagComment(tag: ts.JSDocTag): string { - const comment = tag.comment; - if (typeof comment === 'string') { - return comment; - } - if (comment === undefined) { - return ''; - } - return comment.map((part) => part.getText()).join(''); -} - -function deprecationTextForExportedFunction(relativePath: string, functionName: string): string { - const declaration = exportedFunction(parseRepoFile(relativePath), functionName); - if (declaration === null) { - return ''; - } - const deprecatedTag = ts.getJSDocTags(declaration) - .find((tag) => tag.tagName.text === 'deprecated'); - if (deprecatedTag === undefined) { - return ''; - } - return jsDocTagComment(deprecatedTag); -} - -describe('Worldline-first legacy API deprecation posture', () => { - it('marks openWarpGraph as a deprecated compatibility surface', () => { - const deprecation = deprecationTextForExportedFunction('src/domain/WarpGraph.ts', 'openWarpGraph'); - - expect(deprecation).toContain('root intent/timeline/reading/receipt API'); - expect(deprecation).toContain('migration-only'); - expect(deprecation).toContain('diagnostic tooling'); - }); - - it('marks WarpApp as a compatibility facade for graph-first migrations', () => { - const source = readRepoFile('src/domain/WarpApp.ts'); - - expect(source).toContain('root intent/timeline/reading/receipt API'); - expect(source).toContain('migration-only compatibility facade'); - }); - - it('keeps WarpCore supported for substrate diagnostics instead of first-use apps', () => { - const source = readRepoFile('src/domain/WarpCore.ts'); - - expect(source).toContain('root intent/timeline/reading/receipt API'); - expect(source).toContain('migration-only'); - expect(source).toContain('substrate tooling'); - expect(source).toContain('diagnostics, replay'); - }); - - it('moves the compatibility module comments to the legacy entrypoint', () => { - const source = readRepoFile('legacy.ts'); - - expect(source).toContain('Deprecated graph-first compatibility surface for v18-era code.'); - expect(source).toContain('@deprecated'); - expect(source).toContain('migration-only'); - expect(source).toContain("@git-stunts/git-warp/legacy"); - expect(source).toContain('export default WarpApp;'); - }); - - it('marks openWarpWorldline as deprecated instead of a replacement path', () => { - const deprecation = deprecationTextForExportedFunction( - 'src/domain/WarpWorldline.ts', - 'openWarpWorldline', - ); - - expect(deprecation).toContain('root `openWarp().timeline(name)` API'); - expect(deprecation).toContain('migration-only'); - }); -}); diff --git a/test/unit/v7-guards.test.ts b/test/unit/v7-guards.test.ts index 53ac8856d..fb2e02925 100644 --- a/test/unit/v7-guards.test.ts +++ b/test/unit/v7-guards.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest'; -import * as publicApi from '../../legacy.ts'; -import { - InMemoryGraphAdapter, - PatchBuilder, - WarpCore, -} from '../../legacy.ts'; +import WarpCore from '../../src/domain/WarpCore.ts'; +import { PatchBuilder } from '../../src/domain/services/PatchBuilder.ts'; +import InMemoryGraphAdapter from '../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; function openCore(graphName: string): Promise { return WarpCore.open({ @@ -14,12 +11,9 @@ function openCore(graphName: string): Promise { }); } -describe('V7 schema-2 public contract', () => { - it('exports the schema-2 PatchBuilder without schema-1 public artifacts', () => { - expect(publicApi.PatchBuilder).toBe(PatchBuilder); - expect(Object.prototype.hasOwnProperty.call(publicApi, 'Reducer')).toBe(false); - expect(Object.prototype.hasOwnProperty.call(publicApi, 'StateSerializer')).toBe(false); - expect(Object.prototype.hasOwnProperty.call(publicApi, 'createPatchV1')).toBe(false); +describe('schema-2 runtime contract', () => { + it('constructs the current schema-2 PatchBuilder', () => { + expect(PatchBuilder).toBeTypeOf('function'); }); it('builds schema-2 patches through the current public graph API', async () => { @@ -51,6 +45,8 @@ describe('V7 schema-2 public contract', () => { await expect(core.hasNode('v7:source')).resolves.toBe(true); await expect(core.hasNode('v7:target')).resolves.toBe(true); await expect(core.getNodeProps('v7:source')).resolves.toEqual({ status: 'current' }); - await expect(core.getEdgeProps('v7:source', 'v7:target', 'relates')).resolves.toEqual({ weight: 7 }); + await expect(core.getEdgeProps('v7:source', 'v7:target', 'relates')).resolves.toEqual({ + weight: 7, + }); }); }); diff --git a/tsconfig.publish.json b/tsconfig.publish.json index dfe76a6e2..cc6979c75 100644 --- a/tsconfig.publish.json +++ b/tsconfig.publish.json @@ -10,11 +10,9 @@ }, "include": [ "index.ts", - "browser.ts", "storage.ts", "advanced.ts", "diagnostics.ts", - "legacy.ts", "src/**/*.ts", "src/globals.d.ts", "bin/**/*.ts", diff --git a/tsconfig.src.json b/tsconfig.src.json index da4697deb..bba76d77b 100644 --- a/tsconfig.src.json +++ b/tsconfig.src.json @@ -2,11 +2,9 @@ "extends": "./tsconfig.base.json", "include": [ "index.ts", - "browser.ts", "storage.ts", "advanced.ts", "diagnostics.ts", - "legacy.ts", "src/**/*.ts", "src/globals.d.ts", "bin/warp-graph.ts", diff --git a/vitest.config.ts b/vitest.config.ts index e4a06e7e3..2096b27fb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/ports/**/*.ts', 'src/**/*.d.ts'], thresholds: { - lines: 92.56, + lines: 92.62, autoUpdate: shouldAutoUpdateCoverageRatchet(), }, }, From d85bcbe3092a50d125c3940b178e3a76b7759796 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 12 Jul 2026 15:31:59 -0700 Subject: [PATCH 2/7] feat(api): enforce the v19 public contract --- README.md | 24 +- advanced.ts | 92 +---- diagnostics.ts | 85 +++-- docs/migrations/v19/README.md | 73 ++-- docs/topics/api/README.md | 32 +- docs/topics/getting-started.md | 48 ++- docs/topics/querying.md | 51 ++- docs/topics/reference.md | 323 ++++-------------- index.ts | 124 ++----- jsr.json | 3 +- package.json | 5 - scripts/check-source-backed-reference.ts | 14 +- scripts/direct-export-name.ts | 6 + src/domain/api/DraftTimelineRuntime.ts | 28 +- src/domain/api/Intent.ts | 54 +-- src/domain/api/IntentBuilders.ts | 7 - src/domain/api/IntentRuntime.ts | 14 +- src/domain/api/JoinReceipt.ts | 35 +- src/domain/api/ReadReceipt.ts | 92 ++++- src/domain/api/Reading.ts | 101 +++++- src/domain/api/ReadingBuilders.ts | 3 + src/domain/api/ReadingRuntime.ts | 189 ++++++++-- src/domain/api/Receipt.ts | 5 + src/domain/api/ReceiptOutcome.ts | 11 + src/domain/api/ReceiptSupport.ts | 16 + src/domain/api/Tick.ts | 23 ++ src/domain/api/TickRuntime.ts | 32 ++ src/domain/api/Timeline.ts | 110 +++++- src/domain/api/TimelineRuntime.ts | 25 +- src/domain/api/TimelineView.ts | 48 +++ src/domain/api/TimelineViewRuntime.ts | 15 + src/domain/api/WriteReceipt.ts | 92 +++-- src/domain/api/WriteRuntime.ts | 79 +++++ src/domain/api/openWarp.ts | 4 +- .../controllers/CheckpointController.ts | 31 +- .../adapters/GitGraphAdapter.ts | 2 +- storage.ts | 32 +- ...st.ts => immutableSnapshotSurface.test.ts} | 2 +- .../v18FirstUseOpticsHonesty.test.ts | 15 + test/type-check/v19-consumer.ts | 61 +++- test/type-check/v19-subpaths.ts | 40 +-- test/unit/domain/ReceiptDiagnostics.test.ts | 39 +++ test/unit/domain/ReceiptOutcome.test.ts | 22 +- test/unit/domain/WarpFacade.test.ts | 150 +++++++- .../GitWarpReceiptEnvelopeBoundary.test.ts | 2 +- test/unit/domain/diagnostics.exports.test.ts | 51 --- ...> internalReadingSurface.behavior.test.ts} | 2 +- .../controllers/CheckpointController.test.ts | 5 +- .../services/merge/TtdMergeInspector.test.ts | 14 +- .../services/query/BoundedSupportRule.test.ts | 4 +- .../services/query/CausalIndexPlan.test.ts | 3 +- .../query/SupportFragmentPlan.test.ts | 10 +- ....test.ts => strandAndRuntimeSeams.test.ts} | 2 +- ...ts => worldlineExecutableExamples.test.ts} | 9 +- ...s => worldlineReadExecutablePaths.test.ts} | 2 +- test/unit/scripts/non-ts-tail-shape.test.ts | 10 +- .../scripts/public-api-aperture-noun.test.ts | 29 -- .../scripts/v19-public-api-boundary.test.ts | 275 +++++++++------ 58 files changed, 1660 insertions(+), 1015 deletions(-) create mode 100644 scripts/direct-export-name.ts create mode 100644 src/domain/api/Receipt.ts create mode 100644 src/domain/api/ReceiptOutcome.ts create mode 100644 src/domain/api/ReceiptSupport.ts create mode 100644 src/domain/api/Tick.ts create mode 100644 src/domain/api/TickRuntime.ts create mode 100644 src/domain/api/TimelineView.ts create mode 100644 src/domain/api/TimelineViewRuntime.ts create mode 100644 src/domain/api/WriteRuntime.ts rename test/conformance/{snapshotPublicApiSurface.test.ts => immutableSnapshotSurface.test.ts} (97%) create mode 100644 test/unit/domain/ReceiptDiagnostics.test.ts delete mode 100644 test/unit/domain/diagnostics.exports.test.ts rename test/unit/domain/{publicReadingSurface.behavior.test.ts => internalReadingSurface.behavior.test.ts} (98%) rename test/unit/domain/{publicStrandAndRuntimeSeams.test.ts => strandAndRuntimeSeams.test.ts} (99%) rename test/unit/domain/{publicApiExecutableExamples.test.ts => worldlineExecutableExamples.test.ts} (96%) rename test/unit/domain/{publicReadApiExecutablePaths.test.ts => worldlineReadExecutablePaths.test.ts} (97%) delete mode 100644 test/unit/scripts/public-api-aperture-noun.test.ts diff --git a/README.md b/README.md index bb6694024..0631a7ebd 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ See [CHANGELOG.md](CHANGELOG.md) for the full in-repository release notes. ## v19 First-Use API This is the public contract new application code should start from. The -deprecated v18 graph-first API remains available only for migration. +v18 graph-first package exports are removed rather than carried as a second +compatibility API. ```typescript import { openWarp, intent, reading } from '@git-stunts/git-warp'; @@ -84,6 +85,10 @@ const write = await events.write( }) ); +if (write.outcome !== 'accepted') { + throw new Error(write.reason); +} + const role = await events.read( reading.property({ subject: 'user:alice', @@ -91,8 +96,12 @@ const role = await events.read( }) ); -console.log(role.value); -console.log(role.receipt); +if (role.receipt.outcome === 'resolved') { + console.log(role.value); + console.log(role.receipt.evidence); +} else { + console.error(role.receipt.reason, role.receipt.repairHints); +} ``` The v18 graph-first API is not exported in v19. Migrate uses of @@ -102,9 +111,9 @@ upgrading. ### Bounded Reads -The v19 public API names bounded questions as readings. The formal optic, -coordinate, observer, and support machinery remains available to expert code -through advanced/diagnostic surfaces, not the package root. +The v19 public API names bounded questions as readings. Formal coordinate and +optic access lives under `advanced`; receipt inspection lives under +`diagnostics`. Observer, support-plan, and host machinery is internal. - A **reading** is the bounded question the public API should expose. - An **optic** is the formal execution shape used by expert and proof-oriented @@ -114,6 +123,9 @@ through advanced/diagnostic surfaces, not the package root. - Missing support should produce an honest receipt outcome, not a silent whole-history materialization. +Create a missing basis with `git warp checkpoint create`, or reconcile +state-cache retention with `git warp doctor --repair-state-cache`. +

For the Nerds™: Optics

diff --git a/advanced.ts b/advanced.ts index f74b6b6a3..13ceb0bec 100644 --- a/advanced.ts +++ b/advanced.ts @@ -1,80 +1,24 @@ /** - * Formal and support-oriented API surface. + * Formal bounded-reading API for expert consumers. * - * These exports keep advanced read/evidence machinery out of the package root. - * Application code should prefer the root intent/timeline/receipt API as it - * lands through v19. + * Capture a `Coordinate` from a public `Timeline`, then lower reads through + * its executable `Optic`. A successful read carries a type-only `Witness` + * through the root receipt evidence contract. */ -export { default as BoundedSupportRule } from './src/domain/services/query/BoundedSupportRule.ts'; -export type { - BoundedSupportDirection, - BoundedSupportKind, - BoundedSupportRuleFields, - BoundedSupportSurface, -} from './src/domain/services/query/BoundedSupportRule.ts'; -export { default as CausalIndexPlan } from './src/domain/services/query/CausalIndexPlan.ts'; -export type { - CausalIndexFamily, - CausalIndexPlanFields, - CausalIndexPlanPosture, -} from './src/domain/services/query/CausalIndexPlan.ts'; -export { - composeWormholes, - createWormhole, - deserializeWormhole, - replayWormhole, - serializeWormhole, -} from './src/application/WormholeServiceDefaults.ts'; -export { default as LiveSelector } from './src/domain/types/LiveSelector.ts'; -export { default as Observer } from './src/domain/services/query/Observer.ts'; -export { default as ObserverAccumulation } from './src/domain/services/query/ObserverAccumulation.ts'; -export { default as ObserverBasis } from './src/domain/services/query/ObserverBasis.ts'; -export { default as ObserverEmission } from './src/domain/services/query/ObserverEmission.ts'; -export { default as ObserverPlan } from './src/domain/services/query/ObserverPlan.ts'; -export type { ObserverPlanFields } from './src/domain/services/query/ObserverPlan.ts'; -export { default as ObserverReadingEnvelope } from './src/domain/services/query/ObserverReadingEnvelope.ts'; -export type { - ObserverReadingEnvelopeBudget, - ObserverReadingEnvelopeFields, -} from './src/domain/services/query/ObserverReadingEnvelope.ts'; -export { - openAperture, - verifyZKWormhole, -} from './src/domain/services/wormhole/ZKWormholeService.ts'; -export { default as Optic } from './src/domain/services/optic/Optic.ts'; -export type { - OpticContextValue, - OpticFields, - OpticPostureFields, -} from './src/domain/services/optic/Optic.ts'; -export { default as OpticAperturePosture } from './src/domain/services/optic/OpticAperturePosture.ts'; -export type { OpticAperturePostureValue } from './src/domain/services/optic/OpticAperturePosture.ts'; -export { default as OpticBasisPosture } from './src/domain/services/optic/OpticBasisPosture.ts'; -export type { OpticBasisPostureValue } from './src/domain/services/optic/OpticBasisPosture.ts'; -export { default as OpticCoordinatePosture } from './src/domain/services/optic/OpticCoordinatePosture.ts'; -export type { OpticCoordinatePostureValue } from './src/domain/services/optic/OpticCoordinatePosture.ts'; -export { default as OpticSupportRule } from './src/domain/services/optic/OpticSupportRule.ts'; -export type { OpticSupportRuleValue } from './src/domain/services/optic/OpticSupportRule.ts'; -export { default as ProjectionHandle } from './src/domain/services/ProjectionHandle.ts'; -export { default as RejectedZKWormhole } from './src/domain/services/wormhole/RejectedZKWormhole.ts'; -export { default as StrandSelector } from './src/domain/types/StrandSelector.ts'; -export { default as SupportFragmentPlan } from './src/domain/services/query/SupportFragmentPlan.ts'; -export type { - SupportFragmentMaterializationPosture, - SupportFragmentPlanFields, -} from './src/domain/services/query/SupportFragmentPlan.ts'; -export { default as VerifiedZKWormhole } from './src/domain/services/wormhole/VerifiedZKWormhole.ts'; -export { default as WarpWorldlineCoordinate } from './src/domain/WarpWorldlineCoordinate.ts'; +export { default as Coordinate } from './src/domain/WarpWorldlineCoordinate.ts'; +export { default as Optic } from './src/domain/services/optic/WorldlineOptic.ts'; export type { WarpWorldlineCoordinateFrontierEntry } from './src/domain/WarpWorldlineCoordinate.ts'; -export { default as WarpWorldlineOpticBasis } from './src/domain/WarpWorldlineOpticBasis.ts'; -export { default as WorldlineSelector } from './src/domain/types/WorldlineSelector.ts'; -export { default as ZKWormholeEdge } from './src/domain/services/wormhole/ZKWormholeEdge.ts'; -export type { ZKWormholeEdgeFields } from './src/domain/services/wormhole/ZKWormholeEdge.ts'; +export type { NeighborhoodOpticReadOptions } from './src/domain/services/optic/NeighborhoodOptic.ts'; +export type { + NeighborhoodOpticCompleteness, + NeighborhoodOpticEdge, + NeighborhoodOpticReadDirection, +} from './src/domain/services/optic/NeighborhoodOpticReadResult.ts'; export type { - ApertureOpeningVerificationResult, - ZKWormholeVerificationResult, -} from './src/domain/services/wormhole/ZKWormholeVerificationResult.ts'; -export { default as ZKWormholeProofVerifierPort } from './src/ports/ZKWormholeProofVerifierPort.ts'; -export type { Aperture, ObserverConfig } from './src/domain/types/Aperture.ts'; -export * from './src/continuumExports.ts'; + default as Witness, + ReadIdentityFrontierEntry, + ReadIdentityIndexShard, + ReadIdentityOptions, + ReadIdentityTailWitness, +} from './src/domain/services/optic/ReadIdentity.ts'; diff --git a/diagnostics.ts b/diagnostics.ts index c9369d21e..b93bb9b35 100644 --- a/diagnostics.ts +++ b/diagnostics.ts @@ -1,47 +1,44 @@ /** - * Operator diagnostics and migration inspection surface. - * - * Diagnostic exports stay separate from the application root so public - * first-use code does not learn substrate nouns before it needs them. + * Operator-facing inspection helpers for the v19 application API. */ -export { default as BisectService } from './src/domain/services/BisectService.ts'; -export { default as CommitDagTraversalService } from './src/domain/services/dag/CommitDagTraversalService.ts'; -export { default as ContentAttachmentProjection } from './src/domain/services/ContentAttachmentProjection.ts'; -export { - exportCoordinateComparisonFact, - exportCoordinateTransferPlanFact, -} from './src/domain/services/CoordinateFactExport.ts'; -export { default as GraphDiff } from './src/domain/services/comparison/GraphDiff.ts'; -export type { GraphDiffFields } from './src/domain/services/comparison/GraphDiff.ts'; -export type { GraphDiffOptions } from './src/domain/capabilities/ComparisonCapability.ts'; -export { default as GraphOpAlgebraProjection } from './src/domain/services/GraphOpAlgebraProjection.ts'; -export { default as QueryBuilder } from './src/domain/services/query/QueryBuilder.ts'; -export { default as TtdMergeBranch } from './src/domain/services/merge/TtdMergeBranch.ts'; -export type { TtdMergeBranchFields } from './src/domain/services/merge/TtdMergeBranch.ts'; -export { default as TtdMergeFootprint } from './src/domain/services/merge/TtdMergeFootprint.ts'; -export type { TtdMergeFootprintFields } from './src/domain/services/merge/TtdMergeFootprint.ts'; -export { default as TtdMergeInspection } from './src/domain/services/merge/TtdMergeInspection.ts'; -export type { TtdMergeInspectionFields } from './src/domain/services/merge/TtdMergeInspection.ts'; -export type { TtdMergeInspectionDomain } from './src/domain/services/merge/TtdMergeInspectionDomain.ts'; -export { default as TtdMergeInspector } from './src/domain/services/merge/TtdMergeInspector.ts'; -export type { - TtdMergeObjectBranchInput, - TtdMergeObjectInspectionInput, -} from './src/domain/services/merge/TtdMergeInspector.ts'; -export type { TtdMergeLoweringSurface } from './src/domain/services/merge/TtdMergeLoweringSurface.ts'; -export { default as TtdMergeLoweringWitness } from './src/domain/services/merge/TtdMergeLoweringWitness.ts'; -export type { TtdMergeLoweringWitnessFields } from './src/domain/services/merge/TtdMergeLoweringWitness.ts'; -export { default as TtdMergeObstructionWitness } from './src/domain/services/merge/TtdMergeObstructionWitness.ts'; -export type { TtdMergeObstructionWitnessFields } from './src/domain/services/merge/TtdMergeObstructionWitness.ts'; -export { default as TtdMergePolicyRequirement } from './src/domain/services/merge/TtdMergePolicyRequirement.ts'; -export type { TtdMergePolicyRequirementFields } from './src/domain/services/merge/TtdMergePolicyRequirement.ts'; -export { - normalizeVisibleStateScope, - nodeIdInVisibleStateScope, - scopeMaterializedState, -} from './src/domain/services/VisibleStateScope.ts'; -export type { - VisibleStateScope, - VisibleStateScopePrefixFilter, -} from './src/domain/services/VisibleStateScope.ts'; +import type { Receipt } from './src/domain/api/Receipt.ts'; + +export type ReceiptInspection = { + readonly operation: Receipt['operation']; + readonly outcome: Receipt['outcome']; + readonly timeline: string; + readonly writer: string; + readonly reason: string | undefined; + readonly evidence: 'present' | 'absent'; + readonly objectIds: readonly string[]; +}; + +export function inspectReceipt(receipt: Receipt): ReceiptInspection { + const objectIds = receiptObjectIds(receipt); + return Object.freeze({ + operation: receipt.operation, + outcome: receipt.outcome, + timeline: receipt.timeline, + writer: receipt.writer, + reason: receipt.reason, + evidence: receipt.operation === 'read' && receipt.evidence !== undefined ? 'present' : 'absent', + objectIds: Object.freeze(objectIds), + }); +} + +function receiptObjectIds(receipt: Receipt): string[] { + if (receipt.operation === 'write') { + return receipt.patchSha === undefined ? [] : [receipt.patchSha]; + } + if (receipt.operation === 'join') { + return [...receipt.patchShas]; + } + if (receipt.evidence === undefined) { + return []; + } + return [ + receipt.evidence.checkpointSha, + ...receipt.evidence.tailWitnesses.map((witness) => witness.sha), + ]; +} diff --git a/docs/migrations/v19/README.md b/docs/migrations/v19/README.md index 6a033e3c0..6840bda14 100644 --- a/docs/migrations/v19/README.md +++ b/docs/migrations/v19/README.md @@ -1,7 +1,7 @@ # v19 Public API Migration Plan This migration plan covers consumers moving from v18 or earlier public -surfaces to the planned v19 public API. It is intentionally explicit because +surfaces to the v19 public API. It is intentionally explicit because v19 is a major-version boundary: root exports become small and application oriented, while graph-shaped compatibility surfaces are removed. @@ -36,12 +36,12 @@ Diagnostics and expert WARP terms move to explicit subpaths: ## Subpath Policy -| Subpath | Contract | -| ------------- | ------------------------------------------ | -| Root | first-use public API; no graph substrate | -| `storage` | supported persistence adapters | -| `advanced` | stable formal WARP concepts for expert use | -| `diagnostics` | operator and inspection tools | +| Subpath | Contract | +| ------------- | --------------------------------------------------- | +| Root | first-use public API; no graph substrate | +| `storage` | supported persistence adapters | +| `advanced` | bounded `Coordinate`, `Optic`, and `Witness` access | +| `diagnostics` | receipt inspection | The former `browser` and `legacy` subpaths do not exist in v19. Consumers must remove those imports before upgrading. @@ -121,8 +121,8 @@ switch (receipt.outcome) { } ``` -`commit((patch) => ...)` is deprecated legacy API. The first-use write surface -is `timeline.write(intent)`. +`commit((patch) => ...)` is removed from the package contract. The write +surface is `timeline.write(intent)`. ## Read Migration @@ -157,7 +157,8 @@ result.receipt; ``` `reading` is the public request. `Optic` remains the advanced execution and -proof shape. +proof shape. A missing bounded basis returns an `obstructed` receipt with +repair hints; it does not trigger whole-state materialization. ## Time Travel Migration @@ -227,30 +228,30 @@ and joins first. ## Symbol Disposition Table -| v18 or earlier symbol | v19 path | Notes | -| ------------------------ | ---------------------------------- | ------------------------------------------------- | -| `openWarpWorldline()` | root `openWarp().timeline(name)` | preferred application opener | -| `WarpWorldline` | root `Timeline` | public handle rename | -| `GitGraphAdapter` | `storage` `GitStorageAdapter` | graph name removed | -| `InMemoryGraphAdapter` | `storage` `MemoryStorageAdapter` | graph name removed | -| `GraphPersistencePort` | root `WarpStorage` for app options | old graph-shaped port removed from public API | -| `commit((patch) => ...)` | `timeline.write(intent.*)` | receipt-returning | -| `PatchBuilder` | removed | replace with intent builders | -| `PatchSession` | removed | replace with receipt-returning writes | -| `createNodeAdd()` | removed | use intent builders | -| `createEdgeAdd()` | removed | use intent builders | -| `createPropSet()` | removed | use intent builders | -| `openWarpGraph()` | removed | replace diagnostics with explicit diagnostic APIs | -| `WarpApp` | removed | no root default export in v19 | -| `WarpCore` | removed | replace diagnostics with explicit diagnostic APIs | -| `GraphNode` | removed | no public export | -| `GraphDiff` | `diagnostics` | operator-facing comparison | -| `Optic` | `advanced` | readings are root | -| `Coordinate` | `advanced` or receipt fields | ticks are root | -| `Observer` | `advanced` | readings are first-use root | -| `Strand` | `advanced` | drafts are first-use root | -| `Braid` | `advanced` | joins are first-use root | -| Continuum evidence nouns | `advanced` or `diagnostics` | receipt evidence stays root-facing | +| v18 or earlier symbol | v19 path | Notes | +| ------------------------ | ------------------------------------- | ------------------------------------------------- | +| `openWarpWorldline()` | root `openWarp().timeline(name)` | preferred application opener | +| `WarpWorldline` | root `Timeline` | public handle rename | +| `GitGraphAdapter` | `storage` `GitStorageAdapter` | graph name removed | +| `InMemoryGraphAdapter` | `storage` `MemoryStorageAdapter` | graph name removed | +| `GraphPersistencePort` | root `StorageAdapter` for app options | old graph-shaped port removed from public API | +| `commit((patch) => ...)` | `timeline.write(intent.*)` | receipt-returning | +| `PatchBuilder` | removed | replace with intent builders | +| `PatchSession` | removed | replace with receipt-returning writes | +| `createNodeAdd()` | removed | use intent builders | +| `createEdgeAdd()` | removed | use intent builders | +| `createPropSet()` | removed | use intent builders | +| `openWarpGraph()` | removed | replace diagnostics with explicit diagnostic APIs | +| `WarpApp` | removed | no root default export in v19 | +| `WarpCore` | removed | replace diagnostics with explicit diagnostic APIs | +| `GraphNode` | removed | no public export | +| `GraphDiff` | removed | no public-handle comparison API ships in v19 | +| `Optic` | `advanced` | readings are root | +| `Coordinate` | `advanced` or receipt fields | ticks are root | +| `Observer` | removed | readings are first-use root | +| `Strand` | removed | drafts are first-use root | +| `Braid` | removed | joins are first-use root | +| Continuum evidence nouns | removed | receipt evidence stays root-facing | ## Receipt Outcome Migration @@ -302,8 +303,8 @@ classes or operation fields, not in `receipt.outcome`. 3. Convert `commit((patch) => ...)` calls to `timeline.write(intent.*)` calls. 4. Rewrite direct live/query/optic reads as `timeline.read(reading.*)` calls. 5. Move `seek()`/coordinate-first call sites to `tick()` and `at(tick)`. -6. Move diagnostics, materialization, and graph diff code to explicit - `diagnostics` APIs. +6. Replace direct diagnostics with `inspectReceipt()`; keep graph diff and + materialization integrations internal until they accept public handles. 7. Remove remaining graph-shaped package imports. ## Compatibility Window diff --git a/docs/topics/api/README.md b/docs/topics/api/README.md index 14dfb3141..cde9bbcf7 100644 --- a/docs/topics/api/README.md +++ b/docs/topics/api/README.md @@ -79,14 +79,10 @@ tool: Optic Coordinate Witness -Worldline -Strand -Braid -Hologram ``` -Those terms should live in advanced, diagnostics, or internal surfaces, not in -the first-use root API. +Worldline, Strand, Braid, Hologram, Observer, and Continuum families remain +internal. They are not moved to `advanced` merely because they left root. ## Root Shape @@ -139,8 +135,12 @@ const role = await timeline.read( }) ); -console.log(role.value); -console.log(role.receipt); +if (role.receipt.outcome === 'resolved') { + console.log(role.value); + console.log(role.receipt.evidence); +} else { + console.log(role.receipt.reason, role.receipt.repairHints); +} ``` The read path should return a result object, not a naked value. Provenance is @@ -384,12 +384,12 @@ Use explicit subpaths: The boundaries mean different things: -| Surface | Meaning | -| ------------- | ------------------------------------------------------- | -| Root | first-use product API | -| `storage` | supported persistence adapters | -| `advanced` | formal WARP nouns for expert use | -| `diagnostics` | inspection, materialization, replay, and operator tools | +| Surface | Meaning | +| ------------- | --------------------------------------------------- | +| Root | first-use product API | +| `storage` | supported persistence adapters | +| `advanced` | bounded `Coordinate`, `Optic`, and `Witness` access | +| `diagnostics` | receipt inspection | Do not turn `advanced` into a junk drawer. Symbols that exist only for removed graph-first consumers are not part of the v19 package boundary. @@ -404,11 +404,11 @@ Each old root symbol needs one explicit disposition: | `GitGraphAdapter` | `GitStorageAdapter` from `storage` | | `InMemoryGraphAdapter` | `MemoryStorageAdapter` from `storage` | | `commit((patch) => ...)` | `timeline.write(intent.*)` | -| `coordinate()` | `tick()` publicly, `Coordinate` in advanced/evidence | +| `coordinate()` | `tick()` publicly; formal access remains advanced | | `optic()` | `timeline.read(reading.*)` or `advanced` | | `openWarpGraph()` | removed; replace diagnostics with explicit diagnostic APIs | | `PatchBuilder` | removed; use intent builders | -| `GraphDiff` | `diagnostics` | +| `GraphDiff` | removed until a public-handle comparison API exists | | graph op creators | removed; use intent builders | The compatibility story should be honest: diff --git a/docs/topics/getting-started.md b/docs/topics/getting-started.md index 72da0efc8..d3896b3ac 100644 --- a/docs/topics/getting-started.md +++ b/docs/topics/getting-started.md @@ -48,8 +48,11 @@ const receipt = await audit.write( }) ); -console.log(receipt.outcome); -console.log(receipt.patchSha); +if (receipt.outcome === 'accepted') { + console.log(receipt.patchSha); +} else { + console.error(receipt.reason); +} ``` Every call writes one intent and returns a `WriteReceipt`. Treat @@ -79,7 +82,46 @@ console.log(exists.value, exists.receipt); ``` Readings ask bounded questions. The receipt records how the runtime supported -the answer. +the answer. A resolved receipt carries checkpoint-tail evidence. If no bounded +basis exists, `read()` returns an `obstructed` receipt with repair hints instead +of materializing the whole timeline. + +Create or repair the operator-owned basis before retrying: + +```bash +git warp checkpoint create --repo ./security-repo --graph security-audit +git warp doctor --repo ./security-repo --repair-state-cache +``` + +Read a bounded neighborhood with the same result-and-receipt shape: + +```typescript +const related = await audit.read( + reading.neighborhood({ + subject: 'finding:oauth-state-mismatch', + direction: 'out', + limit: 50, + }) +); +``` + +Use `readValue()` only when unresolved readings should throw instead of +participating in receipt-based control flow. + +## Read At A Tick + +```typescript +const tick = await audit.tick(); +const historical = await audit.at(tick).read( + reading.property({ + subject: 'finding:oauth-state-mismatch', + key: 'severity', + }) +); +``` + +`TimelineView` is read-only. Formal coordinate and optic access lives under the +`advanced` subpath. ## Work In A Draft diff --git a/docs/topics/querying.md b/docs/topics/querying.md index d9729cd80..20acedaeb 100644 --- a/docs/topics/querying.md +++ b/docs/topics/querying.md @@ -6,13 +6,14 @@ earlier releases. ## Choose The Surface -| Need | Surface | -| ---------------------------------------------------- | ------------------------------------ | -| Read one known property | `reading.property(...)` | -| Check whether one known node exists | `reading.node.exists(...)` | -| Inspect comparison or visible-state diagnostics | `diagnostics` subpath | -| Work with formal optics, observers, or support plans | `advanced` subpath | -| Run operator-oriented graph commands | `git warp query`, `path`, or `optic` | +| Need | Surface | +| ------------------------------------- | ------------------------------------ | +| Read one known property | `reading.property(...)` | +| Check whether one known node exists | `reading.node.exists(...)` | +| Read one bounded adjacency page | `reading.neighborhood(...)` | +| Inspect a receipt | `diagnostics` subpath | +| Capture a formal coordinate and optic | `advanced` subpath | +| Run operator-oriented graph commands | `git warp query`, `path`, or `optic` | The `advanced` and `diagnostics` subpaths expose expert components, not a second graph-first application facade. @@ -53,10 +54,32 @@ console.log(task.value); console.log(task.receipt.outcome); ``` +## Read A Neighborhood + +```typescript +const dependencies = await team.read( + reading.neighborhood({ + subject: 'task:auth', + direction: 'out', + labels: ['dependsOn'], + limit: 100, + }) +); + +console.log(dependencies.value); +console.log(dependencies.receipt.evidence); +``` + +When the required checkpoint-tail basis is absent, a reading returns `null` +with an `obstructed` receipt and `repairHints`. It never falls back to broad +materialization. `readValue()` converts that unresolved result into an +exception for callers that explicitly prefer convenience over receipt control +flow. + ## Unsupported Root Queries The root does not currently provide wildcard matching, arbitrary traversal, -aggregation, historical coordinate selection, or draft reads. Do not recover +aggregation, arbitrary traversal, or draft reads. Do not recover those APIs by importing internal modules. Use the operator CLI or an explicit expert surface where one exists, and keep application code on readings. @@ -65,15 +88,15 @@ as a bounded read. ## Diagnostics -`@git-stunts/git-warp/diagnostics` exports operator-oriented components such as -`GraphDiff`, `QueryBuilder`, `TtdMergeInspector`, and visible-state scope -helpers. Callers are responsible for supplying their required runtime context. +`@git-stunts/git-warp/diagnostics` exports `inspectReceipt()`. It accepts the +same write, read, and join receipts returned by the root API and does not +require an internal runtime host. ## Advanced Read Machinery -`@git-stunts/git-warp/advanced` exports formal concepts such as `Optic`, -`Observer`, `BoundedSupportRule`, and `CausalIndexPlan`. These are useful for -proof-oriented or runtime-integration work; they are not first-use query verbs. +`@git-stunts/git-warp/advanced` exports `Coordinate`, executable `Optic`, and +the type-only `Witness`. Capture a coordinate with `timeline.coordinate()`; +ordinary application reads should continue to use `reading.*`. ## Removed Compatibility Surface diff --git a/docs/topics/reference.md b/docs/topics/reference.md index 95b37d91b..9c1eb495d 100644 --- a/docs/topics/reference.md +++ b/docs/topics/reference.md @@ -14,300 +14,145 @@ public API export, CLI command, package entrypoint, or public error class. | npm export | `./storage` | `types=./dist/storage.d.ts; import=./dist/storage.js; default=./dist/storage.js` | `package.json#L32` | | npm export | `./advanced` | `types=./dist/advanced.d.ts; import=./dist/advanced.js; default=./dist/advanced.js` | `package.json#L37` | | npm export | `./diagnostics` | `types=./dist/diagnostics.d.ts; import=./dist/diagnostics.js; default=./dist/diagnostics.js` | `package.json#L42` | -| npm export | `./sha1sync` | `types=./dist/src/infrastructure/adapters/sha1sync.d.ts; import=./dist/src/infrastructure/adapters/sha1sync.js; default=./dist/src/infrastructure/adapters/sha1sync.js` | `package.json#L47` | -| npm export | `./package.json` | `./package.json` | `package.json#L52` | +| npm export | `./package.json` | `./package.json` | `package.json#L47` | | JSR export | `.` | `./index.ts` | `jsr.json#L8` | | JSR export | `./storage` | `./storage.ts` | `jsr.json#L9` | | JSR export | `./advanced` | `./advanced.ts` | `jsr.json#L10` | | JSR export | `./diagnostics` | `./diagnostics.ts` | `jsr.json#L11` | -| JSR export | `./sha1sync` | `./src/infrastructure/adapters/sha1sync.ts` | `jsr.json#L12` | ## Root API export surface First-use product API: `openWarp`, `intent`, `reading`, timelines, and receipts. -### Export modules - -| Module | Kind | Source | -| --- | --- | --- | -| `./src/domain/memory/index.ts` | export * | `index.ts#L58` | - ### Value exports -Source: `index.ts`. Count: 73. +Source: `index.ts`. Count: 3. ```text -AlfredOperationPolicyAdapter @ index.ts#L71 -AuditError @ index.ts#L74 -BunHttpAdapter @ index.ts#L108 -canonicalEmissionJson @ index.ts#L129 -canonicalObservationJson @ index.ts#L130 -CasContentEncryptionPolicy @ index.ts#L65 -checkAborted @ index.ts#L110 -ChunkEffectSink @ index.ts#L140 -ConsoleEffectSink @ index.ts#L139 -ConsoleLogger @ index.ts#L102 -ContinuumArtifactAuthorityError @ index.ts#L75 -createDeliveryObservation @ index.ts#L127 -createEffectEmission @ index.ts#L123 -createExternalizationPolicy @ index.ts#L133 -createTickReceipt @ index.ts#L114 -createTimeoutSignal @ index.ts#L110 -CryptoPort @ index.ts#L104 -DELIVERY_MODES @ index.ts#L124 -DELIVERY_OUTCOMES @ index.ts#L125 -DenoHttpAdapter @ index.ts#L109 -DraftTimeline @ index.ts#L18 -EffectPipeline @ index.ts#L121 -EffectSinkPort @ index.ts#L119 -EncryptionError @ index.ts#L76 -ForkError @ index.ts#L77 -HealthCheckService @ index.ts#L97 -HealthStatus @ index.ts#L98 -HttpServerPort @ index.ts#L105 -IndexError @ index.ts#L78 -INSPECT_LENS @ index.ts#L136 -intent @ index.ts#L21 -Intent @ index.ts#L22 -JoinReceipt @ index.ts#L23 -JoinResult @ index.ts#L24 -LIVE_LENS @ index.ts#L134 -LoggerPort @ index.ts#L100 -LogLevel @ index.ts#L102 -MemoryBudgetError @ index.ts#L79 -MultiplexSink @ index.ts#L120 -NodeCryptoAdapter @ index.ts#L106 -NoOpEffectSink @ index.ts#L138 -NoOpLogger @ index.ts#L101 -NoopOperationPolicyAdapter @ index.ts#L72 +intent @ index.ts#L18 openWarp @ index.ts#L17 -OperationAbortedError @ index.ts#L80 -OperationPolicyExhaustedError @ index.ts#L81 -OperationPolicyPort @ index.ts#L60 -OperationPolicyTimeoutError @ index.ts#L82 -PatchError @ index.ts#L83 -QueryError @ index.ts#L84 -reading @ index.ts#L25 -Reading @ index.ts#L26 -ReadingResult @ index.ts#L27 -ReadReceipt @ index.ts#L28 -REPLAY_LENS @ index.ts#L135 -SchemaUnsupportedError @ index.ts#L85 -ShardCorruptionError @ index.ts#L86 -ShardLoadError @ index.ts#L87 -ShardValidationError @ index.ts#L88 -StorageError @ index.ts#L89 -StrandError @ index.ts#L90 -SyncError @ index.ts#L91 -SyncSecret @ index.ts#L111 -TICK_RECEIPT_OP_TYPES @ index.ts#L115 -TICK_RECEIPT_RESULT_TYPES @ index.ts#L116 -tickReceiptCanonicalJson @ index.ts#L118 -Timeline @ index.ts#L20 -TraversalError @ index.ts#L92 -Warp @ index.ts#L19 -WebCryptoAdapter @ index.ts#L107 -WormholeError @ index.ts#L93 -WriteReceipt @ index.ts#L29 -WriterError @ index.ts#L103 +reading @ index.ts#L19 ``` ### Type exports -Source: `index.ts`. Count: 32. +Source: `index.ts`. Count: 43. ```text -CasContentEncryptionDiagnostics @ index.ts#L67 -CasContentEncryptionScheme @ index.ts#L68 -CasResolvedVaultKeyOptions @ index.ts#L69 -EdgeIntentFields @ index.ts#L32 -EdgePropertyIntentFields @ index.ts#L33 -IntentBuilders @ index.ts#L39 -IntentDescriptor @ index.ts#L34 -IntentKind @ index.ts#L35 +DraftTimeline @ index.ts#L20 +EdgeIntentFields @ index.ts#L34 +Intent @ index.ts#L25 +IntentBuilders @ index.ts#L40 +IntentDescriptor @ index.ts#L35 +IntentKind @ index.ts#L36 JoinMode @ index.ts#L41 -JoinOptions @ index.ts#L46 -JoinPolicy @ index.ts#L46 -JoinReceiptOptions @ index.ts#L42 -JoinReceiptOutcome @ index.ts#L43 -JoinResultOptions @ index.ts#L45 -NodeIntentFields @ index.ts#L36 -NodeReadingFields @ index.ts#L48 -OpenWarpOptions @ index.ts#L30 -OperationPolicyExecuteOptions @ index.ts#L62 -OperationRetryDecision @ index.ts#L63 -PropertyIntentFields @ index.ts#L37 -PropertyReadingFields @ index.ts#L49 -ReadingBuilders @ index.ts#L53 -ReadingDescriptor @ index.ts#L50 -ReadingKind @ index.ts#L51 -ReadingResultOptions @ index.ts#L54 -ReadingValue @ index.ts#L54 +JoinOptions @ index.ts#L43 +JoinOutcome @ index.ts#L57 +JoinPolicy @ index.ts#L43 +JoinReceipt @ index.ts#L26 +JoinReceiptOptions @ index.ts#L41 +JoinResult @ index.ts#L27 +JoinResultOptions @ index.ts#L42 +NeighborhoodReadingFields @ index.ts#L45 +NodeIntentFields @ index.ts#L37 +NodeReadingFields @ index.ts#L46 +OpenWarpOptions @ index.ts#L32 +PropertyIntentFields @ index.ts#L38 +PropertyReadingFields @ index.ts#L47 +ReadEvidence @ index.ts#L55 +Reading @ index.ts#L28 +ReadingBuilders @ index.ts#L52 +ReadingDescriptor @ index.ts#L49 +ReadingDirection @ index.ts#L48 +ReadingKind @ index.ts#L50 +ReadingResult @ index.ts#L29 +ReadingResultOptions @ index.ts#L53 +ReadingValue @ index.ts#L53 +ReadOutcome @ index.ts#L58 +ReadReceipt @ index.ts#L30 ReadReceiptOptions @ index.ts#L55 -ReadReceiptOutcome @ index.ts#L55 -ReceiptOutcome @ index.ts#L56 -SyncRateLimitConfig @ index.ts#L112 -WarpStorage @ index.ts#L30 -WriteReceiptOptions @ index.ts#L56 +Receipt @ index.ts#L54 +ReceiptOutcome @ index.ts#L59 +RepairHint @ index.ts#L62 +StorageAdapter @ index.ts#L32 +Tick @ index.ts#L23 +Timeline @ index.ts#L22 +TimelineView @ index.ts#L24 +Warp @ index.ts#L21 +WriteOutcome @ index.ts#L60 +WriteReceipt @ index.ts#L31 +WriteReceiptOptions @ index.ts#L63 ``` ## Storage export surface -Supported persistence and crypto adapters for first-use applications. +Git-backed and in-memory adapters for first-use applications. ### Value exports -Source: `storage.ts`. Count: 5. +Source: `storage.ts`. Count: 2. ```text -CasContentEncryptionPolicy @ storage.ts#L13 -GitStorageAdapter @ storage.ts#L9 -MemoryStorageAdapter @ storage.ts#L10 -NodeCryptoAdapter @ storage.ts#L11 -WebCryptoAdapter @ storage.ts#L12 +GitStorageAdapter @ storage.ts#L15 +MemoryStorageAdapter @ storage.ts#L21 ``` ### Type exports -Source: `storage.ts`. Count: 6. +Source: `storage.ts`. Count: 1. ```text -CasContentEncryptionDiagnostics @ storage.ts#L15 -CasContentEncryptionScheme @ storage.ts#L16 -CasResolvedVaultKeyOptions @ storage.ts#L17 -CollectableStream @ storage.ts#L20 -GitError @ storage.ts#L21 -GitPlumbing @ storage.ts#L22 +GitStorageAdapterOptions @ storage.ts#L13 ``` ## Advanced export surface -Formal WARP and Continuum concepts for expert use; not first-use root API. - -### Export modules - -| Module | Kind | Source | -| --- | --- | --- | -| `./src/continuumExports.ts` | export * | `advanced.ts#L80` | +Bounded Coordinate, Optic, and Witness concepts for expert use. ### Value exports -Source: `advanced.ts`. Count: 31. +Source: `advanced.ts`. Count: 2. ```text -BoundedSupportRule @ advanced.ts#L9 -CausalIndexPlan @ advanced.ts#L16 -composeWormholes @ advanced.ts#L23 -createWormhole @ advanced.ts#L24 -deserializeWormhole @ advanced.ts#L25 -LiveSelector @ advanced.ts#L29 -Observer @ advanced.ts#L30 -ObserverAccumulation @ advanced.ts#L31 -ObserverBasis @ advanced.ts#L32 -ObserverEmission @ advanced.ts#L33 -ObserverPlan @ advanced.ts#L34 -ObserverReadingEnvelope @ advanced.ts#L36 -openAperture @ advanced.ts#L42 -Optic @ advanced.ts#L45 -OpticAperturePosture @ advanced.ts#L51 -OpticBasisPosture @ advanced.ts#L53 -OpticCoordinatePosture @ advanced.ts#L55 -OpticSupportRule @ advanced.ts#L57 -ProjectionHandle @ advanced.ts#L59 -RejectedZKWormhole @ advanced.ts#L60 -replayWormhole @ advanced.ts#L26 -serializeWormhole @ advanced.ts#L27 -StrandSelector @ advanced.ts#L61 -SupportFragmentPlan @ advanced.ts#L62 -VerifiedZKWormhole @ advanced.ts#L67 -verifyZKWormhole @ advanced.ts#L43 -WarpWorldlineCoordinate @ advanced.ts#L68 -WarpWorldlineOpticBasis @ advanced.ts#L70 -WorldlineSelector @ advanced.ts#L71 -ZKWormholeEdge @ advanced.ts#L72 -ZKWormholeProofVerifierPort @ advanced.ts#L78 +Coordinate @ advanced.ts#L9 +Optic @ advanced.ts#L10 ``` ### Type exports -Source: `advanced.ts`. Count: 25. +Source: `advanced.ts`. Count: 10. ```text -Aperture @ advanced.ts#L79 -ApertureOpeningVerificationResult @ advanced.ts#L75 -BoundedSupportDirection @ advanced.ts#L11 -BoundedSupportKind @ advanced.ts#L12 -BoundedSupportRuleFields @ advanced.ts#L13 -BoundedSupportSurface @ advanced.ts#L14 -CausalIndexFamily @ advanced.ts#L18 -CausalIndexPlanFields @ advanced.ts#L19 -CausalIndexPlanPosture @ advanced.ts#L20 -ObserverConfig @ advanced.ts#L79 -ObserverPlanFields @ advanced.ts#L35 -ObserverReadingEnvelopeBudget @ advanced.ts#L38 -ObserverReadingEnvelopeFields @ advanced.ts#L39 -OpticAperturePostureValue @ advanced.ts#L52 -OpticBasisPostureValue @ advanced.ts#L54 -OpticContextValue @ advanced.ts#L47 -OpticCoordinatePostureValue @ advanced.ts#L56 -OpticFields @ advanced.ts#L48 -OpticPostureFields @ advanced.ts#L49 -OpticSupportRuleValue @ advanced.ts#L58 -SupportFragmentMaterializationPosture @ advanced.ts#L64 -SupportFragmentPlanFields @ advanced.ts#L65 -WarpWorldlineCoordinateFrontierEntry @ advanced.ts#L69 -ZKWormholeEdgeFields @ advanced.ts#L73 -ZKWormholeVerificationResult @ advanced.ts#L76 +NeighborhoodOpticCompleteness @ advanced.ts#L14 +NeighborhoodOpticEdge @ advanced.ts#L15 +NeighborhoodOpticReadDirection @ advanced.ts#L16 +NeighborhoodOpticReadOptions @ advanced.ts#L12 +ReadIdentityFrontierEntry @ advanced.ts#L20 +ReadIdentityIndexShard @ advanced.ts#L21 +ReadIdentityOptions @ advanced.ts#L22 +ReadIdentityTailWitness @ advanced.ts#L23 +WarpWorldlineCoordinateFrontierEntry @ advanced.ts#L11 +Witness @ advanced.ts#L19 ``` ## Diagnostics export surface -Operator, inspection, comparison, and replay tools. +Operator inspection helpers that consume public receipt handles. ### Value exports -Source: `diagnostics.ts`. Count: 18. +Source: `diagnostics.ts`. Count: 1. ```text -BisectService @ diagnostics.ts#L8 -CommitDagTraversalService @ diagnostics.ts#L9 -ContentAttachmentProjection @ diagnostics.ts#L10 -exportCoordinateComparisonFact @ diagnostics.ts#L12 -exportCoordinateTransferPlanFact @ diagnostics.ts#L13 -GraphDiff @ diagnostics.ts#L15 -GraphOpAlgebraProjection @ diagnostics.ts#L18 -nodeIdInVisibleStateScope @ diagnostics.ts#L41 -normalizeVisibleStateScope @ diagnostics.ts#L40 -QueryBuilder @ diagnostics.ts#L19 -scopeMaterializedState @ diagnostics.ts#L42 -TtdMergeBranch @ diagnostics.ts#L20 -TtdMergeFootprint @ diagnostics.ts#L22 -TtdMergeInspection @ diagnostics.ts#L24 -TtdMergeInspector @ diagnostics.ts#L27 -TtdMergeLoweringWitness @ diagnostics.ts#L33 -TtdMergeObstructionWitness @ diagnostics.ts#L35 -TtdMergePolicyRequirement @ diagnostics.ts#L37 +inspectReceipt @ diagnostics.ts#L17 ``` ### Type exports -Source: `diagnostics.ts`. Count: 14. +Source: `diagnostics.ts`. Count: 1. ```text -GraphDiffFields @ diagnostics.ts#L16 -GraphDiffOptions @ diagnostics.ts#L17 -TtdMergeBranchFields @ diagnostics.ts#L21 -TtdMergeFootprintFields @ diagnostics.ts#L23 -TtdMergeInspectionDomain @ diagnostics.ts#L26 -TtdMergeInspectionFields @ diagnostics.ts#L25 -TtdMergeLoweringSurface @ diagnostics.ts#L32 -TtdMergeLoweringWitnessFields @ diagnostics.ts#L34 -TtdMergeObjectBranchInput @ diagnostics.ts#L29 -TtdMergeObjectInspectionInput @ diagnostics.ts#L30 -TtdMergeObstructionWitnessFields @ diagnostics.ts#L36 -TtdMergePolicyRequirementFields @ diagnostics.ts#L38 -VisibleStateScope @ diagnostics.ts#L45 -VisibleStateScopePrefixFilter @ diagnostics.ts#L46 +ReceiptInspection @ diagnostics.ts#L7 ``` ## CLI command registry @@ -348,26 +193,4 @@ Source: `bin/warp-graph.ts#L129`. ## Public error classes -| Class | Module | Source | -| --- | --- | --- | -| `AuditError` | `./AuditError.ts` | `src/domain/errors/index.ts#L3` | -| `ContinuumArtifactAuthorityError` | `./ContinuumArtifactAuthorityError.ts` | `src/domain/errors/index.ts#L4` | -| `EncryptionError` | `./EncryptionError.ts` | `src/domain/errors/index.ts#L5` | -| `ForkError` | `./ForkError.ts` | `src/domain/errors/index.ts#L6` | -| `IndexError` | `./IndexError.ts` | `src/domain/errors/index.ts#L7` | -| `MemoryBudgetError` | `./MemoryBudgetError.ts` | `src/domain/errors/index.ts#L8` | -| `OperationAbortedError` | `./OperationAbortedError.ts` | `src/domain/errors/index.ts#L9` | -| `OperationPolicyExhaustedError` | `./OperationPolicyExhaustedError.ts` | `src/domain/errors/index.ts#L10` | -| `OperationPolicyTimeoutError` | `./OperationPolicyTimeoutError.ts` | `src/domain/errors/index.ts#L11` | -| `PatchError` | `./PatchError.ts` | `src/domain/errors/index.ts#L12` | -| `QueryError` | `./QueryError.ts` | `src/domain/errors/index.ts#L13` | -| `SyncError` | `./SyncError.ts` | `src/domain/errors/index.ts#L14` | -| `ShardCorruptionError` | `./ShardCorruptionError.ts` | `src/domain/errors/index.ts#L15` | -| `ShardIdOverflowError` | `./ShardIdOverflowError.ts` | `src/domain/errors/index.ts#L16` | -| `ShardLoadError` | `./ShardLoadError.ts` | `src/domain/errors/index.ts#L17` | -| `ShardValidationError` | `./ShardValidationError.ts` | `src/domain/errors/index.ts#L18` | -| `StorageError` | `./StorageError.ts` | `src/domain/errors/index.ts#L19` | -| `SchemaUnsupportedError` | `./SchemaUnsupportedError.ts` | `src/domain/errors/index.ts#L20` | -| `TraversalError` | `./TraversalError.ts` | `src/domain/errors/index.ts#L21` | -| `StrandError` | `./StrandError.ts` | `src/domain/errors/index.ts#L22` | -| `WormholeError` | `./WormholeError.ts` | `src/domain/errors/index.ts#L23` | +The v19 package root does not export error constructors. diff --git a/index.ts b/index.ts index 23ef7c34b..68cb53d7c 100644 --- a/index.ts +++ b/index.ts @@ -4,7 +4,7 @@ * Public v19 application boundary. * * Root users should write intents, read timelines, and keep receipts. The - * The graph-first compatibility surface is no longer publicly exported. + * graph-first compatibility surface is no longer publicly exported. * Storage adapters live under `@git-stunts/git-warp/storage`; formal read, * evidence, and support machinery lives under `@git-stunts/git-warp/advanced`; * operator inspection tools live under `@git-stunts/git-warp/diagnostics`. @@ -15,121 +15,49 @@ import { installDefaultRuntimeHostNodePorts } from './src/application/RuntimeHos installDefaultRuntimeHostNodePorts(); export { openWarp } from './src/domain/api/openWarp.ts'; -export { default as DraftTimeline } from './src/domain/api/DraftTimeline.ts'; -export { default as Warp } from './src/domain/api/Warp.ts'; -export { default as Timeline } from './src/domain/api/Timeline.ts'; export { intent } from './src/domain/api/IntentBuilders.ts'; -export { default as Intent } from './src/domain/api/Intent.ts'; -export { default as JoinReceipt } from './src/domain/api/JoinReceipt.ts'; -export { default as JoinResult } from './src/domain/api/JoinResult.ts'; export { reading } from './src/domain/api/ReadingBuilders.ts'; -export { default as Reading } from './src/domain/api/Reading.ts'; -export { default as ReadingResult } from './src/domain/api/ReadingResult.ts'; -export { default as ReadReceipt } from './src/domain/api/ReadReceipt.ts'; -export { default as WriteReceipt } from './src/domain/api/WriteReceipt.ts'; -export type { OpenWarpOptions, WarpStorage } from './src/domain/api/openWarp.ts'; +export type { default as DraftTimeline } from './src/domain/api/DraftTimeline.ts'; +export type { default as Warp } from './src/domain/api/Warp.ts'; +export type { default as Timeline } from './src/domain/api/Timeline.ts'; +export type { default as Tick } from './src/domain/api/Tick.ts'; +export type { default as TimelineView } from './src/domain/api/TimelineView.ts'; +export type { default as Intent } from './src/domain/api/Intent.ts'; +export type { default as JoinReceipt } from './src/domain/api/JoinReceipt.ts'; +export type { default as JoinResult } from './src/domain/api/JoinResult.ts'; +export type { default as Reading } from './src/domain/api/Reading.ts'; +export type { default as ReadingResult } from './src/domain/api/ReadingResult.ts'; +export type { default as ReadReceipt } from './src/domain/api/ReadReceipt.ts'; +export type { default as WriteReceipt } from './src/domain/api/WriteReceipt.ts'; +export type { OpenWarpOptions, StorageAdapter } from './src/domain/api/openWarp.ts'; export type { EdgeIntentFields, - EdgePropertyIntentFields, IntentDescriptor, IntentKind, NodeIntentFields, PropertyIntentFields, } from './src/domain/api/Intent.ts'; export type { IntentBuilders } from './src/domain/api/IntentBuilders.ts'; -export type { JoinMode, JoinReceiptOptions, JoinReceiptOutcome } from './src/domain/api/JoinReceipt.ts'; +export type { JoinMode, JoinReceiptOptions } from './src/domain/api/JoinReceipt.ts'; export type { JoinResultOptions } from './src/domain/api/JoinResult.ts'; export type { JoinOptions, JoinPolicy } from './src/domain/api/Timeline.ts'; export type { + NeighborhoodReadingFields, NodeReadingFields, PropertyReadingFields, + ReadingDirection, ReadingDescriptor, ReadingKind, } from './src/domain/api/Reading.ts'; export type { ReadingBuilders } from './src/domain/api/ReadingBuilders.ts'; export type { ReadingResultOptions, ReadingValue } from './src/domain/api/ReadingResult.ts'; -export type { ReadReceiptOptions, ReadReceiptOutcome } from './src/domain/api/ReadReceipt.ts'; -export type { ReceiptOutcome, WriteReceiptOptions } from './src/domain/api/WriteReceipt.ts'; - -export * from './src/domain/memory/index.ts'; - -export { default as OperationPolicyPort } from './src/ports/OperationPolicyPort.ts'; +export type { Receipt } from './src/domain/api/Receipt.ts'; +export type { ReadEvidence, ReadReceiptOptions } from './src/domain/api/ReadReceipt.ts'; export type { - OperationPolicyExecuteOptions, - OperationRetryDecision, -} from './src/ports/OperationPolicyPort.ts'; -export { default as CasContentEncryptionPolicy } from './src/infrastructure/adapters/CasContentEncryptionPolicy.ts'; -export type { - CasContentEncryptionDiagnostics, - CasContentEncryptionScheme, - CasResolvedVaultKeyOptions, -} from './src/infrastructure/adapters/CasContentEncryptionPolicy.ts'; -export { default as AlfredOperationPolicyAdapter } from './src/infrastructure/adapters/AlfredOperationPolicyAdapter.ts'; -export { default as NoopOperationPolicyAdapter } from './src/infrastructure/adapters/NoopOperationPolicyAdapter.ts'; -export { - AuditError, - ContinuumArtifactAuthorityError, - EncryptionError, - ForkError, - IndexError, - MemoryBudgetError, - OperationAbortedError, - OperationPolicyExhaustedError, - OperationPolicyTimeoutError, - PatchError, - QueryError, - SchemaUnsupportedError, - ShardCorruptionError, - ShardLoadError, - ShardValidationError, - StorageError, - StrandError, - SyncError, - TraversalError, - WormholeError, -} from './src/domain/errors/index.ts'; - -export { default as HealthCheckService, HealthStatus } from './src/domain/services/HealthCheckService.ts'; -export { default as LoggerPort } from './src/ports/LoggerPort.ts'; -export { default as NoOpLogger } from './src/infrastructure/adapters/NoOpLogger.ts'; -export { default as ConsoleLogger, LogLevel } from './src/infrastructure/adapters/ConsoleLogger.ts'; -export { default as WriterError } from './src/domain/errors/WriterError.ts'; -export { default as CryptoPort } from './src/ports/CryptoPort.ts'; -export { default as HttpServerPort } from './src/ports/HttpServerPort.ts'; -export { default as NodeCryptoAdapter } from './src/infrastructure/adapters/NodeCryptoAdapter.ts'; -export { default as WebCryptoAdapter } from './src/infrastructure/adapters/WebCryptoAdapter.ts'; -export { default as BunHttpAdapter } from './src/infrastructure/adapters/BunHttpAdapter.ts'; -export { default as DenoHttpAdapter } from './src/infrastructure/adapters/DenoHttpAdapter.ts'; -export { checkAborted, createTimeoutSignal } from './src/domain/utils/cancellation.ts'; -export { default as SyncSecret } from './src/domain/services/sync/SyncSecret.ts'; -export type { SyncRateLimitConfig } from './src/domain/services/sync/SyncRateLimiter.ts'; -export { - createTickReceipt, - OP_TYPES as TICK_RECEIPT_OP_TYPES, - RESULT_TYPES as TICK_RECEIPT_RESULT_TYPES, -} from './src/domain/types/TickReceipt.ts'; -export { tickReceiptCanonicalJson } from './src/infrastructure/codecs/ReceiptJsonCodec.ts'; -export { default as EffectSinkPort } from './src/ports/EffectSinkPort.ts'; -export { MultiplexSink } from './src/domain/services/MultiplexSink.ts'; -export { EffectPipeline } from './src/domain/services/EffectPipeline.ts'; -export { - createEffectEmission, - DELIVERY_MODES, - DELIVERY_OUTCOMES, -} from './src/domain/types/EffectEmission.ts'; -export { - createDeliveryObservation, -} from './src/domain/types/DeliveryObservation.ts'; -export { - canonicalEmissionJson, - canonicalObservationJson, -} from './src/infrastructure/codecs/ReceiptJsonCodec.ts'; -export { - createExternalizationPolicy, - LIVE_LENS, - REPLAY_LENS, - INSPECT_LENS, -} from './src/domain/types/ExternalizationPolicy.ts'; -export { NoOpEffectSink } from './src/infrastructure/adapters/NoOpEffectSink.ts'; -export { ConsoleEffectSink } from './src/infrastructure/adapters/ConsoleEffectSink.ts'; -export { ChunkEffectSink } from './src/infrastructure/adapters/ChunkEffectSink.ts'; + JoinOutcome, + ReadOutcome, + ReceiptOutcome, + WriteOutcome, +} from './src/domain/api/ReceiptOutcome.ts'; +export type { RepairHint } from './src/domain/api/ReceiptSupport.ts'; +export type { WriteReceiptOptions } from './src/domain/api/WriteReceipt.ts'; diff --git a/jsr.json b/jsr.json index c4fc4d23d..96928c458 100644 --- a/jsr.json +++ b/jsr.json @@ -8,8 +8,7 @@ ".": "./index.ts", "./storage": "./storage.ts", "./advanced": "./advanced.ts", - "./diagnostics": "./diagnostics.ts", - "./sha1sync": "./src/infrastructure/adapters/sha1sync.ts" + "./diagnostics": "./diagnostics.ts" }, "publish": { "include": [ diff --git a/package.json b/package.json index bfa9acb1f..3246cf094 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,6 @@ "import": "./dist/diagnostics.js", "default": "./dist/diagnostics.js" }, - "./sha1sync": { - "types": "./dist/src/infrastructure/adapters/sha1sync.d.ts", - "import": "./dist/src/infrastructure/adapters/sha1sync.js", - "default": "./dist/src/infrastructure/adapters/sha1sync.js" - }, "./package.json": "./package.json" }, "files": [ diff --git a/scripts/check-source-backed-reference.ts b/scripts/check-source-backed-reference.ts index 248542408..43c347cce 100644 --- a/scripts/check-source-backed-reference.ts +++ b/scripts/check-source-backed-reference.ts @@ -1,5 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; import process from 'node:process'; +import { directExportName } from './direct-export-name.ts'; const OUTPUT_PATH = 'docs/topics/reference.md'; class SourceText { readonly path: string; @@ -131,6 +132,8 @@ function captureExports(indexSource: SourceText, kind: 'values' | 'types'): read for (let index = 0; index < indexSource.lines.length; index += 1) { const line = indexSource.line(index); + const directName = directExportName(line, kind); + if (directName !== null) { items.push(inventoryItem(directName, kind, indexSource.ref(index))); continue; } if (!line.trimStart().startsWith(prefix)) { continue; } @@ -234,7 +237,6 @@ function generate(): string { const jsrSource = new SourceText('jsr.json'); const registrySource = new SourceText('bin/cli/commands/registry.ts'); const cliSource = new SourceText('bin/warp-graph.ts'); - const errorSource = new SourceText('src/domain/errors/index.ts'); const rootSource = new SourceText('index.ts'); const storageSource = new SourceText('storage.ts'); const advancedSource = new SourceText('advanced.ts'); @@ -243,7 +245,7 @@ function generate(): string { const packageExports = captureExportEntries(packageSource, 'exports').filter((item) => item.name.startsWith('.')); const jsrExports = captureExportEntries(jsrSource, 'exports').filter((item) => item.name.startsWith('.')); const commands = captureCommands(registrySource); - const errors = captureErrorClasses(errorSource); + const errors = captureErrorClasses(rootSource); return `${[ '# Source-backed reference', @@ -262,11 +264,11 @@ function generate(): string { '', ...exportSurface('Root API export surface', rootSource, 'First-use product API: `openWarp`, `intent`, `reading`, timelines, and receipts.'), '', - ...exportSurface('Storage export surface', storageSource, 'Supported persistence and crypto adapters for first-use applications.'), + ...exportSurface('Storage export surface', storageSource, 'Git-backed and in-memory adapters for first-use applications.'), '', - ...exportSurface('Advanced export surface', advancedSource, 'Formal WARP and Continuum concepts for expert use; not first-use root API.'), + ...exportSurface('Advanced export surface', advancedSource, 'Bounded Coordinate, Optic, and Witness concepts for expert use.'), '', - ...exportSurface('Diagnostics export surface', diagnosticsSource, 'Operator, inspection, comparison, and replay tools.'), + ...exportSurface('Diagnostics export surface', diagnosticsSource, 'Operator inspection helpers that consume public receipt handles.'), '', '## CLI command registry', '', @@ -279,7 +281,7 @@ function generate(): string { '', '## Public error classes', '', - table(['Class', 'Module', 'Source'], errors.map((item) => [`\`${item.name}\``, `\`${item.detail}\``, `\`${item.source}\``])), + ...(errors.length === 0 ? ['The v19 package root does not export error constructors.'] : [table(['Class', 'Module', 'Source'], errors.map((item) => [`\`${item.name}\``, `\`${item.detail}\``, `\`${item.source}\``]))]), ].join('\n')}\n`; } diff --git a/scripts/direct-export-name.ts b/scripts/direct-export-name.ts new file mode 100644 index 000000000..e8b08539d --- /dev/null +++ b/scripts/direct-export-name.ts @@ -0,0 +1,6 @@ +export function directExportName(line: string, kind: 'values' | 'types'): string | null { + const pattern = kind === 'types' + ? /^export (?:type|interface) ([A-Za-z0-9_]+)/ + : /^export (?:async )?(?:function|class|const|let|var|enum) ([A-Za-z0-9_]+)/; + return pattern.exec(line.trimStart())?.[1] ?? null; +} diff --git a/src/domain/api/DraftTimelineRuntime.ts b/src/domain/api/DraftTimelineRuntime.ts index 16ec03694..151c8695b 100644 --- a/src/domain/api/DraftTimelineRuntime.ts +++ b/src/domain/api/DraftTimelineRuntime.ts @@ -6,7 +6,8 @@ import { applyIntentToPatch } from './IntentRuntime.ts'; import JoinReceipt from './JoinReceipt.ts'; import JoinResult from './JoinResult.ts'; import type { JoinOptions } from './Timeline.ts'; -import WriteReceipt from './WriteReceipt.ts'; +import type WriteReceipt from './WriteReceipt.ts'; +import { executeIntentWrite } from './WriteRuntime.ts'; type DraftTimelineState = { readonly runtime: WarpWorldline; @@ -20,7 +21,6 @@ type DraftTimelineState = { type DraftWriteFields = { readonly runtime: WarpWorldline; - readonly timelineName: string; readonly draftName: string; readonly state: DraftTimelineState; readonly intent: Intent; @@ -64,7 +64,6 @@ export async function createDraftTimeline( writer: runtime.writerId, writeDraft: async (intent) => await writeDraftIntent({ runtime, - timelineName, draftName, state, intent, @@ -176,18 +175,21 @@ function createDraftState(runtime: WarpWorldline): DraftTimelineState { } async function writeDraftIntent(fields: DraftWriteFields): Promise { - const patchSha = await fields.runtime.patchDraft(fields.draftName, (patch) => { - applyIntentToPatch(fields.intent, patch); - }); + const receipt = await executeIntentWrite( + fields.runtime, + fields.intent, + async (build) => await fields.runtime.patchDraft(fields.draftName, build), + ); + if (receipt.outcome !== 'accepted') { + return receipt; + } + const { patchSha } = receipt; + if (patchSha === undefined) { + throw new WarpError('Accepted draft write is missing its patch SHA', 'E_DRAFT_WRITE_RECEIPT'); + } fields.state.draftPatchShas.push(patchSha); fields.state.intents.push(fields.intent); - return new WriteReceipt({ - timeline: fields.timelineName, - writer: fields.runtime.writerId, - intent: fields.intent, - outcome: 'accepted', - patchSha, - }); + return receipt; } function rejectedJoin(fields: RejectedJoinFields): JoinResult { diff --git a/src/domain/api/Intent.ts b/src/domain/api/Intent.ts index 31efb219a..652baee57 100644 --- a/src/domain/api/Intent.ts +++ b/src/domain/api/Intent.ts @@ -2,13 +2,7 @@ import WarpError from '../errors/WarpError.ts'; import { isPropValue, type PropValue } from '../types/PropValue.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; -export type IntentKind = - | 'node.add' - | 'node.remove' - | 'edge.add' - | 'edge.remove' - | 'property.set' - | 'edgeProperty.set'; +export type IntentKind = 'node.add' | 'node.remove' | 'edge.add' | 'edge.remove' | 'property.set'; export type NodeIntentFields = { readonly subject: string; @@ -26,25 +20,18 @@ export type PropertyIntentFields = { readonly value: PropValue; }; -export type EdgePropertyIntentFields = EdgeIntentFields & { - readonly key: string; - readonly value: PropValue; -}; - export type IntentDescriptor = | (NodeIntentFields & { readonly kind: 'node.add' }) | (NodeIntentFields & { readonly kind: 'node.remove' }) | (EdgeIntentFields & { readonly kind: 'edge.add' }) | (EdgeIntentFields & { readonly kind: 'edge.remove' }) - | (PropertyIntentFields & { readonly kind: 'property.set' }) - | (EdgePropertyIntentFields & { readonly kind: 'edgeProperty.set' }); + | (PropertyIntentFields & { readonly kind: 'property.set' }); const NODE_ADD: 'node.add' = 'node.add'; const NODE_REMOVE: 'node.remove' = 'node.remove'; const EDGE_ADD: 'edge.add' = 'edge.add'; const EDGE_REMOVE: 'edge.remove' = 'edge.remove'; const PROPERTY_SET: 'property.set' = 'property.set'; -const EDGE_PROPERTY_SET: 'edgeProperty.set' = 'edgeProperty.set'; export default class Intent { readonly #descriptor: IntentDescriptor; @@ -74,10 +61,6 @@ export default class Intent { return new Intent(propertyDescriptor(fields)); } - static setEdgeProperty(fields: EdgePropertyIntentFields): Intent { - return new Intent(edgePropertyDescriptor(fields)); - } - get kind(): IntentKind { return this.#descriptor.kind; } @@ -108,31 +91,34 @@ function normalizeKnownDescriptor(descriptor: IntentDescriptor): IntentDescripto if (descriptor.kind === PROPERTY_SET) { return propertyDescriptor(descriptor); } - if (descriptor.kind === EDGE_PROPERTY_SET) { - return edgePropertyDescriptor(descriptor); - } throw new WarpError('Intent kind is unsupported', 'E_INTENT_KIND'); } function isNodeDescriptor( - descriptor: IntentDescriptor, + descriptor: IntentDescriptor ): descriptor is NodeIntentFields & { readonly kind: 'node.add' | 'node.remove' } { return descriptor.kind === NODE_ADD || descriptor.kind === NODE_REMOVE; } function isEdgeDescriptor( - descriptor: IntentDescriptor, + descriptor: IntentDescriptor ): descriptor is EdgeIntentFields & { readonly kind: 'edge.add' | 'edge.remove' } { return descriptor.kind === EDGE_ADD || descriptor.kind === EDGE_REMOVE; } -function nodeDescriptor(kind: 'node.add' | 'node.remove', fields: NodeIntentFields): IntentDescriptor { +function nodeDescriptor( + kind: 'node.add' | 'node.remove', + fields: NodeIntentFields +): IntentDescriptor { const checkedFields = requireIntentFields(fields); requireNonEmptyString(checkedFields.subject, 'intent.subject'); return Object.freeze({ kind, subject: checkedFields.subject }); } -function edgeDescriptor(kind: 'edge.add' | 'edge.remove', fields: EdgeIntentFields): IntentDescriptor { +function edgeDescriptor( + kind: 'edge.add' | 'edge.remove', + fields: EdgeIntentFields +): IntentDescriptor { const checkedFields = requireIntentFields(fields); requireNonEmptyString(checkedFields.from, 'intent.from'); requireNonEmptyString(checkedFields.to, 'intent.to'); @@ -157,22 +143,6 @@ function propertyDescriptor(fields: PropertyIntentFields): IntentDescriptor { }); } -function edgePropertyDescriptor(fields: EdgePropertyIntentFields): IntentDescriptor { - const checkedFields = requireIntentFields(fields); - requireNonEmptyString(checkedFields.from, 'intent.from'); - requireNonEmptyString(checkedFields.to, 'intent.to'); - requireNonEmptyString(checkedFields.label, 'intent.label'); - requireNonEmptyString(checkedFields.key, 'intent.key'); - return Object.freeze({ - kind: EDGE_PROPERTY_SET, - from: checkedFields.from, - to: checkedFields.to, - label: checkedFields.label, - key: checkedFields.key, - value: requireIntentValue(checkedFields.value), - }); -} - function requireIntentFields(fields: TFields | null | undefined): TFields { if (fields === null || fields === undefined) { throw new WarpError('Intent fields are required', 'E_INTENT_FIELDS'); diff --git a/src/domain/api/IntentBuilders.ts b/src/domain/api/IntentBuilders.ts index 2bd1b8a3d..50df884a4 100644 --- a/src/domain/api/IntentBuilders.ts +++ b/src/domain/api/IntentBuilders.ts @@ -1,6 +1,5 @@ import Intent, { type EdgeIntentFields, - type EdgePropertyIntentFields, type NodeIntentFields, type PropertyIntentFields, } from './Intent.ts'; @@ -17,9 +16,6 @@ export type IntentBuilders = { readonly property: { readonly set: (fields: PropertyIntentFields) => Intent; }; - readonly edgeProperty: { - readonly set: (fields: EdgePropertyIntentFields) => Intent; - }; }; export const intent: IntentBuilders = Object.freeze({ @@ -34,7 +30,4 @@ export const intent: IntentBuilders = Object.freeze({ property: Object.freeze({ set: (fields: PropertyIntentFields) => Intent.setProperty(fields), }), - edgeProperty: Object.freeze({ - set: (fields: EdgePropertyIntentFields) => Intent.setEdgeProperty(fields), - }), }); diff --git a/src/domain/api/IntentRuntime.ts b/src/domain/api/IntentRuntime.ts index 5ce587ca9..2a089a443 100644 --- a/src/domain/api/IntentRuntime.ts +++ b/src/domain/api/IntentRuntime.ts @@ -11,7 +11,6 @@ const lowerers: ReadonlyMap = new Map([ ['edge.add', lowerEdgeAdd], ['edge.remove', lowerEdgeRemove], ['property.set', lowerPropertySet], - ['edgeProperty.set', lowerEdgePropertySet], ]); export function applyIntentToPatch(intent: Intent, patch: PatchBuilder): void { @@ -48,20 +47,9 @@ function lowerPropertySet(descriptor: IntentDescriptor, patch: PatchBuilder): vo patch.setProperty(descriptor.subject, descriptor.key, descriptor.value); } -function lowerEdgePropertySet(descriptor: IntentDescriptor, patch: PatchBuilder): void { - assertDescriptorKind(descriptor, 'edgeProperty.set'); - patch.setEdgeProperty( - descriptor.from, - descriptor.to, - descriptor.label, - descriptor.key, - descriptor.value, - ); -} - function assertDescriptorKind( descriptor: IntentDescriptor, - kind: K, + kind: K ): asserts descriptor is Extract { if (descriptor.kind !== kind) { throw new WarpError('Intent lowerer received a mismatched descriptor', 'E_INTENT_KIND'); diff --git a/src/domain/api/JoinReceipt.ts b/src/domain/api/JoinReceipt.ts index 8fd91018a..69c358dd9 100644 --- a/src/domain/api/JoinReceipt.ts +++ b/src/domain/api/JoinReceipt.ts @@ -1,10 +1,11 @@ import WarpError from '../errors/WarpError.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; import DraftTimeline from './DraftTimeline.ts'; -import { RECEIPT_OUTCOMES, type ReceiptOutcome } from './WriteReceipt.ts'; +import type { JoinOutcome } from './ReceiptOutcome.ts'; +import { RECEIPT_OUTCOMES } from './WriteReceipt.ts'; export type JoinMode = 'preview' | 'join'; -export type JoinReceiptOutcome = ReceiptOutcome; +export type JoinReceiptOutcome = JoinOutcome; export type JoinReceiptOptions = { readonly timeline: string; @@ -22,6 +23,7 @@ const JOIN_RECEIPT_OUTCOMES: ReadonlySet = RECEIPT_OUTCOMES; export default class JoinReceipt { readonly draft: DraftTimeline; readonly mode: JoinMode; + readonly operation: 'join' = 'join'; readonly outcome: JoinReceiptOutcome; readonly patchShas: readonly string[]; readonly reason: string | undefined; @@ -47,21 +49,42 @@ export default class JoinReceipt { function validateJoinReceiptFields(fields: JoinReceiptOptions): void { requireNonEmptyString(fields.timeline, 'joinReceipt.timeline'); requireNonEmptyString(fields.writer, 'joinReceipt.writer'); - if (!(fields.draft instanceof DraftTimeline)) { + validateDraft(fields.draft); + validateJoinMode(fields.mode); + validateJoinOutcome(fields.outcome); + validateJoinReason(fields); +} + +function validateDraft(draft: DraftTimeline): void { + if (!(draft instanceof DraftTimeline)) { throw new WarpError('JoinReceipt requires a DraftTimeline', 'E_JOIN_RECEIPT_DRAFT'); } - if (!JOIN_MODES.has(fields.mode)) { +} + +function validateJoinMode(mode: JoinMode): void { + if (!JOIN_MODES.has(mode)) { throw new WarpError('JoinReceipt mode is unsupported', 'E_JOIN_RECEIPT_MODE'); } - if (!JOIN_RECEIPT_OUTCOMES.has(fields.outcome)) { +} + +function validateJoinOutcome(outcome: JoinReceiptOutcome): void { + if (!JOIN_RECEIPT_OUTCOMES.has(outcome)) { throw new WarpError('JoinReceipt outcome is unsupported', 'E_JOIN_RECEIPT_OUTCOME'); } +} + +function validateJoinReason(fields: JoinReceiptOptions): void { if (fields.reason !== undefined) { requireNonEmptyString(fields.reason, 'joinReceipt.reason'); } + if (fields.outcome !== 'accepted' && fields.reason === undefined) { + throw new WarpError('Unaccepted JoinReceipt requires a reason', 'E_JOIN_RECEIPT_REASON'); + } } -function requireJoinReceiptOptions(options: JoinReceiptOptions | null | undefined): JoinReceiptOptions { +function requireJoinReceiptOptions( + options: JoinReceiptOptions | null | undefined +): JoinReceiptOptions { if (options === null || options === undefined) { throw new WarpError('JoinReceipt options are required', 'E_JOIN_RECEIPT_OPTIONS'); } diff --git a/src/domain/api/ReadReceipt.ts b/src/domain/api/ReadReceipt.ts index 9b295b65a..b415e7784 100644 --- a/src/domain/api/ReadReceipt.ts +++ b/src/domain/api/ReadReceipt.ts @@ -1,21 +1,38 @@ import WarpError from '../errors/WarpError.ts'; +import type ReadIdentity from '../services/optic/ReadIdentity.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; import Reading from './Reading.ts'; +import type { ReadOutcome } from './ReceiptOutcome.ts'; +import { freezeRepairHints, type RepairHint } from './ReceiptSupport.ts'; -export type ReadReceiptOutcome = - | 'resolved' - | 'obstructed' - | 'underdetermined' - | 'rejected'; +export type ReadReceiptOutcome = ReadOutcome; export type ReadReceiptOptions = { readonly timeline: string; readonly writer: string; readonly reading: Reading; readonly outcome: ReadReceiptOutcome; + readonly evidence?: ReadEvidence; + readonly repairHints?: readonly RepairHint[]; readonly reason?: string; }; +export type ReadEvidence = Readonly< + Pick< + ReadIdentity, + | 'kind' + | 'basis' + | 'worldline' + | 'entityAspect' + | 'checkpointSha' + | 'checkpointFrontier' + | 'checkpointIndexShards' + | 'tailWitnesses' + | 'reducerVersion' + | 'projectionVersion' + > +>; + const READ_RECEIPT_OUTCOMES: ReadonlySet = new Set([ 'resolved', 'obstructed', @@ -24,36 +41,77 @@ const READ_RECEIPT_OUTCOMES: ReadonlySet = new Set([ ]); export default class ReadReceipt { + readonly evidence: ReadEvidence | undefined; + readonly operation: 'read' = 'read'; readonly outcome: ReadReceiptOutcome; readonly reading: Reading; + readonly repairHints: readonly RepairHint[]; readonly reason: string | undefined; readonly timeline: string; readonly writer: string; constructor(options: ReadReceiptOptions | null | undefined) { const fields = requireReadReceiptOptions(options); - requireNonEmptyString(fields.timeline, 'readReceipt.timeline'); - requireNonEmptyString(fields.writer, 'readReceipt.writer'); - if (!(fields.reading instanceof Reading)) { - throw new WarpError('ReadReceipt requires a Reading', 'E_READ_RECEIPT_READING'); - } - if (!READ_RECEIPT_OUTCOMES.has(fields.outcome)) { - throw new WarpError('ReadReceipt outcome is unsupported', 'E_READ_RECEIPT_OUTCOME'); - } - if (fields.reason !== undefined) { - requireNonEmptyString(fields.reason, 'readReceipt.reason'); - } + validateReadReceiptFields(fields); this.timeline = fields.timeline; this.writer = fields.writer; this.reading = fields.reading; this.outcome = fields.outcome; + this.evidence = fields.evidence; + this.repairHints = freezeRepairHints(fields.repairHints ?? []); this.reason = fields.reason; Object.freeze(this); } } -function requireReadReceiptOptions(options: ReadReceiptOptions | null | undefined): ReadReceiptOptions { +function validateReadReceiptFields(fields: ReadReceiptOptions): void { + requireNonEmptyString(fields.timeline, 'readReceipt.timeline'); + requireNonEmptyString(fields.writer, 'readReceipt.writer'); + validateReading(fields.reading); + validateReadOutcome(fields.outcome); + validateReadResolution(fields); +} + +function validateReading(reading: Reading): void { + if (!(reading instanceof Reading)) { + throw new WarpError('ReadReceipt requires a Reading', 'E_READ_RECEIPT_READING'); + } +} + +function validateReadOutcome(outcome: ReadReceiptOutcome): void { + if (!READ_RECEIPT_OUTCOMES.has(outcome)) { + throw new WarpError('ReadReceipt outcome is unsupported', 'E_READ_RECEIPT_OUTCOME'); + } +} + +function validateReadResolution(fields: ReadReceiptOptions): void { + validateOptionalReason(fields.reason); + validateResolvedEvidence(fields); + validateUnresolvedReason(fields); +} + +function validateOptionalReason(reason: string | undefined): void { + if (reason !== undefined) { + requireNonEmptyString(reason, 'readReceipt.reason'); + } +} + +function validateResolvedEvidence(fields: ReadReceiptOptions): void { + if (fields.outcome === 'resolved' && fields.evidence === undefined) { + throw new WarpError('Resolved ReadReceipt requires evidence', 'E_READ_RECEIPT_EVIDENCE'); + } +} + +function validateUnresolvedReason(fields: ReadReceiptOptions): void { + if (fields.outcome !== 'resolved' && fields.reason === undefined) { + throw new WarpError('Unresolved ReadReceipt requires a reason', 'E_READ_RECEIPT_REASON'); + } +} + +function requireReadReceiptOptions( + options: ReadReceiptOptions | null | undefined +): ReadReceiptOptions { if (options === null || options === undefined) { throw new WarpError('ReadReceipt options are required', 'E_READ_RECEIPT_OPTIONS'); } diff --git a/src/domain/api/Reading.ts b/src/domain/api/Reading.ts index ef7cfe77f..525b6eee1 100644 --- a/src/domain/api/Reading.ts +++ b/src/domain/api/Reading.ts @@ -3,10 +3,12 @@ import { requireNonEmptyString } from '../utils/scalarValidation.ts'; const PROPERTY_GET: 'property.get' = 'property.get'; const NODE_EXISTS: 'node.exists' = 'node.exists'; +const NEIGHBORHOOD: 'neighborhood' = 'neighborhood'; +const READING_DIRECTIONS: ReadonlySet = new Set(['out', 'in', 'both']); -export type ReadingKind = - | 'property.get' - | 'node.exists'; +export type ReadingKind = 'property.get' | 'node.exists' | 'neighborhood'; + +export type ReadingDirection = 'out' | 'in' | 'both'; export type PropertyReadingFields = { readonly subject: string; @@ -17,9 +19,27 @@ export type NodeReadingFields = { readonly subject: string; }; +export type NeighborhoodReadingFields = { + readonly subject: string; + readonly direction?: ReadingDirection; + readonly labels?: readonly string[]; + readonly limit?: number; + readonly cursor?: string; +}; + export type ReadingDescriptor = | (PropertyReadingFields & { readonly kind: 'property.get' }) - | (NodeReadingFields & { readonly kind: 'node.exists' }); + | (NodeReadingFields & { readonly kind: 'node.exists' }) + | (NeighborhoodReadingFields & { readonly kind: 'neighborhood' }); + +type MutableNeighborhoodDescriptor = { + kind: 'neighborhood'; + subject: string; + direction?: ReadingDirection; + labels?: readonly string[]; + limit?: number; + cursor?: string; +}; export default class Reading { readonly #descriptor: ReadingDescriptor; @@ -37,6 +57,10 @@ export default class Reading { return new Reading(nodeExistsDescriptor(fields)); } + static neighborhood(fields: NeighborhoodReadingFields): Reading { + return new Reading(neighborhoodDescriptor(fields)); + } + get kind(): ReadingKind { return this.#descriptor.kind; } @@ -64,6 +88,9 @@ function normalizeKnownDescriptor(descriptor: ReadingDescriptor): ReadingDescrip if (descriptor.kind === NODE_EXISTS) { return nodeExistsDescriptor(descriptor); } + if (descriptor.kind === NEIGHBORHOOD) { + return neighborhoodDescriptor(descriptor); + } throw new WarpError('Reading kind is unsupported', 'E_READING_KIND'); } @@ -87,6 +114,72 @@ function nodeExistsDescriptor(fields: NodeReadingFields): ReadingDescriptor { }); } +function neighborhoodDescriptor(fields: NeighborhoodReadingFields): ReadingDescriptor { + const checkedFields = requireReadingFields(fields); + requireNonEmptyString(checkedFields.subject, 'reading.subject'); + validateNeighborhoodCursor(checkedFields.cursor); + validateNeighborhoodDirection(checkedFields.direction); + validateNeighborhoodLimit(checkedFields.limit); + const labels = freezeNeighborhoodLabels(checkedFields.labels); + const descriptor: MutableNeighborhoodDescriptor = { + kind: NEIGHBORHOOD, + subject: checkedFields.subject, + }; + assignNeighborhoodOptions(descriptor, checkedFields, labels); + return Object.freeze(descriptor); +} + +function assignNeighborhoodOptions( + descriptor: MutableNeighborhoodDescriptor, + fields: NeighborhoodReadingFields, + labels: readonly string[] | undefined +): void { + if (fields.direction !== undefined) { + descriptor.direction = fields.direction; + } + if (labels !== undefined) { + descriptor.labels = labels; + } + if (fields.limit !== undefined) { + descriptor.limit = fields.limit; + } + if (fields.cursor !== undefined) { + descriptor.cursor = fields.cursor; + } +} + +function validateNeighborhoodCursor(cursor: string | undefined): void { + if (cursor !== undefined) { + requireNonEmptyString(cursor, 'reading.cursor'); + } +} + +function validateNeighborhoodDirection(direction: ReadingDirection | undefined): void { + if (direction !== undefined && !READING_DIRECTIONS.has(direction)) { + throw new WarpError('Reading neighborhood direction is unsupported', 'E_READING_DIRECTION'); + } +} + +function validateNeighborhoodLimit(limit: number | undefined): void { + if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) { + throw new WarpError('Reading neighborhood limit must be a positive integer', 'E_READING_LIMIT'); + } +} + +function freezeNeighborhoodLabels( + labels: readonly string[] | undefined +): readonly string[] | undefined { + if (labels === undefined) { + return undefined; + } + return Object.freeze( + labels.map((label) => { + requireNonEmptyString(label, 'reading.label'); + return label; + }) + ); +} + function requireReadingFields(fields: TFields | null | undefined): TFields { if (fields === null || fields === undefined) { throw new WarpError('Reading fields are required', 'E_READING_FIELDS'); diff --git a/src/domain/api/ReadingBuilders.ts b/src/domain/api/ReadingBuilders.ts index 4f16cbaf0..605d1bca7 100644 --- a/src/domain/api/ReadingBuilders.ts +++ b/src/domain/api/ReadingBuilders.ts @@ -1,10 +1,12 @@ import Reading, { + type NeighborhoodReadingFields, type NodeReadingFields, type PropertyReadingFields, } from './Reading.ts'; export type ReadingBuilders = { readonly property: (fields: PropertyReadingFields) => Reading; + readonly neighborhood: (fields: NeighborhoodReadingFields) => Reading; readonly node: { readonly exists: (fields: NodeReadingFields) => Reading; }; @@ -12,6 +14,7 @@ export type ReadingBuilders = { export const reading: ReadingBuilders = Object.freeze({ property: (fields: PropertyReadingFields) => Reading.property(fields), + neighborhood: (fields: NeighborhoodReadingFields) => Reading.neighborhood(fields), node: Object.freeze({ exists: (fields: NodeReadingFields) => Reading.nodeExists(fields), }), diff --git a/src/domain/api/ReadingRuntime.ts b/src/domain/api/ReadingRuntime.ts index 4c7adf6c6..ae9f99c86 100644 --- a/src/domain/api/ReadingRuntime.ts +++ b/src/domain/api/ReadingRuntime.ts @@ -1,62 +1,201 @@ import type WarpWorldline from '../WarpWorldline.ts'; import WarpError from '../errors/WarpError.ts'; +import type ReadIdentity from '../services/optic/ReadIdentity.ts'; +import type { NeighborhoodOpticReadOptions } from '../services/optic/NeighborhoodOptic.ts'; +import type NeighborhoodOpticReadResult from '../services/optic/NeighborhoodOpticReadResult.ts'; +import type WorldlineOptic from '../services/optic/WorldlineOptic.ts'; +import { createSnapshotPropValue } from '../services/ImmutableSnapshot.ts'; import type { SnapshotPropValue } from '../services/snapshot/SnapshotPropValue.ts'; import type Reading from './Reading.ts'; -import type { - ReadingDescriptor, - ReadingKind, -} from './Reading.ts'; -import ReadReceipt from './ReadReceipt.ts'; +import type { ReadingDescriptor, ReadingKind } from './Reading.ts'; +import ReadReceipt, { type ReadReceiptOutcome } from './ReadReceipt.ts'; +import type { RepairHint } from './ReceiptSupport.ts'; import ReadingResult from './ReadingResult.ts'; +type BoundedReading = { + readonly evidence: ReadIdentity; + readonly value: SnapshotPropValue; +}; + type ReadingExecutor = ( descriptor: ReadingDescriptor, - runtime: WarpWorldline, -) => Promise; + optic: WorldlineOptic +) => Promise; + +type OperationalReadFailure = { + readonly outcome: Exclude; + readonly reason: string; + readonly repairHints: readonly RepairHint[]; +}; + +const REPAIR_BOUNDED_BASIS = Object.freeze([ + Object.freeze({ + code: 'repair_bounded_basis', + message: + 'Create the timeline checkpoint, or repair its state-cache retention, before retrying.', + }), +]); const readers: ReadonlyMap = new Map([ ['property.get', readProperty], ['node.exists', readNodeExists], + ['neighborhood', readNeighborhood], ]); export async function executeReading( runtime: WarpWorldline, reading: Reading, + optic?: WorldlineOptic ): Promise { const { descriptor } = reading; - const reader = readers.get(reading.kind); + const reader = requireReader(reading.kind); + try { + const result = await reader(descriptor, optic ?? runtime.optic()); + return readingResult(runtime, reading, result); + } catch (error) { + if (!(error instanceof WarpError)) { + throw error; + } + const failure = operationalReadFailure(error); + if (failure === null) { + throw error; + } + return unresolvedReadingResult(runtime, reading, failure); + } +} + +function requireReader(kind: ReadingKind): ReadingExecutor { + const reader = readers.get(kind); if (reader === undefined) { throw new WarpError('Reading kind is unsupported', 'E_READING_KIND'); } - const value = await reader(descriptor, runtime); - return new ReadingResult({ - value, - receipt: new ReadReceipt({ - timeline: runtime.worldlineName, - writer: runtime.writerId, - reading, - outcome: 'resolved', - }), - }); + return reader; } async function readProperty( descriptor: ReadingDescriptor, - runtime: WarpWorldline, -): Promise { + optic: WorldlineOptic +): Promise { if (descriptor.kind !== 'property.get') { throw new WarpError('Reading executor received a mismatched descriptor', 'E_READING_KIND'); } - const props = await runtime.live().getNodeProps(descriptor.subject); - return props?.[descriptor.key] ?? null; + const result = await optic.node(descriptor.subject).prop(descriptor.key).read(); + return { + value: result.value === undefined ? null : createSnapshotPropValue(result.value), + evidence: result.readIdentity, + }; } async function readNodeExists( descriptor: ReadingDescriptor, - runtime: WarpWorldline, -): Promise { + optic: WorldlineOptic +): Promise { if (descriptor.kind !== 'node.exists') { throw new WarpError('Reading executor received a mismatched descriptor', 'E_READING_KIND'); } - return await runtime.live().hasNode(descriptor.subject); + const result = await optic.node(descriptor.subject).read(); + return { value: result.alive, evidence: result.readIdentity }; +} + +async function readNeighborhood( + descriptor: ReadingDescriptor, + optic: WorldlineOptic +): Promise { + if (descriptor.kind !== 'neighborhood') { + throw new WarpError('Reading executor received a mismatched descriptor', 'E_READING_KIND'); + } + const result = await optic.neighborhood(descriptor.subject).read(neighborhoodOptions(descriptor)); + return { evidence: result.readIdentity, value: neighborhoodValue(result) }; +} + +function neighborhoodOptions( + descriptor: Extract +): NeighborhoodOpticReadOptions { + const options: { + direction?: 'out' | 'in' | 'both'; + labels?: readonly string[]; + limit?: number; + cursor?: string; + } = {}; + if (descriptor.direction !== undefined) { + options.direction = descriptor.direction; + } + if (descriptor.labels !== undefined) { + options.labels = descriptor.labels; + } + if (descriptor.limit !== undefined) { + options.limit = descriptor.limit; + } + if (descriptor.cursor !== undefined) { + options.cursor = descriptor.cursor; + } + return options; +} + +function neighborhoodValue(result: NeighborhoodOpticReadResult): SnapshotPropValue { + const edges = result.edges.map((edge) => + Object.freeze({ + direction: edge.direction, + neighborId: edge.neighborId, + label: edge.label, + }) + ); + return Object.freeze({ + subject: result.nodeId, + direction: result.direction, + edges: Object.freeze(edges), + completeness: result.completeness, + cursor: result.cursor, + }); +} + +function readingResult( + runtime: WarpWorldline, + reading: Reading, + result: BoundedReading +): ReadingResult { + return new ReadingResult({ + value: result.value, + receipt: new ReadReceipt({ + timeline: runtime.worldlineName, + writer: runtime.writerId, + reading, + outcome: 'resolved', + evidence: result.evidence, + }), + }); +} + +function unresolvedReadingResult( + runtime: WarpWorldline, + reading: Reading, + failure: OperationalReadFailure +): ReadingResult { + return new ReadingResult({ + value: null, + receipt: new ReadReceipt({ + timeline: runtime.worldlineName, + writer: runtime.writerId, + reading, + ...failure, + }), + }); +} + +function operationalReadFailure(error: WarpError): OperationalReadFailure | null { + if (error.code === 'E_OPTIC_NO_BOUNDED_BASIS') { + return { + outcome: 'obstructed', + reason: 'missing_bounded_basis', + repairHints: REPAIR_BOUNDED_BASIS, + }; + } + if (error.code === 'E_OPTIC_TAIL_BUDGET_EXCEEDED') { + return { + outcome: 'underdetermined', + reason: 'tail_budget_exceeded', + repairHints: REPAIR_BOUNDED_BASIS, + }; + } + return null; } diff --git a/src/domain/api/Receipt.ts b/src/domain/api/Receipt.ts new file mode 100644 index 000000000..b2a8091ec --- /dev/null +++ b/src/domain/api/Receipt.ts @@ -0,0 +1,5 @@ +import type JoinReceipt from './JoinReceipt.ts'; +import type ReadReceipt from './ReadReceipt.ts'; +import type WriteReceipt from './WriteReceipt.ts'; + +export type Receipt = WriteReceipt | ReadReceipt | JoinReceipt; diff --git a/src/domain/api/ReceiptOutcome.ts b/src/domain/api/ReceiptOutcome.ts new file mode 100644 index 000000000..391061509 --- /dev/null +++ b/src/domain/api/ReceiptOutcome.ts @@ -0,0 +1,11 @@ +export type ReceiptOutcome = + | 'accepted' + | 'resolved' + | 'obstructed' + | 'conflicted' + | 'underdetermined' + | 'rejected'; + +export type WriteOutcome = Exclude; +export type ReadOutcome = Exclude; +export type JoinOutcome = WriteOutcome; diff --git a/src/domain/api/ReceiptSupport.ts b/src/domain/api/ReceiptSupport.ts new file mode 100644 index 000000000..8d3dfaf45 --- /dev/null +++ b/src/domain/api/ReceiptSupport.ts @@ -0,0 +1,16 @@ +import { requireNonEmptyString } from '../utils/scalarValidation.ts'; + +export type RepairHint = { + readonly code: string; + readonly message: string; +}; + +export function freezeRepairHints(hints: readonly RepairHint[]): readonly RepairHint[] { + return Object.freeze( + hints.map((hint) => { + requireNonEmptyString(hint.code, 'receipt.repairHint.code'); + requireNonEmptyString(hint.message, 'receipt.repairHint.message'); + return Object.freeze({ code: hint.code, message: hint.message }); + }) + ); +} diff --git a/src/domain/api/Tick.ts b/src/domain/api/Tick.ts new file mode 100644 index 000000000..e2fe8f7bf --- /dev/null +++ b/src/domain/api/Tick.ts @@ -0,0 +1,23 @@ +import WarpError from '../errors/WarpError.ts'; +import { requireNonEmptyString } from '../utils/scalarValidation.ts'; + +type TickOptions = { + readonly id: string; + readonly timeline: string; +}; + +export default class Tick { + readonly id: string; + readonly timeline: string; + + constructor(options: TickOptions | null | undefined) { + if (options === null || options === undefined) { + throw new WarpError('Tick options are required', 'E_TICK_OPTIONS'); + } + requireNonEmptyString(options.id, 'tick.id'); + requireNonEmptyString(options.timeline, 'tick.timeline'); + this.id = options.id; + this.timeline = options.timeline; + Object.freeze(this); + } +} diff --git a/src/domain/api/TickRuntime.ts b/src/domain/api/TickRuntime.ts new file mode 100644 index 000000000..bbfdf3421 --- /dev/null +++ b/src/domain/api/TickRuntime.ts @@ -0,0 +1,32 @@ +import type WarpWorldline from '../WarpWorldline.ts'; +import type WarpWorldlineCoordinate from '../WarpWorldlineCoordinate.ts'; +import WarpError from '../errors/WarpError.ts'; +import Tick from './Tick.ts'; + +const tickCoordinates = new WeakMap(); + +export async function createTick(runtime: WarpWorldline): Promise { + await runtime.prepareOpticBasis(); + const coordinate = await runtime.coordinate(); + const tick = new Tick({ + timeline: runtime.worldlineName, + id: tickId(coordinate), + }); + tickCoordinates.set(tick, coordinate); + return tick; +} + +export function requireTickCoordinate(runtime: WarpWorldline, tick: Tick): WarpWorldlineCoordinate { + const coordinate = tickCoordinates.get(tick); + if (coordinate === undefined || tick.timeline !== runtime.worldlineName) { + throw new WarpError('Tick does not belong to this Timeline', 'E_TIMELINE_TICK_MISMATCH'); + } + return coordinate; +} + +function tickId(coordinate: WarpWorldlineCoordinate): string { + const frontier = coordinate.frontierEntries + .map((entry) => `${encodeURIComponent(entry.writerId)}=${entry.patchSha}`) + .join(','); + return `tick:${coordinate.checkpointSha}:${frontier}`; +} diff --git a/src/domain/api/Timeline.ts b/src/domain/api/Timeline.ts index 4eb4dfe70..459bc45f3 100644 --- a/src/domain/api/Timeline.ts +++ b/src/domain/api/Timeline.ts @@ -1,10 +1,13 @@ import WarpError from '../errors/WarpError.ts'; +import type WarpWorldlineCoordinate from '../WarpWorldlineCoordinate.ts'; import { assertTimelineNameIdentity, assertWriterIdentity } from './assertIdentity.ts'; import DraftTimeline from './DraftTimeline.ts'; import Intent from './Intent.ts'; import type JoinResult from './JoinResult.ts'; import Reading from './Reading.ts'; -import type ReadingResult from './ReadingResult.ts'; +import type { default as ReadingResult, ReadingValue } from './ReadingResult.ts'; +import Tick from './Tick.ts'; +import type TimelineView from './TimelineView.ts'; import type WriteReceipt from './WriteReceipt.ts'; const DETERMINISTIC_JOIN_POLICY: 'deterministic' = 'deterministic'; @@ -19,20 +22,29 @@ export type JoinOptions = { type TimelineConstructionOptions = { readonly name: string; readonly writer: string; + readonly captureCoordinate?: CaptureCoordinate; + readonly captureTick?: CaptureTick; readonly joinDraft?: JoinDraft; readonly openDraft?: OpenDraft; + readonly openView?: OpenView; readonly previewJoinDraft?: JoinDraft; readonly readReading?: ReadReading; readonly writeIntent?: WriteIntent; }; type JoinDraft = (draft: DraftTimeline, options: JoinOptions) => Promise; +type CaptureCoordinate = () => Promise; +type CaptureTick = () => Promise; type OpenDraft = (name: string) => Promise; +type OpenView = (tick: Tick) => TimelineView; type ReadReading = (reading: Reading) => Promise; type WriteIntent = (intent: Intent) => Promise; type TimelinePortErrorCode = | 'E_TIMELINE_JOINER' + | 'E_TIMELINE_COORDINATE_READER' + | 'E_TIMELINE_TICK_READER' | 'E_TIMELINE_DRAFT_OPENER' + | 'E_TIMELINE_VIEW_OPENER' | 'E_TIMELINE_READER' | 'E_TIMELINE_WRITER'; @@ -44,9 +56,12 @@ type TimelinePortErrorCode = * root API contract. */ export default class Timeline { + readonly #captureCoordinate: CaptureCoordinate | null; + readonly #captureTick: CaptureTick | null; readonly #joinDraft: JoinDraft | null; readonly #name: string; readonly #openDraft: OpenDraft | null; + readonly #openView: OpenView | null; readonly #previewJoinDraft: JoinDraft | null; readonly #readReading: ReadReading | null; readonly #writeIntent: WriteIntent | null; @@ -63,8 +78,11 @@ export default class Timeline { code: 'E_TIMELINE_IDENTITY', }); this.#joinDraft = optionalPort(options.joinDraft); + this.#captureCoordinate = optionalPort(options.captureCoordinate); + this.#captureTick = optionalPort(options.captureTick); this.#name = options.name; this.#openDraft = optionalPort(options.openDraft); + this.#openView = optionalPort(options.openView); this.#previewJoinDraft = optionalPort(options.previewJoinDraft); this.#writer = options.writer; this.#readReading = optionalPort(options.readReading); @@ -91,6 +109,30 @@ export default class Timeline { return await this.#openDraft(name); } + async coordinate(): Promise { + if (this.#captureCoordinate === null) { + throw new WarpError('Timeline was not opened by openWarp', 'E_TIMELINE_RUNTIME_UNAVAILABLE'); + } + return await this.#captureCoordinate(); + } + + async tick(): Promise { + if (this.#captureTick === null) { + throw new WarpError('Timeline was not opened by openWarp', 'E_TIMELINE_RUNTIME_UNAVAILABLE'); + } + return await this.#captureTick(); + } + + at(tick: Tick): TimelineView { + if (!(tick instanceof Tick)) { + throw new WarpError('Timeline.at requires a Tick', 'E_TIMELINE_AT_TICK'); + } + if (this.#openView === null) { + throw new WarpError('Timeline was not opened by openWarp', 'E_TIMELINE_RUNTIME_UNAVAILABLE'); + } + return this.#openView(tick); + } + async previewJoin(draft: DraftTimeline, options?: JoinOptions): Promise { assertDraftTimeline(draft); if (this.#previewJoinDraft === null) { @@ -115,6 +157,23 @@ export default class Timeline { return await this.#readReading(reading); } + async readValue(reading: Reading): Promise { + const result = await this.read(reading); + if (result.receipt.outcome !== 'resolved') { + throw new WarpError( + 'Timeline.readValue requires a resolved reading', + 'E_TIMELINE_READ_UNRESOLVED', + { + context: { + outcome: result.receipt.outcome, + reason: result.receipt.reason, + }, + } + ); + } + return result.value; + } + async write(intent: Intent): Promise { assertIntentInstance(intent); if (this.#writeIntent === null) { @@ -126,7 +185,10 @@ export default class Timeline { function assertDraftTimeline(draft: DraftTimeline): void { if (!(draft instanceof DraftTimeline)) { - throw new WarpError('Timeline join operations require a DraftTimeline', 'E_TIMELINE_JOIN_DRAFT'); + throw new WarpError( + 'Timeline join operations require a DraftTimeline', + 'E_TIMELINE_JOIN_DRAFT' + ); } } @@ -150,21 +212,40 @@ function assertTimelineConstructionOptions(options: TimelineConstructionOptions) if (options === null || options === undefined) { throw new WarpError( 'Timeline requires construction options', - 'E_TIMELINE_CONSTRUCTION_OPTIONS', + 'E_TIMELINE_CONSTRUCTION_OPTIONS' ); } assertJoinDraft(options.joinDraft); + assertCaptureCoordinate(options.captureCoordinate); + assertCaptureTick(options.captureTick); assertOpenDraft(options.openDraft); + assertOpenView(options.openView); assertJoinDraft(options.previewJoinDraft); assertReadReading(options.readReading); assertWriteIntent(options.writeIntent); } +function assertCaptureCoordinate(capture: CaptureCoordinate | undefined): void { + assertOptionalFunctionPort( + capture, + 'Timeline requires a coordinate function when provided', + 'E_TIMELINE_COORDINATE_READER' + ); +} + +function assertCaptureTick(capture: CaptureTick | undefined): void { + assertOptionalFunctionPort( + capture, + 'Timeline requires a tick function when provided', + 'E_TIMELINE_TICK_READER' + ); +} + function assertJoinDraft(joinDraft: JoinDraft | undefined): void { assertOptionalFunctionPort( joinDraft, 'Timeline requires a join function when provided', - 'E_TIMELINE_JOINER', + 'E_TIMELINE_JOINER' ); } @@ -172,7 +253,15 @@ function assertOpenDraft(openDraft: OpenDraft | undefined): void { assertOptionalFunctionPort( openDraft, 'Timeline requires an openDraft function when provided', - 'E_TIMELINE_DRAFT_OPENER', + 'E_TIMELINE_DRAFT_OPENER' + ); +} + +function assertOpenView(openView: OpenView | undefined): void { + assertOptionalFunctionPort( + openView, + 'Timeline requires an openView function when provided', + 'E_TIMELINE_VIEW_OPENER' ); } @@ -180,7 +269,7 @@ function assertReadReading(readReading: ReadReading | undefined): void { assertOptionalFunctionPort( readReading, 'Timeline requires a readReading function when provided', - 'E_TIMELINE_READER', + 'E_TIMELINE_READER' ); } @@ -204,7 +293,10 @@ function assertJoinOptionsObject(options: JoinOptions | null): asserts options i function assertNoDryRunTrap(options: JoinOptions): void { if ('dryRun' in options) { - throw new WarpError('Use Timeline.previewJoin() instead of a dryRun option', 'E_TIMELINE_JOIN_DRY_RUN'); + throw new WarpError( + 'Use Timeline.previewJoin() instead of a dryRun option', + 'E_TIMELINE_JOIN_DRY_RUN' + ); } } @@ -212,14 +304,14 @@ function assertWriteIntent(writeIntent: WriteIntent | undefined): void { assertOptionalFunctionPort( writeIntent, 'Timeline requires a writeIntent function when provided', - 'E_TIMELINE_WRITER', + 'E_TIMELINE_WRITER' ); } function assertOptionalFunctionPort( port: TPort | undefined, message: string, - code: TimelinePortErrorCode, + code: TimelinePortErrorCode ): void { if (port !== undefined && typeof port !== 'function') { throw new WarpError(message, code); diff --git a/src/domain/api/TimelineRuntime.ts b/src/domain/api/TimelineRuntime.ts index 71c027f0e..af21dc713 100644 --- a/src/domain/api/TimelineRuntime.ts +++ b/src/domain/api/TimelineRuntime.ts @@ -5,10 +5,11 @@ import { joinDraftTimeline, previewDraftJoin, } from './DraftTimelineRuntime.ts'; -import { applyIntentToPatch } from './IntentRuntime.ts'; import { executeReading } from './ReadingRuntime.ts'; +import { createTick } from './TickRuntime.ts'; import Timeline from './Timeline.ts'; -import WriteReceipt from './WriteReceipt.ts'; +import { createTimelineView } from './TimelineViewRuntime.ts'; +import { executeIntentWrite } from './WriteRuntime.ts'; const timelineRuntimes = new WeakMap(); @@ -16,22 +17,18 @@ export function createTimeline(runtime: WarpWorldline): Timeline { const timeline = new Timeline({ name: runtime.worldlineName, writer: runtime.writerId, + captureCoordinate: async () => { + await runtime.prepareOpticBasis(); + return await runtime.coordinate(); + }, + captureTick: async () => await createTick(runtime), joinDraft: (draft, options) => joinDraftTimeline(runtime, draft, options), openDraft: (name) => createDraftTimeline(runtime, runtime.worldlineName, name), + openView: (tick) => createTimelineView(runtime, tick), previewJoinDraft: (draft, options) => previewDraftJoin(runtime, draft, options), readReading: (reading) => executeReading(runtime, reading), - writeIntent: async (intent) => { - const patchSha = await runtime.commit((patch) => { - applyIntentToPatch(intent, patch); - }); - return new WriteReceipt({ - timeline: runtime.worldlineName, - writer: runtime.writerId, - intent, - outcome: 'accepted', - patchSha, - }); - }, + writeIntent: async (intent) => + await executeIntentWrite(runtime, intent, runtime.commit.bind(runtime)), }); timelineRuntimes.set(timeline, runtime); return timeline; diff --git a/src/domain/api/TimelineView.ts b/src/domain/api/TimelineView.ts new file mode 100644 index 000000000..2afd729aa --- /dev/null +++ b/src/domain/api/TimelineView.ts @@ -0,0 +1,48 @@ +import WarpError from '../errors/WarpError.ts'; +import Reading from './Reading.ts'; +import type { default as ReadingResult, ReadingValue } from './ReadingResult.ts'; +import type Tick from './Tick.ts'; + +type TimelineViewOptions = { + readonly name: string; + readonly writer: string; + readonly tick: Tick; + readonly readReading: (reading: Reading) => Promise; +}; + +export default class TimelineView { + readonly #readReading: TimelineViewOptions['readReading']; + readonly name: string; + readonly tick: Tick; + readonly writer: string; + + constructor(options: TimelineViewOptions) { + if (options === null || options === undefined || typeof options.readReading !== 'function') { + throw new WarpError('TimelineView requires runtime options', 'E_TIMELINE_VIEW_OPTIONS'); + } + this.name = options.name; + this.writer = options.writer; + this.tick = options.tick; + this.#readReading = options.readReading; + Object.freeze(this); + } + + async read(reading: Reading): Promise { + if (!(reading instanceof Reading)) { + throw new WarpError('TimelineView.read requires a Reading', 'E_TIMELINE_VIEW_READING'); + } + return await this.#readReading(reading); + } + + async readValue(reading: Reading): Promise { + const result = await this.read(reading); + if (result.receipt.outcome !== 'resolved') { + throw new WarpError( + 'TimelineView.readValue requires a resolved reading', + 'E_TIMELINE_VIEW_READ_UNRESOLVED', + { context: { outcome: result.receipt.outcome, reason: result.receipt.reason } } + ); + } + return result.value; + } +} diff --git a/src/domain/api/TimelineViewRuntime.ts b/src/domain/api/TimelineViewRuntime.ts new file mode 100644 index 000000000..e005aae5e --- /dev/null +++ b/src/domain/api/TimelineViewRuntime.ts @@ -0,0 +1,15 @@ +import type WarpWorldline from '../WarpWorldline.ts'; +import { executeReading } from './ReadingRuntime.ts'; +import type Tick from './Tick.ts'; +import { requireTickCoordinate } from './TickRuntime.ts'; +import TimelineView from './TimelineView.ts'; + +export function createTimelineView(runtime: WarpWorldline, tick: Tick): TimelineView { + const coordinate = requireTickCoordinate(runtime, tick); + return new TimelineView({ + name: runtime.worldlineName, + writer: runtime.writerId, + tick, + readReading: async (reading) => await executeReading(runtime, reading, coordinate.optic()), + }); +} diff --git a/src/domain/api/WriteReceipt.ts b/src/domain/api/WriteReceipt.ts index 88d1f963e..8025ab892 100644 --- a/src/domain/api/WriteReceipt.ts +++ b/src/domain/api/WriteReceipt.ts @@ -1,24 +1,31 @@ import WarpError from '../errors/WarpError.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; import Intent from './Intent.ts'; +import type { WriteOutcome } from './ReceiptOutcome.ts'; +import { freezeRepairHints, type RepairHint } from './ReceiptSupport.ts'; -export type ReceiptOutcome = - | 'accepted' - | 'obstructed' - | 'conflicted' - | 'underdetermined' - | 'rejected'; - -export type WriteReceiptOptions = { +type WriteReceiptFields = { readonly timeline: string; readonly writer: string; readonly intent: Intent; - readonly outcome: ReceiptOutcome; - readonly patchSha: string; - readonly reason?: string; + readonly repairHints?: readonly RepairHint[]; }; -export const RECEIPT_OUTCOMES: ReadonlySet = new Set([ +export type WriteReceiptOptions = WriteReceiptFields & + ( + | { + readonly outcome: 'accepted'; + readonly patchSha: string; + readonly reason?: never; + } + | { + readonly outcome: Exclude; + readonly patchSha?: never; + readonly reason: string; + } + ); + +export const RECEIPT_OUTCOMES: ReadonlySet = new Set([ 'accepted', 'obstructed', 'conflicted', @@ -28,38 +35,69 @@ export const RECEIPT_OUTCOMES: ReadonlySet = new Set([ export default class WriteReceipt { readonly intent: Intent; - readonly outcome: ReceiptOutcome; - readonly patchSha: string; + readonly operation: 'write' = 'write'; + readonly outcome: WriteOutcome; + readonly patchSha: string | undefined; + readonly repairHints: readonly RepairHint[]; readonly reason: string | undefined; readonly timeline: string; readonly writer: string; constructor(options: WriteReceiptOptions | null | undefined) { const fields = requireWriteReceiptOptions(options); - requireNonEmptyString(fields.timeline, 'writeReceipt.timeline'); - requireNonEmptyString(fields.writer, 'writeReceipt.writer'); - requireNonEmptyString(fields.patchSha, 'writeReceipt.patchSha'); - if (!(fields.intent instanceof Intent)) { - throw new WarpError('WriteReceipt requires an Intent', 'E_WRITE_RECEIPT_INTENT'); - } - if (!RECEIPT_OUTCOMES.has(fields.outcome)) { - throw new WarpError('WriteReceipt outcome is unsupported', 'E_WRITE_RECEIPT_OUTCOME'); - } - if (fields.reason !== undefined) { - requireNonEmptyString(fields.reason, 'writeReceipt.reason'); - } + validateWriteReceiptFields(fields); this.timeline = fields.timeline; this.writer = fields.writer; this.intent = fields.intent; this.outcome = fields.outcome; this.patchSha = fields.patchSha; + this.repairHints = freezeRepairHints(fields.repairHints ?? []); this.reason = fields.reason; Object.freeze(this); } } -function requireWriteReceiptOptions(options: WriteReceiptOptions | null | undefined): WriteReceiptOptions { +function validateWriteReceiptFields(fields: WriteReceiptOptions): void { + requireNonEmptyString(fields.timeline, 'writeReceipt.timeline'); + requireNonEmptyString(fields.writer, 'writeReceipt.writer'); + validateIntent(fields.intent); + validateWriteOutcome(fields.outcome); + validateWriteSettlement(fields); +} + +function validateIntent(intent: Intent): void { + if (!(intent instanceof Intent)) { + throw new WarpError('WriteReceipt requires an Intent', 'E_WRITE_RECEIPT_INTENT'); + } +} + +function validateWriteOutcome(outcome: WriteOutcome): void { + if (!RECEIPT_OUTCOMES.has(outcome)) { + throw new WarpError('WriteReceipt outcome is unsupported', 'E_WRITE_RECEIPT_OUTCOME'); + } +} + +function validateWriteSettlement(fields: WriteReceiptOptions): void { + if (fields.outcome === 'accepted') { + requireNonEmptyString(fields.patchSha, 'writeReceipt.patchSha'); + if (fields.reason !== undefined) { + throw new WarpError('Accepted WriteReceipt cannot carry a reason', 'E_WRITE_RECEIPT_REASON'); + } + return; + } + requireNonEmptyString(fields.reason, 'writeReceipt.reason'); + if (fields.patchSha !== undefined) { + throw new WarpError( + 'Unaccepted WriteReceipt cannot carry a patch SHA', + 'E_WRITE_RECEIPT_PATCH' + ); + } +} + +function requireWriteReceiptOptions( + options: WriteReceiptOptions | null | undefined +): WriteReceiptOptions { if (options === null || options === undefined) { throw new WarpError('WriteReceipt options are required', 'E_WRITE_RECEIPT_OPTIONS'); } diff --git a/src/domain/api/WriteRuntime.ts b/src/domain/api/WriteRuntime.ts new file mode 100644 index 000000000..817ab8d5a --- /dev/null +++ b/src/domain/api/WriteRuntime.ts @@ -0,0 +1,79 @@ +import type { default as WarpWorldline, WarpWorldlinePatchBuild } from '../WarpWorldline.ts'; +import WarpError from '../errors/WarpError.ts'; +import type Intent from './Intent.ts'; +import { applyIntentToPatch } from './IntentRuntime.ts'; +import type { WriteOutcome } from './ReceiptOutcome.ts'; +import type { RepairHint } from './ReceiptSupport.ts'; +import WriteReceipt from './WriteReceipt.ts'; + +type IntentCommit = (build: WarpWorldlinePatchBuild) => Promise; + +type OperationalWriteFailure = { + readonly outcome: Exclude; + readonly reason: string; + readonly repairHints: readonly RepairHint[]; +}; + +const MATERIALIZE_HINT = Object.freeze([ + Object.freeze({ + code: 'materialize_write_basis', + message: 'Materialize the timeline before retrying this state-dependent intent.', + }), +]); +export async function executeIntentWrite( + runtime: WarpWorldline, + intent: Intent, + commit: IntentCommit +): Promise { + try { + const patchSha = await commit((patch) => { + applyIntentToPatch(intent, patch); + }); + return acceptedWriteReceipt(runtime, intent, patchSha); + } catch (error) { + if (!(error instanceof WarpError)) { + throw error; + } + const failure = operationalWriteFailure(error); + if (failure === null) { + throw error; + } + return new WriteReceipt({ + timeline: runtime.worldlineName, + writer: runtime.writerId, + intent, + ...failure, + }); + } +} + +function acceptedWriteReceipt( + runtime: WarpWorldline, + intent: Intent, + patchSha: string +): WriteReceipt { + return new WriteReceipt({ + timeline: runtime.worldlineName, + writer: runtime.writerId, + intent, + outcome: 'accepted', + patchSha, + }); +} + +function operationalWriteFailure(error: WarpError): OperationalWriteFailure | null { + if (error.code === 'E_PATCH_NO_STATE') { + return { + outcome: 'obstructed', + reason: 'missing_write_basis', + repairHints: MATERIALIZE_HINT, + }; + } + if (error.code === 'E_PATCH_DELETE_WITH_DATA') { + return { outcome: 'conflicted', reason: 'attached_data', repairHints: [] }; + } + if (error.code === 'E_PATCH_ENTITY_NOT_FOUND') { + return { outcome: 'rejected', reason: 'entity_not_found', repairHints: [] }; + } + return null; +} diff --git a/src/domain/api/openWarp.ts b/src/domain/api/openWarp.ts index 491f16865..50f34780b 100644 --- a/src/domain/api/openWarp.ts +++ b/src/domain/api/openWarp.ts @@ -7,10 +7,10 @@ import { createTimeline } from './TimelineRuntime.ts'; import WarpError from '../errors/WarpError.ts'; import { OPEN_WARP_IDENTITY_FAILURE } from './OpenWarpIdentityFailure.ts'; -export type WarpStorage = CorePersistence & Partial; +export type StorageAdapter = CorePersistence & Partial; export type OpenWarpOptions = { - readonly storage: WarpStorage; + readonly storage: StorageAdapter; readonly writer: string; }; diff --git a/src/domain/services/controllers/CheckpointController.ts b/src/domain/services/controllers/CheckpointController.ts index f7e4ac1ec..47ee7a6ad 100644 --- a/src/domain/services/controllers/CheckpointController.ts +++ b/src/domain/services/controllers/CheckpointController.ts @@ -8,6 +8,7 @@ */ import QueryError from '../../errors/QueryError.ts'; +import PersistenceError from '../../errors/PersistenceError.ts'; import { SchemaUnsupportedError } from '../../errors/index.ts'; import { buildWriterRef, buildCheckpointRef, buildCoverageRef } from '../../utils/RefLayout.ts'; import { createFrontier, updateFrontier, frontierFingerprint } from '../Frontier.ts'; @@ -367,8 +368,7 @@ export default class CheckpointController { } async _validateMigrationBoundary(): Promise { - const checkpoint = await this._loadLatestCheckpoint(); - if (isCurrentCheckpointSchema(checkpoint?.schema)) { return; } + if (await this._hasCurrentCheckpointSchema()) { return; } const hasSchema1History = await this._hasSchema1Patches(); if (hasSchema1History) { @@ -378,6 +378,33 @@ export default class CheckpointController { } } + private async _hasCurrentCheckpointSchema(): Promise { + const checkpointSha = await this._readCheckpointSha(); + if (typeof checkpointSha !== 'string' || checkpointSha.length === 0) { + return false; + } + const persistence = this._host._persistence; + this._assertLoadPersistence(persistence); + const message = await persistence.showNode(checkpointSha); + if (typeof message !== 'string' || message.length === 0) { + return false; + } + if (this._host._commitMessageCodec.detectKind(message) !== 'checkpoint') { + return false; + } + const checkpoint = this._host._commitMessageCodec.decodeCheckpoint(message); + if (isCurrentCheckpointSchema(checkpoint.schema)) { + return true; + } + throw new PersistenceError( + `Checkpoint ${checkpointSha} is schema:${checkpoint.schema}. ` + + `Only schema:${CURRENT_CHECKPOINT_SCHEMA} checkpoints are supported by the shipped runtime. ` + + 'Run `npm run upgrade -- --graph ` before loading this graph.', + 'E_CHECKPOINT_UNSUPPORTED_SCHEMA', + { context: { checkpointSha, schema: checkpoint.schema } }, + ); + } + async _hasSchema1Patches(): Promise { const h = this._host; const writerIds = await h.discoverWriters(); diff --git a/src/infrastructure/adapters/GitGraphAdapter.ts b/src/infrastructure/adapters/GitGraphAdapter.ts index ebb84d4d2..e0832d81c 100644 --- a/src/infrastructure/adapters/GitGraphAdapter.ts +++ b/src/infrastructure/adapters/GitGraphAdapter.ts @@ -48,7 +48,7 @@ import type OperationPolicyPort from '../../ports/OperationPolicyPort.ts'; import type { OperationPolicyExecuteOptions } from '../../ports/OperationPolicyPort.ts'; export type { GitPlumbing, GitError, CollectableStream } from './gitErrorClassification.ts'; -interface GitGraphAdapterOptions { +export interface GitGraphAdapterOptions { readonly plumbing: GitPlumbing; readonly retryOptions?: Partial; readonly policy?: OperationPolicyPort; diff --git a/storage.ts b/storage.ts index 837140034..072633d86 100644 --- a/storage.ts +++ b/storage.ts @@ -1,23 +1,21 @@ /** * Storage adapters for public git-warp applications. * - * These aliases intentionally avoid exposing graph-shaped adapter names in the - * first-use API. The graph-named adapters remain only in deprecated - * migration-only compatibility surfaces. + * The package root stays storage-agnostic; applications opt into one of these + * adapters explicitly. */ -export { default as GitStorageAdapter } from './src/infrastructure/adapters/GitGraphAdapter.ts'; -export { default as MemoryStorageAdapter } from './src/infrastructure/adapters/InMemoryGraphAdapter.ts'; -export { default as NodeCryptoAdapter } from './src/infrastructure/adapters/NodeCryptoAdapter.ts'; -export { default as WebCryptoAdapter } from './src/infrastructure/adapters/WebCryptoAdapter.ts'; -export { default as CasContentEncryptionPolicy } from './src/infrastructure/adapters/CasContentEncryptionPolicy.ts'; -export type { - CasContentEncryptionDiagnostics, - CasContentEncryptionScheme, - CasResolvedVaultKeyOptions, -} from './src/infrastructure/adapters/CasContentEncryptionPolicy.ts'; -export type { - CollectableStream, - GitError, - GitPlumbing, +import GitGraphAdapter, { + type GitGraphAdapterOptions, } from './src/infrastructure/adapters/GitGraphAdapter.ts'; +import InMemoryGraphAdapter from './src/infrastructure/adapters/InMemoryGraphAdapter.ts'; + +export type GitStorageAdapterOptions = GitGraphAdapterOptions; + +export class GitStorageAdapter extends GitGraphAdapter { + constructor(options: GitStorageAdapterOptions) { + super(options); + } +} + +export class MemoryStorageAdapter extends InMemoryGraphAdapter {} diff --git a/test/conformance/snapshotPublicApiSurface.test.ts b/test/conformance/immutableSnapshotSurface.test.ts similarity index 97% rename from test/conformance/snapshotPublicApiSurface.test.ts rename to test/conformance/immutableSnapshotSurface.test.ts index 81585caf3..2fde5aeaf 100644 --- a/test/conformance/snapshotPublicApiSurface.test.ts +++ b/test/conformance/immutableSnapshotSurface.test.ts @@ -35,7 +35,7 @@ async function assertSnapshotPublicApiSurface(graph: WarpCore): Promise { void snapshotValue; } -describe('snapshot public API surface', () => { +describe('immutable snapshot surface', () => { it('exports runtime snapshot classes from the package root', () => { expect(ImmutableBytes).toBeTypeOf('function'); expect(SnapshotORSet).toBeTypeOf('function'); diff --git a/test/conformance/v18FirstUseOpticsHonesty.test.ts b/test/conformance/v18FirstUseOpticsHonesty.test.ts index a80b47ff2..81ed02143 100644 --- a/test/conformance/v18FirstUseOpticsHonesty.test.ts +++ b/test/conformance/v18FirstUseOpticsHonesty.test.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import { openWarp, reading } from '../../index.ts'; import { openWarpWorldline } from '../../src/domain/WarpWorldline.ts'; import { openRuntimeHostProduct } from '../../src/domain/warp/RuntimeHostProduct.ts'; import InMemoryGraphAdapter from '../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; @@ -161,6 +162,15 @@ describe('v18 first-use Optics honesty gate', () => { commitMessageCodec: runtime._commitMessageCodec, }); + const warp = await openWarp({ storage: persistence, writer: 'app' }); + const timeline = await warp.timeline('v18-first-use-optics-honesty'); + const property = await timeline.read( + reading.property({ + subject: NODE_ID, + key: PROPERTY_KEY, + }) + ); + persistence.forbidTreeOidMapReads(); const basis = await events.prepareOpticBasis(); persistence.allowTreeOidMapReads(); @@ -170,6 +180,11 @@ describe('v18 first-use Optics honesty gate', () => { expect(basis.checkpointSha).toBe(checkpointSha); expect(coordinate.checkpointSha).toBe(checkpointSha); expect(node).toMatchObject({ nodeId: NODE_ID, alive: true }); + expect(property.value).toBe('open'); + expect(property.receipt).toMatchObject({ + outcome: 'resolved', + evidence: { checkpointSha }, + }); expect(persistence.forbiddenOperations()).toEqual([]); }); diff --git a/test/type-check/v19-consumer.ts b/test/type-check/v19-consumer.ts index 29fcca505..571633d86 100644 --- a/test/type-check/v19-consumer.ts +++ b/test/type-check/v19-consumer.ts @@ -5,31 +5,38 @@ */ import { - DraftTimeline, intent, - Intent, - JoinReceipt, - JoinResult, openWarp, reading, - Reading, - ReadingResult, - ReadReceipt, - Timeline, - Warp, - WriteReceipt, - type OpenWarpOptions, + type DraftTimeline, + type Intent, + type JoinOutcome, + type JoinReceipt, + type JoinResult, type JoinOptions, type JoinPolicy, - type ReadReceiptOutcome, + type NeighborhoodReadingFields, + type OpenWarpOptions, + type ReadEvidence, + type ReadOutcome, + type Reading, + type ReadingResult, type ReadingValue, + type ReadReceipt, type ReceiptOutcome, - type WarpStorage, + type RepairHint, + type Tick, + type Timeline, + type TimelineView, + type Warp, + type StorageAdapter, + type WriteReceipt, + type WriteOutcome, } from '../../index.ts'; import { MemoryStorageAdapter } from '../../storage.ts'; const storage = new MemoryStorageAdapter(); -const publicStorage: WarpStorage = storage; +const publicStorage: StorageAdapter = storage; const options: OpenWarpOptions = { storage: publicStorage, @@ -40,21 +47,34 @@ const warp: Warp = await openWarp(options); const timeline: Timeline = await warp.timeline('events'); const timelineName: string = timeline.name; const timelineWriter: string = timeline.writer; +const tick: Tick = await timeline.tick(); +const historical: TimelineView = timeline.at(tick); const writeIntent: Intent = intent.property.set({ subject: 'user:alice', key: 'role', value: 'admin', }); const receipt: WriteReceipt = await timeline.write(writeIntent); -const outcome: ReceiptOutcome = receipt.outcome; +const outcome: WriteOutcome = receipt.outcome; const readRequest: Reading = reading.property({ subject: 'user:alice', key: 'role', }); +const neighborhoodFields: NeighborhoodReadingFields = { + subject: 'user:alice', + direction: 'out', + labels: ['memberOf'], + limit: 25, +}; +const neighborhoodRequest: Reading = reading.neighborhood(neighborhoodFields); const readResult: ReadingResult = await timeline.read(readRequest); +const historicalResult: ReadingResult = await historical.read(readRequest); +const convenienceValue: ReadingValue = await timeline.readValue(readRequest); const readValue: ReadingValue = readResult.value; const readReceipt: ReadReceipt = readResult.receipt; -const readOutcome: ReadReceiptOutcome = readReceipt.outcome; +const readOutcome: ReadOutcome = readReceipt.outcome; +const readEvidence: ReadEvidence | undefined = readReceipt.evidence; +const repairHints: readonly RepairHint[] = readReceipt.repairHints; const draft: DraftTimeline = await timeline.draft('try-admin-role'); const draftReceipt: WriteReceipt = await draft.write(writeIntent); const joinPolicy: JoinPolicy = 'deterministic'; @@ -62,12 +82,13 @@ const joinOptions: JoinOptions = { policy: joinPolicy }; const preview: JoinResult = await timeline.previewJoin(draft, joinOptions); const joined: JoinResult = await timeline.join(draft); const joinReceipt: JoinReceipt = joined.receipt; +const joinOutcome: JoinOutcome = joinReceipt.outcome; // @ts-expect-error receipt outcomes do not expose operation names. const operationOutcome: ReceiptOutcome = 'write'; // @ts-expect-error read receipt outcomes do not expose operation names. -const readOperationOutcome: ReadReceiptOutcome = 'read'; +const readOperationOutcome: ReadOutcome = 'read'; // @ts-expect-error previewJoin is a dedicated method, not a dryRun boolean trap. await timeline.previewJoin(draft, { dryRun: true }); @@ -83,11 +104,17 @@ timeline.commit; void timelineName; void timelineWriter; +void historicalResult; void outcome; void operationOutcome; void readValue; +void convenienceValue; +void neighborhoodRequest; void readOutcome; +void readEvidence; void readOperationOutcome; +void repairHints; void draftReceipt; void preview; void joinReceipt; +void joinOutcome; diff --git a/test/type-check/v19-subpaths.ts b/test/type-check/v19-subpaths.ts index 07a311f6a..eeb7b1685 100644 --- a/test/type-check/v19-subpaths.ts +++ b/test/type-check/v19-subpaths.ts @@ -5,27 +5,29 @@ * named expert surfaces. */ -import { GitStorageAdapter, MemoryStorageAdapter, NodeCryptoAdapter } from '../../storage.ts'; -import { GitWarpTickHologram, Observer, Optic, ProjectionHandle } from '../../advanced.ts'; -import { QueryBuilder, TtdMergeInspector, normalizeVisibleStateScope } from '../../diagnostics.ts'; +import { + GitStorageAdapter, + MemoryStorageAdapter, + type GitStorageAdapterOptions, +} from '../../storage.ts'; +import { type Receipt, type Timeline } from '../../index.ts'; +import { Coordinate, Optic, type Witness } from '../../advanced.ts'; +import { inspectReceipt, type ReceiptInspection } from '../../diagnostics.ts'; -const storageAdapter: typeof MemoryStorageAdapter = MemoryStorageAdapter; -const gitStorageAdapter: typeof GitStorageAdapter = GitStorageAdapter; -const nodeCryptoAdapter: typeof NodeCryptoAdapter = NodeCryptoAdapter; -const observer: typeof Observer = Observer; -const optic: typeof Optic = Optic; -const projectionHandle: typeof ProjectionHandle = ProjectionHandle; -const tickHologram: typeof GitWarpTickHologram = GitWarpTickHologram; -const queryBuilder: typeof QueryBuilder = QueryBuilder; -const ttdMergeInspector: typeof TtdMergeInspector = TtdMergeInspector; +declare const gitStorageOptions: GitStorageAdapterOptions; + +const storageAdapter = new MemoryStorageAdapter(); +const gitStorageAdapter = new GitStorageAdapter(gitStorageOptions); +declare const timeline: Timeline; +const coordinate: InstanceType = await timeline.coordinate(); +const optic: InstanceType = coordinate.optic(); +const node = await optic.node('user:alice').read(); +const witness: Witness = node.readIdentity; +declare const receipt: Receipt; +const inspection: ReceiptInspection = inspectReceipt(receipt); void storageAdapter; void gitStorageAdapter; -void nodeCryptoAdapter; -void observer; void optic; -void projectionHandle; -void tickHologram; -void queryBuilder; -void ttdMergeInspector; -void normalizeVisibleStateScope; +void witness; +void inspection; diff --git a/test/unit/domain/ReceiptDiagnostics.test.ts b/test/unit/domain/ReceiptDiagnostics.test.ts new file mode 100644 index 000000000..d1ad66b72 --- /dev/null +++ b/test/unit/domain/ReceiptDiagnostics.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; + +import { inspectReceipt } from '../../../diagnostics.ts'; +import { intent } from '../../../src/domain/api/IntentBuilders.ts'; +import WriteReceipt from '../../../src/domain/api/WriteReceipt.ts'; + +describe('receipt diagnostics', () => { + it('inspects accepted write receipts without requiring substrate handles', () => { + const receipt = new WriteReceipt({ + timeline: 'events', + writer: 'agent-1', + intent: intent.node.add({ subject: 'user:alice' }), + outcome: 'accepted', + patchSha: 'patch-1', + }); + + expect(inspectReceipt(receipt)).toEqual({ + operation: 'write', + outcome: 'accepted', + timeline: 'events', + writer: 'agent-1', + reason: undefined, + evidence: 'absent', + objectIds: ['patch-1'], + }); + }); + + it('does not invent object identities for rejected writes', () => { + const receipt = new WriteReceipt({ + timeline: 'events', + writer: 'agent-1', + intent: intent.node.add({ subject: 'user:alice' }), + outcome: 'rejected', + reason: 'policy_rejected', + }); + + expect(inspectReceipt(receipt).objectIds).toEqual([]); + }); +}); diff --git a/test/unit/domain/ReceiptOutcome.test.ts b/test/unit/domain/ReceiptOutcome.test.ts index ddd837458..d2a8a0e93 100644 --- a/test/unit/domain/ReceiptOutcome.test.ts +++ b/test/unit/domain/ReceiptOutcome.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from 'vitest'; import DraftTimeline from '../../../src/domain/api/DraftTimeline.ts'; +import { intent } from '../../../src/domain/api/IntentBuilders.ts'; import JoinReceipt from '../../../src/domain/api/JoinReceipt.ts'; -import { RECEIPT_OUTCOMES } from '../../../src/domain/api/WriteReceipt.ts'; +import WriteReceipt, { RECEIPT_OUTCOMES } from '../../../src/domain/api/WriteReceipt.ts'; describe('receipt outcomes', () => { it('shares the canonical write receipt outcomes with join receipts', () => { @@ -19,9 +20,28 @@ describe('receipt outcomes', () => { draft, mode: 'join', outcome, + ...(outcome === 'accepted' ? {} : { reason: `join_${outcome}` }), }); expect(receipt.outcome).toBe(outcome); + expect(receipt.operation).toBe('join'); } }); + + it('represents rejected writes without inventing patch identities', () => { + const receipt = new WriteReceipt({ + timeline: 'events', + writer: 'agent-1', + intent: intent.node.add({ subject: 'user:alice' }), + outcome: 'rejected', + reason: 'policy_rejected', + }); + + expect(receipt).toMatchObject({ + operation: 'write', + outcome: 'rejected', + reason: 'policy_rejected', + patchSha: undefined, + }); + }); }); diff --git a/test/unit/domain/WarpFacade.test.ts b/test/unit/domain/WarpFacade.test.ts index 5a5434030..7ea132b7c 100644 --- a/test/unit/domain/WarpFacade.test.ts +++ b/test/unit/domain/WarpFacade.test.ts @@ -2,21 +2,18 @@ import { readFileSync } from 'node:fs'; import ts from 'typescript'; import { describe, expect, it } from 'vitest'; -import { - DraftTimeline, - intent, - JoinReceipt, - JoinResult, - openWarp, - reading, - ReadReceipt, - ReadingResult, - Timeline, - Warp, -} from '../../../index.ts'; +import { intent, openWarp, reading } from '../../../index.ts'; +import DraftTimeline from '../../../src/domain/api/DraftTimeline.ts'; +import JoinReceipt from '../../../src/domain/api/JoinReceipt.ts'; +import JoinResult from '../../../src/domain/api/JoinResult.ts'; import { OPEN_WARP_IDENTITY_FAILURE } from '../../../src/domain/api/OpenWarpIdentityFailure.ts'; +import ReadReceipt from '../../../src/domain/api/ReadReceipt.ts'; +import ReadingResult from '../../../src/domain/api/ReadingResult.ts'; +import Timeline from '../../../src/domain/api/Timeline.ts'; import { requireTimelineRuntime } from '../../../src/domain/api/TimelineRuntime.ts'; +import Warp from '../../../src/domain/api/Warp.ts'; import { MAX_WRITER_ID_LENGTH } from '../../../src/domain/utils/RefLayout.ts'; +import { openRuntimeHostProduct } from '../../../src/domain/warp/RuntimeHostProduct.ts'; import { MemoryStorageAdapter } from '../../../storage.ts'; const FORBIDDEN_ROOT_SUBSTRATE_EXPORTS = Object.freeze([ @@ -136,6 +133,19 @@ function collectExportedDeclarationName(statement: ts.Statement, exportedNames: } } +async function createBoundedReadBasis( + storage: InstanceType, + graphName: string +): Promise { + const runtime = await openRuntimeHostProduct({ + persistence: storage, + graphName, + writerId: 'agent-1', + }); + await runtime.materialize(); + await runtime.createCheckpoint(); +} + describe('v19 Warp facade', () => { it('opens named timelines through root application nouns', async () => { const warp = await openWarp({ @@ -272,6 +282,7 @@ describe('v19 Warp facade', () => { ); expect(nodeReceipt.outcome).toBe('accepted'); + expect(nodeReceipt.operation).toBe('write'); expect(nodeReceipt.intent.kind).toBe('node.add'); expect(typeof nodeReceipt.patchSha).toBe('string'); expect(propertyReceipt.outcome).toBe('accepted'); @@ -293,13 +304,34 @@ describe('v19 Warp facade', () => { expect(result.nodes).toEqual([{ id: 'user:alice', props: { role: 'admin' } }]); }); - it('reads public readings and returns resolved read receipts', async () => { + it('returns operational write failures as receipts', async () => { const warp = await openWarp({ storage: new MemoryStorageAdapter(), writer: 'agent-1', }); const timeline = await warp.timeline('events'); + const receipt = await timeline.write(intent.node.remove({ subject: 'user:alice' })); + + expect(receipt).toMatchObject({ + operation: 'write', + outcome: 'obstructed', + patchSha: undefined, + reason: 'missing_write_basis', + }); + expect(receipt.repairHints).toEqual([ + expect.objectContaining({ code: 'materialize_write_basis' }), + ]); + }); + + it('reads public readings and returns resolved read receipts', async () => { + const storage = new MemoryStorageAdapter(); + const warp = await openWarp({ + storage, + writer: 'agent-1', + }); + const timeline = await warp.timeline('events'); + await timeline.write(intent.node.add({ subject: 'user:alice' })); await timeline.write( intent.property.set({ @@ -308,6 +340,15 @@ describe('v19 Warp facade', () => { value: 'admin', }) ); + await timeline.write(intent.node.add({ subject: 'team:ops' })); + await timeline.write( + intent.edge.add({ + from: 'user:alice', + to: 'team:ops', + label: 'memberOf', + }) + ); + await createBoundedReadBasis(storage, timeline.name); const propertyResult = await timeline.read( reading.property({ @@ -320,29 +361,110 @@ describe('v19 Warp facade', () => { subject: 'user:alice', }) ); + const neighborhoodResult = await timeline.read( + reading.neighborhood({ + subject: 'user:alice', + direction: 'out', + labels: ['memberOf'], + }) + ); expect(propertyResult).toBeInstanceOf(ReadingResult); expect(propertyResult.receipt).toBeInstanceOf(ReadReceipt); expect(propertyResult.value).toBe('admin'); expect(propertyResult.receipt.outcome).toBe('resolved'); + expect(propertyResult.receipt.operation).toBe('read'); expect(propertyResult.receipt.reading.kind).toBe('property.get'); expect(propertyResult.receipt.timeline).toBe('events'); expect(propertyResult.receipt.writer).toBe('agent-1'); + expect(propertyResult.receipt.evidence).toMatchObject({ + kind: 'checkpoint-tail-read', + worldline: 'events', + }); expect(existsResult).toBeInstanceOf(ReadingResult); expect(existsResult.value).toBe(true); expect(existsResult.receipt.outcome).toBe('resolved'); expect(existsResult.receipt.reading.kind).toBe('node.exists'); + expect(neighborhoodResult.value).toMatchObject({ + subject: 'user:alice', + direction: 'out', + completeness: 'complete', + edges: [{ direction: 'out', neighborId: 'team:ops', label: 'memberOf' }], + }); + expect(neighborhoodResult.receipt).toMatchObject({ + outcome: 'resolved', + evidence: { kind: 'checkpoint-tail-read' }, + }); + const coordinate = await timeline.coordinate(); + const coordinateRole = await coordinate.optic().node('user:alice').prop('role').read(); + expect(coordinateRole.value).toBe('admin'); + await expect( + timeline.readValue( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ) + ).resolves.toBe('admin'); + + const tick = await timeline.tick(); + await timeline.write( + intent.property.set({ + subject: 'user:alice', + key: 'role', + value: 'owner', + }) + ); + const currentRole = await timeline.readValue( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ); + const historicalRole = await timeline.at(tick).readValue( + reading.property({ + subject: 'user:alice', + key: 'role', + }) + ); + expect(currentRole).toBe('owner'); + expect(historicalRole).toBe('admin'); }); - it('drafts speculative writes, previews joins, and joins with receipts', async () => { + it('returns an obstructed receipt instead of materializing a missing read basis', async () => { const warp = await openWarp({ storage: new MemoryStorageAdapter(), writer: 'agent-1', }); const timeline = await warp.timeline('events'); + const result = await timeline.read(reading.node.exists({ subject: 'user:alice' })); + + expect(result.value).toBeNull(); + expect(result.receipt).toMatchObject({ + outcome: 'obstructed', + reason: 'missing_bounded_basis', + }); + expect(result.receipt.evidence).toBeUndefined(); + expect(result.receipt.repairHints).toEqual([ + expect.objectContaining({ code: 'repair_bounded_basis' }), + ]); + await expect( + timeline.readValue(reading.node.exists({ subject: 'user:alice' })) + ).rejects.toMatchObject({ code: 'E_TIMELINE_READ_UNRESOLVED' }); + }); + + it('drafts speculative writes, previews joins, and joins with receipts', async () => { + const storage = new MemoryStorageAdapter(); + const warp = await openWarp({ + storage, + writer: 'agent-1', + }); + const timeline = await warp.timeline('events'); + await timeline.write(intent.node.add({ subject: 'user:alice' })); + await createBoundedReadBasis(storage, timeline.name); const draft = await timeline.draft('try-admin-role'); const draftWrite = await draft.write( diff --git a/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts b/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts index f860d055d..2681022f7 100644 --- a/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts +++ b/test/unit/domain/continuum/GitWarpReceiptEnvelopeBoundary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { GitWarpReceiptEnvelopeBoundary } from '../../../../advanced.ts'; +import GitWarpReceiptEnvelopeBoundary from '../../../../src/domain/continuum/GitWarpReceiptEnvelopeBoundary.ts'; import WarpError from '../../../../src/domain/errors/WarpError.ts'; import { TickReceipt } from '../../../../src/domain/types/TickReceipt.ts'; diff --git a/test/unit/domain/diagnostics.exports.test.ts b/test/unit/domain/diagnostics.exports.test.ts deleted file mode 100644 index 13fb4164b..000000000 --- a/test/unit/domain/diagnostics.exports.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Tests for the diagnostics inspection exports. - * - * Verifies that materialized-state inspection helpers have an explicit - * diagnostics home instead of forcing operators through legacy. - */ - -import { describe, expect, it } from 'vitest'; - -import ORSet from '../../../src/domain/crdt/ORSet.ts'; -import VersionVector from '../../../src/domain/crdt/VersionVector.ts'; -import WarpState from '../../../src/domain/services/state/WarpState.ts'; -import { - normalizeVisibleStateScope, - nodeIdInVisibleStateScope, - scopeMaterializedState, -} from '../../../diagnostics.ts'; - -function emptyWarpState(): WarpState { - return new WarpState({ - nodeAlive: ORSet.empty(), - edgeAlive: ORSet.empty(), - prop: new Map(), - observedFrontier: VersionVector.empty(), - edgeBirthEvent: new Map(), - }); -} - -describe('diagnostics.ts exports', () => { - it('exports visible-state scope helpers for materialization inspection', () => { - const scope = normalizeVisibleStateScope({ - nodeIdPrefixes: { - exclude: ['task:archived:'], - include: ['task:'], - }, - }); - - expect(scope).toEqual({ - nodeIdPrefixes: { - exclude: ['task:archived:'], - include: ['task:'], - }, - }); - expect(nodeIdInVisibleStateScope('task:1', scope)).toBe(true); - expect(nodeIdInVisibleStateScope('task:archived:1', scope)).toBe(false); - - const state = emptyWarpState(); - - expect(scopeMaterializedState(state, null)).toBe(state); - }); -}); diff --git a/test/unit/domain/publicReadingSurface.behavior.test.ts b/test/unit/domain/internalReadingSurface.behavior.test.ts similarity index 98% rename from test/unit/domain/publicReadingSurface.behavior.test.ts rename to test/unit/domain/internalReadingSurface.behavior.test.ts index 86c77b599..d35b39886 100644 --- a/test/unit/domain/publicReadingSurface.behavior.test.ts +++ b/test/unit/domain/internalReadingSurface.behavior.test.ts @@ -21,7 +21,7 @@ function openOptions( }; } -describe('public reading surfaces', () => { +describe('internal reading surfaces', () => { it('opens WarpGraph as a capability bag without public materialization or runtime escapes', async () => { const graph = await openWarpGraph(openOptions('public-reading-graph', 'writer-graph')); diff --git a/test/unit/domain/services/controllers/CheckpointController.test.ts b/test/unit/domain/services/controllers/CheckpointController.test.ts index 9687a9d3f..44205cc8a 100644 --- a/test/unit/domain/services/controllers/CheckpointController.test.ts +++ b/test/unit/domain/services/controllers/CheckpointController.test.ts @@ -134,6 +134,7 @@ function createMockHost(overrides = {}) { updateRef: vi.fn().mockResolvedValue(undefined), commitNode: vi.fn().mockResolvedValue('anchor-sha'), getNodeInfo: vi.fn().mockResolvedValue({ message: 'msg', parents: [] }), + showNode: vi.fn().mockResolvedValue('msg'), }, _cachedState: null, _stateDirty: false, @@ -145,6 +146,7 @@ function createMockHost(overrides = {}) { _codec: { decode: vi.fn() }, _commitMessageCodec: { detectKind: detectMessageKindMock, + decodeCheckpoint: vi.fn(), decodePatch: vi.fn((message: string) => { const decoded = decodePatchMessageMock(message) as { storage?: { strategy: string; version: string | null; schema: string | null; encrypted: boolean }; @@ -436,7 +438,8 @@ describe('CheckpointController', () => { describe('_validateMigrationBoundary', () => { it('passes when checkpoint has v5 schema', async () => { ((host['_persistence'] as any).readRef as any).mockResolvedValue('cp-sha'); - loadCheckpointMock.mockResolvedValue({ state: stubState(), frontier: new Map(), stateHash: 'h', schema: 5, appliedVV: null, indexShardOids: null }); + detectMessageKindMock.mockReturnValue('checkpoint'); + ((host['_commitMessageCodec'] as any).decodeCheckpoint as any).mockReturnValue({ schema: 5 }); isCurrentCheckpointSchemaMock.mockReturnValue(true); await expect(ctrl._validateMigrationBoundary()).resolves.toBeUndefined(); diff --git a/test/unit/domain/services/merge/TtdMergeInspector.test.ts b/test/unit/domain/services/merge/TtdMergeInspector.test.ts index 45f47cb77..3c18a921b 100644 --- a/test/unit/domain/services/merge/TtdMergeInspector.test.ts +++ b/test/unit/domain/services/merge/TtdMergeInspector.test.ts @@ -1,13 +1,11 @@ import { assert, describe, expect, it } from 'vitest'; -import { - TtdMergeBranch, - TtdMergeFootprint, - TtdMergeInspector, - TtdMergeLoweringWitness, - TtdMergeObstructionWitness, - TtdMergePolicyRequirement, -} from '../../../../../diagnostics.ts'; +import TtdMergeBranch from '../../../../../src/domain/services/merge/TtdMergeBranch.ts'; +import TtdMergeFootprint from '../../../../../src/domain/services/merge/TtdMergeFootprint.ts'; +import TtdMergeInspector from '../../../../../src/domain/services/merge/TtdMergeInspector.ts'; +import TtdMergeLoweringWitness from '../../../../../src/domain/services/merge/TtdMergeLoweringWitness.ts'; +import TtdMergeObstructionWitness from '../../../../../src/domain/services/merge/TtdMergeObstructionWitness.ts'; +import TtdMergePolicyRequirement from '../../../../../src/domain/services/merge/TtdMergePolicyRequirement.ts'; function itemAt(items: readonly T[], index: number): T { const item = items[index]; diff --git a/test/unit/domain/services/query/BoundedSupportRule.test.ts b/test/unit/domain/services/query/BoundedSupportRule.test.ts index f0997322d..51b8c5bd8 100644 --- a/test/unit/domain/services/query/BoundedSupportRule.test.ts +++ b/test/unit/domain/services/query/BoundedSupportRule.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { BoundedSupportRule } from '../../../../../advanced.ts'; -import { QueryBuilder } from '../../../../../diagnostics.ts'; import QueryError from '../../../../../src/domain/errors/QueryError.ts'; +import BoundedSupportRule from '../../../../../src/domain/services/query/BoundedSupportRule.ts'; +import QueryBuilder from '../../../../../src/domain/services/query/QueryBuilder.ts'; import type { QueryNeighborEntry, QueryReadModel, diff --git a/test/unit/domain/services/query/CausalIndexPlan.test.ts b/test/unit/domain/services/query/CausalIndexPlan.test.ts index 1856d875a..d1f73b687 100644 --- a/test/unit/domain/services/query/CausalIndexPlan.test.ts +++ b/test/unit/domain/services/query/CausalIndexPlan.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { BoundedSupportRule, CausalIndexPlan } from '../../../../../advanced.ts'; import QueryError from '../../../../../src/domain/errors/QueryError.ts'; +import BoundedSupportRule from '../../../../../src/domain/services/query/BoundedSupportRule.ts'; +import CausalIndexPlan from '../../../../../src/domain/services/query/CausalIndexPlan.ts'; describe('CausalIndexPlan', () => { it('maps entity support to the provenance entity-patch index family', () => { diff --git a/test/unit/domain/services/query/SupportFragmentPlan.test.ts b/test/unit/domain/services/query/SupportFragmentPlan.test.ts index 8d689acbe..5bc3e2245 100644 --- a/test/unit/domain/services/query/SupportFragmentPlan.test.ts +++ b/test/unit/domain/services/query/SupportFragmentPlan.test.ts @@ -1,12 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { - BoundedSupportRule, - CausalIndexPlan, - SupportFragmentPlan, -} from '../../../../../advanced.ts'; -import { QueryBuilder } from '../../../../../diagnostics.ts'; import QueryError from '../../../../../src/domain/errors/QueryError.ts'; +import BoundedSupportRule from '../../../../../src/domain/services/query/BoundedSupportRule.ts'; +import CausalIndexPlan from '../../../../../src/domain/services/query/CausalIndexPlan.ts'; +import QueryBuilder from '../../../../../src/domain/services/query/QueryBuilder.ts'; +import SupportFragmentPlan from '../../../../../src/domain/services/query/SupportFragmentPlan.ts'; import type { QueryNeighborEntry, QueryReadModel, diff --git a/test/unit/domain/publicStrandAndRuntimeSeams.test.ts b/test/unit/domain/strandAndRuntimeSeams.test.ts similarity index 99% rename from test/unit/domain/publicStrandAndRuntimeSeams.test.ts rename to test/unit/domain/strandAndRuntimeSeams.test.ts index c3b150987..13be90ead 100644 --- a/test/unit/domain/publicStrandAndRuntimeSeams.test.ts +++ b/test/unit/domain/strandAndRuntimeSeams.test.ts @@ -31,7 +31,7 @@ import type { import type { IndexShard } from '../../../src/domain/artifacts/IndexShard.ts'; import type CodecValue from '../../../src/domain/types/codec/CodecValue.ts'; -describe('public strand and runtime host seams', () => { +describe('strand and runtime host seams', () => { it('uses StrandError as the public speculative-lane error noun', () => { const error = new StrandError('invalid strand id', { code: 'E_STRAND_ID_INVALID', diff --git a/test/unit/domain/publicApiExecutableExamples.test.ts b/test/unit/domain/worldlineExecutableExamples.test.ts similarity index 96% rename from test/unit/domain/publicApiExecutableExamples.test.ts rename to test/unit/domain/worldlineExecutableExamples.test.ts index 8565b06c6..fdb01bffa 100644 --- a/test/unit/domain/publicApiExecutableExamples.test.ts +++ b/test/unit/domain/worldlineExecutableExamples.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { Observer, type Aperture } from '../../../advanced.ts'; import WarpApp from '../../../src/domain/WarpApp.ts'; +import Observer from '../../../src/domain/services/query/Observer.ts'; +import type { Aperture } from '../../../src/domain/types/Aperture.ts'; import { openWarpGraph } from '../../../src/domain/WarpGraph.ts'; import WarpWorldline, { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; @@ -46,7 +47,7 @@ async function seedGuideGraph(events: WarpWorldline): Promise { }); } -describe('public API executable examples', () => { +describe('internal worldline executable examples', () => { it('opens the internal graph composition root', async () => { const graph = await openWarpGraph({ persistence: new InMemoryGraphAdapter(), @@ -201,7 +202,7 @@ describe('public API executable examples', () => { await expect(historicalObserver.hasNode('user:bob')).resolves.toBe(false); }); - it('signals public read costs through runtime capability posture objects', async () => { + it('signals worldline read costs through runtime capability posture objects', async () => { const events = await openEventsWorldline(); const report = events.capabilities(); @@ -219,7 +220,7 @@ describe('public API executable examples', () => { expect(report.requireCapability('legacy-query-arrays').posture.toString()).toBe('legacy'); }); - it('uses migrated in-memory test fixtures through the public worldline handle', async () => { + it('uses migrated in-memory test fixtures through the worldline handle', async () => { const repo = createInMemoryRepo(); try { const events = await openWarpWorldline({ diff --git a/test/unit/domain/publicReadApiExecutablePaths.test.ts b/test/unit/domain/worldlineReadExecutablePaths.test.ts similarity index 97% rename from test/unit/domain/publicReadApiExecutablePaths.test.ts rename to test/unit/domain/worldlineReadExecutablePaths.test.ts index 213c5d6b7..7ed4fe9ba 100644 --- a/test/unit/domain/publicReadApiExecutablePaths.test.ts +++ b/test/unit/domain/worldlineReadExecutablePaths.test.ts @@ -4,7 +4,7 @@ import { openWarpWorldline } from '../../../src/domain/WarpWorldline.ts'; import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; import { openRuntimeHostProduct } from '../../../src/domain/warp/RuntimeHostProduct.ts'; -describe('public read API executable paths', () => { +describe('worldline read executable paths', () => { it('reads live, historical, and observer-filtered worldlines through public handles', async () => { const audit = await openWarpWorldline({ persistence: new InMemoryGraphAdapter(), diff --git a/test/unit/scripts/non-ts-tail-shape.test.ts b/test/unit/scripts/non-ts-tail-shape.test.ts index ca5e0d414..8702ed3b6 100644 --- a/test/unit/scripts/non-ts-tail-shape.test.ts +++ b/test/unit/scripts/non-ts-tail-shape.test.ts @@ -41,13 +41,9 @@ describe('non-TS tail shape', () => { expect(existsSync(`${repoRoot}vitest.config.js`)).toBe(false); }); - it('keeps the sha1sync export honest without a standalone declaration file', () => { - expect(packageJson.exports['./sha1sync']).toEqual({ - types: './dist/src/infrastructure/adapters/sha1sync.d.ts', - import: './dist/src/infrastructure/adapters/sha1sync.js', - default: './dist/src/infrastructure/adapters/sha1sync.js', - }); - expect(jsrJson.exports['./sha1sync']).toBe('./src/infrastructure/adapters/sha1sync.ts'); + it('keeps sha1sync internal without a standalone declaration file', () => { + expect('./sha1sync' in packageJson.exports).toBe(false); + expect('./sha1sync' in jsrJson.exports).toBe(false); expect(trackedNonTypeScriptTail()).not.toContain('src/infrastructure/adapters/sha1sync.d.ts'); }); diff --git a/test/unit/scripts/public-api-aperture-noun.test.ts b/test/unit/scripts/public-api-aperture-noun.test.ts deleted file mode 100644 index 74ae43016..000000000 --- a/test/unit/scripts/public-api-aperture-noun.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { Observer } from '../../../advanced.ts'; -import { computeTranslationCost } from '../../../src/domain/services/TranslationCost.ts'; -import type { Aperture, ObserverConfig } from '../../../advanced.ts'; - -function acceptAperture(config: Aperture): ObserverConfig { - return config; -} - -describe('Aperture is a first-class public noun', () => { - it('exports runtime observer surfaces used by Aperture consumers', () => { - expect(Observer).toBeDefined(); - expect(typeof computeTranslationCost).toBe('function'); - }); - - it('exports Aperture and keeps ObserverConfig assignable as a compatibility alias', () => { - const aperture = { - match: 'user:*', - expose: ['name'], - redact: ['secret'], - } satisfies Aperture; - - const observerConfig: ObserverConfig = acceptAperture(aperture); - - expect(observerConfig.match).toBe('user:*'); - expect(observerConfig.expose).toEqual(['name']); - expect(observerConfig.redact).toEqual(['secret']); - }); -}); diff --git a/test/unit/scripts/v19-public-api-boundary.test.ts b/test/unit/scripts/v19-public-api-boundary.test.ts index 8c5c613e4..cee6ffb61 100644 --- a/test/unit/scripts/v19-public-api-boundary.test.ts +++ b/test/unit/scripts/v19-public-api-boundary.test.ts @@ -1,112 +1,140 @@ import { readFileSync } from 'node:fs'; +import ts from 'typescript'; import { describe, expect, it } from 'vitest'; -import { extractJsExports, parseExportBlock } from '../../../scripts/check-dts-surface.ts'; - const REPO_ROOT = new URL('../../../', import.meta.url); -const ROOT_ERROR_ALLOWLIST = new Set([ - 'PatchError', - 'QueryError', - 'StrandError', - 'WormholeError', -]); - -const GRAPH_SUBSTRATE_NOUNS = new Set([ - 'BitmapIndexBuilder', - 'BitmapIndexReader', - 'BlobStoragePort', - 'ContentAttachmentProjection', - 'EdgeId', - 'EdgePropertyWriteIntent', - 'EdgeRecord', - 'EdgeTypeId', - 'IndexRebuildService', - 'InMemoryBlobStorageAdapter', - 'LegacyEdgePropertyKey', - 'LegacyNodePropertyKey', - 'LegacyPropertyProjection', - 'LegacyPropertyValue', - 'NodeId', - 'NodePropertyWriteIntent', - 'NodeRecord', - 'NodeTypeId', - 'VisibleEdgePropertyRecord', - 'VisibleNodePropertyRecord', -]); - -function collectSourceExports(relativePath: string): string[] { - return sorted(collectSourceExportsFrom(new URL(relativePath, REPO_ROOT), new Set())); -} +const ROOT_VALUE_EXPORTS = ['intent', 'openWarp', 'reading'] as const; -function collectSourceExportsFrom(sourceUrl: URL, visited: Set): Set { - const visitKey = sourceUrl.href; - if (visited.has(visitKey)) { - return new Set(); - } - visited.add(visitKey); +const ROOT_TYPE_EXPORTS = [ + 'DraftTimeline', + 'EdgeIntentFields', + 'Intent', + 'IntentBuilders', + 'IntentDescriptor', + 'IntentKind', + 'JoinMode', + 'JoinOutcome', + 'JoinOptions', + 'JoinPolicy', + 'JoinReceipt', + 'JoinReceiptOptions', + 'JoinResult', + 'JoinResultOptions', + 'NeighborhoodReadingFields', + 'NodeIntentFields', + 'NodeReadingFields', + 'OpenWarpOptions', + 'PropertyIntentFields', + 'PropertyReadingFields', + 'ReadEvidence', + 'ReadOutcome', + 'ReadReceipt', + 'ReadReceiptOptions', + 'Reading', + 'ReadingBuilders', + 'ReadingDescriptor', + 'ReadingDirection', + 'ReadingKind', + 'ReadingResult', + 'ReadingResultOptions', + 'ReadingValue', + 'Receipt', + 'ReceiptOutcome', + 'RepairHint', + 'Tick', + 'Timeline', + 'TimelineView', + 'Warp', + 'StorageAdapter', + 'WriteReceipt', + 'WriteOutcome', + 'WriteReceiptOptions', +] as const; - const source = readFileSync(sourceUrl, 'utf8'); - const names = extractJsExports(source); - for (const match of source.matchAll(/export\s+type\s*\{([^}]+)\}/g)) { - for (const name of parseExportBlock(match[1] ?? '')) { - names.add(name); +type ModuleSurface = { + readonly starExports: readonly string[]; + readonly typeExports: readonly string[]; + readonly valueExports: readonly string[]; +}; + +function moduleSurface(relativePath = 'index.ts'): ModuleSurface { + const source = readFileSync(new URL(relativePath, REPO_ROOT), 'utf8'); + const sourceFile = ts.createSourceFile( + relativePath, + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); + const starExports: string[] = []; + const typeExports: string[] = []; + const valueExports: string[] = []; + + for (const statement of sourceFile.statements) { + if (!ts.isExportDeclaration(statement)) { + collectExportedDeclaration(statement, typeExports, valueExports); + continue; } - } - for (const match of source.matchAll(/export\s+\*\s+from\s+['"]([^'"]+)['"]/g)) { - const specifier = match[1]; - if (specifier !== undefined) { - for (const name of collectSourceExportsFrom(new URL(specifier, sourceUrl), visited)) { - names.add(name); - } + if (statement.exportClause === undefined) { + starExports.push(statement.moduleSpecifier?.getText(sourceFile) ?? ''); + continue; + } + if (!ts.isNamedExports(statement.exportClause)) { + continue; + } + for (const element of statement.exportClause.elements) { + const target = statement.isTypeOnly || element.isTypeOnly ? typeExports : valueExports; + target.push(element.name.text); } } - return names; -} -function sorted(values: Iterable): string[] { - return Array.from(values).sort(); + return { + starExports: sorted(starExports), + typeExports: sorted(typeExports), + valueExports: sorted(valueExports), + }; } -function hasForbiddenVocabulary(name: string): boolean { - if (ROOT_ERROR_ALLOWLIST.has(name)) { - return false; +function collectExportedDeclaration( + statement: ts.Statement, + typeExports: string[], + valueExports: string[] +): void { + if (!hasExportModifier(statement)) { + return; } + if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) { + typeExports.push(statement.name.text); + return; + } + if ( + (ts.isFunctionDeclaration(statement) || + ts.isClassDeclaration(statement) || + ts.isEnumDeclaration(statement)) && + statement.name !== undefined + ) { + valueExports.push(statement.name.text); + return; + } + if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + if (ts.isIdentifier(declaration.name)) { + valueExports.push(declaration.name.text); + } + } + } +} +function hasExportModifier(statement: ts.Statement): boolean { return ( - name.includes('Graph') || - name.includes('Worldline') || - name.includes('Strand') || - name.includes('Optic') || - name.includes('Hologram') || - name.includes('Witness') || - name.includes('Braid') || - name.includes('Wormhole') || - name.includes('Projection') || - name.includes('Observer') || - name.includes('Query') || - name.includes('Coordinate') || - name.includes('Selector') || - name === 'WarpApp' || - name === 'WarpCore' || - name === 'PatchBuilder' || - name === 'PatchSession' || - name.startsWith('createNode') || - name.startsWith('createEdge') || - name.startsWith('createProp') || - name === 'createInlineValue' || - name === 'createBlobValue' || - name === 'decodeEdgePropKey' || - name === 'encodeEdgePropKey' || - name === 'isEdgePropKey' || - GRAPH_SUBSTRATE_NOUNS.has(name) + ts.canHaveModifiers(statement) && + ts + .getModifiers(statement) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true ); } -function forbiddenExportsFrom(exportNames: readonly string[]): string[] { - return sorted(exportNames.filter((name) => hasForbiddenVocabulary(name))); -} - function packageExportNames(relativePath: string): string[] { const value: unknown = JSON.parse(readFileSync(new URL(relativePath, REPO_ROOT), 'utf8')); if (!isRecord(value) || !isRecord(value['exports'])) { @@ -115,19 +143,72 @@ function packageExportNames(relativePath: string): string[] { return sorted(Object.keys(value['exports'])); } +function sorted(values: Iterable): string[] { + return Array.from(values).sort(); +} + function isRecord(value: unknown): value is Readonly> { return typeof value === 'object' && value !== null && !Array.isArray(value); } describe('v19 public API boundary', () => { - it('keeps graph, optic, worldline, witness, and diagnostic nouns out of package root', () => { - expect(forbiddenExportsFrom(collectSourceExports('index.ts'))).toEqual([]); + it('exports only the three first-use runtime values from package root', () => { + const surface = moduleSurface(); + expect(surface.starExports).toEqual([]); + expect(surface.valueExports).toEqual(sorted(ROOT_VALUE_EXPORTS)); + }); + + it('locks the package-root companion types to an explicit contract', () => { + expect(moduleSurface().typeExports).toEqual(sorted(ROOT_TYPE_EXPORTS)); + }); + + it('keeps the storage subpath limited to application adapters', () => { + const surface = moduleSurface('storage.ts'); + expect(surface.starExports).toEqual([]); + expect(surface.valueExports).toEqual(['GitStorageAdapter', 'MemoryStorageAdapter']); + expect(surface.typeExports).toEqual(['GitStorageAdapterOptions']); + }); + + it('keeps the advanced subpath limited to bounded coordinate reads', () => { + const surface = moduleSurface('advanced.ts'); + expect(surface.starExports).toEqual([]); + expect(surface.valueExports).toEqual(['Coordinate', 'Optic']); + expect(surface.typeExports).toEqual( + sorted([ + 'NeighborhoodOpticCompleteness', + 'NeighborhoodOpticEdge', + 'NeighborhoodOpticReadDirection', + 'NeighborhoodOpticReadOptions', + 'ReadIdentityFrontierEntry', + 'ReadIdentityIndexShard', + 'ReadIdentityOptions', + 'ReadIdentityTailWitness', + 'WarpWorldlineCoordinateFrontierEntry', + 'Witness', + ]) + ); + }); + + it('keeps diagnostics usable from public receipt handles', () => { + const surface = moduleSurface('diagnostics.ts'); + expect(surface.starExports).toEqual([]); + expect(surface.valueExports).toEqual(['inspectReceipt']); + expect(surface.typeExports).toEqual(['ReceiptInspection']); }); - it('does not publish retired browser or legacy compatibility entrypoints', () => { - expect(packageExportNames('package.json')).not.toContain('./browser'); - expect(packageExportNames('package.json')).not.toContain('./legacy'); - expect(packageExportNames('jsr.json')).not.toContain('./browser'); - expect(packageExportNames('jsr.json')).not.toContain('./legacy'); + it('publishes only the supported v19 subpaths', () => { + expect(packageExportNames('package.json')).toEqual([ + '.', + './advanced', + './diagnostics', + './package.json', + './storage', + ]); + expect(packageExportNames('jsr.json')).toEqual([ + '.', + './advanced', + './diagnostics', + './storage', + ]); }); }); From 050bd1cda8b5a1ed7c64955257d82c225c7777ad Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 12 Jul 2026 23:50:58 -0700 Subject: [PATCH 3/7] Bound checkpoint neighborhood reads --- docs/topics/querying.md | 7 + scripts/source-size-gate.ts | 1 - .../CheckpointNeighborhoodBitmapPager.ts | 308 ++++++++++++++++ .../optic/CheckpointNeighborhoodPageReader.ts | 342 ++++++++++++++++++ .../optic/CheckpointShardFactReader.ts | 311 ++-------------- .../optic/CheckpointTailWitnessLocator.ts | 84 +---- .../services/optic/NeighborhoodPageCursor.ts | 170 +++++++++ src/domain/utils/roaring.ts | 2 + .../v18NeighborhoodOpticReadBasis.test.ts | 22 +- .../v19BoundedNeighborhoodReading.test.ts | 184 ++++++++++ ...CheckpointShardFactReader.manifest.test.ts | 9 +- test/unit/domain/utils/roaring.test.ts | 10 +- .../adapters/RoaringLoaderAdapter.test.ts | 10 +- .../source-size-inventory-command.test.ts | 1 - 14 files changed, 1088 insertions(+), 373 deletions(-) create mode 100644 src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts create mode 100644 src/domain/services/optic/CheckpointNeighborhoodPageReader.ts create mode 100644 src/domain/services/optic/NeighborhoodPageCursor.ts create mode 100644 test/conformance/v19BoundedNeighborhoodReading.test.ts diff --git a/docs/topics/querying.md b/docs/topics/querying.md index 20acedaeb..84f3905d2 100644 --- a/docs/topics/querying.md +++ b/docs/topics/querying.md @@ -70,6 +70,13 @@ console.log(dependencies.value); console.log(dependencies.receipt.evidence); ``` +Neighborhood reads return one page in deterministic index order; they do not +promise lexical node-ID ordering. The default page size is 100 and the maximum +accepted `limit` is 1,000. When `value.cursor` is non-null, pass that opaque +cursor unchanged with the same subject, direction, and labels to read the next +page. Cursors are bound to the checkpoint basis and must not be reused after +that basis advances. + When the required checkpoint-tail basis is absent, a reading returns `null` with an `obstructed` receipt and `repairHints`. It never falls back to broad materialization. `readValue()` converts that unresolved result into an diff --git a/scripts/source-size-gate.ts b/scripts/source-size-gate.ts index 8e7c1cd85..885d0e32c 100644 --- a/scripts/source-size-gate.ts +++ b/scripts/source-size-gate.ts @@ -49,7 +49,6 @@ export const SOURCE_SIZE_RELAXATIONS = Object.freeze([ 'src/domain/services/audit/AuditChainVerifier.ts', 'src/domain/services/controllers/CheckpointController.ts', 'src/domain/services/optic/CheckpointBasisManifest.ts', - 'src/domain/services/optic/CheckpointShardFactReader.ts', 'src/domain/services/state/WarpState.ts', 'test/unit/domain/WarpGraph.coverageGaps.test.ts', 'test/unit/domain/services/AuditVerifierService.test.ts', diff --git a/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts b/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts new file mode 100644 index 000000000..6318dac64 --- /dev/null +++ b/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts @@ -0,0 +1,308 @@ +import QueryError from '../../errors/QueryError.ts'; +import { getRoaringBitmap32, type RoaringBitmapSubset } from '../../utils/roaring.ts'; +import toBytes from '../../utils/toBytes.ts'; +import type { Direction } from '../../../ports/NeighborProviderPort.ts'; +import type { NeighborhoodOpticEdge } from './NeighborhoodOpticReadResult.ts'; +import type { NeighborhoodCandidatePosition } from './NeighborhoodPageCursor.ts'; + +export type DecodedNeighborhoodEdgeShard = Record< + string, + Record> +>; + +export type NeighborhoodLabelRegistry = { + readonly byId: ReadonlyMap; + readonly byName: ReadonlyMap; +}; + +export type NeighborhoodBitmapCandidate = NeighborhoodCandidatePosition & { + readonly bitmap: RoaringBitmapSubset; + readonly index: number; +}; + +export type NeighborhoodBitmapPageSource = { + readCandidateHeap(options: { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: 'in' | 'out'; + readonly labels: NeighborhoodLabelRegistry; + readonly nodeId: string; + readonly requestedLabels: readonly string[]; + readonly sourceGlobalId: number; + }): Promise; + resolveLiveNode(globalId: number): Promise; +}; + +type NeighborhoodCollectOptions = { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: Direction; + readonly labels: NeighborhoodLabelRegistry; + readonly limit: number; + readonly loader: NeighborhoodBitmapPageSource; + readonly nodeId: string; + readonly requestedLabels: readonly string[]; + readonly sourceGlobalId: number; +}; + +export type NeighborhoodBitmapPageResult = { + readonly edges: NeighborhoodOpticEdge[]; + readonly hasMore: boolean; + readonly last: NeighborhoodCandidatePosition | null; +}; + +export async function collectNeighborhoodBitmapPage( + options: NeighborhoodCollectOptions, +): Promise { + const edges: NeighborhoodOpticEdge[] = []; + let last: NeighborhoodCandidatePosition | null = null; + for (const direction of readableDirections(options.direction, options.after)) { + const result = await collectDirection({ direction, edges, initialLast: last, options }); + if (result.hasMore) { + return { edges, ...result }; + } + last = result.last; + } + return { edges, hasMore: false, last }; +} + +export function createNeighborhoodCandidateHeap( + buckets: DecodedNeighborhoodEdgeShard, + options: { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: 'in' | 'out'; + readonly labels: NeighborhoodLabelRegistry; + readonly requestedLabels: readonly string[]; + readonly sourceGlobalId: number; + }, +): NeighborhoodBitmapCandidate[] { + const heap: NeighborhoodBitmapCandidate[] = []; + for (const [label, labelId] of selectedLabels(buckets, options.labels, options.requestedLabels)) { + addLabelBitmap(heap, buckets, { ...options, label, labelId }); + } + return heap; +} + +async function collectDirection( + context: { + readonly direction: 'in' | 'out'; + readonly edges: NeighborhoodOpticEdge[]; + readonly initialLast: NeighborhoodCandidatePosition | null; + readonly options: NeighborhoodCollectOptions; + }, +): Promise> { + const { direction, edges, options } = context; + const heap = await options.loader.readCandidateHeap({ ...options, direction }); + let last = context.initialLast; + while (heap.length > 0) { + const candidate = popAndAdvance(heap); + const neighborId = await options.loader.resolveLiveNode(candidate.globalId); + if (neighborId === null) { + continue; + } + if (edges.length === options.limit) { + return { hasMore: true, last }; + } + edges.push(Object.freeze({ direction, neighborId, label: candidate.label })); + last = candidatePosition(candidate); + } + return { hasMore: false, last }; +} + +function addLabelBitmap( + heap: NeighborhoodBitmapCandidate[], + buckets: DecodedNeighborhoodEdgeShard, + options: { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: 'in' | 'out'; + readonly label: string; + readonly labelId: number; + readonly sourceGlobalId: number; + }, +): void { + const bytes = buckets[String(options.labelId)]?.[String(options.sourceGlobalId)]; + if (bytes === undefined) { + return; + } + const bitmap = getRoaringBitmap32().deserialize(toBytes(bytes), true); + const index = firstCandidateIndex(bitmap, options); + if (index < bitmap.size) { + heapPush(heap, candidateAt(bitmap, index, options)); + } +} + +function candidateAt( + bitmap: RoaringBitmapSubset, + index: number, + options: { readonly direction: 'in' | 'out'; readonly label: string }, +): NeighborhoodBitmapCandidate { + return { + direction: options.direction, + globalId: bitmapValueAt(bitmap, index), + label: options.label, + bitmap, + index, + }; +} + +function selectedLabels( + buckets: DecodedNeighborhoodEdgeShard, + registry: NeighborhoodLabelRegistry, + requested: readonly string[], +): readonly (readonly [string, number])[] { + if (requested.length > 0) { + return Object.freeze(requested.flatMap((label) => { + const id = registry.byName.get(label); + return id === undefined ? [] : [[label, id] as [string, number]]; + })); + } + return labelsPresentInShard(buckets, registry); +} + +function labelsPresentInShard( + buckets: DecodedNeighborhoodEdgeShard, + registry: NeighborhoodLabelRegistry, +): readonly (readonly [string, number])[] { + return Object.freeze(Object.keys(buckets) + .filter((bucket) => bucket !== 'all') + .map((bucket) => [registry.byId.get(Number.parseInt(bucket, 10)), Number.parseInt(bucket, 10)] as const) + .filter((entry): entry is readonly [string, number] => entry[0] !== undefined) + .sort(([left], [right]) => compareText(left, right))); +} + +function firstCandidateIndex( + bitmap: RoaringBitmapSubset, + options: { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: 'in' | 'out'; + readonly label: string; + }, +): number { + const { after } = options; + if (after === null || directionOrder(options.direction) > directionOrder(after.direction)) { + return 0; + } + if (directionOrder(options.direction) < directionOrder(after.direction)) { + return bitmap.size; + } + return compareText(options.label, after.label) <= 0 + ? bitmap.rank(after.globalId) + : rankBefore(bitmap, after.globalId); +} + +function rankBefore(bitmap: RoaringBitmapSubset, value: number): number { + return value === 0 ? 0 : bitmap.rank(value - 1); +} + +function popAndAdvance(heap: NeighborhoodBitmapCandidate[]): NeighborhoodBitmapCandidate { + const candidate = heapPop(heap); + const nextIndex = candidate.index + 1; + if (nextIndex < candidate.bitmap.size) { + heapPush(heap, { + ...candidate, + globalId: bitmapValueAt(candidate.bitmap, nextIndex), + index: nextIndex, + }); + } + return candidate; +} + +function bitmapValueAt(bitmap: RoaringBitmapSubset, index: number): number { + const value = bitmap.at(index); + if (value === undefined) { + throw new QueryError('Neighborhood bitmap rank resolved outside the candidate set.', { + code: 'E_OPTIC_BASIS_INVALID', + }); + } + return value; +} + +function heapPush( + heap: NeighborhoodBitmapCandidate[], + candidate: NeighborhoodBitmapCandidate, +): void { + heap.push(candidate); + let index = heap.length - 1; + while (index > 0) { + const parent = Math.floor((index - 1) / 2); + if (compareCandidates(heap[parent]!, heap[index]!) <= 0) { + return; + } + [heap[parent], heap[index]] = [heap[index]!, heap[parent]!]; + index = parent; + } +} + +function heapPop(heap: NeighborhoodBitmapCandidate[]): NeighborhoodBitmapCandidate { + const [first] = heap; + const last = heap.pop(); + if (first === undefined || last === undefined) { + throw new QueryError('Neighborhood bitmap candidate heap is empty.', { code: 'E_OPTIC_BASIS_INVALID' }); + } + if (heap.length > 0) { + heap[0] = last; + heapSiftDown(heap); + } + return first; +} + +function heapSiftDown(heap: NeighborhoodBitmapCandidate[]): void { + let index = 0; + while (true) { + const child = smallerChildIndex(heap, index); + if (child === null || compareCandidates(heap[index]!, heap[child]!) <= 0) { + return; + } + [heap[index], heap[child]] = [heap[child]!, heap[index]!]; + index = child; + } +} + +function smallerChildIndex(heap: NeighborhoodBitmapCandidate[], parent: number): number | null { + const left = parent * 2 + 1; + if (left >= heap.length) { + return null; + } + const right = left + 1; + return right < heap.length && compareCandidates(heap[right]!, heap[left]!) < 0 + ? right + : left; +} + +function compareCandidates( + left: NeighborhoodCandidatePosition, + right: NeighborhoodCandidatePosition, +): number { + return directionOrder(left.direction) - directionOrder(right.direction) + || left.globalId - right.globalId + || compareText(left.label, right.label); +} + +function candidatePosition( + candidate: NeighborhoodCandidatePosition, +): NeighborhoodCandidatePosition { + return Object.freeze({ + direction: candidate.direction, + globalId: candidate.globalId, + label: candidate.label, + }); +} + +function readableDirections( + direction: Direction, + after: NeighborhoodCandidatePosition | null, +): readonly ('in' | 'out')[] { + return requestedDirections(direction).filter((candidate) => ( + after === null || directionOrder(candidate) >= directionOrder(after.direction) + )); +} + +function requestedDirections(direction: Direction): readonly ('in' | 'out')[] { + return direction === 'both' ? Object.freeze(['in', 'out']) : Object.freeze([direction]); +} + +function directionOrder(direction: 'in' | 'out'): number { + return direction === 'in' ? 0 : 1; +} + +function compareText(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} diff --git a/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts b/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts new file mode 100644 index 000000000..3a5d83d51 --- /dev/null +++ b/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts @@ -0,0 +1,342 @@ +import QueryError from '../../errors/QueryError.ts'; +import { getRoaringBitmap32, type RoaringBitmapSubset } from '../../utils/roaring.ts'; +import computeShardKey from '../../utils/shardKey.ts'; +import toBytes from '../../utils/toBytes.ts'; +import type { Direction } from '../../../ports/NeighborProviderPort.ts'; +import { + collectNeighborhoodBitmapPage, + createNeighborhoodCandidateHeap, + type DecodedNeighborhoodEdgeShard, + type NeighborhoodBitmapCandidate, + type NeighborhoodLabelRegistry, +} from './CheckpointNeighborhoodBitmapPager.ts'; +import type { CheckpointTailIndexBasis } from './CheckpointTailBasisLoader.ts'; +import type CheckpointTailOpticSource from './CheckpointTailOpticSource.ts'; +import type { NeighborhoodOpticEdge } from './NeighborhoodOpticReadResult.ts'; +import { + encodeNeighborhoodCursor, + neighborhoodCursorScope, + parseNeighborhoodCursor, + parseNeighborhoodLimit, + type NeighborhoodCandidatePosition, + type NeighborhoodCursorScope, +} from './NeighborhoodPageCursor.ts'; +import type { ReadIdentityIndexShard } from './ReadIdentity.ts'; + +const LABELS_PATH = 'labels.cbor'; + +type ReadBlob = (path: string, oid: string) => Promise; + +type DecodedMetaShard = { + readonly nodeToGlobal: Array<[string, number]> | Record; + readonly alive: Uint8Array | ArrayLike; +}; + +type DecodedLabels = Array<[string, number]> | Record; + +type LoadedMetaShard = { + readonly alive: RoaringBitmapSubset; + readonly byGlobalId: ReadonlyMap; +}; + +type NeighborhoodPageReadContext = { + readonly basis: CheckpointTailIndexBasis; + readonly options: CheckpointShardNeighborhoodReadOptions; + readonly readBlob: ReadBlob; + readonly source: CheckpointTailOpticSource; +}; + +export type CheckpointShardNeighborhoodReadOptions = { + readonly nodeId: string; + readonly direction: Direction; + readonly labels: readonly string[]; + readonly cursor?: string | null; + readonly limit?: number | null; +}; + +export type CheckpointShardNeighborhoodPage = { + readonly edges: readonly NeighborhoodOpticEdge[]; + readonly cursor: string | null; + readonly checkpointIndexShards: readonly ReadIdentityIndexShard[]; +}; + +class ShardEvidence { + private readonly _byPath = new Map(); + + add(path: string, oid: string): void { + this._byPath.set(path, oid); + } + + values(): readonly ReadIdentityIndexShard[] { + return Object.freeze([...this._byPath] + .sort(([left], [right]) => compareText(left, right)) + .map(([path, oid]) => Object.freeze({ path, oid }))); + } +} + +class NeighborhoodShardLoader { + private readonly _basis: CheckpointTailIndexBasis; + private readonly _codec: CheckpointTailOpticSource['_codec']; + private readonly _evidence: ShardEvidence; + private readonly _readBlob: ReadBlob; + private _loadedMeta: { readonly path: string; readonly value: LoadedMetaShard } | null = null; + + constructor(options: { + readonly basis: CheckpointTailIndexBasis; + readonly codec: CheckpointTailOpticSource['_codec']; + readonly evidence: ShardEvidence; + readonly readBlob: ReadBlob; + }) { + this._basis = options.basis; + this._codec = options.codec; + this._evidence = options.evidence; + this._readBlob = options.readBlob; + } + + hasAdjacency(direction: Direction, nodeId: string): boolean { + return requestedDirections(direction).some((candidateDirection) => { + const path = edgePath(candidateDirection, nodeId); + return edgeRoots(this._basis, candidateDirection).get(path) !== undefined; + }); + } + + async readSourceGlobalId(nodeId: string): Promise { + const meta = await this._readMeta(metaPathForNodeId(nodeId)); + if (meta === null) { + return null; + } + for (const [globalId, candidateNodeId] of meta.byGlobalId) { + if (candidateNodeId === nodeId) { + return meta.alive.has(globalId) ? globalId : null; + } + } + return null; + } + + async readLabels(): Promise { + const oid = this._basis.manifest.edgeFactRoots.get(LABELS_PATH); + if (oid === undefined) { + throw missingLabelsError(); + } + const bytes = await this._read(LABELS_PATH, oid); + return decodeLabels(this._codec.decode(bytes)); + } + + async readCandidateHeap(options: { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: 'in' | 'out'; + readonly labels: NeighborhoodLabelRegistry; + readonly nodeId: string; + readonly requestedLabels: readonly string[]; + readonly sourceGlobalId: number; + }): Promise { + const path = edgePath(options.direction, options.nodeId); + const oid = edgeRoots(this._basis, options.direction).get(path); + if (oid === undefined) { + return []; + } + const bytes = await this._read(path, oid); + const buckets = this._codec.decode(bytes); + return createNeighborhoodCandidateHeap(buckets, options); + } + + async resolveLiveNode(globalId: number): Promise { + const path = metaPathForGlobalId(globalId); + const meta = await this._readMeta(path); + if (meta === null || !meta.alive.has(globalId)) { + return null; + } + return meta.byGlobalId.get(globalId) ?? null; + } + + private async _readMeta(path: string): Promise { + if (this._loadedMeta?.path === path) { + return this._loadedMeta.value; + } + const oid = this._basis.manifest.livenessRoots.get(path); + if (oid === undefined) { + return null; + } + const bytes = await this._read(path, oid); + const value = decodeMeta(this._codec.decode(bytes)); + this._loadedMeta = { path, value }; + return value; + } + + private async _read(path: string, oid: string): Promise { + const bytes = await this._readBlob(path, oid); + this._evidence.add(path, oid); + return bytes; + } +} + +export default class CheckpointNeighborhoodPageReader { + private readonly _source: CheckpointTailOpticSource; + private readonly _readBlob: ReadBlob; + + constructor(options: { readonly source: CheckpointTailOpticSource; readonly readBlob: ReadBlob }) { + this._source = options.source; + this._readBlob = options.readBlob; + Object.freeze(this); + } + + async read( + basis: CheckpointTailIndexBasis, + options: CheckpointShardNeighborhoodReadOptions, + ): Promise { + return await readCheckpointNeighborhoodPage({ + basis, + options, + readBlob: this._readBlob, + source: this._source, + }); + } +} + +async function readCheckpointNeighborhoodPage( + options: NeighborhoodPageReadContext, +): Promise { + const scope = neighborhoodCursorScope(options.basis.checkpointSha, options.options); + const limit = parseNeighborhoodLimit(options.options.limit ?? null); + const after = parseNeighborhoodCursor(options.options.cursor ?? null, scope); + const evidence = new ShardEvidence(); + const loader = new NeighborhoodShardLoader({ + basis: options.basis, + codec: options.source._codec, + evidence, + readBlob: options.readBlob, + }); + const sourceGlobalId = await loader.readSourceGlobalId(options.options.nodeId); + if (sourceGlobalId === null + || !loader.hasAdjacency(options.options.direction, options.options.nodeId)) { + return emptyPage(evidence); + } + return await readPopulatedPage({ + after, + direction: options.options.direction, + evidence, + labels: options.options.labels, + limit, + loader, + nodeId: options.options.nodeId, + scope, + sourceGlobalId, + }); +} + +async function readPopulatedPage(options: { + readonly after: NeighborhoodCandidatePosition | null; + readonly direction: Direction; + readonly evidence: ShardEvidence; + readonly labels: readonly string[]; + readonly limit: number; + readonly loader: NeighborhoodShardLoader; + readonly nodeId: string; + readonly scope: NeighborhoodCursorScope; + readonly sourceGlobalId: number; +}): Promise { + const labels = await options.loader.readLabels(); + const collected = await collectNeighborhoodBitmapPage({ + after: options.after, + direction: options.direction, + labels, + limit: options.limit, + loader: options.loader, + nodeId: options.nodeId, + requestedLabels: options.labels, + sourceGlobalId: options.sourceGlobalId, + }); + return completedPage(options.scope, options.evidence, collected); +} + +function completedPage( + scope: NeighborhoodCursorScope, + evidence: ShardEvidence, + collected: { + edges: NeighborhoodOpticEdge[]; + hasMore: boolean; + last: NeighborhoodCandidatePosition | null; + }, +): CheckpointShardNeighborhoodPage { + const cursor = collected.hasMore && collected.last !== null + ? encodeNeighborhoodCursor(scope, collected.last) + : null; + return Object.freeze({ + edges: Object.freeze(collected.edges), + cursor, + checkpointIndexShards: evidence.values(), + }); +} + +function decodeMeta(decoded: DecodedMetaShard): LoadedMetaShard { + const entries = Array.isArray(decoded.nodeToGlobal) + ? decoded.nodeToGlobal + : Object.entries(decoded.nodeToGlobal); + const byGlobalId = new Map(); + for (const [nodeId, globalId] of entries) { + byGlobalId.set(globalId, nodeId); + } + const alive = decoded.alive.length === 0 + ? new (getRoaringBitmap32())() + : getRoaringBitmap32().deserialize(toBytes(decoded.alive), true); + return { alive, byGlobalId }; +} + +function decodeLabels(decoded: DecodedLabels): NeighborhoodLabelRegistry { + const entries = Array.isArray(decoded) ? decoded : Object.entries(decoded); + const byId = new Map(); + const byName = new Map(); + for (const [label, id] of entries) { + byId.set(id, label); + byName.set(label, id); + } + return { byId, byName }; +} + +function missingLabelsError(): QueryError { + return new QueryError('Checkpoint neighborhood basis is missing its label registry.', { + code: 'E_OPTIC_NO_BOUNDED_BASIS', + context: { reason: 'checkpoint-shard-unavailable', path: LABELS_PATH }, + }); +} + +function emptyPage(evidence: ShardEvidence): CheckpointShardNeighborhoodPage { + return Object.freeze({ + edges: Object.freeze([]), + cursor: null, + checkpointIndexShards: evidence.values(), + }); +} + +function requestedDirections(direction: Direction): readonly ('in' | 'out')[] { + if (direction === 'both') { + return Object.freeze(['in', 'out']); + } + return Object.freeze([direction]); +} + +function edgePath(direction: 'in' | 'out', nodeId: string): string { + return `${direction === 'out' ? 'fwd' : 'rev'}_${computeShardKey(nodeId)}.cbor`; +} + +function edgeRoots( + basis: CheckpointTailIndexBasis, + direction: 'in' | 'out', +): { get(path: string): string | undefined } { + return direction === 'out' + ? basis.manifest.outgoingAdjacencyRoots + : basis.manifest.incomingAdjacencyRoots; +} + +function metaPathForNodeId(nodeId: string): string { + return `meta_${computeShardKey(nodeId)}.cbor`; +} + +function metaPathForGlobalId(globalId: number): string { + const shardKey = ((globalId >>> 24) & 0xff).toString(16).padStart(2, '0'); + return `meta_${shardKey}.cbor`; +} + +function compareText(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} diff --git a/src/domain/services/optic/CheckpointShardFactReader.ts b/src/domain/services/optic/CheckpointShardFactReader.ts index b1759d3c6..9077595d3 100644 --- a/src/domain/services/optic/CheckpointShardFactReader.ts +++ b/src/domain/services/optic/CheckpointShardFactReader.ts @@ -1,17 +1,22 @@ import type { PropValue } from '../../types/PropValue.ts'; import computeShardKey from '../../utils/shardKey.ts'; -import { getRoaringBitmap32 } from '../../utils/roaring.ts'; -import toBytes from '../../utils/toBytes.ts'; import IndexError from '../../errors/IndexError.ts'; import PersistenceError from '../../errors/PersistenceError.ts'; import QueryError from '../../errors/QueryError.ts'; import LogicalIndexReader from '../index/LogicalIndexReader.ts'; import PropertyIndexReader from '../index/PropertyIndexReader.ts'; +import CheckpointNeighborhoodPageReader, { + type CheckpointShardNeighborhoodPage, + type CheckpointShardNeighborhoodReadOptions, +} from './CheckpointNeighborhoodPageReader.ts'; import type { CheckpointTailIndexBasis } from './CheckpointTailBasisLoader.ts'; import type CheckpointTailOpticSource from './CheckpointTailOpticSource.ts'; import type { ReadIdentityIndexShard } from './ReadIdentity.ts'; -import type { Direction, NeighborEdge } from '../../../ports/NeighborProviderPort.ts'; -import type { NeighborhoodOpticEdge } from './NeighborhoodOpticReadResult.ts'; + +export type { + CheckpointShardNeighborhoodPage, + CheckpointShardNeighborhoodReadOptions, +} from './CheckpointNeighborhoodPageReader.ts'; const MAX_CACHED_CHECKPOINT_PROPERTY_SHARDS = 1; const INDEX_SHARD_MISSING_CODE = 'E_INDEX_SHARD_MISSING'; @@ -30,19 +35,21 @@ type CheckpointShardReadFailureContext = { readonly oid: string; }; -type DecodedEdgeShardBuckets = Record>>; - -export type CheckpointShardNeighborhoodReadOptions = { - readonly nodeId: string; - readonly direction: Direction; - readonly labels: readonly string[]; -}; - export default class CheckpointShardFactReader { private readonly _source: CheckpointTailOpticSource; + private readonly _neighborhoodReader: CheckpointNeighborhoodPageReader; constructor(options: { readonly source: CheckpointTailOpticSource }) { this._source = options.source; + this._neighborhoodReader = new CheckpointNeighborhoodPageReader({ + source: options.source, + readBlob: async (path, oid) => await readShardBlob({ + graphName: options.source.graphName, + persistence: options.source._persistence, + path, + oid, + }), + }); Object.freeze(this); } @@ -96,13 +103,9 @@ export default class CheckpointShardFactReader { async readNeighborhood( basis: CheckpointTailIndexBasis, options: CheckpointShardNeighborhoodReadOptions, - ): Promise { - const baseShardOids = neighborhoodShardOidMap(basis, options); - if (!hasAdjacencyShard(baseShardOids)) { - return Object.freeze([]); - } + ): Promise { try { - return await this._readNeighborhoodWithBaseShards(basis, options, baseShardOids); + return await this._neighborhoodReader.read(basis, options); } catch (error) { const context = firstShardFailureContext(this._source.graphName); const failure = error instanceof Error ? checkpointLogicalShardReadFailure(error, context) : null; @@ -127,16 +130,6 @@ export default class CheckpointShardFactReader { return shardIdentities([{ path, oid: basis.manifest.propertyRoots.get(path) }]); } - neighborhoodShardIdentities( - basis: CheckpointTailIndexBasis, - options: CheckpointShardNeighborhoodReadOptions, - ): readonly ReadIdentityIndexShard[] { - return shardIdentities( - Object.entries(neighborhoodShardOidMap(basis, options)) - .map(([path, oid]) => ({ path, oid })), - ); - } - private _metaPath(nodeId: string): string { return `meta_${computeShardKey(nodeId)}.cbor`; } @@ -145,264 +138,6 @@ export default class CheckpointShardFactReader { return `props_${computeShardKey(nodeId)}.cbor`; } - private async _readNeighborhoodWithBaseShards( - basis: CheckpointTailIndexBasis, - options: CheckpointShardNeighborhoodReadOptions, - baseShardOids: Record, - ): Promise { - const baseTree = await this._readShardTree(baseShardOids); - const baseReader = new LogicalIndexReader({ codec: this._source._codec }) - .loadFromTree(baseTree); - const baseIndex = baseReader.toLogicalIndex(); - const labelIds = labelIdsFor(baseIndex.getLabelRegistry(), options.labels); - const shardOids = neighborhoodShardOidMapWithNeighborLiveness({ - basis, - options, - baseShardOids, - baseTree, - sourceGlobalId: baseIndex.getGlobalId(options.nodeId), - labelIds, - codec: this._source._codec, - }); - const tree = await this._readShardTree(shardOids); - const reader = new LogicalIndexReader({ codec: this._source._codec }) - .loadFromTree(tree); - return Object.freeze(neighborhoodEdges(reader.toLogicalIndex(), options, labelIds)); - } - - private async _readShardTree(shardOids: Record): Promise> { - return await readShardTree({ - graphName: this._source.graphName, - persistence: this._source._persistence, - shardOids, - }); - } -} - -function neighborhoodShardOidMap( - basis: CheckpointTailIndexBasis, - options: CheckpointShardNeighborhoodReadOptions, -): Record { - const shardOids: Record = {}; - addOptionalShardRoot( - shardOids, - basis.manifest.livenessRoots.get(metaPathForNodeId(options.nodeId)), - metaPathForNodeId(options.nodeId), - ); - addOptionalShardRoot(shardOids, basis.manifest.edgeFactRoots.get('labels.cbor'), 'labels.cbor'); - const shardKey = computeShardKey(options.nodeId); - if (options.direction === 'out' || options.direction === 'both') { - const path = `fwd_${shardKey}.cbor`; - addOptionalShardRoot(shardOids, basis.manifest.outgoingAdjacencyRoots.get(path), path); - } - if (options.direction === 'in' || options.direction === 'both') { - const path = `rev_${shardKey}.cbor`; - addOptionalShardRoot(shardOids, basis.manifest.incomingAdjacencyRoots.get(path), path); - } - return sortShardOidMap(shardOids); -} - -function neighborhoodShardOidMapWithNeighborLiveness(options: { - readonly basis: CheckpointTailIndexBasis; - readonly options: CheckpointShardNeighborhoodReadOptions; - readonly baseShardOids: Record; - readonly baseTree: Record; - readonly sourceGlobalId: number | undefined; - readonly labelIds: number[] | undefined; - readonly codec: CheckpointTailOpticSource['_codec']; -}): Record { - const shardOids: Record = { ...options.baseShardOids }; - if (options.sourceGlobalId === undefined) { - return sortShardOidMap(shardOids); - } - const neighborShardKeys = neighborhoodNeighborShardKeys({ - tree: options.baseTree, - sourceGlobalId: options.sourceGlobalId, - direction: options.options.direction, - labelIds: options.labelIds, - codec: options.codec, - }); - for (const shardKey of neighborShardKeys) { - const path = `meta_${shardKey}.cbor`; - addOptionalShardRoot(shardOids, options.basis.manifest.livenessRoots.get(path), path); - } - return sortShardOidMap(shardOids); -} - -function addOptionalShardRoot(target: Record, oid: string | undefined, path: string): void { - if (oid !== undefined) { - target[path] = oid; - } -} - -function hasAdjacencyShard(shardOids: Record): boolean { - return Object.keys(shardOids) - .some((path) => path.startsWith('fwd_') || path.startsWith('rev_')); -} - -function metaPathForNodeId(nodeId: string): string { - return `meta_${computeShardKey(nodeId)}.cbor`; -} - -function neighborhoodNeighborShardKeys(options: { - readonly tree: Record; - readonly sourceGlobalId: number; - readonly direction: Direction; - readonly labelIds: number[] | undefined; - readonly codec: CheckpointTailOpticSource['_codec']; -}): readonly string[] { - const keys = new Set(); - for (const [path, bytes] of Object.entries(options.tree)) { - if (shouldReadEdgeShard(path, options.direction)) { - addNeighborShardKeys(keys, { - buckets: options.codec.decode(bytes), - sourceGlobalId: options.sourceGlobalId, - labelIds: options.labelIds, - }); - } - } - return Object.freeze([...keys].sort(compareText)); -} - -function shouldReadEdgeShard(path: string, direction: Direction): boolean { - if (path.startsWith('fwd_')) { - return direction === 'out' || direction === 'both'; - } - if (path.startsWith('rev_')) { - return direction === 'in' || direction === 'both'; - } - return false; -} - -function addNeighborShardKeys(target: Set, options: { - readonly buckets: DecodedEdgeShardBuckets; - readonly sourceGlobalId: number; - readonly labelIds: number[] | undefined; -}): void { - const sourceGlobalId = String(options.sourceGlobalId); - const selectedBuckets = selectedEdgeBuckets(options.buckets, options.labelIds); - const RoaringBitmap32 = getRoaringBitmap32(); - for (const bucket of selectedBuckets) { - const bitmapBytes = options.buckets[bucket]?.[sourceGlobalId]; - if (bitmapBytes !== undefined) { - for (const globalId of RoaringBitmap32.deserialize(toBytes(bitmapBytes), true).toArray()) { - target.add(shardKeyForGlobalId(globalId)); - } - } - } -} - -function selectedEdgeBuckets( - buckets: DecodedEdgeShardBuckets, - labelIds: number[] | undefined, -): readonly string[] { - if (labelIds !== undefined) { - return Object.freeze(labelIds.map((labelId) => String(labelId))); - } - return Object.freeze(Object.keys(buckets) - .filter((bucket) => bucket !== 'all') - .sort(compareText)); -} - -function shardKeyForGlobalId(globalId: number): string { - return ((globalId >>> 24) & 0xff).toString(16).padStart(2, '0'); -} - -function sortShardOidMap(source: Record): Record { - const sorted = new Map(); - for (const path of Object.keys(source).sort()) { - const oid = source[path]; - if (oid !== undefined) { - sorted.set(path, oid); - } - } - return Object.fromEntries(sorted); -} - -function labelIdsFor( - registry: Map, - labels: readonly string[], -): number[] | undefined { - if (labels.length === 0) { - return undefined; - } - const ids: number[] = []; - for (const label of labels) { - const id = registry.get(label); - if (id !== undefined) { - ids.push(id); - } - } - return ids; -} - -function neighborhoodEdges( - index: ReturnType, - options: CheckpointShardNeighborhoodReadOptions, - labelIds: number[] | undefined, -): NeighborhoodOpticEdge[] { - const edges: NeighborhoodOpticEdge[] = []; - if (options.direction === 'out' || options.direction === 'both') { - edges.push(...tagEdges('out', index.getEdges(options.nodeId, 'out', labelIds))); - } - if (options.direction === 'in' || options.direction === 'both') { - edges.push(...tagEdges('in', index.getEdges(options.nodeId, 'in', labelIds))); - } - return dedupeSortNeighborhoodEdges(edges); -} - -function tagEdges( - direction: 'in' | 'out', - edges: readonly NeighborEdge[], -): readonly NeighborhoodOpticEdge[] { - return edges.map((edge) => Object.freeze({ - direction, - neighborId: edge.neighborId, - label: edge.label, - })); -} - -function dedupeSortNeighborhoodEdges( - edges: readonly NeighborhoodOpticEdge[], -): NeighborhoodOpticEdge[] { - const byKey = new Map(); - for (const edge of edges) { - byKey.set(edgeKey(edge), edge); - } - return [...byKey.values()].sort(compareNeighborhoodEdges); -} - -function compareNeighborhoodEdges( - left: NeighborhoodOpticEdge, - right: NeighborhoodOpticEdge, -): number { - return compareText(left.direction, right.direction) - || compareText(left.neighborId, right.neighborId) - || compareText(left.label, right.label); -} - -function edgeKey(edge: NeighborhoodOpticEdge): string { - return `${edge.direction}\u0000${edge.neighborId}\u0000${edge.label}`; -} - -async function readShardTree(options: { - readonly graphName: string; - readonly persistence: CheckpointTailOpticSource['_persistence']; - readonly shardOids: Record; -}): Promise> { - const tree = new Map(); - for (const path of Object.keys(options.shardOids).sort()) { - const oid = options.shardOids[path]; - if (oid !== undefined) { - tree.set(path, await readShardBlob({ - graphName: options.graphName, - persistence: options.persistence, - path, - oid, - })); - } - } - return Object.fromEntries(tree); } async function readShardBlob(options: { @@ -434,10 +169,6 @@ function firstShardFailureContext( return { graphName, path: 'checkpoint-neighborhood-index', oid: 'decoded-shards' }; } -function compareText(left: string, right: string): number { - return left < right ? -1 : left > right ? 1 : 0; -} - function shardIdentities( shards: readonly CheckpointShardIdentityCandidate[], ): readonly ReadIdentityIndexShard[] { diff --git a/src/domain/services/optic/CheckpointTailWitnessLocator.ts b/src/domain/services/optic/CheckpointTailWitnessLocator.ts index 09219486c..758d9939a 100644 --- a/src/domain/services/optic/CheckpointTailWitnessLocator.ts +++ b/src/domain/services/optic/CheckpointTailWitnessLocator.ts @@ -22,6 +22,7 @@ import OpticSupportRule from './OpticSupportRule.ts'; import type TraversalOpticReadResult from './TraversalOpticReadResult.ts'; import type { TraversalOpticReadOptions } from './TraversalOptic.ts'; import type ReadIdentity from './ReadIdentity.ts'; +import type { ReadIdentityIndexShard } from './ReadIdentity.ts'; const DEFAULT_MAX_TAIL_PATCHES = 10_000; @@ -183,8 +184,10 @@ export default class CheckpointTailWitnessLocator { const basis = await this._basisLoader.load(); const tail = await this._scanTailForNeighborhood(basis, { nodeId, direction, labels }); this._factReducer.assertNeighborhoodTailStable(tail.entries); - const allEdges = await this._shardReader.readNeighborhood(basis, { nodeId, direction, labels }); - const windowed = windowNeighborhoodEdges(allEdges, { + const page = await this._shardReader.readNeighborhood(basis, { + nodeId, + direction, + labels, cursor: options.cursor ?? null, limit: options.limit ?? null, }); @@ -194,9 +197,8 @@ export default class CheckpointTailWitnessLocator { labels, nodeId, readIdentityBuilder: this._readIdentityBuilder, - shardReader: this._shardReader, tail, - windowed, + page, }); } @@ -311,80 +313,26 @@ function normalizeLabels(labels: readonly string[]): readonly string[] { return Object.freeze([...new Set(labels)].sort()); } -function windowNeighborhoodEdges( - edges: readonly NeighborhoodOpticEdge[], - options: { - readonly cursor: string | null; - readonly limit: number | null; - }, -): { - readonly edges: readonly NeighborhoodOpticEdge[]; - readonly cursor: string | null; -} { - const start = parseCursor(options.cursor); - const limit = parseLimit(options.limit); - if (limit === null) { - return { edges: Object.freeze(edges.slice(start)), cursor: null }; - } - const end = start + limit; - const window = edges.slice(start, end); - return { - edges: Object.freeze(window), - cursor: end < edges.length ? String(end) : null, - }; -} - -function parseCursor(cursor: string | null): number { - if (cursor === null || cursor.length === 0) { - return 0; - } - const parsed = Number.parseInt(cursor, 10); - if (!isCanonicalCursor(parsed, cursor)) { - throw new QueryError('Neighborhood optic cursor must be a non-negative integer string.', { - code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', - context: { field: 'cursor' }, - }); - } - return parsed; -} - -function isCanonicalCursor(parsed: number, raw: string): boolean { - return Number.isInteger(parsed) && parsed >= 0 && String(parsed) === raw; -} - -function parseLimit(limit: number | null): number | null { - if (limit === null) { - return null; - } - if (!Number.isInteger(limit) || limit < 1) { - throw new QueryError('Neighborhood optic limit must be a positive integer.', { - code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', - context: { field: 'limit' }, - }); - } - return limit; -} - function neighborhoodReadResult(options: { readonly basis: CheckpointTailIndexBasis; readonly direction: Direction; readonly labels: readonly string[]; readonly nodeId: string; readonly readIdentityBuilder: CheckpointTailReadIdentityBuilder; - readonly shardReader: CheckpointShardFactReader; readonly tail: TailWitnessScan; - readonly windowed: { + readonly page: { readonly edges: readonly NeighborhoodOpticEdge[]; readonly cursor: string | null; + readonly checkpointIndexShards: readonly ReadIdentityIndexShard[]; }; }): NeighborhoodOpticReadResult { const readIdentity = neighborhoodReadIdentity(options); return new NeighborhoodOpticReadResult({ nodeId: options.nodeId, direction: options.direction, - edges: options.windowed.edges, - completeness: options.windowed.cursor === null ? 'complete' : 'truncated', - cursor: options.windowed.cursor, + edges: options.page.edges, + completeness: options.page.cursor === null ? 'complete' : 'truncated', + cursor: options.page.cursor, readIdentity, }); } @@ -395,19 +343,17 @@ function neighborhoodReadIdentity(options: { readonly labels: readonly string[]; readonly nodeId: string; readonly readIdentityBuilder: CheckpointTailReadIdentityBuilder; - readonly shardReader: CheckpointShardFactReader; readonly tail: TailWitnessScan; + readonly page: { + readonly checkpointIndexShards: readonly ReadIdentityIndexShard[]; + }; }): ReadIdentity { return options.readIdentityBuilder.neighborhood({ basis: options.basis, nodeId: options.nodeId, direction: options.direction, labels: options.labels, - checkpointIndexShards: options.shardReader.neighborhoodShardIdentities(options.basis, { - nodeId: options.nodeId, - direction: options.direction, - labels: options.labels, - }), + checkpointIndexShards: options.page.checkpointIndexShards, tailWitnesses: options.tail.witnesses, }); } diff --git a/src/domain/services/optic/NeighborhoodPageCursor.ts b/src/domain/services/optic/NeighborhoodPageCursor.ts new file mode 100644 index 000000000..8d3c6c685 --- /dev/null +++ b/src/domain/services/optic/NeighborhoodPageCursor.ts @@ -0,0 +1,170 @@ +import QueryError from '../../errors/QueryError.ts'; +import type { Direction } from '../../../ports/NeighborProviderPort.ts'; + +export const DEFAULT_NEIGHBORHOOD_PAGE_SIZE = 100; +export const MAX_NEIGHBORHOOD_PAGE_SIZE = 1_000; + +const CURSOR_PREFIX = 'warp-neighborhood-v1:'; + +export type NeighborhoodCursorScope = { + readonly checkpointSha: string; + readonly nodeId: string; + readonly direction: Direction; + readonly labels: readonly string[]; +}; + +export type NeighborhoodCandidatePosition = { + readonly direction: 'in' | 'out'; + readonly globalId: number; + readonly label: string; +}; + +export function neighborhoodCursorScope( + checkpointSha: string, + options: { + readonly nodeId: string; + readonly direction: Direction; + readonly labels: readonly string[]; + }, +): NeighborhoodCursorScope { + return Object.freeze({ + checkpointSha, + nodeId: options.nodeId, + direction: options.direction, + labels: Object.freeze([...options.labels]), + }); +} + +export function encodeNeighborhoodCursor( + scope: NeighborhoodCursorScope, + after: NeighborhoodCandidatePosition, +): string { + return CURSOR_PREFIX + [ + encodeURIComponent(scope.checkpointSha), + encodeURIComponent(scope.nodeId), + scope.direction, + scope.labels.map((label) => encodeURIComponent(label)).join(','), + after.direction, + String(after.globalId), + encodeURIComponent(after.label), + ].join('|'); +} + +export function parseNeighborhoodCursor( + raw: string | null, + scope: NeighborhoodCursorScope, +): NeighborhoodCandidatePosition | null { + if (raw === null || raw.length === 0) { + return null; + } + try { + return decodeCursorParts(requireCursorParts(raw), scope); + } catch (error) { + if (error instanceof QueryError) { + throw error; + } + throw invalidCursorError(); + } +} + +export function parseNeighborhoodLimit(limit: number | null): number { + const resolved = limit ?? DEFAULT_NEIGHBORHOOD_PAGE_SIZE; + if (!Number.isInteger(resolved) || resolved < 1 || resolved > MAX_NEIGHBORHOOD_PAGE_SIZE) { + throw new QueryError(`Neighborhood optic limit must be between 1 and ${MAX_NEIGHBORHOOD_PAGE_SIZE}.`, { + code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', + context: { field: 'limit', max: MAX_NEIGHBORHOOD_PAGE_SIZE }, + }); + } + return resolved; +} + +function requireCursorParts(raw: string): readonly string[] { + if (!raw.startsWith(CURSOR_PREFIX)) { + throw invalidCursorError(); + } + const parts = raw.slice(CURSOR_PREFIX.length).split('|'); + if (parts.length !== 7) { + throw invalidCursorError(); + } + return parts; +} + +function decodeCursorParts( + parts: readonly string[], + scope: NeighborhoodCursorScope, +): NeighborhoodCandidatePosition { + const decodedScope = decodeCursorScope(parts); + if (!cursorScopeMatches(decodedScope, scope)) { + throw invalidCursorError(); + } + return decodeCursorPosition(parts); +} + +function decodeCursorScope(parts: readonly string[]): Omit & { + readonly labels: readonly string[]; +} { + return { + checkpointSha: decodeURIComponent(parts[0]!), + nodeId: decodeURIComponent(parts[1]!), + direction: parseDirection(parts[2]!), + labels: decodeLabels(parts[3]!), + }; +} + +function cursorScopeMatches( + decoded: NeighborhoodCursorScope, + expected: NeighborhoodCursorScope, +): boolean { + return decoded.checkpointSha === expected.checkpointSha + && decoded.nodeId === expected.nodeId + && decoded.direction === expected.direction + && sameLabels(decoded.labels, expected.labels); +} + +function decodeCursorPosition(parts: readonly string[]): NeighborhoodCandidatePosition { + return Object.freeze({ + direction: parseConcreteDirection(parts[4]!), + globalId: parseGlobalId(parts[5]!), + label: decodeURIComponent(parts[6]!), + }); +} + +function decodeLabels(value: string): readonly string[] { + return value.length === 0 + ? Object.freeze([]) + : Object.freeze(value.split(',').map((label) => decodeURIComponent(label))); +} + +function sameLabels(value: readonly string[], expected: readonly string[]): boolean { + return value.length === expected.length + && value.every((label, index) => label === expected[index]); +} + +function parseDirection(value: string): Direction { + if (value === 'in' || value === 'out' || value === 'both') { + return value; + } + throw invalidCursorError(); +} + +function parseConcreteDirection(value: string): 'in' | 'out' { + if (value === 'in' || value === 'out') { + return value; + } + throw invalidCursorError(); +} + +function parseGlobalId(value: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed < 0 || parsed > 0xffff_ffff || String(parsed) !== value) { + throw invalidCursorError(); + } + return parsed; +} + +function invalidCursorError(): QueryError { + return new QueryError('Neighborhood optic cursor is invalid, stale, or belongs to another reading.', { + code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', + context: { field: 'cursor' }, + }); +} diff --git a/src/domain/utils/roaring.ts b/src/domain/utils/roaring.ts index e2e73e3ba..b4ec7e668 100644 --- a/src/domain/utils/roaring.ts +++ b/src/domain/utils/roaring.ts @@ -9,6 +9,8 @@ export type RoaringBitmapSubset = { orInPlace(other: Iterable): void; serialize(portable: boolean): Uint8Array; toArray(): number[]; + rank(value: number): number; + at(index: number): number | undefined; [Symbol.iterator](): Iterator; }; diff --git a/test/conformance/v18NeighborhoodOpticReadBasis.test.ts b/test/conformance/v18NeighborhoodOpticReadBasis.test.ts index c61fca59e..e1a3d743d 100644 --- a/test/conformance/v18NeighborhoodOpticReadBasis.test.ts +++ b/test/conformance/v18NeighborhoodOpticReadBasis.test.ts @@ -50,8 +50,8 @@ describe('v18 NeighborhoodOptic checkpoint-tail read basis', () => { completeness: 'complete', cursor: null, edges: [ - { direction: 'out', neighborId: ALPHA_NODE_ID, label: OWNS_LABEL }, { direction: 'out', neighborId: BETA_NODE_ID, label: FOLLOWS_LABEL }, + { direction: 'out', neighborId: ALPHA_NODE_ID, label: OWNS_LABEL }, ], }); expect(incoming).toMatchObject({ @@ -62,8 +62,8 @@ describe('v18 NeighborhoodOptic checkpoint-tail read basis', () => { expect(both).toMatchObject({ edges: [ { direction: 'in', neighborId: GAMMA_NODE_ID, label: FOLLOWS_LABEL }, - { direction: 'out', neighborId: ALPHA_NODE_ID, label: OWNS_LABEL }, { direction: 'out', neighborId: BETA_NODE_ID, label: FOLLOWS_LABEL }, + { direction: 'out', neighborId: ALPHA_NODE_ID, label: OWNS_LABEL }, ], }); expect(ownsOnly).toMatchObject({ @@ -98,24 +98,26 @@ describe('v18 NeighborhoodOptic checkpoint-tail read basis', () => { const readPath = new V17PublicOpticReadPath(graph.worldline()); const first = await readPath.readNeighborhood(HUB_NODE_ID, { direction: 'out', limit: 1 }); + const firstCursor = neighborhoodCursor(first); + expect(firstCursor).not.toBeNull(); const second = await readPath.readNeighborhood(HUB_NODE_ID, { direction: 'out', limit: 1, - cursor: '1', + cursor: firstCursor ?? undefined, }); expect(first).toMatchObject({ completeness: 'truncated', - cursor: '1', + cursor: firstCursor, edges: [ - { direction: 'out', neighborId: ALPHA_NODE_ID, label: OWNS_LABEL }, + { direction: 'out', neighborId: BETA_NODE_ID, label: FOLLOWS_LABEL }, ], }); expect(second).toMatchObject({ completeness: 'complete', cursor: null, edges: [ - { direction: 'out', neighborId: BETA_NODE_ID, label: FOLLOWS_LABEL }, + { direction: 'out', neighborId: ALPHA_NODE_ID, label: OWNS_LABEL }, ], }); }); @@ -168,3 +170,11 @@ function readIdentityShardPaths(result: object): readonly string[] { }), ); } + +function neighborhoodCursor(result: object): string | null { + const cursor = Reflect.get(result, 'cursor'); + if (cursor !== null && typeof cursor !== 'string') { + throw new Error('expected neighborhood cursor'); + } + return cursor; +} diff --git a/test/conformance/v19BoundedNeighborhoodReading.test.ts b/test/conformance/v19BoundedNeighborhoodReading.test.ts new file mode 100644 index 000000000..ea788ad71 --- /dev/null +++ b/test/conformance/v19BoundedNeighborhoodReading.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, it } from 'vitest'; +import computeShardKey from '../../src/domain/utils/shardKey.ts'; +import V17CheckpointTailOpticGraphFixture from './fixtures/V17CheckpointTailOpticGraphFixture.ts'; +import V17MaterializationFallbackTrap from './fixtures/V17MaterializationFallbackTrap.ts'; +import V17PublicOpticReadPath from './fixtures/V17PublicOpticReadPath.ts'; + +const HUB_NODE_ID = 'node:bounded-hub'; +const EDGE_LABEL = 'contains'; +const DEFAULT_PAGE_SIZE = 100; +const MAX_PAGE_SIZE = 1_000; + +describe('v19 bounded neighborhood reading', () => { + it('bounds the default page and reads only page-supporting liveness shards', async () => { + const fixture = await V17CheckpointTailOpticGraphFixture.openEmpty('v19-bounded-neighborhood-page'); + const graph = fixture.graph; + const neighborIds = Array.from({ length: 160 }, (_, index) => `node:neighbor:${index}`); + await graph.patch((patch) => { + patch.addNode(HUB_NODE_ID); + for (const neighborId of neighborIds) { + patch.addNode(neighborId); + patch.addEdge(HUB_NODE_ID, neighborId, EDGE_LABEL); + } + }); + await graph.materialize(); + await graph.createCheckpoint(); + const materialization = new V17MaterializationFallbackTrap( + graph, + 'bounded neighborhood reads must not full-materialize', + ); + const readPath = new V17PublicOpticReadPath(graph.worldline()); + + const first = await readPath.readNeighborhood(HUB_NODE_ID, { direction: 'out' }); + const firstEdges = edgesOf(first); + const firstCursor = cursorOf(first); + const shardPaths = readIdentityShardPaths(first); + + expect(firstEdges).toHaveLength(DEFAULT_PAGE_SIZE); + expect(firstCursor).not.toBeNull(); + expect(Reflect.get(first, 'completeness')).toBe('truncated'); + expect(shardPaths.filter((path) => path.startsWith('meta_')).length) + .toBeLessThanOrEqual(DEFAULT_PAGE_SIZE + 2); + const returned = new Set(firstEdges.map((edge) => edge.neighborId)); + const unreadShard = neighborIds + .filter((nodeId) => !returned.has(nodeId)) + .map((nodeId) => `meta_${computeShardKey(nodeId)}.cbor`) + .find((path) => !shardPaths.includes(path)); + expect(unreadShard).toBeDefined(); + + const allEdges = [...firstEdges]; + let cursor = firstCursor; + while (cursor !== null) { + const page = await readPath.readNeighborhood(HUB_NODE_ID, { + direction: 'out', + limit: DEFAULT_PAGE_SIZE, + cursor, + }); + allEdges.push(...edgesOf(page)); + cursor = cursorOf(page); + } + expect(new Set(allEdges.map((edge) => edge.neighborId))).toEqual(new Set(neighborIds)); + expect(allEdges).toHaveLength(neighborIds.length); + materialization.expectUnused(); + }); + + it('rejects oversized pages and cursors from another reading or checkpoint', async () => { + const fixture = await V17CheckpointTailOpticGraphFixture.openEmpty('v19-bounded-neighborhood-cursor'); + const graph = fixture.graph; + await graph.patch((patch) => { + patch.addNode(HUB_NODE_ID); + patch.addNode('node:first'); + patch.addNode('node:second'); + patch.addEdge(HUB_NODE_ID, 'node:first', EDGE_LABEL); + patch.addEdge(HUB_NODE_ID, 'node:second', EDGE_LABEL); + }); + await graph.materialize(); + await graph.createCheckpoint(); + const readPath = new V17PublicOpticReadPath(graph.worldline()); + const first = await readPath.readNeighborhood(HUB_NODE_ID, { direction: 'out', limit: 1 }); + const cursor = requireCursor(first); + + await expect(readPath.readNeighborhood(HUB_NODE_ID, { + direction: 'out', + limit: MAX_PAGE_SIZE + 1, + })).rejects.toMatchObject({ + code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', + context: { field: 'limit', max: MAX_PAGE_SIZE }, + }); + await expect(readPath.readNeighborhood(HUB_NODE_ID, { + direction: 'in', + limit: 1, + cursor, + })).rejects.toMatchObject({ + code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', + context: { field: 'cursor' }, + }); + + await graph.patch((patch) => { + patch.addNode('node:new-checkpoint'); + }); + await graph.materialize(); + await graph.createCheckpoint(); + await expect(readPath.readNeighborhood(HUB_NODE_ID, { + direction: 'out', + limit: 1, + cursor, + })).rejects.toMatchObject({ + code: 'E_OPTIC_NEIGHBORHOOD_OPTIONS', + context: { field: 'cursor' }, + }); + }); + + it('continues across directions and labels that share one neighbor bitmap ID', async () => { + const fixture = await V17CheckpointTailOpticGraphFixture.openEmpty('v19-bounded-neighborhood-order'); + const graph = fixture.graph; + await graph.patch((patch) => { + patch.addNode(HUB_NODE_ID); + patch.addNode('node:incoming'); + patch.addNode('node:outgoing'); + patch.addEdge('node:incoming', HUB_NODE_ID, 'incoming'); + patch.addEdge(HUB_NODE_ID, 'node:outgoing', 'zeta'); + patch.addEdge(HUB_NODE_ID, 'node:outgoing', 'alpha'); + }); + await graph.materialize(); + await graph.createCheckpoint(); + const readPath = new V17PublicOpticReadPath(graph.worldline()); + const edges: ReadEdge[] = []; + let cursor: string | null = null; + + do { + const page = await readPath.readNeighborhood(HUB_NODE_ID, { + direction: 'both', + limit: 1, + ...(cursor === null ? {} : { cursor }), + }); + edges.push(...edgesOf(page)); + cursor = cursorOf(page); + } while (cursor !== null); + + expect(edges).toEqual([ + { direction: 'in', neighborId: 'node:incoming', label: 'incoming' }, + { direction: 'out', neighborId: 'node:outgoing', label: 'alpha' }, + { direction: 'out', neighborId: 'node:outgoing', label: 'zeta' }, + ]); + }); +}); + +type ReadEdge = { + readonly direction: 'in' | 'out'; + readonly neighborId: string; + readonly label: string; +}; + +function edgesOf(result: object): readonly ReadEdge[] { + const edges = Reflect.get(result, 'edges'); + if (!Array.isArray(edges)) { + throw new Error('expected neighborhood edges array'); + } + return edges as readonly ReadEdge[]; +} + +function cursorOf(result: object): string | null { + const cursor = Reflect.get(result, 'cursor'); + if (cursor !== null && typeof cursor !== 'string') { + throw new Error('expected neighborhood cursor'); + } + return cursor; +} + +function requireCursor(result: object): string { + const cursor = cursorOf(result); + if (cursor === null) { + throw new Error('expected truncated neighborhood cursor'); + } + return cursor; +} + +function readIdentityShardPaths(result: object): readonly string[] { + const readIdentity = Reflect.get(result, 'readIdentity'); + const shards = Reflect.get(readIdentity, 'checkpointIndexShards'); + if (!Array.isArray(shards)) { + throw new Error('expected checkpointIndexShards array'); + } + return shards.map((shard) => String(Reflect.get(shard, 'path'))); +} diff --git a/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts b/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts index d95c31f04..bfbd836d6 100644 --- a/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts +++ b/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts @@ -103,13 +103,14 @@ describe('CheckpointShardFactReader manifest-backed routing', () => { nodeId: NODE_ID, direction: 'out', labels: [], - })).resolves.toEqual([ - Object.freeze({ + })).resolves.toMatchObject({ + cursor: null, + edges: [{ direction: 'out', neighborId: NEIGHBOR_ID, label: EDGE_LABEL, - }), - ]); + }], + }); }); }); diff --git a/test/unit/domain/utils/roaring.test.ts b/test/unit/domain/utils/roaring.test.ts index 68952c39d..59c2b6ca9 100644 --- a/test/unit/domain/utils/roaring.test.ts +++ b/test/unit/domain/utils/roaring.test.ts @@ -46,7 +46,15 @@ class FakeBitmap implements RoaringBitmapSubset { } toArray(): number[] { - return [...this._values]; + return [...this._values].sort((left, right) => left - right); + } + + rank(value: number): number { + return this.toArray().filter((candidate) => candidate <= value).length; + } + + at(index: number): number | undefined { + return this.toArray().at(index); } [Symbol.iterator](): Iterator { diff --git a/test/unit/infrastructure/adapters/RoaringLoaderAdapter.test.ts b/test/unit/infrastructure/adapters/RoaringLoaderAdapter.test.ts index a7036e381..4750d1b3f 100644 --- a/test/unit/infrastructure/adapters/RoaringLoaderAdapter.test.ts +++ b/test/unit/infrastructure/adapters/RoaringLoaderAdapter.test.ts @@ -48,7 +48,15 @@ class FakeBitmap implements RoaringBitmapSubset { } toArray(): number[] { - return [...this._values]; + return [...this._values].sort((left, right) => left - right); + } + + rank(value: number): number { + return this.toArray().filter((candidate) => candidate <= value).length; + } + + at(index: number): number | undefined { + return this.toArray().at(index); } [Symbol.iterator](): Iterator { diff --git a/test/unit/scripts/source-size-inventory-command.test.ts b/test/unit/scripts/source-size-inventory-command.test.ts index d55826269..6320e1a0d 100644 --- a/test/unit/scripts/source-size-inventory-command.test.ts +++ b/test/unit/scripts/source-size-inventory-command.test.ts @@ -20,7 +20,6 @@ const SOURCE_OVER_BUDGET_PATHS = Object.freeze([ 'src/domain/services/audit/AuditChainVerifier.ts', 'src/domain/services/controllers/CheckpointController.ts', 'src/domain/services/optic/CheckpointBasisManifest.ts', - 'src/domain/services/optic/CheckpointShardFactReader.ts', 'src/domain/services/state/WarpState.ts', ]); From a7fa7d142c1c75fc50680a37122b69733d36aba2 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 13 Jul 2026 14:44:24 -0700 Subject: [PATCH 4/7] Fix traversal resume across bounded neighborhood pages --- .../CheckpointNeighborhoodBitmapPager.ts | 14 ++- .../optic/CheckpointNeighborhoodPageReader.ts | 6 + .../optic/CheckpointTailTraversalReader.ts | 48 ++++---- .../optic/CheckpointTailWitnessLocator.ts | 63 ++++++---- .../v18TraversalOpticReadBasis.test.ts | 23 ++-- .../CheckpointTailTraversalReader.test.ts | 114 ++++++++++++++++++ 6 files changed, 201 insertions(+), 67 deletions(-) create mode 100644 test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts diff --git a/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts b/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts index 6318dac64..58bb4643c 100644 --- a/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts +++ b/src/domain/services/optic/CheckpointNeighborhoodBitmapPager.ts @@ -47,21 +47,23 @@ export type NeighborhoodBitmapPageResult = { readonly edges: NeighborhoodOpticEdge[]; readonly hasMore: boolean; readonly last: NeighborhoodCandidatePosition | null; + readonly resumeAfter: Array; }; export async function collectNeighborhoodBitmapPage( options: NeighborhoodCollectOptions, ): Promise { const edges: NeighborhoodOpticEdge[] = []; - let last: NeighborhoodCandidatePosition | null = null; + const resumeAfter: Array = []; + let last = options.after; for (const direction of readableDirections(options.direction, options.after)) { - const result = await collectDirection({ direction, edges, initialLast: last, options }); + const result = await collectDirection({ direction, edges, initialLast: last, options, resumeAfter }); if (result.hasMore) { - return { edges, ...result }; + return { edges, resumeAfter, ...result }; } last = result.last; } - return { edges, hasMore: false, last }; + return { edges, hasMore: false, last, resumeAfter }; } export function createNeighborhoodCandidateHeap( @@ -87,8 +89,9 @@ async function collectDirection( readonly edges: NeighborhoodOpticEdge[]; readonly initialLast: NeighborhoodCandidatePosition | null; readonly options: NeighborhoodCollectOptions; + readonly resumeAfter: Array; }, -): Promise> { +): Promise> { const { direction, edges, options } = context; const heap = await options.loader.readCandidateHeap({ ...options, direction }); let last = context.initialLast; @@ -101,6 +104,7 @@ async function collectDirection( if (edges.length === options.limit) { return { hasMore: true, last }; } + context.resumeAfter.push(last); edges.push(Object.freeze({ direction, neighborId, label: candidate.label })); last = candidatePosition(candidate); } diff --git a/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts b/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts index 3a5d83d51..189f46825 100644 --- a/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts +++ b/src/domain/services/optic/CheckpointNeighborhoodPageReader.ts @@ -57,6 +57,7 @@ export type CheckpointShardNeighborhoodReadOptions = { export type CheckpointShardNeighborhoodPage = { readonly edges: readonly NeighborhoodOpticEdge[]; readonly cursor: string | null; + readonly resumeCursors: readonly (string | null)[]; readonly checkpointIndexShards: readonly ReadIdentityIndexShard[]; }; @@ -256,6 +257,7 @@ function completedPage( edges: NeighborhoodOpticEdge[]; hasMore: boolean; last: NeighborhoodCandidatePosition | null; + resumeAfter: Array; }, ): CheckpointShardNeighborhoodPage { const cursor = collected.hasMore && collected.last !== null @@ -264,6 +266,9 @@ function completedPage( return Object.freeze({ edges: Object.freeze(collected.edges), cursor, + resumeCursors: Object.freeze(collected.resumeAfter.map((position) => ( + position === null ? null : encodeNeighborhoodCursor(scope, position) + ))), checkpointIndexShards: evidence.values(), }); } @@ -304,6 +309,7 @@ function emptyPage(evidence: ShardEvidence): CheckpointShardNeighborhoodPage { return Object.freeze({ edges: Object.freeze([]), cursor: null, + resumeCursors: Object.freeze([]), checkpointIndexShards: evidence.values(), }); } diff --git a/src/domain/services/optic/CheckpointTailTraversalReader.ts b/src/domain/services/optic/CheckpointTailTraversalReader.ts index 8908dbb17..2bb61096b 100644 --- a/src/domain/services/optic/CheckpointTailTraversalReader.ts +++ b/src/domain/services/optic/CheckpointTailTraversalReader.ts @@ -15,7 +15,14 @@ import TraversalOpticReadResult, { type TraversalNeighborhoodRead = ( nodeId: string, options: NeighborhoodOpticReadOptions, -) => Promise; +) => Promise; + +export type TraversalNeighborhoodReadResult = Pick< + NeighborhoodOpticReadResult, + 'edges' | 'cursor' | 'readIdentity' +> & { + readonly resumeCursors: readonly (string | null)[]; +}; type NormalizedTraversalOptions = { readonly startNodeId: string; @@ -98,7 +105,7 @@ export default class CheckpointTailTraversalReader { current: TraversalOpticFrontierEntry, traversal: NormalizedTraversalOptions, resultEdges: readonly TraversalOpticEdge[], - ): Promise { + ): Promise { const remainingEdgeBudget = traversal.maxEdges - resultEdges.length; return await this._readNeighborhood(current.nodeId, { direction: traversal.direction, @@ -112,7 +119,7 @@ export default class CheckpointTailTraversalReader { function processNeighborhoodRead( state: TraversalRunState, current: TraversalOpticFrontierEntry, - neighborhood: NeighborhoodOpticReadResult, + neighborhood: TraversalNeighborhoodReadResult, ): TraversalOpticReadResult | null { const depth = current.depth + 1; for (let edgeIndex = 0; edgeIndex < neighborhood.edges.length; edgeIndex += 1) { @@ -124,7 +131,7 @@ function processNeighborhoodRead( state, current, neighborId: edge.neighborId, - edgeIndex, + resumeCursor: resumeCursorForEdge(neighborhood, edgeIndex), }); if (boundaryResult !== null) { return boundaryResult; @@ -142,7 +149,7 @@ function maybeOpenForNodeLimit(options: { readonly state: TraversalRunState; readonly current: TraversalOpticFrontierEntry; readonly neighborId: string; - readonly edgeIndex: number; + readonly resumeCursor: string | null; }): TraversalOpticReadResult | null { if (options.state.visited.has(options.neighborId) || options.state.visited.size < options.state.traversal.maxNodes) { return null; @@ -150,7 +157,7 @@ function maybeOpenForNodeLimit(options: { options.state.frontier.unshift(frontierEntry( options.current.nodeId, options.current.depth, - blockedEdgeCursor(options.current, options.edgeIndex), + options.resumeCursor, )); return openTraversalResult(options.state); } @@ -192,29 +199,18 @@ function maybeOpenForNeighborhoodCursor( return openTraversalResult(state); } -function blockedEdgeCursor( - current: TraversalOpticFrontierEntry, +function resumeCursorForEdge( + neighborhood: TraversalNeighborhoodReadResult, edgeIndex: number, -): string { - return String(traversalEdgeCursorOffset(current.edgeCursor) + edgeIndex); -} - -function traversalEdgeCursorOffset(cursor: string | null): number { - if (cursor === null || cursor.length === 0) { - return 0; - } - const parsed = Number.parseInt(cursor, 10); - if (!isCanonicalTraversalEdgeCursor(parsed, cursor)) { - throw new QueryError('Traversal optic frontier edge cursor must be a non-negative integer string.', { - code: 'E_OPTIC_TRAVERSAL_OPTIONS', - context: { field: 'cursor.frontier.edgeCursor' }, +): string | null { + const cursor = neighborhood.resumeCursors[edgeIndex]; + if (cursor === undefined) { + throw new QueryError('Traversal neighborhood page omitted an edge resume cursor.', { + code: 'E_OPTIC_BASIS_INVALID', + context: { edgeIndex }, }); } - return parsed; -} - -function isCanonicalTraversalEdgeCursor(parsed: number, raw: string): boolean { - return Number.isInteger(parsed) && parsed >= 0 && String(parsed) === raw; + return cursor; } function createTraversalRunState( diff --git a/src/domain/services/optic/CheckpointTailWitnessLocator.ts b/src/domain/services/optic/CheckpointTailWitnessLocator.ts index 758d9939a..419851dae 100644 --- a/src/domain/services/optic/CheckpointTailWitnessLocator.ts +++ b/src/domain/services/optic/CheckpointTailWitnessLocator.ts @@ -1,24 +1,25 @@ import CheckpointTailBasisLoader, { type CheckpointTailIndexBasis, } from './CheckpointTailBasisLoader.ts'; -import CheckpointShardFactReader from './CheckpointShardFactReader.ts'; +import CheckpointShardFactReader, { + type CheckpointShardNeighborhoodPage, +} from './CheckpointShardFactReader.ts'; import CheckpointTailFactReducer from './CheckpointTailFactReducer.ts'; import CheckpointTailReadFailure from './CheckpointTailReadFailure.ts'; import CheckpointTailReadIdentityBuilder from './CheckpointTailReadIdentityBuilder.ts'; -import CheckpointTailTraversalReader from './CheckpointTailTraversalReader.ts'; +import CheckpointTailTraversalReader, { + type TraversalNeighborhoodReadResult, +} from './CheckpointTailTraversalReader.ts'; import QueryError from '../../errors/QueryError.ts'; import type { Direction } from '../../../ports/NeighborProviderPort.ts'; import NodeOpticReadResult from './NodeOpticReadResult.ts'; import NodePropertyOpticReadResult from './NodePropertyOpticReadResult.ts'; import type CheckpointTailOpticSource from './CheckpointTailOpticSource.ts'; import CheckpointTailWitnessScan, { type TailWitnessScan } from './CheckpointTailWitnessScan.ts'; -import NeighborhoodOpticReadResult, { - type NeighborhoodOpticEdge, -} from './NeighborhoodOpticReadResult.ts'; +import NeighborhoodOpticReadResult from './NeighborhoodOpticReadResult.ts'; import type { NeighborhoodOpticReadOptions } from './NeighborhoodOptic.ts'; import Optic from './Optic.ts'; -import OpticReadTarget, { type OpticKindValue } from './OpticReadTarget.ts'; -import OpticSupportRule from './OpticSupportRule.ts'; +import type { OpticKindValue } from './OpticReadTarget.ts'; import type TraversalOpticReadResult from './TraversalOpticReadResult.ts'; import type { TraversalOpticReadOptions } from './TraversalOptic.ts'; import type ReadIdentity from './ReadIdentity.ts'; @@ -26,6 +27,11 @@ import type { ReadIdentityIndexShard } from './ReadIdentity.ts'; const DEFAULT_MAX_TAIL_PATCHES = 10_000; +type WitnessedNeighborhoodPage = TraversalNeighborhoodReadResult & { + readonly nodeId: string; + readonly direction: Direction; +}; + export default class CheckpointTailWitnessLocator { private readonly _graphName: string; private readonly _basisLoader: CheckpointTailBasisLoader; @@ -179,6 +185,21 @@ export default class CheckpointTailWitnessLocator { nodeId: string, options: NeighborhoodOpticReadOptions, ): Promise { + const page = await this._readWitnessedNeighborhoodPage(nodeId, options); + return new NeighborhoodOpticReadResult({ + nodeId: page.nodeId, + direction: page.direction, + edges: page.edges, + completeness: page.cursor === null ? 'complete' : 'truncated', + cursor: page.cursor, + readIdentity: page.readIdentity, + }); + } + + private async _readWitnessedNeighborhoodPage( + nodeId: string, + options: NeighborhoodOpticReadOptions, + ): Promise { const direction = normalizeDirection(options.direction); const labels = normalizeLabels(options.labels ?? []); const basis = await this._basisLoader.load(); @@ -191,7 +212,7 @@ export default class CheckpointTailWitnessLocator { cursor: options.cursor ?? null, limit: options.limit ?? null, }); - return neighborhoodReadResult({ + return witnessedNeighborhoodPage({ basis, direction, labels, @@ -206,14 +227,13 @@ export default class CheckpointTailWitnessLocator { optic: Optic, options: TraversalOpticReadOptions, ): Promise { - return await this._traversalReaderFor(optic).read(optic.nodeId(), options); + return await this._traversalReader().read(optic.nodeId(), options); } - private _traversalReaderFor(optic: Optic): CheckpointTailTraversalReader { + private _traversalReader(): CheckpointTailTraversalReader { return new CheckpointTailTraversalReader({ - readNeighborhood: async (nodeId, readOptions) => await this.readNeighborhood( - optic.withTarget(OpticReadTarget.neighborhood(nodeId), OpticSupportRule.neighborhood()), - readOptions, + readNeighborhood: async (nodeId, readOptions) => ( + await this._readWitnessedNeighborhoodPage(nodeId, readOptions) ), }); } @@ -313,27 +333,22 @@ function normalizeLabels(labels: readonly string[]): readonly string[] { return Object.freeze([...new Set(labels)].sort()); } -function neighborhoodReadResult(options: { +function witnessedNeighborhoodPage(options: { readonly basis: CheckpointTailIndexBasis; readonly direction: Direction; readonly labels: readonly string[]; readonly nodeId: string; readonly readIdentityBuilder: CheckpointTailReadIdentityBuilder; readonly tail: TailWitnessScan; - readonly page: { - readonly edges: readonly NeighborhoodOpticEdge[]; - readonly cursor: string | null; - readonly checkpointIndexShards: readonly ReadIdentityIndexShard[]; - }; -}): NeighborhoodOpticReadResult { - const readIdentity = neighborhoodReadIdentity(options); - return new NeighborhoodOpticReadResult({ + readonly page: CheckpointShardNeighborhoodPage; +}): WitnessedNeighborhoodPage { + return Object.freeze({ nodeId: options.nodeId, direction: options.direction, edges: options.page.edges, - completeness: options.page.cursor === null ? 'complete' : 'truncated', cursor: options.page.cursor, - readIdentity, + resumeCursors: options.page.resumeCursors, + readIdentity: neighborhoodReadIdentity(options), }); } diff --git a/test/conformance/v18TraversalOpticReadBasis.test.ts b/test/conformance/v18TraversalOpticReadBasis.test.ts index 43c98f81f..d79c98c89 100644 --- a/test/conformance/v18TraversalOpticReadBasis.test.ts +++ b/test/conformance/v18TraversalOpticReadBasis.test.ts @@ -36,13 +36,12 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { completeness: 'goal-found', cursor: null, edges: [ - { fromNodeId: ROOT_NODE_ID, toNodeId: ALPHA_NODE_ID, label: LINK_LABEL, depth: 1 }, { fromNodeId: ROOT_NODE_ID, toNodeId: BETA_NODE_ID, label: LINK_LABEL, depth: 1 }, - { fromNodeId: ALPHA_NODE_ID, toNodeId: GAMMA_NODE_ID, label: LINK_LABEL, depth: 2 }, + { fromNodeId: ROOT_NODE_ID, toNodeId: ALPHA_NODE_ID, label: LINK_LABEL, depth: 1 }, { fromNodeId: BETA_NODE_ID, toNodeId: DELTA_NODE_ID, label: LINK_LABEL, depth: 2 }, ], }); - expect(Reflect.get(result, 'readIdentities')).toHaveLength(3); + expect(Reflect.get(result, 'readIdentities')).toHaveLength(2); materialization.expectUnused(); }); @@ -68,18 +67,18 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { expect(first).toMatchObject({ completeness: 'frontier-open', frontier: [ - { nodeId: ALPHA_NODE_ID, depth: 1, edgeCursor: null }, { nodeId: BETA_NODE_ID, depth: 1, edgeCursor: null }, + { nodeId: ALPHA_NODE_ID, depth: 1, edgeCursor: null }, ], edges: [ - { fromNodeId: ROOT_NODE_ID, toNodeId: ALPHA_NODE_ID, label: LINK_LABEL, depth: 1 }, { fromNodeId: ROOT_NODE_ID, toNodeId: BETA_NODE_ID, label: LINK_LABEL, depth: 1 }, + { fromNodeId: ROOT_NODE_ID, toNodeId: ALPHA_NODE_ID, label: LINK_LABEL, depth: 1 }, ], }); expect(cursor).toMatchObject({ frontier: [ - { nodeId: ALPHA_NODE_ID, depth: 1, edgeCursor: null }, { nodeId: BETA_NODE_ID, depth: 1, edgeCursor: null }, + { nodeId: ALPHA_NODE_ID, depth: 1, edgeCursor: null }, ], visitedNodeIds: [ALPHA_NODE_ID, BETA_NODE_ID, ROOT_NODE_ID], }); @@ -87,8 +86,8 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { completeness: 'complete', cursor: null, edges: [ - { fromNodeId: ALPHA_NODE_ID, toNodeId: GAMMA_NODE_ID, label: LINK_LABEL, depth: 2 }, { fromNodeId: BETA_NODE_ID, toNodeId: DELTA_NODE_ID, label: LINK_LABEL, depth: 2 }, + { fromNodeId: ALPHA_NODE_ID, toNodeId: GAMMA_NODE_ID, label: LINK_LABEL, depth: 2 }, ], }); }); @@ -115,19 +114,19 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { expect(first).toMatchObject({ completeness: 'frontier-open', frontier: [ - { nodeId: ROOT_NODE_ID, depth: 0, edgeCursor: '1' }, - { nodeId: ALPHA_NODE_ID, depth: 1, edgeCursor: null }, + { nodeId: ROOT_NODE_ID, depth: 0, edgeCursor: expect.stringMatching(/^warp-neighborhood-v1:/) }, + { nodeId: BETA_NODE_ID, depth: 1, edgeCursor: null }, ], edges: [ - { fromNodeId: ROOT_NODE_ID, toNodeId: ALPHA_NODE_ID, label: LINK_LABEL, depth: 1 }, + { fromNodeId: ROOT_NODE_ID, toNodeId: BETA_NODE_ID, label: LINK_LABEL, depth: 1 }, ], }); expect(second).toMatchObject({ completeness: 'complete', edges: [ - { fromNodeId: ROOT_NODE_ID, toNodeId: BETA_NODE_ID, label: LINK_LABEL, depth: 1 }, - { fromNodeId: ALPHA_NODE_ID, toNodeId: GAMMA_NODE_ID, label: LINK_LABEL, depth: 2 }, + { fromNodeId: ROOT_NODE_ID, toNodeId: ALPHA_NODE_ID, label: LINK_LABEL, depth: 1 }, { fromNodeId: BETA_NODE_ID, toNodeId: DELTA_NODE_ID, label: LINK_LABEL, depth: 2 }, + { fromNodeId: ALPHA_NODE_ID, toNodeId: GAMMA_NODE_ID, label: LINK_LABEL, depth: 2 }, ], }); }); diff --git a/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts b/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts new file mode 100644 index 000000000..150f1734c --- /dev/null +++ b/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from 'vitest'; +import CheckpointTailTraversalReader, { + type TraversalNeighborhoodReadResult, +} from '../../../../../src/domain/services/optic/CheckpointTailTraversalReader.ts'; +import ReadIdentity from '../../../../../src/domain/services/optic/ReadIdentity.ts'; + +const ROOT = 'node:root'; +const ALPHA = 'node:alpha'; +const BETA = 'node:beta'; +const GAMMA = 'node:gamma'; +const DELTA = 'node:delta'; +const OPAQUE_AFTER_BETA = 'warp-neighborhood-v1:checkpoint|node%3Aroot|out||out|7|link'; + +const READ_IDENTITY = new ReadIdentity({ + worldline: 'traversal-reader-test', + entityAspect: 'neighborhood', + checkpointSha: 'checkpoint', + checkpointFrontier: [], + checkpointIndexShards: [], + tailWitnesses: [], + reducerVersion: 'test', + projectionVersion: 'test', +}); + +describe('CheckpointTailTraversalReader', () => { + it('resumes a node-budget boundary with the opaque cursor before the blocked edge', async () => { + const observedCursors: Array = []; + const reader = new CheckpointTailTraversalReader({ + readNeighborhood: (nodeId, options) => { + const cursor = options.cursor ?? null; + observedCursors.push(cursor); + return Promise.resolve(neighborhoodPage(nodeId, cursor)); + }, + }); + + const first = await reader.read(ROOT, { + direction: 'out', + maxDepth: 2, + maxNodes: 2, + maxEdges: 10, + }); + expect(first).toMatchObject({ + completeness: 'frontier-open', + frontier: [ + { nodeId: ROOT, depth: 0, edgeCursor: OPAQUE_AFTER_BETA }, + { nodeId: BETA, depth: 1, edgeCursor: null }, + ], + edges: [ + { fromNodeId: ROOT, toNodeId: BETA, label: 'link', depth: 1 }, + ], + }); + if (first.cursor === null) { + throw new Error('expected an open traversal cursor'); + } + + const second = await reader.read(ROOT, { + direction: 'out', + maxDepth: 2, + maxNodes: 10, + maxEdges: 10, + cursor: first.cursor, + }); + + expect(second).toMatchObject({ + completeness: 'complete', + edges: [ + { fromNodeId: ROOT, toNodeId: ALPHA, label: 'link', depth: 1 }, + { fromNodeId: BETA, toNodeId: DELTA, label: 'link', depth: 2 }, + { fromNodeId: ALPHA, toNodeId: GAMMA, label: 'link', depth: 2 }, + ], + }); + expect(observedCursors).toEqual([null, OPAQUE_AFTER_BETA, null, null]); + }); +}); + +function neighborhoodPage( + nodeId: string, + cursor: string | null, +): TraversalNeighborhoodReadResult { + if (nodeId === ROOT && cursor === null) { + return page([ + { direction: 'out', neighborId: BETA, label: 'link' }, + { direction: 'out', neighborId: ALPHA, label: 'link' }, + ], [null, OPAQUE_AFTER_BETA]); + } + if (nodeId === ROOT && cursor === OPAQUE_AFTER_BETA) { + return page([ + { direction: 'out', neighborId: ALPHA, label: 'link' }, + ], [OPAQUE_AFTER_BETA]); + } + if (nodeId === BETA) { + return page([ + { direction: 'out', neighborId: DELTA, label: 'link' }, + ], [null]); + } + if (nodeId === ALPHA) { + return page([ + { direction: 'out', neighborId: GAMMA, label: 'link' }, + ], [null]); + } + return page([], []); +} + +function page( + edges: TraversalNeighborhoodReadResult['edges'], + resumeCursors: readonly (string | null)[], +): TraversalNeighborhoodReadResult { + return { + edges, + cursor: null, + resumeCursors, + readIdentity: READ_IDENTITY, + }; +} From 8fa569111ee3553da2c89bcb1cea8c07c1b30f70 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 13 Jul 2026 15:16:22 -0700 Subject: [PATCH 5/7] Fix v19 receipt and composition contracts --- CHANGELOG.md | 8 +- README.md | 2 +- bin/warp-graph.ts | 3 + docs/migrations/v19/README.md | 62 +++++++----- docs/topics/api/README.md | 2 +- docs/topics/getting-started.md | 2 +- docs/topics/reference.md | 94 +++++++++---------- index.ts | 4 - src/domain/api/DraftTimelineRuntime.ts | 77 ++++++++------- src/domain/api/Intent.ts | 6 +- src/domain/api/JoinReceipt.ts | 32 +++++-- src/domain/api/ReadReceipt.ts | 47 ++++++---- src/domain/api/ReadingRuntime.ts | 4 +- src/domain/api/ReceiptOutcome.ts | 15 ++- src/domain/api/Timeline.ts | 4 +- src/domain/api/TimelineView.ts | 4 +- src/domain/api/WriteReceipt.ts | 10 +- src/domain/api/openWarp.ts | 17 ++-- src/domain/graph/LegacyPropertyValue.ts | 32 +------ .../services/optic/NeighborhoodPageCursor.ts | 2 +- src/domain/types/PropValue.ts | 51 +++++++--- .../v18FirstUseOpticsHonesty.test.ts | 2 +- .../v18TraversalOpticReadBasis.test.ts | 40 ++++---- test/type-check/v19-consumer.ts | 6 ++ test/unit/domain/ReceiptOutcome.test.ts | 71 ++++++++++++-- test/unit/domain/WarpFacade.test.ts | 45 ++++++++- .../CheckpointTailTraversalReader.test.ts | 36 +++---- .../scripts/v19-public-api-boundary.test.ts | 59 ++++++++++-- 28 files changed, 459 insertions(+), 278 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7a3b72f..443c0c5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Added the v19 `openWarp()` product opener plus `Warp` and `Timeline` - facade handles to the package root. +- Added the v19 `openWarp()` product opener. `Warp` and `Timeline` are returned + as runtime handles and exported from the root as companion types. - Moved supported storage, formal WARP, and operator inspection exports out of the package root into the explicit `storage`, `advanced`, and `diagnostics` subpaths. The package root now rejects graph-first compatibility nouns @@ -38,8 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 CBOR adapter now owns the named checkpoint artifact encoding for runtime checkpoint creation and loading instead of exposing a stale single `state.cbor` result. -- Added visible-state scope helpers to the `diagnostics` subpath so - materialized-state inspection has an explicit non-legacy import path. +- Reduced the `diagnostics` subpath to receipt-based `inspectReceipt()`; + graph-diff and materialized-state helpers are no longer public contracts. - Moved receipt canonical JSON and ORSet/full-state wire encoding out of domain types and storage adapters into infrastructure codec modules; `ORSet` no longer exposes `serialize()` or `deserialize()`. diff --git a/README.md b/README.md index 0631a7ebd..5c4c24dc2 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ const role = await events.read( }) ); -if (role.receipt.outcome === 'resolved') { +if (role.receipt.outcome === 'accepted') { console.log(role.value); console.log(role.receipt.evidence); } else { diff --git a/bin/warp-graph.ts b/bin/warp-graph.ts index b6a34008a..4080d3935 100755 --- a/bin/warp-graph.ts +++ b/bin/warp-graph.ts @@ -1,10 +1,13 @@ #!/usr/bin/env node import process from 'node:process'; +import { installDefaultRuntimeHostNodePorts } from '../src/application/RuntimeHostNodeDefaults.ts'; import { EXIT_CODES, HELP_TEXT, CliError, parseArgs, usageError } from './cli/infrastructure.ts'; import { stableStringify, compactStringify } from './presenters/json.ts'; import { COMMANDS } from './cli/commands/registry.ts'; +installDefaultRuntimeHostNodePorts(); + // Output format must be captured from raw process.argv BEFORE parseArgs() runs. // If parseArgs() itself throws (e.g., unknown flag, malformed input), the `options` // object will not exist, so the error handler cannot read `options.json`. By diff --git a/docs/migrations/v19/README.md b/docs/migrations/v19/README.md index 6840bda14..eba4d3e8a 100644 --- a/docs/migrations/v19/README.md +++ b/docs/migrations/v19/README.md @@ -228,30 +228,44 @@ and joins first. ## Symbol Disposition Table -| v18 or earlier symbol | v19 path | Notes | -| ------------------------ | ------------------------------------- | ------------------------------------------------- | -| `openWarpWorldline()` | root `openWarp().timeline(name)` | preferred application opener | -| `WarpWorldline` | root `Timeline` | public handle rename | -| `GitGraphAdapter` | `storage` `GitStorageAdapter` | graph name removed | -| `InMemoryGraphAdapter` | `storage` `MemoryStorageAdapter` | graph name removed | -| `GraphPersistencePort` | root `StorageAdapter` for app options | old graph-shaped port removed from public API | -| `commit((patch) => ...)` | `timeline.write(intent.*)` | receipt-returning | -| `PatchBuilder` | removed | replace with intent builders | -| `PatchSession` | removed | replace with receipt-returning writes | -| `createNodeAdd()` | removed | use intent builders | -| `createEdgeAdd()` | removed | use intent builders | -| `createPropSet()` | removed | use intent builders | -| `openWarpGraph()` | removed | replace diagnostics with explicit diagnostic APIs | -| `WarpApp` | removed | no root default export in v19 | -| `WarpCore` | removed | replace diagnostics with explicit diagnostic APIs | -| `GraphNode` | removed | no public export | -| `GraphDiff` | removed | no public-handle comparison API ships in v19 | -| `Optic` | `advanced` | readings are root | -| `Coordinate` | `advanced` or receipt fields | ticks are root | -| `Observer` | removed | readings are first-use root | -| `Strand` | removed | drafts are first-use root | -| `Braid` | removed | joins are first-use root | -| Continuum evidence nouns | removed | receipt evidence stays root-facing | +| v18 or earlier symbol | v19 path | Notes | +| -------------------------- | ------------------------------------- | ------------------------------------------------- | +| `openWarpWorldline()` | root `openWarp().timeline(name)` | preferred application opener | +| `WarpWorldline` | root `Timeline` | public handle rename | +| `Warp` | root type | obtain the runtime handle from `openWarp()` | +| `Timeline` | root type | obtain the runtime handle from `warp.timeline()` | +| `DraftTimeline` | root type | obtain the runtime handle from `timeline.draft()` | +| `Intent` | root type | construct with the root `intent` builders | +| `Reading` | root type | construct with the root `reading` builders | +| `ReadingResult` | root type | returned by `timeline.read()` | +| `WriteReceipt` | root type | returned by `timeline.write()` | +| `ReadReceipt` | root type | returned on `ReadingResult.receipt` | +| `JoinReceipt` | root type | returned on `JoinResult.receipt` | +| `JoinResult` | root type | returned by `previewJoin()` and `join()` | +| `WarpStorage` | root `StorageAdapter` | storage option contract rename | +| `ReadReceiptOutcome` | root `ReadOutcome` | operation-specific outcome alias rename | +| `JoinReceiptOutcome` | root `JoinOutcome` | operation-specific outcome alias rename | +| `EdgePropertyIntentFields` | removed | no edge-property intent ships in the v19 root | +| `GitGraphAdapter` | `storage` `GitStorageAdapter` | graph name removed | +| `InMemoryGraphAdapter` | `storage` `MemoryStorageAdapter` | graph name removed | +| `GraphPersistencePort` | root `StorageAdapter` for app options | old graph-shaped port removed from public API | +| `commit((patch) => ...)` | `timeline.write(intent.*)` | receipt-returning | +| `PatchBuilder` | removed | replace with intent builders | +| `PatchSession` | removed | replace with receipt-returning writes | +| `createNodeAdd()` | removed | use intent builders | +| `createEdgeAdd()` | removed | use intent builders | +| `createPropSet()` | removed | use intent builders | +| `openWarpGraph()` | removed | replace diagnostics with explicit diagnostic APIs | +| `WarpApp` | removed | no root default export in v19 | +| `WarpCore` | removed | replace diagnostics with explicit diagnostic APIs | +| `GraphNode` | removed | no public export | +| `GraphDiff` | removed | no public-handle comparison API ships in v19 | +| `Optic` | `advanced` | readings are root | +| `Coordinate` | `advanced` or receipt fields | ticks are root | +| `Observer` | removed | readings are first-use root | +| `Strand` | removed | drafts are first-use root | +| `Braid` | removed | joins are first-use root | +| Continuum evidence nouns | removed | receipt evidence stays root-facing | ## Receipt Outcome Migration diff --git a/docs/topics/api/README.md b/docs/topics/api/README.md index cde9bbcf7..98fd0d92a 100644 --- a/docs/topics/api/README.md +++ b/docs/topics/api/README.md @@ -135,7 +135,7 @@ const role = await timeline.read( }) ); -if (role.receipt.outcome === 'resolved') { +if (role.receipt.outcome === 'accepted') { console.log(role.value); console.log(role.receipt.evidence); } else { diff --git a/docs/topics/getting-started.md b/docs/topics/getting-started.md index d3896b3ac..d5e5f817e 100644 --- a/docs/topics/getting-started.md +++ b/docs/topics/getting-started.md @@ -82,7 +82,7 @@ console.log(exists.value, exists.receipt); ``` Readings ask bounded questions. The receipt records how the runtime supported -the answer. A resolved receipt carries checkpoint-tail evidence. If no bounded +the answer. An accepted receipt carries checkpoint-tail evidence. If no bounded basis exists, `read()` returns an `obstructed` receipt with repair hints instead of materializing the whole timeline. diff --git a/docs/topics/reference.md b/docs/topics/reference.md index 9c1eb495d..985923e4d 100644 --- a/docs/topics/reference.md +++ b/docs/topics/reference.md @@ -29,9 +29,9 @@ First-use product API: `openWarp`, `intent`, `reading`, timelines, and receipts. Source: `index.ts`. Count: 3. ```text -intent @ index.ts#L18 -openWarp @ index.ts#L17 -reading @ index.ts#L19 +intent @ index.ts#L14 +openWarp @ index.ts#L13 +reading @ index.ts#L15 ``` ### Type exports @@ -39,49 +39,49 @@ reading @ index.ts#L19 Source: `index.ts`. Count: 43. ```text -DraftTimeline @ index.ts#L20 -EdgeIntentFields @ index.ts#L34 -Intent @ index.ts#L25 -IntentBuilders @ index.ts#L40 -IntentDescriptor @ index.ts#L35 -IntentKind @ index.ts#L36 -JoinMode @ index.ts#L41 -JoinOptions @ index.ts#L43 -JoinOutcome @ index.ts#L57 -JoinPolicy @ index.ts#L43 -JoinReceipt @ index.ts#L26 -JoinReceiptOptions @ index.ts#L41 -JoinResult @ index.ts#L27 -JoinResultOptions @ index.ts#L42 -NeighborhoodReadingFields @ index.ts#L45 -NodeIntentFields @ index.ts#L37 -NodeReadingFields @ index.ts#L46 -OpenWarpOptions @ index.ts#L32 -PropertyIntentFields @ index.ts#L38 -PropertyReadingFields @ index.ts#L47 -ReadEvidence @ index.ts#L55 -Reading @ index.ts#L28 -ReadingBuilders @ index.ts#L52 -ReadingDescriptor @ index.ts#L49 -ReadingDirection @ index.ts#L48 -ReadingKind @ index.ts#L50 -ReadingResult @ index.ts#L29 -ReadingResultOptions @ index.ts#L53 -ReadingValue @ index.ts#L53 -ReadOutcome @ index.ts#L58 -ReadReceipt @ index.ts#L30 -ReadReceiptOptions @ index.ts#L55 -Receipt @ index.ts#L54 -ReceiptOutcome @ index.ts#L59 -RepairHint @ index.ts#L62 -StorageAdapter @ index.ts#L32 -Tick @ index.ts#L23 -Timeline @ index.ts#L22 -TimelineView @ index.ts#L24 -Warp @ index.ts#L21 -WriteOutcome @ index.ts#L60 -WriteReceipt @ index.ts#L31 -WriteReceiptOptions @ index.ts#L63 +DraftTimeline @ index.ts#L16 +EdgeIntentFields @ index.ts#L30 +Intent @ index.ts#L21 +IntentBuilders @ index.ts#L36 +IntentDescriptor @ index.ts#L31 +IntentKind @ index.ts#L32 +JoinMode @ index.ts#L37 +JoinOptions @ index.ts#L39 +JoinOutcome @ index.ts#L53 +JoinPolicy @ index.ts#L39 +JoinReceipt @ index.ts#L22 +JoinReceiptOptions @ index.ts#L37 +JoinResult @ index.ts#L23 +JoinResultOptions @ index.ts#L38 +NeighborhoodReadingFields @ index.ts#L41 +NodeIntentFields @ index.ts#L33 +NodeReadingFields @ index.ts#L42 +OpenWarpOptions @ index.ts#L28 +PropertyIntentFields @ index.ts#L34 +PropertyReadingFields @ index.ts#L43 +ReadEvidence @ index.ts#L51 +Reading @ index.ts#L24 +ReadingBuilders @ index.ts#L48 +ReadingDescriptor @ index.ts#L45 +ReadingDirection @ index.ts#L44 +ReadingKind @ index.ts#L46 +ReadingResult @ index.ts#L25 +ReadingResultOptions @ index.ts#L49 +ReadingValue @ index.ts#L49 +ReadOutcome @ index.ts#L54 +ReadReceipt @ index.ts#L26 +ReadReceiptOptions @ index.ts#L51 +Receipt @ index.ts#L50 +ReceiptOutcome @ index.ts#L55 +RepairHint @ index.ts#L58 +StorageAdapter @ index.ts#L28 +Tick @ index.ts#L19 +Timeline @ index.ts#L18 +TimelineView @ index.ts#L20 +Warp @ index.ts#L17 +WriteOutcome @ index.ts#L56 +WriteReceipt @ index.ts#L27 +WriteReceiptOptions @ index.ts#L59 ``` ## Storage export surface @@ -189,7 +189,7 @@ ReceiptInspection @ diagnostics.ts#L7 Structured CLI errors for `--json` and `--ndjson` use the payload shape `{ error: { code, message, cause? } }` from the CLI entry point. -Source: `bin/warp-graph.ts#L129`. +Source: `bin/warp-graph.ts#L132`. ## Public error classes diff --git a/index.ts b/index.ts index 68cb53d7c..80d0594df 100644 --- a/index.ts +++ b/index.ts @@ -10,10 +10,6 @@ * operator inspection tools live under `@git-stunts/git-warp/diagnostics`. */ -import { installDefaultRuntimeHostNodePorts } from './src/application/RuntimeHostNodeDefaults.ts'; - -installDefaultRuntimeHostNodePorts(); - export { openWarp } from './src/domain/api/openWarp.ts'; export { intent } from './src/domain/api/IntentBuilders.ts'; export { reading } from './src/domain/api/ReadingBuilders.ts'; diff --git a/src/domain/api/DraftTimelineRuntime.ts b/src/domain/api/DraftTimelineRuntime.ts index 151c8695b..333f4cc39 100644 --- a/src/domain/api/DraftTimelineRuntime.ts +++ b/src/domain/api/DraftTimelineRuntime.ts @@ -26,15 +26,19 @@ type DraftWriteFields = { readonly intent: Intent; }; -type JoinResultFields = { +type JoinResultFieldsBase = { readonly runtime: WarpWorldline; readonly draft: DraftTimeline; readonly mode: 'preview' | 'join'; - readonly outcome: 'accepted' | 'rejected'; readonly patchShas: readonly string[]; - readonly reason?: string; }; +type JoinResultFields = JoinResultFieldsBase & + ( + | { readonly outcome: 'accepted'; readonly reason?: never } + | { readonly outcome: 'rejected'; readonly reason: string } + ); + type RejectedJoinFields = { readonly runtime: WarpWorldline; readonly draft: DraftTimeline; @@ -54,7 +58,7 @@ const draftStates = new WeakMap(); export async function createDraftTimeline( runtime: WarpWorldline, timelineName: string, - draftName: string, + draftName: string ): Promise { await runtime.createDraft(draftName); const state = createDraftState(runtime); @@ -62,12 +66,13 @@ export async function createDraftTimeline( name: draftName, timeline: timelineName, writer: runtime.writerId, - writeDraft: async (intent) => await writeDraftIntent({ - runtime, - draftName, - state, - intent, - }), + writeDraft: async (intent) => + await writeDraftIntent({ + runtime, + draftName, + state, + intent, + }), }); draftStates.set(draft, state); return draft; @@ -76,7 +81,7 @@ export async function createDraftTimeline( export async function previewDraftJoin( runtime: WarpWorldline, draft: DraftTimeline, - options: JoinOptions, + options: JoinOptions ): Promise { void options; requireDraftState(runtime, draft); @@ -93,7 +98,7 @@ export async function previewDraftJoin( export async function joinDraftTimeline( runtime: WarpWorldline, draft: DraftTimeline, - options: JoinOptions, + options: JoinOptions ): Promise { void options; const state = requireDraftState(runtime, draft); @@ -117,7 +122,7 @@ export async function joinDraftTimeline( function rejectedJoinPrecondition( runtime: WarpWorldline, draft: DraftTimeline, - state: DraftTimelineState, + state: DraftTimelineState ): JoinResult | null { if (state.joined) { return rejectedJoin({ runtime, draft, reason: 'Draft has already joined' }); @@ -178,7 +183,7 @@ async function writeDraftIntent(fields: DraftWriteFields): Promise const receipt = await executeIntentWrite( fields.runtime, fields.intent, - async (build) => await fields.runtime.patchDraft(fields.draftName, build), + async (build) => await fields.runtime.patchDraft(fields.draftName, build) ); if (receipt.outcome !== 'accepted') { return receipt; @@ -205,7 +210,7 @@ function rejectedJoin(fields: RejectedJoinFields): JoinResult { async function commitDraftIntents( runtime: WarpWorldline, - state: DraftTimelineState, + state: DraftTimelineState ): Promise { for (const intent of state.intents) { try { @@ -223,30 +228,34 @@ async function commitDraftIntents( function requireDraftState(runtime: WarpWorldline, draft: DraftTimeline): DraftTimelineState { const state = draftStates.get(draft); if (state === undefined || state.runtime !== runtime) { - throw new WarpError('DraftTimeline was not opened by this Timeline', 'E_DRAFT_RUNTIME_UNAVAILABLE'); + throw new WarpError( + 'DraftTimeline was not opened by this Timeline', + 'E_DRAFT_RUNTIME_UNAVAILABLE' + ); } return state; } function joinResult(fields: JoinResultFields): JoinResult { - const receipt = fields.reason === undefined - ? new JoinReceipt({ - timeline: fields.runtime.worldlineName, - writer: fields.runtime.writerId, - draft: fields.draft, - mode: fields.mode, - outcome: fields.outcome, - patchShas: fields.patchShas, - }) - : new JoinReceipt({ - timeline: fields.runtime.worldlineName, - writer: fields.runtime.writerId, - draft: fields.draft, - mode: fields.mode, - outcome: fields.outcome, - patchShas: fields.patchShas, - reason: fields.reason, - }); + const receipt = + fields.outcome === 'accepted' + ? new JoinReceipt({ + timeline: fields.runtime.worldlineName, + writer: fields.runtime.writerId, + draft: fields.draft, + mode: fields.mode, + outcome: fields.outcome, + patchShas: fields.patchShas, + }) + : new JoinReceipt({ + timeline: fields.runtime.worldlineName, + writer: fields.runtime.writerId, + draft: fields.draft, + mode: fields.mode, + outcome: fields.outcome, + patchShas: fields.patchShas, + reason: fields.reason, + }); return new JoinResult({ receipt, }); diff --git a/src/domain/api/Intent.ts b/src/domain/api/Intent.ts index 652baee57..33608ada0 100644 --- a/src/domain/api/Intent.ts +++ b/src/domain/api/Intent.ts @@ -1,5 +1,5 @@ import WarpError from '../errors/WarpError.ts'; -import { isPropValue, type PropValue } from '../types/PropValue.ts'; +import { copyPropValue, isPropValue, type PropValue } from '../types/PropValue.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; export type IntentKind = 'node.add' | 'node.remove' | 'edge.add' | 'edge.remove' | 'property.set'; @@ -66,7 +66,7 @@ export default class Intent { } get descriptor(): IntentDescriptor { - return this.#descriptor; + return normalizeKnownDescriptor(this.#descriptor); } } @@ -152,7 +152,7 @@ function requireIntentFields(fields: TFields | null | undefined): TFiel function requireIntentValue(value: PropValue): PropValue { if (isPropValue(value)) { - return value; + return copyPropValue(value); } throw new WarpError('Intent value must be property-compatible data', 'E_INTENT_VALUE'); } diff --git a/src/domain/api/JoinReceipt.ts b/src/domain/api/JoinReceipt.ts index 69c358dd9..e70ef07c4 100644 --- a/src/domain/api/JoinReceipt.ts +++ b/src/domain/api/JoinReceipt.ts @@ -1,22 +1,31 @@ import WarpError from '../errors/WarpError.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; import DraftTimeline from './DraftTimeline.ts'; -import type { JoinOutcome } from './ReceiptOutcome.ts'; -import { RECEIPT_OUTCOMES } from './WriteReceipt.ts'; +import { RECEIPT_OUTCOMES, type JoinOutcome } from './ReceiptOutcome.ts'; export type JoinMode = 'preview' | 'join'; export type JoinReceiptOutcome = JoinOutcome; -export type JoinReceiptOptions = { +type JoinReceiptFields = { readonly timeline: string; readonly writer: string; readonly draft: DraftTimeline; readonly mode: JoinMode; - readonly outcome: JoinReceiptOutcome; readonly patchShas?: readonly string[]; - readonly reason?: string; }; +export type JoinReceiptOptions = JoinReceiptFields & + ( + | { + readonly outcome: 'accepted'; + readonly reason?: never; + } + | { + readonly outcome: Exclude; + readonly reason: string; + } + ); + const JOIN_MODES: ReadonlySet = new Set(['preview', 'join']); const JOIN_RECEIPT_OUTCOMES: ReadonlySet = RECEIPT_OUTCOMES; @@ -74,11 +83,16 @@ function validateJoinOutcome(outcome: JoinReceiptOutcome): void { } function validateJoinReason(fields: JoinReceiptOptions): void { - if (fields.reason !== undefined) { - requireNonEmptyString(fields.reason, 'joinReceipt.reason'); + if (fields.outcome === 'accepted') { + rejectAcceptedJoinReason(fields.reason); + return; } - if (fields.outcome !== 'accepted' && fields.reason === undefined) { - throw new WarpError('Unaccepted JoinReceipt requires a reason', 'E_JOIN_RECEIPT_REASON'); + requireNonEmptyString(fields.reason, 'joinReceipt.reason'); +} + +function rejectAcceptedJoinReason(reason: string | undefined): void { + if (reason !== undefined) { + throw new WarpError('Accepted JoinReceipt cannot carry a reason', 'E_JOIN_RECEIPT_REASON'); } } diff --git a/src/domain/api/ReadReceipt.ts b/src/domain/api/ReadReceipt.ts index b415e7784..314b13e06 100644 --- a/src/domain/api/ReadReceipt.ts +++ b/src/domain/api/ReadReceipt.ts @@ -2,21 +2,32 @@ import WarpError from '../errors/WarpError.ts'; import type ReadIdentity from '../services/optic/ReadIdentity.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; import Reading from './Reading.ts'; -import type { ReadOutcome } from './ReceiptOutcome.ts'; +import { RECEIPT_OUTCOMES, type ReadOutcome } from './ReceiptOutcome.ts'; import { freezeRepairHints, type RepairHint } from './ReceiptSupport.ts'; export type ReadReceiptOutcome = ReadOutcome; -export type ReadReceiptOptions = { +type ReadReceiptFields = { readonly timeline: string; readonly writer: string; readonly reading: Reading; - readonly outcome: ReadReceiptOutcome; - readonly evidence?: ReadEvidence; readonly repairHints?: readonly RepairHint[]; - readonly reason?: string; }; +export type ReadReceiptOptions = ReadReceiptFields & + ( + | { + readonly outcome: 'accepted'; + readonly evidence: ReadEvidence; + readonly reason?: never; + } + | { + readonly outcome: Exclude; + readonly evidence?: ReadEvidence; + readonly reason: string; + } + ); + export type ReadEvidence = Readonly< Pick< ReadIdentity, @@ -33,12 +44,7 @@ export type ReadEvidence = Readonly< > >; -const READ_RECEIPT_OUTCOMES: ReadonlySet = new Set([ - 'resolved', - 'obstructed', - 'underdetermined', - 'rejected', -]); +const READ_RECEIPT_OUTCOMES: ReadonlySet = RECEIPT_OUTCOMES; export default class ReadReceipt { readonly evidence: ReadEvidence | undefined; @@ -87,8 +93,8 @@ function validateReadOutcome(outcome: ReadReceiptOutcome): void { function validateReadResolution(fields: ReadReceiptOptions): void { validateOptionalReason(fields.reason); - validateResolvedEvidence(fields); - validateUnresolvedReason(fields); + validateAcceptedEvidence(fields); + validateReadReason(fields); } function validateOptionalReason(reason: string | undefined): void { @@ -97,15 +103,18 @@ function validateOptionalReason(reason: string | undefined): void { } } -function validateResolvedEvidence(fields: ReadReceiptOptions): void { - if (fields.outcome === 'resolved' && fields.evidence === undefined) { - throw new WarpError('Resolved ReadReceipt requires evidence', 'E_READ_RECEIPT_EVIDENCE'); +function validateAcceptedEvidence(fields: ReadReceiptOptions): void { + if (fields.outcome === 'accepted' && fields.evidence === undefined) { + throw new WarpError('Accepted ReadReceipt requires evidence', 'E_READ_RECEIPT_EVIDENCE'); } } -function validateUnresolvedReason(fields: ReadReceiptOptions): void { - if (fields.outcome !== 'resolved' && fields.reason === undefined) { - throw new WarpError('Unresolved ReadReceipt requires a reason', 'E_READ_RECEIPT_REASON'); +function validateReadReason(fields: ReadReceiptOptions): void { + if (fields.outcome === 'accepted' && fields.reason !== undefined) { + throw new WarpError('Accepted ReadReceipt cannot carry a reason', 'E_READ_RECEIPT_REASON'); + } + if (fields.outcome !== 'accepted' && fields.reason === undefined) { + throw new WarpError('Unaccepted ReadReceipt requires a reason', 'E_READ_RECEIPT_REASON'); } } diff --git a/src/domain/api/ReadingRuntime.ts b/src/domain/api/ReadingRuntime.ts index ae9f99c86..92ccdb492 100644 --- a/src/domain/api/ReadingRuntime.ts +++ b/src/domain/api/ReadingRuntime.ts @@ -23,7 +23,7 @@ type ReadingExecutor = ( ) => Promise; type OperationalReadFailure = { - readonly outcome: Exclude; + readonly outcome: Exclude; readonly reason: string; readonly repairHints: readonly RepairHint[]; }; @@ -160,7 +160,7 @@ function readingResult( timeline: runtime.worldlineName, writer: runtime.writerId, reading, - outcome: 'resolved', + outcome: 'accepted', evidence: result.evidence, }), }); diff --git a/src/domain/api/ReceiptOutcome.ts b/src/domain/api/ReceiptOutcome.ts index 391061509..f821ac1c5 100644 --- a/src/domain/api/ReceiptOutcome.ts +++ b/src/domain/api/ReceiptOutcome.ts @@ -1,11 +1,18 @@ export type ReceiptOutcome = | 'accepted' - | 'resolved' | 'obstructed' | 'conflicted' | 'underdetermined' | 'rejected'; -export type WriteOutcome = Exclude; -export type ReadOutcome = Exclude; -export type JoinOutcome = WriteOutcome; +export type WriteOutcome = ReceiptOutcome; +export type ReadOutcome = ReceiptOutcome; +export type JoinOutcome = ReceiptOutcome; + +export const RECEIPT_OUTCOMES: ReadonlySet = new Set([ + 'accepted', + 'obstructed', + 'conflicted', + 'underdetermined', + 'rejected', +]); diff --git a/src/domain/api/Timeline.ts b/src/domain/api/Timeline.ts index 459bc45f3..8e3fb3a5d 100644 --- a/src/domain/api/Timeline.ts +++ b/src/domain/api/Timeline.ts @@ -159,9 +159,9 @@ export default class Timeline { async readValue(reading: Reading): Promise { const result = await this.read(reading); - if (result.receipt.outcome !== 'resolved') { + if (result.receipt.outcome !== 'accepted') { throw new WarpError( - 'Timeline.readValue requires a resolved reading', + 'Timeline.readValue requires an accepted reading', 'E_TIMELINE_READ_UNRESOLVED', { context: { diff --git a/src/domain/api/TimelineView.ts b/src/domain/api/TimelineView.ts index 2afd729aa..1c3f67ec2 100644 --- a/src/domain/api/TimelineView.ts +++ b/src/domain/api/TimelineView.ts @@ -36,9 +36,9 @@ export default class TimelineView { async readValue(reading: Reading): Promise { const result = await this.read(reading); - if (result.receipt.outcome !== 'resolved') { + if (result.receipt.outcome !== 'accepted') { throw new WarpError( - 'TimelineView.readValue requires a resolved reading', + 'TimelineView.readValue requires an accepted reading', 'E_TIMELINE_VIEW_READ_UNRESOLVED', { context: { outcome: result.receipt.outcome, reason: result.receipt.reason } } ); diff --git a/src/domain/api/WriteReceipt.ts b/src/domain/api/WriteReceipt.ts index 8025ab892..f1ed0d0b6 100644 --- a/src/domain/api/WriteReceipt.ts +++ b/src/domain/api/WriteReceipt.ts @@ -1,7 +1,7 @@ import WarpError from '../errors/WarpError.ts'; import { requireNonEmptyString } from '../utils/scalarValidation.ts'; import Intent from './Intent.ts'; -import type { WriteOutcome } from './ReceiptOutcome.ts'; +import { RECEIPT_OUTCOMES, type WriteOutcome } from './ReceiptOutcome.ts'; import { freezeRepairHints, type RepairHint } from './ReceiptSupport.ts'; type WriteReceiptFields = { @@ -25,14 +25,6 @@ export type WriteReceiptOptions = WriteReceiptFields & } ); -export const RECEIPT_OUTCOMES: ReadonlySet = new Set([ - 'accepted', - 'obstructed', - 'conflicted', - 'underdetermined', - 'rejected', -]); - export default class WriteReceipt { readonly intent: Intent; readonly operation: 'write' = 'write'; diff --git a/src/domain/api/openWarp.ts b/src/domain/api/openWarp.ts index 50f34780b..88f33fe81 100644 --- a/src/domain/api/openWarp.ts +++ b/src/domain/api/openWarp.ts @@ -1,4 +1,5 @@ import type RuntimeStorageCapabilityPort from '../../ports/RuntimeStorageCapabilityPort.ts'; +import { installDefaultRuntimeHostNodePorts } from '../../application/RuntimeHostNodeDefaults.ts'; import type { CorePersistence } from '../types/WarpPersistence.ts'; import { openWarpWorldline } from '../WarpWorldline.ts'; import Warp from './Warp.ts'; @@ -17,17 +18,19 @@ export type OpenWarpOptions = { export function openWarp(options: OpenWarpOptions): Promise { return Promise.resolve().then(() => { assertOpenWarpOptions(options); + installDefaultRuntimeHostNodePorts(); const { storage, writer } = options; return new Warp({ writer, - openTimeline: async (name) => createTimeline( - await openWarpWorldline({ - persistence: storage, - worldlineName: name, - writerId: writer, - }), - ), + openTimeline: async (name) => + createTimeline( + await openWarpWorldline({ + persistence: storage, + worldlineName: name, + writerId: writer, + }) + ), }); }); } diff --git a/src/domain/graph/LegacyPropertyValue.ts b/src/domain/graph/LegacyPropertyValue.ts index 09a84b19a..6ab11b84d 100644 --- a/src/domain/graph/LegacyPropertyValue.ts +++ b/src/domain/graph/LegacyPropertyValue.ts @@ -1,18 +1,18 @@ import WarpError from '../errors/WarpError.ts'; -import { isPropValue, type PropValue } from '../types/PropValue.ts'; +import { copyPropValue, isPropValue, type PropValue } from '../types/PropValue.ts'; /** Runtime-backed value for a legacy property compatibility record. */ export default class LegacyPropertyValue { private readonly value: PropValue; constructor(value: PropValue) { - this.value = clonePropValue(requirePropValue(value)); + this.value = copyPropValue(requirePropValue(value)); Object.freeze(this); } /** Returns a defensive copy of the property-compatible value. */ toPropValue(): PropValue { - return clonePropValue(this.value); + return copyPropValue(this.value); } } @@ -23,29 +23,3 @@ function requirePropValue(value: PropValue): PropValue { } return value; } - -/** Copies recursive property values so source carriers stay outside the noun. */ -function clonePropValue(value: PropValue): PropValue { - if (value instanceof Uint8Array) { - return new Uint8Array(value); - } - if (Array.isArray(value)) { - return value.map((entry) => clonePropValue(entry)); - } - if (isPropValueObject(value)) { - const copy: { [key: string]: PropValue } = {}; - for (const [key, entry] of Object.entries(value)) { - copy[key] = clonePropValue(entry); - } - return copy; - } - return value; -} - -/** Narrows recursive property objects. */ -function isPropValueObject(value: PropValue): value is { [key: string]: PropValue } { - return value !== null - && typeof value === 'object' - && !(value instanceof Uint8Array) - && !Array.isArray(value); -} diff --git a/src/domain/services/optic/NeighborhoodPageCursor.ts b/src/domain/services/optic/NeighborhoodPageCursor.ts index 8d3c6c685..4d62e45bc 100644 --- a/src/domain/services/optic/NeighborhoodPageCursor.ts +++ b/src/domain/services/optic/NeighborhoodPageCursor.ts @@ -4,7 +4,7 @@ import type { Direction } from '../../../ports/NeighborProviderPort.ts'; export const DEFAULT_NEIGHBORHOOD_PAGE_SIZE = 100; export const MAX_NEIGHBORHOOD_PAGE_SIZE = 1_000; -const CURSOR_PREFIX = 'warp-neighborhood-v1:'; +const CURSOR_PREFIX = 'git-warp:neighborhood-cursor:1:'; export type NeighborhoodCursorScope = { readonly checkpointSha: string; diff --git a/src/domain/types/PropValue.ts b/src/domain/types/PropValue.ts index e2081c8d3..b406c68fe 100644 --- a/src/domain/types/PropValue.ts +++ b/src/domain/types/PropValue.ts @@ -17,14 +17,14 @@ export type PropValue = const FORBIDDEN_PROPERTY_VALUE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); function isScalarPropValue( - value: T, + value: T ): value is T & (string | number | boolean | null | Uint8Array) { return ( - value === null - || typeof value === 'string' - || typeof value === 'number' - || typeof value === 'boolean' - || value instanceof Uint8Array + value === null || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' || + value instanceof Uint8Array ); } @@ -60,7 +60,7 @@ function canTraversePropValueObject(value: T, seen: WeakSet): value i function isPropValueObject( value: T, - seen: WeakSet, + seen: WeakSet ): value is T & { [key: string]: PropValue } { if (!canTraversePropValueObject(value, seen)) { return false; @@ -86,16 +86,43 @@ function isNonArrayPlainObject(value: object): boolean { if (Array.isArray(value) || value instanceof Uint8Array) { return false; } - return Object.getPrototypeOf(value) === Object.prototype - || Object.getPrototypeOf(value) === null; + return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null; } export function isPropValue(value: T): value is T & PropValue { return isPropValueWithSeen(value, new WeakSet()); } +export function copyPropValue(value: PropValue): PropValue { + if (value === null || typeof value !== 'object') { + return value; + } + return copyCompositePropValue(value); +} + +function copyCompositePropValue( + value: Uint8Array | PropValue[] | { [key: string]: PropValue } +): PropValue { + if (value instanceof Uint8Array) { + return new Uint8Array(value); + } + if (Array.isArray(value)) { + return value.map(copyPropValue); + } + const copy: { [key: string]: PropValue } = {}; + for (const [key, entry] of Object.entries(value)) { + Object.defineProperty(copy, key, { + value: copyPropValue(entry), + enumerable: true, + configurable: true, + writable: true, + }); + } + return copy; +} + function isPropValueWithSeen(value: T, seen: WeakSet): value is T & PropValue { - return isScalarPropValue(value) - || isPropValueArray(value, seen) - || isPropValueObject(value, seen); + return ( + isScalarPropValue(value) || isPropValueArray(value, seen) || isPropValueObject(value, seen) + ); } diff --git a/test/conformance/v18FirstUseOpticsHonesty.test.ts b/test/conformance/v18FirstUseOpticsHonesty.test.ts index 81ed02143..19834a09c 100644 --- a/test/conformance/v18FirstUseOpticsHonesty.test.ts +++ b/test/conformance/v18FirstUseOpticsHonesty.test.ts @@ -182,7 +182,7 @@ describe('v18 first-use Optics honesty gate', () => { expect(node).toMatchObject({ nodeId: NODE_ID, alive: true }); expect(property.value).toBe('open'); expect(property.receipt).toMatchObject({ - outcome: 'resolved', + outcome: 'accepted', evidence: { checkpointSha }, }); expect(persistence.forbiddenOperations()).toEqual([]); diff --git a/test/conformance/v18TraversalOpticReadBasis.test.ts b/test/conformance/v18TraversalOpticReadBasis.test.ts index d79c98c89..3bc14bb34 100644 --- a/test/conformance/v18TraversalOpticReadBasis.test.ts +++ b/test/conformance/v18TraversalOpticReadBasis.test.ts @@ -18,7 +18,7 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { const graph = fixture.graph; const materialization = new V17MaterializationFallbackTrap( graph, - 'traversal optic read must not full-materialize', + 'traversal optic read must not full-materialize' ); const readPath = new V17PublicOpticReadPath(graph.worldline()); @@ -114,12 +114,14 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { expect(first).toMatchObject({ completeness: 'frontier-open', frontier: [ - { nodeId: ROOT_NODE_ID, depth: 0, edgeCursor: expect.stringMatching(/^warp-neighborhood-v1:/) }, + { + nodeId: ROOT_NODE_ID, + depth: 0, + edgeCursor: expect.stringMatching(/^git-warp:neighborhood-cursor:1:/), + }, { nodeId: BETA_NODE_ID, depth: 1, edgeCursor: null }, ], - edges: [ - { fromNodeId: ROOT_NODE_ID, toNodeId: BETA_NODE_ID, label: LINK_LABEL, depth: 1 }, - ], + edges: [{ fromNodeId: ROOT_NODE_ID, toNodeId: BETA_NODE_ID, label: LINK_LABEL, depth: 1 }], }); expect(second).toMatchObject({ completeness: 'complete', @@ -175,23 +177,23 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { const fixture = await openTraversalFixture(graphName); const readPath = new V17PublicOpticReadPath(fixture.graph.worldline()); - await expect(readPath.readTraversal(ROOT_NODE_ID, { direction: 'out' })) - .rejects - .toMatchObject({ - code: 'E_OPTIC_TRAVERSAL_UNBOUNDED', - context: { - graphName, - opticKind: 'traversal', - target: { nodeId: ROOT_NODE_ID }, - cause: 'requires-global-scan', - reason: 'requires-global-scan', - recoveryHints: [], - }, - }); + await expect(readPath.readTraversal(ROOT_NODE_ID, { direction: 'out' })).rejects.toMatchObject({ + code: 'E_OPTIC_TRAVERSAL_UNBOUNDED', + context: { + graphName, + opticKind: 'traversal', + target: { nodeId: ROOT_NODE_ID }, + cause: 'requires-global-scan', + reason: 'requires-global-scan', + recoveryHints: [], + }, + }); }); }); -async function openTraversalFixture(graphName: string): Promise { +async function openTraversalFixture( + graphName: string +): Promise { const fixture = await V17CheckpointTailOpticGraphFixture.openEmpty(graphName); await fixture.graph.patch((patch) => { patch.addNode(ROOT_NODE_ID); diff --git a/test/type-check/v19-consumer.ts b/test/type-check/v19-consumer.ts index 571633d86..aff989367 100644 --- a/test/type-check/v19-consumer.ts +++ b/test/type-check/v19-consumer.ts @@ -83,6 +83,7 @@ const preview: JoinResult = await timeline.previewJoin(draft, joinOptions); const joined: JoinResult = await timeline.join(draft); const joinReceipt: JoinReceipt = joined.receipt; const joinOutcome: JoinOutcome = joinReceipt.outcome; +const acceptedOutcome: ReceiptOutcome = 'accepted'; // @ts-expect-error receipt outcomes do not expose operation names. const operationOutcome: ReceiptOutcome = 'write'; @@ -90,6 +91,9 @@ const operationOutcome: ReceiptOutcome = 'write'; // @ts-expect-error read receipt outcomes do not expose operation names. const readOperationOutcome: ReadOutcome = 'read'; +// @ts-expect-error resolved is not a receipt outcome in the v19 contract. +const retiredReadOutcome: ReadOutcome = 'resolved'; + // @ts-expect-error previewJoin is a dedicated method, not a dryRun boolean trap. await timeline.previewJoin(draft, { dryRun: true }); @@ -106,6 +110,7 @@ void timelineName; void timelineWriter; void historicalResult; void outcome; +void acceptedOutcome; void operationOutcome; void readValue; void convenienceValue; @@ -113,6 +118,7 @@ void neighborhoodRequest; void readOutcome; void readEvidence; void readOperationOutcome; +void retiredReadOutcome; void repairHints; void draftReceipt; void preview; diff --git a/test/unit/domain/ReceiptOutcome.test.ts b/test/unit/domain/ReceiptOutcome.test.ts index d2a8a0e93..9871522b7 100644 --- a/test/unit/domain/ReceiptOutcome.test.ts +++ b/test/unit/domain/ReceiptOutcome.test.ts @@ -3,9 +3,20 @@ import { describe, expect, it } from 'vitest'; import DraftTimeline from '../../../src/domain/api/DraftTimeline.ts'; import { intent } from '../../../src/domain/api/IntentBuilders.ts'; import JoinReceipt from '../../../src/domain/api/JoinReceipt.ts'; -import WriteReceipt, { RECEIPT_OUTCOMES } from '../../../src/domain/api/WriteReceipt.ts'; +import { RECEIPT_OUTCOMES } from '../../../src/domain/api/ReceiptOutcome.ts'; +import WriteReceipt from '../../../src/domain/api/WriteReceipt.ts'; describe('receipt outcomes', () => { + it('locks the canonical outcome axis to the approved five values', () => { + expect([...RECEIPT_OUTCOMES]).toEqual([ + 'accepted', + 'obstructed', + 'conflicted', + 'underdetermined', + 'rejected', + ]); + }); + it('shares the canonical write receipt outcomes with join receipts', () => { const draft = new DraftTimeline({ name: 'try-admin-role', @@ -14,20 +25,62 @@ describe('receipt outcomes', () => { }); for (const outcome of RECEIPT_OUTCOMES) { - const receipt = new JoinReceipt({ - timeline: 'events', - writer: 'agent-1', - draft, - mode: 'join', - outcome, - ...(outcome === 'accepted' ? {} : { reason: `join_${outcome}` }), - }); + const receipt = + outcome === 'accepted' + ? new JoinReceipt({ + timeline: 'events', + writer: 'agent-1', + draft, + mode: 'join', + outcome, + }) + : new JoinReceipt({ + timeline: 'events', + writer: 'agent-1', + draft, + mode: 'join', + outcome, + reason: `join_${outcome}`, + }); expect(receipt.outcome).toBe(outcome); expect(receipt.operation).toBe('join'); } }); + it('rejects impossible join receipt settlements from JavaScript callers', () => { + const draft = new DraftTimeline({ + name: 'try-admin-role', + timeline: 'events', + writer: 'agent-1', + }); + + expect( + () => + new JoinReceipt({ + timeline: 'events', + writer: 'agent-1', + draft, + mode: 'join', + outcome: 'accepted', + // @ts-expect-error runtime validation accepts JavaScript callers. + reason: 'accepted_with_reason', + }) + ).toThrow('Accepted JoinReceipt cannot carry a reason'); + expect( + () => + new JoinReceipt({ + timeline: 'events', + writer: 'agent-1', + draft, + mode: 'join', + outcome: 'rejected', + // @ts-expect-error runtime validation accepts JavaScript callers. + reason: undefined, + }) + ).toThrow('joinReceipt.reason must be a non-empty string'); + }); + it('represents rejected writes without inventing patch identities', () => { const receipt = new WriteReceipt({ timeline: 'events', diff --git a/test/unit/domain/WarpFacade.test.ts b/test/unit/domain/WarpFacade.test.ts index 7ea132b7c..02a633e5b 100644 --- a/test/unit/domain/WarpFacade.test.ts +++ b/test/unit/domain/WarpFacade.test.ts @@ -193,6 +193,11 @@ describe('v19 Warp facade', () => { }); it('rejects missing storage and blank identities', async () => { + await expect( + // @ts-expect-error runtime validation accepts JavaScript callers. + openWarp(undefined) + ).rejects.toThrow('openWarp options are required'); + await expect( openWarp({ // @ts-expect-error runtime validation accepts JavaScript callers. @@ -304,6 +309,38 @@ describe('v19 Warp facade', () => { expect(result.nodes).toEqual([{ id: 'user:alice', props: { role: 'admin' } }]); }); + it('detaches property intents from caller-owned recursive values', () => { + const bytes = new Uint8Array([1, 2]); + const source = { + roles: ['admin'], + evidence: bytes, + }; + const propertyIntent = intent.property.set({ + subject: 'user:alice', + key: 'access', + value: source, + }); + + source.roles[0] = 'owner'; + bytes[0] = 9; + const exposed = propertyIntent.descriptor; + expect(exposed).toMatchObject({ + kind: 'property.set', + value: { roles: ['admin'], evidence: new Uint8Array([1, 2]) }, + }); + if (exposed.kind !== 'property.set') { + throw new Error('property intent descriptor kind must be stable'); + } + const exposedValue = exposed.value as { roles: string[]; evidence: Uint8Array }; + exposedValue.roles[0] = 'auditor'; + exposedValue.evidence[0] = 7; + + expect(propertyIntent.descriptor).toMatchObject({ + kind: 'property.set', + value: { roles: ['admin'], evidence: new Uint8Array([1, 2]) }, + }); + }); + it('returns operational write failures as receipts', async () => { const warp = await openWarp({ storage: new MemoryStorageAdapter(), @@ -324,7 +361,7 @@ describe('v19 Warp facade', () => { ]); }); - it('reads public readings and returns resolved read receipts', async () => { + it('reads public readings and returns accepted read receipts', async () => { const storage = new MemoryStorageAdapter(); const warp = await openWarp({ storage, @@ -372,7 +409,7 @@ describe('v19 Warp facade', () => { expect(propertyResult).toBeInstanceOf(ReadingResult); expect(propertyResult.receipt).toBeInstanceOf(ReadReceipt); expect(propertyResult.value).toBe('admin'); - expect(propertyResult.receipt.outcome).toBe('resolved'); + expect(propertyResult.receipt.outcome).toBe('accepted'); expect(propertyResult.receipt.operation).toBe('read'); expect(propertyResult.receipt.reading.kind).toBe('property.get'); expect(propertyResult.receipt.timeline).toBe('events'); @@ -384,7 +421,7 @@ describe('v19 Warp facade', () => { expect(existsResult).toBeInstanceOf(ReadingResult); expect(existsResult.value).toBe(true); - expect(existsResult.receipt.outcome).toBe('resolved'); + expect(existsResult.receipt.outcome).toBe('accepted'); expect(existsResult.receipt.reading.kind).toBe('node.exists'); expect(neighborhoodResult.value).toMatchObject({ subject: 'user:alice', @@ -393,7 +430,7 @@ describe('v19 Warp facade', () => { edges: [{ direction: 'out', neighborId: 'team:ops', label: 'memberOf' }], }); expect(neighborhoodResult.receipt).toMatchObject({ - outcome: 'resolved', + outcome: 'accepted', evidence: { kind: 'checkpoint-tail-read' }, }); const coordinate = await timeline.coordinate(); diff --git a/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts b/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts index 150f1734c..b96f984fb 100644 --- a/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts +++ b/test/unit/domain/services/optic/CheckpointTailTraversalReader.test.ts @@ -9,7 +9,7 @@ const ALPHA = 'node:alpha'; const BETA = 'node:beta'; const GAMMA = 'node:gamma'; const DELTA = 'node:delta'; -const OPAQUE_AFTER_BETA = 'warp-neighborhood-v1:checkpoint|node%3Aroot|out||out|7|link'; +const OPAQUE_AFTER_BETA = 'git-warp:neighborhood-cursor:1:checkpoint|node%3Aroot|out||out|7|link'; const READ_IDENTITY = new ReadIdentity({ worldline: 'traversal-reader-test', @@ -45,9 +45,7 @@ describe('CheckpointTailTraversalReader', () => { { nodeId: ROOT, depth: 0, edgeCursor: OPAQUE_AFTER_BETA }, { nodeId: BETA, depth: 1, edgeCursor: null }, ], - edges: [ - { fromNodeId: ROOT, toNodeId: BETA, label: 'link', depth: 1 }, - ], + edges: [{ fromNodeId: ROOT, toNodeId: BETA, label: 'link', depth: 1 }], }); if (first.cursor === null) { throw new Error('expected an open traversal cursor'); @@ -73,37 +71,31 @@ describe('CheckpointTailTraversalReader', () => { }); }); -function neighborhoodPage( - nodeId: string, - cursor: string | null, -): TraversalNeighborhoodReadResult { +function neighborhoodPage(nodeId: string, cursor: string | null): TraversalNeighborhoodReadResult { if (nodeId === ROOT && cursor === null) { - return page([ - { direction: 'out', neighborId: BETA, label: 'link' }, - { direction: 'out', neighborId: ALPHA, label: 'link' }, - ], [null, OPAQUE_AFTER_BETA]); + return page( + [ + { direction: 'out', neighborId: BETA, label: 'link' }, + { direction: 'out', neighborId: ALPHA, label: 'link' }, + ], + [null, OPAQUE_AFTER_BETA] + ); } if (nodeId === ROOT && cursor === OPAQUE_AFTER_BETA) { - return page([ - { direction: 'out', neighborId: ALPHA, label: 'link' }, - ], [OPAQUE_AFTER_BETA]); + return page([{ direction: 'out', neighborId: ALPHA, label: 'link' }], [OPAQUE_AFTER_BETA]); } if (nodeId === BETA) { - return page([ - { direction: 'out', neighborId: DELTA, label: 'link' }, - ], [null]); + return page([{ direction: 'out', neighborId: DELTA, label: 'link' }], [null]); } if (nodeId === ALPHA) { - return page([ - { direction: 'out', neighborId: GAMMA, label: 'link' }, - ], [null]); + return page([{ direction: 'out', neighborId: GAMMA, label: 'link' }], [null]); } return page([], []); } function page( edges: TraversalNeighborhoodReadResult['edges'], - resumeCursors: readonly (string | null)[], + resumeCursors: readonly (string | null)[] ): TraversalNeighborhoodReadResult { return { edges, diff --git a/test/unit/scripts/v19-public-api-boundary.test.ts b/test/unit/scripts/v19-public-api-boundary.test.ts index cee6ffb61..5df4d052f 100644 --- a/test/unit/scripts/v19-public-api-boundary.test.ts +++ b/test/unit/scripts/v19-public-api-boundary.test.ts @@ -59,14 +59,7 @@ type ModuleSurface = { }; function moduleSurface(relativePath = 'index.ts'): ModuleSurface { - const source = readFileSync(new URL(relativePath, REPO_ROOT), 'utf8'); - const sourceFile = ts.createSourceFile( - relativePath, - source, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS - ); + const sourceFile = sourceFileFor(relativePath); const starExports: string[] = []; const typeExports: string[] = []; const valueExports: string[] = []; @@ -96,6 +89,43 @@ function moduleSurface(relativePath = 'index.ts'): ModuleSurface { }; } +function sourceFileFor(relativePath: string): ts.SourceFile { + const source = readFileSync(new URL(relativePath, REPO_ROOT), 'utf8'); + return ts.createSourceFile(relativePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); +} + +function importedModules(relativePath: string): readonly string[] { + return sourceFileFor(relativePath).statements.flatMap((statement) => + ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) + ? [statement.moduleSpecifier.text] + : [] + ); +} + +function exportedFunctionCalls( + relativePath: string, + functionName: string, + calledIdentifier: string +): boolean { + const declaration = sourceFileFor(relativePath).statements.find( + (statement) => ts.isFunctionDeclaration(statement) && statement.name?.text === functionName + ); + if (declaration === undefined) { + return false; + } + let found = false; + const visit = (node: ts.Node): void => { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { + found ||= node.expression.text === calledIdentifier; + } + if (!found) { + ts.forEachChild(node, visit); + } + }; + visit(declaration); + return found; +} + function collectExportedDeclaration( statement: ts.Statement, typeExports: string[], @@ -211,4 +241,17 @@ describe('v19 public API boundary', () => { './storage', ]); }); + + it('installs Node runtime defaults at openWarp instead of package import time', () => { + const defaultsModule = '../../application/RuntimeHostNodeDefaults.ts'; + expect(importedModules('index.ts')).not.toContain(defaultsModule); + expect(importedModules('src/domain/api/openWarp.ts')).toContain(defaultsModule); + expect( + exportedFunctionCalls( + 'src/domain/api/openWarp.ts', + 'openWarp', + 'installDefaultRuntimeHostNodePorts' + ) + ).toBe(true); + }); }); From b725e47238613aeef18281512f7abc324a8d3a10 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 13 Jul 2026 15:49:08 -0700 Subject: [PATCH 6/7] Address v19 public boundary review --- CHANGELOG.md | 4 + README.md | 2 +- advanced.ts | 7 +- docs/migrations/v19/README.md | 8 +- docs/topics/api/README.md | 10 +- docs/topics/getting-started.md | 7 +- docs/topics/optic-reads.md | 9 +- docs/topics/querying.md | 13 +- docs/topics/reference.md | 29 ++-- scripts/check-source-backed-reference.ts | 2 +- src/domain/api/TickRuntime.ts | 19 ++- src/domain/api/Timeline.ts | 22 --- src/domain/api/TimelineRuntime.ts | 4 - src/domain/api/captureCoordinate.ts | 12 ++ .../controllers/CheckpointController.ts | 12 +- .../optic/CheckpointTailFactReducer.ts | 70 ++++----- .../optic/CheckpointTailWitnessLocator.ts | 133 ++++++++++++++++-- .../v18TraversalOpticReadBasis.test.ts | 64 ++++++++- .../v19BoundedNeighborhoodReading.test.ts | 8 +- test/type-check/v19-consumer.ts | 3 + test/type-check/v19-subpaths.ts | 4 +- test/unit/domain/WarpFacade.test.ts | 9 +- .../domain/WarpGraph.coverageGaps.test.ts | 4 +- .../controllers/CheckpointController.test.ts | 30 ++++ ...CheckpointShardFactReader.manifest.test.ts | 13 +- .../scripts/v19-public-api-boundary.test.ts | 27 +++- 26 files changed, 395 insertions(+), 130 deletions(-) create mode 100644 src/domain/api/captureCoordinate.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 443c0c5f1..f087f7788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the package root into the explicit `storage`, `advanced`, and `diagnostics` subpaths. The package root now rejects graph-first compatibility nouns through the v19 public API boundary audit. +- Moved formal coordinate capture to advanced `captureCoordinate()` so the root + `Timeline` exposes opaque ticks but no coordinate machinery. +- Bound ticks to the exact timeline runtime that created them and made bounded + traversal reuse one request-scoped checkpoint basis and tail support index. - Locked the package root to the v19 facade allowlist so support ports, infrastructure adapters, memory helpers, cancellation utilities, sync internals, and canonical serialization helpers stay behind explicit subpaths. diff --git a/README.md b/README.md index 5c4c24dc2..087d008ad 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ The runtime is designed so reads stay scoped. It avoids the “just materialize > > A **frontier** is that cut's boundary — an **antichain** of writer tips, the maximal elements of the ideal. > -> A read's **causal cone** $D(v) = {\downarrow}\{v\}$ is the down-set generated by $v$: everything that could have influenced it, and nothing that couldn't. Because the poset is well-founded, $D(v)$ is finite even when $H$ is unbounded — which is _why_ a bounded read can exist at all. The **bounded support rule** is then just the claim "this optic's answer factors through $D(v)$, so materializing the rest of $H \setminus D(v)$ is provably unnecessary." +> A read's **causal cone** $D(v) = {\downarrow}\{v\}$ is the down-set generated by $v$: everything that could have influenced it, and nothing that couldn't. Well-foundedness alone does not make that cone finite. A bounded read exists when the runtime establishes finite causal support through its optic basis. The **bounded support rule** is the claim "this optic's answer factors through that finite support in $D(v)$, so materializing the rest of $H \setminus D(v)$ is unnecessary." diff --git a/advanced.ts b/advanced.ts index 13ceb0bec..ab35cf2b3 100644 --- a/advanced.ts +++ b/advanced.ts @@ -1,11 +1,12 @@ /** * Formal bounded-reading API for expert consumers. * - * Capture a `Coordinate` from a public `Timeline`, then lower reads through - * its executable `Optic`. A successful read carries a type-only `Witness` - * through the root receipt evidence contract. + * Capture a `Coordinate` from a public `Timeline` with `captureCoordinate()`, + * then lower reads through its executable `Optic`. A successful read carries + * a type-only `Witness` through the root receipt evidence contract. */ +export { default as captureCoordinate } from './src/domain/api/captureCoordinate.ts'; export { default as Coordinate } from './src/domain/WarpWorldlineCoordinate.ts'; export { default as Optic } from './src/domain/services/optic/WorldlineOptic.ts'; export type { WarpWorldlineCoordinateFrontierEntry } from './src/domain/WarpWorldlineCoordinate.ts'; diff --git a/docs/migrations/v19/README.md b/docs/migrations/v19/README.md index eba4d3e8a..e1531db3d 100644 --- a/docs/migrations/v19/README.md +++ b/docs/migrations/v19/README.md @@ -40,7 +40,7 @@ Diagnostics and expert WARP terms move to explicit subpaths: | ------------- | --------------------------------------------------- | | Root | first-use public API; no graph substrate | | `storage` | supported persistence adapters | -| `advanced` | bounded `Coordinate`, `Optic`, and `Witness` access | +| `advanced` | bounded coordinate capture, `Optic`, and `Witness` access | | `diagnostics` | receipt inspection | The former `browser` and `legacy` subpaths do not exist in v19. Consumers must @@ -186,8 +186,8 @@ const historical = await events.at(tick).read( ); ``` -Use `Tick` for public time-travel handles. Use `Coordinate` only where formal -evidence posture matters. +Use `Tick` for public time-travel handles. Import `captureCoordinate()` from +the `advanced` subpath only where formal evidence posture matters. ## Speculative Work Migration @@ -261,7 +261,7 @@ and joins first. | `GraphNode` | removed | no public export | | `GraphDiff` | removed | no public-handle comparison API ships in v19 | | `Optic` | `advanced` | readings are root | -| `Coordinate` | `advanced` or receipt fields | ticks are root | +| `Coordinate` | `advanced` or receipt fields | capture with advanced `captureCoordinate()` | | `Observer` | removed | readings are first-use root | | `Strand` | removed | drafts are first-use root | | `Braid` | removed | joins are first-use root | diff --git a/docs/topics/api/README.md b/docs/topics/api/README.md index 98fd0d92a..949b7c8a0 100644 --- a/docs/topics/api/README.md +++ b/docs/topics/api/README.md @@ -344,6 +344,12 @@ const roleAtTick = await timeline.at(tick).read( Use `Coordinate` for formal evidence and proof posture on advanced surfaces. Do not make first-use application examples import coordinate machinery. +```typescript +import { captureCoordinate } from '@git-stunts/git-warp/advanced'; + +const coordinate = await captureCoordinate(timeline); +``` + ```text public: tick handle advanced: coordinate evidence @@ -388,7 +394,7 @@ The boundaries mean different things: | ------------- | --------------------------------------------------- | | Root | first-use product API | | `storage` | supported persistence adapters | -| `advanced` | bounded `Coordinate`, `Optic`, and `Witness` access | +| `advanced` | bounded coordinate capture, `Optic`, and `Witness` access | | `diagnostics` | receipt inspection | Do not turn `advanced` into a junk drawer. Symbols that exist only for removed @@ -404,7 +410,7 @@ Each old root symbol needs one explicit disposition: | `GitGraphAdapter` | `GitStorageAdapter` from `storage` | | `InMemoryGraphAdapter` | `MemoryStorageAdapter` from `storage` | | `commit((patch) => ...)` | `timeline.write(intent.*)` | -| `coordinate()` | `tick()` publicly; formal access remains advanced | +| `coordinate()` | `tick()` publicly; use advanced `captureCoordinate()` | | `optic()` | `timeline.read(reading.*)` or `advanced` | | `openWarpGraph()` | removed; replace diagnostics with explicit diagnostic APIs | | `PatchBuilder` | removed; use intent builders | diff --git a/docs/topics/getting-started.md b/docs/topics/getting-started.md index d5e5f817e..8f0bbdf11 100644 --- a/docs/topics/getting-started.md +++ b/docs/topics/getting-started.md @@ -137,10 +137,11 @@ await draft.write( ); const preview = await audit.previewJoin(draft); -const joined = await audit.join(draft); - console.log(preview.receipt); -console.log(joined.receipt); +if (preview.receipt.outcome === 'accepted') { + const joined = await audit.join(draft); + console.log(joined.receipt); +} ``` Draft writes stay separate until joined. `previewJoin` and `join` are separate diff --git a/docs/topics/optic-reads.md b/docs/topics/optic-reads.md index 23a5fb150..341ad08d4 100644 --- a/docs/topics/optic-reads.md +++ b/docs/topics/optic-reads.md @@ -11,8 +11,9 @@ basis posture, support rule, and evidence posture. ## The shipped path ```typescript -await events.prepareOpticBasis(); -const coordinate = await events.coordinate(); +import { captureCoordinate } from '@git-stunts/git-warp/advanced'; + +const coordinate = await captureCoordinate(events); const role = await coordinate .optic() @@ -21,7 +22,7 @@ const role = await coordinate .read(); ``` -`prepareOpticBasis()` verifies an existing checkpoint-tail basis. It does not +`captureCoordinate()` verifies an existing checkpoint-tail basis. It does not create one by materializing the whole graph. If the runtime cannot prove a bounded basis, the read fails closed with `E_OPTIC_NO_BOUNDED_BASIS`. @@ -77,7 +78,7 @@ could not lawfully establish or preserve the read's evidence boundary. The useful product version is simple: `S` is the coordinate, `A` is the bounded question, and `M` is the rest of history that the read deliberately does not -materialize. `prepareOpticBasis()` is the witness that the read can factor +materialize. `captureCoordinate()` verifies that the read can factor through a small enough support. Chaining `.node().prop()` composes optics. The composed read remains bounded diff --git a/docs/topics/querying.md b/docs/topics/querying.md index 84f3905d2..3f0a82dd7 100644 --- a/docs/topics/querying.md +++ b/docs/topics/querying.md @@ -86,9 +86,9 @@ flow. ## Unsupported Root Queries The root does not currently provide wildcard matching, arbitrary traversal, -aggregation, arbitrary traversal, or draft reads. Do not recover -those APIs by importing internal modules. Use the operator CLI or an explicit -expert surface where one exists, and keep application code on readings. +aggregation, or draft reads. Do not recover those APIs by importing internal +modules. Use the operator CLI or an explicit expert surface where one exists, +and keep application code on readings. This boundary is intentional: a broad materialization call must not masquerade as a bounded read. @@ -101,9 +101,10 @@ require an internal runtime host. ## Advanced Read Machinery -`@git-stunts/git-warp/advanced` exports `Coordinate`, executable `Optic`, and -the type-only `Witness`. Capture a coordinate with `timeline.coordinate()`; -ordinary application reads should continue to use `reading.*`. +`@git-stunts/git-warp/advanced` exports `captureCoordinate()`, `Coordinate`, +executable `Optic`, and the type-only `Witness`. Capture a coordinate with +`captureCoordinate(timeline)`; ordinary application reads should continue to +use `reading.*`. ## Removed Compatibility Surface diff --git a/docs/topics/reference.md b/docs/topics/reference.md index 985923e4d..28b04ebb5 100644 --- a/docs/topics/reference.md +++ b/docs/topics/reference.md @@ -107,15 +107,16 @@ GitStorageAdapterOptions @ storage.ts#L13 ## Advanced export surface -Bounded Coordinate, Optic, and Witness concepts for expert use. +Bounded coordinate capture, Optic, and Witness concepts for expert use. ### Value exports -Source: `advanced.ts`. Count: 2. +Source: `advanced.ts`. Count: 3. ```text -Coordinate @ advanced.ts#L9 -Optic @ advanced.ts#L10 +captureCoordinate @ advanced.ts#L9 +Coordinate @ advanced.ts#L10 +Optic @ advanced.ts#L11 ``` ### Type exports @@ -123,16 +124,16 @@ Optic @ advanced.ts#L10 Source: `advanced.ts`. Count: 10. ```text -NeighborhoodOpticCompleteness @ advanced.ts#L14 -NeighborhoodOpticEdge @ advanced.ts#L15 -NeighborhoodOpticReadDirection @ advanced.ts#L16 -NeighborhoodOpticReadOptions @ advanced.ts#L12 -ReadIdentityFrontierEntry @ advanced.ts#L20 -ReadIdentityIndexShard @ advanced.ts#L21 -ReadIdentityOptions @ advanced.ts#L22 -ReadIdentityTailWitness @ advanced.ts#L23 -WarpWorldlineCoordinateFrontierEntry @ advanced.ts#L11 -Witness @ advanced.ts#L19 +NeighborhoodOpticCompleteness @ advanced.ts#L15 +NeighborhoodOpticEdge @ advanced.ts#L16 +NeighborhoodOpticReadDirection @ advanced.ts#L17 +NeighborhoodOpticReadOptions @ advanced.ts#L13 +ReadIdentityFrontierEntry @ advanced.ts#L21 +ReadIdentityIndexShard @ advanced.ts#L22 +ReadIdentityOptions @ advanced.ts#L23 +ReadIdentityTailWitness @ advanced.ts#L24 +WarpWorldlineCoordinateFrontierEntry @ advanced.ts#L12 +Witness @ advanced.ts#L20 ``` ## Diagnostics export surface diff --git a/scripts/check-source-backed-reference.ts b/scripts/check-source-backed-reference.ts index 43c347cce..9fc26a135 100644 --- a/scripts/check-source-backed-reference.ts +++ b/scripts/check-source-backed-reference.ts @@ -266,7 +266,7 @@ function generate(): string { '', ...exportSurface('Storage export surface', storageSource, 'Git-backed and in-memory adapters for first-use applications.'), '', - ...exportSurface('Advanced export surface', advancedSource, 'Bounded Coordinate, Optic, and Witness concepts for expert use.'), + ...exportSurface('Advanced export surface', advancedSource, 'Bounded coordinate capture, Optic, and Witness concepts for expert use.'), '', ...exportSurface('Diagnostics export surface', diagnosticsSource, 'Operator inspection helpers that consume public receipt handles.'), '', diff --git a/src/domain/api/TickRuntime.ts b/src/domain/api/TickRuntime.ts index bbfdf3421..5724db91b 100644 --- a/src/domain/api/TickRuntime.ts +++ b/src/domain/api/TickRuntime.ts @@ -3,7 +3,12 @@ import type WarpWorldlineCoordinate from '../WarpWorldlineCoordinate.ts'; import WarpError from '../errors/WarpError.ts'; import Tick from './Tick.ts'; -const tickCoordinates = new WeakMap(); +type TickBinding = { + readonly coordinate: WarpWorldlineCoordinate; + readonly runtime: WarpWorldline; +}; + +const tickBindings = new WeakMap(); export async function createTick(runtime: WarpWorldline): Promise { await runtime.prepareOpticBasis(); @@ -12,16 +17,20 @@ export async function createTick(runtime: WarpWorldline): Promise { timeline: runtime.worldlineName, id: tickId(coordinate), }); - tickCoordinates.set(tick, coordinate); + tickBindings.set(tick, { coordinate, runtime }); return tick; } export function requireTickCoordinate(runtime: WarpWorldline, tick: Tick): WarpWorldlineCoordinate { - const coordinate = tickCoordinates.get(tick); - if (coordinate === undefined || tick.timeline !== runtime.worldlineName) { + const binding = tickBindings.get(tick); + if ( + binding === undefined || + binding.runtime !== runtime || + tick.timeline !== runtime.worldlineName + ) { throw new WarpError('Tick does not belong to this Timeline', 'E_TIMELINE_TICK_MISMATCH'); } - return coordinate; + return binding.coordinate; } function tickId(coordinate: WarpWorldlineCoordinate): string { diff --git a/src/domain/api/Timeline.ts b/src/domain/api/Timeline.ts index 8e3fb3a5d..55b1755f0 100644 --- a/src/domain/api/Timeline.ts +++ b/src/domain/api/Timeline.ts @@ -1,5 +1,4 @@ import WarpError from '../errors/WarpError.ts'; -import type WarpWorldlineCoordinate from '../WarpWorldlineCoordinate.ts'; import { assertTimelineNameIdentity, assertWriterIdentity } from './assertIdentity.ts'; import DraftTimeline from './DraftTimeline.ts'; import Intent from './Intent.ts'; @@ -22,7 +21,6 @@ export type JoinOptions = { type TimelineConstructionOptions = { readonly name: string; readonly writer: string; - readonly captureCoordinate?: CaptureCoordinate; readonly captureTick?: CaptureTick; readonly joinDraft?: JoinDraft; readonly openDraft?: OpenDraft; @@ -33,7 +31,6 @@ type TimelineConstructionOptions = { }; type JoinDraft = (draft: DraftTimeline, options: JoinOptions) => Promise; -type CaptureCoordinate = () => Promise; type CaptureTick = () => Promise; type OpenDraft = (name: string) => Promise; type OpenView = (tick: Tick) => TimelineView; @@ -41,7 +38,6 @@ type ReadReading = (reading: Reading) => Promise; type WriteIntent = (intent: Intent) => Promise; type TimelinePortErrorCode = | 'E_TIMELINE_JOINER' - | 'E_TIMELINE_COORDINATE_READER' | 'E_TIMELINE_TICK_READER' | 'E_TIMELINE_DRAFT_OPENER' | 'E_TIMELINE_VIEW_OPENER' @@ -56,7 +52,6 @@ type TimelinePortErrorCode = * root API contract. */ export default class Timeline { - readonly #captureCoordinate: CaptureCoordinate | null; readonly #captureTick: CaptureTick | null; readonly #joinDraft: JoinDraft | null; readonly #name: string; @@ -78,7 +73,6 @@ export default class Timeline { code: 'E_TIMELINE_IDENTITY', }); this.#joinDraft = optionalPort(options.joinDraft); - this.#captureCoordinate = optionalPort(options.captureCoordinate); this.#captureTick = optionalPort(options.captureTick); this.#name = options.name; this.#openDraft = optionalPort(options.openDraft); @@ -109,13 +103,6 @@ export default class Timeline { return await this.#openDraft(name); } - async coordinate(): Promise { - if (this.#captureCoordinate === null) { - throw new WarpError('Timeline was not opened by openWarp', 'E_TIMELINE_RUNTIME_UNAVAILABLE'); - } - return await this.#captureCoordinate(); - } - async tick(): Promise { if (this.#captureTick === null) { throw new WarpError('Timeline was not opened by openWarp', 'E_TIMELINE_RUNTIME_UNAVAILABLE'); @@ -216,7 +203,6 @@ function assertTimelineConstructionOptions(options: TimelineConstructionOptions) ); } assertJoinDraft(options.joinDraft); - assertCaptureCoordinate(options.captureCoordinate); assertCaptureTick(options.captureTick); assertOpenDraft(options.openDraft); assertOpenView(options.openView); @@ -225,14 +211,6 @@ function assertTimelineConstructionOptions(options: TimelineConstructionOptions) assertWriteIntent(options.writeIntent); } -function assertCaptureCoordinate(capture: CaptureCoordinate | undefined): void { - assertOptionalFunctionPort( - capture, - 'Timeline requires a coordinate function when provided', - 'E_TIMELINE_COORDINATE_READER' - ); -} - function assertCaptureTick(capture: CaptureTick | undefined): void { assertOptionalFunctionPort( capture, diff --git a/src/domain/api/TimelineRuntime.ts b/src/domain/api/TimelineRuntime.ts index af21dc713..463636e0d 100644 --- a/src/domain/api/TimelineRuntime.ts +++ b/src/domain/api/TimelineRuntime.ts @@ -17,10 +17,6 @@ export function createTimeline(runtime: WarpWorldline): Timeline { const timeline = new Timeline({ name: runtime.worldlineName, writer: runtime.writerId, - captureCoordinate: async () => { - await runtime.prepareOpticBasis(); - return await runtime.coordinate(); - }, captureTick: async () => await createTick(runtime), joinDraft: (draft, options) => joinDraftTimeline(runtime, draft, options), openDraft: (name) => createDraftTimeline(runtime, runtime.worldlineName, name), diff --git a/src/domain/api/captureCoordinate.ts b/src/domain/api/captureCoordinate.ts new file mode 100644 index 000000000..aac649cc7 --- /dev/null +++ b/src/domain/api/captureCoordinate.ts @@ -0,0 +1,12 @@ +import type WarpWorldlineCoordinate from '../WarpWorldlineCoordinate.ts'; +import type Timeline from './Timeline.ts'; +import { requireTimelineRuntime } from './TimelineRuntime.ts'; + +/** Captures one bounded formal coordinate from a public timeline handle. */ +export default async function captureCoordinate( + timeline: Timeline +): Promise { + const runtime = requireTimelineRuntime(timeline); + await runtime.prepareOpticBasis(); + return await runtime.coordinate(); +} diff --git a/src/domain/services/controllers/CheckpointController.ts b/src/domain/services/controllers/CheckpointController.ts index 47ee7a6ad..282601520 100644 --- a/src/domain/services/controllers/CheckpointController.ts +++ b/src/domain/services/controllers/CheckpointController.ts @@ -387,10 +387,10 @@ export default class CheckpointController { this._assertLoadPersistence(persistence); const message = await persistence.showNode(checkpointSha); if (typeof message !== 'string' || message.length === 0) { - return false; + throw invalidCheckpointReference(checkpointSha, 'empty-checkpoint-message'); } if (this._host._commitMessageCodec.detectKind(message) !== 'checkpoint') { - return false; + throw invalidCheckpointReference(checkpointSha, 'non-checkpoint-message'); } const checkpoint = this._host._commitMessageCodec.decodeCheckpoint(message); if (isCurrentCheckpointSchema(checkpoint.schema)) { @@ -600,3 +600,11 @@ export default class CheckpointController { }; } } + +function invalidCheckpointReference(checkpointSha: string, reason: string): PersistenceError { + return new PersistenceError( + `Checkpoint ref resolved to invalid object ${checkpointSha}: ${reason}`, + 'E_CHECKPOINT_REF_INVALID', + { context: { checkpointSha, reason } }, + ); +} diff --git a/src/domain/services/optic/CheckpointTailFactReducer.ts b/src/domain/services/optic/CheckpointTailFactReducer.ts index eae02fe20..38c9b50a4 100644 --- a/src/domain/services/optic/CheckpointTailFactReducer.ts +++ b/src/domain/services/optic/CheckpointTailFactReducer.ts @@ -11,6 +11,10 @@ import { normalizeRawOp } from '../OpNormalizer.ts'; import type { CheckpointTailPatchEntry } from './CheckpointTailOpticSource.ts'; type NormalizedTailOperation = ReturnType; +type NeighborhoodTailScope = { + readonly direction: 'in' | 'out' | 'both'; + readonly labels: readonly string[]; +}; export default class CheckpointTailFactReducer { private readonly _graphName: string; @@ -41,16 +45,22 @@ export default class CheckpointTailFactReducer { includesNeighborhood( entry: CheckpointTailPatchEntry, - options: { - readonly nodeId: string; - readonly direction: 'in' | 'out' | 'both'; - readonly labels: readonly string[]; - }, + options: NeighborhoodTailScope & { readonly nodeId: string }, ): boolean { - return entry.patch.ops.some((rawOp) => { - const op = normalizeRawOp(rawOp); - return isTargetNeighborhoodEdge(op, options); - }); + return this.neighborhoodNodeIds(entry, options).includes(options.nodeId); + } + + neighborhoodNodeIds( + entry: CheckpointTailPatchEntry, + options: NeighborhoodTailScope, + ): readonly string[] { + const nodeIds = new Set(); + for (const rawOp of entry.patch.ops) { + for (const nodeId of neighborhoodNodeIdsForOperation(normalizeRawOp(rawOp), options)) { + nodeIds.add(nodeId); + } + } + return Object.freeze([...nodeIds].sort()); } reduceNodeLiveness( @@ -194,21 +204,6 @@ function isTargetNodeRemove( return op instanceof NodeRemove && op.node === nodeId; } -function isTargetNeighborhoodEdge( - op: NormalizedTailOperation, - options: { - readonly nodeId: string; - readonly direction: 'in' | 'out' | 'both'; - readonly labels: readonly string[]; - }, -): op is EdgeAdd | EdgeRemove { - if (!(op instanceof EdgeAdd) && !(op instanceof EdgeRemove)) { - return false; - } - return matchesNeighborhoodLabel(op, options.labels) - && matchesNeighborhoodDirection(op, options.nodeId, options.direction); -} - function matchesNeighborhoodLabel( op: EdgeAdd | EdgeRemove, labels: readonly string[], @@ -216,18 +211,23 @@ function matchesNeighborhoodLabel( return labels.length === 0 || labels.includes(op.label); } -function matchesNeighborhoodDirection( - op: EdgeAdd | EdgeRemove, - nodeId: string, - direction: 'in' | 'out' | 'both', -): boolean { - if (direction === 'out') { - return op.from === nodeId; - } - if (direction === 'in') { - return op.to === nodeId; +function neighborhoodNodeIdsForOperation( + op: NormalizedTailOperation, + options: NeighborhoodTailScope, +): readonly string[] { + if (!isEdgeMutation(op) || !matchesNeighborhoodLabel(op, options.labels)) { + return []; } - return op.from === nodeId || op.to === nodeId; + const byDirection: Readonly> = { + in: [op.to], + out: [op.from], + both: op.from === op.to ? [op.from] : [op.from, op.to], + }; + return byDirection[options.direction]; +} + +function isEdgeMutation(op: NormalizedTailOperation): op is EdgeAdd | EdgeRemove { + return op instanceof EdgeAdd || op instanceof EdgeRemove; } function readScalarTailPropertyValue( diff --git a/src/domain/services/optic/CheckpointTailWitnessLocator.ts b/src/domain/services/optic/CheckpointTailWitnessLocator.ts index 419851dae..716d784eb 100644 --- a/src/domain/services/optic/CheckpointTailWitnessLocator.ts +++ b/src/domain/services/optic/CheckpointTailWitnessLocator.ts @@ -32,6 +32,34 @@ type WitnessedNeighborhoodPage = TraversalNeighborhoodReadResult & { readonly direction: Direction; }; +type TraversalReadContext = { + readonly basis: CheckpointTailIndexBasis; + readonly tailByNode: ReadonlyMap; +}; + +type TailWitnessScanDraft = { + readonly entries: Array; + readonly witnesses: Array; +}; + +type NeighborhoodReadSupport = { + readonly basis: CheckpointTailIndexBasis; + readonly tail: TailWitnessScan; +}; + +type AddTraversalTailEntryOptions = { + readonly drafts: Map; + readonly entryIndex: number; + readonly reducer: CheckpointTailFactReducer; + readonly scope: { readonly direction: Direction; readonly labels: readonly string[] }; + readonly tail: TailWitnessScan; +}; + +const EMPTY_TAIL_WITNESS_SCAN: TailWitnessScan = Object.freeze({ + entries: Object.freeze([]), + witnesses: Object.freeze([]), +}); + export default class CheckpointTailWitnessLocator { private readonly _graphName: string; private readonly _basisLoader: CheckpointTailBasisLoader; @@ -199,11 +227,16 @@ export default class CheckpointTailWitnessLocator { private async _readWitnessedNeighborhoodPage( nodeId: string, options: NeighborhoodOpticReadOptions, + traversal: TraversalReadContext | null = null, ): Promise { const direction = normalizeDirection(options.direction); const labels = normalizeLabels(options.labels ?? []); - const basis = await this._basisLoader.load(); - const tail = await this._scanTailForNeighborhood(basis, { nodeId, direction, labels }); + const { basis, tail } = await this._neighborhoodReadSupport({ + direction, + labels, + nodeId, + traversal, + }); this._factReducer.assertNeighborhoodTailStable(tail.entries); const page = await this._shardReader.readNeighborhood(basis, { nodeId, @@ -213,31 +246,60 @@ export default class CheckpointTailWitnessLocator { limit: options.limit ?? null, }); return witnessedNeighborhoodPage({ - basis, - direction, - labels, - nodeId, + basis, direction, labels, nodeId, readIdentityBuilder: this._readIdentityBuilder, - tail, - page, + tail, page, }); } + private async _neighborhoodReadSupport(options: { + readonly direction: Direction; + readonly labels: readonly string[]; + readonly nodeId: string; + readonly traversal: TraversalReadContext | null; + }): Promise { + if (options.traversal !== null) { + return { + basis: options.traversal.basis, + tail: options.traversal.tailByNode.get(options.nodeId) ?? EMPTY_TAIL_WITNESS_SCAN, + }; + } + const basis = await this._basisLoader.load(); + const tail = await this._scanTailForNeighborhood(basis, options); + return { basis, tail }; + } + private async _readTraversalResult( optic: Optic, options: TraversalOpticReadOptions, ): Promise { - return await this._traversalReader().read(optic.nodeId(), options); + const traversal = await this._createTraversalReadContext({ + direction: normalizeDirection(options.direction), + labels: normalizeLabels(options.labels ?? []), + }); + return await this._traversalReader(traversal).read(optic.nodeId(), options); } - private _traversalReader(): CheckpointTailTraversalReader { + private _traversalReader(traversal: TraversalReadContext): CheckpointTailTraversalReader { return new CheckpointTailTraversalReader({ readNeighborhood: async (nodeId, readOptions) => ( - await this._readWitnessedNeighborhoodPage(nodeId, readOptions) + await this._readWitnessedNeighborhoodPage(nodeId, readOptions, traversal) ), }); } + private async _createTraversalReadContext(options: { + readonly direction: Direction; + readonly labels: readonly string[]; + }): Promise { + const basis = await this._basisLoader.load(); + const tail = await this._tailScan.collect({ basis, includeEntry: () => true }); + return Object.freeze({ + basis, + tailByNode: indexTraversalNeighborhoodTail(tail, this._factReducer, options), + }); + } + private async _scanTailForNode( basis: CheckpointTailIndexBasis, nodeId: string, @@ -274,6 +336,55 @@ export default class CheckpointTailWitnessLocator { } } +function indexTraversalNeighborhoodTail( + tail: TailWitnessScan, + reducer: CheckpointTailFactReducer, + options: { readonly direction: Direction; readonly labels: readonly string[] }, +): ReadonlyMap { + const drafts = new Map(); + for (let entryIndex = 0; entryIndex < tail.entries.length; entryIndex += 1) { + addTraversalTailEntry({ drafts, entryIndex, reducer, scope: options, tail }); + } + return new Map([...drafts.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([nodeId, draft]) => [nodeId, Object.freeze({ + entries: Object.freeze(draft.entries), + witnesses: Object.freeze(draft.witnesses), + })])); +} + +function addTraversalTailEntry(options: AddTraversalTailEntryOptions): void { + const { entry, witness } = requireTailWitnessPair(options.tail, options.entryIndex); + for (const nodeId of options.reducer.neighborhoodNodeIds(entry, options.scope)) { + const draft = options.drafts.get(nodeId) ?? { entries: [], witnesses: [] }; + draft.entries.push(entry); + draft.witnesses.push(witness); + options.drafts.set(nodeId, draft); + } +} + +function requireTailWitnessPair( + tail: TailWitnessScan, + entryIndex: number, +): { + readonly entry: TailWitnessScan['entries'][number]; + readonly witness: TailWitnessScan['witnesses'][number]; +} { + const entry = tail.entries[entryIndex]; + const witness = tail.witnesses[entryIndex]; + if (entry === undefined || witness === undefined) { + throw tailWitnessAlignmentError(entryIndex); + } + return { entry, witness }; +} + +function tailWitnessAlignmentError(entryIndex: number): QueryError { + return new QueryError('Traversal tail entry is missing its witness.', { + code: 'E_OPTIC_READ_IDENTITY', + context: { entryIndex }, + }); +} + function normalizeDirection(direction: Direction | undefined): Direction { if (direction === undefined) { return 'out'; diff --git a/test/conformance/v18TraversalOpticReadBasis.test.ts b/test/conformance/v18TraversalOpticReadBasis.test.ts index 3bc14bb34..5d48b02d1 100644 --- a/test/conformance/v18TraversalOpticReadBasis.test.ts +++ b/test/conformance/v18TraversalOpticReadBasis.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import V17CheckpointTailOpticGraphFixture from './fixtures/V17CheckpointTailOpticGraphFixture.ts'; import V17MaterializationFallbackTrap from './fixtures/V17MaterializationFallbackTrap.ts'; import V17PublicOpticReadPath from './fixtures/V17PublicOpticReadPath.ts'; @@ -10,6 +10,7 @@ const GAMMA_NODE_ID = 'node:gamma'; const DELTA_NODE_ID = 'node:delta'; const MISSING_GOAL_NODE_ID = 'node:missing-goal'; const LINK_LABEL = 'link'; +const NEIGHBORHOOD_CURSOR_PATTERN = /^git-warp:neighborhood-cursor:1:/; describe('v18 TraversalOptic checkpoint-tail read basis', () => { it('expands deterministic breadth-first traversal and reports goal-found completion', async () => { @@ -92,6 +93,65 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { }); }); + it('loads one checkpoint basis and writer tail per traversal request', async () => { + const fixture = await openTraversalFixture('v18-traversal-optic-request-support'); + const checkpointReads = vi.spyOn(fixture.graph, '_readCheckpointSha'); + const writerTailReads = vi.spyOn(fixture.graph, '_loadWriterPatches'); + const readPath = new V17PublicOpticReadPath(fixture.graph.worldline()); + + const result = await readPath.readTraversal(ROOT_NODE_ID, { + direction: 'out', + maxDepth: 2, + maxNodes: 10, + maxEdges: 10, + }); + + expect(Reflect.get(result, 'readIdentities')).toHaveLength(3); + expect(checkpointReads).toHaveBeenCalledTimes(1); + expect(writerTailReads).toHaveBeenCalledTimes(1); + }); + + it('fails closed when the tail changes a traversed neighborhood', async () => { + const fixture = await openTraversalFixture('v18-traversal-optic-tail-change'); + await fixture.graph.patch((patch) => { + patch.addNode('node:tail-neighbor'); + patch.addEdge(ROOT_NODE_ID, 'node:tail-neighbor', LINK_LABEL); + }); + const readPath = new V17PublicOpticReadPath(fixture.graph.worldline()); + + await expect(readPath.readTraversal(ROOT_NODE_ID, { + direction: 'out', + maxDepth: 1, + maxNodes: 10, + maxEdges: 10, + })).rejects.toMatchObject({ + code: 'E_OPTIC_NO_BOUNDED_BASIS', + context: { cause: 'tail-neighborhood-needs-adjacency-witnesses' }, + }); + }); + + it('does not contaminate traversal support with unrelated tail edges', async () => { + const fixture = await openTraversalFixture('v18-traversal-optic-unrelated-tail'); + await fixture.graph.patch((patch) => { + patch.addNode('node:unrelated-left'); + patch.addNode('node:unrelated-right'); + patch.addEdge('node:unrelated-left', 'node:unrelated-right', LINK_LABEL); + }); + const readPath = new V17PublicOpticReadPath(fixture.graph.worldline()); + + const result = await readPath.readTraversal(ROOT_NODE_ID, { + direction: 'out', + maxDepth: 1, + maxNodes: 10, + maxEdges: 10, + }); + + expect(result).toMatchObject({ + completeness: 'complete', + visitedNodeIds: [ALPHA_NODE_ID, BETA_NODE_ID, ROOT_NODE_ID], + }); + }); + it('resumes a node-budget-open frontier at the blocked edge', async () => { const fixture = await openTraversalFixture('v18-traversal-optic-node-budget-cursor'); const readPath = new V17PublicOpticReadPath(fixture.graph.worldline()); @@ -117,7 +177,7 @@ describe('v18 TraversalOptic checkpoint-tail read basis', () => { { nodeId: ROOT_NODE_ID, depth: 0, - edgeCursor: expect.stringMatching(/^git-warp:neighborhood-cursor:1:/), + edgeCursor: expect.stringMatching(NEIGHBORHOOD_CURSOR_PATTERN), }, { nodeId: BETA_NODE_ID, depth: 1, edgeCursor: null }, ], diff --git a/test/conformance/v19BoundedNeighborhoodReading.test.ts b/test/conformance/v19BoundedNeighborhoodReading.test.ts index ea788ad71..cf1af275e 100644 --- a/test/conformance/v19BoundedNeighborhoodReading.test.ts +++ b/test/conformance/v19BoundedNeighborhoodReading.test.ts @@ -180,5 +180,11 @@ function readIdentityShardPaths(result: object): readonly string[] { if (!Array.isArray(shards)) { throw new Error('expected checkpointIndexShards array'); } - return shards.map((shard) => String(Reflect.get(shard, 'path'))); + return shards.map((shard) => { + const path = Reflect.get(shard, 'path'); + if (typeof path !== 'string') { + throw new Error('expected checkpoint shard path'); + } + return path; + }); } diff --git a/test/type-check/v19-consumer.ts b/test/type-check/v19-consumer.ts index aff989367..fb1cc078d 100644 --- a/test/type-check/v19-consumer.ts +++ b/test/type-check/v19-consumer.ts @@ -49,6 +49,9 @@ const timelineName: string = timeline.name; const timelineWriter: string = timeline.writer; const tick: Tick = await timeline.tick(); const historical: TimelineView = timeline.at(tick); + +// @ts-expect-error formal coordinate capture lives on the advanced subpath. +await timeline.coordinate(); const writeIntent: Intent = intent.property.set({ subject: 'user:alice', key: 'role', diff --git a/test/type-check/v19-subpaths.ts b/test/type-check/v19-subpaths.ts index eeb7b1685..a3ca9752f 100644 --- a/test/type-check/v19-subpaths.ts +++ b/test/type-check/v19-subpaths.ts @@ -11,7 +11,7 @@ import { type GitStorageAdapterOptions, } from '../../storage.ts'; import { type Receipt, type Timeline } from '../../index.ts'; -import { Coordinate, Optic, type Witness } from '../../advanced.ts'; +import { captureCoordinate, Coordinate, Optic, type Witness } from '../../advanced.ts'; import { inspectReceipt, type ReceiptInspection } from '../../diagnostics.ts'; declare const gitStorageOptions: GitStorageAdapterOptions; @@ -19,7 +19,7 @@ declare const gitStorageOptions: GitStorageAdapterOptions; const storageAdapter = new MemoryStorageAdapter(); const gitStorageAdapter = new GitStorageAdapter(gitStorageOptions); declare const timeline: Timeline; -const coordinate: InstanceType = await timeline.coordinate(); +const coordinate: InstanceType = await captureCoordinate(timeline); const optic: InstanceType = coordinate.optic(); const node = await optic.node('user:alice').read(); const witness: Witness = node.readIdentity; diff --git a/test/unit/domain/WarpFacade.test.ts b/test/unit/domain/WarpFacade.test.ts index 02a633e5b..db906fd6e 100644 --- a/test/unit/domain/WarpFacade.test.ts +++ b/test/unit/domain/WarpFacade.test.ts @@ -3,6 +3,7 @@ import ts from 'typescript'; import { describe, expect, it } from 'vitest'; import { intent, openWarp, reading } from '../../../index.ts'; +import { captureCoordinate } from '../../../advanced.ts'; import DraftTimeline from '../../../src/domain/api/DraftTimeline.ts'; import JoinReceipt from '../../../src/domain/api/JoinReceipt.ts'; import JoinResult from '../../../src/domain/api/JoinResult.ts'; @@ -184,6 +185,7 @@ describe('v19 Warp facade', () => { expect('commit' in timeline).toBe(false); expect('live' in timeline).toBe(false); expect('optic' in timeline).toBe(false); + expect('coordinate' in timeline).toBe(false); }); it('keeps worldline openers off the root export surface', async () => { @@ -433,7 +435,7 @@ describe('v19 Warp facade', () => { outcome: 'accepted', evidence: { kind: 'checkpoint-tail-read' }, }); - const coordinate = await timeline.coordinate(); + const coordinate = await captureCoordinate(timeline); const coordinateRole = await coordinate.optic().node('user:alice').prop('role').read(); expect(coordinateRole.value).toBe('admin'); await expect( @@ -446,6 +448,11 @@ describe('v19 Warp facade', () => { ).resolves.toBe('admin'); const tick = await timeline.tick(); + const secondWarp = await openWarp({ storage, writer: 'agent-2' }); + const sameNamedForeignTimeline = await secondWarp.timeline('events'); + expect(() => sameNamedForeignTimeline.at(tick)).toThrow( + 'Tick does not belong to this Timeline' + ); await timeline.write( intent.property.set({ subject: 'user:alice', diff --git a/test/unit/domain/WarpGraph.coverageGaps.test.ts b/test/unit/domain/WarpGraph.coverageGaps.test.ts index 9f2ddacde..8723355dd 100644 --- a/test/unit/domain/WarpGraph.coverageGaps.test.ts +++ b/test/unit/domain/WarpGraph.coverageGaps.test.ts @@ -1050,7 +1050,9 @@ describe('WarpCore coverage gaps', () => { }); persistence.listRefs.mockResolvedValue(['refs/warp/test-graph/writers/writer-1']); - persistence.readRef.mockResolvedValue(sha1); + persistence.readRef + .mockResolvedValueOnce(null) + .mockResolvedValue(sha1); persistence.getNodeInfo .mockResolvedValueOnce(mockPatch.nodeInfo) .mockResolvedValueOnce({ ...mockPatch.nodeInfo, parents: [] }); diff --git a/test/unit/domain/services/controllers/CheckpointController.test.ts b/test/unit/domain/services/controllers/CheckpointController.test.ts index 44205cc8a..d7ad18156 100644 --- a/test/unit/domain/services/controllers/CheckpointController.test.ts +++ b/test/unit/domain/services/controllers/CheckpointController.test.ts @@ -5,6 +5,7 @@ import SchemaUnsupportedError from '../../../../../src/domain/errors/SchemaUnsup import GCPolicy from '../../../../../src/domain/services/GCPolicy.ts'; import GCExecuteResult from '../../../../../src/domain/services/GCExecuteResult.ts'; import WarpError from '../../../../../src/domain/errors/WarpError.ts'; +import PersistenceError from '../../../../../src/domain/errors/PersistenceError.ts'; import WarpState from '../../../../../src/domain/services/state/WarpState.ts'; /** @@ -469,6 +470,35 @@ describe('CheckpointController', () => { await expect(ctrl._validateMigrationBoundary()).resolves.toBeUndefined(); }); + + it('rejects a checkpoint ref whose commit message is empty', async () => { + ((host['_persistence'] as any).readRef as any).mockResolvedValue('cp-sha'); + ((host['_persistence'] as any).showNode as any).mockResolvedValue(''); + const validation = ctrl._validateMigrationBoundary(); + + await expect(validation).rejects.toMatchObject({ + code: 'E_CHECKPOINT_REF_INVALID', + context: { + checkpointSha: 'cp-sha', + reason: 'empty-checkpoint-message', + }, + }); + await expect(validation).rejects.toBeInstanceOf(PersistenceError); + }); + + it('rejects a checkpoint ref whose commit is not a checkpoint', async () => { + ((host['_persistence'] as any).readRef as any).mockResolvedValue('cp-sha'); + ((host['_persistence'] as any).showNode as any).mockResolvedValue('patch-message'); + detectMessageKindMock.mockReturnValue('patch'); + + await expect(ctrl._validateMigrationBoundary()).rejects.toMatchObject({ + code: 'E_CHECKPOINT_REF_INVALID', + context: { + checkpointSha: 'cp-sha', + reason: 'non-checkpoint-message', + }, + }); + }); }); /* ================================================================ */ diff --git a/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts b/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts index bfbd836d6..49fb91246 100644 --- a/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts +++ b/test/unit/domain/services/optic/CheckpointShardFactReader.manifest.test.ts @@ -99,17 +99,26 @@ describe('CheckpointShardFactReader manifest-backed routing', () => { }); const reader = new CheckpointShardFactReader({ source }); - await expect(reader.readNeighborhood(basis, { + const result = await reader.readNeighborhood(basis, { nodeId: NODE_ID, direction: 'out', labels: [], - })).resolves.toMatchObject({ + }); + + expect(result).toEqual({ cursor: null, edges: [{ direction: 'out', neighborId: NEIGHBOR_ID, label: EDGE_LABEL, }], + resumeCursors: [null], + checkpointIndexShards: [ + { path: sourceMetaPath, oid: sourceMetaOid }, + { path: neighborMetaPath, oid: neighborMetaOid }, + { path: outgoingPath, oid: outgoingOid }, + { path: labelsPath, oid: labelsOid }, + ].sort((left, right) => left.path.localeCompare(right.path)), }); }); }); diff --git a/test/unit/scripts/v19-public-api-boundary.test.ts b/test/unit/scripts/v19-public-api-boundary.test.ts index 5df4d052f..4e21dda17 100644 --- a/test/unit/scripts/v19-public-api-boundary.test.ts +++ b/test/unit/scripts/v19-public-api-boundary.test.ts @@ -58,8 +58,10 @@ type ModuleSurface = { readonly valueExports: readonly string[]; }; -function moduleSurface(relativePath = 'index.ts'): ModuleSurface { - const sourceFile = sourceFileFor(relativePath); +function moduleSurface(relativePath = 'index.ts', source?: string): ModuleSurface { + const sourceFile = source === undefined + ? sourceFileFor(relativePath) + : parseSourceFile(relativePath, source); const starExports: string[] = []; const typeExports: string[] = []; const valueExports: string[] = []; @@ -73,6 +75,10 @@ function moduleSurface(relativePath = 'index.ts'): ModuleSurface { starExports.push(statement.moduleSpecifier?.getText(sourceFile) ?? ''); continue; } + if (ts.isNamespaceExport(statement.exportClause)) { + starExports.push(statement.moduleSpecifier?.getText(sourceFile) ?? ''); + continue; + } if (!ts.isNamedExports(statement.exportClause)) { continue; } @@ -91,6 +97,10 @@ function moduleSurface(relativePath = 'index.ts'): ModuleSurface { function sourceFileFor(relativePath: string): ts.SourceFile { const source = readFileSync(new URL(relativePath, REPO_ROOT), 'utf8'); + return parseSourceFile(relativePath, source); +} + +function parseSourceFile(relativePath: string, source: string): ts.SourceFile { return ts.createSourceFile(relativePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); } @@ -174,7 +184,7 @@ function packageExportNames(relativePath: string): string[] { } function sorted(values: Iterable): string[] { - return Array.from(values).sort(); + return [...values].sort(); } function isRecord(value: unknown): value is Readonly> { @@ -188,6 +198,15 @@ describe('v19 public API boundary', () => { expect(surface.valueExports).toEqual(sorted(ROOT_VALUE_EXPORTS)); }); + it('classifies namespace re-exports as forbidden star exports', () => { + const surface = moduleSurface( + 'namespace-export.ts', + "export * as graph from './graph.ts';" + ); + + expect(surface.starExports).toEqual(["'./graph.ts'"]); + }); + it('locks the package-root companion types to an explicit contract', () => { expect(moduleSurface().typeExports).toEqual(sorted(ROOT_TYPE_EXPORTS)); }); @@ -202,7 +221,7 @@ describe('v19 public API boundary', () => { it('keeps the advanced subpath limited to bounded coordinate reads', () => { const surface = moduleSurface('advanced.ts'); expect(surface.starExports).toEqual([]); - expect(surface.valueExports).toEqual(['Coordinate', 'Optic']); + expect(surface.valueExports).toEqual(['Coordinate', 'Optic', 'captureCoordinate']); expect(surface.typeExports).toEqual( sorted([ 'NeighborhoodOpticCompleteness', From 03f725e77ab112edc235299f1382840aa1a41fca Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 13 Jul 2026 16:06:28 -0700 Subject: [PATCH 7/7] Restore neighborhood tail short-circuit --- src/domain/services/optic/CheckpointTailFactReducer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/domain/services/optic/CheckpointTailFactReducer.ts b/src/domain/services/optic/CheckpointTailFactReducer.ts index 38c9b50a4..8ddad48b9 100644 --- a/src/domain/services/optic/CheckpointTailFactReducer.ts +++ b/src/domain/services/optic/CheckpointTailFactReducer.ts @@ -47,7 +47,9 @@ export default class CheckpointTailFactReducer { entry: CheckpointTailPatchEntry, options: NeighborhoodTailScope & { readonly nodeId: string }, ): boolean { - return this.neighborhoodNodeIds(entry, options).includes(options.nodeId); + return entry.patch.ops.some((rawOp) => ( + neighborhoodNodeIdsForOperation(normalizeRawOp(rawOp), options).includes(options.nodeId) + )); } neighborhoodNodeIds(