Skip to main content

denckring

CI Coverage ≥ 97% Python 3.11+ Licence: Apache-2.0 AND CC-BY-4.0

A library of experimental writing procedures — lipograms, snowballs, acrostics and a hundred and fifty more — where the validator is the eval.

Every procedure pairs a generator with a validator, and the acceptance criterion is intrinsic: a lipogram either contains the forbidden letter or it does not. Nothing has to be invented to test it, and nothing has to be believed. That is the whole design.

https://github.com/user-attachments/assets/97dd9198-790c-4f82-a9ca-f5f35b155fc2

A model is asked to write without e, says it has, and check("lipogram", ...) says otherwise. The same check then passes the corrected draft: the validator is the eval.

Quickstart

pip install denckring
from denckring import check, produce

report = check("lipogram", "A small conforming bit of writing", forbidden="z")
report.satisfied, report.score  # True, 1.0

produce("cut_up", "one two three four five six", seed=7).texts  # the generating half
$ denckring check lipogram gedicht.txt --lang de   # exits 1 when unsatisfied
$ denckring apply cut_up text.txt --json           # emits a Production
$ denckring status
158 catalogued · 133 implementable · 133 implemented · 133 validated · 25 not mechanically checkable
0 instruments catalogued
63 rows externally evidenced · 70 explained · 0 unexplained

Three interfaces over the same core functions, so they cannot disagree about what a procedure is: the Python API, the CLI, and --json on check, apply, show and describe for callers that are neither.

Letting a model write under a constraint

A model writing to a form can check its own draft instead of guessing, which is the difference between a haiku it believes is a haiku and one that is.

pip install denckring[mcp]    # an MCP server: denckring-mcp

The server exposes four tools — list_procedures, describe_procedure, check_text, apply_procedure — with the procedure as a parameter rather than eighty-six separate tools, because model performance degrades with tool count. Errors come back as data ({"code": "invalid_params", ...}), never as a traceback a model cannot act on.

check_text and apply_procedure refuse a text over DENCKRING_MCP_MAX_CHARS characters (default 50000, counting text plus any string parameters such as source) with a text_too_long error, rather than running against an arbitrarily large remote input. Set DENCKRING_MCP_MAX_CHARS to raise or lower that cap for a known workload.

For Claude Code, the skill at skills/denckring/SKILL.md shells out to the CLI instead, and needs no extra beyond the package itself.

What's here

A hundred and thirty-three of the 158 catalogued procedures are implemented, and every one of them is validated. Of the rest, 25 have no mechanical acceptance criterion and are catalogued rather than implemented — see What can be checked — and all 133 implementable procedures are implemented.

Validated means a row agrees with its golden examples, and most of those were written for the suite, so they show the code does what its author meant, not that the author read the form right. Only an example quoted from a published text is evidence of that. Golden cases added from 2026-10-12 quote public-domain text only and record the URL they were extracted from (ADR 0062); some earlier external cases predate that policy. Every implemented row either has an external case or records why not: the form postdates the public-domain cutoff, its exemplars are in copyright or in a language no pack covers, or none has been found yet, with what was searched or that no search has been made. describe(pid).corpus gives a row's split, as a CorpusShare counting its examples by provenance and recorded verdict beside that reason; denckring catalogue export carries it per row as corpus, and denckring status totals it on its third line.

The gallery has a page per catalogued procedure, generated from the catalogue and the golden fixtures, so every worked example on it is one the test suite enforces and none of it can drift. Locally, denckring list shows what your install can run and denckring list --status catalogued shows everything.

Install options

The extras improve or unlock procedures rather than changing the language itself. Each vendored source keeps its own licence file beside the data it covers, which is why a distribution exists per licence rather than per language (ADR 0013).

