Skip to content

Config-first adoption: declared families replace defaults, and init scaffolds from a config - #76

Merged
dmarx merged 5 commits into
mainfrom
claude/luria-capabilities-docs-gw2iny
Aug 12, 2026
Merged

Config-first adoption: declared families replace defaults, and init scaffolds from a config#76
dmarx merged 5 commits into
mainfrom
claude/luria-capabilities-docs-gw2iny

Conversation

@dmarx

@dmarx dmarx commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Follow-up to #75, resolving the two limits it could only document — and answering the question that reframed them: can luria init not take a config file as input? It can now, and the two changes turned out to be one thought: luria.toml becomes the actual interface to what a record is.

ADR-047 — a declared family replaces the default; settings still merge

One merge rule produced both documented limits: the shipped ADR scheme couldn't be removed, and a declared scheme's omitted output inherited docs/decisions — silently relocating the index on the documented adoption path. A limit that well-documented is a bug with good paperwork.

The fix is noticing the config has two kinds of table:

  • Settings tables (paths, code, lint, site) — keys named by Luria; partial override is the point → merge per key.
  • Family tables (schemes, fragments, journals, remotes) — entries named by the project; declaring one is authorship → replaced whole when declared, shipped default when not.

Rejected: a removal sentinel (ADR = false is three states for a two-state question) and replace-on-any-write ([luria.site.theme.light] would clear the site table).

The two limit tests pinned in #75 fired the moment the rule changed — the only failures in the suite — and are flipped into their inversions. examples/rfcs-and-specs now has no ADR scheme at all; examples/collocated collocates by omission, workaround line retired; examples/many-journals shows an undeclared family surviving.

Upgrading: a config that declared part of a family while relying on the rest from the defaults now owns what it declared. Template-scaffolded records always declared families in full and are unaffected; the changelog says so.

ADR-048 — the scaffold is planned from configuration

luria init was a fixed-tree copy that ignored the one file that says what a record is — a project whose luria.toml declared RFCs and no decisions still got record/decisions.d scaffolded beside it.

Now: --config PATH → the project's own luria.toml → the template's, and the plan is a pure function of the winner. A directory, template and view stub per scheme; templates per journal and fragment dir; a docs index whose view list is generated from the config. ADR/DP keep the shipped rich templates (the doctrine is content Luria actually has); any other scheme gets a neutral template on the {PREFIX}-NNN placeholder luria new already substitutes — Luria has no opinion about what an RFC should say, and the template is honest about it.

The proven loop, now a CI-run test:

luria init --config team.toml   # RFCs + incident log, no decisions
luria new rfc
luria index && luria lint       # clean; no decision directory anywhere

One hard refusal: --config against a project that already has a luria.toml exits 1 — scaffolding one config's shape while another governs the record builds directories the project's machinery doesn't know about.

The guard caught two defects on its first run

Automating the adoption guide's three commands as a test immediately failed on a bare LU-ADR-048 I had just written into the template, and on a bare DP-1 sitting in the principles stub since before scheme-driven detection (#75) could see it. A fresh scaffold would have started red — the worst first lesson a reference-linting tool could teach. Both linked; the test stays as the pin for the class. Also fixed the sibling of #75's heading bug: an index scheme with no stub was titled # Architecture decision records whatever its prefix.

