A project that declares no fragment directories inherits one, and `record.md` documents it as real
- Dominant language
- Python
- Stars
- 21
- Forks
- 2
- Avg merge
- 6h 33m
- Merged PRs (30d)
- 91
Description
Hit on 0.6.0 while converting an adopting project's changelog from a fragment directory to a journal, so that no view consumes its sources. Once `[luria.fragments]` was removed from `luria.toml`, the project still had a fragment directory — one it had never declared, pointing at paths that do not exist — and `record.md` documented it as real.
Characterized per ADR-041; not fixed, since the response is a design call.
## Minimal working example
A project with one journal and no fragment directories:
```console
$ mkdir mwe && cd mwe && git init -q .
$ cat > luria.toml <<'TOML'
[luria.paths]
docs = "docs"
decisions = "docs/decisions"
design_principles = "docs/design-principles.md"
reports = "docs/reports"
[luria.journals.changelog]
dir = "changelog.d"
output = "docs/changelog"
TOML
$ mkdir -p changelog.d docs
$ luria index
Wrote 5 file(s) from 0 ADRs, 0 changelog entries.
```
The project has no `record/` directory at all:
```console
$ ls -d record/changelog.d
ls: cannot access 'record/changelog.d': No such file or directory
```
But the generated record says it assembles one:
```console
$ sed -n '/## Fragment directories/,/^## /p' docs/record.md
## Fragment directories
One file per contribution, assembled into a single document and then consumed. …
| directory | assembled into | style |
|---|---|---|
| `record/changelog.d/` | `CHANGELOG.md` | append |
```
Confirmed in the loaded config, not just the rendering:
```console
$ python -c "from luria.config import current; print({k: str(v.target) for k,v in current().fragments.items()})"
{'record/changelog.d': 'CHANGELOG.md'}
```
**Expected:** a config that declares no fragment directories has none, and `record.md` says so.
**The workaround** is to declare the table and leave it empty — which reads like a no-op and is in fact the only way to express "none":
```console
$ printf '\n[luria.fragments]\n' >> luria.toml
$ luria index && sed -n '/## Fragment directories/,/^## /p' docs/record.md | tail -1
*None configured.*
```
## Mechanism
`DEFAULTS["fragments"]` in `luria/config.py` carries `{"record/changelog.d": "CHANGELOG.md"}`, and the config loader deep-merges the project's TOML over `DEFAULTS`. A deep merge cannot distinguish *absent* from *empty*, so omitting the table inherits the default while an explicitly empty table overrides it. Omission is the spelling every adopting project reaches for first, and it is the one that silently means "give me a fragment directory".
## Why it is more than cosmetic
`collect_dir` unlinks every fragment it reads. Here the phantom target does not exist, so `luria collect` prints `No fragments to collect.` and is harmless. It stops being harmless for any project that has a `record/changelog.d/` on disk for an unrelated reason — a partially-adopted scaffold, a leftover from a migration, a directory someone created by hand. That project never declared a fragment directory, and a `luria collect` it never configured will assemble and then **delete** the contents. The destructive path is reachable from a config that never asked for it.
It also makes `record.md` wrong in the one way that document is supposed to be immune to. Its whole promise is that it is generated from `luria.toml` and therefore cannot drift from it — but the drift here is *inside* the config load, so the generated view faithfully reports a directory the author never wrote.
## Affected sites
- `luria/config.py` — `DEFAULTS["fragments"]`, and the deep merge that makes absent-vs-empty indistinguishable.
- `luria/record_doc.py` — renders the inherited entry into the "Fragment directories" table.
- `luria/collect.py` — `collect_dir`, the destructive consumer reachable through the inherited entry.
## Suggested response
ADR-035 rung: **fix the code**, but the shape is a design call.
The suggestion, offered as one option: move the fragment-directory default out of the loader's `DEFAULTS` and into the **scaffold** — have `luria init` write `[luria.fragments]` into the generated `luria.toml` with the changelog directory in it. A new project then gets exactly the same behaviour it gets today, but the default is *visible in the file it lives in*, and deleting it means what deleting it looks like it means. What a project's record does would then be fully readable from its config, which is the property `record.md` already claims.
Journals are worth checking for the same pattern — the default config names one, and `outputs_for` already guards against rendering an empty index for a journal a project never files into, which suggests the same absent-vs-empty ambiguity was met once already and solved downstream rather than at the load.
Contributor guide
Research direction
Start with DEFAULTS["fragments"] and the deep-merge path in luria/config.py, then trace how the loaded entry reaches luria/record_doc.py and collect_dir in luria/collect.py. Reproduce the minimal working example and review ADR-041 and ADR-035 before choosing the configuration behavior. Done means an omitted fragments table does not create an entry in record.md or expose an undeclared directory to collection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100