Skip to main content

Review of M01 — Object model and tolerant reading

Date: 2026-10-01. Extent: c9d751f to 1f90e8f, both included (15 commits, no pull request: M01 predates tracking on GitHub). The code reviewed is the reader as it stands on main at 2183466, as docs/milestone-review.md asks of a review after the fact. Issue: #136. Session: https://claude.ai/code/session_014r3X2MjDyadgYXM4Z4fw8R.

Summary​

Count
Candidates from pass 1107
Dropped in pass 2, justified37
Raised in pass 20
Refuted0
Narrowed14
Kept23
— fixed here9
— filed under M0211, in 9 issues
— filed under a later milestone2
— filed under no milestone1
— put to the maintainer0

Pass 1 was blind: the critic read the frame (CLAUDE.md, ADRs 1 to 18), docs/milestones/M01.md and the reader's code, and four sub-agents swept, under the same blindness, the public API against its documentation, the tests against the specification, the architecture invariants, and the workflows and supply chain; their candidates and the critic's, 107 in all and many of them the same defect seen from two sides, were written down before pass 2 began. Pass 2 dropped 37 as justified or already filed — the threat model's first version (#135) had filed thirty debts on the reader the day before, and most drops cite one of them — and merged the 70 it kept into 20 findings. Refutation narrowed 13 of the 20 and refuted none outright; two were then split so that each part has one outcome, which gives 22, 14 of them narrowed. Pass 2 raised no finding of its own, but strengthened four: R-01, R-09, R-10 and R-18. A twenty-third, R-23, surfaced after the triage, while the agent that checked R-06's issue reproduced it: the nearby search reaches further than its stated radius near the start of a file. It had no refuter of its own; it was reproduced again from both sides, an object and a section, and filed under M02 as R-06 was. It is counted among the kept, and the maintainer may move it.

No refuter rejected a whole finding, which the procedure asks to be made visible: each of the five batches narrowed some, and the lettered items of 13 findings fell, with the citation that felled them. The verdicts below say which.

