Skip to content

Docket 0.2.0

Choose a tag to compare

@shivanshjain456 shivanshjain456 released this 20 Oct 14:11

2026-10-20

  • Retention of the provider responses the boundary saw. Every model.response event now
    carries response_sha256 over the exact bytes returned to the agent, and
    [evidence] record_responses additionally keeps a bounded copy of the body in a responses
    sub-directory of the run directory, named by its own digest. docket verify re-hashes every
    retained body under the new retained-responses check, so one edited or deleted after the run
    fails. The reason this exists is a provider Docket cannot read: its usage block is in a fourth
    shape, the token counts are estimated from the response text, and that text used to be
    discarded, so the estimate could never be checked. A truncated body carries both digests and
    says which is which; the verifier checks the bytes that were written and never claims to have
    checked bytes it was not given. docket report --redact drops the retention fields rather
    than leaving a pointer to a body the copy does not carry, and records how many under
    retained_responses_not_carried. Retention is off by default: a response body can contain
    the prompt, a tool result, or a credential, so the digest is the default and the body is the
    opt-in. See docs/evidence-retention.md.

  • Published to PyPI as docket-ai 0.1.0. pip install docket-ai installs the same
    package, and the import, the console script and the command are all still docket. The
    distribution name differs because PyPI administratively prohibits the bare name docket:
    the JSON API answers 404 for it, and twine upload still refuses it with "The name
    'docket' isn't allowed"
    . PyPI keeps that list to prevent dependency-confusion attacks, and
    the only way off it is a PEP 541 request reviewed by the PyPI administrators. PUBLISHING.md
    records the reason, the one place the name is written, and the request process.

  • PUBLISHING.md, the maintainer runbook for shipping a release. It records what the
    release gate does, why the PyPI upload stays a manual twine upload rather than a step in
    the workflow (a PyPI token can publish under the project's name forever and cannot be
    scoped to a branch, so a repository secret hands it to everyone who ever gains admin here,
    while github.token is scoped to one run and expires with it), the prohibited-name
    evidence, and how to cut and re-cut a tag.

Changed

  • The README leads with the install path that works. It told a reader that docket was
    not on PyPI, that the name had been unregistered on 2026-10-17, and that
    pip install docket would start working after a future 0.1.0 publication. The release
    exists, so the apology was the only thing left. The install section now opens with
    python3 -m pip install docket-ai, and pipx install docket-ai replaces the wheel URL.
    The clone path stays, because it is what a contributor wants rather than what a user wants.

  • The version note states what is guaranteed instead of hedging. It said the CLI "may
    change before 1.0", which read as an unfinished project. It now says the format is frozen by
    the specification and its 44 vectors across three implementations, and that exit codes,
    flags and the JSON shape of every command are part of the interface and stable across 0.1.x.

  • The exit-code table gains 1, and documents exit-code propagation. docket run hands
    back the exit code of the command it wrapped, so docket run --policy docket.toml -- ./build.sh is safe in a pipeline. That was true and undocumented.

  • The quickstart verifies .docket/latest.receipt.json instead of a DKT-*.receipt.json
    glob.
    The glob worked on the first run and exited 2 on the second, because verify takes
    one receipt and argparse rejects two. The path every run rewrites has no such edge.

  • pyproject.toml uses the SPDX license expression and license-files, so PyPI renders
    the licence rather than a free-text string.

