31. A device is an address space, and it may know which of its readings are real¶
Context¶
docs/expansion_ideas/HANDOVER-denckring-method-zoo.md surveys combinatorial devices
across six traditions and ranks what it proposes by value per unit of change. Its top
two are not new catalogue entries. They are schema changes to the devices already here,
and its own argument for them is that both arrived from traditions this project had not
modelled and both improve the generator side.
A device has always been an odometer and never said so. Device.combinations
multiplies the slot lengths, which is the size of an address space, and nothing else
treated it as one. There was no way to ask for the reading at a given index, or for the
index of a given reading — so the one structural fact every device in this catalogue
shares was expressible only as a count.
A device produces garbage in bulk and had nowhere to say so. denckring.apply spins
the rings and returns whatever comes up. Measured here: 0 of 20,000 spins of
Harsdörffer's rings is a word the German lexicon knows, while 4.40% of German nouns
are spellable on those rings. Both numbers are true and they are about different
questions; nothing in the schema could hold either.
A device's published totals disagree, and the disagreement lived in a YAML comment. Three figures circulate for the Denckring — 82,944,000, 97,209,600, 101,606,400 — and this package computes a fourth, 103,680,000, from the transcription it ships.
Decision¶
D1. Device.at and Device.address are the two directions of an index. Piṅgala's
Chandaḥśāstra (c. 3rd–2nd c. BCE) names both — naṣṭa is address to pattern,
uddiṣṭa is pattern to address — which makes the odometer reading of these devices an
attested procedure rather than a modern gloss on one. Device.radix is the mixed base
they run in, and combinations is its product.
The first slot is the most significant digit, and that is a choice. Incrementing the address turns the last ring, which is what an odometer does and what the reading order suggests. Sources differ on which end of a prastāra row carries the low-order position, so this is stated in the docstring and pinned by a test rather than presented as the convention. A caller who wants the other numbering is doing arithmetic this package does not do for them.
address raises for a reading the device cannot produce rather than returning a nearby
index, because a wrong address is indistinguishable from a right one.
D2. Device.mask records which readings are attested, and holds rather than drops.
Al-Khalīl ibn Aḥmad al-Farāhīdī's Kitāb al-ʿAyn (8th c.) enumerates every ordering of
a root's consonants and then marks which are realised and which are muhmal, neglected.
unmarked_policy defaults to hold because that is what al-Khalīl does: he does not
discard the neglected forms, and the distinction is the intellectual content of the
device rather than an implementation detail.
mask.source names a capability, not a file — lexicon.words is a question a pack
answers, and which pack is installed is the caller's business.
D3. Reading the mask is opt-in, and refuses in both directions.
denckring apply takes attestation: ignore | mask, defaulting to ignore. Asking for
mask on a device that declares none raises InvalidParams; asking it of a pack that
cannot answer raises MissingCapability naming the capability. Neither is silently
ignored.
The default is ignore so that a core-only install spins exactly as it always has.
Making the mask the default would give denckring apply a data dependency it has never
had, on the strength of a field in a YAML file.
D4. denckring now spins max_results times rather than once. A device that flags
which of its own outputs are real can only do so if it has produced more than one.
apply still reads texts[0], which under attestation: mask is an attested word when
any spin found one — validation and generation in one pass, which is the handover's
argument for the mask.
D5. Device.disputed_totals is a field, not a comment. Each entry carries the value,
who asserts it, and a note. combinations stays computed. Recording is not adopting: the
97,209,600 that circulates most widely is not divisible by 144 and so cannot be a product
of rings of 12 and 120 at all, and a survey attributes it to Leibniz's own 1666
calculation without a primary citation. Adopting that attribution would mean asserting
Leibniz computed a number that is not the product of the rings as the same sources
describe them.
Consequences¶
The layer split is not taken here. (It was taken on 2026-09-01; see ADR 0033.) The handover's §3 proposes splitting the collection into
Verfahren (run it, get text) and Instrument (the mechanism is faithfully formalised),
with separate counters and the 500 target belonging to the first. Nothing here needs that
decision: no catalogue entry is added, and both changes are to devices that already ship.
The handover's own kill criterion for that layer — "if after twenty Instrument entries
none has changed how a Verfahren entry is modelled, the layer is decoration" — names
these two as its test cases, and both came from the instrument side and improved the
generator side. That is evidence for the layer, gathered without committing to it.
The mask is close to useless on the shipped rings, and the number is the point. With
drop, a spin of Harsdörffer's rings returns nothing 20,000 times out of 20,000, so the
generator raises DegenerateOutput. That is not a defect to fix: it is what the device
does, and it is why hold is the default. The mask tests run against a synthetic
four-reading device, because a device that attests sometimes had to be built rather
than found.
Device.for_line carries the mask and not the disputed totals. A mask is a claim
about readings, which a line has; a disputed total is a claim about the whole device, and
attaching it to one line would assert that the literature disputed a number nobody
published.
Two more devices could now declare a mask and do not. poesieautomat_2000 composes
lines rather than words, so lexicon.words is the wrong oracle for it, and no capability
in this project answers "is this a German sentence". Left undeclared rather than
declared with an oracle that cannot answer.
The address protocol is unexercised by any procedure. at and address are public
API with tests and no caller inside the package. That is deliberate — the handover wants
enumeration and the index pair as primitives the rest builds on — but it is a cost:
untested-in-anger code, kept honest only by its own round-trip property.
Amendment, 2026-08-31: corpus-derived rings, and why they are not a catalogue row¶
Device.from_text builds a device whose rings are filled from a text the reader
supplies — the handover's §1.1 "corpus-derived rings", which it calls the only
genuinely contemporary capability in the whole set and the one needing no scholarship.
Structure fixed and enumerable, content input-dependent; radix, at and address
work on it unchanged, which is what D1 was for.
It gets no catalogue entry, and the reason is a hole in the schema rather than a
judgement about the technique. attribution offers primary, reference and
traditional, and attested offers author-stated, codified and reconstruction.
None of the six means contemporary, with no source. The handover says so itself: "no
historical precedent". Cataloguing it would put a technique this project invented beside
Harsdörffer's rings under provenance fields that would have to lie, in a catalogue whose
tests enforce those fields.
So it is a device constructor plus scripts/rings_from_text.py, and the devices it
writes are read by the rows that already exist, through the DENCKRING_DEVICE_PATH
cartridge mechanism load already documents. denckring apply poesie_automat -p
device=notes runs on rings drawn from notes.txt with no row claiming anything.
This also answers a cost the Consequences above recorded. at and address were
public API with tests and no caller; a derived device is enumerable by construction and
is the first thing that makes the pair worth having to someone outside this repository.
Two costs of its own, stated. The derivation must be total or a generator produces
output its own checker rejects, so ties are broken by first appearance and a test pins
that — a set iteration here would be a heisenbug in a row's verdict. And
drop_commonest is the whole stoplist: this package ships none, a real one is
per-language data, and dropping the n commonest types is the crude standard
substitute. At its default of 0 the rings fill with function words, which is a faithful
reading of the text and rarely the wanted one.
If the schema ever grows a value for "contemporary, no source" — the layer split in
§3 of the handover is where that would be decided — this is the first entry that would
want it. ADR 0033 took that decision and declined: an instrument must be formalised and
sourced, so a mechanism invented here fails its bar too, and from_text stays a
constructor catalogued on neither layer.