29. Where the catalogue already offered a choice, the parameter now exists¶
Context¶
Three places where this library's own metadata told a caller they had a choice the code did not give them, plus one verdict more confident than its evidence.
A chosen dictionary. The n_plus_7 row's definition has read "Replace every noun
with the seventh noun following it in a chosen dictionary" since the row was written,
sourced to Lescure in La Littérature potentielle (1973). NPlus7Params offered
offset and source. The list was pack.nouns() and nothing else, so the choice the
definition names in so many words was the one thing a caller could not make.
French. Lang has been Literal["en", "de", "fr"] since the first release, and
every one of the 154 catalogue rows carries a French name and a French definition.
get_pack("fr") raised UnknownLanguage, whose message tells the caller to install
denckring[fr] — a distribution that does not exist and was never planned. The error
named the language when what was actually missing was a lexicon, which is the wrong
layer: it says "French is not supported here" where the truth is "seventy of these
procedures need nothing French does not have".
Exact letters only. anagram accepts a candidate that spends every letter of its
source and refuses one that spends some of them. That is the reading its catalogue
definition takes and a defensible default, but it was also the only reading available:
a caller wanting the transposal convention — a text built from some of the source's
letters, never from letters it does not have — had no way to ask, and no way to make
check grade one.
And one verdict. displacement_report accepted an unchanged word that the noun
list carries, because a word list cannot tell you that run is a verb in this
sentence. Accepting is a reasonable call and it was made silently: the count went out
as ambiguous_words, in the honest tradition of estimated_words, but the score had
already been decided by a policy nobody chose and nobody could change.
Decision¶
Four parameters, one pack and one field. Each is placed where the surface it belongs to already is, rather than beside it.
dictionary: list[str] | None goes on NPlus7Params — the check model — and
SPlus7Params now inherits NPlus7Params whole rather than restating offset, so it
gains the field for free. Entries are validated to a non-empty list of single
alphabetic words, which is ADR 0015's rule for nouns() applied to a supplied list for
the same reason: a displacement has to come back from the tokeniser whole.
resolve_dictionary returns the list and the lookup together, because pack.noun_index
lemmatises and a bare list has no lemmatiser behind it — casefolded matching is the
most a supplied list can honestly offer, and it is stated rather than implied.
ambiguous_nouns: "undecidable" | "free" | "strict" decides what an unchanged listed
noun does to the score: leave the position unweighed, accept it (the default, and what
shipped before), or fail it. The three names and their meanings are taken verbatim from
RhymeParams.unknown_rhyme, which solves this exact shape for a different undecidable;
a caller who has met one has met both.
allow_subset: bool goes on AnagramParams, also the check model, because the
convention it names is what counts as a valid anagram. A generator working to one rule
while the checker judged by another is precisely the drift ADR 0025 gave apply a
spine to prevent. It relaxes one half only: a shortfall stops being a violation, a
surplus letter never does. Relaxing both would leave a check no text could fail.
FrenchPack is a built-in default beside English and German, which is ADR 0022's
shape and the argument it made for German unchanged: core carries the languages whose
lexicon-free procedures already work, so a data distribution upgrades a default rather
than introducing a language. It ships no data files and declares tokens, alphabet,
fold_diacritics and letter_shapes. œ and æ fold to oe and ae explicitly,
because unlike ß neither has a casefold mapping nor an NFKD decomposition, and an
unfolded œ is one letter no anagram of oeuvre could match.
Description.runs_in reports the languages this install can actually check a row in,
computed from the packs' capabilities rather than authored.
Consequences¶
dictionary is a checker parameter, so every caller of describe now sees it.
That placement is forced — check must walk the same list apply walked, or the
generator produces text its own checker rejects, which is the defect class this
project's central property exists to catch — and the cost lands on the caller who
never wanted it: params in describe and in MCP's describe_procedure carries a
dictionary field for n_plus_7 and s_plus_7 that most callers must read past and
ignore, and the JSON Schema for a list of strings is not small. An apply-only
parameter would have been invisible to them and wrong.
A supplied dictionary does not unlock N+7 for a language whose pack has no noun
list. require_capability runs before parameters are parsed, so lexicon.nouns is
refused first and the dictionary the caller brought is never looked at. Every
ingredient is present and the call still raises MissingCapability. Fixing it means
conditional capabilities — a requirement a parameter can satisfy — which inverts
two steps of the spine every procedure inherits and would have to be right for all 119
rows rather than for this one. Not attempted here; the limitation is written into
NPlus7Params beside the field so it is found where it bites.
ambiguous_nouns defaults to free, which is today's behaviour and arguably not the
most honest default. undecidable is the reading that matches what the evidence
supports — a word list genuinely cannot decide the position — and it was rejected as a
default only because changing it changes published verdicts on a row people already
check against, and a verdict change deserves its own decision rather than a free ride
on the parameter that makes it expressible. This is recorded as an open question, not
as a settled preference for free.
Under undecidable, a text whose every position is undecided fails rather than
passing. _report scores total == 0 as 1.0, which is right for the wordless case
it was written for and wrong here: the text has words, none of them was ever weighed,
and a satisfied report with nothing behind it is the same overconfident verdict the
reading exists to refuse. It returns an ambiguous_nouns_undecidable violation
instead. The cost is a surface that reads oddly — the most cautious reading is the one
that can fail a text no reading could decide — and the alternative was worse.
anagram under allow_subset has the identical shape for the identical reason: a
candidate with no letters is not a transposal of a source that has some, so
empty_transposal fails it rather than letting a zero denominator score 1.0.
FrenchPack ships no lexicon, so this moves the failure to the right layer rather
than removing it. Measured on this install: 70 of the 119 implemented rows run in
French, and the other 49 do not, wanting syllables.heuristic (29 rows), phonemes
(21), stress (17), lexicon.words (6), lexicon.glosses (2) and lexicon.nouns
(2). A reader who takes "French is a core language" to mean the catalogue runs in
French will meet MissingCapability on two rows in five. What improved is only that
the message now names the capability instead of naming the language — and one thing
got worse: UnknownLanguage was a single, obvious wall, where the new answer is
per-row and has to be asked for row by row, which is what runs_in exists to make
askable in one call.
And the message a French caller now gets used to name a distribution that does not
exist. MissingCapability interpolated the language into pip install
denckring[{lang}], which was harmless while the only languages reaching it had a data
package and became a false promise the moment French did not — this chapter's own
subject, shipped in an error string: 49 rows now fail through that message in French,
each one telling the reader to install denckring[fr]. The remedy is now conditional on
a distribution existing, and says "No data distribution supplies it for 'fr'" otherwise.
The cost of the fix is a list of two languages in core/errors.py that a third data
package must be added to, chosen over reading packaging metadata for the reason
denckring.lang keeps the packs themselves as built-in defaults. What is not fixed,
and is older than this chapter: naming a language's extra still assumes that extra
supplies the missing capability, and denckring[de] carries no stress.
allow_subset changes the ranking key for everyone, not only for callers who set
it. letters_used descending is now the primary key, ahead of the three ADR 0028
describes, because under the flag every single word that fits the source is a valid
transposal — 373 of them for astronomer — and ranked by word count first, one-word
fragments would bury every cover worth reading. With the flag off the key is constant
across covers, so the effective ordering is exactly ADR 0028's and provably so: the
flag-off order is held by a regression test rather than by this argument. The cost is
that the code path is shared. One ranking now serves two conventions, a future change
to it touches both at once, and ADR 0028's Consequences describe three keys where
there are four — that ADR carries an amendment pointer to here rather than being
rewritten, since it is a dated record of what was decided then.
Measured, so that "the flag costs nothing to run" is not taken further than it goes:
astronomer visits 747,769 nodes with allow_subset on, identical to off, because
the flag records a cover at nodes the walk already visits rather than descending
anywhere new. What it multiplies is results, not work — astronomer goes from 1,421
covers to 15,185 and dormitory from 48 to 742, each one fewer out of produce,
which drops the identity cover. dormitory's top three are dirty room, dirt roomy,
dirty moor under both readings.
Production.truncated merges two meanings, and allow_subset makes the quieter one
the common case. The field is true both when a search abandoned its own budget and
when max_results capped the list — a merge that predates this chapter (ADR 0027 gave
Produced its own truncated for the first of the two) and that this chapter makes
routine rather than rare: against the default max_results of 10, dormitory returns
truncated: true from 48 covers with the flag off and from 742 with it on, and the
alarming reading of the field is the one that did not happen. Separating them means a
second field on Production, which is additive and not attempted here; until then a
caller cannot tell a capped result set from an exhausted budget, and should read
truncated as "there is more" and not as "something went wrong".
describe now carries two language fields that answer different questions.
languages is authored editorial scope — wechselsatz is German by nature, not merely
by capability — and runs_in is computed. anagram is where they visibly diverge:
languages: [en], runs_in including de and fr, because its checker needs only
tokens and fold_diacritics. A reader can confuse the two, and the JSON gets a field
whose value depends on what is installed where every neighbouring field does not.
Collapsing them into one was the alternative and it loses the editorial claim, which is
a claim about the form and not about this machine.
The catalogue row's definition still says "exactly", and stays that way. "A text
using exactly the letters of another, rearranged" now describes the default reading
rather than every reading check will grade, and the honest options were to loosen it
or to leave it. It is left, for four reasons: no other editorial parameter in this
project appears in the definition it modifies — fold_diacritics (ADR 0009) is named
in none of the definitions of the rows that carry it, and unknown_rhyme in none of the
rhyme rows'; no count is quoted because the durable claim is "none of them", and a count
is the half that goes stale; the definition matches what an unparameterised call does, which is what a
definition is for; the row's prompt_hints.en says "using each exactly once" and is
what a model is handed, so loosening one without the other splits the row against
itself; and a transposal is arguably a neighbouring form rather than a looser reading
of this one, which would mean a row of its own with a source, an attribution and an
attested value — and this chapter has no citation for the convention beyond the
practice's own vocabulary. Inventing one is what ADR 0018's provenance rules exist to
prevent, so nothing about the row's provenance changes here. The admitted cost: a
caller who passes allow_subset=true is running something the catalogue's definition
does not describe, and only the parameter's own description says so.
The round-trip suite's budget was never a floor, and now is one. Worth recording
because four properties in this chapter rest on it: max_examples=1000 read as the
measured cost of the rarest reachable row and was not measured at all. Replayed over
twelve fixed seeds, the flat-only strategy reached every row on 3 of 12 —
recombination missing on nine of them, every_nth_word on three — so the number was
one lucky derandomize seed rather than a guarantee. TEXT now composes the flat draw
with two built shapes, blank-line paragraphs and terminated sentences, which reaches
every reachable row on 60 of 60 replayed seeds at 300 examples; the budget is 600,
twice that. The cost is that composing branches narrows the flat draw — st.one_of
dedupes by identity and does not sample uniformly, measured at about 20% for a flat
branch against 45% for the prose one — so spoonerism, the row that wants flat text,
is the row this composition starves and the first to disappear when the budget is cut.
Out of scope, and unchanged by this chapter: conditional capabilities, above;
a dictionary parameter for anagram, whose search walks the graded list and whose
max_size is bounded to what that data holds (ADR 0028); changing
ambiguous_nouns' default, above; a third verdict state on Report for
"could not decide", which undecidable approximates with a violation because
satisfied is exactly score == 1.0 and a third state would move a contract the
README names; and a French lexicon, which is a data distribution and a licence
question, not a parameter.