install adds what it unlocks
denckring no data files English, German and French; the lexicon-free procedures in all three
denckring[en] pronouncing dictionary, noun lexicon, SCOWL graded word list, relations and proverbs exact syllables instead of estimates; apply anagram
denckring[de] word lexicon and noun list, from Wikidata Lexemes (CC0) charade, semordnilap, word_square, n_plus_7, s_plus_7 in German
denckring[de-wiktionary] pronunciations, stress, glosses (CC BY-SA) German phonetic and gloss-based rows; no English-only relations or POS
denckring[de-frequency] frequency bands, from the Leipzig Corpora Collection (CC BY) German apply anagram
denckring[fr] word list, nouns, frequency bands, glosses, syllables, phonemes (Lexique 3.82, fr.Wiktionary) French from 72 rows to 108
denckring[mcp] the MCP server denckring-mcp

Without [en], syllables are estimated from spelling and every report says how many words were guessed. apply anagram needs a word list graded by commonness, not one that merely answers whether a string is a word (ADR 0028) — which is why it is the one thing an unlocking extra is required for rather than merely improving. ADR 0030 records the measurement behind [de-wiktionary]: 94.0% token coverage over 341,000 tokens of Wieland, Goethe, Kafka and Mann.

Languages

German and French ship in core because the procedures that need no lexicon work for them unchanged; both are built-in defaults exactly like English (ADR 0022), and their data distributions override those defaults rather than registering through a separate path. On core alone, 72 of the 133 implemented rows run in French.

Language packs declare capabilities; procedures declare what they require. Asking for a language whose pack is not installed, or a procedure whose requirements that pack does not meet, raises MissingCapability naming what is missing rather than quietly returning an approximate answer.

describe answers both halves of "can I run this in French": languages is the row's authored editorial scope — wechselsatz is German by nature, not merely by capability — and runs_in is computed from the installed packs. clerihew is languages: [en], the form being English by nature, and runs_in all three.

The two move independently, and the editorial half is the one that lags: 174 (row, language) pairs ran unclaimed when it was last measured. ADR 0047 closed the thirty French rows whose own source: field is the evidence — lipogram is Perec's La Disparition — and records why the rest stay open rather than being swept.

18 French rows will never close. They are accentual metres, French has no lexical stress, and they are not French forms; declaring the capability to reach a bigger number would be a promise the data cannot keep. ADR 0032 records why Wikidata Lexemes could not supply French as it does German — 12,768 French noun lexemes against 188,948 German — and ADR 0034 the line-level syllable seam French needs and the others do not: a final mute e elides or counts depending on what follows, so French cannot be counted word by word, and its line count is always estimated, never exact.

Whether ä counts as a is an editorial decision rather than a library constant, so it is a parameter: fold_diacritics defaults to true and can be turned off per call. Glyph questions never fold — Masse satisfies the prisoner's constraint and Maße does not, because a written ß carries an ascender.

Reports and Productions

check returns a Report: satisfied, a continuous score in [0, 1], a list of violations with character offsets, free-form metrics, and evidence — the measurements the verdict rests on, each saying whether it came from a dictionary, from a dictionary listing several readings the checker accepted any of (ambiguous), or from a spelling heuristic. A haiku that misses names the words it counted and what it made them; a line that does not scan names the stress it read each word as. It is empty on the rows that need no such account: a lipogram's violation already carries the offending character. describe() says in advance which kind a row is — reading.determinacy is exact for 85 of the 133 and heuristic for 48. The score is monotone in violation count and satisfied is exactly score == 1.0, so a caller driving a retry loop can tell whether a text missed by one word or by fifty.

produce returns a Production — the procedure's id, its texts (best first, never empty), truncated, and metrics — and apply is defined as produce(...).texts[0], the one-text surface for a caller who wants the best answer and not the search behind it.

Every check is deterministic: the same text, language and parameters give the same Report, on every row. A row's catalogue flag deterministic is about the form, not the checker: it is false where the form leaves its output to chance or to the writer, as a cut-up or a homophonic translation does. A generator that draws takes a seed, which repeats the draw and which provenance echoes.

Both carry provenance: the package version, a schema version that moves independently of it, which pack answered and which data distributions it was reading, the fold_diacritics policy in force, and the seed a drawing generator used. German names three distributions there, because GermanWiktionaryFrequencyPack layers three. The seed is the field that makes a draw repeatable — it is an input parameter, so until it was echoed here apply --seed 7 --json printed an object that did not contain 7.

