From a927e5723f839060710c6e56a0e41c2e18e2eeff Mon Sep 17 00:00:00 2001 From: James Yong Date: Fri, 16 Oct 2026 17:17:25 -0700 Subject: [PATCH 1/6] docs(atlas): truffle states its own design intent, graded by Project Atlas What the code cannot say about itself now lives in truffle and is graded against the code: the RFC 012 layer stack (truffle-core's modules as tiers, applications at Layer 7, the Go sidecar at Layer 1, Truffle Swift outside), domains, runtime links, contracts, four hot-path flows, dependency rules and the RFC decisions with their real status. Every rule and decision cites CLAUDE.md, docs/, a README or a code comment with a checked see. - atlas/*.atlas holds the structure, placement, rules and decisions. - @atlas comment lines only, no code change: contracts at their trait or enum, flow steps above the function that does each step, and placement plus single-crate rules at the roots of the unpublished crates. The crates.io crates carry no //! atlas text, so docs.rs shows none. - CLAUDE.md "Atlas" and the atlas skill: placement duties, renames, never weakening a rule, the check command, docs:/chore: commit types. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/atlas/SKILL.md | 467 ++++++++++++++++++ CLAUDE.md | 14 + atlas/components.atlas | 131 +++++ atlas/contracts.atlas | 38 ++ atlas/decisions.atlas | 169 +++++++ atlas/domains.atlas | 127 +++++ atlas/flows.atlas | 59 +++ atlas/layers.atlas | 70 +++ atlas/rules.atlas | 154 ++++++ atlas/system.atlas | 25 + benchmark/app/src-tauri/src/lib.rs | 6 + benchmark/engine/src/lib.rs | 10 + crates/truffle-core/src/envelope/codec.rs | 4 + .../src/file_transfer/receiver.rs | 3 + .../truffle-core/src/file_transfer/sender.rs | 2 + .../truffle-core/src/file_transfer/types.rs | 4 + crates/truffle-core/src/network/mod.rs | 5 + .../src/network/tailscale/bridge.rs | 1 + .../src/network/tailscale/provider.rs | 2 + .../src/network/tailscale/sidecar.rs | 1 + crates/truffle-core/src/node.rs | 5 + crates/truffle-core/src/request_reply.rs | 1 + crates/truffle-core/src/session/mod.rs | 4 + .../truffle-core/src/synced_store/backend.rs | 4 + crates/truffle-core/src/synced_store/mod.rs | 1 + crates/truffle-core/src/synced_store/sync.rs | 2 + crates/truffle-core/src/synced_store/types.rs | 5 + .../truffle-core/src/transport/websocket.rs | 3 + crates/truffle-napi/src/lib.rs | 7 + crates/truffle-tauri-plugin/src/lib.rs | 2 + 30 files changed, 1326 insertions(+) create mode 100644 .claude/skills/atlas/SKILL.md create mode 100644 atlas/components.atlas create mode 100644 atlas/contracts.atlas create mode 100644 atlas/decisions.atlas create mode 100644 atlas/domains.atlas create mode 100644 atlas/flows.atlas create mode 100644 atlas/layers.atlas create mode 100644 atlas/rules.atlas create mode 100644 atlas/system.atlas diff --git a/.claude/skills/atlas/SKILL.md b/.claude/skills/atlas/SKILL.md new file mode 100644 index 00000000..8384d207 --- /dev/null +++ b/.claude/skills/atlas/SKILL.md @@ -0,0 +1,467 @@ +--- +name: atlas +description: Maintain truffle's architecture atlas (atlas/*.atlas plus @atlas comments, graded by Project Atlas) — place a new crate, truffle-core module, npm package or directory in the RFC 012 layer stack and a domain, keep selectors right through renames, write rules/contracts/flow steps/decisions that cite truffle's docs and RFCs, run `atlas check`, and fix dangling or violated claims. +--- + +# Atlas workflow + +The atlas states what truffle'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`. Truffle'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 $subsystems` (truffle-core's file_transfer, + synced_store, request_reply and proxy). +- `atlas/layers.atlas` — the RFC 012 stack (position is rank): truffle-core's module tiers + (node-api, envelope, session, transport, network; `mod:truffle_core` makes node-api the default), + the L7 sub-tiers (examples, cli, bench, react, js-api, bindings), the Go sidecar, Truffle Swift, + distribution and tooling. It places the crates.io crates (truffle-core, truffle, + truffle-sidecar), truffle-cli, the npm packages and the directories. `bench` holds the RFC 027 + hub: truffle-bench and truffle-bench-app place themselves, and this file adds the app's frontend + `@vibecook/truffle-bench-app` and the generated `benchmark/schema/`. +- `atlas/domains.atlas` — the same crates, plus domain claims below crate level and `note`s for + the Go files that do a domain's work. Each truffle-bench workload `touches` the domain it + measures (`workloads::{tcp, udp, quic, video}` raw-transport, `path` mesh, `ws` messaging, + `http` serving, `egress`, `file_transfer`, `synced_store`); no workload calls `request_reply` + directly since `ws` replaced it. +- `atlas/components.atlas` — runtime units and how they talk (`via spawn|ipc|ws|port|http|embed`). +- `atlas/contracts.atlas` — the seams with several anchors: `transport-traits`, + `napi-projection`, `session-wire`, and `bench-app-types` (the 12 types of RFC 027 §7's fixture + list, each mirrored in `benchmark/app/src/api/types.ts`). +- `atlas/flows.atlas` — `discovery`, `send`, `file-push`, `store-update`: step ids, titles, + `via`/`direct`, `by`, and the two steps done in Go (`at file:packages/sidecar-slim/main.go`). +- `atlas/rules.atlas`, `atlas/decisions.atlas` — the rules (including the single-crate rules of + the crates.io crates and truffle-cli, with `crate:` scopes); one decision per significant RFC + with its real status. +- In code (`grep -rn '@atlas' crates benchmark`): + - the roots of the unpublished crates with a crate doc: truffle-napi (its placement and + `napi-pull-model`), truffle-tauri-plugin, truffle-bench (`benchmark/engine/src/lib.rs`: its + placement, `bench-never-sets-hostname`, `legacy-transport-without-tbn1`) and truffle-bench-app + (`benchmark/app/src-tauri/src/lib.rs`: its placement, `bench-app-drives-the-engine`); + - contracts at their single anchor: `NetworkProvider`, `EnvelopeCodec`, `StoreBackend`, + `FtMessage`, `SyncMessage`; + - flow steps at the function that does the step (`// @atlas step send.route`). +- `atlas/atlas.baseline.json` — written only by `atlas baseline` (none yet: see Running it). + +## 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 truffle root: + +```sh +ATLAS="${ATLAS:-$HOME/Projects/project100/protos/project-atlas}" +TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- check "$PWD" --intent "$PWD/atlas" # what is wrong +TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- check "$PWD" --intent "$PWD/atlas" --all # also asserted + verified +TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- explain "$PWD" --intent "$PWD/atlas" crates/truffle-core/src/egress.rs +TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- explain "$PWD" --intent "$PWD/atlas" 'mod:truffle_core::{session, transport}' +"$ATLAS/node_modules/.bin/tsx" "$ATLAS/packages/engine-intent/src/cli.ts" atlas crates benchmark packages examples # syntax only +``` + +- Atlas is private. If `$ATLAS/package.json` is missing, `npm --prefix` exits 254 with no output (`-s`): + skip the check, install nothing, and say so. +- Pass `--intent "$PWD/atlas"`: it pins the DSL root and the baseline + (`atlas/atlas.baseline.json`, beside `system.atlas`) whatever Atlas version you run. +- Rust facts come from `cargo doc` over the whole workspace, napi and tauri included, with the + target dir in Atlas's own cache, not `target/`. It runs truffle's build scripts: + - truffle-core's `build.rs` runs `go build` of the gitignored `crates/truffle-core/tests/test-sidecar` + when the Go sources are newer than it and `go` is on PATH (without `go` it only warns); + - truffle-sidecar's `build.rs` downloads the release sidecar unless `TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1` + is set. Without it a cold cache downloads over the network (Atlas's `CARGO_NET_OFFLINE` does not + stop a build script), an offline run panics, and on a Release Please branch (version bumped, no + pinned checksum yet) it panics with "SECURITY: no pinned checksum" (`docs/RELEASING.md`). + `cargo doc` never runs the sidecar, so every command above sets the variable, as CI's lint job does; + - truffle-tauri-plugin's `build.rs` runs `tauri_plugin::Builder`, which can rewrite the tracked + files under `permissions/`: check `git status` after a cold run. truffle-bench-app's runs + `tauri_build::build()` (its output, `benchmark/app/src-tauri/gen/`, is gitignored), and + truffle-bench's embeds `rustc -V`; + - on Linux, truffle-tauri-plugin and truffle-bench-app need Tauri's system libraries (`Cargo.lock` + has `webkit2gtk-sys` and `gtk-sys`; CI's `cargo doc` excludes both). Without them Atlas reports + `cargo-doc-failed` for them and grades them from the source scan (not tried on Linux). + + A `cargo-doc-failed` error means that crate's facts come from the source scan only, with no + rustdoc symbols: fix the build before trusting its `sym:`, contract or flow-step results. + `check` itself prints only engine errors such as `cargo-doc-failed`; warnings sit in an + atlas.json's `diagnostics` (`check --out `). Two `rustdoc-mismatch` warnings are expected + there: truffle-bench and truffle-bench-app name their bin target like their lib, so `cargo doc` + writes one JSON for both and Atlas models the bin targets (truffle-bench's `cli.rs` and + `legacy/`, the app's `main.rs`) from source. Their libraries keep rustdoc symbols; select bin + code with `path:`/`file:`, not `sym:`. + + A cold run took about 3 minutes; after comment-only edits the Rust step took 9-10 s. 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. It also parses `.go` and `.swift` + comments, which a real build ignores. It calls Atlas's own `tsx` directly: truffle has none, and + `npx` would fetch one from the registry. +- Do not use `--rust-source-only` on truffle: without rustdoc symbols, 69 claims dangle (contract + anchors, domain `touches`, flow steps and hops) and 10 are violated instead of 7. +- `baseline` (`TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- baseline "$PWD" --intent "$PWD/atlas"`) + writes `atlas/atlas.baseline.json` with every violation of that run, so whatever your change + added becomes `known` too. None exists yet. It runs only when the user decides to accept that + run's violations, never to turn the check green or to hide a violation your change caused. + +## Reading `check` + +An excerpt of today's run (rebased on 0.7.18, atlas changes uncommitted, no baseline yet; the +header's hash and dirty count change with every commit); `…` marks what is left out here. Claims +sort by severity, then by the file and line that declare them: today the decisions +(`atlas/decisions.atlas`), then the rules (`atlas/rules.atlas`), then the in-code contract: + +``` +atlas check · Truffle @ 65342ba (+12 dirty) · 520 claims + 473 verified · 40 asserted · 7 violated (7 new, 0 known) · 0 dangling · 0 ambiguous · 0 stale + coverage: layers 100.0% · domains 100.0% of 88,816 prod lines · rules 23 pass, 4 fail, 0 unchecked + +VIOLATED · new (7) + decision:layered-architecture decision layered-architecture "Layers 3-6 behind one Node API; applications are Layer 7" code RFC-0… + at atlas/decisions.atlas:36 · decision · basis none + rule:layers-point-down violated + rule:network-knows-no-upper-layer violated + rule:transport-knows-no-session violated + rule:apps-use-node-api violated + → Accepted, but rule:layers-point-down, rule:network-knows-no-upper-layer, … are failing: the code has drifted from this decision. + decision:synced-store decision synced-store "SyncedStore and a request/reply helper in core" code RFC-016 status accepted + at atlas/decisions.atlas:82 · decision · basis none + contract:sync-wire violated + → Accepted, but contract:sync-wire is failing: the code has drifted from this decision. + rule:layers-point-down layered + at atlas/rules.atlas:10 · rule · basis edge + mod:rs/truffle_core::network → mod:rs/truffle_core::egress + crates/truffle-core/src/network/mod.rs:260 ) -> impl std::future::Future layer:network, layer:transport, layer:session, layer:envelope + at atlas/rules.atlas:65 · rule · basis edge + pkg:cargo/truffle-bench → pkg:cargo/truffle-core + benchmark/engine/src/control.rs:34 use truffle_core::session::{SessionError, DIAL_CANCELLED_SHUTTING_DOWN}; + benchmark/engine/src/control.rs:35 use truffle_core::transport::quic::QuicListener; + … + → 66 forbidden dependencies from layer:apps into layer:network, layer:transport, layer:session, layer:envelope (code); … + … contract:sync-wire + +ASSERTED (40): not listed; --all lists them + +VERIFIED (473): not listed; --all lists them + +7 new violations (7 error, 0 warn/info): exit 1 + fix the code, add `except "reason"`, or record today's state with `atlas baseline` +``` + +- The last line is Atlas's generic advice. Never take its `except` or `atlas baseline` yourself: + an `except` changes the design and the baseline is the user's decision (Running it, Fixing). +- Until the user runs `atlas baseline`, this is what a clean tree prints: exit 1 with exactly these + 7 claims under `VIOLATED · new`. If the user baselines this state, `atlas baseline` writes 13 + keys (3 claim ids and 10 rule pairs); the same 7 claims then print under + `VIOLATED · known (in the baseline) (7)`, the summary reads `7 violated (0 new, 7 known)`, the run + ends with `ok: no new violations`, and the check exits 0. +- Sections in order: VIOLATED · new, VIOLATED · known (in the baseline), 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`. A claim listed + under VIOLATED, DANGLING, AMBIGUOUS or STALE then prints each glob's match count (`* → 41`, and + `→ 0 (matches nothing)` for any selector that matches nothing), the evidence + (`file:line source`) and `→` the remedy. `--all` lists verified and asserted claims with those + first two lines only, no counts, so `explain ''` (each term prints `→ n`) to see what a + verified glob matches. A glob that matches nothing dangles. +- Exit 1 only for a violated claim of severity `error` that is not known; with no baseline, every + violated claim is new. DANGLING, STALE, AMBIGUOUS and `warn` rules (none today) do not fail the + check, but you still leave 0 dangling and 0 ambiguous. One exception: a dangling rule, contract + or flow named in a decision's `confirmed-by` makes that decision violated (exit 1), so fix the + selector. `layered-architecture` and `synced-store` are violated already, so a dangling member of + theirs (any of the 10 rules in layered-architecture's `confirmed-by`, or `sync-wire` and + `request-reply-over-node`) shows only under DANGLING and in the decision's evidence + (`rule:bridge-is-private dangling`), not in the exit code: today the check exits 1 anyway, and + after a baseline the decision's only key is its claim id, so it stays known and exits 0 (Atlas + `grade/decisions.ts`, `main.keys = [main.id]`). +- **Known or new, once a baseline exists** (Atlas `packages/aggregator/src/grade/rules.ts`, + `grade/claims.ts` `evidenceKey`, `packages/aggregator/src/baseline.ts`). A violated claim is + known only when every one of its keys is in the baseline. Its keys: + - a dependency rule (`layered`, `deny`, `only`): one per evidence item, `rule:||`. + That is per package pair for an edge between packages + (`rule:apps-use-node-api|pkg:cargo/truffle-napi|pkg:cargo/truffle-core`); per module pair inside + one, named by the leaf modules at the two ends of the import + (`rule:layers-point-down|mod:rs/truffle_core::network::tailscale::provider|mod:rs/truffle_core::egress`); + and per importing module (Rust) or file (TS) for an `ext:` target + (`deny layer:network -> ext:tokio-tungstenite, ext:quinn`); + - `require` (dependency): one per scope root, `rule:||no code edge into ` + (`transport-knows-no-session`, `session-knows-no-payload`, `request-reply-over-node`, + `bench-app-drives-the-engine`); + - `forbid text`: one per file; `require text`: one per missing literal; `exhaustive`: one per + unplaced file; + - a decision: its claim id, a hash of its header (id, title, `code`, `status`; not its date, + `because` or body); + - a contract, flow claim or relation: its claim id, a hash of its statement line (for a contract + the check line, such as `produced-by crate:truffle-core each-variant`, not its quoted title), + salted with the host symbol when the contract is declared in code (`SyncMessage` for + `sync-wire`). + + So a new pair is new and exits 1: an upward import between two truffle-core modules that had + none, or a lower-layer import from truffle-bench-app or any other package the baseline lacks. + Another import between a listed pair stays known: any further truffle-cli, truffle-napi, + truffle-tauri-plugin or truffle-bench import of a truffle-core lower layer, another egress import + in `network/mod.rs` or `tailscale/provider.rs` (one in any other `network` module is a new pair), + and a second unproduced `SyncMessage` variant. The check still exits 0, so compare each known + claim's evidence and `→` count with the list below. + + Keys name the code. A known finding becomes new with the same count, and the check exits 1, when + you move code at either end of a known module pair (an egress import leaving `network/mod.rs` or + `tailscale/provider.rs`, the hello import leaving `transport/websocket.rs`, the egress types + leaving `egress`), rename a package in a known package pair, or move or rename `SyncMessage`. + That is not a finding of yours: say so to the user; re-running `atlas baseline` is their call. + Rewording a decision's header or a contract's check line changes its id and makes it new too; a + rule's keys use its id, not its text. +- Both coverages are 100%. A new crate, npm package or directory left unplaced drops them (and + fails `exhaustive`). A new truffle-core module never does: it inherits node-api and `mesh`, so + `explain` it and place it on purpose. +- Go, Swift and scripts are never production code to Atlas: they count in no coverage, and only + text rules, witnesses and `see` grade them. +- 0 stale. A claim goes stale (it never fails the check) once 10 commits touched its code since the + last commit of the file holding the statement (the `.atlas` file, or the source file holding the + `@atlas` line); an uncommitted file has none, so commit atlas changes with the code they describe. +- The 7 findings kept for the user, in output order, with their counts (each claim's `→` line + gives its count). Your change is clean only when `VIOLATED · new` lists exactly these 7 claims + with these counts (after a baseline: nothing new, and these 7 under known with these counts); any + other violated claim, or a higher count, is yours. A further import between modules or packages + already listed only lengthens a claim's evidence, so the summary line and the exit code stay the + same: compare the counts. Do not edit a rule, a decision or a module's tier for them, and do not + change their code either (no deleting `Clear`, no moving the hello gate or the egress types) + unless the user asks: + - `decision:layered-architecture`: its 4 failing rules below; + - `decision:synced-store`: `contract:sync-wire`; + - `rule:layers-point-down`: 3 module edges, the two egress ones below and the hello one; + - `rule:network-knows-no-upper-layer`: 2 edges. The `NetworkProvider` egress verbs + (`network/mod.rs:260`, `:270`, `:271`, `:291`, `:301`) and `tailscale/provider.rs:23` name + `egress` types, while `egress.rs` documents them as the Node API of RFC 026 and `egress` sits in + node-api; + - `rule:transport-knows-no-session`: 1 edge. `transport::websocket` imports `session::hello` + (`websocket.rs:57`): the RFC 017 §8 hello runs inside the WebSocket handshake. RFC 012 §3.4 + calls that handshake transport-level while `hello.rs` calls itself Layer 5, and RFC 025 §3.4 + ("Layer 4/5 — the hello gate") added the login gate there; + - `rule:apps-use-node-api`: 66 edges (Atlas's count) in 60 module pairs from 4 packages: + truffle-bench 24, truffle-napi 16, truffle-tauri-plugin 7, truffle-cli 13. They name + `TailscaleProvider`, `PeerEvent`/`PeerState`/`PeerIdentity`, `NetworkStatusEvent`, + `transport::quic`, `RawListener`/`RawIncoming`, `DatagramSocket`, `SessionError`, + `NetworkError` and `network` value types; Node's own signatures return them + (`on_network_status` since 0.7.18, and `lib.rs` does not re-export `NetworkStatusEvent`). + RFC 015 §3 designs the bindings around `Node`, so naming the Layer 3 + provider follows RFC 015 and breaks RFC 012 §3.1. truffle-bench also names + `envelope::{Envelope, EnvelopeCodec, JsonCodec}` (the `ws` workload sizes encoded envelopes) and + `session::DIAL_CANCELLED_SHUTTING_DOWN` (`control.rs`). RFC 027 prescribes these lower-layer + types for the engine (§5.4 `Node` and `PeerEvent::AuthRequired`; §6 + `SessionError::ReconnectBackoff` and `QuicListener::close()`; §8.2 `RawIncoming.stream`, + `DatagramSocket::Network` and `JsonCodec`; §14 #21 the dial cancelled at shutdown), so + truffle-bench's imports follow RFC 027 and break RFC 012 §3.1. Two of the edges exist only + through truffle's own re-exports (`truffle_core::BroadcastReport`, + `truffle_core::proxy::ProxyRoute`): Atlas resolves a re-export to the module that defines the + type; + - `contract:sync-wire`: 1 variant, `SyncMessage::Clear`, is never produced. RFC 016's wire table + broadcasts it when a peer goes offline; RFC 025 §3.5 since drops any `Clear` naming another + device. The receive path still handles it (`synced_store/sync.rs`, `handle_incoming_message`). + +## 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. Module paths also sit in brace + forms (`mod:truffle_core::{node, identity, egress}`, `mod:truffle_napi::{raw_socket, udp_socket, quic}`, + `mod:truffle_bench::workloads::{tcp, udp, quic}`) that a grep for the full path 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 #…?`). An RFC + heading rename dangles every `see` pointing at it: fix them together. A `#L` reference + (`crates/truffle/src/lib.rs#L38-L41`) dangles only when the file gets shorter; re-check that + the line still says it. +- **"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 edge and its sites. Fix the code (route + through Node), unless the edge is one of the 7 findings above. The remedy line also offers an `except …` to paste or a wider `only` list: an + agent never takes either on its own. It changes the design, so the rule and the RFC it cites + change together, and that is the user's call; propose it instead. A sanctioned exception reads + `except "real reason"`; an `except` that excuses nothing goes stale: delete it. +- **Contract violated.** `implemented-by` names the package missing an implementation; + `produced-by … each-variant` names the enum variant nobody constructs (produce it, or delete + the dead variant; `SyncMessage::Clear` is one of the 7 findings above, so neither without the user); `mirror` names the missing `Napi…` export in `crates/truffle-napi/index.d.ts` + (rebuild the addon or rename the type) or, for `bench-app-types`, the type missing from the + hand-written `benchmark/app/src/api/types.ts` (RFC 027 §7: add it there). +- **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 all 27 steps of the four flows are verified. Hops `via` a runtime + mechanism stay asserted (10 today). +- **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. + Stale never fails the check. Do not touch an `.atlas` file only to reset staleness: re-read the + code, and commit a statement change only when the statement needed one. +- **Planned element landed.** `expects … [warn]` resolving means the thing was built: drop + `status planned`/`proposed` and `expects`, then bind it. + +## Authoring + +1. **Crate roots.** Only an inner doc comment (`//!`) in `lib.rs`/`main.rs` binds the whole + package; there `this` is the package, manifest edges included. In a lib + bin package the + lib's `lib.rs` binds the bin's modules too (`explain benchmark/engine/src/legacy/transport.rs`: + `layer:bench (inherited from pkg:cargo/truffle-bench)`). + - **Published crates** (no `publish = false` in `Cargo.toml`: truffle-core, truffle, + truffle-sidecar) get no `//!` atlas lines, because docs.rs would render them. Place them in + `atlas/layers.atlas` and `atlas/domains.atlas` (`contains crate:x`) and write their rules in + `atlas/rules.atlas` with `crate:` scopes. A `//` header comment is not a substitute: it binds + the crate's root module, not the package. Tried on truffle: `explain` showed + `root mod:rs/truffle · depth 2` with `pkg:cargo/truffle` unclaimed, and + `only this -> crate:truffle-core on manifest` passed although `Cargo.toml` names truffle-sidecar. + - **Unpublished crates with a crate doc** end it with one blank `//!` line, then their lines: + + ```rust + //! …the crate's existing docs, unchanged… + //! + //! @atlas layer bindings · domain bindings + //! @atlas rule napi-pull-model "Raw-transport handles pass bytes only as the answer to a JS read, never a ThreadsafeFunction" { + //! forbid text "ThreadsafeFunction" in mod:truffle_napi::{raw_socket, udp_socket, quic} + //! see "docs/rfcs/021-raw-transport-js-api.md#12-decided-questions" + //! see "docs/rfcs/021-raw-transport-js-api.md#5-design-overview" + //! } + ``` + + These lines still render into rustdoc, and CI runs `clippy -D warnings`, + `RUSTDOCFLAGS=-D warnings cargo doc` (every crate but truffle-napi, truffle-tauri-plugin and + truffle-bench-app, so truffle-bench's lines must be rustdoc-clean) 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 (truffle-bench's + end in a doctest), insert after the closing fence. + - **A crate without a crate doc** (truffle-cli's `main.rs`) is placed in `atlas/`. Never + invent a crate doc to hold the lines. +2. **Everything else in Rust uses `//`**, never `///`: directly above the item, above its `///` + docs and `#[…]` attributes, at the item's indentation. `///` would render into rustdoc, and in + `crates/truffle-napi/src` NAPI-RS copies item docs into the committed `index.d.ts`. + + ```rust + // @atlas contract sync-wire "SyncedStore wire messages on namespace \"ss:{id}\"" { + // produced-by crate:truffle-core each-variant + // see "docs/rfcs/016-synced-store-and-request-reply.md#wire-protocol" + // see "docs/rfcs/025-login-scoped-nodes.md#35-sender-bound-slices-tf-2" + // } + /// Wire protocol messages sent on namespace `"ss:{store_id}"`. + ``` +3. **TypeScript uses `//`** (a `*/` in a glob would close a block comment). + `crates/truffle-tauri-plugin/guest-js/` has no `package.json`, so Atlas sees inventory files + only: describe it from `atlas/` with `path:`/`file:` selectors. +4. **Go and Swift are described from `atlas/`, never in-code**: `atlas build` reads no `@atlas` + comment in a `.go` or `.swift` file. Use `path:` for placement, `file:` for text rules and + relation anchors, and `note file:… "…"` to name a Go file in a domain (a `file:` term in + `contains` binds the file's whole directory). +5. **Header pitfall.** A `//` annotation before a file's first code token binds the file's module, + not the declaration below it. Symbol annotations go after the `use` lines. 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. +6. **Standalone files** hold what spans crates: the layer structure and truffle-core's tiers, + domain claims below crate level, components and relations, cross-cutting rules, decisions. + Declaring an element in two places is a parse error. +7. **Cite truffle.** Every rule and decision carries `see "#"`, or `#L` on a code + file or on a Markdown header line under no heading of its own (an RFC's `**Supersedes**` line: + `docs/rfcs/012-layered-architecture-redesign.md#L6`). Cite CLAUDE.md, README.md, + `docs/API-STABILITY.md`, `docs/RELEASING.md`, `docs/TESTING.md`, `docs/tailscale-performance.md`, + `docs/guide/`, `apple/README.md`, `crates/truffle-napi/README.md`, `benchmark/README.md` and RFCs + 003, 010 and 012–027 for decisions in force (RFC 027 superseded RFC 019 §10.2 and RFC 020 §3 and + §6.2), and a comment in the code that states the rule by line + (`crates/truffle/src/lib.rs#L38-L41`, `apple/Package.swift#L4-L6`); cite other pre-012 RFCs only + to record supersession. Never cite `docs/architecture/networking-flows.md` (the archived v1 + architecture). 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 + `### 3.6 Layers 1-2: Bridge + Sidecar (internal to TailscaleProvider)` → + `36-layers-1-2-bridge--sidecar-internal-to-tailscaleprovider`. That is Atlas's rule; GitHub + keeps `_`, so a heading with `_` in it (RFC 027's ``## 5. Engine API (`truffle_bench`)``) gets a + slug Atlas accepts and a link GitHub does not: cite another heading. A non-Markdown file + (`docs/index.html`) takes no `#slug`. When an RFC header's status disagrees with what shipped, + the decision records the real status and names the header in `because`; never edit the RFC + to match (the user's call). +8. **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. + +## Committing + +- An atlas-only change is `docs:` or `chore:`, never `feat:` or `fix:`: Release Please cuts a + release for those (and for the other types its default changelog shows, such as `perf:`, or a + `!` breaking change). `docs:` and `chore:` are hidden types and cut none. +- `.githooks/pre-commit` (enabled by `git config core.hooksPath .githooks`) runs, on what is staged: + 1. NAPI drift: a staged `crates/truffle-napi/src/*.rs`, `Cargo.toml` or `build.rs` runs + `cd crates/truffle-napi && pnpm run build` (`napi build --platform --release`, a cargo build) + and fails while the regenerated `crates/truffle-napi/index.d.ts` is unstaged; without pnpm it + exits 1 at once (it does not skip); + 2. a YAML parse of staged `.github/workflows/*.yml` and `*.yaml` (python3 + PyYAML, skipped + without them); + 3. `rustfmt --edition 2021 --check` on staged `.rs` files; + 4. `pnpm exec prettier --check` on staged `packages/*/src/**/*.ts` and + `benchmark/app/src/**/*.{ts,tsx}` (not `benchmark/app/src/brand/`), skipped without pnpm; + 5. `cargo test -p truffle --doc` when `crates/truffle/src/lib.rs`, `crates/truffle/Cargo.toml`, + `crates/truffle-core/src/lib.rs` or `crates/truffle-core/src/node.rs` is staged; + 6. `pnpm run build` (`pnpm -r run build`, the NAPI addon's cargo build included) when + `packages/*/src/**/*.ts`, a `packages/*/package.json`, `crates/truffle-napi/index.d.ts`, a file + under `benchmark/app/src/` or `benchmark/schema/`, `benchmark/app/package.json`, + `benchmark/app/tsconfig*.json` or `benchmark/app/vite.config.ts` is staged (skipped without pnpm); + 7. `scripts/check-for-secrets.sh` on every commit that stages a file: added lines matching + `tskey-…` outside its allow-list. + + So staging `@atlas` lines in truffle-napi or `node.rs` runs cargo builds; lines in + `benchmark/engine/src/lib.rs` or `benchmark/app/src-tauri/src/lib.rs` get rustfmt and the secret + scan only, and `atlas/`, CLAUDE.md or the skill the secret scan only. + CLAUDE.md allows `--no-verify` rarely; when the hook cannot run because builds are off-limits, + ask the user before skipping it, or commit later. + +## Worked examples + +- **New crate** `crates/truffle-foo`: published (no `publish = false`)? Add `contains crate:truffle-foo` + to a layer in `atlas/layers.atlas` and a domain in `atlas/domains.atlas`. Unpublished with a + crate doc? End its root `//!` docs with `//! @atlas layer · domain `. A consumer + of the Node API sits in an `apps` sub-tier (as truffle-bench does), and `apps-use-node-api` then + holds it to the Node API; only code that uses no truffle crate (scripts, CI, docs, the Go + baseline bench) sits in `tooling`. A new language binding joins `bindings-wrap-core` in + `atlas/rules.atlas`; truffle-core must not depend on it (`core-depends-on-no-crate`). +- **New truffle-core module** `truffle_core::foo`: add it to a tier in `atlas/layers.atlas` and a + domain in `atlas/domains.atlas`. Forgotten, it falls to node-api through `contains mod:truffle_core` + and to `mesh` through `crate:truffle-core`, and nothing reports it: + + ``` + $ TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- explain "$PWD" --intent "$PWD/atlas" crates/truffle-core/src/lib.rs + mod:rs/truffle_core (module of pkg:cargo/truffle-core · truffle_core · slug truffle_core) + layer layer:node-api + … + ✓ layer:node-api contains mod:truffle_core + atlas/layers.atlas:34 · root mod:rs/truffle_core · depth 2 · literal + layer:core contains crate:truffle-core + atlas/layers.atlas:31 · root pkg:cargo/truffle-core · depth 1 · literal ← less specific (depth 1 < 2) + ``` + + Rank matters: `layers-point-down` (`layered`) forbids any edge going up the stack, so pick a + tier at or above every tier the module uses and at or below every tier that uses it; if no such + tier exists, that is a design question for the user. Then compare `layers-point-down`'s edge + count with today's 3. A new subsystem joins `$subsystems` in + `atlas/system.atlas` (node-api tier, `subsystems-send-typed`); give it its own namespace and add + that namespace to `namespaces-owned`. +- **New transport** `transport::foo`: it lands in the transport tier through + `mod:truffle_core::transport`. Add it to `transports-independent` and to the `raw-transport` + domain; keep `tokio-tungstenite` and `quinn` out of `network`. +- **New benchmark workload** `benchmark/engine/src/workloads/foo.rs`: it inherits `bench` and the + `bench` domain from truffle-bench's root; add `touches mod:truffle_bench::workloads::foo` to the + domain it measures (`atlas/domains.atlas`). Its truffle-core imports below the Node API raise + `apps-use-node-api`'s count. That claim is violated already (and, once a baseline exists, known + under the truffle-bench package pair), so the exit code does not show it: report the new count to + the user and do not call the change clean. A new type joins `bench-app-types` when RFC 027 §7's + fixture list names it. +- **New NAPI type** for a core type: `napi-projection` checks every core type in its `at` list + for an `index.d.ts` export named `Napi{name}`. Add the core type when RFC 015's set grows. +- **New npm package or example**: `pkg:@vibecook/example-*` picks up a new example in both + `atlas/layers.atlas` and `atlas/domains.atlas`; any other package needs a `contains pkg:…` line + in a layer and in a domain. +- **Splitting a big file** (node.rs, provider.rs, session/mod.rs): `// @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 + flows' steps are still verified. Once a baseline exists, moving `provider.rs`'s `use crate::egress` + into a child module also turns `layers-point-down` and `network-knows-no-upper-layer` new with + the same counts (exit 1): their keys name the module (Known or new). Tell the user; it is not + your finding. diff --git a/CLAUDE.md b/CLAUDE.md index d1446a15..aa67eb0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,8 @@ Key principles: | `truffle-bench` (`benchmark/engine`) | Benchmark engine (lib) + CLI (bin): workloads, peer control protocol, v3 reports, suites (RFC 027). `publish = false` | | `truffle-bench-app` (`benchmark/app/src-tauri`) | Tauri v2 shell of the "truffle bench" desktop app; frontend is `@vibecook/truffle-bench-app` (`benchmark/app`, React + Vite, private). `publish = false` | +Each crate's place in the RFC 012 stack and its domain are declared in `atlas/` or its crate root and graded by Atlas (see "Atlas" below). + ## Build & Test ```bash @@ -91,6 +93,18 @@ pnpm --filter @vibecook/truffle-bench-app app:dev # the - **Commit types**: release-please ignores a commit only if *every* file is under `benchmark/` (`exclude-paths`). Hub changes that touch anything else use `chore(bench):`, `ci:`, `build:` or `docs:` — never `feat:`/`fix:`. Library changes the hub needs ship separately as `feat(core):` - Nightly: `benchmarks.yml` (`suite --profile nightly --pair`); PR: `integration-tests.yml` runs `-p truffle-bench --tests` (`live_pair`) +## Atlas + +`atlas/*.atlas` plus `@atlas` comment lines declare what code cannot say about itself: the RFC 012 layer stack, domains, runtime links, contracts, four hot-path flows, dependency rules and RFC decisions. Project Atlas grades each statement against the code. It is not a third living document: every rule and decision cites this file, `docs/`, a README or a comment in the code with a checked `see "file#slug"` (`#L` for a line). Authoring guide: the `atlas` skill. + +- **Where**: truffle-napi, truffle-tauri-plugin, truffle-bench and truffle-bench-app end their root `//!` doc (the benchmark crates' `lib.rs`) with their placement; seams and flow steps carry `// @atlas` lines (`grep -rn '@atlas' crates benchmark`). The crates.io crates (truffle-core, truffle, truffle-sidecar) carry no `//!` atlas text, so docs.rs shows none: they, truffle-cli (no crate doc), truffle-core's module tiers, the npm packages (`@vibecook/truffle-bench-app` included) and the directories (`benchmark/schema/` included) are placed in `atlas/layers.atlas` and `atlas/domains.atlas`. +- **New code**: place a new crate, truffle-core module, npm package or top-level directory in a layer and a domain in the same change. A truffle-core module you forget lands in node-api and `mesh` and nothing reports it: `explain` it. +- **Renames**: update every selector that names the code, in `atlas/` and in other code's `@atlas` lines (brace forms like `mod:truffle_core::{node, identity, egress}` hide from grep). `check` must then show 0 DANGLING. Once a baseline exists, moving the code of a known finding also makes it new (the skill's "Known or new"). +- **Never weaken a rule to make it pass**: fix the code. Widening an `only` list, adding an `except`, moving a module to another tier 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`, and `atlas baseline` runs only when the user decides to accept today's violations, never to turn the check green. +- **Check** from the repo root after changing crates, modules, packages, dependencies, `atlas/`, or a heading in CLAUDE.md, a README or `docs/` (`see` cites them by slug): `TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "${ATLAS:-$HOME/Projects/project100/protos/project-atlas}" run -s atlas -- check "$PWD" --intent "$PWD/atlas"`. It runs `cargo doc` over the workspace (a build, build scripts included); the variable keeps truffle-sidecar's build script offline, as CI's lint job does (`cargo doc` never runs the sidecar). Atlas is a private tool: if `$ATLAS/package.json` is missing (the command exits 254 with no output), skip the check, install nothing, and say so. When builds are off-limits, run the syntax-only check: `ATLAS="${ATLAS:-$HOME/Projects/project100/protos/project-atlas}"; "$ATLAS/node_modules/.bin/tsx" "$ATLAS/packages/engine-intent/src/cli.ts" atlas crates benchmark packages examples`. +- **Reading it**: exit 1 = a new error violation. Until the user runs `atlas baseline`, the check exits 1 on a clean tree with 7 findings kept for the user (listed in the skill). Your change is clean only when VIOLATED shows those same 7 claims with the same counts (the exit code is 1 either way, and a further edge into one of the 7 only raises its count); do not change code, rules or tiers for them unless asked. DANGLING does not fail the check but must be fixed, and a dangling rule, contract or flow named in a decision's `confirmed-by` makes that decision violated (exit 1; `layered-architecture` and `synced-store` are violated already, so for their members only DANGLING and the decision's evidence show it): fix the selector. +- **Commits**: an atlas-only change is `docs:` or `chore:`, never `feat:`/`fix:`, which make Release Please cut a release. Staged `@atlas` lines still trigger the pre-commit hook: a staged truffle-napi `src/*.rs`, `Cargo.toml` or `build.rs` runs a release `napi build` (and fails if pnpm is missing); a staged truffle-core `lib.rs`/`node.rs` or truffle `lib.rs`/`Cargo.toml` runs `cargo test -p truffle --doc`; every staged `.rs` gets rustfmt (the skill lists all 7 checks). Never put `@atlas` in a `///` line: rustdoc renders it, and NAPI-RS copies item docs into `index.d.ts`. + ## RFCs RFCs live in `docs/rfcs/`. Key ones: diff --git a/atlas/components.atlas b/atlas/components.atlas new file mode 100644 index 00000000..58612e74 --- /dev/null +++ b/atlas/components.atlas @@ -0,0 +1,131 @@ +// 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 their code (comments stripped). A link without `via` is graded against code +// edges. Go, Swift and guest-js files are reachable only by `file:`/`path:` anchors. + +component core "truffle-core (in-process library)" kind library { + intent "A Rust host embeds a Node, which owns its sidecar child and loopback bridge." + contains crate:truffle-core + see "CLAUDE.md#architecture" +} +component sidecar "sidecar-slim (Go child process, tsnet inside)" kind process { + intent "tsnet lifecycle, tailnet dial and listen, peer watch, the RFC 023 serve engine and RFC 026 egress; no envelopes, sessions or subsystems." + contains path:packages/sidecar-slim/, !path:packages/sidecar-slim/cmd/ + see "docs/rfcs/012-layered-architecture-redesign.md#36-layers-1-2-bridge--sidecar-internal-to-tailscaleprovider" +} +component cli "truffle (CLI binary)" kind cli { + contains crate:truffle-cli + component daemon "truffle up --foreground, or bare truffle (the TUI): owns the Node" kind daemon { + contains file:crates/truffle-cli/src/daemon/{server, handler}.rs, path:crates/truffle-cli/src/tui/ + } + component agent "truffle : a short-lived JSON-RPC client" kind cli { + contains path:crates/truffle-cli/src/commands/, file:crates/truffle-cli/src/daemon/client.rs + } + see "docs/rfcs/013-tui-redesign.md#3-architecture-decision-tui--foreground-daemon" +} +component bench "truffle-bench (the engine library and its CLI: hubs that run and answer workloads, or a pair in one process)" kind library { + contains crate:truffle-bench + see "docs/rfcs/027-benchmark-hub.md#54-hub" + see "docs/rfcs/027-benchmark-hub.md#10-cli" +} +component bench-app "truffle bench (Tauri v2 desktop app: the engine's Hub runs in the app process)" kind app { + contains crate:truffle-bench-app + component bench-ui "React frontend (in the webview)" kind ui { contains pkg:@vibecook/truffle-bench-app } + see "docs/rfcs/027-benchmark-hub.md#111-shell-and-process" +} +component napi "NAPI addon (inside the Node.js or Electron process)" kind native-addon { + contains crate:truffle-napi, pkg:@vibecook/truffle-native +} +component js-sdk "@vibecook/truffle" kind library { contains pkg:@vibecook/truffle } +component react "@vibecook/truffle-react" kind library { contains pkg:@vibecook/truffle-react } +component tauri "Tauri plugin (Rust, in the Tauri app process)" kind plugin { + contains crate:truffle-tauri-plugin + component tauri-guest "guest-js (in the webview)" kind ui-library { contains path:crates/truffle-tauri-plugin/guest-js/ } +} +component swift "Truffle Swift (in-process on iOS and macOS, no sidecar)" kind library { + contains path:apple/Sources/ + see "docs/rfcs/024-truffle-swift.md#52-runtime-split-one-api-two-engines" +} +component tailnet "Tailscale control plane, DERP and WireGuard peers" external +component peer "Another truffle node on the tailnet (Rust or Swift)" external +component tailscalekit "TailscaleKit / libtailscale (pinned XCFramework)" external +component host-apps "Other processes on the node's host (served targets, egress clients)" external + +core -> sidecar "spawned child; JSON lines: commands on stdin, events on stdout" via spawn { + at mod:truffle_core::network::tailscale::sidecar, mod:truffle_core::network::tailscale::protocol + at file:packages/sidecar-slim/main.go + witness "kill_on_drop" + witness "tsnet:start" +} +sidecar -> core "loopback TCP bridge: TRFF header and session token, then raw bytes" via ipc { + at mod:truffle_core::network::tailscale::{bridge, header}, file:packages/sidecar-slim/main.go + witness "127.0.0.1:0" + witness "0x54524646" +} +core -> peer "peer session: WebSocket /ws on tailnet port 9417, hello v2, then JSON envelopes" via ws { + at mod:truffle_core::transport::websocket, mod:truffle_core::session::hello + witness "/ws" +} +core -> peer "raw tailnet TCP, UDP and app-scoped QUIC (ALPN truffle-raw.{appId})" via port { + at mod:truffle_core::transport::{tcp, quic}, mod:truffle_core::network::tailscale::provider + witness "truffle-raw" +} +core -> sidecar "loopback UDP relay for datagrams and QUIC, HMAC-tokened per binding" via port { + at mod:truffle_core::network::tailscale::provider, file:packages/sidecar-slim/udp_relay.go + witness "TRUFFLE_UDP_REGISTER" +} +swift -> peer "the same session protocol, a second implementation" via ws { + at file:apple/Sources/Truffle/Session/RFC6455FrameTransport.swift + witness "GET /ws HTTP/1.1" +} +sidecar -> tailnet "tsnet.Server: WireGuard tunnels; peers from LocalAPI WatchIPNBus" via port { + at file:packages/sidecar-slim/main.go + witness "WatchIPNBus" +} +swift -> tailscalekit "tailscale_dial and tailscale_listen; LocalAPI WatchIPNBus and WhoIs" via embed { + at file:apple/Sources/TruffleTailscale/TailscaleKitBackend.swift + witness "tailscale_dial" +} +agent -> daemon "JSON-RPC 2.0, one object per line, over a Unix socket or named pipe" via ipc { + at file:crates/truffle-cli/src/daemon/{ipc, protocol}.rs, file:crates/truffle-cli/src/config.rs + witness "truffle.sock" + witness "push_file" +} +agent -> daemon "truffle up re-execs itself detached as `up --foreground`" via spawn { + at file:crates/truffle-cli/src/commands/up.rs + witness "--foreground" +} +tauri-guest -> tauri "invoke plugin:truffle commands; listen to truffle:// events" via ipc { + at file:crates/truffle-tauri-plugin/guest-js/index.ts, mod:truffle_tauri_plugin::events + witness "plugin:truffle|start" + witness "truffle://peer-event" +} +host-apps -> sidecar "RFC 026 egress: loopback SOCKS5, fixed routes, CONNECT proxy" via port { + at file:packages/sidecar-slim/egress.go + witness "127.0.0.1:0" +} +tailnet -> sidecar "HTTP(S) from any tailnet client to a mesh.serve listener" via http { + at file:packages/sidecar-slim/main.go + witness "ListenTLS" +} +sidecar -> host-apps "RFC 023 serve: reverse proxy to a loopback target with Tailscale identity headers" via http { + at file:packages/sidecar-slim/main.go + witness "Tailscale-User-Login" +} +bench -> peer "RFC 027 control protocol: hello, clock, prepare, start and finish, each acked, on namespace \"bench\"" via ws { + at mod:truffle_bench::control + witness "\"prepare\"" + witness "\".ack\"" +} +bench-ui -> bench-app "invoke the RFC 027 §11.2 commands; hub events on bench://event, run progress on a Channel" via ipc { + at file:benchmark/app/src/api/{api, tauri}.ts, mod:truffle_bench_app::commands + witness "bench://event" + witness "bench_run" +} + +js-sdk -> napi "loads the addon in-process: NapiNode, socket handles, event callbacks" +react -> js-sdk "hooks over the mesh node" +napi -> core "binds one Node" +tauri -> core "binds one Node" +cli -> core "the daemon owns one Node" +bench -> core "builds every node with NodeBuilder (one Node per hub) and drives the workloads on it" +bench-app -> bench "the shell owns one Hub; every run and suite executes in the engine" diff --git a/atlas/contracts.atlas b/atlas/contracts.atlas new file mode 100644 index 00000000..d8f5ce62 --- /dev/null +++ b/atlas/contracts.atlas @@ -0,0 +1,38 @@ +// Seams with several anchors are declared here. Contracts with one anchor are declared in-code at their trait or +// enum: network-provider (NetworkProvider), envelope-codec (EnvelopeCodec), store-backend (StoreBackend), ft-wire +// (FtMessage) and sync-wire (SyncMessage). + +contract transport-traits "Layer 4 transport trait families" { + intent "Framed streams (WebSocket, QUIC), raw byte streams (TCP) and datagrams (UDP); Layer 5 consumes FramedStream." + at sym:truffle_core::transport::{StreamTransport, FramedStream, RawTransport, DatagramTransport} + implemented-by crate:truffle-core + see "docs/rfcs/012-layered-architecture-redesign.md#34-layer-4-transport" +} + +contract napi-projection "The key types every binding bridges, as NAPI exports" { + intent "RFC 015's list of types to bridge; each has a Napi-prefixed export in the generated index.d.ts." + at sym:truffle_core::{Peer, NamespacedMessage, FileOffer, FileTransferEvent, TransferResult} + at sym:truffle_core::session::PeerEvent, sym:truffle_core::network::{NodeIdentity, PingResult} + mirror "crates/truffle-napi/index.d.ts#Napi{name}" + note file:crates/truffle-napi/index.d.ts "Generated by NAPI-RS; the pre-commit hook rebuilds it and blocks a commit whose copy is stale." + see "docs/rfcs/015-binding-rewrite.md#3-current-node-api-what-bindings-must-wrap" + see "CLAUDE.md#pre-commit-hook-one-time-setup-per-clone" +} + +contract session-wire "Session wire contract: hello v2 and the JSON envelope" { + intent "Both engines speak it verbatim; the wire `from` is never trusted, the receiver stamps the WhoIs-verified id." + at sym:truffle_core::Envelope, sym:truffle_core::session::hello::HelloEnvelope + note file:apple/Sources/Truffle/Wire/Envelope.swift, file:apple/Sources/Truffle/Wire/HelloEnvelope.swift "The Swift mirrors, field for field." + note path:apple/Tests/TruffleTests/Fixtures/ "Shared fixtures, decoded by WireTests.swift and by crates/truffle-core/tests/interop_fixtures.rs." + see "docs/rfcs/024-truffle-swift.md#81-session-protocol-as-shipped" + see "apple/README.md#wire-compatibility-rfc-024-8" +} + +contract bench-app-types "The RFC 027 §7 fixture types, mirrored by hand in TypeScript" { + intent "Rust is the source of truth (benchmark/schema is generated from it); app/src/api/types.ts is hand-written, and fixtures.test.ts type-checks every fixture against it." + at sym:truffle_bench::types::{HubEvent, NodeState, Progress, RunSummary, AppInfo, Settings, BenchPeer, PeerProbe, SuiteProfile} + at sym:truffle_bench::params::WorkloadParams, sym:truffle_bench::report::RunReport, sym:truffle_bench::error::AppError + mirror "benchmark/app/src/api/types.ts#{name}" + see "docs/rfcs/027-benchmark-hub.md#7-types-shared-with-the-app" + see "benchmark/README.md#reports" +} diff --git a/atlas/decisions.atlas b/atlas/decisions.atlas new file mode 100644 index 00000000..8cb5f2cc --- /dev/null +++ b/atlas/decisions.atlas @@ -0,0 +1,169 @@ +// Decisions with their real status, which is not always the RFC header's: several headers say Draft or Proposed for +// work that shipped. Verified only when accepted and every confirmed-by rule, contract or flow is verified; +// proposed, hypothesis and superseded decisions are asserted. + +decision rust-core-go-shim "Rust core over a thin Go shim that keeps tsnet" code RFC-003 status accepted date 2026-02-24 { + because "The Go shim keeps only tsnet lifecycle and TCP bridging; its no-HTTP clause was later reversed by RFC 023 (the serve engine) and RFC 026 (egress)." + affects crate:truffle-core, path:packages/sidecar-slim/ + see "docs/rfcs/003-rust-rewrite.md#6-what-stays-go" + see "docs/rfcs/023-http-serving.md#12-decisions" +} + +decision vision-008 "TruffleRuntime, STAR mesh and a six-layer stack" code RFC-008 status superseded date 2026-03-18 { + because "The header says Implemented; RFC 012's rebuild replaced it, and its non-goals (no iOS SDK, no sidecar distribution) were reversed later." + see "docs/rfcs/008-truffle-vision.md#2-architecture-layers" + see "docs/rfcs/012-layered-architecture-redesign.md#L6" +} + +decision binary-frames-009 "Binary frame header and a typed Namespace enum" code RFC-009 status superseded date 2026-03-19 { + because "The header says Implemented (2026-03-19); the shipped envelope is JSON with an opaque namespace string, and RFC 024 records that RFC 009 frames never shipped for the session." + see "docs/rfcs/009-wire-protocol-redesign.md#31-binary-framing" + see "docs/rfcs/024-truffle-swift.md#83-message-envelope-as-shipped--envelopemodrs" +} + +decision cli-daemon "A CLI daemon over a local socket; no STAR topology or election" code RFC-010 status accepted date 2026-03-20 { + because "The header still says Proposed; the daemon, IPC and JSON-RPC protocol shipped in truffle-cli." + affects crate:truffle-cli + see "docs/rfcs/010-p2p-and-cli-redesign.md#3-cli-daemon-architecture" +} + +decision ft-over-http-011 "File transfer as HTTP PUT over direct tailnet TCP" code RFC-011 status superseded date 2026-03-22 { + because "The header says Implemented (v0.2.4-v0.2.5); RFC 012 removed its HTTP path (axum, TcpProxyHandler), and RFC 014 moved file transfer into core over raw TCP." + see "docs/rfcs/011-file-transfer-over-mesh.md#3-proposed-design-direct-tcp-over-mesh" + see "docs/rfcs/012-layered-architecture-redesign.md#5-network-paths" +} + +decision layered-architecture "Layers 3-6 behind one Node API; applications are Layer 7" code RFC-012 status accepted date 2026-03-24 { + because "The earlier core coupled layers: file transfer straddled two paths, discovery mixed polling and announces, apps managed bridge internals." + affects crate:truffle-core + confirmed-by layers-point-down, network-knows-no-upper-layer, peers-from-layer-3, + transport-knows-no-session, transports-independent, session-knows-no-payload, + envelope-knows-no-transport, bridge-is-private, apps-use-node-api, core-depends-on-no-crate + see "docs/rfcs/012-layered-architecture-redesign.md#1-motivation" + see "CLAUDE.md#architecture" +} + +decision lazy-connections "Dial a peer's WebSocket on the first send, not on discovery" code RFC-012 status superseded date 2026-03-24 { + because "RFC 022 Phase C made eager identity the default: one hello dial per online peer, without waiting for a send. A send still dials when no WebSocket is open." + affects mod:truffle_core::session + see "docs/rfcs/012-layered-architecture-redesign.md#93-why-lazy-connections" + see "docs/rfcs/022-peer-handle-api.md#phase-c--eager-identity-default-on--done" +} + +decision raw-tcp-for-files "Raw TCP for bulk data, WebSocket for messages" code RFC-012 status accepted date 2026-03-24 { + because "No HTTP stack on the data path; messaging still needs framing, so WebSocket stays." + affects mod:truffle_core::{file_transfer, transport} + confirmed-by no-http-stack-on-data-path, file-push + see "docs/rfcs/012-layered-architecture-redesign.md#94-why-raw-tcp-for-file-transfer-instead-of-http" + see "docs/rfcs/012-layered-architecture-redesign.md#95-why-keep-websocket-not-just-raw-tcp-for-everything" +} + +decision tui-is-the-daemon "The TUI runs the daemon in-process; agents use one-shot commands" code RFC-013 status accepted date 2026-03-28 { + affects crate:truffle-cli + see "docs/rfcs/013-tui-redesign.md#3-architecture-decision-tui--foreground-daemon" + see "docs/rfcs/013-tui-redesign.md#11-agent-mode-one-shot-commands" +} + +decision file-transfer-in-core "File transfer is a core subsystem with an accept/reject API" code RFC-014 status accepted date 2026-03-29 { + because "The header says Draft; core file_transfer/ shipped, reversing RFC 012's file transfer as a Layer 7 app. Phase 2 is partly done: cli/src/apps/file_transfer/ survives as a thin wrapper." + affects mod:truffle_core::file_transfer + confirmed-by ft-wire + see "docs/rfcs/014-file-transfer-core.md#4-proposed-architecture" + see "docs/rfcs/014-file-transfer-core.md#11-decided-questions" +} + +decision bindings-on-node "NAPI and Tauri bindings rewritten against Node" code RFC-015 status accepted date 2026-03-30 { + because "The header says Draft; both bindings were rebuilt on Node. Its dropped list (store, proxy, raw sockets) was reversed by later RFCs." + affects crate:truffle-napi, crate:truffle-tauri-plugin + confirmed-by bindings-wrap-core, napi-projection + see "docs/rfcs/015-binding-rewrite.md#3-current-node-api-what-bindings-must-wrap" +} + +decision synced-store "SyncedStore and a request/reply helper in core" code RFC-016 status accepted date 2026-03-30 { + because "The header says Draft; phases 1-3 shipped (FileBackend persistence included), the phase 4 CLI did not. RFC 025 later bound every slice and Clear to its sender." + affects mod:truffle_core::{synced_store, request_reply} + confirmed-by sync-wire, request-reply-over-node + see "docs/rfcs/016-synced-store-and-request-reply.md#3-design" + see "docs/rfcs/025-login-scoped-nodes.md#35-sender-bound-slices-tf-2" +} + +decision app-namespacing "appId isolation and the truffle- hostname prefix" code RFC-017 status accepted date 2026-04-11 { + affects mod:truffle_core::{identity, session::hello}, path:apple/Sources/Truffle/Identity/ + affects mod:truffle_core::transport::websocket, mod:truffle_core::network::tailscale::provider + see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" + see "docs/rfcs/017-identity-and-namespacing.md#8-wire-protocol-peer-metadata-exchange" +} + +decision real-network-harness "Real-tailnet tests and benchmarks, locally and in CI" code RFC-019 status accepted date 2026-04-21 { + because "RFC 019 is partly stale (its CRDT parts went with RFC 018); RFC 020 added the CI workflows; RFC 027 superseded RFC 019 §10.2 and RFC 020 §3 and §6.2 (benchmarks.yml)." + affects crate:truffle-bench + confirmed-by bench-keys-from-env + see "docs/rfcs/019-local-testing-and-benchmarking.md#4-design-overview" + see "docs/rfcs/020-ci-testing.md#4-design-overview" +} + +decision raw-transport-js "Raw TCP, UDP and QUIC in the JS API with pull-model handles" code RFC-021 status accepted date 2026-07-07 { + because "Phases 1-4 shipped; phase 5 (WebTransport, QUIC datagrams, listener ACLs) is future work." + affects mod:truffle_napi::{raw_socket, udp_socket, quic}, file:packages/core/src/{net, dgram, quic, ws}.ts + confirmed-by napi-pull-model, websocket-stays-internal + see "docs/rfcs/021-raw-transport-js-api.md#12-decided-questions" +} + +decision peer-handles "Peer handles; an honest deviceId; WhoIs attribution" code RFC-022 status accepted date 2026-07-09 { + because "The header says in progress and unreleased; it shipped in 0.6.0. Phase D (hostinfo) has not started." + affects mod:truffle_core::session, mod:truffle_napi::peer, file:packages/core/src/peer.ts + see "docs/rfcs/022-peer-handle-api.md#61-peer-handle" + see "docs/rfcs/022-peer-handle-api.md#75-message-path" +} + +decision http-serving "Serve over the tailnet: JS handlers over the bridge, other targets in the Go engine" code RFC-023 status accepted date 2026-07-12 { + because "Phases 1-3 shipped; Funnel (4) and service discovery (5) are future, and proxy/discovery.rs stays inert until then (D10)." + affects mod:truffle_core::proxy, file:packages/core/src/{serve, http}.ts, path:packages/sidecar-slim/ + see "docs/rfcs/023-http-serving.md#5-design-overview" + see "docs/rfcs/023-http-serving.md#12-decisions" +} + +decision swift-engine "Truffle Swift: an Apple-native engine on TailscaleKit" code RFC-024 status accepted date 2026-07-16 { + because "Phases 0-2 shipped; phase 3 has not started and phase 4 is partial. It does not supersede the desktop stack." + affects path:apple/ + confirmed-by tailscalekit-below-product + see "docs/rfcs/024-truffle-swift.md#2-decision" +} + +decision login-scoped-nodes "A per-node login allow-list as the mesh boundary" code RFC-025 status accepted date 2026-09-16 { + affects mod:truffle_core::{network::login_allow, session::hello, synced_store} + affects mod:truffle_core::transport::websocket, mod:truffle_core::network::tailscale::provider + see "docs/rfcs/025-login-scoped-nodes.md#2-decision" + see "docs/rfcs/025-login-scoped-nodes.md#8-decisions" +} + +decision egress "Egress for the node's own host processes, loopback only" code RFC-026 status accepted date 2026-09-30 { + because "Sidecar, core and NAPI shipped; mesh.egress in TS and Tauri, and Apple E2/E3, are future work." + affects mod:truffle_core::egress, mod:truffle_napi::egress, file:packages/sidecar-slim/egress.go + confirmed-by egress-loopback-only + see "docs/rfcs/026-egress.md#2-decision" + see "docs/rfcs/026-egress.md#10-decisions" +} + +decision benchmark-hub "One benchmark engine that the CLI and the desktop app both drive" code RFC-027 status accepted date 2026-10-16 { + because "The header says Implemented on feat/benchmark-hub (not yet merged); it merged to main in #213. truffle-bench moved to benchmark/engine as a library plus a CLI." + affects crate:truffle-bench, crate:truffle-bench-app, pkg:@vibecook/truffle-bench-app, path:benchmark/schema/ + confirmed-by bench-keys-from-env, bench-never-sets-hostname, legacy-transport-without-tbn1, + bench-app-drives-the-engine, bench-ui-renders-text-only, bench-app-types + see "docs/rfcs/027-benchmark-hub.md#2-goals" + see "benchmark/README.md#what-lives-here" +} + +decision public-surfaces "Which artifacts are stable surfaces, and which are implementation details" status accepted { + affects pkg:@vibecook/truffle, pkg:@vibecook/truffle-react, crate:truffle-cli, crate:truffle, crate:truffle-core, crate:truffle-sidecar + affects pkg:@vibecook/truffle-native + confirmed-by native-is-internal + see "docs/API-STABILITY.md#from-10" +} + +decision one-release-unit "Every public artifact ships at one version; sidecar binaries fail closed" status accepted { + because "scripts/check-release-versions.mjs enforces the lockstep; manifests are config, so Atlas cannot see it. The unpublished benchmark hub keeps its own versions." + affects path:scripts/, crate:truffle-sidecar + see "docs/RELEASING.md#release-contract" + see "docs/RELEASING.md#the-benchmark-hub-is-not-released" +} diff --git a/atlas/domains.atlas b/atlas/domains.atlas new file mode 100644 index 00000000..6b726037 --- /dev/null +++ b/atlas/domains.atlas @@ -0,0 +1,127 @@ +// Domains cut across layers and languages. An unpublished crate with a root doc names its domain in-code +// (`//! @atlas layer … · domain …`); the claims below are the crates.io crates (truffle-core, truffle, +// truffle-sidecar), truffle-cli, the npm packages, the directories, and the modules and files that serve another +// domain than their crate (each truffle-bench workload `touches` the domain it measures). The most specific +// `contains` wins; `touches` adds a secondary membership and never competes. Go and Swift files are counted by no +// coverage: a `note` names the Go file that does a domain's work without binding it (a `file:` term would bind its +// whole directory). + +domain mesh "Mesh core: Node, discovery, peers and sessions" { + intent "Discovery-aware peers: Layer 3 (WatchIPNBus) is the source of truth, never transport connections." + contains crate:truffle-core, mod:truffle_napi::peer, file:packages/core/src/peer.ts + touches path:apple/Sources/Truffle/Mesh/ + touches mod:truffle_bench::workloads::path + see "CLAUDE.md#architecture" + see "docs/rfcs/012-layered-architecture-redesign.md#33-layer-5-session" + see "docs/rfcs/022-peer-handle-api.md#61-peer-handle" +} + +domain messaging "Namespaced messaging" { + contains mod:truffle_core::envelope + contains file:crates/truffle-cli/src/commands/{send, recv}.rs, file:crates/truffle-cli/src/apps/messaging.rs + contains file:crates/truffle-cli/src/tui/commands/{send, broadcast}.rs + touches sym:truffle_core::node::Node::{send_typed, broadcast_typed, subscribe} + touches mod:truffle_bench::workloads::ws + see "CLAUDE.md#key-conventions" + see "docs/rfcs/012-layered-architecture-redesign.md#32-layer-6-envelope" +} + +domain raw-transport "Raw TCP, UDP and QUIC (RFC 021)" { + intent "Node-shaped sockets over the tailnet: plain handles in NAPI, native shapes in TS." + contains mod:truffle_core::transport::{tcp, udp, quic, quic_socket} + contains mod:truffle_napi::{raw_socket, udp_socket, quic}, mod:truffle_tauri_plugin::raw_transport + contains file:packages/core/src/{net, dgram, quic, ws}.ts, file:crates/truffle-cli/src/commands/tcp.rs + touches mod:truffle_bench::workloads::{tcp, udp, quic, video} + note file:packages/sidecar-slim/udp_relay.go "The UDP loopback relay that carries datagrams and QUIC." + see "docs/rfcs/021-raw-transport-js-api.md#5-design-overview" +} + +domain file-transfer "File transfer (RFC 014)" { + intent "Signaling on namespace \"ft\"; bytes on a one-off raw tailnet TCP stream, SHA-256 verified." + contains mod:truffle_core::file_transfer, mod:truffle_napi::file_transfer + contains path:crates/truffle-cli/src/apps/file_transfer/, file:crates/truffle-cli/src/commands/cp.rs + contains file:crates/truffle-cli/src/tui/commands/cp.rs + contains file:crates/truffle-cli/src/tui/ui/{transfer_dialog, file_picker}.rs + touches mod:truffle_bench::workloads::file_transfer + see "docs/rfcs/014-file-transfer-core.md#4-proposed-architecture" + see "CLAUDE.md#key-conventions" +} + +domain synced-store "Synced store (RFC 016)" { + intent "Device-owned slices, last write wins by version, namespace \"ss:{id}\"." + contains mod:truffle_core::synced_store, mod:truffle_napi::synced_store + contains file:packages/react/src/{use-synced-store, synced-store-controller}.ts + touches mod:truffle_bench::workloads::synced_store + see "docs/rfcs/016-synced-store-and-request-reply.md#32-syncedstore" +} + +domain request-reply "Request/reply (RFC 016)" { + contains mod:truffle_core::request_reply + see "docs/rfcs/016-synced-store-and-request-reply.md#31-requestreply-utility" +} + +domain serving "HTTP serving over the tailnet (RFC 023)" { + intent "A JS handler gets the bytes over the bridge; a directory or another process is served by the Go engine." + contains mod:truffle_core::proxy, mod:truffle_napi::proxy + contains file:packages/core/src/{serve, http}.ts, file:crates/truffle-cli/src/commands/{serve, proxy}.rs + touches mod:truffle_bench::workloads::http + note file:packages/sidecar-slim/main.go "The serve engine (routes, reverse proxy, static files, identity headers) runs in the sidecar." + see "docs/rfcs/023-http-serving.md#5-design-overview" + see "docs/guide/serving-http.md#publishing-existing-services-with-meshserve" +} + +domain egress "Egress for the node's own host processes (RFC 026)" { + contains mod:truffle_core::egress, mod:truffle_napi::egress + touches mod:truffle_bench::workloads::egress + note file:packages/sidecar-slim/egress.go "The loopback SOCKS5, fixed-route and CONNECT listeners run in the sidecar." + see "docs/rfcs/026-egress.md#2-decision" +} + +domain identity "Identity, app isolation and the login gate (RFC 017, 022, 025)" { + intent "truffle-{appId}-{slug} hostnames, ULID device ids, the hello v2 gate and the per-node login allow-list." + contains mod:truffle_core::{identity, session::hello, network::login_allow} + touches path:apple/Sources/Truffle/Identity/ + see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" + see "docs/rfcs/025-login-scoped-nodes.md#2-decision" +} + +domain bindings "Bindings and the JS API" { + intent "Projections of the Node API: the NAPI addon, the Tauri v2 plugin, the primary JS API and its React hooks." + contains pkg:@vibecook/truffle, pkg:@vibecook/truffle-react, pkg:@vibecook/truffle-native + contains path:crates/truffle-tauri-plugin/guest-js/ + see "README.md#packages" + see "docs/rfcs/015-binding-rewrite.md#3-current-node-api-what-bindings-must-wrap" +} + +domain cli "CLI, TUI and daemon (RFC 010, 013)" { + intent "The TUI is the daemon in the foreground; one-shot agent commands talk to it over local JSON-RPC." + contains crate:truffle-cli, pkg:@vibecook/truffle-cli + see "docs/rfcs/013-tui-redesign.md#3-architecture-decision-tui--foreground-daemon" + see "docs/rfcs/010-p2p-and-cli-redesign.md#3-cli-daemon-architecture" +} + +domain distribution "Release and sidecar distribution" { + intent "One release unit at one version; sidecar binaries are checksum-verified and fail closed." + contains crate:truffle, crate:truffle-sidecar, pkg:@vibecook/truffle-sidecar-*, path:scripts/ + contains file:packages/core/src/sidecar.ts, file:crates/truffle-napi/helpers.js + contains file:crates/truffle-cli/src/auto_update.rs, file:crates/truffle-cli/src/commands/update.rs + see "docs/RELEASING.md#release-contract" +} + +domain bench "Benchmarks and real-network tests (RFC 019, 020, 027)" { + contains pkg:@vibecook/truffle-bench-app, path:benchmark/schema/, path:packages/sidecar-slim/cmd/ + see "docs/rfcs/027-benchmark-hub.md#2-goals" + see "benchmark/README.md#what-lives-here" + see "docs/TESTING.md#benchmarks" +} + +domain swift "Truffle Swift (RFC 024)" { + contains path:apple/ + see "docs/rfcs/024-truffle-swift.md#2-decision" + see "apple/README.md#package-layout" +} + +domain examples "Example apps" { + contains pkg:@vibecook/example-*, path:apple/Examples/ + see "README.md#examples" +} diff --git a/atlas/flows.atlas b/atlas/flows.atlas new file mode 100644 index 00000000..44acceb2 --- /dev/null +++ b/atlas/flows.atlas @@ -0,0 +1,59 @@ +// Four hot paths. A step done by a Rust function is bound in-code (`// @atlas step .` above it; list them +// with `grep -rn '@atlas step' crates`); a step done in Go keeps a `file:` anchor here, because `atlas build` reads no +// `@atlas` comment in a .go file. A step with no anchor grades `asserted`: 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 discovery "Tailnet peer change → PeerEvent at every consumer" { + intent "Layer 3 is the source of truth for peers: a peer exists because WatchIPNBus says so, before any connection." + by mod:truffle_core::node + step watch "sidecar: WatchIPNBus and a status diff emit tsnet:peerChanged" at file:packages/sidecar-slim/main.go + step read "stdout reader parses each JSON line and dispatches the event" via spawn + step admit "provider drops self, keeps truffle-{appId}- peers, applies the login gate" + step registry "PeerRegistry stamps a generation, emits PeerEvent, schedules the eager hello" + step fanout "Node::on_peer_change feeds NAPI, Tauri, the daemon and SyncedStore" + see "CLAUDE.md#architecture" + see "docs/rfcs/012-layered-architecture-redesign.md#33-layer-5-session" +} + +flow send "Node::send_typed → the remote namespace subscriber" { + intent "If no WebSocket is open (eager identity off, or it dropped), the first send dials it and concurrent callers share that one dial; envelopes travel as Binary frames, attributed to the WhoIs-verified id." + by mod:truffle_core::node + step encode "send_typed wraps the payload in an Envelope (JSON codec)" + step route "PeerRegistry::send resolves the peer; with no WebSocket open it starts or joins the peer's one dial" direct + step dial "WebSocketTransport::connect dials through Layer 3 and runs the client hello" direct + step bridge "TailscaleProvider registers the pending dial and sends bridge:dial" + step tsnet "sidecar dials over tsnet and bridges the stream back behind a TRFF header" at file:packages/sidecar-slim/main.go via spawn + step handoff "Bridge checks the session token and hands the TcpStream to the pending dial" via ipc + step accept "remote: WebSocketTransport::listen accepts; the server hello verifies WhoIs" via ws + step frame "the connection task writes the envelope as one Binary frame" via ws + step receive "remote: the connection task reads the frame; from is the connection's WhoIs-verified id" via ws + step deliver "remote: the envelope router decodes and fans out by namespace" + see "docs/rfcs/012-layered-architecture-redesign.md#116-truffle-send" + see "docs/rfcs/022-peer-handle-api.md#75-message-path" + see "docs/rfcs/022-peer-handle-api.md#81-priority" +} + +flow file-push "FileTransfer::send_file → a verified file on the receiver" { + intent "Signaling on the WebSocket namespace \"ft\"; the bytes on a one-off raw tailnet TCP stream." + step offer "sender hashes the file in 64 KiB chunks and builds the OFFER" + step wait "send_and_wait: one send_typed on \"ft\" and a token-filtered subscribe" direct + step answer "receiver: the app's OfferResponder decides, or auto-accept" via ws + step accept "receiver listens on a fresh tailnet port and answers ACCEPT with it" + step open "sender opens raw tailnet TCP to that port, TLS forced off" via ws + step stream "8-byte size, 64-hex SHA-256, then 64 KiB chunks" + step verify "receiver checks the SHA-256, places the file per its overwrite policy, ACKs 0x01" via port + see "docs/rfcs/014-file-transfer-core.md#52-sending-files" + see "docs/rfcs/012-layered-architecture-redesign.md#94-why-raw-tcp-for-file-transfer-instead-of-http" +} + +flow store-update "SyncedStore::set → StoreEvent::PeerUpdated on every connected peer" { + intent "Each device writes only its own slice; a remote copy is replaced only by a newer version from its owner." + step set "bump the version, persist through the backend, coalesce in a watch channel" + step push "the sync task broadcasts update on ss:{id}" + step envelope "Node::broadcast_typed wraps the slice in an Envelope" + step fan "PeerRegistry::broadcast: connected peers only, no lazy connect" + step apply "remote: the sender must own the slice; a newer version replaces it" via ws + see "docs/rfcs/016-synced-store-and-request-reply.md#wire-protocol" + see "docs/rfcs/025-login-scoped-nodes.md#35-sender-bound-slices-tf-2" +} diff --git a/atlas/layers.atlas b/atlas/layers.atlas new file mode 100644 index 00000000..1bc438bb --- /dev/null +++ b/atlas/layers.atlas @@ -0,0 +1,70 @@ +// RFC 012's stack. Position is rank, top to bottom; nested layers are ordered sub-tiers. An unpublished crate with a +// root doc places itself (`//! @atlas layer …`: truffle-napi, truffle-tauri-plugin, truffle-bench, truffle-bench-app); +// this file places the crates.io crates (truffle-core, truffle, truffle-sidecar: no atlas text on docs.rs), +// truffle-core's module tiers (the RFC 012 layers are modules of one crate), truffle-cli (its main.rs has no crate +// doc), the npm packages and the directories. `exhaustive`: an unplaced production file is a violation. A truffle-core +// module missing from the tiers below falls to node-api through `mod:truffle_core`, never to the unranked `core` band, +// and nothing reports it: `explain` a new module. + +layers stack "RFC 012: each layer builds only on the layers below it" exhaustive { + see "docs/rfcs/012-layered-architecture-redesign.md#2-architecture-overview" + see "CLAUDE.md#architecture" + layer apps "Layer 7 · Applications" code L7 { + intent "User code, not part of truffle-core: every app and binding reaches the mesh through the Node API." + layer examples "Example apps" { contains pkg:@vibecook/example-* } + layer cli "truffle CLI, TUI and daemon" { + contains crate:truffle-cli, pkg:@vibecook/truffle-cli + note pkg:@vibecook/truffle-cli "Private TS CLI (dev, init, status) absent from README's package table; RFC 008 had the Rust CLI replace it." + } + layer bench "Benchmarks (RFC 027): the truffle-bench engine and CLI, and the truffle bench desktop app" { + contains pkg:@vibecook/truffle-bench-app, path:benchmark/schema/ + note path:benchmark/schema/ "Generated from the engine's Rust types (schemars); the app's mock API and its tests read the fixtures." + } + layer react "React hooks" { contains pkg:@vibecook/truffle-react } + layer js-api "JS API" { contains pkg:@vibecook/truffle } + layer bindings "Native bindings (NAPI addon, Tauri plugin)" { + contains pkg:@vibecook/truffle-native, path:crates/truffle-tauri-plugin/guest-js/ + } + } + layer core "truffle-core: the Node API and Layers 3-6" { + intent "Structure only: the package as a whole. Every production module ranks in a tier below; node-api takes the crate root and any module the tiers do not list." + contains crate:truffle-core + layer node-api "Node API and core subsystems" { + intent "Node wires Layers 3-6; FileTransfer, SyncedStore, request/reply and the serve proxy are built on it." + contains mod:truffle_core, mod:truffle_core::{node, identity, egress}, $subsystems + note mod:truffle_core "The crate root (lib.rs, the pub use facade). A module not placed in a tier below lands here, so layered still ranks it." + note mod:truffle_core::egress "egress.rs documents the Node::loopback and egress_* API and lib.rs re-exports its types, yet the NetworkProvider egress verbs name them." + see "docs/rfcs/012-layered-architecture-redesign.md#4-the-node-api" + } + layer envelope "Layer 6 · Envelope" code L6 { contains mod:truffle_core::envelope } + layer session "Layer 5 · Session" code L5 { contains mod:truffle_core::session } + layer transport "Layer 4 · Transport" code L4 { contains mod:truffle_core::transport } + layer network "Layer 3 · Network" code L3 { + contains mod:truffle_core::network + note mod:truffle_core::network::tailscale::{bridge, header, protocol, sidecar} "RFC 012 Layers 1-2 on the Rust side: located in network/tailscale/ and private to TailscaleProvider (RFC 012 §3.6), so they rank with Layer 3." + } + } + layer sidecar "Layer 1 · Sidecar (Go process embedding tsnet)" code L1 { + contains path:packages/sidecar-slim/ + intent "A child process of TailscaleProvider; it talks to Rust over stdin/stdout JSON lines and the loopback bridge, never in code." + } + layer apple "Truffle Swift: the Apple engine" outside { + intent "A second engine for the same product on TailscaleKit, joined to truffle-core by the wire contract only." + see "docs/rfcs/024-truffle-swift.md#51-layering-maps-to-rfc-012-apple-runtime" + layer apple-support "Swift tests and the TailscaleKit scripts: they import or build the tiers below" { + contains path:apple/Tests/, path:apple/scripts/ + } + layer apple-examples "Host app example: host apps import only RFC 024 Layers 3-4" { contains path:apple/Examples/ } + layer apple-product "Truffle and TruffleSwiftUI (RFC 024 Layers 2-4)" { + contains path:apple/Sources/Truffle/, path:apple/Sources/TruffleSwiftUI/ + } + layer apple-tailscale "TruffleTailscale (RFC 024 Layers 0-1)" { contains path:apple/Sources/TruffleTailscale/ } + layer apple-vendor "Pinned libtailscale and patches (RFC 024 Layer 0)" { contains path:apple/Vendor/, path:apple/patches/ } + } + layer distribution "Re-export crate and sidecar distribution" outside { + contains crate:truffle, crate:truffle-sidecar, pkg:@vibecook/truffle-sidecar-* + } + layer tooling "Release tooling, CI, docs and the Go baseline bench" outside { + contains path:scripts/, path:.github/, path:.githooks/, path:docs/, path:packages/sidecar-slim/cmd/ + } +} diff --git a/atlas/rules.atlas b/atlas/rules.atlas new file mode 100644 index 00000000..77028855 --- /dev/null +++ b/atlas/rules.atlas @@ -0,0 +1,154 @@ +// Cross-cutting rules. A rule whose scope is one unpublished crate lives in that crate's root +// (`grep -rn '@atlas rule' crates benchmark`); the crates.io crates (truffle-core, truffle, truffle-sidecar) carry +// no atlas text on docs.rs, and truffle-cli has no crate doc, so their rules live here with `crate:` scopes. Every +// rule cites where truffle states it. Checks see production code only (no tests, cargo benches or cargo examples, +// cfg(test) code or dev-dependencies; the example apps under examples/ are production). +// Never weaken a rule to make it pass: fix the code. An `except "real reason"` or a wider `only` list +// changes the design: that is the user's call. + +rule layers-point-down "Each layer builds only on the layers below it" { + layered + see "docs/rfcs/012-layered-architecture-redesign.md#2-architecture-overview" + see "CLAUDE.md#architecture" +} + +rule core-depends-on-no-crate "truffle-core depends on no other workspace crate" { + only crate:truffle-core -> nothing on code, manifest + see "docs/rfcs/012-layered-architecture-redesign.md#2-architecture-overview" + see "crates/truffle/src/lib.rs#L38-L41" +} + +rule peers-from-layer-3 "Layer 3 is the source of truth for peers: no announce protocol" { + forbid text "device-announce" in crate:truffle-core + see "CLAUDE.md#architecture" + see "docs/rfcs/012-layered-architecture-redesign.md#35-layer-3-network" +} + +rule network-knows-no-upper-layer "Layer 3 knows no transport protocol, session, envelope or Node" { + deny layer:network -> layer:transport, layer:session, layer:envelope, layer:node-api + deny layer:network -> ext:tokio-tungstenite, ext:quinn + see "docs/rfcs/012-layered-architecture-redesign.md#35-layer-3-network" +} + +rule transport-knows-no-session "Layer 4 knows no peers, sessions or envelopes, and dials through Layer 3" { + deny layer:transport -> layer:session, layer:envelope, layer:node-api + require layer:transport -> layer:network + see "docs/rfcs/012-layered-architecture-redesign.md#34-layer-4-transport" +} + +rule transports-independent "Each transport implementation is independent" { + independent mod:truffle_core::transport::{websocket, tcp, udp, quic} + see "docs/rfcs/012-layered-architecture-redesign.md#34-layer-4-transport" +} + +rule session-knows-no-payload "Layer 5 knows no namespaces or envelopes; it consumes Layer 3 and delegates to Layer 4" { + deny layer:session -> layer:envelope, layer:node-api + require layer:session -> layer:network + require layer:session -> layer:transport + see "docs/rfcs/012-layered-architecture-redesign.md#33-layer-5-session" +} + +rule envelope-knows-no-transport "Layer 6 knows no transport and names no application namespace" { + deny layer:envelope -> layer:transport, layer:network + deny layer:envelope -> ext:tokio-tungstenite, ext:quinn + forbid text "\"mesh\"", "\"device-announce\"", "\"file-transfer\"" in layer:envelope + see "docs/rfcs/012-layered-architecture-redesign.md#32-layer-6-envelope" +} + +rule bridge-is-private "Layers 1-2 are invisible outside TailscaleProvider" { + deny *, !mod:truffle_core::network::tailscale -> mod:truffle_core::network::tailscale::{bridge, header, protocol, sidecar} + see "docs/rfcs/012-layered-architecture-redesign.md#36-layers-1-2-bridge--sidecar-internal-to-tailscaleprovider" + see "docs/rfcs/012-layered-architecture-redesign.md#10-success-criteria" +} + +rule apps-use-node-api "Applications reach the mesh through the Node API only, never a lower layer" { + deny layer:apps -> layer:network, layer:transport, layer:session, layer:envelope + see "docs/rfcs/012-layered-architecture-redesign.md#31-layer-7-application" + see "docs/rfcs/012-layered-architecture-redesign.md#4-the-node-api" +} + +rule bindings-wrap-core "The bindings wrap truffle-core's Node and no other workspace crate" { + only crate:truffle-napi, crate:truffle-tauri-plugin -> crate:truffle-core on code, manifest + see "docs/rfcs/015-binding-rewrite.md#3-current-node-api-what-bindings-must-wrap" +} + +rule meta-crate-is-thin "truffle is a thin re-export of truffle-core plus the bundled sidecar" { + only crate:truffle -> crate:truffle-core, crate:truffle-sidecar on code, manifest + see "CLAUDE.md#crates" + see "crates/truffle/src/lib.rs#L5-L7" +} + +rule namespaces-owned "Each subsystem owns its namespace" { + forbid text "\"ft\"" in crate:truffle-core, !mod:truffle_core::file_transfer + forbid text "\"ss:" in crate:truffle-core, !mod:truffle_core::synced_store + forbid text "\"chat\"" in crate:truffle-core + forbid text "\"ft\"", "'ft'", "\"ss:", "'ss:", "`ss:" in layer:apps + see "CLAUDE.md#architecture" + see "CLAUDE.md#key-conventions" + see "docs/rfcs/012-layered-architecture-redesign.md#31-layer-7-application" +} + +rule subsystems-send-typed "Subsystems send with send_typed and broadcast_typed, never another send method" { + forbid text "node.send(", "node.broadcast(", "allow(deprecated)" in $subsystems + forbid text ".send_json(", ".send_bytes(", ".broadcast_json(", ".broadcast_bytes(" in $subsystems + see "CLAUDE.md#architecture" +} + +rule request-reply-over-node "Request/reply is a free function over Node, not a Node method" { + deny mod:truffle_core::node -> mod:truffle_core::request_reply + require mod:truffle_core::request_reply -> mod:truffle_core::node + see "docs/rfcs/016-synced-store-and-request-reply.md#31-requestreply-utility" +} + +rule websocket-stays-internal "No native user-facing WebSocket: websocket.rs stays the internal envelope pipe" { + deny *, !crate:truffle-core -> mod:truffle_core::transport::websocket + see "docs/rfcs/021-raw-transport-js-api.md#12-decided-questions" +} + +rule no-http-stack-on-data-path "No axum, no hyper for the data path" { + forbid text "axum", "hyper" in crate:truffle-core, path:crates/truffle-cli/src/daemon/ + see "docs/rfcs/012-layered-architecture-redesign.md#94-why-raw-tcp-for-file-transfer-instead-of-http" + see "docs/rfcs/012-layered-architecture-redesign.md#10-success-criteria" + see "crates/truffle-cli/src/daemon/mod.rs#L7" +} + +rule native-is-internal "@vibecook/truffle-native is an implementation detail behind @vibecook/truffle" { + deny *, !pkg:@vibecook/truffle -> pkg:@vibecook/truffle-native + see "docs/API-STABILITY.md#from-10" + see "crates/truffle-napi/README.md#vibecooktruffle-native" +} + +rule tailscalekit-below-product "Only TruffleTailscale (RFC 024 Layers 0-1) imports TailscaleKit" { + forbid text "import TailscaleKit" in path:apple/Sources/Truffle/, path:apple/Sources/TruffleSwiftUI/, path:apple/Examples/ + see "apple/Package.swift#L4-L6" + see "docs/rfcs/024-truffle-swift.md#51-layering-maps-to-rfc-012-apple-runtime" + see "docs/rfcs/024-truffle-swift.md#53-package-layout-proposed" + see "apple/README.md#package-layout" +} + +rule egress-loopback-only "Every egress listener binds 127.0.0.1" { + forbid text "0.0.0.0", "\":0\"", "[::]" in file:packages/sidecar-slim/egress.go + require text "\"127.0.0.1:0\"" in file:packages/sidecar-slim/egress.go + see "docs/rfcs/026-egress.md#2-decision" +} + +rule bench-keys-from-env "Benchmark auth keys never come from argv, only from a key file, the environment or the app's sign-in form" { + require text "\"TRUFFLE_BENCH_AUTH_KEY\"", "\"TRUFFLE_TEST_AUTHKEY\"", "\"TS_AUTHKEY\"" in crate:truffle-bench + require text "env::var(\"TRUFFLE_TEST_AUTHKEY\")" in path:benchmark/engine/src/legacy/ + require text "env::var(\"TRUFFLE_BENCH_AUTH_KEY\")" in crate:truffle-bench-app + require text "os.Getenv(\"TRUFFLE_TEST_AUTHKEY\")" in file:packages/sidecar-slim/cmd/tsnet-bench/main.go + see "CLAUDE.md#benchmarks" + see "docs/rfcs/027-benchmark-hub.md#10-cli" + see "docs/tailscale-performance.md#measuring-the-tailscale-stack" + see "docs/rfcs/019-local-testing-and-benchmarking.md#5-environment-contract" +} + +rule bench-ui-renders-text-only "The bench app renders peer and report strings as text only" { + forbid text "dangerouslySetInnerHTML" in pkg:@vibecook/truffle-bench-app + see "docs/rfcs/027-benchmark-hub.md#114-identity-rules" +} + +rule windows-key-press "The TUI handles only KeyEventKind::Press (crossterm emits Press and Release on Windows)" { + require text "KeyEventKind::Press" in path:crates/truffle-cli/src/tui/ + see "CLAUDE.md#key-conventions" +} diff --git a/atlas/system.atlas b/atlas/system.atlas new file mode 100644 index 00000000..b541df33 --- /dev/null +++ b/atlas/system.atlas @@ -0,0 +1,25 @@ +atlas 1 + +// Truffle's design intent: what the code cannot say about itself, graded against the code by Project Atlas. +// Unpublished crate roots, single-anchor seams and flow-step functions carry their own `@atlas` lines +// (`grep -rn '@atlas' crates benchmark`). The crates.io crates (truffle-core, truffle, truffle-sidecar) carry no `//!` +// atlas text, so docs.rs shows none: their placement and rules live in this directory. +// Duties and the one command: CLAUDE.md "Atlas"; the authoring guide is the `atlas` skill. + +system truffle "Truffle" { + intent "Mesh networking for local-first apps on Tailscale: devices discover each other, message, sync state and move files with no central server." + thesis """ + RFC 012 layering: Network (L3) → Transport (L4) → Session (L5) → Envelope (L6) → the Node API; applications are Layer 7 and talk to Node only. + Layer 3 (WatchIPNBus) is the source of truth for peers, never transport connections. + The Go sidecar (tsnet) and its loopback bridge are private to TailscaleProvider: nothing above Layer 3 knows they exist. + FileTransfer and SyncedStore live in core, built on Node primitives, and own "ft" and "ss:{id}"; request/reply and the serve proxy also live in core. + One product, two engines: truffle-core + sidecar on desktop, Truffle Swift on TailscaleKit on Apple, joined only by the wire contract. + """ + see "CLAUDE.md#architecture" + see "docs/rfcs/012-layered-architecture-redesign.md#2-architecture-overview" + see "docs/rfcs/024-truffle-swift.md#52-runtime-split-one-api-two-engines" +} + +// truffle-core's subsystems on Node: file transfer, synced store and request/reply (the send_typed docs in node.rs) +// and the serve proxy (RFC 023 D2). Egress is Node API, not a subsystem: it sends no message. +let $subsystems = mod:truffle_core::{file_transfer, synced_store, request_reply, proxy} diff --git a/benchmark/app/src-tauri/src/lib.rs b/benchmark/app/src-tauri/src/lib.rs index dd1024d7..b3a0e832 100644 --- a/benchmark/app/src-tauri/src/lib.rs +++ b/benchmark/app/src-tauri/src/lib.rs @@ -11,6 +11,12 @@ //! //! Closing the window hides it (macOS); the hub keeps answering peers until //! the app quits, and quitting cancels runs and stops the hub within 5 s. +//! +//! @atlas layer bench · domain bench +//! @atlas rule bench-app-drives-the-engine "One engine: the desktop app is a shell that drives truffle-bench" { +//! require this -> crate:truffle-bench +//! see "docs/rfcs/027-benchmark-hub.md#2-goals" +//! } use std::sync::Arc; use std::time::Duration; diff --git a/benchmark/engine/src/lib.rs b/benchmark/engine/src/lib.rs index 7dced1fb..b4da064c 100644 --- a/benchmark/engine/src/lib.rs +++ b/benchmark/engine/src/lib.rs @@ -39,6 +39,16 @@ //! assert!(WorkloadParams::from_value(serde_json::json!({"workload": "tcp", "streams": 17})).is_err()); //! # Ok::<(), truffle_bench::AppError>(()) //! ``` +//! +//! @atlas layer bench · domain bench +//! @atlas rule bench-never-sets-hostname "truffle-bench never calls NodeBuilder::hostname, which breaks app-scoped discovery" { +//! forbid text ".hostname(" in this +//! see "docs/rfcs/027-benchmark-hub.md#53-configuration-and-sidecar-resolution" +//! } +//! @atlas rule legacy-transport-without-tbn1 "The legacy transport stays byte-compatible with tsnet-bench and never uses the TBN1 header" { +//! forbid text "TBN1", "STREAM_MAGIC", "StreamHeader", "write_header", "read_header" in path:benchmark/engine/src/legacy/ +//! see "docs/rfcs/027-benchmark-hub.md#91-moves" +//! } pub mod clock; pub mod config; diff --git a/crates/truffle-core/src/envelope/codec.rs b/crates/truffle-core/src/envelope/codec.rs index a85be7a7..a544093b 100644 --- a/crates/truffle-core/src/envelope/codec.rs +++ b/crates/truffle-core/src/envelope/codec.rs @@ -10,6 +10,10 @@ use super::{Envelope, EnvelopeError}; // EnvelopeCodec trait // --------------------------------------------------------------------------- +// @atlas contract envelope-codec "Pluggable envelope codec" { +// implemented-by crate:truffle-core +// see "docs/rfcs/012-layered-architecture-redesign.md#32-layer-6-envelope" +// } /// A codec that serializes and deserializes [`Envelope`]s. /// /// Implementations must be `Send + Sync` so they can be shared across diff --git a/crates/truffle-core/src/file_transfer/receiver.rs b/crates/truffle-core/src/file_transfer/receiver.rs index ce2405f4..fa00099a 100644 --- a/crates/truffle-core/src/file_transfer/receiver.rs +++ b/crates/truffle-core/src/file_transfer/receiver.rs @@ -308,6 +308,7 @@ pub fn spawn_receive_handler( }) } +// @atlas step file-push.answer /// Handle an incoming OFFER: forward to offer channel, wait for decision, /// then accept/reject accordingly. #[allow(clippy::too_many_arguments)] @@ -410,6 +411,8 @@ async fn handle_incoming_offer( } } +// @atlas step file-push.accept +// @atlas step file-push.verify /// Accept an incoming offer: open TCP listener, receive file streaming to /// disk, verify SHA-256, and send ACK. #[allow(clippy::too_many_arguments)] diff --git a/crates/truffle-core/src/file_transfer/sender.rs b/crates/truffle-core/src/file_transfer/sender.rs index 003a17ca..1258622c 100644 --- a/crates/truffle-core/src/file_transfer/sender.rs +++ b/crates/truffle-core/src/file_transfer/sender.rs @@ -25,6 +25,8 @@ use super::types::{ TransferResult, TransferTimings, }; +// @atlas step file-push.offer +// @atlas step file-push.stream /// Send a local file to a remote peer. /// /// Uses constant memory — the file is streamed in 64KB chunks for both diff --git a/crates/truffle-core/src/file_transfer/types.rs b/crates/truffle-core/src/file_transfer/types.rs index f1a7f35c..0a5a8893 100644 --- a/crates/truffle-core/src/file_transfer/types.rs +++ b/crates/truffle-core/src/file_transfer/types.rs @@ -12,6 +12,10 @@ use serde::{Deserialize, Serialize}; // Wire protocol messages (sent via WS namespace "ft") // --------------------------------------------------------------------------- +// @atlas contract ft-wire "File-transfer signaling on namespace \"ft\"" { +// produced-by crate:truffle-core each-variant +// see "docs/rfcs/014-file-transfer-core.md#2-goals" +// } /// File transfer signaling message types (sent via WS namespace "ft"). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] diff --git a/crates/truffle-core/src/network/mod.rs b/crates/truffle-core/src/network/mod.rs index db553549..5d5bcbac 100644 --- a/crates/truffle-core/src/network/mod.rs +++ b/crates/truffle-core/src/network/mod.rs @@ -32,6 +32,11 @@ pub enum NetworkTrust { UnverifiedLoopback, } +// @atlas contract network-provider "Layer 3: the swappable network provider" { +// implemented-by crate:truffle-core +// note sym:truffle_core::network::tailscale::TailscaleProvider "The only production implementor; every MockNetworkProvider is test code." +// see "docs/rfcs/012-layered-architecture-redesign.md#35-layer-3-network" +// } /// Provides network-level peer discovery and raw connectivity. /// /// The primary implementation is [`TailscaleProvider`](tailscale::TailscaleProvider) diff --git a/crates/truffle-core/src/network/tailscale/bridge.rs b/crates/truffle-core/src/network/tailscale/bridge.rs index 81b496ba..e5d22772 100644 --- a/crates/truffle-core/src/network/tailscale/bridge.rs +++ b/crates/truffle-core/src/network/tailscale/bridge.rs @@ -283,6 +283,7 @@ impl Bridge { } } + // @atlas step send.handoff /// Handle a single incoming bridge connection. async fn handle_connection( mut stream: TcpStream, diff --git a/crates/truffle-core/src/network/tailscale/provider.rs b/crates/truffle-core/src/network/tailscale/provider.rs index 48b4f85c..a3c73433 100644 --- a/crates/truffle-core/src/network/tailscale/provider.rs +++ b/crates/truffle-core/src/network/tailscale/provider.rs @@ -610,6 +610,7 @@ impl TailscaleProvider { } } + // @atlas step discovery.admit /// Spawn the background event processing loop that maps sidecar events /// to peer events and updates cached state. #[allow(clippy::too_many_arguments)] @@ -1393,6 +1394,7 @@ impl super::super::NetworkProvider for TailscaleProvider { self.dial_tcp_opts(addr, port, DialOpts::default()).await } + // @atlas step send.bridge async fn dial_tcp_opts( &self, addr: &str, diff --git a/crates/truffle-core/src/network/tailscale/sidecar.rs b/crates/truffle-core/src/network/tailscale/sidecar.rs index fea7a323..a7d0c174 100644 --- a/crates/truffle-core/src/network/tailscale/sidecar.rs +++ b/crates/truffle-core/src/network/tailscale/sidecar.rs @@ -482,6 +482,7 @@ impl GoSidecar { }); } + // @atlas step discovery.read fn spawn_stdout_reader( stdout: tokio::process::ChildStdout, event_tx: broadcast::Sender, diff --git a/crates/truffle-core/src/node.rs b/crates/truffle-core/src/node.rs index b88f0fc7..7908f542 100644 --- a/crates/truffle-core/src/node.rs +++ b/crates/truffle-core/src/node.rs @@ -467,6 +467,7 @@ impl Node { }); } + // @atlas step send.deliver /// Spawn a background task that reads incoming raw messages from the /// session layer, deserializes them as envelopes, and routes them to /// the global channel and per-namespace subscribers. @@ -865,6 +866,7 @@ impl Node { peer } + // @atlas step discovery.fanout /// Subscribe to peer change events (joined, left, connected, etc.). pub fn on_peer_change(&self) -> broadcast::Receiver { self.session.on_peer_change() @@ -1230,6 +1232,7 @@ impl Node { }) } + // @atlas step send.encode /// Send a namespaced message with an explicit `msg_type` and JSON payload. /// /// Unlike [`send`](Self::send), this method takes a pre-built @@ -1254,6 +1257,7 @@ impl Node { Ok(()) } + // @atlas step store-update.envelope /// Broadcast a namespaced message with an explicit `msg_type` and JSON /// payload to all connected peers. pub async fn broadcast_typed( @@ -1393,6 +1397,7 @@ impl Node { // ── Raw streams (Layer 4 direct) ───────────────────────────────────── + // @atlas step file-push.open /// Open a raw TCP stream to a peer on the given port. /// /// Resolves the peer ID to an IP address via the session's peer list, diff --git a/crates/truffle-core/src/request_reply.rs b/crates/truffle-core/src/request_reply.rs index e5cbb331..4991b507 100644 --- a/crates/truffle-core/src/request_reply.rs +++ b/crates/truffle-core/src/request_reply.rs @@ -44,6 +44,7 @@ pub enum RequestError { ChannelClosed, } +// @atlas step file-push.wait /// Send a namespaced message and wait for a reply that matches a predicate. /// /// 1. Subscribes to `namespace` (before sending, to avoid missing the reply). diff --git a/crates/truffle-core/src/session/mod.rs b/crates/truffle-core/src/session/mod.rs index eab644d8..d928646b 100644 --- a/crates/truffle-core/src/session/mod.rs +++ b/crates/truffle-core/src/session/mod.rs @@ -739,6 +739,7 @@ impl PeerRegistry { self.spawn_accept_loop().await; } + // @atlas step discovery.registry /// Spawn a task that subscribes to Layer 3 peer events and updates the /// internal peer list. fn spawn_peer_event_loop(&self) { @@ -1101,6 +1102,7 @@ impl PeerRegistry { self.event_tx.subscribe() } + // @atlas step send.route /// Send data to a specific peer. /// /// If no WebSocket connection exists to the peer, one is lazily @@ -1255,6 +1257,7 @@ impl PeerRegistry { Err(SessionError::UnknownPeer(peer_id.to_string())) } + // @atlas step store-update.fan /// Broadcast data to all peers with active WebSocket connections. /// /// Sends to all currently connected peers. Peers with no active @@ -1867,6 +1870,7 @@ async fn mark_disconnected_unless_replaced(ctx: &ConnCtx, peer_id: &str) { // Connection task — exclusively owns the WsFramedStream // --------------------------------------------------------------------------- +// @atlas step send.receive /// Spawn a background task that exclusively owns a `WsFramedStream`. /// /// The task uses `tokio::select!` to multiplex between: diff --git a/crates/truffle-core/src/synced_store/backend.rs b/crates/truffle-core/src/synced_store/backend.rs index 74a4faa0..b7249f5a 100644 --- a/crates/truffle-core/src/synced_store/backend.rs +++ b/crates/truffle-core/src/synced_store/backend.rs @@ -4,6 +4,10 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +// @atlas contract store-backend "SyncedStore persistence seam" { +// implemented-by crate:truffle-core +// see "docs/rfcs/016-synced-store-and-request-reply.md#persistence-optional" +// } /// Backend for persisting store data across restarts. /// /// Methods are synchronous — file I/O for small JSON payloads is fast enough diff --git a/crates/truffle-core/src/synced_store/mod.rs b/crates/truffle-core/src/synced_store/mod.rs index 6af5d56b..ca4122ee 100644 --- a/crates/truffle-core/src/synced_store/mod.rs +++ b/crates/truffle-core/src/synced_store/mod.rs @@ -170,6 +170,7 @@ impl SyncedStor // ── Write ─────────────────────────────────────────────────────── + // @atlas step store-update.set /// Update this device's data. Increments version and broadcasts. /// /// Rapid successive calls coalesce on the wire: peers are guaranteed to diff --git a/crates/truffle-core/src/synced_store/sync.rs b/crates/truffle-core/src/synced_store/sync.rs index b18aa682..7d38d3f8 100644 --- a/crates/truffle-core/src/synced_store/sync.rs +++ b/crates/truffle-core/src/synced_store/sync.rs @@ -18,6 +18,7 @@ use crate::session::PeerEvent; use super::types::{Slice, StoreEvent, SyncMessage}; use super::StoreInner; +// @atlas step store-update.push /// Spawn the background sync task. /// /// Returns a `JoinHandle` that can be aborted to stop the task. @@ -132,6 +133,7 @@ where }) } +// @atlas step store-update.apply /// Handle an incoming sync message from a peer. /// /// `from` is the sender's WhoIs-verified Tailscale id (the session's diff --git a/crates/truffle-core/src/synced_store/types.rs b/crates/truffle-core/src/synced_store/types.rs index a44d0955..7c57f812 100644 --- a/crates/truffle-core/src/synced_store/types.rs +++ b/crates/truffle-core/src/synced_store/types.rs @@ -26,6 +26,11 @@ pub struct Slice { // SyncMessage — wire protocol for store synchronization // --------------------------------------------------------------------------- +// @atlas contract sync-wire "SyncedStore wire messages on namespace \"ss:{id}\"" { +// produced-by crate:truffle-core each-variant +// see "docs/rfcs/016-synced-store-and-request-reply.md#wire-protocol" +// see "docs/rfcs/025-login-scoped-nodes.md#35-sender-bound-slices-tf-2" +// } /// Wire protocol messages sent on namespace `"ss:{store_id}"`. /// /// Uses `serde_json::Value` for the data field to keep this type non-generic. diff --git a/crates/truffle-core/src/transport/websocket.rs b/crates/truffle-core/src/transport/websocket.rs index d05fb918..47317160 100644 --- a/crates/truffle-core/src/transport/websocket.rs +++ b/crates/truffle-core/src/transport/websocket.rs @@ -475,6 +475,7 @@ async fn server_hello_exchange( impl StreamTransport for WebSocketTransport { type Stream = WsFramedStream; + // @atlas step send.dial async fn connect(&self, addr: &PeerAddr) -> Result { // Step 1: Dial TCP via Layer 3 let dial_addr = resolve_dial_addr(addr); @@ -537,6 +538,7 @@ impl StreamTransport for WebSocketTransport { )) } + // @atlas step send.accept async fn listen(&self) -> Result, TransportError> { let port = self.config.port; tracing::debug!(port, "ws: starting listener"); @@ -953,6 +955,7 @@ async fn heartbeat_loop( } impl FramedStream for WsFramedStream { + // @atlas step send.frame async fn send(&mut self, data: &[u8]) -> Result<(), TransportError> { if self.closed.load(Ordering::Acquire) { return Err(TransportError::ConnectionClosed( diff --git a/crates/truffle-napi/src/lib.rs b/crates/truffle-napi/src/lib.rs index 3605bb49..b1f2e833 100644 --- a/crates/truffle-napi/src/lib.rs +++ b/crates/truffle-napi/src/lib.rs @@ -5,6 +5,13 @@ //! //! - [`NapiNode`] — peer discovery, messaging, diagnostics //! - [`NapiFileTransfer`] — file send/receive/pull +//! +//! @atlas layer bindings · domain bindings +//! @atlas rule napi-pull-model "Raw-transport handles pass bytes only as the answer to a JS read, never a ThreadsafeFunction" { +//! forbid text "ThreadsafeFunction" in mod:truffle_napi::{raw_socket, udp_socket, quic} +//! see "docs/rfcs/021-raw-transport-js-api.md#12-decided-questions" +//! see "docs/rfcs/021-raw-transport-js-api.md#5-design-overview" +//! } pub mod egress; pub mod file_transfer; diff --git a/crates/truffle-tauri-plugin/src/lib.rs b/crates/truffle-tauri-plugin/src/lib.rs index ca6545cf..c1253a2e 100644 --- a/crates/truffle-tauri-plugin/src/lib.rs +++ b/crates/truffle-tauri-plugin/src/lib.rs @@ -57,6 +57,8 @@ //! - `truffle://file-transfer-event` — File transfer progress/completion //! - `truffle://file-offer` — Incoming file offers //! - `truffle://auth-required` — Authentication URL when login is needed +//! +//! @atlas layer bindings · domain bindings use std::collections::HashMap; use std::sync::Arc; From 5ce988cf4ad685a2910924aada089509e0e10cea Mon Sep 17 00:00:00 2001 From: James Yong Date: Fri, 16 Oct 2026 18:37:13 -0700 Subject: [PATCH 2/6] docs(atlas): baseline today's seven findings as known `atlas baseline` records the violations the atlas found when it landed, so `check` exits 0 and a violation an agent introduces shows as new. They stay violated, tagged known, and the guidance lists them with their counts: - layers-point-down, network-knows-no-upper-layer: the NetworkProvider egress verbs and tailscale::provider name egress types (node-api tier); - layers-point-down, transport-knows-no-session: transport::websocket imports session::hello (the RFC 017 hello gate); - apps-use-node-api: truffle-cli, truffle-napi, truffle-tauri-plugin and truffle-bench name Layer 3-6 types, many of them returned by Node itself; - sync-wire: SyncMessage::Clear is consumed but never produced; - the layered-architecture and synced-store decisions, through those. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/atlas/SKILL.md | 61 +++++++++++++++++------------------ CLAUDE.md | 4 +-- atlas/atlas.baseline.json | 18 +++++++++++ 3 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 atlas/atlas.baseline.json diff --git a/.claude/skills/atlas/SKILL.md b/.claude/skills/atlas/SKILL.md index 8384d207..3e41d3d6 100644 --- a/.claude/skills/atlas/SKILL.md +++ b/.claude/skills/atlas/SKILL.md @@ -45,7 +45,7 @@ cites them, never replaces them. Read CLAUDE.md "Atlas" first. The DSL spec is - contracts at their single anchor: `NetworkProvider`, `EnvelopeCodec`, `StoreBackend`, `FtMessage`, `SyncMessage`; - flow steps at the function that does the step (`// @atlas step send.route`). -- `atlas/atlas.baseline.json` — written only by `atlas baseline` (none yet: see Running it). +- `atlas/atlas.baseline.json` — written only by `atlas baseline`; it holds the 7 findings kept for the user (13 keys), recorded when the atlas landed. ## Running it @@ -101,29 +101,30 @@ TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- explain "$ anchors, domain `touches`, flow steps and hops) and 10 are violated instead of 7. - `baseline` (`TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- baseline "$PWD" --intent "$PWD/atlas"`) writes `atlas/atlas.baseline.json` with every violation of that run, so whatever your change - added becomes `known` too. None exists yet. It runs only when the user decides to accept that - run's violations, never to turn the check green or to hide a violation your change caused. + added becomes `known` too. The committed one holds the 7 findings kept for the user. It runs + again only when the user decides to accept that run's violations, never to turn the check green + or to hide a violation your change caused. ## Reading `check` -An excerpt of today's run (rebased on 0.7.18, atlas changes uncommitted, no baseline yet; the -header's hash and dirty count change with every commit); `…` marks what is left out here. Claims -sort by severity, then by the file and line that declare them: today the decisions -(`atlas/decisions.atlas`), then the rules (`atlas/rules.atlas`), then the in-code contract: +An excerpt of the run on 0.7.18 with this atlas and its baseline committed (the header's hash +changes with every commit); `…` marks what is left out here. Claims sort by severity, then by the +file and line that declare them: today the decisions (`atlas/decisions.atlas`), then the rules +(`atlas/rules.atlas`), then the in-code contract: ``` -atlas check · Truffle @ 65342ba (+12 dirty) · 520 claims - 473 verified · 40 asserted · 7 violated (7 new, 0 known) · 0 dangling · 0 ambiguous · 0 stale +atlas check · Truffle @ 0c08350 · 520 claims + 473 verified · 40 asserted · 7 violated (0 new, 7 known) · 0 dangling · 0 ambiguous · 0 stale coverage: layers 100.0% · domains 100.0% of 88,816 prod lines · rules 23 pass, 4 fail, 0 unchecked -VIOLATED · new (7) +VIOLATED · known (in the baseline) (7) decision:layered-architecture decision layered-architecture "Layers 3-6 behind one Node API; applications are Layer 7" code RFC-0… at atlas/decisions.atlas:36 · decision · basis none rule:layers-point-down violated rule:network-knows-no-upper-layer violated rule:transport-knows-no-session violated rule:apps-use-node-api violated - → Accepted, but rule:layers-point-down, rule:network-knows-no-upper-layer, … are failing: the code has drifted from this decision. + → Accepted, but rule:layers-point-down, rule:network-knows-no-upper-layer, rule:transport-knows-no-session, rule:apps-use-node-api are failing: the code has drifted from this decision. decision:synced-store decision synced-store "SyncedStore and a request/reply helper in core" code RFC-016 status accepted at atlas/decisions.atlas:82 · decision · basis none contract:sync-wire violated @@ -148,17 +149,16 @@ ASSERTED (40): not listed; --all lists them VERIFIED (473): not listed; --all lists them -7 new violations (7 error, 0 warn/info): exit 1 - fix the code, add `except "reason"`, or record today's state with `atlas baseline` +ok: no new violations ``` -- The last line is Atlas's generic advice. Never take its `except` or `atlas baseline` yourself: - an `except` changes the design and the baseline is the user's decision (Running it, Fixing). -- Until the user runs `atlas baseline`, this is what a clean tree prints: exit 1 with exactly these - 7 claims under `VIOLATED · new`. If the user baselines this state, `atlas baseline` writes 13 - keys (3 claim ids and 10 rule pairs); the same 7 claims then print under - `VIOLATED · known (in the baseline) (7)`, the summary reads `7 violated (0 new, 7 known)`, the run - ends with `ok: no new violations`, and the check exits 0. +- With a new violation the run ends instead with `N new violations (…): exit 1` and Atlas's generic + advice, "fix the code, add `except "reason"`, or record today's state with + `atlas baseline`". Never take its `except` or `atlas baseline` yourself: an `except` changes the + design and the baseline is the user's decision (Running it, Fixing). +- This is what a clean tree prints: the 7 findings kept for the user under + `VIOLATED · known (in the baseline) (7)`, `7 violated (0 new, 7 known)`, `ok: no new violations`, + exit 0. The baseline holds 13 keys (3 claim ids and 10 rule pairs). - Sections in order: VIOLATED · new, VIOLATED · known (in the baseline), 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`. A claim listed @@ -167,17 +167,15 @@ VERIFIED (473): not listed; --all lists them (`file:line source`) and `→` the remedy. `--all` lists verified and asserted claims with those first two lines only, no counts, so `explain ''` (each term prints `→ n`) to see what a verified glob matches. A glob that matches nothing dangles. -- Exit 1 only for a violated claim of severity `error` that is not known; with no baseline, every - violated claim is new. DANGLING, STALE, AMBIGUOUS and `warn` rules (none today) do not fail the +- Exit 1 only for a violated claim of severity `error` that is not known. DANGLING, STALE, AMBIGUOUS and `warn` rules (none today) do not fail the check, but you still leave 0 dangling and 0 ambiguous. One exception: a dangling rule, contract or flow named in a decision's `confirmed-by` makes that decision violated (exit 1), so fix the selector. `layered-architecture` and `synced-store` are violated already, so a dangling member of theirs (any of the 10 rules in layered-architecture's `confirmed-by`, or `sync-wire` and `request-reply-over-node`) shows only under DANGLING and in the decision's evidence - (`rule:bridge-is-private dangling`), not in the exit code: today the check exits 1 anyway, and - after a baseline the decision's only key is its claim id, so it stays known and exits 0 (Atlas - `grade/decisions.ts`, `main.keys = [main.id]`). -- **Known or new, once a baseline exists** (Atlas `packages/aggregator/src/grade/rules.ts`, + (`rule:bridge-is-private dangling`), not in the exit code: the decision's only key is its claim + id, so it stays known and the check exits 0 (Atlas `grade/decisions.ts`, `main.keys = [main.id]`). +- **Known or new** (Atlas `packages/aggregator/src/grade/rules.ts`, `grade/claims.ts` `evidenceKey`, `packages/aggregator/src/baseline.ts`). A violated claim is known only when every one of its keys is in the baseline. Its keys: - a dependency rule (`layered`, `deny`, `only`): one per evidence item, `rule:||`. @@ -223,9 +221,8 @@ VERIFIED (473): not listed; --all lists them last commit of the file holding the statement (the `.atlas` file, or the source file holding the `@atlas` line); an uncommitted file has none, so commit atlas changes with the code they describe. - The 7 findings kept for the user, in output order, with their counts (each claim's `→` line - gives its count). Your change is clean only when `VIOLATED · new` lists exactly these 7 claims - with these counts (after a baseline: nothing new, and these 7 under known with these counts); any - other violated claim, or a higher count, is yours. A further import between modules or packages + gives its count). Your change is clean only when nothing is new and these 7 are known with these + counts; any other violated claim, or a higher count, is yours. A further import between modules or packages already listed only lengthens a claim's evidence, so the summary line and the exit code stay the same: compare the counts. Do not edit a rule, a decision or a module's tier for them, and do not change their code either (no deleting `Clear`, no moving the hello gate or the egress types) @@ -449,8 +446,8 @@ VERIFIED (473): not listed; --all lists them - **New benchmark workload** `benchmark/engine/src/workloads/foo.rs`: it inherits `bench` and the `bench` domain from truffle-bench's root; add `touches mod:truffle_bench::workloads::foo` to the domain it measures (`atlas/domains.atlas`). Its truffle-core imports below the Node API raise - `apps-use-node-api`'s count. That claim is violated already (and, once a baseline exists, known - under the truffle-bench package pair), so the exit code does not show it: report the new count to + `apps-use-node-api`'s count. That claim is violated already and known under the + truffle-bench package pair, so the exit code does not show it: report the new count to the user and do not call the change clean. A new type joins `bench-app-types` when RFC 027 §7's fixture list names it. - **New NAPI type** for a core type: `napi-projection` checks every core type in its `at` list @@ -461,7 +458,7 @@ VERIFIED (473): not listed; --all lists them - **Splitting a big file** (node.rs, provider.rs, session/mod.rs): `// @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 - flows' steps are still verified. Once a baseline exists, moving `provider.rs`'s `use crate::egress` + flows' steps are still verified. Moving `provider.rs`'s `use crate::egress` into a child module also turns `layers-point-down` and `network-knows-no-upper-layer` new with the same counts (exit 1): their keys name the module (Known or new). Tell the user; it is not your finding. diff --git a/CLAUDE.md b/CLAUDE.md index aa67eb0e..b1b4a945 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,10 +99,10 @@ pnpm --filter @vibecook/truffle-bench-app app:dev # the - **Where**: truffle-napi, truffle-tauri-plugin, truffle-bench and truffle-bench-app end their root `//!` doc (the benchmark crates' `lib.rs`) with their placement; seams and flow steps carry `// @atlas` lines (`grep -rn '@atlas' crates benchmark`). The crates.io crates (truffle-core, truffle, truffle-sidecar) carry no `//!` atlas text, so docs.rs shows none: they, truffle-cli (no crate doc), truffle-core's module tiers, the npm packages (`@vibecook/truffle-bench-app` included) and the directories (`benchmark/schema/` included) are placed in `atlas/layers.atlas` and `atlas/domains.atlas`. - **New code**: place a new crate, truffle-core module, npm package or top-level directory in a layer and a domain in the same change. A truffle-core module you forget lands in node-api and `mesh` and nothing reports it: `explain` it. -- **Renames**: update every selector that names the code, in `atlas/` and in other code's `@atlas` lines (brace forms like `mod:truffle_core::{node, identity, egress}` hide from grep). `check` must then show 0 DANGLING. Once a baseline exists, moving the code of a known finding also makes it new (the skill's "Known or new"). +- **Renames**: update every selector that names the code, in `atlas/` and in other code's `@atlas` lines (brace forms like `mod:truffle_core::{node, identity, egress}` hide from grep). `check` must then show 0 DANGLING. Moving the code of a known finding makes it new (the skill's "Known or new"). - **Never weaken a rule to make it pass**: fix the code. Widening an `only` list, adding an `except`, moving a module to another tier 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`, and `atlas baseline` runs only when the user decides to accept today's violations, never to turn the check green. - **Check** from the repo root after changing crates, modules, packages, dependencies, `atlas/`, or a heading in CLAUDE.md, a README or `docs/` (`see` cites them by slug): `TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "${ATLAS:-$HOME/Projects/project100/protos/project-atlas}" run -s atlas -- check "$PWD" --intent "$PWD/atlas"`. It runs `cargo doc` over the workspace (a build, build scripts included); the variable keeps truffle-sidecar's build script offline, as CI's lint job does (`cargo doc` never runs the sidecar). Atlas is a private tool: if `$ATLAS/package.json` is missing (the command exits 254 with no output), skip the check, install nothing, and say so. When builds are off-limits, run the syntax-only check: `ATLAS="${ATLAS:-$HOME/Projects/project100/protos/project-atlas}"; "$ATLAS/node_modules/.bin/tsx" "$ATLAS/packages/engine-intent/src/cli.ts" atlas crates benchmark packages examples`. -- **Reading it**: exit 1 = a new error violation. Until the user runs `atlas baseline`, the check exits 1 on a clean tree with 7 findings kept for the user (listed in the skill). Your change is clean only when VIOLATED shows those same 7 claims with the same counts (the exit code is 1 either way, and a further edge into one of the 7 only raises its count); do not change code, rules or tiers for them unless asked. DANGLING does not fail the check but must be fixed, and a dangling rule, contract or flow named in a decision's `confirmed-by` makes that decision violated (exit 1; `layered-architecture` and `synced-store` are violated already, so for their members only DANGLING and the decision's evidence show it): fix the selector. +- **Reading it**: exit 1 = a new error violation. `atlas/atlas.baseline.json` records the 7 findings kept for the user (listed in the skill), so a clean tree prints them under `VIOLATED · known` and exits 0. Known means per package or module pair: a further import between a pair already listed stays known, so compare each known claim's count with the skill's list. Do not change code, rules or tiers for them unless asked. DANGLING does not fail the check but must be fixed, and a dangling rule, contract or flow named in a decision's `confirmed-by` makes that decision violated (exit 1; for `layered-architecture` and `synced-store`, already known, only DANGLING and the decision's evidence show it): fix the selector. - **Commits**: an atlas-only change is `docs:` or `chore:`, never `feat:`/`fix:`, which make Release Please cut a release. Staged `@atlas` lines still trigger the pre-commit hook: a staged truffle-napi `src/*.rs`, `Cargo.toml` or `build.rs` runs a release `napi build` (and fails if pnpm is missing); a staged truffle-core `lib.rs`/`node.rs` or truffle `lib.rs`/`Cargo.toml` runs `cargo test -p truffle --doc`; every staged `.rs` gets rustfmt (the skill lists all 7 checks). Never put `@atlas` in a `///` line: rustdoc renders it, and NAPI-RS copies item docs into `index.d.ts`. ## RFCs diff --git a/atlas/atlas.baseline.json b/atlas/atlas.baseline.json new file mode 100644 index 00000000..7e5f0495 --- /dev/null +++ b/atlas/atlas.baseline.json @@ -0,0 +1,18 @@ +{ + "schema": "atlas.baseline/1", + "violations": [ + "claim:contract:sync-wire/contract/565c34d2", + "claim:decision:layered-architecture/decision/74afec74", + "claim:decision:synced-store/decision/6487f263", + "rule:apps-use-node-api|pkg:cargo/truffle-bench|pkg:cargo/truffle-core", + "rule:apps-use-node-api|pkg:cargo/truffle-cli|pkg:cargo/truffle-core", + "rule:apps-use-node-api|pkg:cargo/truffle-napi|pkg:cargo/truffle-core", + "rule:apps-use-node-api|pkg:cargo/truffle-tauri-plugin|pkg:cargo/truffle-core", + "rule:layers-point-down|mod:rs/truffle_core::network::tailscale::provider|mod:rs/truffle_core::egress", + "rule:layers-point-down|mod:rs/truffle_core::network|mod:rs/truffle_core::egress", + "rule:layers-point-down|mod:rs/truffle_core::transport::websocket|mod:rs/truffle_core::session::hello", + "rule:network-knows-no-upper-layer|mod:rs/truffle_core::network::tailscale::provider|mod:rs/truffle_core::egress", + "rule:network-knows-no-upper-layer|mod:rs/truffle_core::network|mod:rs/truffle_core::egress", + "rule:transport-knows-no-session|mod:rs/truffle_core::transport::websocket|mod:rs/truffle_core::session::hello" + ] +} From b950b5804e72a9b992abbb1e2b20ffb02377af76 Mon Sep 17 00:00:00 2001 From: James Yong Date: Sat, 24 Oct 2026 22:53:38 -0700 Subject: [PATCH 3/6] docs(atlas): sanction Node::bridge_stats in bridge-is-private Protocol 10 (#218) added Node::bridge_stats(), which returns BridgeStats from the private network::tailscale::bridge module, and made the tailscale module doc list BridgeStats among its public types. On 0.7.20 that is a new bridge-is-private violation (node -> tailscale::bridge). The except names the symbol, so it sanctions only edges that name BridgeStats; any other bridge type reaching above Layer 3 still fails. It cites the module doc line that makes BridgeStats public. Co-Authored-By: Claude Opus 5.5 --- atlas/rules.atlas | 2 ++ 1 file changed, 2 insertions(+) diff --git a/atlas/rules.atlas b/atlas/rules.atlas index 77028855..28f03ad9 100644 --- a/atlas/rules.atlas +++ b/atlas/rules.atlas @@ -57,8 +57,10 @@ rule envelope-knows-no-transport "Layer 6 knows no transport and names no applic rule bridge-is-private "Layers 1-2 are invisible outside TailscaleProvider" { deny *, !mod:truffle_core::network::tailscale -> mod:truffle_core::network::tailscale::{bridge, header, protocol, sidecar} + except mod:truffle_core::node -> sym:truffle_core::network::tailscale::BridgeStats "Node::bridge_stats exposes the bridge's delivery and drop counters (protocol 10, #218), which the tailscale module documents as public" see "docs/rfcs/012-layered-architecture-redesign.md#36-layers-1-2-bridge--sidecar-internal-to-tailscaleprovider" see "docs/rfcs/012-layered-architecture-redesign.md#10-success-criteria" + see "crates/truffle-core/src/network/tailscale/mod.rs#L16-L17" } rule apps-use-node-api "Applications reach the mesh through the Node API only, never a lower layer" { From ddb2e818503d2a067a683f3b50f04fdcd75e171f Mon Sep 17 00:00:00 2001 From: James Yong Date: Sat, 24 Oct 2026 22:54:06 -0700 Subject: [PATCH 4/6] docs(atlas): confirm seven more decisions; two rules check what their titles claim Eight accepted decisions had no confirmed-by, so drift in them could not go red. Seven now have rules that fail if the code reverses them: - rust-core-go-shim (RFC 003): shim-speaks-no-mesh-protocol - cli-daemon (RFC 010): daemon-ipc-is-local, commands-are-daemon-clients, no-star-or-election - tui-is-the-daemon (RFC 013): tui-hosts-the-daemon, commands-are-daemon-clients - app-namespacing (RFC 017): hostname-from-identity, apps-isolated - peer-handles (RFC 022): peer-handles-interned, device-id-honest, from-is-the-connection - http-serving (RFC 023): serve-engine-in-go, proxy-discovery-inert, no-fixed-sidecar-listener - login-scoped-nodes (RFC 025): login-gate-at-admission-and-hello, store-slices-bound-to-sender one-release-unit stays unconfirmed. release-lockstep and sidecar-fail-closed check the release scripts, but truffle update, the auto-update and the install scripts install the CLI archive's sidecar with no checksum, and truffle-sidecar falls back to PATH at runtime; a comment above the decision says so. bench-keys-from-env now forbids a key on argv in every bench argv parser (a clap field, long name or env binding, the app's argv, the Go baseline's flags). windows-key-press requires the Press filter in both key readers and forbids other key kinds and other readers. The agent -> daemon spawn link also anchors daemon/client.rs, whose auto_up path starts the daemon too. `let $shim` names the Go shim's production code. Co-Authored-By: Claude Opus 5.5 --- atlas/components.atlas | 4 +- atlas/decisions.atlas | 15 +++- atlas/rules.atlas | 167 ++++++++++++++++++++++++++++++++++++++++- atlas/system.atlas | 3 + 4 files changed, 184 insertions(+), 5 deletions(-) diff --git a/atlas/components.atlas b/atlas/components.atlas index 58612e74..068f14d3 100644 --- a/atlas/components.atlas +++ b/atlas/components.atlas @@ -90,8 +90,8 @@ agent -> daemon "JSON-RPC 2.0, one object per line, over a Unix socket or named witness "truffle.sock" witness "push_file" } -agent -> daemon "truffle up re-execs itself detached as `up --foreground`" via spawn { - at file:crates/truffle-cli/src/commands/up.rs +agent -> daemon "truffle up, and any command when auto_up is on, re-execs the binary detached as `up --foreground`" via spawn { + at file:crates/truffle-cli/src/commands/up.rs, file:crates/truffle-cli/src/daemon/client.rs witness "--foreground" } tauri-guest -> tauri "invoke plugin:truffle commands; listen to truffle:// events" via ipc { diff --git a/atlas/decisions.atlas b/atlas/decisions.atlas index 8cb5f2cc..83b52e17 100644 --- a/atlas/decisions.atlas +++ b/atlas/decisions.atlas @@ -5,6 +5,7 @@ decision rust-core-go-shim "Rust core over a thin Go shim that keeps tsnet" code RFC-003 status accepted date 2026-02-24 { because "The Go shim keeps only tsnet lifecycle and TCP bridging; its no-HTTP clause was later reversed by RFC 023 (the serve engine) and RFC 026 (egress)." affects crate:truffle-core, path:packages/sidecar-slim/ + confirmed-by shim-speaks-no-mesh-protocol see "docs/rfcs/003-rust-rewrite.md#6-what-stays-go" see "docs/rfcs/023-http-serving.md#12-decisions" } @@ -23,7 +24,8 @@ decision binary-frames-009 "Binary frame header and a typed Namespace enum" code decision cli-daemon "A CLI daemon over a local socket; no STAR topology or election" code RFC-010 status accepted date 2026-03-20 { because "The header still says Proposed; the daemon, IPC and JSON-RPC protocol shipped in truffle-cli." - affects crate:truffle-cli + affects crate:truffle-cli, crate:truffle-core + confirmed-by daemon-ipc-is-local, commands-are-daemon-clients, no-star-or-election see "docs/rfcs/010-p2p-and-cli-redesign.md#3-cli-daemon-architecture" } @@ -60,6 +62,7 @@ decision raw-tcp-for-files "Raw TCP for bulk data, WebSocket for messages" code decision tui-is-the-daemon "The TUI runs the daemon in-process; agents use one-shot commands" code RFC-013 status accepted date 2026-03-28 { affects crate:truffle-cli + confirmed-by tui-hosts-the-daemon, commands-are-daemon-clients see "docs/rfcs/013-tui-redesign.md#3-architecture-decision-tui--foreground-daemon" see "docs/rfcs/013-tui-redesign.md#11-agent-mode-one-shot-commands" } @@ -90,6 +93,7 @@ decision synced-store "SyncedStore and a request/reply helper in core" code RFC- decision app-namespacing "appId isolation and the truffle- hostname prefix" code RFC-017 status accepted date 2026-04-11 { affects mod:truffle_core::{identity, session::hello}, path:apple/Sources/Truffle/Identity/ affects mod:truffle_core::transport::websocket, mod:truffle_core::network::tailscale::provider + confirmed-by hostname-from-identity, apps-isolated see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" see "docs/rfcs/017-identity-and-namespacing.md#8-wire-protocol-peer-metadata-exchange" } @@ -112,6 +116,7 @@ decision raw-transport-js "Raw TCP, UDP and QUIC in the JS API with pull-model h decision peer-handles "Peer handles; an honest deviceId; WhoIs attribution" code RFC-022 status accepted date 2026-07-09 { because "The header says in progress and unreleased; it shipped in 0.6.0. Phase D (hostinfo) has not started." affects mod:truffle_core::session, mod:truffle_napi::peer, file:packages/core/src/peer.ts + confirmed-by peer-handles-interned, device-id-honest, from-is-the-connection see "docs/rfcs/022-peer-handle-api.md#61-peer-handle" see "docs/rfcs/022-peer-handle-api.md#75-message-path" } @@ -119,6 +124,7 @@ decision peer-handles "Peer handles; an honest deviceId; WhoIs attribution" code decision http-serving "Serve over the tailnet: JS handlers over the bridge, other targets in the Go engine" code RFC-023 status accepted date 2026-07-12 { because "Phases 1-3 shipped; Funnel (4) and service discovery (5) are future, and proxy/discovery.rs stays inert until then (D10)." affects mod:truffle_core::proxy, file:packages/core/src/{serve, http}.ts, path:packages/sidecar-slim/ + confirmed-by serve-engine-in-go, proxy-discovery-inert, no-fixed-sidecar-listener see "docs/rfcs/023-http-serving.md#5-design-overview" see "docs/rfcs/023-http-serving.md#12-decisions" } @@ -133,6 +139,7 @@ decision swift-engine "Truffle Swift: an Apple-native engine on TailscaleKit" co decision login-scoped-nodes "A per-node login allow-list as the mesh boundary" code RFC-025 status accepted date 2026-09-16 { affects mod:truffle_core::{network::login_allow, session::hello, synced_store} affects mod:truffle_core::transport::websocket, mod:truffle_core::network::tailscale::provider + confirmed-by login-gate-at-admission-and-hello, store-slices-bound-to-sender see "docs/rfcs/025-login-scoped-nodes.md#2-decision" see "docs/rfcs/025-login-scoped-nodes.md#8-decisions" } @@ -161,8 +168,12 @@ decision public-surfaces "Which artifacts are stable surfaces, and which are imp see "docs/API-STABILITY.md#from-10" } +// Not confirmed by release-lockstep and sidecar-fail-closed on purpose: `truffle update`, the auto-update and +// scripts/install.{sh,ps1} install the CLI archive's sidecar-slim with no checksum, and truffle-sidecar's resolver +// falls back to "sidecar-slim" on PATH at runtime, which the release contract allows only through its two escape +// hatches. Confirm the decision once those paths fail closed or the contract is narrowed (found 2026-10-24). decision one-release-unit "Every public artifact ships at one version; sidecar binaries fail closed" status accepted { - because "scripts/check-release-versions.mjs enforces the lockstep; manifests are config, so Atlas cannot see it. The unpublished benchmark hub keeps its own versions." + because "scripts/check-release-versions.mjs enforces the lockstep; manifests are config, so the rules check the release scripts, not the versions. The unpublished benchmark hub keeps its own versions." affects path:scripts/, crate:truffle-sidecar see "docs/RELEASING.md#release-contract" see "docs/RELEASING.md#the-benchmark-hub-is-not-released" diff --git a/atlas/rules.atlas b/atlas/rules.atlas index 28f03ad9..456b2bc7 100644 --- a/atlas/rules.atlas +++ b/atlas/rules.atlas @@ -24,6 +24,13 @@ rule peers-from-layer-3 "Layer 3 is the source of truth for peers: no announce p see "docs/rfcs/012-layered-architecture-redesign.md#35-layer-3-network" } +rule no-star-or-election "No primary election and no STAR routing through a hub: the mesh is peer to peer" { + forbid text "Election", "DeviceRole", "ViaPrimary" in crate:truffle-core + forbid text "\"election-", "\"route-message\"", "\"route-broadcast\"" in crate:truffle-core + see "docs/rfcs/010-p2p-and-cli-redesign.md#21-what-gets-deleted" + see "docs/rfcs/010-p2p-and-cli-redesign.md#11-rationale-why-not-keep-election-as-optional" +} + rule network-knows-no-upper-layer "Layer 3 knows no transport protocol, session, envelope or Node" { deny layer:network -> layer:transport, layer:session, layer:envelope, layer:node-api deny layer:network -> ext:tokio-tungstenite, ext:quinn @@ -80,6 +87,28 @@ rule meta-crate-is-thin "truffle is a thin re-export of truffle-core plus the bu see "crates/truffle/src/lib.rs#L5-L7" } +// Manifests and Cargo.lock are config to Atlas, so these two rules check the release scripts that police them. +rule release-lockstep "The release check holds the crates, the npm packages and both lockfiles to one version" { + require text "'packages/core/package.json'", "'packages/react/package.json'", "'crates/truffle-napi/package.json'" in file:scripts/check-release-versions.mjs + require text "'crates/truffle-napi/npm/", "'npm/truffle-sidecar-" in file:scripts/check-release-versions.mjs + require text "'crates/truffle/Cargo.toml'", "'crates/truffle-core/Cargo.toml'", "'crates/truffle-sidecar/Cargo.toml'" in file:scripts/check-release-versions.mjs + require text "'crates/truffle-cli/Cargo.toml'", "'crates/truffle-napi/Cargo.toml'" in file:scripts/check-release-versions.mjs + require text "read('Cargo.lock')", "'crates/truffle-napi/package-lock.json'" in file:scripts/check-release-versions.mjs + see "docs/RELEASING.md#release-contract" + see "docs/RELEASING.md#normal-release" +} + +rule sidecar-fail-closed "The release gate pins a SHA-256 for every tsnet-sidecar asset, and the npm installer refuses any other" { + require text "validateReleaseChecksums(" in file:scripts/check-release-versions.mjs + require text "/^[0-9a-f]{64}$/", "sidecar checksum maps disagree" in file:scripts/release-checksums.mjs + require text "[checksums] missing asset" in file:scripts/update-sidecar-checksums.mjs + require text "expected = requireExpectedChecksum(", "assertChecksum(actual, expected, `" in file:packages/core/scripts/postinstall.cjs + require text "Refusing to download an unverified executable" in file:packages/core/scripts/postinstall.cjs + see "docs/RELEASING.md#release-contract" + see "packages/core/scripts/postinstall.cjs#L6-L14" + see "scripts/update-sidecar-checksums.mjs#L46-L47" +} + rule namespaces-owned "Each subsystem owns its namespace" { forbid text "\"ft\"" in crate:truffle-core, !mod:truffle_core::file_transfer forbid text "\"ss:" in crate:truffle-core, !mod:truffle_core::synced_store @@ -107,6 +136,69 @@ rule websocket-stays-internal "No native user-facing WebSocket: websocket.rs sta see "docs/rfcs/021-raw-transport-js-api.md#12-decided-questions" } +// Identity, peers and the login gate (RFC 017, 022, 025), in both engines. Text checks scoped to a `sym:` search +// only that function: renaming it dangles the rule, so fix the selector with the rename. +rule hostname-from-identity "By default a node's hostname is truffle-{appId}-{slug}, built by each engine's identity code" { + require mod:truffle_core::node -> sym:truffle_core::identity::tailscale_hostname + require text "\"truffle-\"" in mod:truffle_core::identity + require text "\"truffle-\"" in file:apple/Sources/Truffle/Identity/Slug.swift + see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" + see "docs/rfcs/024-truffle-swift.md#72-tailscale-hostname" + see "crates/truffle-core/src/node.rs#L2059-L2060" +} + +rule apps-isolated "Discovery admits only truffle-{appId}- hostnames, and the hello refuses another app's id" { + require text "\"truffle-{app_id}-\"" in sym:truffle_core::network::tailscale::provider::is_app_peer + require text "TransportError::AppMismatch" in sym:truffle_core::transport::websocket::validate_hello + require text "guard Hostname.isAppPeer(" in file:apple/Sources/Truffle/Mesh/MeshNode.swift + require text "HelloValidationError.appMismatch(" in file:apple/Sources/Truffle/Wire/HelloEnvelope.swift + see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" + see "docs/rfcs/017-identity-and-namespacing.md#8-wire-protocol-peer-metadata-exchange" + see "crates/truffle-core/src/network/tailscale/provider.rs#L887-L890" +} + +rule device-id-honest "deviceId is a ULID or absent, never a Tailscale id in its place" { + require text "pub device_id: Option," in sym:truffle_core::Peer + require text "distinct from tailscale_id" in sym:truffle_core::transport::websocket::validate_hello + require text "get deviceId(): string | null" in file:packages/core/src/peer.ts + see "docs/rfcs/022-peer-handle-api.md#61-peer-handle" + see "docs/rfcs/022-peer-handle-api.md#73-projection-honesty" + see "crates/truffle-core/src/transport/websocket.rs#L259-L260" +} + +rule from-is-the-connection "Inbound messages are attributed to the connection's WhoIs-verified Tailscale id, never the envelope's own from" { + require text "from: from_tailscale_id" in sym:truffle_core::session::spawn_connection_task + require text "from: msg.from" in sym:truffle_core::node::Node::spawn_envelope_router + forbid text "envelope.from" in crate:truffle-core + require text "getByTailscaleId(raw.from)" in file:packages/core/src/create-mesh-node.ts + see "docs/rfcs/022-peer-handle-api.md#75-message-path" + see "crates/truffle-core/src/session/mod.rs#L1914-L1915" +} + +rule peer-handles-interned "The JS mesh node hands out Peer handles through PeerRegistry" { + require file:packages/core/src/create-mesh-node.ts -> sym:packages/core/src/peer.ts#PeerRegistry + see "docs/rfcs/022-peer-handle-api.md#76-same-instance-guarantee" + see "packages/core/src/peer.ts#L7-L9" +} + +rule login-gate-at-admission-and-hello "The login allow-list gates Layer 3 admission and the inbound hello, in both engines" { + require mod:truffle_core::network::tailscale::provider -> sym:truffle_core::network::login_allow::login_allowed + require mod:truffle_core::transport::websocket -> sym:truffle_core::network::login_allow::login_allowed + require text "guard LoginGlob.allowed(" in file:apple/Sources/Truffle/Mesh/MeshNode.swift + require text "guard LoginGlob.allowed(" in file:apple/Sources/Truffle/Session/Handshake.swift + see "docs/rfcs/025-login-scoped-nodes.md#33-layer-3--the-login-is-a-peer-fact" + see "docs/rfcs/025-login-scoped-nodes.md#34-layer-45--the-hello-gate" + see "docs/rfcs/024-truffle-swift.md#812-login-gate-rfc-025-2026-09-16" +} + +rule store-slices-bound-to-sender "A synced-store slice or Clear counts only under its sender's device id, which also keys a departed peer's slice" { + require text "sender_owns(", "\"slice\")", "\"clear\")" in sym:truffle_core::synced_store::sync::handle_incoming_message + require text "node.peer_device_id(from)" in sym:truffle_core::synced_store::sync::sender_owns + require text "published_device_id()" in mod:truffle_core::synced_store::sync + see "docs/rfcs/025-login-scoped-nodes.md#35-sender-bound-slices-tf-2" + see "crates/truffle-core/src/synced_store/sync.rs#L139-L143" +} + rule no-http-stack-on-data-path "No axum, no hyper for the data path" { forbid text "axum", "hyper" in crate:truffle-core, path:crates/truffle-cli/src/daemon/ see "docs/rfcs/012-layered-architecture-redesign.md#94-why-raw-tcp-for-file-transfer-instead-of-http" @@ -114,6 +206,21 @@ rule no-http-stack-on-data-path "No axum, no hyper for the data path" { see "crates/truffle-cli/src/daemon/mod.rs#L7" } +rule serve-engine-in-go "Directories and other processes are served by the Go engine; mesh.serve only configures it" { + require text "httputil.ReverseProxy", "http.ServeContent", "\"Tailscale-User-Login\"" in file:packages/sidecar-slim/main.go + deny file:packages/core/src/serve.ts -> file:packages/core/src/{net, http, ws}.ts + see "docs/rfcs/023-http-serving.md#5-design-overview" + see "docs/rfcs/023-http-serving.md#12-decisions" + see "packages/core/src/serve.ts#L5-L10" +} + +rule proxy-discovery-inert "Serve discovery stays inert until RFC 023 Phase 5: nothing calls proxy::discovery" { + deny * -> mod:truffle_core::proxy::discovery + see "docs/rfcs/023-http-serving.md#12-decisions" + see "docs/rfcs/023-http-serving.md#11-implementation-plan" + see "crates/truffle-core/src/proxy/types.rs#L44-L47" +} + rule native-is-internal "@vibecook/truffle-native is an implementation detail behind @vibecook/truffle" { deny *, !pkg:@vibecook/truffle -> pkg:@vibecook/truffle-native see "docs/API-STABILITY.md#from-10" @@ -134,12 +241,36 @@ rule egress-loopback-only "Every egress listener binds 127.0.0.1" { see "docs/rfcs/026-egress.md#2-decision" } +rule shim-speaks-no-mesh-protocol "The Go shim embeds tsnet and knows none of truffle's own protocol" { + require text "\"tailscale.com/tsnet\"" in $shim + forbid text "msg_type", "device_id", "\"/ws\"" in $shim + forbid text "\"ft\"", "\"ss:" in $shim + see "docs/rfcs/003-rust-rewrite.md#context" + see "docs/rfcs/003-rust-rewrite.md#6-what-stays-go" + see "docs/rfcs/012-layered-architecture-redesign.md#35-layer-3-network" + see "packages/sidecar-slim/main.go#L7" +} + +rule no-fixed-sidecar-listener "The sidecar hardcodes neither the session port 9417 nor the v1 :443 listener" { + forbid text "9417", "\":443\"" in $shim + see "packages/sidecar-slim/main.go#L1259-L1263" + see "docs/rfcs/023-http-serving.md#43-the-port-443-fossil" + see "docs/rfcs/023-http-serving.md#81-port-443-reclamation" +} + rule bench-keys-from-env "Benchmark auth keys never come from argv, only from a key file, the environment or the app's sign-in form" { require text "\"TRUFFLE_BENCH_AUTH_KEY\"", "\"TRUFFLE_TEST_AUTHKEY\"", "\"TS_AUTHKEY\"" in crate:truffle-bench require text "env::var(\"TRUFFLE_TEST_AUTHKEY\")" in path:benchmark/engine/src/legacy/ require text "env::var(\"TRUFFLE_BENCH_AUTH_KEY\")" in crate:truffle-bench-app require text "os.Getenv(\"TRUFFLE_TEST_AUTHKEY\")" in file:packages/sidecar-slim/cmd/tsnet-bench/main.go + // No argv parser takes the key: no clap field or flag for it, and no clap `env` binding, which accepts argv too. + forbid text "auth_key:", "\"auth-key\"", "\"authkey\"" in file:benchmark/engine/src/{cli.rs, legacy/transport.rs} + forbid text "env = \"TRUFFLE_BENCH_AUTH_KEY\"", "env = \"TRUFFLE_TEST_AUTHKEY\"", "env = \"TS_AUTHKEY\"" in file:benchmark/engine/src/{cli.rs, legacy/transport.rs} + forbid text "authkey:" in file:benchmark/engine/src/cli.rs + forbid text "\"--auth-key", "\"--authkey", "\"--key" in file:benchmark/app/src-tauri/src/state.rs + forbid text "authkey\"", "auth-key\"", "auth_key\"" in file:packages/sidecar-slim/cmd/tsnet-bench/main.go see "CLAUDE.md#benchmarks" + see "benchmark/engine/src/cli.rs#L6-L7" see "docs/rfcs/027-benchmark-hub.md#10-cli" see "docs/tailscale-performance.md#measuring-the-tailscale-stack" see "docs/rfcs/019-local-testing-and-benchmarking.md#5-environment-contract" @@ -151,6 +282,40 @@ rule bench-ui-renders-text-only "The bench app renders peer and report strings a } rule windows-key-press "The TUI handles only KeyEventKind::Press (crossterm emits Press and Release on Windows)" { - require text "KeyEventKind::Press" in path:crates/truffle-cli/src/tui/ + // Key events are read in two files; each filters Press, and no other kind is handled anywhere. + require text "KeyEventKind::Press" in file:crates/truffle-cli/src/tui/event.rs + require text "KeyEventKind::Press" in file:crates/truffle-cli/src/tui/onboarding.rs + forbid text "KeyEventKind::Release", "KeyEventKind::Repeat" in crate:truffle-cli + forbid text "event::read()", "EventStream" in crate:truffle-cli, !file:crates/truffle-cli/src/tui/{event, onboarding}.rs see "CLAUDE.md#key-conventions" + see "crates/truffle-cli/src/tui/event.rs#L73-L74" +} + +rule daemon-ipc-is-local "CLI and daemon talk over a Unix socket (a named pipe on Windows), never a TCP port" { + require file:crates/truffle-cli/src/daemon/server.rs -> file:crates/truffle-cli/src/daemon/ipc.rs + require file:crates/truffle-cli/src/daemon/client.rs -> file:crates/truffle-cli/src/daemon/ipc.rs + require text "UnixListener", "UnixStream", "named_pipe" in file:crates/truffle-cli/src/daemon/ipc.rs + forbid text "TcpListener", "TcpStream" in file:crates/truffle-cli/src/daemon/{ipc, server, client}.rs + see "docs/rfcs/010-p2p-and-cli-redesign.md#16-appendix-b-decision-log" + see "docs/rfcs/010-p2p-and-cli-redesign.md#142-windows-deferred" + see "crates/truffle-cli/src/daemon/ipc.rs#L1-L4" +} + +rule commands-are-daemon-clients "The daemon owns the Node; one-shot commands are its JSON-RPC clients" { + require file:crates/truffle-cli/src/daemon/server.rs -> mod:truffle_core::node + // up.rs is left out: `truffle up --foreground`, which `truffle up` re-execs, is the daemon. + deny path:crates/truffle-cli/src/commands/, file:crates/truffle-cli/src/daemon/client.rs, + !file:crates/truffle-cli/src/commands/up.rs -> crate:truffle-core, file:crates/truffle-cli/src/daemon/{server, handler}.rs + require path:crates/truffle-cli/src/commands/ -> file:crates/truffle-cli/src/daemon/client.rs + see "docs/rfcs/010-p2p-and-cli-redesign.md#32-daemon-architecture" + see "docs/rfcs/013-tui-redesign.md#2-vision" + see "crates/truffle-cli/src/daemon/mod.rs#L3-L4" +} + +rule tui-hosts-the-daemon "The TUI starts DaemonServer in-process and reads the Node directly" { + require path:crates/truffle-cli/src/tui/ -> file:crates/truffle-cli/src/daemon/server.rs + require path:crates/truffle-cli/src/tui/ -> mod:truffle_core::node + see "docs/rfcs/013-tui-redesign.md#3-architecture-decision-tui--foreground-daemon" + see "docs/rfcs/013-tui-redesign.md#phase-a1-skeleton--terminal--daemon--static-layout" + see "crates/truffle-cli/src/tui/mod.rs#L3-L4" } diff --git a/atlas/system.atlas b/atlas/system.atlas index b541df33..3488de3e 100644 --- a/atlas/system.atlas +++ b/atlas/system.atlas @@ -23,3 +23,6 @@ system truffle "Truffle" { // truffle-core's subsystems on Node: file transfer, synced store and request/reply (the send_typed docs in node.rs) // and the serve proxy (RFC 023 D2). Egress is Node API, not a subsystem: it sends no message. let $subsystems = mod:truffle_core::{file_transfer, synced_store, request_reply, proxy} + +// The Go shim's production code: sidecar-slim without cmd/, the Go baseline bench (tooling). +let $shim = path:packages/sidecar-slim/, !path:packages/sidecar-slim/cmd/ From 4ec7cd1e742ddf29daa75f83a0b48256a27ef0b9 Mon Sep 17 00:00:00 2001 From: James Yong Date: Sat, 24 Oct 2026 23:00:33 -0700 Subject: [PATCH 5/6] docs(atlas): the skill's figures and sample output for 0.7.20 The sample check output, the source-only warning and the egress line references now match 0.7.20 with this atlas. The skill also notes `let $shim`, the one sanctioned except, the one accepted decision left unconfirmed, and that the function-scoped text checks dangle when the function is renamed. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/atlas/SKILL.md | 39 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/.claude/skills/atlas/SKILL.md b/.claude/skills/atlas/SKILL.md index 3e41d3d6..11d9b256 100644 --- a/.claude/skills/atlas/SKILL.md +++ b/.claude/skills/atlas/SKILL.md @@ -15,7 +15,8 @@ cites them, never replaces them. Read CLAUDE.md "Atlas" first. The DSL spec is ## Where it lives - `atlas/system.atlas` — system, thesis, `let $subsystems` (truffle-core's file_transfer, - synced_store, request_reply and proxy). + synced_store, request_reply and proxy) and `let $shim` (the Go shim: sidecar-slim without the + Go baseline bench in `cmd/`). - `atlas/layers.atlas` — the RFC 012 stack (position is rank): truffle-core's module tiers (node-api, envelope, session, transport, network; `mod:truffle_core` makes node-api the default), the L7 sub-tiers (examples, cli, bench, react, js-api, bindings), the Go sidecar, Truffle Swift, @@ -36,7 +37,9 @@ cites them, never replaces them. Read CLAUDE.md "Atlas" first. The DSL spec is `via`/`direct`, `by`, and the two steps done in Go (`at file:packages/sidecar-slim/main.go`). - `atlas/rules.atlas`, `atlas/decisions.atlas` — the rules (including the single-crate rules of the crates.io crates and truffle-cli, with `crate:` scopes); one decision per significant RFC - with its real status. + with its real status. Every accepted decision has a `confirmed-by` but `one-release-unit`: the + comment above it names the release paths that do not fail closed yet. `bridge-is-private` holds + the one sanctioned `except` (`BridgeStats`, public since 0.7.20). - In code (`grep -rn '@atlas' crates benchmark`): - the roots of the unpublished crates with a crate doc: truffle-napi (its placement and `napi-pull-model`), truffle-tauri-plugin, truffle-bench (`benchmark/engine/src/lib.rs`: its @@ -97,8 +100,9 @@ TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- explain "$ and duplicate ids only, no selector or reference checks. It also parses `.go` and `.swift` comments, which a real build ignores. It calls Atlas's own `tsx` directly: truffle has none, and `npx` would fetch one from the registry. -- Do not use `--rust-source-only` on truffle: without rustdoc symbols, 69 claims dangle (contract - anchors, domain `touches`, flow steps and hops) and 10 are violated instead of 7. +- Do not use `--rust-source-only` on truffle: without rustdoc symbols, 81 claims dangle (contract + anchors, `sym:`-scoped checks, domain `touches`, flow steps and hops) and 14 are violated instead + of 7 (the `BridgeStats` except needs its symbol too). - `baseline` (`TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- baseline "$PWD" --intent "$PWD/atlas"`) writes `atlas/atlas.baseline.json` with every violation of that run, so whatever your change added becomes `known` too. The committed one holds the 7 findings kept for the user. It runs @@ -107,37 +111,37 @@ TRUFFLE_SIDECAR_SKIP_DOWNLOAD=1 npm --prefix "$ATLAS" run -s atlas -- explain "$ ## Reading `check` -An excerpt of the run on 0.7.18 with this atlas and its baseline committed (the header's hash +An excerpt of the run on 0.7.20 with this atlas and its baseline committed (the header's hash changes with every commit); `…` marks what is left out here. Claims sort by severity, then by the file and line that declare them: today the decisions (`atlas/decisions.atlas`), then the rules (`atlas/rules.atlas`), then the in-code contract: ``` -atlas check · Truffle @ 0c08350 · 520 claims - 473 verified · 40 asserted · 7 violated (0 new, 7 known) · 0 dangling · 0 ambiguous · 0 stale - coverage: layers 100.0% · domains 100.0% of 88,816 prod lines · rules 23 pass, 4 fail, 0 unchecked +atlas check · Truffle @ 59fab9c · 630 claims + 590 verified · 33 asserted · 7 violated (0 new, 7 known) · 0 dangling · 0 ambiguous · 0 stale + coverage: layers 100.0% · domains 100.0% of 89,563 prod lines · rules 40 pass, 4 fail, 0 unchecked VIOLATED · known (in the baseline) (7) decision:layered-architecture decision layered-architecture "Layers 3-6 behind one Node API; applications are Layer 7" code RFC-0… - at atlas/decisions.atlas:36 · decision · basis none + at atlas/decisions.atlas:38 · decision · basis none rule:layers-point-down violated rule:network-knows-no-upper-layer violated rule:transport-knows-no-session violated rule:apps-use-node-api violated → Accepted, but rule:layers-point-down, rule:network-knows-no-upper-layer, rule:transport-knows-no-session, rule:apps-use-node-api are failing: the code has drifted from this decision. decision:synced-store decision synced-store "SyncedStore and a request/reply helper in core" code RFC-016 status accepted - at atlas/decisions.atlas:82 · decision · basis none + at atlas/decisions.atlas:85 · decision · basis none contract:sync-wire violated → Accepted, but contract:sync-wire is failing: the code has drifted from this decision. rule:layers-point-down layered at atlas/rules.atlas:10 · rule · basis edge mod:rs/truffle_core::network → mod:rs/truffle_core::egress - crates/truffle-core/src/network/mod.rs:260 ) -> impl std::future::Future impl std::future::Future layer:network, layer:transport, layer:session, layer:envelope - at atlas/rules.atlas:65 · rule · basis edge + at atlas/rules.atlas:74 · rule · basis edge pkg:cargo/truffle-bench → pkg:cargo/truffle-core benchmark/engine/src/control.rs:34 use truffle_core::session::{SessionError, DIAL_CANCELLED_SHUTTING_DOWN}; benchmark/engine/src/control.rs:35 use truffle_core::transport::quic::QuicListener; @@ -145,9 +149,9 @@ VIOLATED · known (in the baseline) (7) → 66 forbidden dependencies from layer:apps into layer:network, layer:transport, layer:session, layer:envelope (code); … … contract:sync-wire -ASSERTED (40): not listed; --all lists them +ASSERTED (33): not listed; --all lists them -VERIFIED (473): not listed; --all lists them +VERIFIED (590): not listed; --all lists them ok: no new violations ``` @@ -231,7 +235,7 @@ ok: no new violations - `decision:synced-store`: `contract:sync-wire`; - `rule:layers-point-down`: 3 module edges, the two egress ones below and the hello one; - `rule:network-knows-no-upper-layer`: 2 edges. The `NetworkProvider` egress verbs - (`network/mod.rs:260`, `:270`, `:271`, `:291`, `:301`) and `tailscale/provider.rs:23` name + (`network/mod.rs:261`, `:271`, `:272`, `:292`, `:302`) and `tailscale/provider.rs:23` name `egress` types, while `egress.rs` documents them as the Node API of RFC 026 and `egress` sits in node-api; - `rule:transport-knows-no-session`: 1 edge. `transport::websocket` imports `session::hello` @@ -266,7 +270,10 @@ ok: no new violations grep) and fix the selector in the same change as the rename. Module paths also sit in brace forms (`mod:truffle_core::{node, identity, egress}`, `mod:truffle_napi::{raw_socket, udp_socket, quic}`, `mod:truffle_bench::workloads::{tcp, udp, quic}`) that a grep for the full path misses: grep the - stem. If the code is gone for good, delete the claim with it. + stem. If the code is gone for good, delete the claim with it. Some text checks search one + function (`sym:…::validate_hello`, `is_app_peer`, `spawn_connection_task`, + `Node::spawn_envelope_router`, `handle_incoming_message`, `sender_owns`): renaming it dangles + them, and the decision they confirm then grades violated. - **Dangling `see`.** The message suggests the nearest heading (`Did you mean #…?`). An RFC heading rename dangles every `see` pointing at it: fix them together. A `#L` reference (`crates/truffle/src/lib.rs#L38-L41`) dangles only when the file gets shorter; re-check that From adc42a576cd6df8aec445b1b91a63bea48d74b83 Mon Sep 17 00:00:00 2001 From: James Yong Date: Thu, 29 Oct 2026 10:17:38 -0700 Subject: [PATCH 6/6] docs(atlas): refresh source citations after rebasing onto main --- atlas/rules.atlas | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/atlas/rules.atlas b/atlas/rules.atlas index 456b2bc7..78372680 100644 --- a/atlas/rules.atlas +++ b/atlas/rules.atlas @@ -144,7 +144,7 @@ rule hostname-from-identity "By default a node's hostname is truffle-{appId}-{sl require text "\"truffle-\"" in file:apple/Sources/Truffle/Identity/Slug.swift see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" see "docs/rfcs/024-truffle-swift.md#72-tailscale-hostname" - see "crates/truffle-core/src/node.rs#L2059-L2060" + see "crates/truffle-core/src/node.rs#L2135-L2136" } rule apps-isolated "Discovery admits only truffle-{appId}- hostnames, and the hello refuses another app's id" { @@ -154,7 +154,7 @@ rule apps-isolated "Discovery admits only truffle-{appId}- hostnames, and the he require text "HelloValidationError.appMismatch(" in file:apple/Sources/Truffle/Wire/HelloEnvelope.swift see "docs/rfcs/017-identity-and-namespacing.md#4-design-two-namespaces" see "docs/rfcs/017-identity-and-namespacing.md#8-wire-protocol-peer-metadata-exchange" - see "crates/truffle-core/src/network/tailscale/provider.rs#L887-L890" + see "crates/truffle-core/src/network/tailscale/provider.rs#L1125-L1128" } rule device-id-honest "deviceId is a ULID or absent, never a Tailscale id in its place" { @@ -253,7 +253,7 @@ rule shim-speaks-no-mesh-protocol "The Go shim embeds tsnet and knows none of tr rule no-fixed-sidecar-listener "The sidecar hardcodes neither the session port 9417 nor the v1 :443 listener" { forbid text "9417", "\":443\"" in $shim - see "packages/sidecar-slim/main.go#L1259-L1263" + see "packages/sidecar-slim/main.go#L1309-L1313" see "docs/rfcs/023-http-serving.md#43-the-port-443-fossil" see "docs/rfcs/023-http-serving.md#81-port-443-reclamation" }