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-004
str(raw)          "['SCENE-001', 'SCENE-004']"
first code        SCENE-001        checked, resolved
SCENE-004                          never looked at
finding           none
edge (ADR-tmpxmnac)                one, to SCENE-001

A 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 source papers is a modelling error the anthology wants caught, and this would pass it. The declaration is the point.
  • min / max cardinalities. 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. many is the smallest key that answers the evidence, and a count can be added beside it later without changing what many means.
  • 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.