texts is derived from candidates, which is where a generator that ranks says why: produce("anagram", "dormitory") returns dirty room first, carrying the SCOWL size band of its least common word, ahead of covers built from rarer ones (ADR 0027). A caller reading texts gets the order without the reasons, which is a real cost of keeping that field; the reasons are there for anyone who asks for candidates by name.

What can be checked

Not every procedure in the catalogue can be graded by a program, and the catalogue says which. checkability: self means decidable from the text alone; source means it needs the text it was made from, supplied as --source; none means no computable acceptance criterion exists — canada_dry is defined as having no operative constraint, and a calligram's shape is not something code judges. Those rows are catalogued because the forms belong in an honest survey, and denckring status counts them separately rather than reporting a gap that can never close.

What is stable

0.x means the API can change in a minor release, and the changelog says when it does. Eight surfaces are treated as contracts regardless, because things outside this repository are built on them:

  • Procedure ids. An id that has shipped does not change meaning. When a row is replaced, the old id stays findable through denckring search as an alias — though get resolves ids only, and raises UnknownProcedure naming the replacement. multiple_constraint, which replaced univocalic_lipogram_pair, is the precedent.
  • The names denckring exports, imported from denckring itself. check, apply, produce, describe, get, list_procedures, summaries, all_procedures and rules keep their signatures and meaning, as do checker_history and CheckerChange (ADR 0061); a new parameter arrives as a keyword whose default keeps today's answer. Report, Violation, Evidence, Production, Description, Summary, Scholarly, Meta and Lang keep the type and meaning of every field they have, except the fields listed below as not yet promised. DenckringError and every subclass keep their names, their place under DenckringError and their code. The same objects reached through denckring.core are not covered: the path is part of the promise. The rest of __all__ is published but not yet promised, as listed below. Also promised, because the sibling denckring-bench reads them (ADR 0060): golden_cases and GoldenCase, the shipped examples with the verdict each must give; unmet_requirements, which says, given a case and a pack, why the case cannot run (the capabilities it needs that the pack lacks, or []); pack_provenance and PackProvenance, the record Report.provenance.pack carries, whose digests (from pack_provenance, empty on a Report) hold a lower-case hex SHA-256 per surface, changing exactly when that surface's content does, so a snapshot keys on them rather than on distribution versions, which move with every release. graded_words is sha256(json.dumps(sorted(pack.graded_words().items()), ensure_ascii=False, separators=(",", ":")).encode("utf-8")); nouns is the same over sorted(pack.nouns()); files hashes name\0path\0bytes\0 for each file under each data distribution's import package directory (sorted, paths relative to it, bytecode excluded, nothing from *.dist-info), so the release version does not move it and a wheel and an editable install of the same data agree. A surface the pack lacks omits its key; Description.reading, including reading.units, reading.word_examples, reading.vowels, reading.rhyme and reading.counting (a Counting, or None on a row that totals no letter values; ADR 0061 D4); Description.corpus, a CorpusShare (ADR 0062) whose counts move as golden cases land and whose gap is None or one of in_copyright, after_cutoff, unsupported_language and none_found, each keeping its meaning; and PARAMETER_ROLES, which names each x-denckring-role value a parameter schema carries and what it means. Roles may be added; none is removed or renamed without a Breaking: entry in the changelog.
  • Report as JSON — procedure, satisfied, score, violations, metrics, evidence, provenance, estimated — and the --json output of check, show and describe that carries it. Fields may be added; the ones already there do not change type or meaning. estimated says whether the verdict rests on anything estimated or left unjudged, and is what to read in place of metrics["estimated_words"]. Each evidence entry is one measurement the verdict rests on: subject (the word or line), scope (word or line), offset, value and basis, which is one of dictionary (looked up, read one way), ambiguous (looked up, more than one listed reading, and the checker accepted any; not an estimate) and estimated (no listing, so a heuristic answered). A caller deciding to leave a verdict unscored reads estimated, or basis per entry, and needs no metrics key. A new basis would arrive with a new schema_version. describe's Description carries runs_in under the same promise, and rules, the row's denckring.rules in the same sorted order, which describe --json, show --json and the MCP server's describe_procedure carry too. provenance carries its own schema_version, which moves when the shape of these objects does and not when the package is released: 1.3 since provenance.checkers (1.2 added the ambiguous value of Evidence.basis; 1.1 added estimated).
  • Production as JSON — procedure, candidates, texts, truncated, metrics, provenance — and the --json output of apply. Covered by the same promise in the same words. Both candidates and texts are ordered, best first, because apply returns texts[0].
  • The catalogue export schema (denckring catalogue export), including the licence and attribution keys the CC BY terms are carried by, and each row's corpus: the row's CorpusShare, or null on a row with no checker (ADR 0062).
  • The language-pack surface: denckring.get_pack (also denckring.lang.get_pack) and the LanguagePack protocol it returns, with the PosTag its pos_tags answers, all importable from denckring. A method on the protocol keeps its signature and meaning. A new pack method arrives as an optional member, read with getattr and a fallback, because the protocol is runtime_checkable and a required method added later would make every pack written before it fail isinstance.
  • The denckring.lang entry-point group and the capability names a pack declares, so an installed third-party pack keeps working. This covers replacing or extending the data behind English, German or French (ADR 0044) — Lang itself is a closed three-member type, so a pack cannot register a new language through this mechanism.
  • Violation rule strings (ADR 0059). A rule published through denckring.rules(procedure_id) keeps its name and meaning, so a caller can map rules to its own classes once. A row may add rules. Removing or renaming one is a breaking change, and the changelog names it.
  • Checker versions (ADR 0061). Each row has a checker_version, an integer in describe().checker_version, bumped whenever a change can move satisfied, score or the set of violation rules on some text; evidence and message wording do not bump it. report.provenance.checkers maps the row, and every row it delegates to (each entry of a multiple_constraint call, and whatever those entries delegate to in turn), to the version that decided the verdict. denckring.checker_history (returning CheckerChange entries {version, since, reason}) says what each version changed and in which release. A caller compares results across releases per row by comparing these. Versions only rise, and every row's history begins at 1 in 0.5.0.
  • Promised metrics (ADR 0060). A row may name metrics keys in describe().metrics, key to meaning; each is a float in every report of that row, and removing or renaming one is a breaking change.
  • Parameter defaults. A parameter's default, as the schema default in describe().params, is part of the promise. Changing one moves verdicts for callers who left it unset, so it is Breaking:.

