dmarx / dmarx/luria

A field forbidden by another field's value — the mirror `required_when` never got

Open
#191 0 comments 0 reactions 0 assignees View on GitHub
enhancement help wanted
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.