Context
This document lints clean:
status: Active
superseded_by:
- ADR-002It claims to be in force and to have been replaced. The record can demand a
field in a state (required_when, #170): superseded_by is required while
status is Superseded (ADR-071), and since ADR-085 that rule is a built-in
declaration the scheme can replace. Nothing can say the converse, that a
field is nonsense in a state. #191 verified the gap; it still held on main
a month later, after ADR-098 moved value groups onto any field and #141
compiled every obligation into one contract.
What could have expressed it, and why each falls short:
groupswithexcluded_by(ADR-054, ADR-098) compares values within one field.statusandsuperseded_byare two fields.field_groupswithat-most-onecounts which fields are present;statusalways is.- A derived field is a
str.formattemplate with no conditionals.
The design question #191 raised first: required_when and excluded_by
are adjacent ideas in two spellings, and a third key risks the
grammar-per-table cost ADR-063 and ADR-076 name. One mechanism or several?
Decision
forbidden_when takes the condition required_when takes, with the
opposite sense.
schemes:
ADR:
references:
superseded_by:
scheme: ADR
many: true
required: false
required_when: {status: [Superseded]}
forbidden_when: {status: [Active, Proposed]}- One condition type, read and validated by one function, in every table a
field is declared in (
references,fieldswith or without a vocabulary). The field it names and the values it lists are checked at load against what the scheme can say, exactly as forrequired_when: a misspelled field or value is a config error, not a rule that never fires. - The condition is read through the contract’s effective value: a status
carrying a note is that status, and a vocabulary field with a
defaultthat a document omits reads as the default. - A document that carries the field (any non-empty value) while the condition holds is a violation, and the finding names the value that turned the rule on and the key that declared it. Every instance is wrong on its own, which is what separates a rule from a smell (#188).
- Declarations that contradict themselves are refused at load:
required: truewithforbidden_when(always demanded and sometimes forbidden),required_whenandforbidden_whenon the same field and overlapping values (demanded and forbidden at once), andforbidden_whenon a field with adefault(a default is never absent, so the rule would fire on documents that never wrote the field).
The built-in successor field forbids itself while the document is in
force. contract.built_in already gives every scheme superseded_by,
required while status is the scheme’s retires_on word. It now also
carries forbidden_when: {status: [<active>]}, using the scheme’s own
active word, so #191’s case fails in every record with no config. Only
the in-force word: whether a Proposed, Deferred or Rejected document
may name a successor is a record’s own call, and a record that wants more
declares the field in its references table, which replaces the built-in
outright (ADR-085).
excluded_by stays as it is. A group constrains which values of one
field may be combined; a condition constrains whether a field may be
present, given another field’s value. Restated as a condition, “sound
excludes the failure modes” would be a field forbidden by its own value,
which says nothing about which values were meant. The two keys now divide
cleanly: *_when is presence against another field, groups is
combination within a field. ADR-098 is what made this division visible,
because both now attach to a field.
Alternatives considered
- A predicate language (
and,or,not, comparisons). It would cover the temporal half and every combination. But the value of the existing condition is that a reader sees the rule in the line, and a config that can state arbitrary predicates is one nobody reads at a glance. Two conditions on one field are still out of reach. That is the same limitrequired_whenhas, and no case has needed it. allowed_when, a whitelist (superseded_byallowed only whileSuperseded). Tighter for the motivating case, but it makes a record enumerate every legal state, so a new status word silently becomes a state in which the field is forbidden. A forbid list fails the other way: a new word is unconstrained until someone says otherwise, which is the default everywhere else in the contract.- Folding
excluded_byinto the condition. The consolidation #191 asked about. Rejected above: it is a different unit (values within a field, not presence of a field), and restating it would need a condition over list membership on the field being constrained. - A report instead of a violation. Reports are for findings that need
judgement and carry an acknowledgement directive. There is nothing to
acknowledge here: an
Activedocument with a successor is wrong every time. - No built-in,
forbidden_whenonly. Every record would have to declare the rule to get the check the issue was filed for, and the shippedADRscheme declares no references. - Status quo. The contradiction stays writable, and readers of the index and the typed edges see a document both in force and replaced.
Consequences
- An existing record holding an in-force document that names a successor
fails
luria lintafter upgrading. The finding says which field to drop or which status to change. Fixing it takes a judgement (was the document replaced or not?), so there is no--fix. - The record page (
docs/record.md) states the built-in rule alongside the standard fields, as it already does for the requirement. promote_vocabularycarriesforbidden_whenfrom a vocabulary field to the reference that replaces it, as it carriesrequired_when.- The temporal half of #191’s state-machine idea, which transitions between values are legal, is still out of scope. It needs history, which a condition over one revision cannot see.