Not stable, and expected to move: metrics keys a row does not promise, message wording, the wording of prompt hints, and everything under denckring.core. A check's verdict and the rule it names for a failure are contracts; the wording that explains the failure is not.

Published, not yet promised. These are exported and documented, so nothing has to reach into denckring.core for them, but they may still change in a minor release, with a changelog line when they do. All are new in 0.3.2:

  • rule_categories and failure_categories: the kind of failure each rule names;
  • the catalogue flags Meta.unique_answer and Meta.hidden_material, which Description carries too, and Meta.hint_omits;
  • prompt_hint and render_hint, which render the catalogue's hint templates;
  • scope, scopes, admits and witness;
  • words and nouns.

The catalogue as data

The catalogue is 158 sourced procedures and is a contribution in its own right — useful to someone who will never install the package.

denckring search rhopalic            # finds snowball by its alias
denckring list --family form         # the prosodic and stanzaic entries
denckring catalogue export --format json --output catalogue.json

Every entry records how its source was established, which is the field that makes the dataset trustworthy: primary names an author, work and year the catalogue stands behind; reference means the form is attested in a standard work — the Oulipo Compendium, Borgmann's Language on Vacation, Word Ways — rather than traced to an origin; traditional means no single origin exists to name. A reference row is not a weaker primary; it is an honest statement that the origin is not established.

Entries are filed under one of eight families — letter, word, syntax, form, permutation, procedural, translation, visual — and carry the other names each form travels under, so a search for isogram finds heterogram.

Each entry also declares a layer. A verfahren is accepted the way everything in this catalogue has always been accepted: run it, get text, have a checker score it. An instrument is a combinatorial device — temurah, the zāʾirja — admitted for a mechanism that is faithfully formalised and sourced, whether or not anyone would read its output. Every row is a verfahren today, and denckring status counts the two on separate lines so that the headline number keeps meaning what it has always meant (ADR 0033).

