A vocabulary's alert belongs to the set, like its blurb (#281) - #282
Conversation
0.25.0 shipped `alert` (#273) and the nested vocabulary form (#279) and the two had never been used together. They did not compose: the discriminator was spelled inline as {"label", "blurb", "values"}, so a vocabulary carrying an alert was refused — with a message about a value named `values`, describing a different fault entirely. The inline set is the smaller half. `alert` was on the FIELD, which was not a decision but where the Vocabulary object happens to be built; #273's own docstring says the opposite. It now comes from the central table beside `label` and `blurb`, so a vocabulary three schemes name carries one alert rather than three copies free to drift — the argument ADR-098 already settled and #279 already cited. VOCABULARY_KEYS is a named constant now, read by both the discriminator and the refusal message, which listed three keys while the dataclass carried four. A tag group's alert stays put: a group is declared inline and has no central table to move to. Fired on anthology-of-the-sota, which is what found it: `topics` declares label, blurb and alert together, one declaration reaches SOTA, LIT and THEORY, and a bad tag now prints the sentence #273 exists to print. 1318 tests pass, luria lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeX27AYgHU7geZaYkwshBS
This works, but it's pretty inelegant. Can't we use omegaconf's structured config to give us typing here? Also, I propose that instead of "values", we use "entries" or "members" or "terms" or something like that. |
… review) Two things review asked for, and both are right. **The key set comes from a dataclass now.** The first pass named a `VOCABULARY_KEYS` frozenset, which fixed the bug and left the cause: a hand-written list of the keys a table may carry, which is the same shape of thing as the inline set it replaced. `VocabularyTable` declares `label`, `blurb`, `alert` and `terms`; `KEYS` is read off its fields, and `_vocabulary_tables` builds one and reads its attributes, so a fifth key reaches the discriminator, the refusal message and the metadata in one edit. The ADR's own rejected alternative argued against deriving from `Vocabulary` — true, since that object also carries `many`, `required`, `closed` and `default`, which are per-field — but that is an argument for a second dataclass shaped like the table, not against deriving at all. I also tried the omegaconf route the review suggested, since omegaconf is already a dependency. It does work: merging a flat table against a nested structured schema raises ConfigKeyError, so try/except discriminates. It is still the wrong trade here — exception handling as control flow for an expected case, this project's tested refusal message replaced by omegaconf's, and one structured table in a module that hand-parses every other object it builds. Adopting structured configs across the loader is a real question and gets its own issue rather than riding this one. **The values live under `terms:`.** A controlled vocabulary has terms, and `values` was the commonest word for the entries themselves, which is precisely why a project naming one of them `values` was plausible enough to need a refusal. Breaking against 0.25.0, which is the only release carrying the nested form, hours old, with one unmerged branch using it — so a hard rename and no alias. 1318 tests pass. The refusal test now reads its expected keys off `VocabularyTable` rather than spelling them, so a fifth key fails it until the message names it. Fired on the real case: anthology-of-the-sota's held branch, respelled to `terms:`, loads and lints clean, and a bad tag still prints the set's alert under the violation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeX27AYgHU7geZaYkwshBS
|
Both taken. Pushed in f0b423b. The rename
Hard rename, no alias: 0.25.0 is the only release carrying the nested form, it's hours old, and the only record using it is on an unmerged branch. The typingYou're right that the frozenset was inelegant, and the inelegance was worse than it looked — it was a hand-written list of the keys a table may carry, which is the same shape of thing as the inline set it replaced. The ADR even argued against deriving from a dataclass, on the grounds that @dataclass(frozen=True)
class VocabularyTable:
label: str = ""
blurb: str = ""
alert: str = ""
terms: dict[str, dict] = dcfield(default_factory=dict)
VocabularyTable.KEYS = frozenset(f.name for f in dcfields(VocabularyTable))
On omegaconf specificallyI tried it rather than guessing, and my instinct was wrong — it does discriminate: So
The third is the real one, and it makes this too big for a four-key table to settle, so I filed it as #283 with what I'd want to look at first: port Checks1318 pass. Fired on the real case as well as the suite: Generated by Claude Code |
All five `Proposed` ADRs went into force when they merged; the status was lagging the code. `Active` here means "in force — the current answer, and what a citation should normally point at", and by that definition ADR-106, ADR-107, ADR-108 and ADR-109 have been Active since their releases, ADR-110 since #282 merged an hour ago. The report said they were cited nowhere and concluded that nothing depends on the answer. Plenty does — the citations were pointing at the wrong kind of object. Between them these five decisions account for `Reference.invariant`, the cross-scheme chain refusal, whole-field derivation cardinality, `alert` on a vocabulary and a tag group, the label/blurb/title pairs on nine config objects, and `VocabularyTable`. The code implementing all of it cites `#272`, `#276`, `#273`, `#279` and `#281`, forty-two times, and never once the ADR. An issue is where the argument happened: unversioned, statusless, and invisible to the reference check, which sees `#272` as text rather than a code. So any of these could be superseded tomorrow and nothing would flag the thirty-odd sites implementing it — the exact failure the check exists to catch, by a route it cannot see. `adr_index.py` already had the right shape in one place, `(ADR-103, #249)`. That shape is now at fourteen definition sites, one per (decision, file) pair — both codes, because the ADR is what is in force and the issue is where to read the argument. ADR-110 also goes to v2: its summary still described a VOCABULARY_KEYS constant and a `values:` key, both of which review replaced. A summary describing the first draft is worse than none, since the summary is what the index shows. Pending decisions: 5 -> 0. 1318 tests pass. Lint unchanged from main — the two unresolved placeholder codes are pre-existing and identical there. No behaviour change, so the changelog fragment is a stub saying so. Claude-Session: https://claude.ai/code/session_01FeX27AYgHU7geZaYkwshBS Co-authored-by: Claude <noreply@anthropic.com>
Closes #281.
0.25.0 shipped
alert(#273) and the nested vocabulary form (#279). They had never been used together, and they don't compose — the first record to adopt both is refused with a message about a value namedvalues, describing a completely different fault.The smaller half
alertisn't in that set. The message named three keys, the code allowed three, the dataclass carried four.VOCABULARY_KEYSis a named constant now and both the discriminator and the message read it — a list that has to be edited in step with a dataclass is one that won't be.The larger half
alertwas read from the field, not the set. That wasn't a decision; it's where theVocabularyobject happens to be built, and #273's own docstring says the opposite — "attached to the vocabulary rather than the field so it rides the one rule it describes."It now comes from the central table beside
labelandblurb. The rule an alert explains — whether the list is closed because it's finished — is a fact about the set, and a record whose three schemes name one vocabulary was otherwise writing the same sentence three times. That's the drift ADR-098 centralised vocabularies to prevent and that #279 cited when it movedblurb.A
TagGroup'salertis unchanged: a group is declared inline under the field whose values it constrains, and has no central table to move to. Not an inconsistency — a different shape of thing.Fired on the real case
anthology-of-the-sotais what found this. Withtopicsdeclaring all three keys, the config loads, one declaration reachesSOTA,LITandTHEORY, and a bad tag prints:That's #273's whole purpose reaching a real reader for the first time.
Checks
python -m pytest tests -q→ 1318 passed, 33 skipped. Three new tests: the two features composing, one alert reaching every scheme that names the vocabulary, and the refusal message listing every key the constant holds — that last one caught my own first attempt at the message fix, which silently didn't apply.test_a_closed_vocabulary_can_print_its_own_advicenow declares its alert on the set, which is also the first exercise of the nested form outside the tests written for it.luria lintclean. Views regenerated locally and reverted.Left open
Nothing checks that a vocabulary's
alertis meaningful only where some field declaresclosed: true. An alert on an open vocabulary is inert rather than wrong, and a finding for it would need to know which of the naming fields is closed.🤖 Generated with Claude Code
https://claude.ai/code/session_01FeX27AYgHU7geZaYkwshBS
Generated by Claude Code