Verification

  • 380 tests pass (369 at branch point): +5 merge-rule units, +5 init end-to-end, flipped pins, hardened adr_paths()
  • All four init paths fired by hand before being pinned; luria lint clean, luria index --check current, luria site stages
  • ADR-047 and ADR-048 filed Proposed; devlog carries the round, including the same-branch changelog-fragment collision after a squash merge (hand-named this round's fragment — worth a naming convention if branch-restarts become a habit)

Generated by Claude Code

claude added 3 commits August 11, 2026 23:40
One merge rule produced both limits the examples had to document: the
shipped ADR scheme could not be removed, and a declared scheme's omitted
`output` inherited `docs/decisions` from the default entry — silently
relocating the index on the documented adoption path. A limit that
well-documented is a bug with good paperwork.

The split is by what a table is (ADR-047). Settings tables — paths, code,
lint, site — keep per-key merge: their keys are Luria's vocabulary, and
partial override is the point. Family tables — schemes, fragments,
journals, remotes — are replaced whole when declared: their entries are
names the project chose, so declaring the family is authorship, and "you
get the ones you wrote" is the only reading under which a family can
shrink. Undeclared families keep the shipped defaults.

The two pinned limit tests fired when the rule changed — what pins are for
— and are flipped into their inversions: rfcs-and-specs now has no ADR
scheme at all, collocated collocates by omission with its workaround line
retired, many-journals shows an undeclared family surviving. Hardened for
the newly-legal shape: adr_paths() answers {} without an ADR scheme, and
the index-stub fallback names the scheme it renders instead of
"Architecture decision records" — the sibling of the document-stub bug,
found by the same examples.

Upgrading: a config that declared part of a family while relying on the
rest from the defaults now owns what it declared; the changelog fragment
carries the note. Records scaffolded by `luria init` always declared their
families in full and are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAGvXUrSmX597dCXGjgtvW
The scaffold was a fixed-tree copy, which ignored the one file that says
what a record is: a project whose luria.toml declared RFCs and no decisions
still got record/decisions.d scaffolded beside it, and there was no way to
hand init the config you actually wanted (ADR-048).

init now resolves a config — `--config PATH`, else the project's own
luria.toml, else the shipped template's — and writes the shape it declares:
a directory, template and view stub per scheme, templates per journal and
fragment directory, and a docs index whose view list is generated from the
config so it names exactly what this record renders. ADR and a
document-rendered DP keep the shipped rich templates; any other scheme
gets a neutral one built on the `{PREFIX}-NNN` placeholder `luria new`
already substitutes (ADR-036), because Luria has no doctrine about what an
RFC should say. Never-overwrite stands, with one hard refusal: `--config`
against a project that already has a luria.toml errors instead of
skipping, since scaffolding one config's shape while another governs the
record builds directories the project's machinery doesn't know about.
config.load gains text= so the plan and --dry-run work before anything is
on disk.

Automating the adoption guide's three commands as a test caught two
template defects on the first run — a bare LU-ADR-048 and a bare DP-1 that
scheme-driven detection (ADR-046) can now see — so a fresh scaffold would
have started red. Both linked; the test stays as the guard for the class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAGvXUrSmX597dCXGjgtvW
The configuration reference's "What is not configurable" list loses its two
ADR entries and says why; the adopting guide gains the three init modes and
drops the output-workaround warning; the examples README turns its limits
section into a demonstration of the replacement rule. ADR-046 gains an
unresolved-ok for its illustrative DP-17 — the detector it describes now
sees the code it quotes.

The devlog carries the round: the two-rule split stated as vocabulary
ownership, the pins firing as designed, the same-branch changelog-fragment
collision after a squash merge, and the template ceasing to be a copyable
tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAGvXUrSmX597dCXGjgtvW
Comment thread docs/devlog/2026-08.md

**Same-branch, second contribution: the changelog fragment collides.** The
branch restarted from main after the squash merge, so `luria new changelog`
would have reopened the *merged* fragment (named for the branch, which

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

named for the branch

why are we naming changelogs fragments from the branch? this is a solved problem. use the timestamped filenaming scheme we're using for the devlog. it basically ensures fragments never collide. we should just derive changelog's similar to how we do with devlogs. also, I'm wondering if we should merge PRs into an intermediate branch without firing documentation builds on the PR, then we can build documentation on the intermediate branch. That way, every PR doesn't come up as changing 20 files, which is confusing in reviews.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both points addressed in aa23377.

Fragment naming — done as you describe. luria new now stamps an unnamed fragment with its filing moment (20260812-021035.md), the identity the devlog already uses. Flat rather than yyyy/mm/dd/ nested, because the collector and the lint glob a fragment directory one level deep. --name stays as the explicit override and still reopens rather than duplicates. One behavioral note: branch naming gave "second ask on this branch returns the same fragment" — timestamps can't, so two asks make two fragments, and they collect into the same dated batch anyway, which preserves ADR-002's reading intent via the collector instead of filename discipline. ADR-036 carries it as a version bump with history (the scaffold choice stands; only the naming detail moved).

The 20-files-in-review problem — I took the cheap swing first. .gitattributes now marks the generated views linguist-generated=true, which GitHub collapses in the Files tab by default (with a "load diff" click-through). A contribution's diff then reads as its sources, and nothing about the committed-views architecture changes — one file, reversible.

On the intermediate-branch idea itself: worth weighing if the collapse isn't relief enough, but it has real teeth. (1) The record a reviewer approves is no longer the record that lands — views regenerate after review, and the rendered index/summaries are sometimes where wording problems show. (2) luria lint's staleness guarantee currently assumes sources and views travel together in one tree; splitting them across branches means the lint passes on a branch whose views are supposed to be stale, so the check needs a mode. (3) Merge conflicts in generated files move from the contributor (who can regenerate) to the bot (which has to). If you want to go that way regardless — the collapse genuinely doesn't help git diff locally, only GitHub review — it's an ADR-sized decision and I'm happy to draft it with these trade-offs as the alternatives section.


Generated by Claude Code

… review

Review on #76 called the fragment collision a solved problem being re-solved
badly, and it was: the devlog never collides because an entry's identity is
when it was filed, while a changelog fragment's identity was the git branch
— an address that stopped being stable the moment squash-merge-and-restart
became the workflow. `luria new` now stamps unnamed fragments
(20260812-021035.md), flat rather than yyyy/mm/dd/ nested because the
collector and the lint glob a fragment directory one level deep. `--name`
stays as the explicit override and still reopens rather than duplicates;
two stamped fragments from one contribution collect into the same dated
batch, so ADR-002's reading intent is preserved by the collector rather
than filename discipline. ADR-036 carries the change as a version bump with
history — the scaffold choice stands, only the naming detail moved.

For the twenty-changed-files review noise: .gitattributes marks the
generated views linguist-generated, so GitHub collapses them in PR review
and a contribution's diff reads as its sources. The views stay committed —
ADR-032's reasons stand — this changes only how review renders them. If an
intermediate-branch build is still wanted after this, it deserves its own
decision; the devlog names the trade to weigh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAGvXUrSmX597dCXGjgtvW
@dmarx

dmarx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

agree that it's an ADR-sized decision: I'm going to continue brainstorming outloud here. the issue is often conflicts over incremental codes taking the same the number across different branches. I'm wondering if maybe we should have a mechanism for issuing temporary codes, so within a branch the new docs can still be referenced by code, and then we can use a separate mechanism to concretize temporary codes into human-friendly incremental codes. so e.g. user wants to create a new document, so they run luria new adr and it generates record/decisions.d/adr-47fje0.md, so within the PR the user can sprinkle in references like [[adr-47fje0]]. in another PR, another user adds a fragment as well, [[adr-rn5gdh]]. they both merge together into the intermediate branch, no conflicts. Then the job that merges from the intermediate branch into main assigns them each real numbers, so adr-47fje0 becomes adr-123 and the other becomes adr-124, instead of them both claiming adr-123 and creating a conflict.

dmarx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

This is the same move as the fragment fix, one level up: identity at filing time shouldn't require global coordination, and the sequence counter should live at the one place merges serialize. Filing-time numbering is a distributed claim on a global counter; that's the bug, and temp codes are the right shape of fix. A few things fall out of thinking it through:

The concretizer already has a name in the record. ADR-040 (Proposed) is "renaming schemes and moving documents without losing the record's memory" — and temp→real is exactly rename-plus-rewrite-every-reference, which luria link --fix and the wikilink machinery already know how to do inside the tree. Building concretization is building ADR-040's rename pass with a specific rename policy. One mechanism, two consumers, and the migration command stops being hypothetical.

The sharp edge is citations the rewriter can't reach. By the time adr-47fje0 becomes ADR-123, the temp code is in the PR thread, review comments, commit messages (immutable), maybe another repo's LU--prefixed reference. If concretization renames, all of those go dead. If it aliases — the new file keeps aka: adr-47fje0 in frontmatter and resolution honors aliases forever — then a temp code is never a dead reference, just an old name. That's consistent with ADR-013's whole argument (identity survives retitling because the code, not the title, is the address) and it keeps ADR-014's contract: no code that ever meant something resolves to nothing.

Temp codes don't actually need the intermediate branch. Concretization has to run wherever merges serialize — but a merge queue, or the merge-to-main job running it per-PR, serializes fine. The intermediate branch buys batching (and your docs-build point); the collision fix is orthogonal to it. Worth deciding them separately so the smaller mechanism isn't coupled to the bigger architecture question.

Mechanical touchpoints, for scoping: Scheme.number_of/documents() only match numeric filenames today, so temp docs would be invisible to the index and lint on the branch — the temp shape needs to be first-class there (indexed, linted, citable), with a distinguishable pattern (adr- + base36, say, never ambiguous with \d+). And main gets the inverse guard: a temp code on main is a lint failure — always wrong, mechanically fixable (run the concretizer), so it qualifies for the failing tier under ADR-035's own admission rule.

One genuine improvement hiding in it: the template says numbering carries information — the order decisions were made. Filing-time numbers actually lie about that under concurrency (branch A files first, merges second). Merge-time assignment makes the sequence mean what it already claims to mean.

When it pays: a single-human flow rarely collides, and today's collision is at least loud (add/add conflict on the same filename). N concurrent agent branches is where it becomes real — which I gather is exactly the workflow here.

Happy to draft this as a Proposed ADR — temp codes + alias-preserving concretization as the decision, with the intermediate branch, filing-time numbering, and timestamp-codes-forever (the devlog answer, which trades away small citable numbers) as the alternatives section. Say the word and it lands as its own PR.


Generated by Claude Code

@dmarx

dmarx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

draft this as a proposed ADR, file the proposal with this branch, merge this change, start a new PR on a new branch implementing the proposal.

…zation point

Drafted from the review discussion on #76. Filing-time numbering is a
distributed claim on a global counter — two branches both mint ADR-123 —
and the fix follows the fragment fix one level up: identity at filing must
not require global coordination, so allocation moves to where merges
serialize. `allocate = "merge"` issues temporary codes (ADR-x47fje,
disjoint from the numeric pattern by construction) that are first-class on
their branch; `luria concretize` assigns real numbers in merge order,
rewrites the tree, and records each temp code as a permanent `aka:` alias
so nothing cited in a PR thread or commit message ever goes dead.
`concretize --check` guards the trunk under ADR-035's admission rule.

Proposed, not implemented — the implementation follows in its own PR, and
this record adopting merge allocation for itself is a separate decision
from the mechanism existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAGvXUrSmX597dCXGjgtvW
@dmarx
dmarx merged commit 5343187 into main Aug 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants