Skip to content

fix: keep the index dense and hash every other table keyed by numbers through the seed - #308

Merged
AdCodicem merged 64 commits into
mainfrom
ccr-6d6c0ee6-dqm4lm-155
Oct 30, 2026
Merged

AdCodicem merged 64 commits into
mainfrom
ccr-6d6c0ee6-dqm4lm-155

Conversation

@AdCodicem

Copy link
Copy Markdown
Owner

What this changes

Batch 17 of M02's step 4: numbers a file chooses so that they share a hash table's bucket (ADR 52).

  • The index keeps its entries in arrays (PdfXRefTable). Numbers and entries lie in two arrays, in the order the
    table first recorded them, which stays the order it enumerates them in; a number finds its entry in a slot while
    numbers run in order — the slots no longer than twice the entries held or made room for, and 1,024 more —, and past
    them in a map hashed through the process's seed, which gives back its room once the slots have taken most of its
    numbers. 40 bytes an object numbered in order, against 52 to 63: opening 300,000 objects allocates 12.0, 22.5 and
    24.2 MB for a table, a stream and a chain of a thousand sections, against 16.9, 27.4 and 30.6 MB, budgets
    recalibrated.
  • A stale /Size keeps bounding the room until an older section numbers past the newer ones. A /Size the
    sections number past stands for one past the highest number they gave; once an older section, or its /XRefStm
    stream, numbers past the newer ones, it bounds nothing, so that the index doubles rather than growing by one entry at
    each section — which, with arrays sized exactly, copied the whole index each time: 3.6 GB in 3.5 s for a thousand
    sections, found by the first review round.
  • Every other table keyed by numbers hashes them through the process's seed (SeededComparer). The reader's cache,
    its object streams, its sets and memos keyed by numbers or offsets, the copies it marks and restores, and the
    validator's walks: System.HashCode, drawn once a process as .NET does for strings, both halves of an offset mixed
    in apart.
  • PdfObjectId and PdfDiagnostic hash through the seed. A record struct's own hash is a fixed linear combination
    a file inverts; equality stays the record's, and a hash now differs from one process to the next. No signature
    changes.
  • Two tests read the code (SeededComparerTests). One reads the compiled library's IL and refuses every hash
    collection over a key a file can steer — a number wider than a byte, a tuple or a pair, a struct keeping .NET's
    default hash, a record's compiler-written hash, a key typed by any interface — unless the comparer of its key, taken
    last, is SeededComparer.Instance handed straight; and always a table keyed by any object, a collection over a type
    parameter, or one built for a type argument over such a key, and every collection that enumerates by hash — concurrent, frozen, immutable, a StringDictionary —, whatever its key
    and comparer, but PdfName's interning table, built once and called on only to find or add a name, so that no order a
    seed sets reaches an output (invariant 6). It takes out the probes code coverage inserts, by their exact shape. The
    other reads the sources and names the file and line.
  • The shapes measured are held by ReaderTimingTests, each against the same file numbered in order, and the stale
    /Size's room by DocumentReaderTests on three chains; IndexBenchmarks gains a table of numbers three apart.
  • Mutation testing becomes a finite step of every pull request that changes src/ (ADR 53, a line in CLAUDE.md).
    This batch's review swept its guard test with mutants from its twenty-fifth round on, each sweep's catalog finer than
    the last and each survivor counting as a finding, so that no round came back without one. The maintainer bounded it
    on 2026-10-30, then made the rule: a fixed, first-order catalog over the lines of src/ a pull request changes, and
    in a test that reads the library's code over the conditions it adds or changes; a survivor killed by a test, classed
    equivalent or, in such a test, widening, each with its reason, or filed as debt, and never a review finding.

Why

Closes #155

Measured on main at e3e2034, each shape a single cold run against the same file numbered in order: 34,167 one-row
subsections opened in 1.9 s against 0.04 s and validated in 4.0 s against 0.09 s; 36,350 objects and no table rebuilt
in 8.0 s against 0.07 s; a million references to those numbers resolved in 56 s against 0.73 s. No guard bounds it.
Four ways were prototyped and measured; the maintainer settled the four questions as recommended on 2026-10-28: the
index dense and a seeded hash elsewhere, the process's seed, PdfObjectId and PdfDiagnostic seeded, and a test that
reads the code. 32 adversarial review rounds, 286 findings, each put to a refuter; the first round's three
majors fixed with a test that failed before its fix, none after. The last eight rounds swept the guard test with
3,676 mutants, until ADR 53 made the survivors the mutation step's to settle rather than the review's. The journal of
2026-10-28 in docs/status.md lists them.

Filed under M23: #305, the seeded hash the validator's walks pay (see below); #306, the source reading's mutation
survivors, shapes it misses that the compiled reading refuses. Filed under M02: #307, the test of a disposed document's memory, which
failed once in CI's compat (.NET 11) job on d7d3697 (run 836), a commit that changes only SeededComparerTests,
the process letting go of 92 MB between the test's first two measures; run 837, on the next commit, passed it.

Definition of done

The project's definition of done applies to every change:

  • Unit tests cover the behavior, including its degenerate and hostile cases — 230 new, 5,343 to 5,573 (7,715 to 7,945 with the remote documents)
  • Anything an independent tool should confirm is covered by the integration suite — 3,688 against qpdf, 3,682 passed and 6 skipped, twice, on the library code this ends with
  • The build has no new warnings, and any suppression is local and justified
  • docs/status.md records the batch: the journal of 2026-10-28 and the figures at a glance
  • Public API carries XML documentation, and the site under docs/website matches what now exists — 266 pages built, the site's check clean
  • dotnet format has been run

Anything reviewers should look at closely

  • What the validator pays. Under a medium job, twice, main and the branch alternated, the rules over 1,000 pages
    already read take 2.9 to 3.2 ms against 2.6 to 2.7, about 15 % more, where the prototype the decision rested on had
    measured 5 to 8 %
    ; opening and validating 7.0 to 7.3 ms against 6.7 to 6.8, about 5 % more, the lighter index paying
    part of it back; the Arlington walk alone 2 to 6 % more; allocation the same. The cost is the seeded hash in the
    validator's thirty sets and maps keyed by object number. The validator's walks pay a seeded hash for every object number they meet: the rules take about 15 % longer since #155 #305 files it under M23, with a set indexed by the index's
    slots as the way to measure first.
  • What opening gains. Indexing 1,000 pages 200 to 222 µs and 83 KB against 230 to 242 µs and 123 KB; an index of
    300,000 objects 8 to 13 % faster and 4 to 6 MB lighter; numbers spread far apart, which the map serves, keep 60 to 110
    bytes an object (concepts/lazy-reading.md gives the figures by shape).
  • CI was red from 32849ec to 3109cb5 (runs 813 to 819): under coverage, as CI runs the unit suite, the
    instrumentation puts a probe between the comparer's getter and the table's constructor, which the reading of the
    compiled library took for code the source wrote. 3abfe27 reads through the probes, with a test that emits a probed
    method and a truly wedged one; run 820 and the runs after it are green. 8de9e16 fails dotnet format too, an
    endless loop spaced otherwise than the formatter writes it, which dbee093 corrects. CI ran on 49 of the 64 commits
    as each was pushed; no commit was checked alone otherwise, a check the maintainer dropped on 2026-10-29 since batches
    are squashed. A rebase merge would leave those commits failing; a squash would not.
  • Coverage: all 191 measurable lines of the patch; src/ 99.43 % (8,224 of 8,271) on the committed corpus.
  • Checked:
    • integration twice on 31d0870, whose library code the branch ends with (later commits change comments and tests);
    • the 401 corpus documents on main and the branch, twice each: the same 2,475 lines on all four runs;
    • mutation: the stale /Size flag's assignments and every condition of the compiled reading, each failing a test
      written for it but those judged equivalent; eight sweeps of the compiled reading, 3,676 mutants, every survivor C#
      reaches failing a sample written for it, the others classed equivalent or widening with their reasons; the source
      reading's survivors are held by samples or filed as The source reading of tables keyed by numbers holds some of its own conditions with no sample #306;
    • dotnet format; Remote corpus runs 87, 89, 90 and 93, whole at their first attempt, runs 90 and 93 on the
      library code this ends with.

Merge: squash recommended, as a fix: (no public signature changes; the hashes of PdfObjectId and
PdfDiagnostic change value, which a caller must not have stored).


Generated by Claude Code

claude added 30 commits October 28, 2026 06:41
A record struct's own hash is a fixed sum of its fields: PdfObjectId's is
Number * -1521134295 + Generation, which a file inverts to put identifiers in
one bucket of any table keyed by them, or to give two of them the same hash
outright; PdfDiagnostic's adds the position last, so that reports alike but
for where they lie share a bucket when placed a table's size apart.

Both types now hash through System.HashCode, which .NET seeds once a process
as it does strings, the position's two halves combined. Equality is the
record's, unchanged; a hash differs from one process to the next.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The index was a Dictionary<int, XRefEntry>. An int is its own hash, and .NET
picks a key's bucket modulo a prime from a public list, so object numbers a
multiple of that prime apart all fall in one bucket: 34,167 one-row
subsections took 1.9 s to open against 9 ms numbered in order, and 36,350
objects with no table 8.2 s to rebuild against 65 ms.

The entries now lie in two arrays, in the order the table first recorded
their numbers, which stays the order it enumerates them in. A number finds its
entry in a slot while numbers run in order, the slots stretching only while
they stay within twice the entries held or made room for, and past them
through a map hashed through the process's seed (SeededComparer). An entry
takes 40 bytes against 52 to 63: opening 300,000 objects allocates 15.0 MB
against 16.9 for a table and 25.5 against 29.5 for a stream, budgets
recalibrated. IndexBenchmarks gains a table of numbers three apart, which the
map serves.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
Past the index, the reader's object cache, its object streams and its sets
of objects loading, cut, unproduced or reported, its memos keyed by offsets,
and the validator's walks of the page tree, the reachable objects, the object
streams' dependencies and the Arlington model were keyed by numbers hashed as
themselves: 200,000 references to 20,000 objects a cache's size apart took
14.6 s to resolve against 88 ms, and 100,000 references to 30,000 free
numbers chosen alike 6.3 s to validate against 96 ms.

SeededComparer now also compares offsets, both halves of a long mixed in
apart, since a long hashes as its halves exclusive-ored, and the four tuple
shapes the reader keys by. Every one of those tables takes it, the copies the
reader marks and restores included.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A test reads the sources and refuses any Dictionary or HashSet keyed by an
int, a long or a tuple holding one that is built without SeededComparer: a
declaration given [] or new(), a new without it, a copy without it, a name
declared with such a type and given [] or new() later, and any LINQ operator
that hashes. A second opens, rebuilds and validates a document and checks the
comparer of every such table the reader holds, and of the tables they hold.
Planting a set built with [] or new HashSet<long>() fails both, naming the
file and line.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
ADR 52 records the maintainer's four decisions: the index kept dense,
every other table keyed by numbers hashed through the process's seed, the
public PdfObjectId and PdfDiagnostic hashed through it too, and a test that
reads the sources; with the measurements, the alternatives measured and
rejected, and what would reopen it.

The threat model's known gap becomes a section of its own among the shared
defenses, and a row in the object syntax, indexing, resolving and validator
tables, each naming its tests. The lazy-reading concept and the reader limits
reference give the index's new cost: 40 bytes an object, about 80 at most
when a rebuild grew it, 335 MB at the default bound against 436.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
ReserveIndex capped the room at one past the highest number read when the
newest /Size was stale. A chain whose older sections each number one past the
highest read so far then made room for one entry more at each section, and
the index, now arrays sized exactly, was copied whole each time: a newest
section of 100,000 objects under a /Size of 1 and a thousand older one-row
sections allocated 3.6 GB in 3.5 s. The map it replaced grew by .NET's primes,
about 1.2 times a step, and hid it.

A size the sections have read past is stale and bounds nothing, as if absent:
the room doubles, 12.5 MB for the same file.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The slots doubled from one up to the first power of two past the highest
number, even where a section had made room for every row: an index numbered
in order kept 40 to 44 bytes an object and allocated up to 52. They now
stretch at once to the room made, still within twice it and the slack: 40
bytes an object, allocated and kept. Opening 300,000 objects allocates 12.0 MB
for a classic table and 22.5 MB for a cross-reference stream, against 16.9 and
29.5 with the map it replaced; budgets recalibrated.

A number the slots come to reach leaves the map in place, rather than the map
being copied whole whenever one does. A copy's slots reach the highest number
they hold and no further. XRefTableTests now check where a number lives, the
slots made once, and the enumeration through every interface.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…omparer

The reading of the sources let a table through when another argument of the
same statement named SeededComparer, and never saw one whose type the source
does not write: reverting the set an out var receives in NullWhileDecoding,
or a copy in MarkReports, failed no test. The runtime check it leaned on
opened a sound classic file and saw twenty fields.

A test now reads the compiled library's IL: every Dictionary, HashSet,
ConcurrentDictionary, OrderedDictionary or Hashtable over a key a file can
steer, an integer, a tuple holding one or a struct hashed as the compiler
writes a record struct's hash, must be built with SeededComparer.Instance as
its comparer, and no LINQ, frozen or immutable operator may hash. Both reverts
fail it; samples hold what it refuses. The source reading, kept for the file
and line it names, looks for the comparer among a constructor's own arguments,
and knows more table types and hashing operators.

PdfDiagnostic's position halves, the tuple comparisons whose first items
differ, and an open timed against the seeded map itself gain tests; the
timing tests run both files in turn until neither gets faster.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…e code hold

The public documentation of MaxIndexEntries, the architecture and the
threat model's row on declared counts still gave the hash table's 52 to 108
bytes an object; lazy-reading's measured section still gave its figures and
budgets. The index now costs 40 bytes an object numbered in order, allocated
and kept, about 80 when a rebuild grew it and about 96 for numbers spread far
apart; 12.0, 22.5 and 24.2 MB to open the generated indexes, held to 12.6,
23.6 and 25.5 MB.

ADR 52 and the threat model say which shapes the timing tests hold, that a
number in the map costs more than it did, that a stale /Size bounds nothing,
that tables enumerate in an order their adds and removes set, and what the
two tests that read the code refuse. A record struct's hash is a fixed linear
combination of its fields, not a sum.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…he rest

Since the last fix, a /Size the sections had read past bounded nothing as
soon as the newest section numbered past it. An update chain that kept its
original /Size, and whose newest sections number the highest objects, then
doubled the index past its count: three sections of 60,000 rows made room
for 240,008 entries, 520 of 300 for 309,248, where one past the highest
number read had landed on 180,000 and 156,000 exactly.

One past the highest number read stands for a stale size again, until an
older section numbers past the newer ones: from then on it bounds nothing,
so that a chain whose older sections each number one further still doubles
the index rather than copying it at each.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
Numbers the slots come to reach leave the map in place, which kept the
room it grew to for the life of the document whenever one number stayed
past the slots: 3.1 MB for one far number beside a chain of a thousand
sections. Once the numbers that stay are fewer than a quarter of its room,
the map is trimmed to them, one copy of those that stay; a stretch that
moves a few numbers out of many leaves it as it is.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…parer

The compiled reading let through a comparer handed on one path only, as a
ternary or a ?? gives it, any comparer an operator was given, the frozen
operators whose comparer is optional, immutable and frozen factories and
their Empty, a table over a type parameter, a base constructor, and keys
that hash as numbers it did not list: nullable numbers, System.Tuple,
char, floating point numbers, record classes. No test held the condition
that tells the seeded comparer from any other.

A hash collection over a steerable key, built by a constructor, a base
constructor, a LINQ operator, a factory, Empty or Activator, now passes
only when the comparer it takes last is SeededComparer.Instance, the
instruction before it the property's getter and itself no branch's target;
over a type parameter, when it takes a comparer at all. Samples hold each
shape refused and each let be.

The source reading blanks literals as it blanks comments, closes an
assignment at its matching bracket, reads an initializer as a new's
arguments, knows more numeric keys, and asks a hashing operator to name a
comparer rather than refusing it whatever it is given.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The second shape measured as this work began, the 34,167 one-row
subsections validated once open, was measured but held by no test: 4.2 s
against 2.3 ms on main. It now validates in about the time of the same rows
numbered in order.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
ADR 34 still priced the index at the hash table's 52 to 108 bytes an
object. The figures quoted for main predated the last batch's stream
changes: a cross-reference stream allocated 27.4 MB there, not 29.5, and
the grown table 32.6 MB. The threat model's 40 bytes a number the guard
lets the index take are 44 where one number lies twice the room away. A
number in the map costs 20 to 35 bytes more than a slot, not twice one, and
the slots follow the newest /Size's last step, up to 160 bytes an object.

ADR 52 and the threat model say what the code readings now refuse, how a
stale /Size bounds the room, and which shapes the timing tests hold.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
No test told the flag that stops a stale /Size from bounding the room from
one recomputed at each section, nor from one an older section's /XRefStm
stream never sets: a chain whose older sections alternate between one row
past the highest read and none, or whose streams number past it while
their tables number below, made room one entry at a time again, 1.8 GB and
3.6 GB, and every test passed. Both chains now open within 5 % of the
12.6 MB and 14.3 MB they allocate.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The compiled reading let through a hashing operator taken as a method
group, an immutable dictionary handed a seeded value comparer and the
default key comparer, a table over a type parameter handed null or the
default comparer, a comparer of a number handed to the library's own code,
a table type given to its generic code, and anonymous keys; and took the
lookups of an immutable dictionary's factory class for builders, and a
frozen set's empty one for a table to seed.

A method group now takes no comparer; the comparer an operator or factory
takes last must be its key's, as declared; over a type parameter it must
be one the method was given, an argument or a field; a call into the
library's own code is read as any table is; anonymous types hash as
tuples do. Each shape has its sample, refused or let be, and each
condition a sample that fails without it.

The source reading reads an interpolated string's holes as code, a
verbatim string before a raw one, an apostrophe that closes nothing as no
character literal, a tuple element's type rather than its name, and lets
string.Join, Path.Join and arrays of tables be.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A number in the index's map keeps 20 to about 42 bytes beside its entry
and slot, by where the map's count falls against .NET's prime steps, not
20 to 35: an object numbered apart keeps about 60 to 85 bytes in
subsections of ten rows or more, 90 to 115 in one-row subsections, and a
rebuild over such numbers allocates about 125 to 220, not 155 to 190.

ADR 52 and the threat model say what the compiled reading now refuses,
that ADR 52's first measurements are single cold runs, and ReserveIndex
how a hybrid file's newest stream and a size below zero are bounded.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…nothing

The flag's stickiness was held on the classic line only: recomputed at each
/XRefStm stream, it was cleared by a stream holding no rows behind a table
numbering one past the highest read, and the next table's room was bounded
again, one entry at a time, 3.6 GB. A third chain, older tables numbering
past and their streams numbering nothing, opens within 5 % of its
14.1 MB.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…ilds

Reading a comparer handed to generic code through to its callers never
closed: this, a field set to the default comparer, a value comparer of the
key's type, a comparer in a closure all passed, and a table type reached
new() through a nested type argument, Lazy or LazyInitializer unread. The
library builds no hash collection over a type parameter, so the reading
now refuses any, whatever its comparer, and refuses a table over a
steerable key built for a type argument by new(), Activator, Lazy or
LazyInitializer without a factory, nested or not. A library call that
hands a comparer is no longer read apart: what it builds is.

A value comparer of the key's type is no longer taken for the key's, a
builder factory and a nullable tuple key are held by samples, and every
condition of the reading by a sample that fails without it.

The source reading reads a nullable tuple key as a tuple, a character
literal up to '\U0000FFFF', an interpolation's format clause as text and
an array's initializer as the tables it builds, and lets only string.Join
and Path.Join be, qualified or not.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…osts

ADR 52 and the threat model say what the compiled reading refuses since
the last change: any hash collection over a type parameter, a table built
for a type argument by new(), Activator, Lazy or LazyInitializer, and an
immutable collection's Empty, not a frozen one's. A rebuild over numbers
spread far apart allocates about 110 to 220 bytes an object, not 125 to
220; one-row subsections keep 85 to 115, the more the wider the numbers;
and lazy-reading gives the kept figures by shape rather than a flat 96.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
ADR 52 gave the prototype's 5 to 8 % for validating 1,000 pages. Measured
on the batch's last code against main, a medium job of each twice,
alternated: the rules alone over a document already read take 2.9 to 3.2
ms against 2.6 to 2.7, about 13 % more; opening and validating it 7.0 to
7.3 ms against 6.7 to 6.8, about 5 % more, the lighter index paying back
part of it. Opening an index of 300,000 objects takes 8 to 13 % less, and
lazy-reading's BenchmarkDotNet rows give what indexing 1,000 pages costs
now: 200 to 222 µs and 83 KB, 3.7 ms and 1.7 MB to read every page.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The source reading took a whole array initializer, to its statement's end,
for one table's arguments: one seeded element, or a comparer another
argument of the call took, cleared every other. Each element an
initializer builds target-typed is now read with its own arguments. The
compiled reading refused Lazy's value constructor, which builds nothing:
the value it is handed is read where it is built. A guard that changed no
outcome is gone.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The introduction and the features page still gave 229 µs and 393 KB to
index a thousand pages and 6.2 ms and 5.9 MB to read them, figures older
than the last batches: about 210 µs and 83 KB, and 3.7 ms and 1.7 MB, now,
the features page pointing to the measured table. ADR 52's validation
figure is about 15 % by its own medium runs, and one-row subsections of
spread numbers keep 80 to 110 bytes an object.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
Under coverage, the instrumentation writes a probe at each sequence point, between the comparer's
getter and the table's constructor, so the reading of the compiled library took the seeded comparer
as wedged there and refused a table that hands it straight. The reading now drops the probes before
it judges, and a branch that reached into a probe lands on the instruction the probe guarded. A test
emits a probed method and a truly wedged one, and only the second is refused.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…t builds them

The source reading split an initializer's elements counting angle brackets, which a comparison, a
shift or a lambda's arrow inside an element threw off: an array of seeded tables was refused, and an
unseeded new() after an element holding an operator let be. It read only an element that starts
with new( or a collection expression, so a table a conditional, a coalescing operator, parentheses
or a multidimensional array's row built was let be, and it passed over an array of nullable tables
and a declared array given a collection expression, reading their whole statement as one table.
It now splits at the commas no parenthesis, bracket or brace holds, reads each target-typed new( an
element starts with or a conditional or coalescing operator gives, and an element that is a
collection expression, through a row, for a new array, a nullable one, and a declared one alike.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
… or not

ADR 52 and the threat model say that no table that enumerates by hash, concurrent, frozen or
immutable, is keyed by numbers, so that the seed, which differs from one process to the next,
orders nothing the library reports or writes. It held, read in the code, but no test held it: the
compiled reading let such a table be when it was seeded, and samples asserted so.
A concurrent dictionary, or a frozen or immutable hash collection, its factories and its builder
included, over a key a file can steer is now refused whatever its comparer, by both readings; the
seeded samples of those shapes move to the refused ones. A factory is refused before its comparer
is read, so the reading no longer asks which comparer a factory takes last.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
… it is one

M23 still quoted the budgets and figures of the hash table the index was: 17.7, 31.0 and 32.1 MB,
16.9, 29.5 and 30.6 measured, where the tests now hold 12.6, 23.6 and 25.5 MB and measure 12.0, 22.5
and 24.2. Lazy-reading dated the chain's grown figure, 33.2 MB, to the day of the others, where it
was measured on 2026-10-14. Five comments still called the whole index a map that doubles or
resizes, the word now kept for the seeded map past the slots.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
MapCapacity was read only on tables that had made a map, so its answer for one that never did, the
common case, went untested: the one line of the batch's patch that coverage left partial.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
ADR 52 measured the rules about 15 % slower, against the prototype's 5 to 8 %, and left it there.
The debt filed under M23 says where the cost lies, the validator's walks keyed by object number,
and what to measure first: a set indexed by the index's slots.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…les declared first

The source reading stopped at the ? of a nullable array of tables given a collection expression,
and let its unseeded tables be; read a jagged array's row as one table, refusing an empty row and
letting a seeded table beside an unseeded one hide it; and refused a concurrent table seeded or not
only where a new built it inline, not where a name declared first was given one later. It now
passes over a nullable annotation after each rank, reads a row as a row while ranks remain, and
holds a name declared with a concurrent, frozen or immutable type to the same rule.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
claude added 27 commits October 28, 2026 21:35
The loop stand-in had been changed to build its table at the loop head, which
holds a rule weakened to a strict comparison, but no longer a branch landing
ahead of the construction, on the comparer it takes: a rule weakened to "not
itself a branch target" passed every test. A second stand-in keeps that shape,
each failing the mutant the other lets through.

Wrapper's summary, which the last change left above the interface inserted
before it, is back on Wrapper; two test comments still said "before any branch"
and "a member that hands it out is refused", where the rule says ahead of every
branch target and a call to such a member.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…amples

The reading knew the LINQ operators that hash by name, but not .NET 10's
asynchronous ToHashSetAsync, ToDictionaryAsync and ToLookupAsync: a set of
numbers built through one hashed them as themselves. Both readings know them
now; their cancellation token follows the comparer, so one is refused even
seeded, a shape the library does not use.

Four conditions held by no sample: a key typed ValueType; a lookup let be only
where declared on the table or a view of it, not a member named like one that
hands it out; the table built by the static constructor, not an instance one
that rebuilds it at each instance; and a branch that lands on a coverage probe
moved to what follows it, which keeps a comparer handed on one path refused.
Each has a sample now, whose mutant fails.

ADR 52 said the source reading names an operator over a numeric key only,
where it names one whatever the key; two comments gave a tuple's hash as the
compiler's and the reaching rule as refusing members rather than calls and
static fields.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
… a sample

A mutation of each condition and set entry of the compiled reading found
these held by no sample: twelve of the sixteen number types and fifteen of the
eighteen hashing operators, any of which could be dropped from its set; a key
of an anonymous type in a table that does not enumerate by hash; a tuple that
holds a number beside another item; a comparer handed as an object to a method
whose new() builds the table; a long backward branch and a switch closing the
loop that builds the interning table; and a probe taking out the instruction
that follows it. Each has a sample now, and its mutant fails.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…s it

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…nditions

A Rune, an NFloat and a Complex hash as the number they hold, a code point
or a floating point value, yet neither reading counted them among the numbers:
a table keyed by code points from a file's text would have been let be. Both
readings count them now.

A mutation sweep of the compiled reading found conditions held by no sample:
the reaching rule never met a field load, a field address or a non-virtual
method group, nor the table two levels deep, a constructor of its pairs or a
lookup on a container of it; the table rule never met a virtual call building
a table for its type argument; two asynchronous operators had no sample; and a
switch closing a loop at the construction itself. Each has a sample now, whose
mutant fails.

A sweep of the source reading found 198 surviving mutants. Every number name,
every hashing operator, a comparer other than the seeded one, the four table
types no sample held, the line the reading names and fifteen realistic shapes
are held now; the rest, each a shape the compiled reading refuses, is filed
under M23. Three comments described rules an earlier change had altered.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…steer

.NET hashes a struct that keeps the default hash and holds a reference, a
floating point number or padding by its first field alone: two thousand
KeyValuePair<long, int> sharing their key took one hash, and a set of twenty
thousand took 25 s against 6 ms. Neither reading counted such a key, a pair
among them, so a set of them was let be. The compiled reading counts every
struct that keeps the default hash, the source reading a KeyValuePair as it
does a tuple, with samples; none in the library is refused.

A comparer named like the seeded one but not it had no sample on a table a new
builds; two comments named only calls and method groups where the reading
refuses static fields of a type built over the interning table's pairs; two
summaries said more than the reading holds; ADR 52 said every numeric key
where it meant every key it lists.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…ace key

A struct that hides GetHashCode with new was read as hashing itself, though a
table calls the virtual slot, still ValueType's, and hashes it by its first
field: the reading now follows the slot. A key typed as an interface was
counted only where a number implements it, though a boxed tuple, record or
struct keeping the default hash may stand behind any interface; every
interface key is counted now, and none in the library is refused.

No pair sample was free of numbers, so a reading that counted a pair only for
a number it holds passed every test; a pair of strings is a sample now. The
source reading counts a pair whatever it holds, as it hashes by its first
field, and reads a key written with global:: or as ValueTuple.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…can steer

A class deriving from a tuple hashes as the tuple it derives from, so the
reading of the compiled library now reads the tuple clause on the type
whose hash a table calls, not only on the key's own definition. A record
that is not sealed stands for every record derived from it, whose hash the
compiler writes, so it counts as steerable whatever its own hash; a sealed
record hashed by hand is let be. The source reading recognizes a number or
a tuple written after global::.

Samples hold each condition, and the documents name both keys, a table
built for a type argument refused only over such a key, a type parameter,
or one that enumerates by hash.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…its own

Both samples of a type deriving from a tuple derived from it directly, so a
reading that took the key's base type for the type whose hash a table calls
passed every test; a key deriving from a tuple of a string and a number
through a class of its own is a sample now. A sealed record hashed by hand
was read as hashed by the compiler when it derived from a record so hashed,
since the attribute the compiler puts on a hash was looked for through the
base types too; it is looked for on the hash itself, with a sample.

The source reading recognizes a number written after its own namespace,
System.Numerics or System.Text, and a Nullable written out, each with a
sample. The documents say a type deriving from a tuple counts when that
tuple holds a number a file steers, and two summaries say which tables the
reading gives no key: a non-generic hash table, a StringDictionary among
them.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…erializers

The compiled reading knew Activator, Lazy and LazyInitializer as builders of
a type argument without a factory, but not Lazy<T, TMetadata>, whose
constructors taking only metadata go on to Lazy<T>'s, nor a
ConditionalWeakTable's GetOrCreateValue. A sweep of .NET 10's shared
framework, reading every method body, found two more that may build a table:
a BindingList, for AddNew, and Marshal.PtrToStructure. Each is refused now
over a table a file can steer or that enumerates by hash; a BindingList where
it is built too, since its non-generic interface reaches AddNew out of sight.
Lazy's constructors are read on their definition, so that metadata of the
value's own type is not taken for a value handed.

Such a builder over a type parameter with no new() constraint is refused
too: what its callers give it is not read. A generic JSON deserializer of
System.Text.Json or System.Net.Http.Json is refused whatever its type
argument, since it builds every table that type holds with .NET's default
comparer, which no option changes. The library uses none of these; samples
hold each condition, and the documents list the builders.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A store alone into a static field of a generic type runs its static
initializer for the type arguments it names, which may build one through
new(); the reading read loads of a static field and their addresses, not
stores, so a table a type parameter with new() stood for could be built
unread. Stores are read now, with a sample.

A Lazy or a BindingList that Activator, new(), LazyInitializer, an outer
Lazy or a ConditionalWeakTable builds builds its value, or an item for
AddNew, without a factory: such a builder over a Lazy or a BindingList of
a table counts as building that table. Only Lazy's sample held a builder
over a type parameter with no new() constraint; Activator's,
LazyInitializer's, Marshal's, the ConditionalWeakTable's and the
BindingList's have one each too.

The documents said a JSON deserializer builds its tables with .NET's
default comparer whatever its options; a contract resolver, a converter or
populating a table the type holds can choose another, out of the reading's
sight, and the library reads no JSON, which is why every such call is
refused. A comment left out that only a type parameter with no new()
constraint is refused at the builder.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A builder over a type parameter with the new() constraint was let be, its
callers read where they give it a type. They are not all read: a static
abstract member called on another type parameter, a constrained call on a
default struct, and a default interface implementation each reached a
new() that built a table of numbers with .NET's default comparer, no test
failing. The maintainer chose to close the whole family: the library
builds no type parameter it is given, whatever its constraints, and
generic code takes a factory instead. The new() samples that were let be
are refused now, and a Lazy over a type parameter that Activator builds
has a sample of its own.

Three summaries are brought in line: the reading's own summary names the
Lazy or the BindingList a builder builds, the library test gives the
whole JSON rationale, and the instructions considered say why each is.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…tself

A DictionaryBase keeps a Hashtable keyed by any object, which it
enumerates by hash, and a type derived from it builds that table without
naming a table constructor: it is among the non-generic tables now, with a
sample. A non-generic hash table a method returns under its own type was
counted only where CollectionsUtil built it, though framework getters such
as a TraceSource's Attributes or a DataTable's ExtendedProperties build and
return a StringDictionary or a Hashtable alike: any such method is
counted now, with a sample.

The documents say that an UnsafeAccessor, whose body the runtime writes,
is reflection and not read, and that a new() constraint met at a call site
is read for a table type it is given, a type parameter handed to it being
held where the library's own new() builds it.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…ollection

The clause that counts a non-generic hash table a method returns under its
own type had samples returning a Hashtable or a StringDictionary exactly,
so a reading that matched the exact type alone passed every test, though a
DataTable's ExtendedProperties returns a PropertyCollection derived from a
Hashtable: that getter is a sample now.

An XmlSchemaCollection keeps its schemas in a Hashtable keyed by their
namespaces, which it enumerates by hash, so the process's seed orders it;
obsolete, it is named among the non-generic tables, with a sample, its
warning suppressed where it is named. The instructions considered say that
a virtual method group is read for such a table too.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A sweep of 243 mutants over the compiled reading left conditions no sample
held, beside survivors equivalent by construction:

- a builder over a non-generic table that does not hand it back as one, a
  BindingList of Hashtables;
- a builder over a table keyed by its first type argument, a dictionary of
  numbers to strings that Activator builds;
- a tuple whose number is not its first item, a type deriving from a tuple
  whose number is, and a tuple of three;
- a key typed by a generic interface;
- the deserializers whose name is not Deserialize, GetFromJsonAsync or
  ReadFromJsonAsync;
- LazyInitializer's guarded overload and Lazy's constructor that takes a
  flag;
- a new() constraint on a second, or a first of two, type parameters;
- a virtual getter that returns a Hashtable.

Each has a sample now, whose mutant fails. Two lists of the collections
that enumerate by hash name the XmlSchemaCollection too.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A second sweep, 325 mutants over the compiled reading and the code added
since the first, left these conditions held by no sample:

- the value a Lazy<T, TMetadata> builds, read from its first type
  argument, whose one sample had metadata of the value's own type;
- a key that holds a type parameter without being one, in a table a
  builder builds;
- a hashing operator of System.Linq beside Enumerable's, a parallel one;
- the interning table enumerated through ToArray, Values or CopyTo;
- a getter returning an XmlSchemaCollection;
- a local an instruction addresses by two bytes, whose high byte read as
  an instruction would hide the table built after it.

Each has a sample now, whose mutant fails. An anonymous type is known by
the name the compiler gives it rather than by an attribute a source can
put on a type of its own. The documents say a parallel query is not read,
the library running none, and that a pair hashes by its first field only
when it holds a reference, a floating point number or padding.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…store

A third mutation sweep left three conditions of the compiled reading that no test held:
an eight-byte floating-point constant read as eight bytes, an anonymous key whose
steerable member is not its first, and an interning table stored through an instance
field rather than a static one. Each now has a sample the reading must find.

The threat model and the decision record now say what the reading holds of a parallel
query: its key and comparer, as for Enumerable, but not the order an unordered one gives.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
… constant

A fourth mutation sweep of the compiled reading, 415 mutants, left conditions no sample
held: the key a LINQ operator is read by, taken by name rather than by place; a tuple's
items read beyond one level, a nullable number among them; reference tuples of more than
two items; a Lazy a ConditionalWeakTable, a BindingList or another builder builds two
levels down; a four-byte floating-point constant read as four bytes; and the interning
table handed on beside a static field it loads rather than stored into one. Each now has
a sample the reading must refuse or find.

The threat model says the Hashtable family is refused when a method returns one under
its own type, as the decision record does, not however it is returned.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A fifth mutation sweep, 521 mutants, left conditions no sample held: a type parameter
two levels below the key; a tuple's or a nullable's item steerable as a nested tuple,
an object, a struct or a record; an immutable dictionary's Empty and its builder; an
OrderedDictionary, or a non-generic table other than a Hashtable, built by a builder;
the ToDictionary of an immutable array, which another System.Linq class declares; a
join's key whose outer element is not a number; and a loop that only the last case of
a switch's jump table closes. Each now has a sample the reading must refuse or find.

The guard test says, as the decision record does, that the library builds no hash
collection over a type parameter, not no collection.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
A sixth mutation sweep applied a closed catalog of operators to every site of the
compiled reading, 492 new mutants, and left combinations no sample held: each kind of
steerable key reached through a builder or as a tuple's or a derived tuple's item; each
kind of table a framework builder, a new() constraint or a new() type may build; a
non-generic table a method returns and hands on without a call; a loop that only the
first case of a switch's jump table closes; and the interning table in the wrappers the
rule names, at each place a member names it. Each now has a sample the reading must
refuse or find, 70 in all.

The comments, the decision record and the threat model now say that a key holding a type
parameter at any depth is refused, that a key typed ValueType is, not any value type, and
which five lookups on the interning table are let be.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…lpers

A seventh mutation sweep specialized every helper of the compiled reading per call site,
642 mutants, and left what no sample held: each number type as a builder's key, a
nullable's value, a tuple's item and a derived tuple's item; a framework type derived
from a non-generic table built by a builder; a seeded FrozenDictionary factory and an
ImmutableDictionary factory over a key no file steers; a view of the interning table
stored into a static field; a key deriving from a tuple of three items or hiding its
hash with new; and a comparer handed to another call beside a table built without one.
A new test counts the nineteen numbers at each of the four places.

The decision record and the threat model name the five lookups let be on the interning
table, whatever they are handed.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
The samples hold each condition of the reading, each arm of a helper and each element of
a set it reads, dropped, negated or narrowed at one place it is read; a change along one
call path and not another, or to two conditions at once, is outside that bound, which the
maintainer set on 2026-10-30 after seven mutation sweeps each went one level deeper.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…ge makes

Mutation testing becomes one finite step of a pull request's verification,
apart from its review. A fixed, first-order catalog runs once over the lines
of src/ the pull request changes, and over the conditions it adds to or
changes in a test that reads the library's code. A helper is mutated once,
in its body; no mutant changes two things at once. A survivor is a gap in
the tests, killed by a test or classed equivalent with its reason, never
reopened, and never a review finding. Only survivors are run again, and the
tests written to kill them are not mutated in their turn.

The record keeps why: batch 17's review swept its guard with a catalog
refined every round, each survivor counted as a minor finding, and the code
mutated grew with each fix, so the review had no fixed point. CLAUDE.md
states the rule among its conventions.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…hand-hashed record

The thirty-second round reran the thirty-first's survivors, the fixed ones
all failing a test and the settled ones all surviving, then swept the
reading again within the bound: 687 mutants, 545 failing a test. Of the 142
survivors, 78 are equivalent, the same as one settled, or only widen what is
refused; 64 are not, and samples now fail each of them:

- the record arm narrowed to concrete records: an abstract record hashed by
  hand, its sealed child keyed through it;
- the generic arms of Mentions narrowed to static methods, and existences
  over type arguments or parameters narrowed to their first or last item;
- each arm of Steerable, Holds' element arm, BuildsTable's type parameter,
  CreatesTable's key test and Constructed's flag narrowed at one place it
  is read: comparable interfaces, key-value pairs, abstract records, record
  structs and reference tuples as keys, items and children; a class with a
  new() constraint; tables a generic class builds over its own parameter.

The sample for a key holding a type parameter is keyed by (T, string),
which a type argument can make steerable. The ref-returning view of the
interning table is held at its return place too. The bound the main test
states now sets aside the mutants that only widen what is refused.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…neric interface too

ADR 52 and the threat model said a GetOrAdd factory's argument reaches the
factory as an object only, which the end of the same sentence contradicts:
the table also reaches it typed as a non-generic interface, and neither is
followed by the reading.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
… run it

The maintainer settled what the first text of the rule left open. Every
pull request that changes src/ runs the mutation step. A test that reads
the library's code is mutated on the conditions the pull request adds to
it or changes in it, a new one whole once. A survivor ends killed, classed
equivalent, classed widening when it only makes such a test refuse more,
or filed as debt when only an input no test can build tells it apart. No
condition is narrowed to a sub-case of it.

An independent reading of the record asked for the rest: the review meant
is the pull request's own, not the milestone's; the history since batch 5
says what was run and how each survivor ended, the debt it filed included;
the thirty-second round's 64 survivors, all narrowings, are in the record;
the terms are defined where first used, and the coupling effect and the
tools are cited for what they show. The bullet in CLAUDE.md states the
operators and the four ways a survivor ends.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
…s merge

The journal of 2026-10-28 gives the batch whole: the question and its
measures, the maintainer's decisions of 2026-10-28 to 2026-10-30, the
thirty-two review rounds and the mutation sweeps that ADR 53 ended, what
was done, the tests, the mutation, the debts filed and the checks: unit
5,573 on the committed corpus and 7,945 with the remote documents, the
site's 266 pages, and Remote corpus run 93 whole on the branch's last code.

At a glance, the batch joins the milestone's list, the tests, coverage, CI
and measurements take its figures, and a bullet states the mutation rule.
Batch 16's record gains its merge and the end of the per-commit check.

Refs #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiFf9bygtdCAcN4va1ss8T
@codecov

codecov Bot commented Oct 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@AdCodicem
AdCodicem merged commit 75999e4 into main Oct 30, 2026
21 checks passed
@AdCodicem
AdCodicem deleted the ccr-6d6c0ee6-dqm4lm-155 branch October 30, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Object numbers chosen to collide turn the index, the cache and the validator's sets into one hash chain

2 participants