The maintainer saw the triage on 2026-10-01 and kept it as proposed: R-02 and R-13 under M02, R-14 settled by aligning the specification with the manifest rather than tightening the test, and the Dependabot gap noticed outside the extent filed on its own (#196).

The trivial fixes are refactor:, test: and docs: commits, with build: for R-09 and R-10, which change build configuration and, for R-09, add one analyzer suppression to the source, changing no behavior; and ci: for R-21, which changes a workflow only. None of the five types starts a release.

What each axis examined​

Consistency of decisions. M01 predates tracking: its fifteen commits, the first nineteen decisions (now ADRs 1 to 20), the journal's Earlier, in brief and the later entries that touched the reader were read, with the ADRs accepted since — 29, 34, 36, 45 and 46 bear on the reader — and every issue of the repository, open or closed. Each settled question was followed through the sessions that came after M01. Four did not hold: the trap on generation numbers, which two later sessions settled the other way without amending it (R-01); the analysis level, lowered minutes after M01 closed while ADR 29, written fifteen minutes later, still states the old one (R-09); InternalsVisibleTo granted to a published package, against the reasoning ADR 36 gives (R-10); and the specification's acceptance row, its repair triggers, its fuzzing and memory figures, which later changes left behind (R-14, R-18). What held: ADR 34's five guards are the five PdfReaderLimits properties with their limit.* codes, each with its test; ADR 14's report is structured as decided; ADR 45's reading of a generation the entry gets wrong is what the code does; the documents and the code follow ADR 20's American spelling (M01's own commit bodies predate its amendment and cannot be rewritten). M01 has no issue on GitHub to reconcile with its exit criteria; each criterion's evidence exists, the memory figure aside (R-18).

Code against specification. Every acceptance condition names a test that exists under that name, and each was read against the condition and the documents it names: row 1 is weaker than its wording only because the wording outran the manifest's definition (R-14); row 2 counts the validator's findings, as the test's comment and c617476 justify; row 3 times opening only (R-15). Every behavior of the Design section was looked for in the code: the PDFDocEncoding it claims is #36's to deliver, the repair triggers changed (R-18), and a rebuild does not always keep the last definition (R-07). Of the Tests required, a file of several hundred thousand objects under a budget (R-15), deep dictionaries, exotic white space and the canonical forms (R-16) had no test; the outrageous /W test that never reaches its code is #158's. Of the Traps, four are held by the tests the specification implies; the fifth, generations, is broken by the code and by a test that pins it (R-01).

Architecture invariants. Invariants 1 to 12 over src/AdCodicem.Pdf/Objects, IO, Diagnostics and Documents. 1 holds — no package, no project reference, the AOT and trimming analyzers on — and only review enforces it (#179). 2 holds through windows and lazy stream data, with the gaps filed: a stream copied whole (#149), raw data kept (#170), a cache bounded by count (#37). 3 holds on the lexer, the parser and the number parser; LZW decoding allocates per code (R-13). 4 and 12: every constant that bounds what a file says carries its ADR 34 class and reason, but for MaxRepairObjects (#183) and the nesting bounds (#163); narrowing before checks is #157; every loop whose exit depends on the file ends. 5: what the reader works around in silence is filed (#119, #141, #162, #172, #173), and one public code is never raised (R-04). 6 holds: invariant culture everywhere, no clock, no random source, diagnostics in program order. 8: the interned names are the only shared mutable state, thread-safe and filed for growth (#37); the object model's mutability is M03's to close (its objects read from a file become read-only). 9 to 11 hold: the reader's optimizations are benchmarked — SortedOffsets through StreamLengthBenchmarks, the index through ReaderBenchmarks —, both test levels run, the corpus is used.

Public API and documentation. The public surface M01 added — Objects, Diagnostics, Documents and PdfFileSource — was read member by member against its XML documentation, and the user pages, docs/architecture.md, ARCHITECTURE.md, README.md, the sample and docs/features/features.json against the code. Every concrete public class is sealed but the exception base type; every public member has documentation; the seventeen codes of PdfDiagnosticCodes are all listed in reference/diagnostics.md, the limits and options defaults match everywhere, and the measurement figures agree with each other. What did not hold: four files with several public types (R-08); codes raised outside their meaning and positions that are not offsets (R-03 to R-05); PdfDocument's behavior where nothing documents it (R-11); the object model's shape before the API baseline (R-12); and documentation that says what the code does not (R-19, R-20).

Security. Every path that reads a byte from a file — the lexer, the parser, the filters, the cross-reference chain, object streams, the endstream search and the rebuild — was checked against docs/threat-model.md, whose reader section was written the day before; twelve of its defenses were opened in the code and the test it names, and the relocation cap was confirmed by mutation (removed, its test fails). Decompression is bounded, recursion is bounded, every loop ends; what is not — work per document, raw data, index size, filter chains — is in its Known gaps. The fuzzing reaches the parser, the reader and the validator; what it does not reach is #176 and #158, and that it replays the same mutants is R-17. The reader opens no path but the caller's, no network, parses no XML, executes nothing. Supply chain: every action is pinned by SHA and every workflow holds least privilege but docs.yml, whose token the checkout kept (R-21, fixed); the release jobs' exposure, the stable step that cannot run and the template expansion are #177; the corpus build's Python pins carry no hash (R-22). Test-only dependencies were read for licenses: none reaches the package; JsonSchema.Net's maintenance-fee EULA, the coverage tool's proprietary terms and iText's AGPL arrived after M01 and stay in test and benchmark projects.

Findings​

R-01 — A reference's generation is never compared with the object it resolves to​

  • Axis: specification (traps), consistency
  • Evidence: src/AdCodicem.Pdf/IO/PdfFileReader.cs:1937 (LoadObject looks up _xref.TryGet(id.Number, …)), :2108 (TryParseNumberedAt compares found.Number != number only), :2274 (TryFindObjectHeader matches the number only), :2013 (relocation writes the asking reference's generation into the index), at 2183466. tests/AdCodicem.Pdf.Tests/HostileInputTests.cs:228-269 asserts that 5 7 R is object 5 0. Against M01.md (Traps: "never drop them from a key"), Objects/PdfObjectId.cs:7 ("never safe to ignore in a key"), ISO 32000-1 §7.3.10.
  • Pass: 1, kept in pass 2. Pass 2 found the choice taken twice after M01 without amending the trap: 15af060 (2026-09-28) keyed the cache and the loading set by number "on purpose", and #118's body calls "the index holds one entry per number" right.
  • Refutation: the number-only cache key is deliberate (PdfFileReader.cs:152-156, 15af060, the threat model's row on parsed objects piling up) and a check before the cache keeps it; an entry whose generation disagrees with its header is decided by ADR 45 (read as the references name it, xref.generation-mismatch a Warning); the rebuilt index at generation 0 is #118. Reproduced: 5 1 R and 5 7 R resolve to 5 0 obj, 7 0 R to 7 2 obj, with no diagnostic and no finding.
  • Verdict: narrowed to the comparison of a reference with its object's header, and relocation writing the asking generation.
  • Outcome: #185 under M02.

R-02 — The number parser does not give the double nearest to the decimal written​

  • Axis: invariant (CLAUDE.md, known traps), specification
  • Evidence: src/AdCodicem.Pdf/IO/PdfNumberParser.cs:75-82 accumulates fraction += digit * scale; scale *= 0.1d: 0.3 reads as 0.30000000000000004. LexerTests.cs:59 and PropertyTests.cs compare with a 1e-9 tolerance. CLAUDE.md (Known traps) writes a value read back as the shortest decimal of the same double; M03.md's slice 1 property and fixed point depend on the reader giving the nearest one.
  • Pass: 1, kept in pass 2 (#157 covers wrapping and overflow, not this).
  • Refutation: nothing justifies or tracks it; the parser's remark justifies hand-writing it, not imprecision. Measured: about 61 % of values below 1 — matrices, colors, opacities — differ from double.Parse, about 0.02 % to 0.04 % with whole parts; a value read, written as its shortest decimal and read again changes in about 61 % of cases below 1.
  • Verdict: stands.
  • Outcome: #186 under M02, blocking M03 slice 1.

R-03 — Five reader diagnostics are raised under codes whose meaning does not cover them​

  • Axis: API and documentation
  • Evidence: xref.rebuilt raised as a Warning for "The file does not start with a PDF header." though nothing is rebuilt (PdfFileReader.cs:1094); xref.offset-adjusted for an object at another index of its object stream (:2953-2955); stream.truncated for "An object stream is shorter than its header claims." (:2899); xref.entry-out-of-range for a section offset, then the same fault again as xref.section-missing (:1388-1394, :1221-1223); xref.section-missing, "not there, nor near it", for a section present but malformed (:1211-1223). The codes are "part of the public contract".
  • Pass: 1, kept in pass 2.
  • Refutation: the header shift reported as xref.offset-adjusted (:1103-1107) is what that code documents: every offset misses by the same shift. The other five reproduced; DocumentReaderTests.cs:172-174 pins only the empty-source case, where a rebuild does follow; CrossReferenceChainTests.cs:69-78 pins the double report with no reason written.
  • Verdict: narrowed to the five situations above.
  • Outcome: #187 under M02, with R-04 and R-05.

R-04 — object.redefined is documented and never raised​

  • Axis: API and documentation
  • Evidence: PdfDiagnosticCodes.ObjectRedefined and its row in reference/diagnostics.md; no reference in src/.
  • Pass: 1, kept in pass 2.
  • Refutation: the repair itself is reported, as xref.rebuilt, and keeping the last definition is the repair as specified; an object already served being replaced in silence is #171.
  • Verdict: narrowed to a public, documented code nothing raises.
  • Outcome: #187 under M02.

R-05 — Diagnostic positions that are not offsets in the file, or missing​

  • Axis: API and documentation
  • Evidence: xref.chain-cycle reported at the header-relative offset (PdfFileReader.cs:1188; :1185 records the absolute one); members of an object stream parsed with base 0 (:2967), so their diagnostics carry an offset into the decoded data; no position for the three object-stream messages at :2899, :2914, :2953, though the stream's is known. PdfDiagnostic.Position is documented as the byte offset in the file.
  • Pass: 1, kept in pass 2.
  • Refutation: reproduced, each; no document or issue justifies or tracks them.
  • Verdict: stands.
  • Outcome: #187 under M02.

R-06 — Relocating a section the chain cannot read can land on one it already read​

  • Axis: specification, API and documentation
  • Evidence: TryRelocateXRefSection (PdfFileReader.cs:1274-1327) skips only candidate == absolute (:1299), never an offset the chain already read (:1172): a malformed first table at 110, named by the /Prev of a sound table at 220, is "found 110 bytes from there" at 220, then reported as a loop.
  • Pass: 1 (from the documentation sweep), kept in pass 2.
  • Refutation: reproduced exactly; the index is marked incomplete and the validator then reports xref.chain-loop, xref.section-shifted and file.size-wrong for a chain that neither loops nor shifts. The check of the issue found the same defect through /XRefStm: a hybrid table whose stream is not at the offset named is relocated onto the table itself, nothing is reported missing, and an object only the stream indexes reads as null. #156 covers an /XRefStm that names a section already read, not a relocation that lands on one.
  • Verdict: stands, with the /XRefStm shape added.
  • Outcome: #188 under M02.

R-07 — A rebuild serves a direct definition over a newer copy in an object stream​

  • Axis: specification
  • Evidence: ExpandEachObjectStream adds members with TryAdd after the scan — "An object written directly in the file wins over a copy inside an object stream" (PdfFileReader.cs:2745-2746, since 9b5fb43, no reason written) —, while the scan keeps the last direct definition (:2625) and M01.md says a rebuild keeps the last definition of each object.
  • Pass: 1, kept in pass 2.
  • Refutation: M01.md speaks of the header scan, but the reader's own reason for last-wins is general; ADR 45 admits a scan may keep the wrong copy when it cannot know, while here it knows both offsets; the hybrid file's ranking of a table over its /XRefStm could justify the rule within one revision, but nothing says so, and a rebuild applies it across revisions. Reproduced. The refutation also noticed that the header scan takes 10 0 objx, which the issue carries.
  • Verdict: stands.
  • Outcome: #189 under M02.

R-08 — Four files of M01 hold several public types​

  • Axis: consistency (CLAUDE.md conventions)
  • Evidence: Objects/PdfObject.cs, Objects/PdfNumber.cs (no type of that name), Diagnostics/PdfDiagnostic.cs, Diagnostics/PdfException.cs.
  • Pass: 1, kept in pass 2 (no justification found).
  • Refutation: the four are the only ones in src/; the rule was in the first CLAUDE.md before the files were written.
  • Verdict: stands.
  • Outcome: fixed in e266dbc.
  • Axis: consistency
  • Evidence: Directory.Build.props:16; CLAUDE.md, Conventions; docs/adr/0029-…md:18.
  • Pass: 1, kept in pass 2: a1d4c0a moved it eight minutes after M01's last commit, 1f90e8f, and ADR 29 was written fifteen minutes later stating the old level.
  • Refutation: a1d4c0a's "without a single new finding" was true then; the whole solution now reports seven findings at latest-recommended, all from commits of 2026-09-25 to 30: CA1720 on a public name in the core, six in the tests.
  • Verdict: stands.
  • Outcome: fixed in 6bbd324 and 3385e46: the six test findings fixed, CA1720 suppressed where it fires with a reason, as ADR 29 allows.

R-10 — The core grants its internals to a package that does not exist yet​

  • Axis: consistency, security
  • Evidence: src/AdCodicem.Pdf/AdCodicem.Pdf.csproj:23-24 (AdCodicem.Pdf.Html, AdCodicem.Pdf.Html.Tests), since 85b69a3; ADR 36 rejects reaching the reader's internals through InternalsVisibleTo granted to a published assembly.
  • Pass: 1, kept in pass 2.
  • Refutation: accessibility is no security boundary in .NET, and the AdCodicem.* prefix is reserved; what remains is a dormant grant to a planned published package that nothing decided, a compatibility risk package validation does not see.
  • Verdict: narrowed to consistency.
  • Outcome: fixed in 1a7d374.

R-11 — PdfDocument behaves where nothing documents it​

  • Axis: API and documentation
  • Evidence: Version returns "1.4" for a file that declares none (PdfFileReader.cs:269, DetectHeader); Open(Stream) reads an exposable MemoryStream from its origin and any other stream from its position (IO/PdfFileSource.cs, FromStream); after Dispose only GetObject throws, and a reference taken before keeps resolving for a document opened from memory.
  • Pass: 1, kept in pass 2.
  • Refutation: negative capacities, the fourth item, are #169's; the threat model scopes use after dispose to GetObject and the validator deliberately, but nothing says what the rest answers.
  • Verdict: narrowed to the three items above.
  • Outcome: #190 under M02.

R-12 — The object model's public shape has loose ends to settle before the API baseline​

  • Axis: API and documentation
  • Evidence: PdfDictionary has an indexer setter, PdfArray none; PdfEncryptedException and PdfLimitExceededException lack the inner-exception constructor their siblings have; PdfDocument has IPdfObjectSource.GetObject's signature without implementing it, and the source a reference carries skips the document's disposed check.
  • Pass: 1, kept in pass 2.
  • Refutation: the factory idioms follow a documented rule; the concrete enumerators are documented as non-allocating; "a caller cannot build a reference that resolves through a document" is false, since a reference's Source can be reused.
  • Verdict: narrowed to the items above.
  • Outcome: #191 under M03, before #35 records the baseline.

R-13 — LZW decoding allocates an array for every code​

  • Axis: invariant 3
  • Evidence: src/AdCodicem.Pdf/IO/Filters/LzwFilter.cs (per-code entries, 256 one-byte arrays per decode); PdfStringDecoder copies its working buffer once more per escaped string.
  • Pass: 1, kept in pass 2.
  • Refutation: the line numbers of the finding were wrong; the LZW part stands in full — one decode of 1 MiB allocated 8.9 MB, about 116,000 per-code arrays —; the string part is one avoidable copy per token, a frugality nit.
  • Verdict: narrowed to LZW, with the copy as a note.
  • Outcome: #192 under M02.

R-14 — The acceptance row says "exactly", where the manifest defines a floor​

  • Axis: specification
  • Evidence: M01.md, acceptance row 1; docs/corpus.md and tests/corpus/manifest.schema.json define requiredDiagnostics as codes the reader must report; the test checks each is reported, and a clean document has no repair and no warning.
  • Pass: 1, kept in pass 2.
  • Refutation: the test has checked containment since it was written; the specification's "exactly" is the side out of line.
  • Verdict: stands, as a wording fault.
  • Outcome: fixed in bf54783, the row aligned with the manifest, as the maintainer chose on 2026-10-01.

R-15 — No test holds a large index to a memory budget, nor a corpus document's full read to a time budget​

  • Axis: specification (tests required), invariant 9
  • Evidence: M01.md, Tests required: "opening a file of several hundred thousand objects within a stated budget"; the million-entry test times only; corpus documents are timed at opening only (CorpusReadingTests.cs:181).
  • Pass: 1, kept in pass 2.
  • Refutation: two allocation budgets exist on the journal, fuzzing holds its seeds to 64 MB and 5 s, and HostileInputTests hold allocation and time, so what falls is "no corpus document has an allocation bound"; the large index and the untimed full read remain. #46 is the remote corpus's very large documents.
  • Verdict: narrowed.
  • Outcome: #193 under M02.

R-16 — Tests the specification requires are missing or narrower than it​

  • Axis: specification (tests required)
  • Evidence: only arrays are nested deep; no lexer test separates tokens with NUL, form feed or a lone CR; no test asserts IsHexadecimal or the ToString forms of PdfNull, PdfBoolean, PdfInteger, PdfReal, PdfReference; the use-case test omits damaged; the manifest-completeness test scans four folders and *.pdf only.
  • Pass: 1, kept in pass 2.
  • Refutation: PdfName and PdfString forms are asserted indirectly; stress cannot be required of the committed corpus, every such document being remote by decision; the four folders are where the schema lets a document live, but a misplaced or .PDF file would pass unseen.
  • Verdict: narrowed.
  • Outcome: fixed in 79d1835.

R-17 — The nightly fuzzing replays the same mutants every night​

  • Axis: security (fuzzing)
  • Evidence: the three targets loop over seeds 0 to Iterations - 1 (FuzzingTests.cs), and fuzz.yml passes no seed offset, while its property step varies its seed with the run number.
  • Pass: 1, kept in pass 2.
  • Refutation: confirmed; #176 does not cover it; fixed seeds make a replay need only the document and the seed, a trade-off nothing records.
  • Verdict: stands.
  • Outcome: #194 under M23.

R-18 — M01.md is out of line with the code and the corpus​

  • Axis: consistency, specification
  • Evidence: its repair triggers (a /Prev loop marks the index incomplete; a broken /Root is looked for among the chain's objects first); its nightly campaign (a core and a rotating share since 2026-09-25); its memory figure (indexing and walking the page tree, 3.2 MB measured on 2026-10-01 against the 2.4 MB quoted, also in the test's comment, lazy-reading.md and docs/status.md); its corpus section (the unsupported list, 56 linearized); its documentation section (paths before ADR 47).
  • Pass: 1, kept in pass 2: the journal dates the rotation, and ADR 47's remapping covered M02 to M31 but not M01.
  • Refutation: PDFDocEncoding is #36's, splice is #176's, "no document skipped" holds for the committed corpus, and the acceptance row on silence is justified by the test's comment and c617476.
  • Verdict: narrowed to the five items above.
  • Outcome: fixed in bf54783, 949c6ff and 01f60bc; the test's comment in 79d1835; docs/status.md in this review's record.

R-19 — The XML documentation of the reader's public API says what the code does not do​

  • Axis: API and documentation
  • Evidence: PdfDiagnostics.Capacity ("Gets or sets" on an init property); ObjectCount and ObjectNumbers (free entries counted); WasRepaired (can turn true after Open); PdfDocument's remark (opening reads the catalog too); the Open overloads (summary, parameters, exceptions); PdfString.FromText ("narrowest"); PdfObjectExtensions' remark (GetRaw); Decode's remark (unsupported filters); MaxTrailerLength; PdfDiagnostics.GetEnumerator (an empty inheritdoc).
  • Pass: 1, kept in pass 2.
  • Refutation: the cache's floor of 64 is #169's; the trailer's documentation is right (ISO 32000-1 §7.5.6, and trailer.root-recovered reports the replacement); "credentials supplied" describes the contract M16 keeps.
  • Verdict: narrowed to the items above.
  • Outcome: fixed in b22d3b0, 0741365, 949c6ff and 01f60bc.

R-20 — User and project documentation out of line with the code​

  • Axis: API and documentation
  • Evidence: lazy-reading.md (what opening keeps, a stream copy in the present tense, "a small fraction"); PdfXRefTable's remark ("the only structure"); reference/diagnostics.md (two definitions of syntax.unexpected-token, the predictor failure missing); reference/reader-limits.md (where GetObject throws); README.md (dotnet test without --solution); ARCHITECTURE.md (27 documents); docs/architecture.md (PdfNumber); the ReadDocument sample (milestone 5, "well formed"); the threat model's sentence on workflow permissions; a remark in PdfFileReader that says the rebuild's scan refuses what it takes.
  • Pass: 1, kept in pass 2.
  • Refutation: the FIFO cache is #37's, features.json is right as written, docs/architecture.md's present tense describes the target design, and the API baseline it mentions is #35.
  • Verdict: narrowed to the items above.
  • Outcome: fixed in 949c6ff, 01f60bc, bf54783, b22d3b0 and 0741365.

R-21 — docs.yml keeps its checkout's token beside pages: write and id-token: write​

  • Axis: security (supply chain)
  • Evidence: .github/workflows/docs.yml: actions/checkout keeps persist-credentials at its default while npm ci and the site build run in the job that deploys.
  • Pass: 1, kept in pass 2.
  • Refutation: in ci.yml and fuzz.yml the persisted token is contents: read on a public repository and grants nothing; docs.yml alone breaks the repository's pattern of false wherever a job holds a write permission.
  • Verdict: narrowed to docs.yml.
  • Outcome: fixed in 8834b9f.

R-22 — The corpus build's Python requirements are pinned without hashes​

  • Axis: security (supply chain)
  • Evidence: tests/corpus/build/requirements.txt.
  • Pass: 1, kept in pass 2.
  • Refutation: no workflow installs it and Dependabot does not cover pip; a maintainer regenerating the corpus is its only use.
  • Verdict: narrowed to hygiene.
  • Outcome: #195 under no milestone.

R-23 — Near the start of a file, the nearby search reaches 1,024 bytes past the offset, not 512​

  • Axis: API and documentation, specification
  • Evidence: TryRelocateXRefSection and TryFindObjectHeader clamp their window's start at 0 but keep its length at 1,024 bytes (PdfFileReader.cs:1286-1287, :2252-2253), so an offset below 512 is searched up to 1,024 bytes after it; NearbySearchRadius, both methods' remarks, the validator's messages, reference/validation-rules.md, the threat model and 3efb5e6 all say 512 bytes either side.
  • Pass: found after the triage, while R-06's issue was checked.
  • Refutation: none by a refuter. Reproduced: an object 797 bytes after an entry at offset 20 is found nearby and judged xref.entry-shifted, a Warning; the same shift past the first 512 bytes is found by a rebuild and judged xref.entry-broken, an Error. A section named at 184 is relocated 734 bytes after it. Nothing breaks on a valid file.
  • Verdict: stands.
  • Outcome: #197 under M02.

Dropped candidates​

Pass 2 justified these, each by what is cited.

  • The process-wide name table: #37 plans a frozen table and one per document; the PdfDocument remark holds, the table being thread-safe (docs/threat-model.md, What the library never does).
  • The object model's mutability shared with the cache: M03.md, The change set (objects read from a file become read-only).
  • A real that overflows to an infinity: #157; PdfReal.ToString is display, and M03 sets the writer's form.
  • The rebuild's trailer scan taking trailer inside stream data: #171, #49.
  • A /Filter entry that is not a name, an array /DecodeParms beside a single filter, a PNG row tag past 4 and a partial row, the TIFF predictor off 8 bits, a non-Identity /Crypt: #162.
  • ObjectNumbers live while the index is rebuilt: #167.
  • MaxRepairObjects unclassified and silent: #183.
  • Numbers narrowed or wrapped before their checks: #157.
  • An index sized by a cross-reference stream's rows: #164.
  • The outrageous /W test that never reads the stream: #158.
  • The cache bounded by count: #37.
  • The nesting and nested-load bounds, and syntax.depth-exceeded raised for loads: #163.
  • Open(Stream) copying seekable streams: #149.
  • SortedOffsets without a benchmark of its own: StreamLengthBenchmarks measures the searches it serves (d4387e9); the filters claim no optimization.
  • qpdf's "damaged" verdict not tied to the reader's: docs/corpus-contributions.md ("Damage and rejection differ"), 628af05.
  • PdfString's "original notation": the flag hex or literal (PdfString.cs); M03 writes literal strings with its own escapes.
  • The stable release step that cannot run, the release jobs building with the right to publish, the prepare step's npm tree, docs.yml's workflow-wide permissions, expressions expanded in run:, the NuGet key on a command line, the referee image by tag: #177.
  • Dependabot's auto-merge failing open: outside the extent (a1d4c0a, after M01); filed on its own, #196.
  • Publishing not tied to main: #178. The NuGet restore not locked: #43. Invariant 1 enforced by review alone: #179.
  • CodeQL's run not visible in the repository: GitHub's default setup (journal of 2026-09-26, T35; docs/threat-model.md).
  • AngleSharp declared centrally and referenced by nothing: ADR 2 names it for the HTML engine.
  • Test-only licenses (JsonSchema.Net, the coverage tool, iText): nothing ships; all arrived after M01.
  • Unclosed containers, a gap of more than four bytes before endstream, reference chains, encryption detection, raw data kept, object-stream tables, pooled buffers, rebuilt generations: #119, #174, #173, #154, #170, #160, #180, #118.
  • The documentation disagreeing on which bounds a valid file reaches: #183, #163.
  • "Reports every repair": each silent repair named is filed (#119, #141, #162, #172, #173, #183) or is R-04.