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 1 | 107 |
| Dropped in pass 2, justified | 37 |
| Raised in pass 2 | 0 |
| Refuted | 0 |
| Narrowed | 14 |
| Kept | 23 |
| — fixed here | 9 |
| — filed under M02 | 11, in 9 issues |
| — filed under a later milestone | 2 |
| — filed under no milestone | 1 |
| — put to the maintainer | 0 |
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(LoadObjectlooks up_xref.TryGet(id.Number, …)),:2108(TryParseNumberedAtcomparesfound.Number != numberonly),:2274(TryFindObjectHeadermatches the number only),:2013(relocation writes the asking reference's generation into the index), at2183466.tests/AdCodicem.Pdf.Tests/HostileInputTests.cs:228-269asserts that5 7 Ris object5 0. AgainstM01.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-mismatcha Warning); the rebuilt index at generation 0 is #118. Reproduced:5 1 Rand5 7 Rresolve to5 0 obj,7 0 Rto7 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-82accumulatesfraction += digit * scale; scale *= 0.1d:0.3reads as 0.30000000000000004.LexerTests.cs:59andPropertyTests.cscompare with a 1e-9 tolerance.CLAUDE.md(Known traps) writes a value read back as the shortest decimal of the samedouble;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.rebuiltraised as a Warning for "The file does not start with a PDF header." though nothing is rebuilt (PdfFileReader.cs:1094);xref.offset-adjustedfor an object at another index of its object stream (:2953-2955);stream.truncatedfor "An object stream is shorter than its header claims." (:2899);xref.entry-out-of-rangefor a section offset, then the same fault again asxref.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-174pins only the empty-source case, where a rebuild does follow;CrossReferenceChainTests.cs:69-78pins 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.ObjectRedefinedand its row inreference/diagnostics.md; no reference insrc/. - 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-cyclereported at the header-relative offset (PdfFileReader.cs:1188;:1185records 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.Positionis 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 onlycandidate == absolute(:1299), never an offset the chain already read (:1172): a malformed first table at 110, named by the/Prevof 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-shiftedandfile.size-wrongfor 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/XRefStmthat names a section already read, not a relocation that lands on one. - Verdict: stands, with the
/XRefStmshape added. - Outcome: #188 under M02.
R-07 — A rebuild serves a direct definition over a newer copy in an object stream
- Axis: specification
- Evidence:
ExpandEachObjectStreamadds members withTryAddafter the scan — "An object written directly in the file wins over a copy inside an object stream" (PdfFileReader.cs:2745-2746, since9b5fb43, no reason written) —, while the scan keeps the last direct definition (:2625) andM01.mdsays a rebuild keeps the last definition of each object. - Pass: 1, kept in pass 2.
- Refutation:
M01.mdspeaks 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/XRefStmcould 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 takes10 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.mdconventions) - 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 firstCLAUDE.mdbefore the files were written. - Verdict: stands.
- Outcome: fixed in
e266dbc.
R-09 — The build runs analysis at latest, where ADR 29 and CLAUDE.md say latest-recommended
- Axis: consistency
- Evidence:
Directory.Build.props:16;CLAUDE.md, Conventions;docs/adr/0029-…md:18. - Pass: 1, kept in pass 2:
a1d4c0amoved 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 atlatest-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
6bbd324and3385e46: 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), since85b69a3; ADR 36 rejects reaching the reader's internals throughInternalsVisibleTogranted 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:
Versionreturns "1.4" for a file that declares none (PdfFileReader.cs:269,DetectHeader);Open(Stream)reads an exposableMemoryStreamfrom its origin and any other stream from its position (IO/PdfFileSource.cs,FromStream); afterDisposeonlyGetObjectthrows, 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
GetObjectand 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:
PdfDictionaryhas an indexer setter,PdfArraynone;PdfEncryptedExceptionandPdfLimitExceededExceptionlack the inner-exception constructor their siblings have;PdfDocumenthasIPdfObjectSource.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
Sourcecan 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);PdfStringDecodercopies 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.mdandtests/corpus/manifest.schema.jsondefinerequiredDiagnosticsas 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
HostileInputTestshold 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
IsHexadecimalor theToStringforms ofPdfNull,PdfBoolean,PdfInteger,PdfReal,PdfReference; the use-case test omitsdamaged; the manifest-completeness test scans four folders and*.pdfonly. - Pass: 1, kept in pass 2.
- Refutation:
PdfNameandPdfStringforms are asserted indirectly;stresscannot 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.PDFfile 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), andfuzz.ymlpasses 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
/Prevloop marks the index incomplete; a broken/Rootis 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.mdanddocs/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,949c6ffand01f60bc; the test's comment in79d1835;docs/status.mdin 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 aninitproperty);ObjectCountandObjectNumbers(free entries counted);WasRepaired(can turn true afterOpen);PdfDocument's remark (opening reads the catalog too); theOpenoverloads (summary, parameters, exceptions);PdfString.FromText("narrowest");PdfObjectExtensions' remark (GetRaw);Decode's remark (unsupported filters);MaxTrailerLength;PdfDiagnostics.GetEnumerator(an emptyinheritdoc). - 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-recoveredreports the replacement); "credentials supplied" describes the contract M16 keeps. - Verdict: narrowed to the items above.
- Outcome: fixed in
b22d3b0,0741365,949c6ffand01f60bc.
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 ofsyntax.unexpected-token, the predictor failure missing);reference/reader-limits.md(whereGetObjectthrows);README.md(dotnet testwithout--solution);ARCHITECTURE.md(27 documents);docs/architecture.md(PdfNumber); theReadDocumentsample (milestone 5, "well formed"); the threat model's sentence on workflow permissions; a remark inPdfFileReaderthat says the rebuild's scan refuses what it takes. - Pass: 1, kept in pass 2.
- Refutation: the FIFO cache is #37's,
features.jsonis 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,b22d3b0and0741365.
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/checkoutkeepspersist-credentialsat its default whilenpm ciand the site build run in the job that deploys. - Pass: 1, kept in pass 2.
- Refutation: in
ci.ymlandfuzz.ymlthe persisted token iscontents: readon a public repository and grants nothing;docs.ymlalone breaks the repository's pattern offalsewherever 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:
TryRelocateXRefSectionandTryFindObjectHeaderclamp 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 and3efb5e6all 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 judgedxref.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
PdfDocumentremark 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.ToStringis display, and M03 sets the writer's form. - The rebuild's trailer scan taking
trailerinside stream data: #171, #49. - A
/Filterentry that is not a name, an array/DecodeParmsbeside a single filter, a PNG row tag past 4 and a partial row, the TIFF predictor off 8 bits, a non-Identity/Crypt: #162. ObjectNumberslive while the index is rebuilt: #167.MaxRepairObjectsunclassified and silent: #183.- Numbers narrowed or wrapped before their checks: #157.
- An index sized by a cross-reference stream's rows: #164.
- The outrageous
/Wtest that never reads the stream: #158. - The cache bounded by count: #37.
- The nesting and nested-load bounds, and
syntax.depth-exceededraised for loads: #163. Open(Stream)copying seekable streams: #149.SortedOffsetswithout a benchmark of its own:StreamLengthBenchmarksmeasures 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 inrun:, 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.