Context
derive refused many on any field, with a message that was true of the
shape it was written for and false of another:
`derive = "{tags}"` reads one value off a list, so the field holds one
— drop `many`
{tags[0]} does read one value off a list. {tags} does not: render()
returns the value itself for a lone field, so the derived field resolves to
the source’s whole list. The declaration said one and the value was many.
Nothing caught that, and the way it failed is the reason this is a decision
rather than a patch. contract.values_of reads a list on a scalar field as
None — “the shape contradicts the declaration” — and effective_values
passes the None through, so vocabularies._listing saw no values on any
document. A consumer that moved a scheme’s tags onto a derivation lost
every tag page of that scheme: fourteen files, no longer written, no
failure, nothing in the lint. A directory that empties is DP-15’s failure
exactly — nothing looks like current.
Decision
A template that is one whole field — no index, no attribute, no format
spec — holds what its source holds. derive.whole_field() is that test:
{tags} yes, {tags[0]} no, {published:.4} no, about {tags} no.
many is declared and checked, not inferred:
schemes:
NOTE:
fields:
tags:
derive: '{tags}'
from: paper
many: true
Two refusals, both eager, both naming the scheme the source lives on:
- the source holds a list and
manyis missing — “sotagsholds a list too — declaremany”, with what goes wrong if it does not; manyis declared and the source holds one value — “dropmany”.
The old blanket refusal survives for the shapes that genuinely render one value, with a message that now says which shapes those are.
Alternatives considered
- Infer
manyfrom the template and the source, declaring nothing. This is whatderive’s own docstring argues for — “the rule is the template’s shape rather than a flag, so nothing has to be declared twice” — and it is the right instinct for a same-document derivation, where the source’s cardinality is known when the field is compiled. It loses on the case that motivated this:from: paperreads a field on another scheme, and_fieldsruns while that scheme may not exist yet. Inferring would mean either rebuilding compiled fields after the load loop, or resolving cardinality lazily at every read. Declaring it costs one line and makes both mistakes findings rather than one of them a silence. - Keep refusing
manyoutright and teachvalues_ofto tolerate a list on a scalar derived field. Moves the contradiction from the config into the reader, where it becomes “a scalar field that is sometimes a list” — andvalues_ofreturningNonefor that shape is a deliberate guard against exactly the stringify-and-read-the-first-element behaviour #141 removed. - Status quo. A derivation cannot carry a list, so a scheme whose axis is derived silently has no tag pages. The silence is the problem; a refusal would at least have said so.
Consequences
plural_fields(scheme) is now one function rather than the same three-table
union written out in _check_derivations, and _check_target_fields takes
what the near scheme declared so it can compare against the far one.
Fired once on a real case before being trusted, as the working agreement
asks. In anthology-of-the-sota, moving NOTE.tags onto
derive: '{tags}' from: paper:
- without
many,luria indexdeletes all fourteendocs/notes/tags/*.mdand reports nothing — the failure this decision is about, reproduced; - with
many, twelve pages are written and two disappear for a real reason (no note carriesinference-optimizationormodel-stabilityany more, because the notes that did were disagreeing with their papers); - with
manydropped from the config, the load now refuses and names the field, the source, its scheme and the consequence.
Left open: the same silence is reachable by any other route to a list on a
scalar field, and values_of still answers None without saying so. A
finding for a document whose written value contradicts its declared shape
would close that, and is not this change.