Devices, corpora and your own data

Three kinds of thing a procedure can need, handled differently. A device — Harsdörffer's five rings — is the procedure, so it ships with it. A language pack describes a language and ships separately when it carries weight, as above. A corpus is somebody's collection, so the package carries the loader and you supply the reading:

denckring check ideenwuerfeln throw.txt --source my-excerpts.json

A device's word-lists do not have to be the ones that shipped. Set DENCKRING_DEVICE_PATH to a colon-separated list of directories and denckring.core.device.load searches them, in order, before the packaged directory — so an earlier entry can add a device under a new id or shadow a packaged one:

DENCKRING_DEVICE_PATH=/path/to/my/devices denckring check denckring wort.txt --param device=my_rings

Each directory holds one YAML file per device, named <id>.yaml, in the shape the packaged devices use (see src/denckring/data/devices/). A directory that does not exist, or cannot be read, is skipped rather than raised on.

What this does not give you: no schema versioning beyond ordinary YAML/Pydantic validation, no record of where a loaded device came from, and if a device on the path shadows a packaged id, the packaged device is simply not what ran — load has no way to say so. The id itself is untrusted input and is validated down to a bare name — no path separators, no .., not absolute — so a device must live in a directory on the path rather than be addressed by a path of its own. A relative entry resolves against the process's working directory at call time, not against wherever the variable was set.

The explorer

A local browser for the catalogue and a bench for trying procedures on your own text, kept outside the distribution in apps/explorer:

uv run --project apps/explorer explorer

It lays the catalogue out as a compositor's type case — one compartment per procedure, filled where a checker exists — builds each parameter form from that procedure's own params_schema(), and marks the offending characters inline using the offsets every Violation carries.

Contributing

One module per procedure, check mandatory. Start with uv run python scripts/new_procedure.py <id>, which scaffolds the module, test, strategy, golden fixture and catalogue row. See CONTRIBUTING.md.

Build the documentation site locally with:

uv run python scripts/build_docs.py && uv run mkdocs serve

Scope of the name

The Fünffacher Denckring der Teutschen Sprache (Harsdörffer, 1651) is one device among the hundred and fifty catalogued here, not the whole subject.

Using the name

The name is reserved — Apache-2.0 grants no trade mark rights (section 6), which is what keeps a fork from publishing as this project. What that reservation permits, stated so it can be complied with rather than guessed at: redistribute the package unmodified under the name freely, say that your work uses or is compatible with denckring freely, and rename before publishing a modified version. "denckring" in the name of a package you publish, or on a site presenting your fork as the original, is the line.

Licence

Code is Apache-2.0 — see LICENSE and NOTICE, which every derivative distribution has to carry. The catalogue is CC BY 4.0 — see LICENSE-DATA for the attribution string and what the licence does and does not cover. The code was MIT before 0.1.0; ADR 0024 records why it moved.

Metadata

Release files for denckring 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for denckring 0.5.0
File Size Uploaded
denckring-0.5.0.tar.gz 1.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for denckring 0.5.0
File Interpreter ABI Platform
denckring-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.8 MB

Release files / denckring-0.5.0.tar.gz

Download URL denckring-0.5.0.tar.gz
Size 1.2 MB
Tags Source
SHA-256 checksum
How to use checksums
ce6cbf66cb658e576d4ccb2739c290946bdb2ca75c23ccbf60fd211c52b9d5ef
BLAKE2b-256 checksum
How to use checksums
ec616c3ba0b613a60c1ca1253776240e080bfdb465e56505a08d12e0723faea0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / denckring-0.5.0-py3-none-any.whl

Download URL denckring-0.5.0-py3-none-any.whl
Size 614.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6025e716e9563f22c5d9983e1b0dc2e155b16f417d7fbbb199a5e20462658979
BLAKE2b-256 checksum
How to use checksums
a9b15583f84f751fa015f912a5845f5d6ec4284e09504578ea183a6917acaed6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release history Release notifications | RSS feed

0.6.0

2 release files

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page