diff --git a/.claude/skills/atlas/SKILL.md b/.claude/skills/atlas/SKILL.md new file mode 100644 index 00000000..5c3680fa --- /dev/null +++ b/.claude/skills/atlas/SKILL.md @@ -0,0 +1,190 @@ +--- +name: atlas +description: Maintain rosetta's architecture atlas (atlas/*.atlas plus @atlas comments, graded by Project Atlas) — place a new crate, package or directory in a layer and domain, keep selectors right through renames, write rules/contracts/flow steps/decisions that cite rosetta's docs, run `atlas check`, and fix dangling or violated claims. +--- + +# Atlas workflow + +The atlas states what rosetta's code cannot say about itself, and Project Atlas grades every +statement against the code: `verified`, `violated`, `asserted` (declared, nothing checkable), +`dangling` (a selector or reference matches nothing), `stale` (≥ 10 commits touched the code since +the claim's file last changed) or `ambiguous`. Rosetta's own docs stay the authority: the atlas +cites them, never replaces them. Read CLAUDE.md "Atlas" first. The DSL spec is +`$ATLAS/docs/DSL.md` (§3 selectors, §4 statements, §5 in-code, §7 grading, §8 agent guide). + +## Where it lives + +- `atlas/system.atlas` — system, thesis, `let $agents` (the 9 agent crates). +- `atlas/layers.atlas` — the layer stack (position is rank), the TS packages, the directories. +- `atlas/domains.atlas` — domain claims below crate level (modules, files, TS paths). +- `atlas/components.atlas` — runtime units and how they talk (`via http|ws|spawn|fs|ipc|port|embed|codegen`). +- `atlas/contracts.atlas` — `wire` (four anchors in two crates). +- `atlas/flows.atlas` — `live` and `search-query`: step ids, titles, `via`/`direct`, type-anchored steps. +- `atlas/rules.atlas`, `atlas/decisions.atlas` — cross-cutting rules; decisions with their status. +- In code (`grep -rn '@atlas' crates clients`): + - every crate root: its layer and domain, and the rules whose scope is that one crate + (core-is-a-leaf, pipeline-deps, thin-protocol, server-deps, search-seam, rosettad-is-root); + - contracts at their single anchor: `AgentAdapter`, `IngestOp`, `SearchBackend`; + - flow steps at the function that does the step (`// @atlas step live.read`); + - `room-dials-nothing` in the header of `clients/room/src/index.ts`. +- `atlas/atlas.baseline.json` — written only by `atlas baseline`. + +## Running it + +Atlas lives at `$ATLAS`, default `$HOME/Projects/project100/protos/project-atlas` +(github.com/vibefield/project-atlas, private; clone it there and `npm install`). From the rosetta root: + +```sh +ATLAS="${ATLAS:-$HOME/Projects/project100/protos/project-atlas}" +npm --prefix "$ATLAS" run -s atlas -- check "$PWD" --intent "$PWD/atlas" # what is wrong +npm --prefix "$ATLAS" run -s atlas -- check "$PWD" --intent "$PWD/atlas" --all # also asserted + verified +npm --prefix "$ATLAS" run -s atlas -- explain "$PWD" --intent "$PWD/atlas" crates/pipeline/src/skim.rs +npm --prefix "$ATLAS" run -s atlas -- explain "$PWD" --intent "$PWD/atlas" 'mod:rosetta_capture::{ledger, segment}' +npx --prefix "$ATLAS" tsx "$ATLAS/packages/engine-intent/src/cli.ts" atlas crates clients # syntax only, 2 s +``` + +- Pass `--intent "$PWD/atlas"`: it pins the DSL root and the baseline + (`atlas/atlas.baseline.json`, beside `system.atlas`) whatever Atlas version you run. + Relative paths resolve against the directory you run from. +- Rust facts come from `cargo doc`, which compiles and runs rosetta's build scripts with its + target dir in Atlas's own cache, not `target/`: about 20 s cold, 2–5 s when nothing changed. + Uncommitted files count. +- The last command parses `atlas/` and every `@atlas` comment without building anything: syntax + and duplicate ids only, no selector or reference checks. +- `baseline` (`npm --prefix "$ATLAS" run -s atlas -- baseline "$PWD" --intent "$PWD/atlas"`) turns + today's violations into `known` ones. Run it only when the user decides to accept them, never to + hide a violation your change caused. + +## Reading `check` + +``` +atlas check · Rosetta @ 4348545 · 381 claims + 339 verified · 37 asserted · 5 violated (5 new, 0 known) · 0 dangling · 0 ambiguous · 0 stale + coverage: layers 100.0% · domains 100.0% of 120,191 prod lines · rules 17 pass, 3 fail, 2 unchecked + +VIOLATED · new (5) + rule:server-deps only this -> crate:rosetta-{core, protocol, store} + at crates/server/src/lib.rs:10 · rule · basis edge + pkg:cargo/rosetta-server → pkg:cargo/rosetta-media + crates/server/src/http/images.rs:11 use rosetta_media::{MAX_INPUT_BYTES, PreparedImage}; + → this depends on 3 units outside its allow-list: rosetta-media, rosetta-pricing, rosetta-search (first at crates/server/src/http.rs:32). Add them to the only-list if intended, route the call through a… +``` + +- Sections in order: VIOLATED · new, VIOLATED · known (baselined), DANGLING, AMBIGUOUS, STALE. + ASSERTED and VERIFIED are only counted unless you pass `--all`. +- Each claim: `subject DSL text [severity]`, then `at file:line · kind · basis`, then the + evidence (`file:line source`), then `→` the remedy. Every glob prints its match count + (`crate:rosetta-adapter-* → 10`): read it; a count of 0 is the bug. +- Exit 1 only for a NEW violation of severity `error`. DANGLING, STALE, AMBIGUOUS and `warn` + rules (module-size) do not fail the check, but you still leave 0 dangling and 0 ambiguous. +- Both coverages are 100%: a drop means new code sits in no layer (also an `exhaustive` + violation) or in no domain (unclaimed). Place it. +- Findings left visible on purpose for the user to decide: `thin-protocol` (protocol re-exports + search/store types), `server-deps` (server uses media, pricing, search), `module-size` (14 + files, 2 of them TS), `ingest-op` (`IngestOp::GitCommit` has no producer), and + `dependency-rule` (fails because of the first two). Do not edit a rule or decision to make + one of them pass. + +## Fixing + +- **Dangling selector.** `explain` the selector: each term prints `→ n` and + `DANGLING: matches nothing` for the bad one. Find where the code went (`git log --follow`, + grep) and fix the selector in the same change as the rename. A crate's name also sits in other + crates' `@atlas` lines, often in a brace form (`crate:rosetta-{core, protocol, store}`) that a + grep for the full name misses: grep the stem. If the code is gone for good, delete the claim + with it. +- **Dangling `see`.** The message suggests the nearest heading (`Did you mean #…?`). A heading + rename in the docs dangles every `see` pointing at it: fix them together. +- **"has no code subject".** An `@atlas` comment sits below the last declaration or in the wrong + place; move it directly above its item. +- **Dependency rule violated.** The evidence names the package edge and its sites. Fix the code + (route through an allowed crate). The remedy line also offers "Add them to the only-list if + intended" and an `except …` to paste: an agent never takes either on its own. Widening an + allow-list or adding an exception changes the design, so the rule and the document it cites + (ARCHITECTURE.md §8) change together, and that is the user's call; propose it instead. A + sanctioned exception reads `except "real reason"` (see `store-sealed`, + `async-at-edge`, `search-seam`); an `except` that excuses nothing goes stale: delete it. +- **Contract violated.** `implemented-by` / `used-by` / `produced-by … each-variant` list the + missing package or enum variant: implement or produce it, or delete the dead variant. +- **Step asserted: "The step has no `at` anchor".** You deleted or renamed a function that + carried `// @atlas step flow.step`. Put the line above the function that now does the work. + A misspelt `@atlas step` id produces no claim and no error at all, so after touching a flow + run `check --all` and confirm every step of `flow:live` and `flow:search-query` is verified. +- **Stale.** At least 10 commits touched the claim's code since its file (the `.atlas` file, or + the source file holding the `@atlas` line) last changed. Re-read the code; fix the statement + if it no longer holds. Staleness is per file in v1, so it clears when that file is next committed. +- **Planned element landed.** `expects … [warn]` resolving means the thing was built: drop + `status planned`/`proposed` and `expects`, then bind it. + +## Authoring + +1. **Crate facts go in the crate root**, at the end of the existing `//!` block, after one blank + `//!` line. Only an inner doc comment in `lib.rs`/`main.rs` binds the whole package, bin + targets included; there `this` is the package. + + ```rust + //! …the crate's existing docs, unchanged… + //! + //! @atlas layer orchestration · domain ingest + //! @atlas rule pipeline-deps "pipeline → {core, capture, store, adapters}" { + //! only this -> crate:rosetta-{core, capture, store}, crate:rosetta-adapter-* + //! see "docs/ARCHITECTURE.md#8-workspace-layout" + //! } + ``` + + These lines render into rustdoc, and CI runs `clippy -D warnings` and doctests. So: never + touch the first doc paragraph; indent continuation lines 2 spaces, never 4+ (an indented + code block becomes a doctest); no `[…]` (intra-doc link), no ``, `-`, `*` or `1.`; when the docs end in a code fence, + insert after the closing fence. +2. **Everything else in Rust uses `//`**, never `///`: put it directly above the item, above its + `///` docs and attributes, at the item's indentation. `///` would render into rustdoc, and + ts-rs copies item docs into `clients/ts/generated/*.ts`. + + ```rust + // @atlas contract ingest-op "IngestOp: where agent weirdness ends" { + // produced-by $agents, crate:rosetta-pipeline each-variant + // used-by crate:rosetta-store + // } + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub enum IngestOp { + ``` +3. **TypeScript uses `//`** (a `*/` in a glob would close a block comment). +4. **Header pitfall.** A `//` annotation before a file's first code token binds the file (Rust: + its module; TS: the file, `index.*` its directory), not the declaration below it. Symbol + annotations go after the imports. The parse command with `--json` shows each annotation's + `host.style` (`inner-doc` for crate roots) and `host.fileHeader`; `explain ` shows + what it bound to (`crates/core/src/lib.rs:6 · root pkg:cargo/rosetta-core`). +5. **Standalone files** hold what spans crates: the layer structure, domain claims below crate + level, components and relations, cross-cutting rules, decisions. Declaring an element in two + places is a parse error. +6. **Cite rosetta.** Every rule and decision carries `see "#"`; `code IRn` only when + CLAUDE.md iron rule n is the source. A slug is the heading lowercased, with every character + that is not a letter, digit, whitespace or `-` dropped, then each whitespace character turned + into `-` (no collapsing); repeated headings get `-1`, `-2`; headings inside code fences don't + count. So `## 4. Layer 2 — Normalizer` → `4-layer-2--normalizer`, and `### 5.1 Transport + decision: one daemon, localhost HTTP/WS` → `51-transport-decision-one-daemon-localhost-httpws`. + A non-Markdown file (`docs/ingest-engine.html`) takes no `#slug`. +7. **Style** (DSL §8): ids are slugs; the coarsest true selector (crate > mod > path > sym); one + statement per line; prose only for what code cannot show, at most two sentences; never restate + a doc comment; do not annotate every module. + +## Worked examples + +- **New agent adapter** `crates/adapters/foo` (package `rosetta-adapter-foo`): end its `lib.rs` + docs with `//! @atlas layer agents · domain normalize`. `$agents` picks it up, so + `adapters-isolated`, `agents-sealed` and the `adapter` contract now cover it (the contract + fails until it implements `AgentAdapter`). Its `usage*.rs`, `images.rs` and `spec.rs` are + claimed by the path globs in `atlas/domains.atlas`. Only `rosettad` may name it. +- **New L2 crate.** Rank matters: `data-flows-up` (`layered`) forbids any edge going up the + stack, so pick the runtime sub-tier that sits above everything the crate depends on, and add + its allowed dependencies to the doc's §8 dependency rule if it has one. +- **New npm package or top-level directory.** No comment can bind a whole npm package: + add `contains pkg:…` or `contains path:dir/` to a layer in `atlas/layers.atlas` and a domain + in `atlas/domains.atlas`. +- **Splitting a big file** (module-size): `// @atlas step` lines move with their functions; + `mod:`/`file:`/`sym:` selectors that named the old path, in `atlas/` or in other code's + `@atlas` lines, dangle. Fix them in the + same change and check `--all` that the flow's steps are still verified. +- **Monitor lands.** When `mod:rosetta_server::metrics` exists, `domain monitor` reports its + `expects` as landed: drop `status planned` and `expects`, keep its `touches`, add `contains`. diff --git a/CLAUDE.md b/CLAUDE.md index 03704f7b..2b9e73f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -219,6 +219,7 @@ tools/groundtruth/ LAYER 1 dev tools (stdlib Python): fixture farm, mock model, census, schema merge, HTML render, boundary/artifact checks tools/ingest-bench/ local Electron cold-ingest profiler + read-only DB inspector +atlas/ graded design intent (*.atlas); see "Atlas" below crates/ all Rust crates adapter-support/ permissive parser + replay-test primitives adapters// raw parsers using canonical ground truth as spec @@ -266,6 +267,32 @@ clients/ TypeScript consumers [Phase 4] - `groundtruth/` is data-only. Rust belongs under `crates/`; repository tooling belongs under `tools/`. +## Atlas + +`atlas/*.atlas` plus `@atlas` comment lines (every crate root, a few trait/enum/fn seams) +declare what code cannot say about itself: layer and domain placement, contracts, the two +hot-path flows, dependency rules, decisions. Project Atlas grades each statement against the +code. It is not a third living document: every rule and decision cites this file, `docs/` or +a package README with a checked `see "file#slug"`. Authoring guide: the `atlas` skill. + +- A new crate, npm package or top-level directory is placed in a layer and a domain in the + same change: a crate ends its root `//!` doc with a blank `//!` line, then + `//! @atlas layer x · domain y`; anything else goes in `atlas/layers.atlas` and + `atlas/domains.atlas`. +- Renaming or moving code updates every selector that names it, in `atlas/` and in other + code's `@atlas` lines (brace forms like `crate:rosetta-{core, store}` hide from grep), and + `check` then shows 0 DANGLING. An `@atlas` line travels with the code it sits on. +- Never weaken a rule to make it pass: fix the code. Widening an `only` list, adding an + `except`, or changing what a rule or decision means is the user's call, even when `check` + suggests it. `atlas/atlas.baseline.json` is written only by `atlas baseline`, never by hand. +- Check after changing crates, packages, modules, dependencies or `atlas/`, with + `ATLAS=${ATLAS:-$HOME/Projects/project100/protos/project-atlas}`: + `npm --prefix "$ATLAS" run -s atlas -- check "$PWD" --intent "$PWD/atlas"`. Exit 1 = a new + error violation; DANGLING does not fail it but must be fixed. It runs `cargo doc`, a build; + when builds are off-limits, + `npx --prefix "$ATLAS" tsx "$ATLAS/packages/engine-intent/src/cli.ts" atlas crates clients` + checks syntax only. + ## Commands - `cargo check` / `cargo test --workspace` / `cargo fmt --all` / diff --git a/atlas/atlas.baseline.json b/atlas/atlas.baseline.json new file mode 100644 index 00000000..3b64ac55 --- /dev/null +++ b/atlas/atlas.baseline.json @@ -0,0 +1,26 @@ +{ + "schema": "atlas.baseline/1", + "violations": [ + "claim:contract:ingest-op/contract/857de262", + "claim:decision:dependency-rule/decision/e8d6f51b", + "rule:module-size|clients/room/src/lib/transcript/families.ts", + "rule:module-size|clients/ts/client.ts", + "rule:module-size|crates/adapters/claude-code/src/normalize.rs", + "rule:module-size|crates/adapters/codex/src/normalize.rs", + "rule:module-size|crates/adapters/copilot/src/normalize.rs", + "rule:module-size|crates/adapters/grok/src/normalize.rs", + "rule:module-size|crates/core/src/timeline.rs", + "rule:module-size|crates/pipeline/src/meta.rs", + "rule:module-size|crates/pipeline/src/replay.rs", + "rule:module-size|crates/rosettad/src/main.rs", + "rule:module-size|crates/rosettad/src/serve.rs", + "rule:module-size|crates/server/src/http.rs", + "rule:module-size|crates/store/src/apply.rs", + "rule:module-size|crates/store/src/queries.rs", + "rule:server-deps|pkg:cargo/rosetta-server|pkg:cargo/rosetta-media", + "rule:server-deps|pkg:cargo/rosetta-server|pkg:cargo/rosetta-pricing", + "rule:server-deps|pkg:cargo/rosetta-server|pkg:cargo/rosetta-search", + "rule:thin-protocol|pkg:cargo/rosetta-protocol|pkg:cargo/rosetta-search", + "rule:thin-protocol|pkg:cargo/rosetta-protocol|pkg:cargo/rosetta-store" + ] +} diff --git a/atlas/components.atlas b/atlas/components.atlas new file mode 100644 index 00000000..9e1a0b14 --- /dev/null +++ b/atlas/components.atlas @@ -0,0 +1,80 @@ +// Runtime units and how they talk. A link with `via` is a runtime claim: asserted, its `at` anchors must +// resolve and each `witness` literal must occur in them. A link without `via` is graded against code edges. + +component daemon "rosettad serve" kind daemon { + intent "One process: the sole synchronous canonical writer thread, a tokio/axum edge, the search worker." + contains crate:rosettad + see "docs/ARCHITECTURE.md#51-transport-decision-one-daemon-localhost-httpws" +} +component protocol-rs "rosetta-protocol (Rust wire types)" kind library { contains crate:rosetta-protocol } +component sdk "@vibefield/rosetta-protocol" kind library { contains pkg:@vibefield/rosetta-protocol } +component room-ui "@vibefield/rosetta-room" kind ui-library { + contains pkg:@vibefield/rosetta-room + see "clients/room/README.md#vibefieldrosetta-room" +} +component app "Agent History (Electron)" kind desktop-app { + contains pkg:@vibefield/rosetta-agent-history + component app-main "Electron main" kind process { + contains file:clients/app/electron/main.mjs, file:clients/app/electron/daemon-service.mjs + } + component preload "Preload bridge" kind bridge { contains file:clients/app/electron/preload.cjs } + component renderer "Renderer (React 19)" kind ui-process { contains path:clients/app/renderer/ } + component fx-host "Worktree host (mille + git)" kind utility-process { + contains file:clients/app/electron/fx-host.mjs, file:clients/app/electron/git-service.mjs + } +} +component gt-tools "Ground-truth pipeline" kind dev-cli { contains path:tools/groundtruth/ } +component ingest-bench "Cold-ingest profiler + DB inspector" kind dev-tool { contains path:tools/ingest-bench/ } +component agent-clis "Agent CLIs (~/.claude, ~/.codex, …)" external +component supervisor "Supervisors (VibeField fieldd, chopsticks)" external + +agent-clis -> daemon "JSONL appends, rewrites, SQLite DBs" via fs { + at mod:rosetta_capture::watch + witness "recommended_watcher" +} +app-main -> daemon "serve --json --announce; version-checked; SIGINT, then SIGKILL" via spawn { + at file:clients/app/electron/daemon-service.mjs + witness "--announce" + witness "PROTOCOL_VERSION" +} +sdk -> daemon "REST /api/*; the version header is checked first" via http { + at sym:clients/ts/client.ts#RosettaClient, mod:rosetta_server::http + witness "/api/search" + witness "x-rosetta-protocol-version" +} +sdk -> daemon "WS /ws/sessions, /ws/meta, /ws/collaboration" via ws { + at sym:clients/ts/client.ts#RosettaClient, mod:rosetta_server::http + witness "/ws/meta" +} +renderer -> app-main "contextBridge: daemon state, git facts, previews" via ipc { + at file:clients/app/electron/preload.cjs, file:clients/app/renderer/src/host/desktop-bridge.ts + witness "agentHistoryHost" +} +renderer -> fx-host "transferred MessagePorts: worktree tree + git streams" via port { + at file:clients/app/renderer/src/host/worktree-ports.ts + witness "ah-worktree-ports" +} +daemon -> supervisor "announce file: origin, pid, protocol version" via fs { + at mod:rosettad::announce + witness "rosetta-daemon/1" +} +supervisor -> daemon "--enroll file: authoritative consent" via fs { + at mod:rosettad::enroll + witness "enabled_kinds" +} +sdk -> protocol-rs "clients/ts/generated/ comes from ts-rs; never hand-written" via codegen { + at file:crates/protocol/src/lib.rs + witness "ts_rs::TS" +} +gt-tools -> daemon "groundtruth.json compiled into every adapter" via embed { + at path:crates/adapters/*/src/spec.rs + witness "groundtruth.json" +} +ingest-bench -> daemon "reads the projection SQLite directly (sanctioned escape hatch)" via fs { + at file:tools/ingest-bench/src/lib/database.js + witness "node:sqlite" +} + +renderer -> room-ui "hosts HistoryRoom and hands it every port" +renderer -> sdk "the reference host builds the room's ports over RosettaClient" +room-ui -> sdk "wire types only; the room dials nothing" diff --git a/atlas/contracts.atlas b/atlas/contracts.atlas new file mode 100644 index 00000000..eaa72b23 --- /dev/null +++ b/atlas/contracts.atlas @@ -0,0 +1,13 @@ +// The wire seam has four anchors in two crates, so it is declared here. Contracts with one anchor are declared +// in-code at their trait or enum: adapter (AgentAdapter), ingest-op (IngestOp), search-backend (SearchBackend). + +contract wire "Wire protocol (REST + WS frames)" { + intent "Protocol re-exports core/store/search types instead of DTO mirrors, so the wire shape follows L2." + at sym:rosetta_protocol::ServerFrame, sym:rosetta_protocol::ErrorCode + at sym:rosetta_store::hot::{Mutation, Stamp} + mirror "clients/ts/generated/{name}.ts#{name}" + used-by crate:rosetta-server + note file:clients/ts/protocol-version.mjs "Bumped together with PROTOCOL_VERSION; the wire golden test checks that both agree." + see "docs/ARCHITECTURE.md#52-the-three-services" + see "clients/ts/README.md#vibefieldrosetta-protocol" +} diff --git a/atlas/decisions.atlas b/atlas/decisions.atlas new file mode 100644 index 00000000..8b85b81c --- /dev/null +++ b/atlas/decisions.atlas @@ -0,0 +1,90 @@ +// Decisions with their real status. Verified only when accepted and every confirmed-by rule, contract or +// flow is verified; proposed and hypothesis decisions are asserted. + +decision dependency-rule "Crate dependency rule: enforced, not aspirational" status accepted { + because "spaghetti's layers leaked into each other; ARCHITECTURE.md §8 states the rule for adapters, pipeline, server, core, protocol and rosettad." + affects path:crates/ + confirmed-by data-flows-up, rosettad-is-root, core-is-a-leaf, + thin-protocol, server-deps, pipeline-deps, adapters-isolated + see "docs/ARCHITECTURE.md#8-workspace-layout" +} + +decision model-hypothesis "Canonical model is a hypothesis until Phase 2" code IR2 status hypothesis { + because "spaghetti overfit its model to one agent (Claude Code)." + affects mod:rosetta_core::{ops, timeline, rows} + see "docs/ARCHITECTURE.md#41-one-canonical-model-consumer-driven" +} + +decision machine-independent-truth "Machine-independent ground truth" code IR3 status accepted { + because "CI enforcement is fixture-only; local agent data may corroborate but is never load-bearing." + affects path:groundtruth/, path:tools/groundtruth/ + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" + see "docs/ARCHITECTURE.md#3-layer-1--ground-truth-evidence" +} + +decision two-representations "Two representations: raw ledger + one projection" code IR4 status accepted { + because "spaghetti kept three coexisting models in a 101-table schema." + affects crate:rosetta-capture, crate:rosetta-store + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" + see "docs/ARCHITECTURE.md#6-storage" +} + +decision persist-what-users-care-about "Persist only what a user cares about" code IR5 status accepted { + because "Normalization is deliberately lossy; reconciliation machinery may live in RAM, never in canonical SQLite." + affects crate:rosetta-store, crate:rosetta-core, crate:rosetta-pipeline + confirmed-by resolver-mints-project-id + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" +} + +decision parsers-once "Parsers exist once, in Rust; TS gets generated types" code IR6 status accepted { + because "spaghetti kept ~20k LOC of duplicate TS parsers." + affects crate:rosetta-protocol, path:clients/ts/generated/ + confirmed-by wire, room-dials-nothing + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" +} + +decision one-groundtruth-file "One ground-truth file per agent" code IR7 status accepted { + because "Intermediates contribute but are never authoritative, so schema truth is read from one place." + affects path:groundtruth/, path:tools/groundtruth/ + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" +} + +decision ledger-first "Ledger-first crash protocol" status accepted { + because "An orphan ledger tail is harmless; a projection row that points at unflushed ledger is forbidden." + affects mod:rosetta_pipeline::stream, mod:rosetta_capture::ledger + confirmed-by live + see "docs/ingest-engine.html" +} + +decision single-writer "Single synchronous writer; async only at the L3 edge" status accepted { + because "Debuggability: plain threads and bounded channels keep flamegraphs readable." + affects crate:rosettad, crate:rosetta-server + confirmed-by async-at-edge + see "docs/ARCHITECTURE.md#51-transport-decision-one-daemon-localhost-httpws" + see "docs/ingest-engine.html" +} + +decision fts5-sidecar "Detached FTS5 sidecar behind a backend-neutral seam" status accepted { + because "Search loss never blocks canonical reads; Tantivy has not cleared relevance or storage acceptance." + affects crate:rosetta-search + confirmed-by search-backend, search-seam, tantivy-benchmark-only + see "docs/ARCHITECTURE.md#6-storage" + see "docs/ARCHITECTURE.md#11-open-decisions-defaults-chosen-cheap-to-revisit" +} + +decision access-control "Open listener by default; bearer and per-session grants on demand" status accepted date 2026-09-30 { + affects mod:rosetta_server::{auth, grant}, mod:rosettad::auth_files + see "docs/ARCHITECTURE.md#11-open-decisions-defaults-chosen-cheap-to-revisit" + see "docs/AUTH.md#2-two-guards-never-one-for-the-other" +} + +decision ingestion-api "Ingestion lifecycle API" status proposed date 2026-10-05 { + expects mod:rosettad::ingestion? + see "docs/INGESTION_API_PROPOSAL.md#decision" +} + +decision daemon-lifecycle "Daemon ownership, control, and recovery" status proposed date 2026-10-06 { + expects mod:rosettad::lifecycle? + affects crate:rosettad, file:clients/app/electron/daemon-service.mjs + see "docs/DAEMON_LIFECYCLE_PROPOSAL.md#1-decisions" +} diff --git a/atlas/domains.atlas b/atlas/domains.atlas new file mode 100644 index 00000000..12411749 --- /dev/null +++ b/atlas/domains.atlas @@ -0,0 +1,124 @@ +// Domains cut across layers and languages. Each crate root names its crate's domain in-code +// (`//! @atlas layer … · domain …`); the claims below are the modules, files and TS paths that serve another +// domain. The most specific `contains` wins; `touches` adds a secondary membership and never competes. + +domain capture "Discovery, consent & capture" { + contains mod:rosetta_pipeline::{discover, skim, snapshot, document} + contains mod:rosetta_server::{consent, enrolled}, mod:rosettad::enroll + contains file:clients/app/renderer/src/components/RollCall.tsx + touches mod:rosetta_adapter_support::glob + see "docs/ARCHITECTURE.md#35-runtime-handoff-watcher-tailer-and-raw-ledger" + see "docs/ARCHITECTURE.md#supervisor-enrolled-runtimes-2026-09-12" +} + +domain ledger "Raw ledger (durable truth)" { + contains mod:rosetta_capture::{ledger, segment, segment_index, recompress} + contains mod:rosetta_pipeline::{replay, command} + contains mod:rosettad::{runtime_ledger, ledger_cmd, ledger_verify, ledger_compression, cold_stage} + see "docs/ARCHITECTURE.md#35-runtime-handoff-watcher-tailer-and-raw-ledger" +} + +domain normalize "Per-agent normalization" { + intent "Where agent weirdness ends: native records in, canonical IngestOps out." + see "docs/ARCHITECTURE.md#4-layer-2--normalizer" +} + +domain ingest "Cold & live ingestion" { + contains mod:rosetta_store::{apply, cold_stream, bulk, reclaim} + contains mod:rosetta_server::{health, board} + contains file:clients/room/src/lib/{ingest, ingest-activity, use-ingest-activity, catalog-status}.ts + contains file:clients/room/src/components/{IngestStatus, ColdStart}.tsx + see "CLAUDE.md#current-phase" + see "docs/ingest-engine.html" +} + +domain projection "Canonical projection" { + intent "One disposable SQLite projection plus content-addressed members; a cache of the ledger." + see "docs/ARCHITECTURE.md#6-storage" +} + +domain search "Full-text search (job 1)" { + contains mod:rosetta_store::search_sync, sym:rosetta_server::http::search + contains path:clients/room/src/components/search/, path:clients/room/src/lib/search-*.ts + touches sym:clients/ts/client.ts#RosettaClient.search + see "docs/ARCHITECTURE.md#52-the-three-services" +} + +domain observe "Live observation (job 2)" { + intent "The History room is this job's client: every catalog list, transcript and live patch." + contains pkg:@vibefield/rosetta-room + contains mod:rosetta_store::hot, mod:rosetta_server::{hub, ws}, mod:rosetta_server::http::activity + contains sym:clients/ts/client.ts#{SessionSubscription, MetaSubscription, CollaborationSubscription} + contains file:clients/ts/ordered-cache.ts + see "docs/ARCHITECTURE.md#52-the-three-services" +} + +domain usage "Usage & spend" { + contains path:tools/pricing/ + contains mod:rosetta_core::{fold, usage_totals}, mod:rosetta_store::apply::usage_owners + contains mod:rosetta_store::{pricing, usage_sql, usage_totals} + contains path:crates/adapters/*/src/usage*.rs, path:crates/adapters/copilot/src/usage/ + contains path:clients/room/src/components/charts/ + contains path:clients/room/src/lib/{spend, chart-data, day-activity, model-labels, use-day-usage}.ts + contains file:clients/room/src/components/{SummaryPanels, SummaryMetrics}.tsx + see "docs/ARCHITECTURE.md#413-usage-accounting-and-list-price-usd-2026-09-22" +} + +domain media "Images & assets" { + contains mod:rosetta_adapter_support::media, mod:rosetta_core::assets + contains mod:rosetta_store::assets, mod:rosetta_server::http::images + contains path:crates/adapters/*/src/images.rs, file:crates/adapters/claude-code/src/media.rs + contains path:clients/room/src/components/file-viewer/, file:clients/room/src/lib/image-media.ts + see "docs/ARCHITECTURE.md#412-authored-images-2026-09-09" +} + +domain auth "Access control" { + contains mod:rosetta_server::{auth, grant}, mod:rosettad::auth_files + contains file:clients/ts/grants.ts, file:clients/ts/base64url.ts, path:tools/auth/ + see "docs/AUTH.md#daemon-access-control" +} + +domain api "Daemon API (REST + WS + SDK)" { + intent "The wire surface every consumer dials; the job domains claim their own routes and frames." + contains pkg:@vibefield/rosetta-protocol + touches path:docs/site/ + see "docs/ARCHITECTURE.md#5-layer-3--service-provider" + see "clients/ts/README.md#package-shape" +} + +domain host "Room hosting & daemon supervision" { + intent "How a host gives the room its ports and owns one daemon." + contains pkg:@vibefield/rosetta-agent-history + contains file:clients/room/src/{ports, host-inputs}.ts, file:clients/room/src/room-context.tsx + contains file:clients/room/src/components/{HostMenu, HostAct}.tsx, file:clients/room/src/lib/session-leases.ts + contains mod:rosettad::announce + see "clients/room/README.md#the-ports-srcportsts" + see "docs/ARCHITECTURE.md#51-transport-decision-one-daemon-localhost-httpws" +} + +domain worktree "Worktree & git column" { + contains path:clients/room/src/components/{worktree, git}/ + contains file:clients/app/electron/{git-service, fx-host, worktree-file-actions, worktree-explorer-config}.mjs + contains file:clients/app/renderer/src/host/worktree-ports.ts + see "clients/room/README.md#the-ports-srcportsts" +} + +domain perf "Performance evidence" { + intent "Frozen before/after pairs authorize optimizations." + contains mod:rosettad::{bench, brutal}, mod:rosetta_pipeline::instrument, path:tools/ingest-bench/ + see "docs/ARCHITECTURE.md#7-performance-playbook" + see "CLAUDE.md#conventions" +} + +domain groundtruth "Ground-truth evidence (L1)" { + contains path:groundtruth/, path:tools/groundtruth/ + contains path:crates/adapters/*/src/spec.rs + see "docs/ARCHITECTURE.md#3-layer-1--ground-truth-evidence" +} + +domain monitor "Metric feeds (job 3)" status planned { + intent "Distilled live metrics for runtime monitors such as chopsticks; WS /v1/metrics is documented but not routed." + expects mod:rosetta_server::metrics? + touches mod:rosetta_server::{health, board}, mod:rosetta_server::http::activity + see "docs/ARCHITECTURE.md#52-the-three-services" +} diff --git a/atlas/flows.atlas b/atlas/flows.atlas new file mode 100644 index 00000000..5a6bef4e --- /dev/null +++ b/atlas/flows.atlas @@ -0,0 +1,35 @@ +// Two hot paths. A step done by a function is bound in-code (`// @atlas step .` above it; list them +// with `grep -rn '@atlas step' crates clients`); a step anchored on a type keeps its `at` here. A step with no +// anchor at all grades `asserted`, not dangling: after deleting or renaming a step's function, re-bind it. +// A hop is verified by an edge between its steps, one shared unit, or a `by` orchestrator reaching both; +// `via` marks a runtime hop (asserted). + +flow live "Live file change → client update" { + intent "The observe hot path; the budget is watch event → subscriber frame p99 < 20 ms." + by mod:rosettad::serve, mod:rosetta_pipeline::stream + step wake "notify event wakes the shared SourceWake" at sym:rosetta_capture::watch::NotifyEvents + step tick "writer thread ticks each live runner" + step map "runner maps the hint to changed sources" direct + step read "tailer reads complete records only" + step parse "adapter turns each record into IngestOps" + step ledger "ledger first: append, then durable flush" + step apply "one SQLite transaction: checkpoint CAS + rows" + step absorb "drained effects reach the hub" + step hub "hub fans mutations into per-session tail rings" direct + step frame "mutation frames on /ws/sessions/{id}" direct + step patch "SDK patches its cache, 16 ms coalescing" at sym:clients/ts/client.ts#SessionSubscription via ws + step render "room hooks re-render the transcript" via port + see "docs/ARCHITECTURE.md#71-cross-adapter-applicability-checkpoint-2026-08-27" +} + +flow search-query "Search query" { + intent "Job 1. A sidecar generation behind the canonical watermark answers `catching-up` from a consistent snapshot." + step sheet "search sheet collects query and facets" + step feed "useSearch asks the injected CatalogPort" + step port "the host's catalog port wraps RosettaClient" via port + step client "GET /api/search?q=…" + step handler "parse grammar, expand session scope" via http + step backend "spawn_blocking → SearchBackend::search" at sym:rosetta_search::SearchBackend + step sidecar "FTS5 MATCH on the generation CURRENT names" + see "docs/ARCHITECTURE.md#52-the-three-services" +} diff --git a/atlas/layers.atlas b/atlas/layers.atlas new file mode 100644 index 00000000..6a954e81 --- /dev/null +++ b/atlas/layers.atlas @@ -0,0 +1,39 @@ +// Position is rank, top to bottom; nested layers are ordered sub-tiers. Every Rust crate places itself in its +// crate root (`//! @atlas layer …`), so the crate leaves below are empty here. This file holds the structure and +// what is not a crate: the TS packages and the directories. `exhaustive`: an unplaced production file is a violation. + +layers stack "Data flows up; dependencies point down" exhaustive { + see "docs/ARCHITECTURE.md#2-the-three-layers" + layer consumers "Consumers" { + intent "Consumers never reach past L3 into the store." + layer host-app "Desktop host" { contains pkg:@vibefield/rosetta-agent-history } + layer room-lib "History room package" { contains pkg:@vibefield/rosetta-room } + layer client-sdk "TS client SDK" { contains pkg:@vibefield/rosetta-protocol } + } + layer services "Services" code L3 { + layer edge "Runtime edge (axum REST + WS)" + layer wire-types "Wire contract" + layer query "Search seam" + note crate:rosetta-search "ARCHITECTURE.md §8 calls it the L3 search boundary; the §2 diagram omits it." + } + layer runtime "Runtime" code L2 { + layer orchestration "Ingest orchestration" + layer agents "Agent adapters" independent + layer adapter-kit "Adapter kit" + layer storage "Projection store" + layer capture-io "Capture & raw ledger" + layer kernels "Pure kernels" + layer model "Canonical model" + note crate:rosetta-media, crate:rosetta-pricing "In no layer diagram of the docs; pure libraries used by both L2 and L3." + } + layer truth "Ground truth" code L1 { + contains path:groundtruth/, path:tools/groundtruth/ + intent "Dev-time evidence; runs no production code. Adapters embed groundtruth.json, so no code edge reaches it." + } + layer root "Composition root" outside { + intent "CLI and daemon in one binary; wires every crate and nothing depends on it." + } + layer tooling "Evidence & tooling" outside { + contains path:tools/, path:docs/site/ + } +} diff --git a/atlas/rules.atlas b/atlas/rules.atlas new file mode 100644 index 00000000..19267bbb --- /dev/null +++ b/atlas/rules.atlas @@ -0,0 +1,90 @@ +// Cross-cutting rules. A rule about one crate or package lives in its root (`grep -rn '@atlas rule' crates clients`). +// Every rule cites where rosetta states it; `code IRn` is CLAUDE.md iron rule n. Checks see production code only +// (no tests, benches, cfg(test) code or dev-dependencies). Never weaken a rule to pass: fix the code. An +// `except "real reason"` or a wider `only` list changes the design: that is the user's call. + +rule data-flows-up "Data flows up only; dependencies point down" code IR1 { + layered + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" + see "docs/ARCHITECTURE.md#2-the-three-layers" +} + +rule adapters-isolated "All knowledge of one agent lives in its adapter pair" code IR1 { + independent $agents + only $agents -> crate:rosetta-core, crate:rosetta-adapter-support + see "docs/ARCHITECTURE.md#31-one-data-pack-and-one-parser-crate-per-agent" + see "docs/ARCHITECTURE.md#8-workspace-layout" +} + +rule agents-sealed "Only the composition root names a concrete adapter" code IR1 { + deny *, !crate:rosettad -> $agents + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" +} + +rule resolver-mints-project-id "The shared meta resolver, never an adapter, mints the canonical project id" code IR5 { + deny $agents, crate:rosetta-adapter-support -> sym:rosetta_core::ids::project_id + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" +} + +rule l3-parses-nothing "L3 never parses agent files" { + deny layer:services -> layer:agents, layer:adapter-kit, layer:capture-io + see "docs/ARCHITECTURE.md#2-the-three-layers" +} + +rule store-sealed "Consumers never reach past L3 into the store" code IR1 { + forbid text "node:sqlite", "better-sqlite3" in path:clients/, path:tools/ + except file:tools/ingest-bench/src/lib/database.js "ARCHITECTURE.md §5.1: the read-only escape hatch onto an unstable internal format" + see "docs/ARCHITECTURE.md#51-transport-decision-one-daemon-localhost-httpws" +} + +rule tooling-stays-out "Production crates never import development tooling" { + deny *, !layer:tooling -> layer:tooling + see "docs/ARCHITECTURE.md#8-workspace-layout" +} + +rule pure-normalizers "normalize() is pure: no IO, no clock, no randomness" { + forbid text "std::fs", "File::open", "std::env", "std::net", "std::process", + "SystemTime", "Instant::now", "thread::spawn", "rand::" in $agents, + crate:rosetta-{core, adapter-support, media, pricing} + see "CLAUDE.md#conventions" + see "docs/ARCHITECTURE.md#42-normalization-is-a-pure-versioned-function" +} + +rule async-at-edge "Async lives only at the L3 edge" { + forbid text "async fn", "tokio::" in path:crates/, !crate:rosetta-server + deny path:crates/, !crate:rosetta-server -> ext:tokio, ext:axum, ext:futures-util, ext:tower, ext:tower-http + except file:crates/rosettad/src/serve.rs "composition root builds the tokio runtime that hosts the L3 server" + see "docs/ingest-engine.html" +} + +rule tantivy-benchmark-only "Tantivy stays a benchmark-only prototype" { + deny *, !crate:rosetta-search-bench -> ext:tantivy + see "docs/ARCHITECTURE.md#11-open-decisions-defaults-chosen-cheap-to-revisit" +} + +rule grants-node-only "Grants sign with node:crypto; no browser bundle imports them" { + deny pkg:@vibefield/rosetta-room, path:clients/app/renderer/, + file:clients/ts/index.ts, file:clients/ts/client.ts -> file:clients/ts/grants.ts + see "clients/ts/README.md#package-shape" +} + +rule generated-bindings "clients/ts/generated/ holds only ts-rs output" code IR6 { + limit code-lines <= 0 in path:clients/ts/generated/ + see "clients/ts/README.md#vibefieldrosetta-protocol" +} + +rule groundtruth-data-only "groundtruth/ is data-only" { + limit code-lines <= 0 in path:groundtruth/ + see "CLAUDE.md#conventions" +} + +rule permissive-raw "Raw serde types are permissive" { + forbid text "deny_unknown_fields" in path:crates/adapters/*/src/raw/ + see "CLAUDE.md#conventions" +} + +rule module-size "Keep modules under ~1,000 lines" warn { + limit lines <= 1000 in path:crates/, path:clients/ + see "CLAUDE.md#conventions" + see "docs/ARCHITECTURE.md#1-lessons-from-spaghetti-what-we-keep-what-we-kill" +} diff --git a/atlas/system.atlas b/atlas/system.atlas new file mode 100644 index 00000000..db263831 --- /dev/null +++ b/atlas/system.atlas @@ -0,0 +1,21 @@ +atlas 1 + +// Rosetta's design intent: what the code cannot say about itself, graded against the code by Project Atlas. +// Crate roots and a few seams carry their own `@atlas` lines (`grep -rn '@atlas' crates clients`). +// Duties and the one command: CLAUDE.md "Atlas"; the authoring guide is the `atlas` skill. + +system rosetta "Rosetta" { + intent "Multi-agent ingestion engine: ledger every raw record coding agents write, serve one canonical projection over localhost HTTP/WS." + thesis """ + Three jobs, in consumer priority: search, observe, monitor. + L1 ground truth → L2 runtime → L3 services: data flows up, dependencies point down. + All knowledge of one agent lives in its pair: groundtruth/ and crates/adapters/. + Two data representations only: an append-only raw ledger and one disposable projection, rebuilt, never migrated. + Parsers exist once, in Rust; consumers get generated types and dial the daemon, never the store. + """ + see "CLAUDE.md#iron-rules-each-one-is-a-spaghetti-failure-mode--do-not-relitigate" + see "docs/ARCHITECTURE.md#rosetta-architecture" +} + +// The adapter glob also matches rosetta-adapter-support; the agents are the other 9. +let $agents = crate:rosetta-adapter-*, !crate:rosetta-adapter-support diff --git a/clients/app/renderer/src/host/rosetta-ports.ts b/clients/app/renderer/src/host/rosetta-ports.ts index 32d57d76..94862970 100644 --- a/clients/app/renderer/src/host/rosetta-ports.ts +++ b/clients/app/renderer/src/host/rosetta-ports.ts @@ -15,6 +15,7 @@ export interface DaemonWords { const NO_WORDS: DaemonWords = { unavailable: () => null, onChange: () => () => {} }; +// @atlas step search-query.port /** The catalog over the daemon's own lists and figures: REST and /ws/meta. */ export function catalogOver(client: RosettaClient, words: DaemonWords = NO_WORDS): CatalogPort { return { diff --git a/clients/room/src/components/search/SearchSheet.tsx b/clients/room/src/components/search/SearchSheet.tsx index d1c09b1b..28033ce8 100644 --- a/clients/room/src/components/search/SearchSheet.tsx +++ b/clients/room/src/components/search/SearchSheet.tsx @@ -211,6 +211,7 @@ const GRAMMAR_HINT = ( ); +// @atlas step search-query.sheet export function SearchSheet({ open, onClose, diff --git a/clients/room/src/index.ts b/clients/room/src/index.ts index dacf0509..37f749c4 100644 --- a/clients/room/src/index.ts +++ b/clients/room/src/index.ts @@ -1,6 +1,11 @@ // @vibefield/rosetta-room — rosetta's History room behind injected ports. A host hands // it a catalog, a way to open one session's body, an asset source and, on a // desktop, a host port; the room dials nothing itself. See README.md. +// +// @atlas rule room-dials-nothing "The room dials nothing: every byte arrives through a port" { +// forbid text "fetch(", "new WebSocket(", "new EventSource(", "XMLHttpRequest" in pkg:@vibefield/rosetta-room +// see "clients/room/README.md#what-the-package-never-does" +// } export { HistoryRoom } from "./HistoryRoom"; export type { diff --git a/clients/room/src/lib/hooks.ts b/clients/room/src/lib/hooks.ts index e8c2949a..b85242f3 100644 --- a/clients/room/src/lib/hooks.ts +++ b/clients/room/src/lib/hooks.ts @@ -326,6 +326,7 @@ export interface TimelineFeed { loadAnchor: (timelineId: string) => Promise; } +// @atlas step live.render export function useTimeline(sessions: SessionLeases, sessionId: string | null): TimelineFeed { const [owner, setOwner] = useState(sessionId); const [attempt, setAttempt] = useState(0); @@ -693,6 +694,7 @@ export interface SearchFacets { export const SEARCH_LIMIT = 200; +// @atlas step search-query.feed /** * One ranked search per settled query. The previous response stays on screen * while its successor is in flight — a list that blanks on every keystroke diff --git a/clients/ts/client.ts b/clients/ts/client.ts index b2fddbde..45916de4 100644 --- a/clients/ts/client.ts +++ b/clients/ts/client.ts @@ -527,6 +527,7 @@ export class RosettaClient { return new Uint8Array(await response.arrayBuffer()); } + // @atlas step search-query.client search(request: SearchRequest, signal?: AbortSignal): Promise { return this.request( `/api/search${queryString({ diff --git a/crates/adapter-support/src/adapter.rs b/crates/adapter-support/src/adapter.rs index fd2adb47..003f4051 100644 --- a/crates/adapter-support/src/adapter.rs +++ b/crates/adapter-support/src/adapter.rs @@ -227,6 +227,10 @@ impl NormalizeCtx { } } +// @atlas contract adapter "Agent adapter interface" { +// implemented-by $agents +// used-by crate:rosetta-pipeline, crate:rosettad +// } /// One implementation per agent. Object-safe: the pipeline holds a registry /// of `&'static dyn AgentAdapter`. pub trait AgentAdapter: Send + Sync { @@ -390,6 +394,7 @@ pub trait AgentAdapter: Send + Sync { true } + // @atlas step live.parse /// Stream phase: one raw record in, canonical ops out. Combines the pure /// parse and normalize stages; each adapter keeps its `parse_line` public /// so the stages stay independently testable and benchable. diff --git a/crates/adapter-support/src/lib.rs b/crates/adapter-support/src/lib.rs index 246af574..710764cf 100644 --- a/crates/adapter-support/src/lib.rs +++ b/crates/adapter-support/src/lib.rs @@ -3,6 +3,8 @@ //! Ground-truth document construction is intentionally implemented by the //! Python tooling under `tools/groundtruth`; this crate contains only the //! pieces that production Rust parsers need. +//! +//! @atlas layer adapter-kit · domain normalize pub mod adapter; mod glob; diff --git a/crates/adapters/antigravity/src/lib.rs b/crates/adapters/antigravity/src/lib.rs index df1e589c..2294b393 100644 --- a/crates/adapters/antigravity/src/lib.rs +++ b/crates/adapters/antigravity/src/lib.rs @@ -9,6 +9,8 @@ //! siblings remain layout inventory. `conversations/.db` owns usage: the //! transcript carries no token counts, while the store's protobuf step //! metadata records every model request (see [`usage`]). +//! +//! @atlas layer agents · domain normalize pub mod adapter; pub mod normalize; diff --git a/crates/adapters/claude-code/src/lib.rs b/crates/adapters/claude-code/src/lib.rs index 15bd68bc..5d934b3c 100644 --- a/crates/adapters/claude-code/src/lib.rs +++ b/crates/adapters/claude-code/src/lib.rs @@ -12,6 +12,8 @@ //! * The workspace carries no date crate and `rosetta-core` exposes no //! timestamp parser, so RFC 3339 → `Ms` lives in [`time`]. Every agent will //! need it; it belongs in `core`. +//! +//! @atlas layer agents · domain normalize pub mod adapter; mod cost_state; diff --git a/crates/adapters/codex/src/lib.rs b/crates/adapters/codex/src/lib.rs index 584d23e5..15094252 100644 --- a/crates/adapters/codex/src/lib.rs +++ b/crates/adapters/codex/src/lib.rs @@ -12,6 +12,8 @@ //! * The workspace carries no date crate and `rosetta-core` exposes no //! timestamp parser, so RFC 3339 → `Ms` lives in [`time`]. Every agent will //! need it; it belongs in `core`. +//! +//! @atlas layer agents · domain normalize pub mod adapter; mod exec_program; diff --git a/crates/adapters/copilot/src/lib.rs b/crates/adapters/copilot/src/lib.rs index 4df43aa1..88cdd286 100644 --- a/crates/adapters/copilot/src/lib.rs +++ b/crates/adapters/copilot/src/lib.rs @@ -1,6 +1,8 @@ //! rosetta-adapter-copilot — the Layer-2 PARSER for GitHub Copilot CLI, //! designed against the common interface with the Layer-1 canonical ground //! truth (`groundtruth/copilot/groundtruth.json`) as its specification. +//! +//! @atlas layer agents · domain normalize pub mod adapter; pub mod normalize; diff --git a/crates/adapters/grok/src/lib.rs b/crates/adapters/grok/src/lib.rs index 8832ec76..2ec652f6 100644 --- a/crates/adapters/grok/src/lib.rs +++ b/crates/adapters/grok/src/lib.rs @@ -1,6 +1,8 @@ //! rosetta-adapter-grok — the Layer-2 PARSER for Grok Build, designed against //! the common interface with the Layer-1 canonical ground truth //! (`groundtruth/grok/groundtruth.json`) as its specification. +//! +//! @atlas layer agents · domain normalize pub mod adapter; mod images; diff --git a/crates/adapters/hermes/src/lib.rs b/crates/adapters/hermes/src/lib.rs index c5bf2eea..89d92806 100644 --- a/crates/adapters/hermes/src/lib.rs +++ b/crates/adapters/hermes/src/lib.rs @@ -2,6 +2,8 @@ //! against the common interface with the Layer-1 canonical ground truth //! (`groundtruth/hermes/groundtruth.json`) as its specification. //! Replay tests prove it against the L1 fixture corpus. +//! +//! @atlas layer agents · domain normalize pub mod adapter; pub mod normalize; diff --git a/crates/adapters/kimi-code/src/lib.rs b/crates/adapters/kimi-code/src/lib.rs index 28824c57..5979a961 100644 --- a/crates/adapters/kimi-code/src/lib.rs +++ b/crates/adapters/kimi-code/src/lib.rs @@ -1,6 +1,8 @@ //! rosetta-adapter-kimi-code — the Layer-2 PARSER for Kimi Code CLI, designed //! against the common interface with the Layer-1 canonical ground truth //! (`groundtruth/kimi-code/groundtruth.json`) as its specification. +//! +//! @atlas layer agents · domain normalize pub mod adapter; mod content; diff --git a/crates/adapters/opencode/src/lib.rs b/crates/adapters/opencode/src/lib.rs index 4981b99a..37f2eac3 100644 --- a/crates/adapters/opencode/src/lib.rs +++ b/crates/adapters/opencode/src/lib.rs @@ -2,6 +2,8 @@ //! against the common interface with the Layer-1 canonical ground truth //! (`groundtruth/opencode/groundtruth.json`) as its specification. //! Replay tests prove it against the L1 fixture corpus. +//! +//! @atlas layer agents · domain normalize pub mod adapter; pub mod normalize; diff --git a/crates/adapters/pi/src/lib.rs b/crates/adapters/pi/src/lib.rs index 1fa34732..c68011a1 100644 --- a/crates/adapters/pi/src/lib.rs +++ b/crates/adapters/pi/src/lib.rs @@ -1,6 +1,8 @@ //! rosetta-adapter-pi — the Layer-2 PARSER for Pi, designed against the //! common interface with the Layer-1 canonical ground truth //! (`groundtruth/pi/groundtruth.json`) as its specification. +//! +//! @atlas layer agents · domain normalize pub mod adapter; pub mod normalize; diff --git a/crates/bench-corpus/src/lib.rs b/crates/bench-corpus/src/lib.rs index 541740b8..405183be 100644 --- a/crates/bench-corpus/src/lib.rs +++ b/crates/bench-corpus/src/lib.rs @@ -20,6 +20,8 @@ //! let report = generate(&spec, Path::new("/tmp/rosetta-bench/cc")).unwrap(); //! println!("{}", report.table()); //! ``` +//! +//! @atlas layer tooling · domain perf pub mod claude_code; pub mod clock; diff --git a/crates/capture/src/ledger.rs b/crates/capture/src/ledger.rs index ed713f74..1a1e97d2 100644 --- a/crates/capture/src/ledger.rs +++ b/crates/capture/src/ledger.rs @@ -292,6 +292,7 @@ impl Ledger { Ok(()) } + // @atlas step live.ledger /// Owned variant for cold pipelines that can move framed payloads out of /// their tail batch. Small records share a bounded frame so checksumming /// and compression writes are amortized. Large records retain the direct diff --git a/crates/capture/src/lib.rs b/crates/capture/src/lib.rs index 881e6823..2fcb9f99 100644 --- a/crates/capture/src/lib.rs +++ b/crates/capture/src/lib.rs @@ -3,6 +3,8 @@ //! Agent-agnostic source IO lives here: append-delimited [`tailer`] reads, //! data-root [`discovery`], bounded meta reads, consistent SQLite snapshots, //! filesystem notification hints, and the append-only raw ledger. +//! +//! @atlas layer capture-io · domain capture pub mod discovery; pub mod document; diff --git a/crates/capture/src/tailer.rs b/crates/capture/src/tailer.rs index 117343bd..f4e7eb00 100644 --- a/crates/capture/src/tailer.rs +++ b/crates/capture/src/tailer.rs @@ -378,6 +378,7 @@ impl Tailer { Ok(Self { config }) } + // @atlas step live.read pub fn read( &self, path: &Path, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 586853de..84963823 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -2,6 +2,12 @@ //! the [`IngestOp`] stream every adapter normalizes into, deterministic ids, //! and the pure fold helpers. Zero IO dependencies by design; every function //! here benches and tests in isolation (docs/ingest-engine.html). +//! +//! @atlas layer model · domain normalize +//! @atlas rule core-is-a-leaf "core depends on nothing internal" { +//! only this -> nothing +//! see "docs/ARCHITECTURE.md#8-workspace-layout" +//! } pub mod assets; pub mod fold; diff --git a/crates/core/src/ops.rs b/crates/core/src/ops.rs index a577f7ca..f9757b16 100644 --- a/crates/core/src/ops.rs +++ b/crates/core/src/ops.rs @@ -15,6 +15,10 @@ use crate::timeline::{ ToolUpdatePatch, TruncationScope, WorkflowJournalObservation, WorkflowRun, }; +// @atlas contract ingest-op "IngestOp: where agent weirdness ends" { +// produced-by $agents, crate:rosetta-pipeline each-variant +// used-by crate:rosetta-store +// } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "op", rename_all = "snake_case")] pub enum IngestOp { diff --git a/crates/media/src/lib.rs b/crates/media/src/lib.rs index 382b004d..a8eaf90d 100644 --- a/crates/media/src/lib.rs +++ b/crates/media/src/lib.rs @@ -1,5 +1,7 @@ //! One bounded, vendor-neutral WebP pipeline. Original bytes remain ledger-only. //! No filesystem, network, clock or vendor knowledge enters image preparation. +//! +//! @atlas layer kernels · domain media mod animation; mod pixels; diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 0bbcdd68..1c70b385 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -10,6 +10,12 @@ //! The pipeline owns id derivation: adapters report NATIVE facts, and every //! rosetta id (runtime, project, session, timeline prefix inputs) is minted //! here from `rosetta_core::ids`. +//! +//! @atlas layer orchestration · domain ingest +//! @atlas rule pipeline-deps "pipeline → {core, capture, store, adapters}" { +//! only this -> crate:rosetta-{core, capture, store}, crate:rosetta-adapter-* +//! see "docs/ARCHITECTURE.md#8-workspace-layout" +//! } pub mod capture; pub mod checkpoint; diff --git a/crates/pipeline/src/live.rs b/crates/pipeline/src/live.rs index 7e074cc9..a23b01dd 100644 --- a/crates/pipeline/src/live.rs +++ b/crates/pipeline/src/live.rs @@ -334,6 +334,7 @@ impl<'a> LiveRunner<'a> { .map(|_| ()) } + // @atlas step live.map /// Drain ready hints and run due recovery work without waiting on this /// runtime. A shared writer waits once on `rosetta_capture::SourceWake` /// between passes, and publishes committed effects after each runner. diff --git a/crates/pricing/src/lib.rs b/crates/pricing/src/lib.rs index 47d5e86b..ba6e9845 100644 --- a/crates/pricing/src/lib.rs +++ b/crates/pricing/src/lib.rs @@ -6,6 +6,8 @@ //! reads no clock, and allocates only to remember a new model id per thread. //! Adapters never call it: the store prices usage records inside the hourly //! rollup, and the server prices one response at read time. +//! +//! @atlas layer kernels · domain usage mod catalog; // Generator output is committed byte-exact (`tools/pricing generate --check`). diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 08214f31..232171f6 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -13,6 +13,12 @@ //! Pagination follows the exact-watermark rule: a cursor binds the session //! watermark at first page; on mismatch the server answers //! [`ErrorCode::CursorExpired`] — match or expired, never a silent rebase. +//! +//! @atlas layer wire-types · domain api +//! @atlas rule thin-protocol "protocol depends on almost nothing" { +//! only this -> crate:rosetta-core +//! see "docs/ARCHITECTURE.md#8-workspace-layout" +//! } use serde::{Deserialize, Serialize}; diff --git a/crates/rosettad/src/engine.rs b/crates/rosettad/src/engine.rs index 8401311b..c9aad93a 100644 --- a/crates/rosettad/src/engine.rs +++ b/crates/rosettad/src/engine.rs @@ -127,6 +127,7 @@ pub(crate) fn now_ms() -> i64 { .unwrap_or(0) } +// @atlas step live.absorb /// Drain both effects seams into the hub, strictly after commit. Row /// effects first — session mutations into the rings, their touched rows /// into the mirror — then the metadata identity keys, the writer thread's diff --git a/crates/rosettad/src/main.rs b/crates/rosettad/src/main.rs index f5f95f9a..218c0251 100644 --- a/crates/rosettad/src/main.rs +++ b/crates/rosettad/src/main.rs @@ -17,6 +17,12 @@ //! `--enroll FILE` (2026-09-12) is a supervisor's enrollment file: consent for //! discovered kinds given at start, plus extra managed data roots of a kind //! that are consented by construction — see `enroll.rs`. +//! +//! @atlas layer root · domain ingest +//! @atlas rule rosettad-is-root "Nothing depends on rosettad" { +//! deny * -> this on code, manifest +//! see "docs/ARCHITECTURE.md#8-workspace-layout" +//! } use std::collections::HashMap; use std::path::PathBuf; diff --git a/crates/rosettad/src/serve.rs b/crates/rosettad/src/serve.rs index 8225746b..12faa3e0 100644 --- a/crates/rosettad/src/serve.rs +++ b/crates/rosettad/src/serve.rs @@ -1664,6 +1664,7 @@ fn run_warm_live_serve( Ok((store, absorbed)) } +// @atlas step live.tick #[allow(clippy::too_many_arguments)] fn drive_live_runners( flags: &Flags, diff --git a/crates/search-bench/src/lib.rs b/crates/search-bench/src/lib.rs index 5b11a48c..5fe52658 100644 --- a/crates/search-bench/src/lib.rs +++ b/crates/search-bench/src/lib.rs @@ -6,6 +6,8 @@ //! every run verifies them before and after, opens SQLite read-only, and emits //! only timings, counts, labels, and digests. Query text, snippets, and row ids //! never enter the report. +//! +//! @atlas layer tooling · domain perf · touches search mod compare; mod fingerprint; diff --git a/crates/search/src/lib.rs b/crates/search/src/lib.rs index 05561d9b..aee8f039 100644 --- a/crates/search/src/lib.rs +++ b/crates/search/src/lib.rs @@ -4,6 +4,8 @@ //! query/readiness contract and the synchronous FTS5 reference backend so the //! server does not depend on an engine-specific query path. A disposable //! sidecar can implement [`SearchBackend`] without changing the L3 protocol. +//! +//! @atlas layer query · domain search use std::fmt; use std::path::{Path, PathBuf}; @@ -382,6 +384,10 @@ impl fmt::Display for SearchError { impl std::error::Error for SearchError {} +// @atlas contract search-backend "Backend-neutral search seam" { +// implemented-by crate:rosetta-search +// used-by crate:rosetta-server, crate:rosettad +// } /// Synchronous interface because search engines and SQLite are blocking. The /// async server runs calls on its blocking pool. pub trait SearchBackend: Send + Sync { diff --git a/crates/search/src/sidecar.rs b/crates/search/src/sidecar.rs index 631ab682..a5cab0d0 100644 --- a/crates/search/src/sidecar.rs +++ b/crates/search/src/sidecar.rs @@ -481,6 +481,7 @@ impl SearchBackend for Fts5SidecarSearch { state } + // @atlas step search-query.sidecar fn search(&self, query: SearchQuery) -> Result { query.validate()?; let before = canonical_state(&self.canonical)?; diff --git a/crates/server/src/http.rs b/crates/server/src/http.rs index da6a8be3..3fcf63a5 100644 --- a/crates/server/src/http.rs +++ b/crates/server/src/http.rs @@ -1122,6 +1122,7 @@ async fn command( Ok(Json(CommandResponse { command })) } +// @atlas step search-query.handler async fn search( State(state): State, Query(request): Query, diff --git a/crates/server/src/hub.rs b/crates/server/src/hub.rs index a401527b..33ae9ce6 100644 --- a/crates/server/src/hub.rs +++ b/crates/server/src/hub.rs @@ -420,6 +420,7 @@ impl Hub { state.mirror.runtimes().cloned().collect() } + // @atlas step live.hub /// Absorb one drained [`Effects`] batch, strictly after its transactions /// committed: refresh the mirror (row diffs → precise meta events), push /// session-scoped mutations into rings, fan out, evict idle rings. diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index b68fe8cd..adfa406b 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -5,6 +5,17 @@ //! [`grant`] (the bearer, per-session grants and the Origin rule — //! docs/AUTH.md). Async lives only in this layer; the ingest engine below //! stays synchronous. +//! +//! @atlas layer edge · domain api +//! @atlas rule server-deps "server → {core, protocol, store}" { +//! only this -> crate:rosetta-{core, protocol, store} +//! see "docs/ARCHITECTURE.md#8-workspace-layout" +//! } +//! @atlas rule search-seam "server searches only through the backend-neutral seam" { +//! deny this -> sym:rosetta_search::{Fts5SidecarSearch, Fts5Search} +//! except mod:rosetta_server::http -> sym:rosetta_search::Fts5SidecarSearch "AppState::new builds the FTS5 default for tests; the daemon injects its backend via with_search" +//! see "docs/ARCHITECTURE.md#6-storage" +//! } pub mod auth; pub mod board; diff --git a/crates/server/src/ws.rs b/crates/server/src/ws.rs index 51d4f506..19fc0a30 100644 --- a/crates/server/src/ws.rs +++ b/crates/server/src/ws.rs @@ -320,6 +320,7 @@ async fn timeline_meta_snapshot( .await } +// @atlas step live.frame /// One session subscription: hello, snapshot-or-replay, then live items /// until the client disconnects. pub(crate) async fn run_session_stream( diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 325930c9..f096e15e 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -2,6 +2,8 @@ //! transactional write path, and the typed read surface. Foreign schema versions //! fail closed on ordinary open; `rosettad rebuild` explicitly reconstructs and //! atomically replaces the projection from the durable raw ledger. +//! +//! @atlas layer storage · domain projection mod activity; pub mod apply; @@ -449,6 +451,7 @@ impl Store { }) } + // @atlas step live.apply /// Transfer an owned batch into a cold planner without copying its /// normalized payload. Other store modes use the ordinary apply path. pub fn apply_owned(&mut self, batch: Batch) -> Result {