ADR-0050 Architecture decision record
What code names, config declares; what the CLI names, discovery finds
A Machine names things outside itself by string, and until now the first sign of a wrong string was an invoke-time failure mid-run. ADR-0049 moved Agents and Sandbox Images INTO the Machine — actor slots and a workspace() option — so those names are typed by xstate's own src typing and by the Machine's own parts. ADR-0051 moved the last one, the Repo, the same way: a slot on the workspace(), bound by url. What this decision fixes is the rule that says where a definition lives, and the gate that enforces it.
Decision
- One rule decides where a definition lives. A thing that code refers to by name and that a Machine cannot carry is declared in
jr2.config.ts, so the type system sees the name. A thing that only the CLI or HTTP API refers to by name is discovered from files. Workflows are the latter —jr2 runandPOST /workflows/:name/runsname them, a nested Machine is reached byimport(ADR-0049) — soworkflows/stays filename-discovered. Agents, images, and Repos are neither: they ride the Machine (ADR-0049, ADR-0051), and theagents/folder,config.agents,config.images, andconfig.reposall retire. One thing code names and a Machine cannot carry is a secret's value: an Effect names it withsecret("name"), and the Instance declares it in the top-levelsecretslist (ADR-0068). The name is a string, not a type: the walk after the typecheck refuses an Effect that names a secret the list does not hold, before anything is built.jr2.config.tsholds reach and credentials (harness,secrets,git.credentials,registry). - The slot key types a Repo. A
workspace()declares its Repos as slots —repos: { target: open, docs: { url, ref } }— and the keys are a phantom on the wrapper type, read throughpool()'sworkerandworkspace()'sbodyas the image is (ADR-0049). Acustomize(machine, { repos: { <slot>: url } })of a slot the Machine does not declare is a compile error, never a silent widening tostring, and the body'sworkspace.repos.<slot>handle is typed by the same keys. There is noRegister, noRepoName, and norepoNames(): the url a Machine writes is the thing, not a reference into someone else's file (ADR-0051). A door that offers a menu of Repos is the Instance's ownz.enumof urls. jr2 uptypechecks the Instance before it builds anything, and refuses on errors, naming them. The scaffold'stsc --noEmitand pinned compiler already exist (ADR-0043's rule for the checker); this makes them a gate instead of a script the user may run. A wrong slot name, acustomize()of an Agent or a Repo Slot the Machine does not carry: all stop here. One Repo check is converge-time and not compile-time: a registered Machine with an open slot nobody bound. Aworkflows/export has no type to hang it on, so the walk that follows the typecheck refuses it, before it builds anything, naming the Machine, the slot, and thecustomizeline that fixes it (ADR-0051).jr2 checkruns the gate and the walk with no cluster. The typecheck and the walk are one function of the CLI,checkInstance.jr2 upandjr2 checkboth call it, so the two give the same refusals with the same text. The walk loads each registered module. Thusjr2 checkalso gives each refusal that a module throws when it loads. These are refusals ofcreateMachine,defineEventandworkspace(), andnpm run typecheckcannot give them.jr2 checkreads no kube context. Thus an Assistant that may not runjr2 upcan run it before it gives a change to the user. It runs in a kit checkout and in an installed Instance. The name is the verb that a user types for "is this folder correct". The glossary's check of a Check pod is a different noun in a different place: one model call in the provider preflight. Thus the name stays, and the glossary entry tells the two apart.
Considered options
A generated declaration (
jr2-env.d.tswritten byinit/upfrom directory discovery), to keepagents/andimages/as folders and still type their names. Rejected: a generated-file class jr2 does not have, an editor staleness window, and — decisive — ADR-0049 removed the need by making the names part of the Machine.Agents and images as config entries (
agents: { scribe },images: { default: "./images/default" }), the shape this ADR first took. Rejected the same day: it typed the names but kept them Instance-scoped, so a packaged Machine still depended on someone else's config holding the right strings, and one run could not hold two Machines each carrying acoder.A Repo catalog in config, typed back through a
Registerinterface (repos: [{ name, url }], aRepoNamederived fromtypeof config,z.enum(repoNames(config))on a door), the shape this ADR took for the one dependency it left in config. Rejected in ADR-0051 by the argument above: a packaged Machine still could not say which repository it works on, and a consumer could bind one only by holding the right NAME. The slot key is the type now.A flag on
jr2 up(jr2 up --check, or--dry-run) in place of a verb. Rejected:upreads the kube context first and its flags are converge flags; a flag that stops it after the walk still asks an Assistant to run the converging verb, which a permission rule that forbidsjr2 upalso forbids. A verb is the line a permission rule can allow.A copy of the walk in
jr2 check. Rejected: two walks drift, and then the check passes a folder thatjr2 uprefuses — the failure thatjr2 checkexists to remove.Stage the Assembly in
jr2 checktoo. Rejected: staging runs a frozen install from the lockfile (ADR-0043), which needs the registry and takes minutes, and its refusals are about the install, not the code an Assistant writes.
Consequences
- Only Machines the Instance's program typechecks get the compile-time guarantee — those under
workflows/and what they import. A packaged Machine compiled elsewhere carries its own parts, so its internal names are checked where it was built; the Repo it works on is an open slot the consumer binds withcustomize, typed by the package's own slot keys, or a per-run slot typed by the consumer's mapper over the door. - Runtime validation stays: an open slot nobody bound is a
jr2 upwalk refusal, and the attach still refuses a per-run url nogit.credentialsentry matches (ADR-0051). The type error is the earlier check, not the only one.