ADR-048: The scaffold is planned from configuration, not copied from a tree

Status Active · Filed 2026-08-11 · Influenced by ADR-036 · ADR-047

Context

ADR-036 made luria new’s kinds derive from the config, and ADR-047 made the config able to say “this record has no decision scheme”. luria init still copied a fixed template tree: the four default layers, whatever the config said. The wart showed at both ends. A project that already had a luria.toml declaring a custom shape got the template’s directories scaffolded beside it — record/decisions.d/ for a record whose config declared no decisions. And there was no way to hand init the config you actually wanted; adoption meant scaffolding the default shape and then renaming, moving, and deleting your way to the record you meant. The question that prompted this — can init not take a config file as input? — is the feature named plainly.

Decision

init resolves a config, in priority order: --config PATH, then a luria.toml already at the project root, then the shipped template’s. The plan is a pure function of that config:

  • per scheme, its directory, a _template.md, and a README.stub;
  • per journal and per fragment directory, its directory and template;
  • a docs README.md whose view list is generated from the config, so the index and the record cannot disagree about what exists on day one;
  • the agent file and CI workflows from the template, unchanged.

Richness follows doctrine, not mechanism. The ADR scheme and a document-rendered DP scheme get the shipped templates — the decision doctrine and the seed principles are the content this package genuinely has to offer a record. Every other scheme gets a neutral template and a stub titled after itself, because Luria has no opinion about what an RFC should say. The neutral template uses the {PREFIX}-NNN placeholder that luria new already substitutes (ADR-036) — one contract, both directions.

Never-overwrite stands: existing files are skipped and reported, re-running fills only holes. The one hard refusal: --config against a project that already has a luria.toml is an error, not a skip. A skip would scaffold the new config’s directories while the old config governs every other command — a record whose own machinery doesn’t know about half its tree.

config.load gained a text= parameter so the plan (and --dry-run) can be computed from a config that is not on disk and, under dry-run, never will be.

Alternatives considered

  • Keep the fixed-tree copy. The status quo, and the wart: init ignored the one file that says what this record is. Every custom adoption started with an apology tour of renames and deletions.
  • Flags instead of a fileluria init --scheme RFC --journal incidents. Re-states in a second vocabulary what luria.toml already says, and the flags drift from the file the moment the config grows a key the flags don’t cover. The config is the interface (ADR-006).
  • Rich per-scheme templates. Generating decision-grade doctrine for an arbitrary prefix means inventing opinions Luria doesn’t hold. The neutral template is honest: frontmatter the lint demands, a placeholder luria new fills, and nothing pretending to be doctrine.
  • Generate the docs README prose too. The view list is derived — it must match the config — but the surrounding prose stays authored in the template file, where it is browsable and editable, with a {views} placeholder marking the one derived region.

Consequences

Config-first adoption works end to end, and the loop is pinned by tests: declare RFCs and an incident log, luria init --config, luria new rfc, luria index, luria lint — clean, with no decision directory anywhere.

Automating “the three documented commands, run as documented” as a test immediately caught two template defects — a bare LU-ADR-048 and a bare DP-1 that scheme-driven reference detection (ADR-046) could now see — meaning a fresh scaffold would have started red. The guard that found them (test_a_fresh_default_init_lints_clean) stays as the pin for that whole class: the adoption guide is a script, and CI now runs it.

What this obliges: the template’s docs/README.md carries a {views} placeholder, so the template is no longer a literally-copyable tree — it is input to a planner. luria init remains the only consumer, and the never-overwrite loop is unchanged.