Fixed

  • The doc-link guard now asserts the opposite of what it used to. It used to fail any
    document that instructed a reader to install an unpublished package, and its own docstring
    said to relax it by removing the instruction once published. It now fails any document that
    still carries the apology, and reads the distribution name from pyproject.toml rather than
    repeating it, so a rename cannot leave the guard and the guide disagreeing.

  • docket verify --impl node: a second opinion from an installation. The claim that the
    format is not this tool needed the other tool to be reachable without a clone, so the wheel now
    carries verify-js/verify.mjs and the conformance corpus under docket/_vendor/, and
    docket verify --impl node runs that program against the same receipt and the same ledger and
    prints its own verdict and its own exit codes (0/1/2). A pipeline that already gates on
    docket verify needs no change when it switches. docket doctor now reports whether this
    installation has both the verifier and a node on PATH, and two tests read the packaging
    configuration rather than a built artifact, so a dropped force-include line fails here
    instead of in a reader's terminal.

  • docket verify finds the ledger beside the receipt instead of checking less. It used to
    need --ledger or it skipped the chain, the roots, the file digest and the re-binding of the
    usage rows. It now looks in the .docket/runs/ directory beside the receipt it was handed and
    selects by the ledger.file_sha256 the receipt carries, so the file used is provably the one
    the receipt was built from — including for a receipt that arrived in an archive with its
    ledger alongside. The line naming the file is printed, --ledger still overrides, and a
    receipt with no ledger anywhere still degrades to SKIP rather than to a red result.

  • A third implementation, written from the specification alone. verify-go/ is a
    dependency-free Go verifier built from spec/ and the vectors without reading the Python or
    Node one, with a CLEANROOM.md that declares the constraint before any code existed. It agrees
    with the other two on the whole corpus — 132 correct, 0 wrong, 0 not judged over 44 vectors —
    and it produced the artifact the claim needed: twelve questions the specification does not
    answer
    , five of which are worth amending (rounding of the sixth decimal, price-table matching,
    the shape of limits_disclosure, blank lines in a ledger, and the marker for a reached cache
    ceiling). make verify-go runs vet, gofmt and 16 table-driven test functions; CI has its own
    job; make conformance includes it whenever the binary exists.

  • Three more members of a receipt were stated and never checked, and now are.
    totals.by_model[], the usage-source counts and totals.cost_basis are derived from
    appendix.usage, so they can be recomputed — but §8 only ever named the total, the token
    totals and the per-row costs. A sweep that rewrote each of them left ok: true every time: a
    breakdown could name a model nobody called (with the total still reconciling), or present four
    estimated calls as reported. by-model-totals and usage-source-counts are new checks;
    cost-basis is a contradiction check that compares only the class word and reports quota mode as SKIP, because the meter's mode is stated nowhere else in the receipt and believing
    it is not checking it. Four new corpus vectors (39–42), eleven new tests in the Python
    suite, eleven in the Node one, and ADR 0007 records why the other designs were rejected.

  • ledger_reject: the corpus can now say "this ledger must be refused". Every vector was a
    receipt that must fail; a malformed ledger — the file an interrupted append or a hand edit
    actually damages — had no way to be expressed, so the two implementations' refusal to repair one
    was asserted by their tests and by nothing a stranger could run. Two vectors (37, 38: a
    duplicated key inside an event, a half-written last line) and a fourth expectation kind close it.
    The rule is the same as for receipts: a reason must be stated, and the vector may only require a
    token that appears in the ledger.

  • The corpus grew to 36 vectors and found four more defects — including one in the same file twice.
    spec/vectors/invalid/ now covers the parsing family (a UTF-8 BOM, trailing bytes, invalid UTF-8 —
    all refused before any check runs) and, for the first time, documents that must be accepted:
    4e0 where an integer is expected, -0, and a decomposed Unicode form. Those three exist because a
    corpus that only says what to refuse lets two implementations disagree about what to read, and the
    first one settled a real disagreement: spec §3.2 says an integral float MUST be normalised, Python
    refused every float token and Node accepted every one. The spec's sentence decided it; both now
    normalise an integral float and refuse a fractional one. Probing the totals member by member also
    found that totals.calls, totals.input_tokens and totals.output_tokens were never compared with
    the appendix in either implementation — a receipt could say "calls": 0 beside four usage rows and
    verify — and that Node's readFileSync(path, "utf8") silently replaced invalid bytes with U+FFFD, so
    it verified a repaired copy of a corrupted document. All four are fixed, with vectors and tests.

  • reason_any_of, because a corpus may not demand one implementation's prose. The first version
    of the BOM vector required the word BOM; Node said Unexpected token '\ufeff', refused correctly,
    and was scored wrong-reason. An expectation now names tokens that must appear in the document
    (NaN, Infinity, a member name), or nothing at all when the damage has no nameable token. Writing
    a corpus before the third implementation exists is an exercise in not encoding the first two's
    vocabulary.

  • docs/adr/0006-the-shape-of-the-conformance-corpus.md records the five design decisions with
    the alternative rejected in each case: the document-plus-expectation layout, the parse class,
    deriving the capability table from output, declaring gaps instead of manufacturing skips, and the
    limits of what a corpus may demand.

  • A negative conformance corpus, and the six verifier defects it found. The vectors could only
    show that a conforming implementation accepts what it should; nothing tested that it refuses what it
    must, which is what a verifier is for. spec/vectors/invalid/ now holds 27 complete documents —
    a doctored ledger, a totals figure that does not sum, a Merkle path with one hex digit zeroed, a
    NaN, an Infinity, a duplicated docket_number, a top-level array, an undisclosed fallback
    price — each with an expected.json naming the check that has to catch it and the rule it enforces.
    spec/vectors/invalid/build.py derives each vector from the valid ones, mutates it in exactly one
    stated way, then runs the reference verifier and refuses to write the corpus unless the first
    failure is the check the vector claims — so a vector cannot quietly document a defect as a feature.
    Building it, and running it against the second implementation, found six real defects: the Python
    verifier accepted a usage_source outside the vocabulary spec §6.1 defines (the row's arithmetic
    still recomputed, so every numeric check passed and the one word a consumer branches on was never
    read); the Node verifier accepted a document that named ledger twice, silently keeping the last
    value, where RFC 8259 §4 makes the meaning undefined; it also accepted the same out-of-vocabulary
    usage_source; it folded "is this row the event the ledger holds at its seq" into the inclusion-proof
    check, so a shuffled row reported a broken proof rather than a re-binding failure; the Python verifier
    emitted every-usage-row-recomputed only for receipts with no usage rows, the one case where the
    question cannot arise; and its assumed-prices row did not exist at all while the Node one reported
    pass for every assumption it noticed. All six are fixed, with a test each.

  • spec/registry.json and tools/conformance.py: the check vocabulary, in a form a machine reads.
    The registry names every id, what it asserts, and which ids each shipped verifier emits. The
    capability table is derived, not written: --capabilities collects the ids each implementation
    actually printed across the whole corpus and fails when the registry disagrees — which is how the
    hand-written version was caught saying the Node verifier emitted twelve ids when it emitted eighteen.
    Ids a verifier does not emit are recorded as declared gaps (node_conditional), with the reason
    stated, rather than as skip rows: a skip claims "this verifier looked and could not judge", which
    would be false. tools/conformance.py runs any verifier (--cmd "my-verify {file} --json{ledger_flag}")
    over the corpus and keeps three outcomes apart — wrong-ok (a document that must be refused was
    accepted), not-judged (a declared gap) and wrong-check (refused, but by a different rule than the
    vector names). make conformance runs both shipped verifiers and the capability comparison; it is
    part of make check. Both implementations now report 58 correct, 0 wrong, 0 not-judged over
    2 positive and 27 negative vectors.

  • A star-count comparison that was quoting two figures traceable to nothing, and no longer does.
    An audit of this project's own claims on 2026-10-17 found two landscape numbers with no source: one
    had been read against dify/dify, a slug that answers 404 (the project is langgenius/dify), and
    one had come from a search listing nobody saved. Both were re-swept by canonical slug against the
    GitHub REST API, unauthenticated, and the query that produced them is recorded with its timestamp so
    the snapshot can be re-taken. What was learned and kept: every figure quoted onward carries the slug
    asked for, the API path called and the URL that answers it, plus three
    total_count search rows. tests/test_strategy_numbers.py reads the page and the sweep together
    and refuses a page whose star counts, ages, rates, thresholds or inline figures stopped matching
    the evidence beside it; it cannot re-ask GitHub (that is the sweep's job, and re-running it means
    re-basing the page), but it can refuse the drift that actually happened.

  • tools/a11y_check.py: the built documentation site is checked for the accessibility defects a
    program can decide.
    The site is the only HTML this project publishes; mkdocs --strict proved it
    built, and nothing looked at whether a reader using a screen reader could use it. Ten rules —
    language, title, exactly one h1 and no skipped heading level, a single main landmark, alt on
    every image, an accessible name on every link and every rendered form control, a header cell or
    caption on every table, no duplicate id — with the accessible name computed from aria-label,
    aria-labelledby resolved through the page's own ids, element text, a nested <img alt>, a nested
    <svg><title>, a <label for=…> elsewhere on the page, or a <label> that wraps the control.
    tests/test_a11y_check.py makes every rule prove it can fail and every one of those naming paths
    prove it does not cry wolf, and the check runs in the release record beside the docs build.

  • One exemption, and it expires. material renders its drawer and search toggles as
    <input class="md-toggle" type="checkbox">, and the theme's stylesheet says .md-toggle{ display:none}: a control that is not rendered is not announced, so no name is due. The checker
    reads the CSS of the site it is inspecting rather than trusting a class name, prints which class
    exempted how many controls, and stops exempting the moment that rule is gone — an exemption that
    cannot expire is how a checker rots into a rubber stamp.

  • Two more guards, both green on the tree they were written for. docs/cli.md is 397 lines of
    flags and none of them had ever been checked against the parser: the new test reads the level-2
    sections whose heading is a docket <command> line, requires the command to exist, and requires
    every flag filed under it to be an option of it — 72 flags, all real, and the scan fails if it reads
    fewer than 60, so a heading rename cannot turn it green by finding nothing. The second refuses an
    absolute path from the machine that wrote the docs — the authoring sandbox's home directory, or a
    /tmp toolchain path — in any prose document, with the raw measurement reports
    exempt because their own README says their command and ledger_path fields are the paths the
    harness ran with, unedited.

  • Two more guards in the file whose subject is documents matching the repository. One fails on a
    row of a graded table that carries no grade — docs/evidence.md's claim ledger and the incident
    corpus's incident table, found by their own header rows — because a claim that loses its grade
    quietly becomes the same weight as a sourced one, which is the failure the grades exist to prevent.
    The other reads every make target named in .github/workflows/*.yml and in the Makefile's help
    text and fails on one that does not exist: a job calling a target nobody defined is a red build for
    whoever enables it, and a help line is a documented command. Both are the shape of the correction
    that preceded them: a number or a name that was true when written and had no guard.

  • A test that refuses a rank the source does not publish. Three shipped documents said OWASP's
    agentic top ten puts "denial of wallet" at risk #2 — the list's second entry is Tool Misuse and
    Exploitation
    , and cost is not one of its ten classes at all. A rank is the most precise kind of
    citation and the cheapest to invent, so
    tests/test_doc_links.py::test_no_document_asserts_a_rank_inside_a_source_that_does_not_publish_one
    reads every document that discusses such a list, fails on a position claim against it, and fails if
    the discussion disappears instead of being corrected — because deleting the paragraph would
    otherwise be a way to pass. Quoting the old wording in order to correct it stays allowed, which is
    how the correction is recorded in docs/incident-corpus.md.

  • A guard against the verbosity trap, in the file whose subject is the documents matching what the
    repository does.
    tests/test_doc_links.py reads pyproject.toml's addopts, asserts exactly one
    -q there, and then checks every pytest command the repository ships — the make test target and
    the four CI jobs — for a second -q (which is -qq, where pytest prints no counts line) and for a
    missing -ra (which is what makes a skip say why it skipped). It is the fifth place the same defect
    turned up in one day, and the first one that cannot come back.

  • Every mutation score in docs/testing.md is re-measured in one round, in one pass, with the
    control run recorded in every report
    (docs/testing.md, The release round). The nine modules
    were swept back to back on one commit with one harness, each report carrying what its unmutated
    sandbox did before any mutant ran — 189 passed, 121 passed, 17 passed, in pytest's own
    words — and the round reproduced every score, every survivor and every timeout the earlier rounds
    had reported, mutant by mutant as (line, operator, from, to). That mattered because those numbers
    had been measured across three days, two environments and a test suite that grew underneath them:
    they now sit in one table that names the interpreter and the subset behind each of them. The
    control column earned its place on its first round
    : the Helm module scored 45 of 47 with a control
    run of 16 passed, 1 skipped, and the skip was the test that runs the real Helm client — the
    harness gives its test processes a fixed PATH so that a score cannot depend on the operator's
    shell, and the client was outside it. With the client reachable the control reads 17 passed, 0 skipped and the score is unchanged, which is a claim a reader can now check in the report
    instead of taking from the author.

  • Two tests that hold the documentation to what a clone can carry (tests/test_doc_links.py,
    15 tests). One reads .gitignore — implementing the forms this repository uses and refusing
    ** rather than guessing at its meaning — matches every rule the way git does (anchored or by name
    at any depth, last match wins, ! re-includes), and fails on any file the documentation names that
    a clone would not have. The other feeds the reader the rule that shipped and the rule that replaced
    it: a checker that cannot fail is decoration, and this one is asked to fail.

  • The dependency matrix in docs/testing.md is re-measured, and a claim this page made about
    pytest is corrected.
    The table had been three rounds stale (it still carried 840 / 845 / 868 /
    878), and the sentence above it said the counts come from "a run with -ra, never a -q run's last
    line" — as if -q were the problem. What matters is how many times the flag is passed:
    pyproject.toml's addopts already carry one, so a command line that adds another asks for -qq,
    and at that level pytest 9 prints no summary line at all — a script that greps for one records zero
    passes for a suite that passed a thousand. A single -q prints it. The page now says which round is
    tabulated, what the earlier rounds measured, and where the two that are not tabulated are filed.

  • A support page that says what support is not (SUPPORT.md, in the documentation's nav and
    linked from the README's Contributing section). The repository had a security policy, a contribution
    guide and a sponsorship pack, and nothing that answered "something is wrong — where do I go": the
    issue templates existed but no page named them, no page said what a report has to contain, and no
    page said the thing most projects leave implicit, that there is no service-level agreement. It routes
    bugs, feature requests, questions, security reports and sponsorship questions to the right place,
    tells a reporter to run docket doctor --json and docket selftest before opening an issue (both
    print what they observed rather than a verdict), lists what this project cannot help with — a
    provider's billing, a Kubernetes cluster nobody here has, a date nobody promised — and states in the
    same words as SPONSORS.md that a sponsorship does not buy support.

  • The five examples are executed by the suite instead of being described by it
    (tests/test_examples.py, 9 tests). examples/ was advertised in the README, in these docs and in
    the adoption plan as five runnable examples, and nothing ran them: a renamed flag, a moved module or
    a policy key the loader stopped accepting would have left five documents that lie and a suite that
    stayed green. The quickstart and the SDK example now run as programs — offline, in a temporary
    directory — and the receipt each one prints is fed to the verification command each one prints; the
    quickstart's own instruction ("edit one byte of the ledger and run it again") is executed, with one
    digit of one token count changed and the verification required to fail; both policy files go
    through a real supervised run; and every README is checked for links, for the repository paths its
    commands name and for the Python files it ships. Writing it found the quickstart's own capture
    stale in two ways — it said 17 events where the tree produces 18, and it omitted the
    cache-savings verdict line entirely — so the block is now a real capture of the current tree, and
    the note beside it says which parts move between machines.

  • Every survivor of the harness's own sweep is closed, and three of them were defects in the
    tests' reach rather than in the code
    (tests/test_mutate.py, 33 tests; tools/mutate.py).
    The harness was re-run over itself, its twelve survivors were read one by one, and each is now
    killed by a test that says what the mutant would have done: raising the span guard's lower bound
    from byte zero to byte one (a site at the first byte of a file was silently dropped — and only a
    file that begins with a mutable node can tell the two bounds apart, so no existing fixture could);
    flipping parents=True or exist_ok=True on the two report paths (the CLI test wrote into a
    two-level directory once but never into the directory it had already made); treating the four
    rmtree(..., ignore_errors=True) calls as strict (the sweep's cleanup runs in finally:, where a
    failure would replace the report — or the refusal — with a PermissionError from the temporary
    directory, so a fixture tree now contains a directory with no write bit and four tests assert that
    each of the four exits still reaches the reader); and the five guards inside _is_docstring, which
    the sweep cannot reach at all because its only caller has already established what they check — a
    direct question is now asked, with is True/is False rather than truthiness, because and turned
    into or there says "yes" to a bare number and filters a whole file's worth of sites out.

  • The ledger's liveness probe has a test that observes signal delivery, not disposition — because
    the sweep's own environment hides the difference
    (tests/test_ledger_contracts.py).
    os.kill(pid, 0) mutated to os.kill(pid, 1) answers the same question and sends SIGHUP while
    asking it: a liveness check with a blast radius, the same defect class as the Windows probe that
    would have terminated the process holding the lock. The existing test could not see it, and the
    reason is worth keeping: every sweep runs under nohup, which ignores SIGHUP and passes that
    disposition to its children, so the signal is a no-op in the environment the score is measured in.
    The new test has the child install its own handler and create a marker file inside it, so a
    delivered SIGHUP is observable where it would otherwise be swallowed. The same file gained a test
    for chain_error's broken-link branch, the one of its four that a file-based reader was covering
    instead.

  • A mutation score was reporting 51 gaps in the verifier that were 51 missing test files
    (tools/mutate.py, DEFAULT_TESTS). The byte-offset fix took verify.py's candidate count from 95
    to 177; the round that followed scored 126 of them and listed the rest as survivors. Reading them
    was the point: nearly every one sits in the block that recomputes a stated cache saving
    (cache-savings, cache-hit-origins, cache-hit-proofs), and that block is exercised from
    tests/test_cache.py — a file the verifier's subset did not name. The subset was part of the
    measurement, the score was reading as the tests' fault, and the fix is one line in the mapping plus
    the explanation beside it. The round after it is recorded next, because the two are the same lesson
    read twice.

  • The harness had no tests, and the first one found a bug that had been hiding a sixth of
    verify.py's mutants
    (tests/test_mutate.py, 16 tests; tools/mutate.py). The tool
    that produces every mutation score in this repository had no tests: the numbers in
    docs/testing.md rested on a tool nothing checked, which is the same shape of gap as a conformance
    vector nothing ran. The new file asserts the properties the harness claims — the splice is a
    byte-for-byte no-op, every mutant changes exactly one line and still compiles, each operator kind is
    found and labelled the way a reader expects, the sample is a function of the seed, the six verdicts
    _run_tests can return are distinguished by real pytest runs, timeouts stay out of the score, and
    the sweep refuses to score anything if the unmutated copy is already red. The em-dash fixture in it
    produced no mutant at all, and the reason was a real defect: ast reports columns in UTF-8
    bytes while the splice indexes characters, so every site on a line with a non-ASCII character
    before it was spliced into the middle of a character, failed to compile, and was dropped by a silent
    continue. Measured on the corpus: verify.py offered 95 candidates instead of 177,
    ledger.py 171 instead of 172, and the other seven mutated files were unaffected because their
    sites sit on ASCII lines. splice_only could not have caught it — splicing the original text back
    is a no-op at any offset — and nothing counted drops, so the effect was invisible from inside the
    tool. Both offsets now go through _char_offset, the regression is pinned by a fixture line with an
    em dash in it, and every score quoted in the documentation was re-measured afterwards.

  • tools/mutate.py --only-line N: re-check one survivor with the harness's own machinery, in
    seconds.
    Reading a survivor list is only useful if the next step is cheap: reproducing a verdict
    by hand is how a reader measures a stale __pycache__, or measures a site the harness never
    mutated. --only-line runs every mutant on the named line (repeatable, sampling nothing) in the
    harness's own sandbox with its own interpreter and environment, records the selection in the JSON
    and markdown reports so a filtered run is never read as a whole-file score, and refuses a line that
    holds no mutant rather than reporting 0 candidates. The ten resilience sites closed in this round
    were verified this way, and the same flag re-checks the three cache sites.

  • A site that produces no mutant is now counted rather than dropped in silence. The four ways a
    site can yield nothing — the span does not lie inside the file, the operator cannot be applied, the
    replacement does not compile, the replacement is the original text — each produce a record that the
    sweep prints and carries into the report as dropped, and the markdown header states the count. Two
    of the four branches are driven by tests rather than waiting for the corpus to produce them. This is
    the second half of the em-dash fix: the bug was not that a site was dropped, it was that a dropped
    site left no trace anywhere a reader could see it.

  • Two more documentation guards, for the two ways a number can go stale without failing a
    build.
    tests/test_doc_links.py now reconciles (1) every count written beside a test filename
    anywhere in the living documents — `tests/test_x.py` (12), (12 tests), and "12 tests in
    `tests/x.py`" — against pytest --collect-only, and (2) the four sentences that quote the size
    of the whole suite (README, CONTRIBUTING, docs/testing.md, docs/sre-coverage.md). Both had
    already rotted: docs/resilience.md and docs/sre-coverage.md said test_resilience.py had 49
    tests (it had 54), docs/sre-coverage.md said test_helm_fidelity.py had 5 (17), docs/caching.md
    said test_cache.py had 41 (55), docs/production-coverage.md said test_chaos.py had 32 (33) —
    each written when its number was true. The suite total had no reader at all. CHANGELOG.md and
    docs/measurements/** are exempt by name, and the reason is in the guard: they are records of
    then, and re-basing them would falsify evidence rather than fix a claim.

  • Twenty-five tests closing every survivor of the 2026-10-15 mutation rounds — three in
    tests/test_cache.py and six in tests/test_resilience.py for the survivors that were gaps, plus
    the harness's own sixteen above. The cache's three were the group identity in _groups (key,
    model, avoided_cost_usd): with the operator mutated, two hits that replayed different requests
    are merged into one row whose stated amount is the first hit's — so saved_usd reports $0.02 for
    $0.03 of saving and the row count no longer matches the ledger. The resilience six close the
    counter defaults, the total_successes += 1 increment, the cooldown boundary (elapsed < cooldown
    versus <=), the sixty-second window boundary, the injector's default seed, the
    getattr(self, name, 0) fallback in HealthState.counter, and the free-space boundary in
    readiness — five of the six are exact boundaries the previous tests stepped past by one tick,
    which is the difference between testing a boundary and testing a rounding. The three
    probes_in_flight mutants that remain are classified as equivalent rather than tested, with the
    reason recorded in docs/testing.md: allow resets the field one statement before its only reader
    on every path into half-open, so no test can observe the difference, and the reset itself is
    killed.

  • A standards page: the norm or paper behind each mechanism, and where this one departs
    (docs/standards.md). Every mechanism a reader can name — the hash chain, the
    canonical form, inclusion proofs, 429/Retry-After, token buckets, the breaker, backoff jitter,
    cache storing rules, percentiles, tail latency, CAP, compensation, locking, clock ordering, chaos
    principles, probes, versioning — now carries its source, the strength of that source, and one line
    saying whether Docket follows it exactly, implements a smaller subset, or deliberately departs. The
    departures are the useful part and are collected in one list: an HTTP-date Retry-After is ignored
    because comparing a remote clock to a local one turns a wait into a hot loop; the cache implements a
    much smaller subset of RFC 9111 than the RFC allows; floats are refused where RFC 8785 would
    canonicalise them; nothing is hedged, so tail latency is measured rather than mitigated; no fencing
    token is issued because there is no distributed resource to fence; and no chaos experiment ever runs
    against production traffic. It also states plainly how each source was read — full text, section,
    abstract or practitioner canon — because "cited" and "read" are different claims.

  • A resilience-practice coverage map (docs/sre-coverage.md), the third of the three maps
    beside the production-coverage concepts and the testing taxonomy: twenty-two SRE practices — stress
    testing, fuzzing, chaos engineering, chaos monkey, game days, red teaming, dynamic analysis,
    vulnerability scanning, synthetic monitoring, proactive monitoring, observability, FMEA, circuit
    breaking, rate limiting, automated rollback, disaster recovery, software rejuvenation, degradation
    testing, blast-radius mitigation, IaC testing, post-mortems, infrastructure hardening — each with
    what this repository actually does, what the evidence is, and where it stops. It was written because
    two of the practices were being asserted in prose rather than done (there was writing about failure
    and no failure analysis; "post-mortem" named documents that were not post-mortems), and a coverage
    document is the easiest place for that to happen again: a test enumerates the twenty-two rows, so a
    practice cannot lose its row or acquire a status outside the legend without failing the build.

  • A failure-modes-and-effects analysis, in the document that already described the failure
    surface
    (docs/resilience.md, "Failure modes, ranked by what they would
    cost"). Sixteen ways a supervised run fails, each rated severity / occurrence / detection and
    ordered by risk, each naming the artefact that catches it — the money loop stopped at the boundary,
    a stale price table, a receipt that verifies but is not true, a torn tail, a leaked secret, a
    rollback that misses a file, two writers on one ledger. The page states plainly that the ratings are
    judgements and why; the detector column is evidence, and a test asserts that every path it names
    exists, that each risk score is the product of its own three ratings, and that the rows are still
    ordered highest-risk-first. The column is the point: a failure mode with a detector named in prose is
    a mechanism someone built, and a failure mode without one is now visible as exactly that.

  • docket selftest and docket game-day run in CI on every push, not only when someone remembers
    them.
    The self-test's nineteen probes and the game day's nine phases are the two things that
    exercise the product end to end offline (about twenty-six seconds together), and a self-test that
    nobody runs is a self-test nobody notices breaking — the same argument as tests/README, applied to
    the surface an operator is most likely to point at a deployment. make selftest and make game-day
    run them locally with no arguments.

  • tools/helm_fidelity.py: the chart render is now compared against a real Helm client, and the
    comparison is a command rather than a memory.
    tools/deploy_check.py renders deploy/helm/docket
    through a declared subset of the template language so that it runs anywhere — no Helm, no cluster,
    no network — and that design costs one claim: the render has to be the render a cluster would get.
    The new tool runs helm template, keys both renders by kind and metadata.name, compares them
    field for field and names the path of the first difference inside a document. Re-run on 2026-10-15
    against Helm v3.16.3: 5/5 documents identical field for field. It is deliberately not part of
    make check — CI has no Helm client — so it exits 2 with that reason, the same convention as
    the checker without PyYAML, and tests/test_helm_fidelity.py asserts that exit code, the pairing
    rule, the difference reporting and (where Helm exists) the comparison itself.

    Its first working run found a defect in the tool: it split the document stream on the characters
    "\n---", which eats the newline ending the previous document's last line — and for the policy's
    | block scalar that newline is part of the value, so the tool reported a one-character difference
    Helm does not have. The separator is matched as a line now, and the test pins it. That is the second
    time in this release that the instrument, not the artefact, was what failed first.

  • Supply-chain checks that read the manifest instead of repeating it
    (tests/test_supply_chain.py, .github/workflows/security.yml). dependencies = [] is the project's
    strongest packaging claim and it is worth what the file it lives in is worth, so a test reads it,
    pins the three extras by name, and then walks the syntax tree of every shipped module to assert that
    the only imports outside the standard library are cryptography and yaml — two imports, in two
    files, both optional, both behind a guard, with a test that fails if either stops being guarded or
    stops being used. That is the failure a dependency scanner cannot see (the author's environment has
    the package for other reasons; the user's raises ModuleNotFoundError). pip-audit covers the other
    half on a schedule rather than on every pull request, because a CVE disclosed in a test dependency
    has nothing to do with tonight's commit and a job that reddens unrelated changes gets ignored; as of
    2026-10-15 it reports no known vulnerabilities across the development and CI toolchain.

  • A response cache at the boundary, with every hit in the ledger (docket run --cache,
    [cache] in the policy, src/docket/cache.py). A repeated request — same upstream path, same
    canonical body — is answered from the response the boundary already received, and the provider
    sees nothing. The feature is built around one rule: a hit is never silent. Each one is a
    model.cache_hit event naming the response it replays (origin.seq, origin.hash), and the
    money it saved is recomputed by docket verify from that origin's token counts and the embedded
    price table, so a saving is arithmetic a stranger can redo rather than a number the producer
    chose. Three checks were added to both verifiers (cache-savings, cache-hit-origins,
    cache-hit-proofs), the receipt carries a cache member only when a cache was in play, and the
    format specification documents the event and the member (a compatible addition under v1).
    What is stored is only what the provider actually sent: an error, a stream cut short in flight,
    or a response rewritten by fault injection is never cached, and the reason travels on the
    response event. What a hit is not is a model call: it moves no cap, no total and no call count,
    so the run's spend keeps describing money that was actually paid, and the saving is reported
    beside it. Three ways an entry goes away, all counted and all tested: TTL expiry, LRU eviction,
    and refusing to store. The hazard is named and bounded rather than hidden — a cache can answer
    a loop that spends nothing, and no dollar cap can end a loop that spends nothing
    , so
    cache.max_hits is a ceiling on the cache's own answers; when it is reached the boundary stops
    answering, the run starts paying again, and the moment is written to the ledger as a note.
    docket demo --cache shows exactly that, and the load harness gained a cached point alongside
    the uncached one (docs/measurements/2026-10-15-2core/cache-report.json): 2,000 identical calls
    across 8 threads, 8 of them reaching the provider, $11.952000 not spent, the receipt still
    verifying. No request coalescing, exact-match keys only, in-memory for one run — each named as a
    limit in docs/caching.md rather than left for a user to discover.

  • A cache conformance vector (spec/vectors/cache.json, cache-digest.json): a small cached
    session both implementations must reproduce, including the saving, the spend it does not
    change, and the case where a hand-edited saving must fail verification.

  • A ninth game-day phase for the cache's own failure mode, and an agent that repeats itself on
    request
    (docket game-day --phases cache-brake, DOCKET_AGENT_MODE=loop). Every other phase
    asks what happens when something breaks; this one asks what happens when nothing does — the loop
    stops costing money, so nothing a dollar cap watches will ever stop it. The reference agent's
    usual modes append a step counter to each prompt, which makes every request unique and a cache
    pointless, so loop sends the same body every call: the traffic a cache is for. The phase runs
    eight calls against a max_hits of 2 and checks the ledger and the disk rather than the
    intention — two model.cache_hit events, the ceiling recorded as a note and named in the
    receipt, the calls after it arriving at the provider again, the saving recomputed by docket verify, and the spend still describing money actually spent. audit_run gained the two facts
    the check needs (cache_hits, cache_ceiling_note) so the audit stays a re-derivation from the
    file, and a phase that could not see them would fail rather than pass vacuously.

  • The test table in docs/testing.md can no longer go stale quietly. Its counts are collected
    (pytest --collect-only) and now checked: tests/test_doc_links.py re-collects the suite and
    fails on any row that disagrees, or on a test file the table has forgotten. The paragraph above
    the table used to say the CI run verified this; nothing did, and the first run of the new test
    caught the table missing a file that had just been added to it. This is the same failure mode as
    the stale conformance vector — a number in a document that no test reads — so it gets the same
    treatment.

  • A load point and a soak, as real supervised runs (python -m docket.loaddriver,
    tools/loadtest.py, make load, make soak). The driver is a supervised child like any other
    agent: it goes through DOCKET_PROXY_URL, so every call it makes is metered, capped, recorded
    and verifiable afterwards, and a load generator that talked to a provider directly would be
    measuring something else. It separates refusals (the boundary enforcing a limit, with the code
    it sent) from errors, paces itself to a target rate so an endurance run lasts the whole window,
    and summarises client latency with the receipt's own nearest-rank rule. The orchestrator starts a
    scripted provider as a service, runs docket run, samples the supervisor's RSS once a second
    (plus VmHWM for the peak a sample can miss), then reads the receipt and the ledger back and
    refuses to call the run usable as evidence if the client's count and the ledger's rows
    disagree, if any response row lacks a latency, or if the receipt no longer verifies. make load
    and make soak are opt-in targets: a shared CI runner's throughput would be a fact about the
    runner, so CI runs this harness's tests instead.

  • docs/testing.md states the measured numbers — throughput, client-latency percentiles and
    memory over ten minutes, with the host, the exact commands and the two things the measurement
    cannot support (a provider's latency, and any claim about another machine).

  • Snapshots and rollback (--snapshot, --rollback-on-breach, docket restore): a
    content-addressed copy of the watched tree's contents, taken before the agent starts, with
    a manifest of path, digest, size, mode and mtime (docket/snapshot/v1). A breached run
    restores the tree automatically and records what it restored as x-docket.rollback-applied;
    docket restore <receipt> performs the same restore on demand, dry-run by default. Paths the
    snapshot could not cover are named in ops.snapshot.not_covered; a tree that does not fit the
    budget is refused before the agent starts (exit 43); a file edited after the run is
    refused unless --force. See ADR 0004.

  • Restore confinement: a restore only ever touches files inside the tree it was pointed at.
    Paths from a ledger or manifest are checked, not trusted — no absolute paths, no .., no
    symlinked component, blobs located by digest inside the store, digests required to be 64 hex
    digits, modes masked to permission bits — and the receipt, ledger and manifest must agree on
    the tree, with --root when they do not.

  • The evidence directory is excluded from the watched tree by the Supervisor itself, from
    resolved paths, and the exclusion is recorded (a note event and the effective policy in the
    receipt). Previously a programmatic run whose docket directory sat inside the working
    directory recorded its own ledger as a file the agent changed, and a rollback plan derived
    from that diff contained an instruction to delete the record it came from.

  • docket game-day gains a rollback phase (--phases rollback): seeded files, an agent
    that damages them, a cap breach, then automated restore — audited against the disk (files
    byte for byte), the receipt and the ledger, including that the run's own evidence survived.

  • Two URL guards, one per kind of caller, applied at every urlopen in the package:
    util.http_url (the scheme must be http or https, and a host must be present) for code that
    stands in for an agent and may be pointed at any provider, and util.local_http_url (that,
    plus the host must be this machine) for Docket's own clients — the boundary recorder and the
    self-test probes — whose address comes from the environment. A DOCKET_PROXY_URL that is not
    loopback is now refused when the recorder is constructed rather than at the first event, and
    0.0.0.0 is not accepted as a destination. This is the answer to the static-analysis
    finding that flags an unchecked urlopen as a possible SSRF or local-file read.

  • An applied restore writes its own record: .docket/restores/<utc-stamp>-<docket-number>.json
    (docket/restore/v1) with the receipt and store it came from, the resolved root, whether
    --force was used, every per-path outcome, the counts, the uncovered paths and the exit code.
    Several restores of one run are all kept. The run's ledger and receipt are never edited: they
    are historical observations, and appending to them after the fact would break the receipt's
    agreement with the ledger it cites. Dry runs write nothing.

  • tools/mutate.py, and make mutation: single-site source mutation — comparison and boolean
    operators, not, boolean and small-integer constants, min/max, and any return, rewritten
    through ast and spliced back by character span. One mutant per run against a declared test
    subset, in a copy of the repository, with the harness falsifying itself first (splicing back
    verbatim must be a byte-for-byte no-op, and the unmutated sandbox must pass before any mutant
    runs). Current scores and every classified survivor: docs/testing.md.

  • make audit: bandit over src/ and tools/, also a CI step. Every remaining class of
    finding is either fixed or has one written-down reason in [tool.bandit] ("PASS" as an
    output word, seeded random, list-argv subprocess); nothing is suppressed without a reason.

  • Per-call latency, and a verifier that recomputes it. Every usage row may now carry
    latency_ms — the round trip the recorder measured, in whole milliseconds — and the receipt
    derives latency from those rows: {"method": "nearest-rank", "measured_calls", "total_rows", "total_ms", "max_ms", "p50_ms", "p95_ms", "p99_ms"}. The rule is stated because a percentile
    without its rule is not a fact: the samples are sorted ascending and the rank is
    ceil(p/100 · n), clamped into the sample, so p99 of 15 calls is the 15th value and p99 of 200
    is the 198th. Both verifiers recompute the summary from the same rows and fail on a member
    that disagrees, a method this verifier does not implement, a member the format does not
    define, a row value that is not a whole non-negative count, an unreadable appendix, or a
    summary stated over rows that carry no measurement at all — the last one being a false-pass
    hole that mutation testing found in this feature rather than a reviewer
    (docs/verification.md, spec §6.2). A receipt with no measurement says
    nothing rather than zero, and coverage is explicit: measured_calls of total_rows.

  • tests/test_invariants.py: the properties no single-feature test can state. Fourteen
    executed invariants over the places an operations reader worries about — two writers on one
    ledger are refused rather than interleaved (a file holding two chains is a file that accuses
    an honest run of tampering); eight threads appending 200 events through one handle produce one
    event per sequence number, in order, with no partial line; a replayed request is recorded as
    two calls and never deduplicated, because collapsing a duplicate would report a run that
    did not happen; a stopped boundary releases its socket and its thread; a supervised agent killed
    with SIGKILL leaves a readable prefix and no receipt that claims a clean exit; a wall-clock
    cap is measured on the monotonic clock, so a clock that jumps forwards cannot extend a run; a
    clock that jumps backwards cannot turn a bad clock into a false breach.

  • DOCKET_AGENT_MODE=backoff in the reference adversarial agent, plus Retry-After parsing
    (DOCKET_AGENT_MAX_SLEEP, DOCKET_AGENT_SEED): full jitter under a ceiling, doubling per
    refusal, with the wait and the hint the boundary asked for written into the ledger as note
    events. This is the client's half of the rate-limit contract, and without it the boundary's
    Retry-After was a promise nothing in the repository ever exercised.

  • A fixed boundary port, and evidence of which port it was. --port N (docket run) binds the
    boundary to a loopback port an orchestrator can probe; 0 (the default) still means any free
    port. The requested port is recorded in session.start.boundary.requested_port and the port that
    was actually bound in a note written once the socket exists, so the evidence never states a
    guess as a fact. Privileged ports are refused at the flag with the range named.

  • --upstream NAME=URL, repeatable, for pointing a provider prefix at a container, a mock, an
    OpenAI-compatible gateway or a self-hosted model. The override is recorded as evidence — name and
    destination only, with query, fragment and userinfo stripped by util.sanitize_url, because a
    token pasted into a base URL must never reach a hash-chained file (tests/test_proxy.py asserts
    the secret is nowhere in the ledger's bytes). docket init's emitted policy and this flag are the
    two ways to configure a container; both are checked by tools/deploy_check.py.

  • python -m docket.fakeupstream <flags>: the scripted provider as a service, with --host,
    --port, --steps, --model, --input-tokens, --output-tokens, --delay-ms, a JSON
    handshake on stdout, a real GET /healthz, exit 2 when the port is taken, and a warning when
    asked to bind beyond loopback. This is what makes the compose file and the Kubernetes manifest
    produce a real receipt offline.

  • The boundary's operations endpoints are documented (docs/cli.md): /docket/healthz,
    /docket/readyz (readiness is an evidence question — ledger writable, volume above its floor,
    breaker shut) and /docket/metrics (Prometheus text). They are loopback-only by design, and
    docs/deployment.md says what that means for probes and scrapers.

  • deploy/: a container recipe (Dockerfile: two stages, non-root, read-only-root-friendly, no
    credentials, STOPSIGNAL SIGTERM), a compose smoke test whose verifier waits for the run and
    mounts evidence read-only, a hardened pod topology (two containers sharing loopback, Recreate
    strategy, probes on declared ports, default-deny NetworkPolicy), and a Helm chart in a declared
    template subset
    so a deterministic renderer can check what it produces.

  • tools/deploy_check.py and make deploy-check: 16 static checks over those artefacts — no
    :latest, non-root, no privilege escalation, all capabilities dropped, read-only root filesystem,
    resource requests and limits, probe ports that are actually declared, one writer per evidence
    volume, a ConfigMap policy parsed by Docket's own loader, default-deny networking, a compose
    verifier that waits, and CI workflows that parse (a workflow with a YAML error runs nothing and
    looks green — this check found exactly that in this repository's own CI). It exits 2 rather than
    passing when its YAML reader is missing. tests/test_deploy_check.py breaks each invariant on
    purpose, one fixture per check, so the checker cannot be decoration.

  • docs/resilience.md — what happens when the world misbehaves (breaker, throttle, timeouts,
    caps, evidence guard, ceilings, readiness, clocks, interruption), each mechanism with how it is
    proved, and the three things deliberately refused. docket init has pointed at this file since
    0.1.0; it now exists.

  • docs/production-coverage.md — a map of 77 production-engineering concepts, each cell
    naming the code and tests behind it, or the document that argues the boundary, or the reason it is
    out of scope, in a controlled vocabulary (built, measured, modelled, reasoned, out of
    scope
    ). It states the three kinds of absence separately instead of blurring them.

  • The docs site builds, strictly, from a staging step that carries what the docs link to.
    mkdocs build --strict aborted on this repository: docs/deployment.md and the measurements
    README link to tools/deploy_check.py and tools/loadtest.py, which are real files, referenced
    the way every other repository path is referenced — and which tools/build_docs_site.py did not
    copy. The staging step now carries them, and tests/test_doc_links.py stages the tree in a test
    and resolves every relative link in the docs against it, so this needs no mkdocs-material to
    check.

  • deploy extra, and the load driver's own guards. pip install -e '.[deploy]' installs the
    checker's YAML reader on its own; tests/test_loadtest.py gained tests for the argument guards
    (calls/concurrency below one, a boundary URL the driver cannot parse), the zero-length
    summary, the report's sorted-key byte-stability, the driver's --help text, and the RSS
    sentinel (UNMEASURABLE, now a named constant rather than a literal -1 compared twice).

  • Two cache tests that close the boundary cases the mutation sweep kept asking for. A mutant of
    the origin guard (seq < 0 → seq <= 0) survived because no test ever stored a hit that copies the
    first event in a ledger: the ledger numbers its events from zero, so event 0 is the first
    provider call of every run — and the response a cache is most likely to be asked for twice. The
    test builds a real ledger, takes event 0, stores a hit that names it, and reads the group back
    through the summary the receipt is built from. The second test is a one-second TTL: a guard written
    ttl_seconds > 1 treats a one-second TTL as "no TTL at all", and the other expiry test's thirty
    seconds cannot tell the two readings apart, so the entry would be served stale until eviction.

  • A second mutation round on resilience.py, and the two paydown claims it falsified. A wider
    re-measurement of the same module reported 25 survivors where the first round had classified
    fourteen, and two of those survivors were covered — or so their docstrings said — by tests that
    named them. The re-run showed what had actually happened: one mutant is equivalent (both
    probes_in_flight = 0 writes in the breaker are overwritten by the reset inside allow, so the
    test that claimed to kill them kills the one that matters), and one was never reached (the
    half-open return True, which needs a probe that fails, a path no test walked). Both docstrings
    now say which mutant they kill and why. Five tests close the rest: a failed probe reporting that
    it re-opened the circuit, the limiter's refusal counter, the fault injector's per-kind
    probability guards (a probability: 0 fault must not be injected, and a drop must never be
    returned as a response rewrite), every fault kind describing itself rather than another kind, and
    a clean EvidenceGaps that is neither degraded nor failing fast. The survivor list is a line
    number index into the source: a claim that one test kills "both" mutants is a claim about
    positions, and it goes stale the moment the file moves.

  • The Helm-fidelity tool's own contract is tested, not just its happy path. The tool existed to
    be run by a person with Helm installed; its mutation sweep killed 13 of 47 candidates, which is
    the shape of a tool whose reporter is untested. The suite now drives main() through a
    stand-in client: renders that agree exit 0 and name the version and the file they skipped,
    a field that differs exits 1 with the object and the field path, a Helm that fails (or cannot be
    executed at all) exits 2 with the client's own message, a stream that names one object twice is
    refused by kind and name, and a client that cannot answer version is labelled unknown rather
    than crashing the comparison. That took the score from 27.7% to over 85% without touching what
    the tool does.

  • The standards map has a guard of its own. docs/testing.md's counts were pinned and the
    standards page was not, so tests/test_doc_links.py now asserts that every source cell names
    something a reader can look up — a year, a domain or a document identifier — that every strength
    comes from the small vocabulary the page defines, and that the size the companion maps quote is the
    size the page has. It caught one row on its first run: a book, named with neither a link nor a year.

Removed

  • The adoption-and-sponsorship model, its detail page, and the star-count sweep that fed them.
    The adoption model argued from GitHub star counts — how many repositories hold more than 100,000
    stars, how fast ten comparable projects reached theirs, and what that implied about this project's
    own twelve-month target — and its companion page costed seven deliverables against a funding goal.
    Both were written for a reader deciding whether to bet money on the project, which is a real reader
    and not the one the repository is for. Presenting the tool primarily through its growth targets is a
    claim about attention, not about software, and the two documents spent more words on the star
    arithmetic than on what the boundary does. The raw sweep JSON went with them, because nothing else
    in the tree quoted it.

    What is kept, and why: the landscape figures in docs/evidence.md §2 — stars
    and forks for comparable projects, fetched 2026-10-13 and labelled as a snapshot — because they
    size the field a reader is choosing in. The governance principle that funding cannot buy a format
    change, a roadmap veto or silence about a defect, because that is a safeguard rather than a pitch.
    And one sentence in the README, in the licence section, pointing at
    SPONSORS.md, which is now a short page stating what support pays for, what it does
    not buy, and that a sponsorship does not buy support.

    tests/test_strategy_numbers.py went too: it existed to reconcile the deleted page's figures
    against the deleted sweep, and a test guarding a removed claim is decoration.

Fixed

  • The tamper demo is now reproducible. docket demo --tamper doctored the ledger and
    printed NOT VERIFIED, then told the reader nothing about how to see that for themselves —
    and the obvious follow-up, docket verify <receipt>, answered VERIFIED, because the receipt
    is intact and nothing had looked for the ledger. The demo now prints the command that
    reproduces its own verdict, receipt and ledger included, and docket verify finds that
    ledger. Four documents also told a reader to run pip install docket as the first step for a
    package PyPI answers 404 for; each now names the install-from-source command that works today
    and says when the published one starts working, with a test that refuses an unmarked one.
    The README's --tamper transcript is labelled as one run of a demo whose event number and
    hashes change each time, and exit code 43 is stated as what it is: the policy could not be
    loaded.

  • The growth model re-based onto the sweep it names. Every figure in its comparison table, its
    three ecosystem-threshold rows and its inline audience sentences became the value in that sweep, with
    ages as months since creation and each rate as the stars over that age. The undated first sweep was
    kept as it was written: it fed the adjacent-audience rows the research log quotes, and an observation
    archive is not re-based.

  • The accessibility checker read a form control's own content as its name, and a wrapping <label>'s
    whole text as a label.
    The checker was swept by the mutation harness (136 candidates, 134 killed,
    98.5% — the score and both survivors are in docs/testing.md), and reading the survivors found three
    defects in the tool. A form control is not named by its content — WAI-ARIA gives textbox, combobox
    and checkbox the author as their name source — so a <label><textarea>draft</textarea></label> was
    named draft, a value used as a name. A wrapping <label> with no text of its own fell back to the
    whole wrapper, which names a control after its own value; it now contributes only the text outside the
    control. And the document title was read twice by two handlers keeping a flag in step, with both
    branches unreachable — the title is read from the finished tree, because an unkillable branch is
    deleted rather than explained.

  • The accessibility checker read the title attribute before the element's own text. No verdict
    changed — a tooltip is a name, so a link named by one passed either way — but the precedence was
    wrong, and the count it fed was wrong with it: the first tally called 442 links by their tooltip
    when a reader hears their text. The order is now the one Accessible Name and Description
    Computation 1.2
    §4.3.2 step 2 specifies — aria-labelledby, aria-label, a control's <label>,
    name from content, and only then the tooltip, which the specification marks as used only if
    nothing else, including subtree content, has provided results
    . Three tests assert the precedence
    from the report rather than from the verdict, because the verdict cannot tell the difference.

  • The code of conduct carried two h1 headings, which the new site check found on its first run.
    CODE_OF_CONDUCT.md opened with a title line above the Contributor Covenant's own heading, so the
    rendered page had two roots for the outline a screen reader announces. The Covenant's heading is
    the document's heading now.

  • tools/a11y_check.py's "could not run" path exited 1, not the 2 its docstring promises.
    sys.exit("message") exits 1 — the code this project reserves for "found a problem" — so a
    directory that was not a built site would have been recorded as a failing check. The tool's own
    test caught it: test_exit_codes_are_0_1_and_2.

  • A documented-flag class of defect that had never been checked, and a re-base that was typed rather
    than measured.
    Three of the last four dependency-matrix rounds were quoted in this page with a
    number worked out by hand from the previous round's, and all three were wrong — by one, by two, by
    two — which is the same failure the matrix section already records for paste | bc, one level up.
    The sections now say so, and the plan's two moving numbers are written by
    mutation/rebase_plan_numbers.py (workspace, not shipped), which reads them from the measurement,
    refuses a missing label instead of reading it as zero, and re-reads the file before reporting.

  • docs/incident-corpus.md opened by giving the failure mode a name it does not have. "Docket's
    whole reason to exist is a failure mode with a name: Denial of Wallet" — the name is
    unbounded consumption (OWASP LLM10), and "denial of wallet" is how practitioners describe it. The
    page now says so, in the same correction round as the fabricated rank above.

  • A fabricated citation rank in four documents, including the README. "OWASP's Top 10 for Agentic
    Applications
    puts denial of wallet at risk #2" appeared in README.md, docs/evidence.md and
    docs/incident-corpus.md, and the phrase it stood for — that the agentic list
    "treats resource/cost abuse as its own class" — is not true either: its classes are ASI01–ASI10, and
    cost exhaustion is a class in the LLM list (LLM10:2025 Unbounded Consumption), which all four
    documents already cite correctly two lines away. The evidence ledger had even hedged that the rank
    "could not be pinned to the primary text, so this repository does not cite a rank for it", so the
    repository was contradicting itself in adjacent paragraphs of the same page. The primary page was
    fetched and read (2026-10-16) to settle it: it states the date and the 100+ reviewer peer review and
    does not enumerate the entries, so the entry names are grade B (five consistent secondary
    write-ups) while the framework's existence, date and review are grade A. All four sites now name the
    classes a runaway spend actually belongs to — ASI08 Cascading Failures and ASI10 Rogue
    Agents
    — and docs/evidence.md carries both grades with the earlier wording marked as wrong.

  • make test and four CI jobs ran the suite two -qs deep, so their logs carried no counts.
    pyproject.toml's addopts already carry one -q; a command line that adds another asks pytest for
    -qq, at which level it prints the progress line and nothing else. It exits 0 either way, which is
    how this survived review: green jobs, no line saying how many tests passed, and a CONTRIBUTING.md
    promising "1028 tests" with nothing in the output to confirm it. Measured before it was fixed — with
    addopts = "-q --strict-markers", pytest tests -q prints 1 passed in 0.00s on a one-test file,
    and the same command at -qq prints a dot and stops. All five lines now run plain
    pytest tests -ra: the counts line is back in the Makefile's output and in every CI log, with the
    skip reasons beside it.

  • Two survivor rows in docs/testing.md account for their mutants line by line now, and one of
    them gave the wrong reason.
    ledger.py:628-646 is six mutants under one verdict, and the verdict
    was written as if all six sat in dead code: "those branches are the else of fcntl is not None,
    so on any platform with flock they cannot run at all". Five of them do — :628's and/not and
    the unlink, return and True→False beside it are all inside _acquire_lock's
    try: # pragma: no cover - Windows block, past the fcntl early return. The sixth, :646's
    unlink(missing_ok=True) in _release_lock, is reached on this platform and is a different
    kind of survivor: missing_ok=False raises FileNotFoundError for a lock file that is already
    gone, and the line above suppresses OSError, so the two spellings cannot be told apart here. The
    row now says which is which and cites every line. The Windows-probe row (ledger.py:664-672) does
    the same: it names all ten of its mutants — the two WinDLL/OpenProcess arguments, the two
    if not … guards, what each guard returns in both spellings, and the liveness comparison in both
    — instead of naming four families of them and leaving the count to the reader's arithmetic.

  • The Helm-fidelity comparison had one gate and two prerequisites. With a Helm client on PATH
    and PyYAML absent, test_the_checkers_render_is_the_render_helm_produces ran and failed on
    tools/helm_fidelity.py's PyYAML is not installed exit 2 — a red suite for a setup the README
    describes, where every other test in the file both skips and says why. The gate named the client
    only; it now names both, in the idiom tests/test_deploy_check.py already used
    (importlib.util.find_spec("yaml") is None). Found by re-running the dependency matrix with the Helm
    client installed, which is what turned a skip into a run: cells 1 and 2 were red with one failure
    each, and the round after the fix is green in all four — 980 / 985 / 1,022 / 1,027 passed with
    47 / 42 / 5 / 0 skipped, and the documented dev setup now runs the whole suite with nothing
    skipped
    .

  • Two measured reports the documentation names were in the authoring checkout and in no clone.
    .gitignore ignored load-report.json and soak-report.json as bare names, which git reads as
    "this name, at any depth" — so the curated reports under docs/measurements/2026-10-15-2core/,
    named by that directory's README, by the FMEA row for tail latency (load-report.json,
    soak-report.json) and by docs/production-coverage.md (cache-report.json), were tracked by
    nobody. Nothing in the test suite noticed, because the test that holds those names to the tree ran
    where the ignored files happened to be present. The rules are anchored to the repository root now
    (the copies make load, make soak and make cache-load write there are still outputs), the four
    reports are in the tree, and the new canary above is the reason this cannot come back quietly. It
    was found by running the release checks against a clone of the bundle rather than against the
    working directory — the same check that exists to notice a difference between the two.

  • Four tests in tests/test_cache.py killed one mutant on their line and stopped one token
    short of the other
    — and what said so was the sweep, not a reading. The verifier's full corpus is
    177 candidates; after the paydown round it stood at 172 of 177 (97.2%) with five survivors, and each survivor was
    re-checked with --only-line, the harness's own machinery, before anything was written. Four of the
    five were near misses of four different kinds: an assertion that ended before the type suffix
    (verify.py:554, where malformed[0] appears twice on one line so the value and the type are
    two mutants), a fixture that measured one call, where p50 and p99 are the same number
    (verify.py:568), a problem list two entries long where the stop condition's two spellings part
    company only at exactly three (verify.py:761), and a refuted group whose second sentence — the
    tally loop's, a separate pass that does not skip the group the guard refused — was never asked for
    (verify.py:867). All four
    repairs are assertions about the same receipts and none of them touched src/docket/verify.py; the
    re-sweep reads 176 of 177 (99.4%), with the argued-equivalent verify.py:186 as its only survivor. The cache's
    re-measured 128 of 128 (100.0%) is in
    the same table, because tests/test_cache.py is in that subset too.

  • Two maps named "33 kinds" where the kind table has 44, and the harness's own documentation named a
    symbol that does not exist.
    TARGET_TESTS was never a name in tools/mutate.py; the map is
    DEFAULT_TESTS, and both the tool's docstring and docs/testing.md said the other one. The count
    in docs/production-coverage.md and docs/sre-coverage.md had rotted the same way — a number
    written when it was true, beside a table that grew. Both are the class of claim that a reader
    cannot check and a test cannot see, which is why the seven brief terms that were answered only in an
    unsearchable spelling (E2E, localization, black-box/white-box/grey-box, A/B,
    postmortem) now appear in the maps in both spellings.

  • The optional-dependency matrix described a suite 152 tests smaller than the one in the
    tree.
    The four cells in docs/testing.md quoted 840/845/868/878 passed — a first row whose own
    two numbers sum to 873 tests where pytest now collects 1025 — from a round measured before
    the contract, cache, i18n and fuzz suites existed. They are re-measured — a fresh venv per cell,
    pip install pytest plus the extras its row names, -ra so every skip carries its reason — and the
    third column now names what each skip would have checked. The previous matrix also recorded
    0 skipped in every cell beside summary lines reading 47, 42, 6 and 1: the skip counts had been
    summed with paste | bc, and bc is not installed everywhere. The sum is awk now, the counts are
    checked against each run's own summary line before they are quoted, and the defect is recorded in
    docs/testing.md rather than quietly fixed.

  • The documentation's own consistency check read the built site as if it were the documentation.
    tools/build_docs_site.py stages a copy of the whole tree into .mkdocs-site/, and
    tests/test_doc_links.py scanned every markdown file it could find — so after make site, the same
    page was read twice, and the copy's own illustrative example of a counted reference failed the guard
    as a test file that does not exist. A copy is not a second opinion. Both build directories are
    excluded from the two scanners, and a twelfth test in that file pins the exclusion by asking the
    predicate about paths — including one that merely contains the word site — rather than by building
    a site in the test.

  • This file had a bullet spliced into another one. The verifier-subset bullet ended mid-sentence
    and a second bullet's text continued from it, so a reader got one paragraph that was two claims and
    a sentence that stopped without a verb. It is repaired above; the two docs/testing.md bullets that
    said the same thing about stale paydown notes ("can go stale" and "two of them were wrong") are one
    bullet now, with the third shape the 2026-10-16 sweep added: the test did kill a mutant on its line,
    and stopped one token short of the next.

  • A mutation score was taken under an interpreter that had lost an optional dependency, and
    5 mutants were recorded as survivors because the tests that would have caught them
    skipped.
    The first re-measurement of src/docket/verify.py on 2026-10-16 read 171 of 177 (96.6%) with 6
    survivors; 5 of them are the mutants of the signature block (verify.py:931, verify.py:933, verify.py:934, verify.py:937), which is
    exactly the region tests/test_signing_and_attach.py and tests/test_vectors.py are in that
    subset to cover. They were not missed by the subset and they are not equivalent: cryptography —
    the optional extra that seals and signs — was absent from the interpreter the harness runs tests
    with, so every signature test skipped, and a skipped test cannot fail. With the extra installed
    the same tree reads 176 of 177 (99.4%) against an unchanged src/docket/verify.py. The harness now carries the
    control run's own counts (passed, failed, skipped) in its JSON and markdown reports, marks
    that control clean only when nothing skipped, and prints a warning beside the score when it did
    not. Writing the four tests that pin it in both polarities found a second way the same number could
    have gone missing: the harness passed -q to a pyproject.toml whose addopts already carry one,
    and at -qq pytest 9 prints no counts line at all — so the control's counts would have been read
    from a line that never printed. The flags are asserted by a real pytest run inside the suite now
    (tests/test_mutate.py, 40 tests) rather than by reading them.

  • The dependency matrix caught a test that depended on how fast the machine was. In the
    pytest + PyYAML cell — the configuration docs/production-coverage.md describes for running
    without the signing extra — tests/test_snapshot_restore.py failed: 1017 passed,
    6 skipped, 1 failed, exit 1, where the other three cells
    were green (977 / 982 / 1,023 passed and 47 / 42 / 1 skipped, all exit 0). The test restores the same run twice and
    looked the two record documents up by filename, expecting the second to carry the -2 collision
    suffix; the two commands stamped their records 20261016T081327Z and 20261016T081328Z — one second apart, so there
    was no collision, no suffix, and a StopIteration on a machine merely slower than the one the test
    was written on. The test now identifies its records by what they say — one did the work, the other
    found it already done, neither overwrote the other — and the collision is a test of its own with
    docket/cli.py's clock pinned to one instant, so the suffix is exercised by construction instead of
    by luck.

  • The guard that checks test counts beside a filename was checking one mention per file, not
    every mention.
    tests/test_doc_links.py collected its references into a dict keyed by the test
    file and used setdefault, so only the alphabetically first sentence about each file was ever
    compared with pytest --collect-only: docs/sre-coverage.md's count of test_chaos.py was wrong
    and unread behind two correct mentions of the same file elsewhere. It now walks every occurrence —
    which failed immediately, named the stale sentence, and now passes. The same shape of defect as the
    one the guard was written for, one level up: a claim with no reader looks exactly like a claim that
    was checked.

  • Every game-day phase after rate-limit was quietly running under the previous phases' limits.
    _phase_policy built each phase's policy by sharing the base policy's Resilience object, so the
    two mutators that write to it edited the base itself: rate-limit's two-calls-per-second bucket and
    ledger-pressure's eight-event ceiling were still in force for ledger-pressure, rollback,
    cache-brake and supervisor-killed. cache-brake is where it surfaced — it was stopped by a
    ledger ceiling it never declared, after a single provider call, with zero hits, and two of its own
    checks failed, so docket game-day (and therefore CI, where it runs on every push) exited 1 with
    8 of 9 phases met. rollback "passed" the same way, with a cap breach that was not the one it
    declares. The policy is now a deepcopy, and tests/test_chaos.py asserts the general property —
    building every phase against a base another phase has already been built against must give exactly
    the policy a fresh base gives, and the base must come back untouched — rather than the two values
    that happened to leak. All nine phases pass with the fix in place, and cache-brake's first run as
    its own experiment is the first time it has reached its own hypothesis: 8 calls, 2 hits, the
    ceiling note in the ledger, and saved_usd recomputed by the verifier.

  • The mutation harness mutated the wrong characters on any line containing non-ASCII text, and
    dropped the result without a word.
    _span added ast's UTF-8 byte offsets to character
    offsets computed from the source's lines, so on a line with an em dash before the token the splice
    landed inside a character; the mutant text did not compile, and a bare continue discarded it.
    Every site on every such line was invisible to every score this repository has published:
    verify.py offered 95 candidate mutants where it has 177 (46% of its sites), ledger.py 171 where
    it has 172. The fix converts both offsets through _char_offset, the drop paths now emit records
    that the sweep prints and writes into the report, and tests/test_mutate.py pins the regression
    with a fixture whose mutable token sits after an em dash. tools/helm_fidelity.py's second round
    (27.7% → 85.1% of 47 mutants) and the new tests above landed in the same pass.

  • A note that said two secondary summaries disagreed about whether an HTTP 429 may be cached now
    cites the norm that settles it.
    RFC 6585 §4 was read in full for this: it ends with "Responses
    with the 429 status code MUST NOT be stored by a cache" — and says the same of 428, 431 and 511.
    Docket's behaviour was already compliant (its cache stores 2xx only, and a test pins that a 429 is
    refused with a reason); what changed is that the rule now has a normative reason in
    docs/standards.md and in the architecture invariant that states it, instead of resting on a
    judgement call.

  • The site navigation listed two ADRs twice. mkdocs.yml carried 0002 and 0003 again after
    0005, which is the kind of defect a strict build does not catch — the pages resolve, so nothing
    fails; the menu simply has the same entry twice. Removed, and docs/sre-coverage.md added to the
    section it belongs in (the link checker asserts that every document is in the navigation, so a new
    document cannot be added without one).

  • A response cache supplied to the boundary was silently ignored while it was empty. RecordingProxy
    chose its cache with cache or ResponseCache(...), and ResponseCache defines __len__ — so an
    empty cache was falsy and a caller who passed their own (enabled, configured) object got a
    different, policy-built one instead. Nothing failed: the boundary simply did no caching, and it
    started caching later if the cache they passed happened to be non-empty, which is a defect that
    appears and disappears with traffic. The unit tests passed a cache through the policy, so none of
    them could have caught it; docket selftest's new cache probe passes one as an argument, the way a
    host application would, and caught it on its first run. Fixed in both places — the boundary compares
    is not None, and ResponseCache.__bool__ says plainly that an empty cache is still a cache —
    with a regression test that supplies a fresh cache and asserts the second identical call is a hit.

  • The Helm chart rendered a policy value Docket read as a float, and the checker that was supposed
    to catch that rendered it as an integer.
    Helm hands every template a float64 for each number
    in values.yaml (YAML → JSON → map[string]any), and Go prints a float64 in scientific
    notation from 1e6 up, so the ConfigMap mounted into a pod said
    max_ledger_bytes = 6.7108864e+07 rather than 67108864. TOML reads that as a float; Docket's
    loader accepts a whole float, so the run would have worked — with a policy whose type is not the
    one the key documents, and with a config in the cluster that differs from the one the repository
    reviews. It went unnoticed because tools/deploy_check.py rendered values the way Python writes
    them instead of the way Helm writes them, which made the checker's verdicts evidence about a
    document no cluster would ever see. Three changes: the chart writes its integers through
    | int64; the checker converts values to the types Helm's templates receive and formats numbers
    with Go's shortest representation (_go_float, pinned against a table produced by running helm template, Helm v3.16.3); and configmap.policy-parses now refuses an integer key that arrives as
    a float, naming the line and the fix. helm lint and helm template were also run against that
    release for the first time — the chart lints clean and its five documents are now identical,
    field for field, to the static renderer's (deploy/README.md). helm install still has not run:
    there is no cluster here.

  • A documented default that silence silently removed. resilience.max_ledger_bytes is
    documented as 256 MiB, and every deployment manifest sets it explicitly — but a hand-written
    policy file that omitted the key got no byte ceiling at all (None), a weaker policy than the
    documentation promised, applied to anyone who used the key's absence as the default it reads as.
    Both shipped example policies were in that position. Absent now means the default, like every
    other key in the section; 0 is how an operator asks for no ceiling on purpose, and the
    behaviour change is stated here rather than slipped into a patch release.

  • A test that could not fail was hiding a stale vector. test_committed_vectors_match_the_generator
    copied the committed vectors into a staging directory but wrote the generator outside it, so the
    generator overwrote the real files and the comparison was between each file and itself. It passed
    for as long as it existed, and spec/vectors/receipt.json had in fact drifted from the
    implementation: the latency member and the policy's cache disclosure line were both missing from
    it. The comparison now runs with the generator inside the staging directory, a canary file is
    overwritten first to prove it wrote there, the set of files produced is compared with the set
    committed, and the vector has been regenerated (two golden renders follow it — the diff is one
    added line in each). The Node suite had a second instance of the same failure mode: it asserted
    that receipt.json carries no latency, which was true only because of the drift, so it now builds
    a receipt without latency rather than reading one.

  • The deployment checker exited 1 when it could not run, which is the code it uses for "found a
    problem".
    Its docstring, docs/cli.md and the operator's own reading all say 2 — "could not
    run" and "found a defect" are the two answers a check has to keep apart. raise SystemExit("text")
    exits 1; the reason now goes to stderr and the code is 2, and the test that pins it shadows the
    yaml import instead of uninstalling anything.

  • Twenty-eight tests failed — did not skip — when PyYAML was absent, so a bare clone ran red
    while the documentation promised a green suite, and the CI test job (which installed only
    pytest and cryptography) could not have passed on any of its three interpreters. The manifest
    tests now skip with the reason the way the Ed25519 tests do; the renderer tests, which never read a
    manifest, keep running without it; and CI installs pyyaml so the job stays at zero skips.

  • The test that claimed to pin the load driver's pacer passed with the pacer mutated. It
    measured total wall clock through a cold boundary, where the first call's own cost cleared the
    threshold — so max → min in the schedule survived a test written to catch it, and the soak's
    central claim ("the load is paced") was pinned by nothing. It warms up first now and asserts the
    schedule's floor and the ratio against an unpaced run.

  • ruff was floored, not pinned, in the dev extra, while CI pinned 0.6.9. The formatter's
    output is not stable between releases — 0.17 rewrites seventeen files in this tree that 0.6.9
    leaves alone — so the documented setup could fail make lint on files CI calls formatted, and
    make fmt could rewrite seventeen unrelated files. The extra now pins the version CI installs.

  • RateLimiter accepted numbers it could not honour, and failed inside a request.
    RateLimiter(per_second=Decimal("0.5")) — which is exactly what the policy loader produces —
    passed construction and then raised TypeError: float * Decimal on the first call, surfacing as
    an HTTP 502 in the middle of a run. Worse, per_second=float("nan") was accepted and silently
    disabled the limiter
    : every comparison against NaN is false, so tokens < 1.0 was never true
    and every call was allowed. per_second, per_minute and burst are now coerced and validated
    at construction (positive, finite, whole where a count is meant) with a ValueError naming the
    value and the type; a burst of zero is refused rather than becoming a limiter that refuses
    everything forever.

  • A provider's error code was read as a Docket refusal. The reference agent took any
    error.code from a non-2xx body as the boundary's refusal code, but the boundary relays upstream
    error bodies verbatim and providers use error.code for their own codes (invalid_api_key):
    a rejected credential would have been recorded as the boundary enforcing a limit, and the agent
    would have backed off politely instead of surfacing a broken key. There is now one reader
    (policy.denial_code) that identifies a refusal by the envelope's type and nothing else, used
    by both the reference agent and the load driver so the two cannot drift.

  • docket init emitted a policy pointing at a document that did not exist (docs/resilience.md),
    and deploy/ would have shipped referencing two more. references.resolve now fails the deploy
    check on any referenced path that is missing, and the link is real.

  • util.sanitize_url mangled IPv6 literals: http://[::1]:9000/v1 became http://::1:9000/v1,
    which is not a URL. Found by the test that pins the port-and-path behaviour, not by reading it.

  • The supervisor printed a negative duration when the clock stepped backwards. elapsed and
    the receipt's duration_seconds were computed as monotonic() - started, and a monotonic
    source that steps back — a VM restored from a snapshot, a container sharing a stale clock, a
    bug in a caller's injected clock — made both negative. A negative duration is a claim no clock
    can support: a reader comparing it to the cap has to invent a rule for what a negative budget
    means. Both are clamped at zero now, and test_invariants.py drives the case. The direction is
    deliberate: a clock that steps backwards can never raise the elapsed reading, so it cannot
    manufacture a breach, and the child's lifetime still bounds the run.

  • A receipt that stated a latency summary over untimed rows could pass. check_summary
    returned "this claim cannot be recomputed" in that one branch, the verifier read a falsy return
    as "no disagreement", and a receipt could therefore claim any p99 at all as long as its usage
    rows carried no latency_ms. Found by mutating the new module (return problems → return None survived) rather than by reading it; the branch now has the test that proves it is
    reachable, and latency.py scores 21/21 killed.

  • latency.py shipped an unkillable clamp. rank = min(max(rank, 1), len(ordered)) could
    never bind: percentile_ms raises on an empty sample and on a percentage outside 1..100, and
    1 <= ceil(p·n/100) <= n follows from those two guards. The clamp was unreachable code whose
    mutants nothing could kill, so it is gone, with the proof in a comment and two tests that pin
    the extremes of the interval (p1 and p100 of every sample size from 1 to 11).

  • quarantine_torn_tail could report a repair it had not performed. When the interrupted
    write had landed its partial line together with the newline that closed it — a flush that
    half-succeeds, not only a kill -9 — the repair split the file at the last newline, which is
    empty in that shape: it reported performed: true, moved zero bytes and left the ledger
    exactly as damaged as it found it. docket doctor --quarantine-torn-tail now splits by the
    reader's own measurement of the torn tail (torn_tail.bytes, plus the newline when the file
    has one), and test_ledger_contracts.py asserts the moved bytes, the sidecar's contents and
    that the repaired file is healthy afterwards.

  • The Windows lock's liveness probe killed live processes. _pid_alive probed with
    os.kill(pid, 0), which is the POSIX idiom and, on Windows, is TerminateProcess: the check
    for "is the stale lock's holder still running?" would have terminated that holder with exit
    code 0. Windows now asks the process table through OpenProcess/GetExitCodeProcess and
    answers "alive" whenever it cannot tell, because a lock is never stolen on a guess. No Windows
    is available in this environment, so the branch is written, marked # pragma: no cover, and
    named as untested in docs/testing.md.

  • read_with_diagnostics returned eleven keys from a healthy file and nine from a missing
    one.
    healthy and chain_error were documented as present on every path and were absent
    from the "file does not exist" path, so a caller that had only ever seen a healthy ledger got
    a KeyError from a ledger that was not there. The key set is now built once, pessimistically,
    and every path fills in what it learned; test_every_diagnostics_path_returns_the_same_keys
    pins it across six states (healthy, empty, missing, torn, corrupt, not-UTF-8).

  • A ledger's repr carried descriptors and a memory address. _handle, _lock_handle and
    the clock were dataclass fields with default reprs, so a ledger printed into a CI log or a
    pasted traceback showed file descriptors, a lock handle and <SteppedClock object at 0x7f...>.
    They are repr=False now; the repr shows the state that matters (path, events, ceilings, bytes
    written, unwritable events).

  • A document with NaN or Infinity in it was read by Docket and refused by every JavaScript
    verifier.
    Those two tokens (and -Infinity) are a Python extension: RFC 8259 has no spelling
    for them, json.loads accepts them, and JSON.parse rejects them. A request body carrying one
    reached the recorder, could not be canonicalised, and the boundary answered
    502 docket_internal_error — a value the client chose made the run's own evidence read
    "internal error". A ledger, manifest or receipt containing one was accepted by the reader and
    then failed the chain check with "the event's data cannot be canonicalised", a confusing verdict
    for what is really "this file is not JSON"; and a hostile Infinity on the last line of a
    ledger was filed as a torn tail, i.e. it claimed the ordinary crash-recovery path. Now every
    parse of a document Docket did not write goes through util.loads_json, which refuses the
    non-extensions by name (Infinity is not a JSON number (RFC 8259 allows neither NaN nor Infinity)), a request body carrying one is refused at the boundary with 400 docket_bad_request/non_finite_number, recorded as net.denied, and never forwarded — a body
    Docket cannot record is a body it must not forward — and a non-finite line is classified as
    damage (bad_lines), never as a torn write. spec §3.2 now states the rule normatively, since
    it is an interoperability rule and not an implementation detail.

  • Parallel model calls could write the ledger out of order. The boundary is a
    ThreadingHTTPServer, so two calls in flight are two append calls: the sequence number,
    the prev link, the digest and the write were computed as separate steps, and a burst of
    eight threads produced a file whose events were not in sequence order — a chain that
    docket verify then reports as broken, i.e. Docket accusing an honest run of tampering.
    Ledger.append now runs under an instance lock, so the file is always one contiguous chain
    in sequence order. tests/test_ledger_concurrency.py (8 threads × 25 events, statuses and
    chain checked from disk) and tests/test_proxy_concurrency.py (parallel calls through the
    real boundary: sound chain, meter total exactly the sum of the rows, a limiter that admits
    exactly its budget, a breaker that counts each failure once, a burst that cannot walk past a
    cap, and a receipt built from the result that still verifies) cover it.

  • A bad --max-usd is a usage error, and --max-usd nan no longer disables the cap.
    Decimal("NaN") compares false against every threshold, so docket run --max-usd nan
    completed with the cap silently switched off and filed an ordinary-looking receipt reading
    $0.00 of $NaN cap; --max-usd abc printed [<class 'decimal.ConversionSyntax'>] and exited
    1. Caps are now validated wherever they are set — an argparse type= for --max-usd,
    --max-tokens, --max-calls, --max-events, --max-calls-per-minute and --max-minutes
    (exit 2, flag named, value quoted, nothing created) and Caps.set_max_usd() for code, which
    is the one way to change the field after construction. inf is refused as a cap too: an unset
    cap is spelled by omitting the flag, and the receipt discloses it as unset rather than as a
    number.

  • An unexpected exception is reported as a sentence. The CLI's top-level handler printed the
    exception's args raw, which for a decimal error is its class object. The type is always named
    now, and the detail only when there is one.

  • Restoring twice no longer blames the operator. After a successful restore the file holds
    the snapshot's bytes rather than the digest the run ended with, so a second docket restore
    refused it as "changed after the run" — a false accusation, and a command that fails when a
    script, a cron job or a nervous hand repeats it. A path that already holds the snapshot's bytes
    and the mode the plan would apply is now reported already restored: no write, no refusal.
    The check runs after the plan has been validated against the snapshot store, so a doctored
    manifest is still refused on its own merits rather than reported as an idempotent no-op.

  • A relative working directory made every recorded path absolute. fsdiff.snapshot
    relativised paths against an unresolved base, and a relative base can never be a prefix of
    the absolute paths a walk produces — so with cwd="." (what docket game-day and
    docket run --root . pass) every fs.change event carried an absolute path. Nothing
    surfaced: no ignore pattern matched, so Docket's own ledger and snapshot store were recorded
    as the agent's changes, and the confined restore then refused its own change list as
    out-of-tree. The base is resolved now, and tests/test_fsdiff.py asserts the relative and
    absolute forms agree.

  • A rollback that restored nothing is no longer recorded as applied. ops.rollback.applied
    was set for having attempted a restore, so a plan whose every item was refused — see the
    defect above — produced applied: true, an x-docket.rollback-applied event and a console
    line reading "the watched tree has been put back", over a tree that had not been touched.
    applied now means files went back; a declined restore is recorded as
    x-docket.rollback-refused with its reason, named in ops.rollback.reason and
    ops.rollback.refused, and printed by both docket run and docket report.

  • spec/vectors had gone stale. The committed conformance vectors still carried a policy
    document from before the [resilience] defaults existed, so their policy digest — which is
    bound into the first ledger event and therefore into the head, the Merkle root and the receipt
    core digest — no longer agreed with the implementation that is supposed to produce it. They
    are regenerated, and test_committed_vectors_match_the_generator now runs the generator into
    a copy and fails if anything differs, which is the check that would have caught it.

  • explain.total_estimated_savings names an unreadable impact instead of skipping it: those
    strings are written by Docket itself, so an unreadable one is a bug, and a silently smaller
    total in a document people make decisions from is worse than a visible caveat.

  • Verification no longer reports two verdicts under one check name. The Merkle-path verdict
    and the ledger re-binding verdict were both reported as usage-inclusion-proofs, so a
    doctored ledger produced FAIL usage-inclusion-proofs and PASS usage-inclusion-proofs
    a few lines apart. Re-binding is now usage-rows-rebound; a test
    (test_no_check_reports_two_verdicts) keeps one name to one verdict. The Node verifier's
    detail line now distinguishes the two causes as well.

  • docket demo --tamper no longer crashes when the event it picks is not a
    model.response: it edited whichever line happened to be fifth and, for any other event,
    assigned data["model"] = data — a self-referencing dict that made json.dumps raise. It
    now tampers with the first model.response and reports nothing to tamper with if there is
    none.