ADR-075: A reference field declares whether it holds one code or many; a list where one was declared is a finding
Status Active · Filed 2026-09-03 · Issue #141
Context
A declared reference (ADR-060) is checked four ways: present, shaped like
a code, belonging to the named scheme, resolving to a document. The check
read the field’s value with str(raw) and took the first code the regex
found. For a scalar that is exact. For a YAML list it is this:
follows:
- SCENE-001
- SCENE-004str(raw) "['SCENE-001', 'SCENE-004']"
first code SCENE-001 checked, resolved
SCENE-004 never looked at
finding none
edge (ADR-tmpxmnac) one, to SCENE-001A record built for narrative world-building reported it: nine of its
fields are intentionally plural — a scene follows several scenes — and
every one was half-validated. The contract pass (#142) and the edge
derivation (#143) inherited the same str(raw) from the check they
replaced, so the defect predates the stack and the stack repeated it.
The failure is worse than missing plural support. A field the tool does not understand should be reported; this one was reported as fine.
Decision
A reference declares its shape. many = true on a reference says the
field holds a list of codes:
[luria.schemes.SCENE.references]
follows = { scheme = "SCENE", many = true }Three rules follow, and each is the one that reports rather than guesses.
A list where one code was declared is a finding, naming the count and
the fix (set many = true there if it should hold several). The tool does
not pick an element. A plural field’s elements are each checked, each
resolved, and each becomes an edge.
Required and plural means non-empty. required = true, many = true
rejects an absent field and an empty list alike; optional and plural
accepts both. The compiled contract carries the shape beside the type
(Field.many), where the record page and the findings read it.
A single value in a plural field is a list of one. Accepted, because it is unambiguous: one value is fully interpreted. The asymmetry with the scalar case is deliberate — a list in a scalar field leaves the tool guessing which element was meant, and guessing is the defect.
Alternatives considered
- Accept scalar-or-list on every reference. Fixes the silence with no
new key, and throws away the shape the author knows: a practice with two
sourcepapers is a modelling error the anthology wants caught, and this would pass it. The declaration is the point. min/maxcardinalities. The general form, and nothing has asked for “between two and five”. There is a measured need for one versus many; there is not one for counts.manyis the smallest key that answers the evidence, and a count can be added beside it later without changing whatmanymeans.- Refuse a single value in a plural field. Symmetrical, and it fails a record whose scene follows exactly one other and wrote it the natural way, for no gain the tool can name.
- Fix the coercion and reject every list. The minimal correction, and it turns nine working fields in a downstream record into nine findings with no way to declare them right.
- Status quo. The tool reports “fine” about a field it read halfway.
Consequences
An existing record with a scalar reference field is unchanged. One that
was quietly passing lists through now gets one finding per document
naming many = true as the remedy; the migration is a one-line config
change, and the finding says which line.
The compiled Field now carries required, reference and many as
three separate facts. The reviewer who reported this also observed that
reference: str | None is starting to want to be a type — Any,
Ref[Scheme], and a controlled vocabulary the same record has a measured
need for. That is a separate decision with its own evidence; this one adds
the shape and no more.
No field in this repository’s record is plural, so the guard is fired on fixtures here and on the downstream record that reported it.