A field forbidden by another field's value — the mirror `required_when` never got
- Dominant language
- Python
- Stars
- 21
- Forks
- 2
- Avg merge
- 6h 33m
- Merged PRs (30d)
- 91
Description
Out of the discussion on #188: making illegal states hard to express, rather than reporting them once written.
## The gap, verified
An `Active` document naming what superseded it **lints clean today**:
```yaml
status: Active
superseded_by:
- ADR-002
```
```
luria: docs lint clean
```
It claims to be in force *and* to have been replaced. Nothing in the config surface can say that is nonsense.
## Two thirds of a constraint language already exist, in two grammars
- **`required_when`** — a field demanded while another field says one of these things. `superseded_by` required when `status` is `Superseded`.
- **`excluded_by`** on a tag group — *"Tags that forbid this whole group. `sound` excluding the failure modes is the motivating case: naming how an argument fails contradicts saying it does not."*
`excluded_by` is already "make an illegal state unwritable". It just only works over tags, and only forbids a whole group.
## What is missing
The mirror of `required_when`:
```toml
[luria.schemes.ADR.fields.superseded_by]
required_when = { status = ["Superseded"] }
forbidden_when = { status = ["Active", "Proposed"] }
```
Same shape, same validation path, opposite sense. A document in a forbidden combination is a violation, not a report — unlike #188, **every instance is individually wrong**, which is what makes it a rule rather than a smell.
## The design question worth settling first
`required_when` and `excluded_by` are two spellings of adjacent ideas, and adding `forbidden_when` makes three. That is the two-grammar cost ADR-063 names, paid a table at a time — and ADR-076 already rejected a parallel `vocabularies` table on exactly this ground.
So the question is whether this is one mechanism or three:
- one condition type with a sense (`required_when` / `forbidden_when`), and `excluded_by` restated in it;
- or `excluded_by` is genuinely different because it constrains a *group of values* rather than a *field*, and should stay.
Worth deciding before implementing, since the cheap version is a third table and the right version may be a consolidation. Note #188's framing is relevant here too: a group-of-values constraint and a field constraint may be the same thing viewed from either end.
## Scope note
This is the *combinatorial* half of the state-machine idea. The *temporal* half — which transitions between values are legal — is separate and does not follow from this. Filed alongside.
Contributor guide
Research direction
Start with the existing `required_when` and `excluded_by` mechanisms, then read #188, ADR-063, and ADR-076 for the constraint-language context. Compare the proposed `forbidden_when` shape with those precedents. Done means the relationship between the mechanisms and the implementation scope have been decided before coding begins.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100