pi-mono/packages/chord/docs/slice-7a.md
Mario Zechner e9c0ff4d84 feat(chord): Chord v2 is the package's root
src/next is src (the main export), src/next-node is src/node with short subpaths (/http, /workers, /plugins,
/plugins/runtime, /app, /surfaces, /web, /react, /tui, /tui/launcher); the foundations get /json and /watch;
Chord v1 moves to src/old (/old, /old/node) until p2 runs on v2; tests and docs move likewise (test/, docs/,
test/old, docs/old). Durable and env import the foundations from their subpaths.
2026-11-02 22:36:55 +00:00

130 lines
9.8 KiB
Markdown

# Slice 7a: the dependency model (spec)
Status: **ready to build** (owner, 2026-10-29: the model decided, "just build this"). Slice 7 is split: 7a builds the
dependency model on what exists (one `load` per host, links, routes, relays); 7b builds reload and rollout on top of it
(removal, dropped versions, installs, own-code reloads, loader D).
Read in full before writing code: `dependencies/model.md` (the model as decided; its section numbers are cited below as
M§), `design.md`, `decisions.md` (the entries of 2026-10-28/29: "No client providers", "Every remote type declares its
placement", "`hostAddress` instead of `placement`", "Only hosts with an address serve remote types", "Getters wait
through `waiting`", "Cycles across hosts: no probe, a diagnostic", "The dependency model is decided"), `slices.md`
(slice 7's carry-over: 7a takes none of it unless named here; 7b takes the rest), the explainer
`dependencies/book.md` for the scenarios (its catalogs are test material).
## 1. Requests and the unbound index (M§1)
- Every reference is a request with one holder (attempt, call, outside code, a link's peer). It ends only when its
holder ends it; never because its provider is missing or does not fit.
- The destination by address is built (`host/destination.ts`, slice 7a's predecessor commit `5ec241882`). Add: version
checks only against the selected provider (no `#assertRoute` comparison with a same-name registration on the
consumer's host); a local type not served here is no longer a throw at `self.get`/`host.hold`/getters but an unbound
request.
- **The publication gate:** a request made on this host, or a hold arriving on its links, before the host's first
publication is not placed: `loading`, placed by the publication. `pendingLoad` no longer gates `Hold.start` or
`#hold` beyond the publication (it ends the startup deadlock: Sol's probe `/tmp/dep-graph-sol-barrier.log` becomes a
regression test that passes with fake timers never advanced).
- **Unbound reasons** (M§1's table): `unpublished` (loading), `not_served` and `version` (`failed { waiting }`),
`compose` for registries (`failed { waiting }`), `no_link` on a path (loading); `authorize` refused or invalid params
stay `broken`, never indexed. Indexes per host by name (local requests; refused holds that arrived over links, kept as
today's refused state in `remote/hold.ts`); entries removed when the request ends (release, call settled, getter gave
up, `closed { origin }`, link close, revocation). GC tests.
## 2. States (M§2)
- `FailureCause` gains `{ kind: "waiting"; error }` (no `retryAt`). The wire's `failed` cause schema carries it; the
pinned list and its test change; `PROTOCOL_VERSION` stays 1.
- The mapping of M§2's table: `loading` for activating, unpublished, no link (and `installing`, 7b); `failed
{ waiting }` for not served, version, compose, and a dependent whose dependency failed.
- A remote refusal for not served / version is `failed { waiting }` instead of `broken`; the provider keeps the refused
hold (it does today) and the consumer never retries it by itself (no backoff).
## 3. Parked attempts (M§3)
- Order: factory, persisted singleton hydration (as today), requests made; the attempt is **parked** while any
dependency (`self.get` local or remote, `self.registry`, keyed pins) is not live and hydrated. `self.follow` is not a
dependency (its attach is an own step).
- What it shows: `loading` while blockers are only `loading`; `failed { waiting, "waiting for X: <cause>" }` (nearest
blocker plus root cause, 500-character rule) when one failed; `broken` when one failed with `invalid/cycle` (off-ring
dependents) or ended.
- **No timer while parked:** no dependency deadline, no identity backoff for dependency failures; the attempt's
`readyTimeout` deadline is paused while parked (as eager migrations pause it) and resumes with the budget left. The
identity backoff stays for the object's own `transient` failures only.
- **The dependency check is the boundary:** once all are live, the own steps run (natives, follow attaches, member
checks, derives) under the deadline; a dependency leaving `live` afterwards is treated as after live (R18): the
attempt goes on.
- **Wakes on every state change** of a dependency (live, loading, failed, a new cause compared by value, ended):
re-evaluate what the object shows, leave parking when all are live; never re-run the factory while parked; coalesce
per object after the batch that caused them; remember changes during own steps or teardown.
- **Retry passes on:** `ref.retry()` on a `failed { waiting }` object retries its `broken` and `transient` blockers hop
by hop (the existing `retry` message), once per operation (an id dedupes diamonds); `waiting` blockers pass it on.
A `retryOn` value call while parked `waiting` does nothing.
## 4. Wakes (M§4)
Two events: a target changes state (the requests bound to it and their dependents); a name becomes usable on a host
(in 7a: `load`'s publication serving types and registries, a registry's value turning usable at a version): the
unbound requests indexed under it, and refused holds, which re-run their start and send `loading`, then `bound` or
`failed`. No new message kind.
## 5. Startup (M§6)
- `load` runs the setups, publishes them in one batch (types, registries served and composed once, contributions),
wakes after that batch, then activates host-lifetime objects.
- **The barrier, decision (a):** `load` resolves when every host-lifetime object of the load has a first outcome:
live, failed by its own cause, or parked with no blocker that is a local object still without its first outcome
(`local` = executing on this host, keyed pins still loading here included). One `firstOutcome` signal per object,
separate from the readiness dependents wait for; counted once; an object of the cohort that ends before its first
outcome settles as ended; `host.close` settles a waiting `load`.
## 6. Getters, holds, calls (M§8, decision "Getters wait through `waiting`")
- `host.hold` returns a reference at once; only invalid params throw (also before `load`); a missing local type gives
`loading` before the publication, `failed { waiting }` after it, before and after `load` alike.
- `host.get`, `ctx.get`, `host.registry`: resolve when live; wait through `loading`, `failed { transient }` and
`failed { waiting }` until their `readyTimeout`, then reject with the last cause and release; reject at once only on
`failed { broken }`.
- Calls: park on `loading` (bounded, as today); reject at once on `failed`; never resent.
## 7. Registries (M§9)
A reader's request: bound while R is served and has a usable value at its version; otherwise unbound `not_served`,
`version` (S2's newer or unmapped reader: `waiting`, not `broken`) or `compose` (first compose or the version mapping
threw), `loading` before the publication. Woken at every transition to usable (`#valued`'s latch goes). The last good
value is kept only while every contribution it was built from is still in the table (decision 5): when one leaves,
the last good value is dropped and `.value` throws until `compose` succeeds.
## 8. Keep-alive, revocation, cycles (M§7, M§10)
- A request over a link counts once in `host.activity.references` from arrival to end, bound or unbound;
`host.references` lists bound references only.
- Revocation ends the requests of a revoked principal, bound and unbound, at every hop.
- `gone` only for revocation (a closing provider host sends nothing, as today).
- **Cycles:** the walk at `self.get` on one host, through parked attempts too; the activation chains across hosts as
today. Remove the dependency deadline (no fallback). Add `HostError` `parked_long { object, waitingFor, since }`:
once per parking episode, when an attempt has been parked longer than its `readyTimeout` on a remote attempt that is
itself only `loading`. The guide (host README) states the four authoring rules of M§7.
## 9. Not in 7a (7b)
Removal and re-serving of types (local re-binding of a `Binding`, a `failed` on a bound hold unbinding it, window
slots), a dropped accepted version, `installing` and install ends, own-code reloads and the parked-attempt handover,
pure swaps parking (decision 4), rollout, loader D. 7a's code must leave those paths possible, not build them.
## 10. Tests and cost
- One `describe` per mechanism; the four catalogs in `dependencies/book.md` (chapters XIV-XVII) are the scenario list:
every row that 7a's scope reaches becomes a test (memory links; relays; fake timers where a test proves no timer
runs). Must include: the startup deadlock without `readyTimeout`; a hold before the publication placed here, not via
the parent; a missing local type on `self.get`, `host.hold`, `host.get` (waits, then rejects with the cause); a
remote refusal kept and re-run when the provider's late `load` publishes; a parked three-level chain across hosts
woken in order; wakes on every state change with causes updated; retry passing on through a diamond once; the
barrier's first-outcome rule (CodeMode → Harness waits; a remote blocker ends the pass); a cohort object ending
before its outcome; registries' waiting reader, wake after a provider arrives, last good dropped when a contribution
leaves; keep-alive counting unbound holds; revocation of unbound holds; `parked_long` reported once; GC tests for the
indexes and a parked chain released from the top.
- Benchmarks (the bar of "The performance and memory bar"): calls, activation churn, a watched derive, links against
HEAD, pinned, alternating, a HEAD copy at a path of the same length; results in `benchmarks.md`.
- Examples: `31-parked-dependencies.ts` (a missing provider arrives; causes shown; no timers), `32-startup.ts` (two
hosts that would have deadlocked).
- Docs: fold the model into `design.md` (§3, §6, §7, §17, §21's 7a parts) in the same change as the build; mark
`dependencies/model.md` as built for 7a.