ADR 0102: One paths registry, rendered artifacts, and leakage guards
Amendments.
- Vocabulary: current spellings are
map(formerlydocs) where it names the command, config, or tree, and Project Script /[scripts].dir(formerly the Recipe surface); the decision and reasoning are unchanged.
Status: accepted; builds on ADR 0099; extends ADR 0026 (the schema stays the config authority; path defaults gain their own registry beneath it) and ADR 0051 (whose guard pattern this applies to paths)
Context
§ADR 0099 makes every source path a prescriptive default with a config escape hatch. That promotes path literals from a style issue to a correctness class: any default hard-coded outside the one authoritative place is a latent bug that surfaces only in a project pointed away from the defaults — precisely the configuration this repo itself runs.
Current reality, mapped before this decision:
- Defaults are scattered: most live as Zod
.default()literals inconfig_schema.ts, the docs default in its owndocs_path.tsmodule, whileTODO.mdandbrief.mdare hard-coded strings in the setup and plan code with no config key at all. - The built-in guidance already renders paths correctly: a strict template engine (
{{docs_dir}}, compile-time validation of every variable) plus a parity test asserting every context variable flows from config. This is the pattern to extend, not invent. - Bundled skills do not render: materialization copies them verbatim, and their prose carries real literals (
guidance.md, root-relativeTODO.mdlinks) alongside informal<docs-dir>placeholders. - The shipped
.gitignorefragment hard-codes a docs path in its comment. - The known failure mode is agents working on discern hard-coding the layout they can see in this repo, with no regard for what a user may have configured. That is a class of defect, and classes get structural guards.
Decision
§- One paths registry. A single module defines every configurable source path: its config key, its default, and its description. The Zod schema's path defaults derive from it; the resolver helpers read through it; setup's seeding, the migration, codegen, and every guard below enumerate from it. Adding a path means adding one registry entry — every satellite either auto-enrols or fails the gate. The ledger and the brief become registry-backed: the ledger gains a config key (
[project].todo); the brief stays at its namespace default without a dedicated key until a real need appears (it is setup-time input, not an ongoing convention). - Bundled artifacts render, never quote, paths. Skill materialization passes bundled markdown through the same strict template engine the guidance compiler uses, with one shared variable context built from resolved config. The literals in bundled skills become variables; the informal
<docs-dir>placeholders normalize to the same syntax. Authored (user) skills stay symlinked and untouched — their paths are their business. The skills currency check compares rendered trees, so a path reconfiguration makes materialized skills stale untilrefresh— the same contract guidance already has. The.gitignorefragment's comment drops its hard-coded docs path for a config-independent pointer. - The leakage guard battery, strongest first:
- Sentinel-render test. Compile the guidance and materialize every bundled skill against a sentinel config (each path pointed at an obviously fake location), then assert no registry default survives in any rendered output. The literal list is derived from the registry, so a new path auto-enrols.
- Parameterized engine run. A representative slice of the scaffolded engine suite runs against a fully non-default paths config — behavioral parity under reconfiguration, the guard no grep can fake.
- Source-literal ban. An architectural test over
src/**fails on any registry default appearing as a string literal outside the registry module itself. - Write-surface test. The write-surface contract of ADR 0099, enforced: every project-tree write site must target a registry path, a provider-registry path, or an enumerated shim.
- Dogfooding on non-defaults. This repo permanently points every ongoing configurable authored source beneath
project/. A registry-driven guard auto-enrols new keyed sources, so any hard-coded default or scattered path diverges from the repo agents can see — leaks become loud.
Consequences
§- The registry is a new closed set in the ADR 0051 family: satellites (schema, resolvers, seeding, migration, codegen, sentinel list, write surface) are tied to it by tests, and a hand-copied path list anywhere is a gate failure.
- Bundled-skill authors (including future agents editing
templates/skills/) must write{{docs_dir}}-style variables; the strict engine makes a stray or misspelled token a loud materialization failure, and prose must avoid literal{{outside a token (audit at conversion; add an escape only if a real case appears). - Currency checks become config-sensitive by construction — moving a path key without
refreshfails the gate instead of leaving stale instructions live. - Two files currently duplicate a suggested-docs-path constant; the registry absorbs that fact.
Alternatives considered
§- Runtime indirection instead of rendering (skills tell agents to run
discern config get docs.dir). Rejected as the default: it survives reconfiguration without a refresh, but costs a tool call per use and makes every skill read like plumbing; Project Scripts already use it where live resolution is the point. - No registry — fix the literals in place. Rejected: that is the current scatter, and it regrows; without one source to enumerate from, the sentinel and write-surface guards would run off hand-copied lists, the exact defect they exist to catch.
- Freeze the defaults (drop the config keys) so literals are harmless. Rejected: pointing is the consent model and the brownfield story; correctness-by-rigidity is the wrong trade.