Skip to content

ADFA-3305: Drop documentation rows nothing reads, and tooltip details that repeat the summary - #46

Open
davidschachterADFA wants to merge 10 commits into
mainfrom
fix/ADFA-3305-drop-unread-rows
Open

davidschachterADFA wants to merge 10 commits into
mainfrom
fix/ADFA-3305-drop-unread-rows

Conversation

@davidschachterADFA

@davidschachterADFA davidschachterADFA commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

ADFA-3305

Two new pipeline steps make documentation.db 6.5 MB smaller (5.6 MB in the .br download) by deleting rows nothing
reads and emptying tooltip details that only repeat their summary.

  • scripts/drop_unread_rows.py deletes four groups of Content rows, with their chunk rows:
    • javadoc-use: javadoc Use pages and module graphs, 4,956 rows. Only other Use pages link to them.
    • pdfjs-cmaps: pdf.js CMaps, 169 rows. No stored PDF has a font that needs one.
    • pdfjs-extras: pdf.js no-WebAssembly fallbacks and scripting sandbox, 5 rows. The step also sets
      enableScripting to false in p/web/viewer.mjs, which is the only way the viewer loads the sandbox.
    • x-placeholders: the x/ "Placeholder for XML layout attribute" pages, 1,238 rows. x.html and x.pdf stay.
  • scripts/clear_repeated_tooltip_detail.py sets Tooltips.detail to '' where it is exactly the summary:
    1,605 tooltips (1,592 Kotlin, 13 Java).

Sizes

Base: the 2026-10-18 database after ADFA-6627's first build (#44 at a3c444e; 110,895,104 bytes). Each result, and the base, was packed with
rebuild_database.py --pack Content,Tooltips --page-size 4096 (from the ADFA-3305 branch) and compressed with
brotli -q 11 -w 24. Packed base: 110,895,104 bytes, .br 87,091,418.

Change Step, group Rows File saved .br saved
D01 drop_unread_rows, javadoc-use 4,956 5,042,176 4,330,908
D04 drop_unread_rows, pdfjs-cmaps 169 860,160 851,736
D05 drop_unread_rows, pdfjs-extras 5 339,968 346,809
D06 clear_repeated_tooltip_detail 1,605 151,552 10,049
D07 drop_unread_rows, x-placeholders 1,238 122,880 103,920
All all five, in this order 6,520,832 5,635,912

All five together: 104,374,272 bytes, .br 81,455,506. Repacking alone moves the .br by up to about 0.2 MB, so the
D06 and D07 .br figures are within that. Neither step needs a dictionary re-mint: the deletions remove rows, the
viewer edit changes one word, and D06 changes Tooltips, which are not Brotli-compressed.

What users see

Only D06 changes anything visible. The first tooltip popup is unchanged. All 1,605 tooltips have buttons, so See More
still shows; its popup used to show the summary again, a blank line, then the links, and now shows only the links. The
info dialog and the feedback e-mail show an empty Detail.

Safety checks

drop_unread_rows.py writes nothing, and exits 1 with the list, if:

  • any kept text row (HTML, CSS, JS, SVG, JSON contexts and the rest, decoded, chunks joined), template, tooltip
    button, tooltip text or Bookshelf entry links to a row it would delete, or names a file only deleted rows have.
    Three references are expected and allowed, and printed: the worker's two fallback imports, which run only when
    WebAssembly cannot start, and the viewer's sandbox path, dead once scripting is off;
  • a row to delete is not what its group holds (for example, an x/ page that is not a placeholder);
  • a stored PDF has a font pdf.js would fetch a CMap for (font names are read from /BaseFont and /FontName), or
    its fonts cannot all be read: it is encrypted, or a stream other than an image or cross-reference stream cannot be
    decoded (Flate, ASCII85, ASCIIHex and RunLength are decoded, in chains);
  • the tooltip tables are in neither documentation schema (2 or 3);
  • the viewer does not hold exactly one enableScripting default, or a kept row, a template, a row to delete or a
    PDF cannot be read.

From documentation version 3 (the newest DocumentationDatabaseVersion row, ordered as the app orders them) every
template is stored Brotli-compressed against the dictionary, so the step decodes templates before reading their
links; below 3 it reads them as plain text. On the 2026-10-19 ship-time build (3.0.0, 1,126 compressed templates) the
link-like strings it reads from templates went from 0 to 29,210; decoding them takes about 1.1 s. That build has
already had its rows dropped, so a dry run there finds nothing to do. On a copy given one x/ placeholder row and a
link to it in page.peb, the step refuses (template page.peb links to x/probe.html); before this fix it exited 0.

On the real database, a separate link checker that shares no code with the step found no remaining row linking to
any of the 6,368 deleted paths.

PDF rendering, in headless Chrome 140 on loopback only: all 15 stored PDFs, plus synthetic form, JPX and CCITT PDFs,
were opened in the stored pdf.js viewer before and after (D05 alone, and all five). Page counts, text and rendered text
layers matched on every page; nothing requested a deleted file; no new 404 or console message appeared. The form PDF
loaded the sandbox before the change and not after, and its field still showed its value. The JPX and CCITT PDFs
used openjpeg.wasm and jbig2.wasm, never a fallback. Chrome 140 lacks Map.prototype.getOrInsertComputed and
Math.sumPrecise, which pdf.js 6.1.200 calls, so the check defined both on both sides; ADFA-6636 tracks that.

Tests

48 tests (scripts/test_drop_unread_rows.py, scripts/test_clear_repeated_tooltip_detail.py), runnable under pytest
or as plain scripts. Each guard in the two steps was broken once, in a copy of the scripts: 92 mutants (49 for the
first version, 26 for the review fixes, 17 for the template decoding), each failing at least one test.

Pipeline position

  • clear_repeated_tooltip_detail.py runs after every step that writes tooltip text and before ADFA-3305: Migrate the tooltip tables to keyed tables (documentation schema 3.0.0) #45's
    migrate_tooltip_schema.py, which moves detail out of Tooltips; it refuses a migrated database.
  • drop_unread_rows.py runs after every step that writes page content, links or tooltips. It reads both tooltip
    schemas and decodes version 3's compressed templates, so it may run on either side of the migration.
  • Both run before rebuild_database.py. Both back the database up, write in one transaction, VACUUM, support
    --dry-run, and change nothing on a second run.

Not covered

  • No test on a device or emulator, so WebAssembly in the app's WebView is untested.
  • The 7 books of the current Bookshelf plugin (appdevforall/addons) need no CMap (checked with pdffonts); books a
    plugin installs on the phone later are not checked.
  • CI (python-tests.yaml) does not run scripts/test_*.py, so these tests run only by hand.

🤖 Generated with Claude Code

https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3

davidschachterADFA and others added 2 commits October 18, 2026 20:13
drop_unread_rows.py deletes four groups of Content rows, with their chunk
rows: the javadoc Use pages and module graphs (4,956 rows), pdf.js's CMaps
(169), pdf.js's no-WebAssembly fallbacks and scripting sandbox (5), and the
x/ placeholder pages (1,238). For the sandbox it also sets enableScripting's
default to false in p/web/viewer.mjs, the only way the viewer loads it.

Before writing, it refuses with the list and exit 1 if any kept text row,
template, tooltip button, tooltip text or Bookshelf entry refers to a row it
would delete; if a row to delete is not what its group holds; if a stored PDF
has a font pdf.js would fetch a CMap for; if the viewer edit cannot be made;
or if a kept row cannot be read. Three references are expected and allowed:
the worker's two fallback imports and the viewer's sandbox path.

Each group alone, on the 2026-10-18 database after ADFA-6627, packed with
rebuild_database.py --pack Content,Tooltips --page-size 4096, saves (file /
brotli -q 11 -w 24 bytes): javadoc-use 5,042,176 / 4,330,908; pdfjs-cmaps
860,160 / 851,736; pdfjs-extras 339,968 / 346,809; x-placeholders 122,880 /
103,920. In headless Chrome, all 15 stored PDFs show the same page count, text
and text layer in pdf.js before and after, and none requests a deleted file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
clear_repeated_tooltip_detail.py sets Tooltips.detail to '' where it is
exactly the summary: 1,605 tooltips (1,592 Kotlin, 13 Java), 151,274 bytes.
All of them have buttons, so See More stays, and its popup lists the links
without repeating the summary above them. The first popup is unchanged.

Packed, the file is 151,552 bytes smaller; the .br change (10,049 bytes) is
within what repacking alone moves it. With drop_unread_rows.py, all five
changes applied in order and packed save 6,520,832 file bytes and 5,635,912
brotli -q 11 -w 24 bytes (110,895,104 -> 104,374,272; 87,091,418 ->
81,455,506).

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

Copy link
Copy Markdown
Collaborator Author

Independent Claude review (head 87b8b62)

Six findings: two medium, four low. None of them deletes a row that something still reads.

ID Severity file:line Finding
R46-01 medium drop_unread_rows.py:456 Crashes on a schema-3 DB, which its stated position allows
R46-02 medium clear_repeated_tooltip_detail.py:1 No PEP 723 block: the documented uv run dies, no brotli
R46-03 low drop_unread_rows.py:353 Chinese-font rule reads /BaseFont; pdf.js reads /FontName
R46-04 low drop_unread_rows.py:335 PDF streams it cannot decode are skipped, not refused
R46-05 low drop_unread_rows.py:284 Links with '..' past the root are dropped; browsers clamp
R46-06 low test_drop_unread_rows.py Mutants of the guards at :403, :421, :474 pass all 24 tests

R46-01. run() always reads TooltipButtons.uri and Tooltips.summary/detail. Schema 3 (#45's migrate_tooltip_schema.py) removes both: buttons get uriId/descriptionId and the text moves to TooltipTexts. The migration writes Tooltips, so both placement rules allow this step after it: the docstring's "after every step that writes page content or links" and the PR body's "...or Tooltips". On a migrated copy, drop_unread_rows.py --dry-run scanned for about a minute and then failed with sqlite3.OperationalError: no such column: uri at line 456; the database was unchanged. The tooltip step's docstring also says "after every step that writes Tooltips" while the PR body says it runs before the migration (it does refuse schema 3 cleanly).

R46-02. clear_repeated_tooltip_detail.py has no # /// script block, but it imports optimize_db_media, which imports brotli and PIL at module level. uv run scripts/clear_repeated_tooltip_detail.py --help fails with ModuleNotFoundError: No module named 'brotli'. The tests hide it because the test file declares both itself.

R46-03. The stored pdf.js 6.1.200 takes the font name from the FontDescriptor's /FontName when it differs from /BaseFont (pdf.worker.mjs:37103-37121), and that name decides the GBK-EUC-H fetch (:36486-36500). cmap_needs checks /BaseFont only: a non-embedded TrueType font with /BaseFont /SimSunX and /FontName /#CB#CE#CC#E5 returns []. A string /FontName is missed too. No stored PDF has this today.

R46-04. pdf_parts silently skips any stream zlib cannot inflate: an object stream filtered [/ASCII85Decode /FlateDecode], or every stream of an encrypted PDF. A crafted object stream holding /Encoding /90ms-RKSJ-H behind ASCII85 + Flate returns []. The docstring says any sign of a CMap need refuses the run, and an undecodable text row does refuse; here it neither sees the font nor refuses. No stored PDF or plugin book is encrypted; their undecoded streams are images.

R46-05. link_paths discards a resolved path that starts with ../. A browser resolves ../../x/a.html from i/clamp.html to /x/a.html, since .. stops at the root, and the file-name fallback does not fire when a kept row shares the name. With the fixture plus i/sub/a.html and that link, the run exits 0 and deletes x/a.html. 117 of the 1,238 x/ file names are shared with kept rows; no such link exists today.

R46-06. Three mutants pass all 24 tests: (a) line 403 without not viewer_changed prints "Nothing to do" and leaves scripting on when the sandbox rows are already gone; (b) line 421 treats an undecodable row to delete as fine; (c) line 474 treats an unreadable PDF as needing no CMap. The PR's "49 mutants, each failing at least one test" does not hold for these three.

Checked and sound

  • A dry run leaves the file unchanged and writes no backup. A real run on a copy deletes exactly 6,368 rows (javadoc group: 4,672 class-use, 224 package-use, 60 module graphs); the only changed row is p/web/viewer.mjs, line 698 only. A second run of each step prints "Nothing to do".
  • No kept row, template, tooltip or button refers to a deleted path, and no app or addons code builds these URLs. All 13,613 XML tooltip buttons point at a/.
  • enableScripting is the only way the viewer loads pdf.sandbox.mjs; the no-WebAssembly fallbacks load only when WebAssembly fails.
  • The 7 Bookshelf-plugin books in appdevforall/addons need no CMap, so the "Not covered" item about plugin books can be closed for the current plugin.
  • The tooltip figures (1,605: 13 Java, 1,592 Kotlin) and "What users see" match ToolTipManager.
  • Chunk rows, rollback, backup and VACUUM are handled correctly; all 33 tests pass and write nothing into the checkout.
  • On a 112,283,648-byte copy of the input, the packed file shrank by 6,733,824 bytes and the .br by 5,764,408, in line with the PR's figures.

🤖 Generated with Claude Code

https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3

davidschachterADFA and others added 6 commits October 18, 2026 21:37
…p before the migration

drop_unread_rows.py read TooltipButtons.uri and Tooltips.summary/detail, which
migrate_tooltip_schema.py removes, so after the migration it scanned for a
minute and then crashed (no such column: uri). It now reads either shape,
detected before the scan, and refuses tables in neither shape before reading
anything.

clear_repeated_tooltip_detail.py said it runs after every step that writes
Tooltips, which would put it after the migration. It now says it runs before
migrate_tooltip_schema.py and refuses a migrated database by name.

PR #46 review finding R46-01.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
clear_repeated_tooltip_detail.py had no PEP 723 block, but it imports
optimize_db_media, which imports brotli and PIL at module level, so the
documented `uv run scripts/clear_repeated_tooltip_detail.py` failed with
ModuleNotFoundError. The test file declared both itself, which hid it; a test
now checks the script's own block. The plain-script test runner also calls
tests that take no directory.

PR #46 review finding R46-02.

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

pdf.js 6.1.200 takes a font's name from its FontDescriptor's /FontName when
that differs from /BaseFont, and that name decides whether it fetches the
GBK-EUC-H CMap. The check read /BaseFont only, as a name. It now reads both
keys, each as a name, a literal string (escapes and nested parentheses) or a
hex string, and refuses if either is one of the ten names.

PR #46 review finding R46-03.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
The CMap check inflated each stream with zlib and silently skipped any that
failed: an object stream filtered [/ASCII85Decode /FlateDecode], every stream
of an encrypted PDF, an LZW stream. A font hidden in one was neither seen nor
refused, though the docstring promised that any sign of a CMap need refuses.
It also read the "stream" inside "endstream" as the start of a stream.

Each stream is now found after its dictionary's ">>", bounded by a direct
/Length when one fits, and decoded through its filters (FlateDecode,
ASCII85Decode, ASCIIHexDecode, RunLengthDecode, in any chain). Image and
cross-reference streams are skipped, since neither can hold a font. An
encrypted PDF, or a stream with any other filter, a predictor or corrupt
data, refuses the run by name. All 15 stored PDFs decode fully and need no
CMap.

PR #46 review finding R46-04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
link_paths discarded any resolved path that climbed past the root, but a
browser resolves ../../x/a.html from i/clamp.html to /x/a.html. When a kept
row shared the file name, the file-name rule did not fire either, so such a
link to a deleted row went unseen. Leading ".." segments are now dropped at
the root.

PR #46 review finding R46-05.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
Three guards could be broken without a test failing: turning scripting off
when the sandbox rows are already gone (the run said "Nothing to do" and left
it on), refusing a row to delete that cannot be read, and refusing a stored
PDF that cannot be read. Each now has a test. The template test also gains a
link that resolves only from the directory of the row using the template,
since stopping ".." at the root made its first link resolve from the root as
well. The tooltip-shape test gains a schema-3 database whose Tooltips still
holds a summary column, which a mutant of that guard passed. The module
docstring now lists what the tests cover.

All 75 mutants (the earlier 49 and 26 of the guards these fixes add) fail at
least one test.

PR #46 review finding R46-06.

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

Copy link
Copy Markdown
Collaborator Author

Response to the review

All six findings are fixed (87b8b62..6c33cac). Each new test failed on 87b8b62 for the reason it is named for.

ID Fixed in Change
R46-01 238a1d4 Drop step reads tooltip links in schema 2 or 3, refuses any other shape before the scan
R46-02 cf65e44 PEP 723 block on the tooltip step; uv run ... --help works
R46-03 dd4424f Chinese-font rule reads /BaseFont and /FontName, as a name, literal or hex string
R46-04 20c7b37 Streams decoded through Flate, ASCII85, ASCIIHex, RunLength chains; unreadable PDFs refuse
R46-05 315366d .. stops at the root when resolving a link
R46-06 6c33cac Tests for the three surviving mutants
  • R46-01: the tooltip step now says it runs before migrate_tooltip_schema.py and refuses a migrated database by name. On 87b8b62 the new tests crashed with no such column: uri.
  • R46-04: image and cross-reference streams are skipped. An encrypted PDF, an unknown filter, a predictor or corrupt data refuses the run by name. endstream no longer reads as the start of a stream. All 6 new cases exited 0 on 87b8b62.
  • Tests: 44 (33 + 11), passing under pytest and as plain scripts. Mutants: 75 of 75 killed (the original 49, two re-targeted where the fixes rewrote the code, plus 26 for the new guards).
  • Real data: on schema 2 the output is identical to the 87b8b62 run (6,368 rows deleted, 1,605 details emptied). On schema 3 (tooltip step, migration, then the drop step) it now runs instead of crashing, deleting the same 6,368 rows. All 15 stored PDFs decode fully, and none needs a CMap.
  • PR body: I corrected the test and mutant counts, the pipeline position and the safety checks, and closed the plugin-books item for the current Bookshelf plugin.

🤖 Generated with Claude Code

https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3

From documentation major version 3, migrate_tooltip_schema.py stores every
template Brotli-compressed against the dictionary, with no marker but the
version. The reference check read the stored bytes as UTF-8, so on a 3.0.0
file it saw no template link at all (1,126 real templates: 0 link-like
strings as stored, 29,210 decoded) and a template linking to a deleted row
was not refused, though the docstring says the step may run after the
migration.

Templates are now decoded with the database's codec when the newest
DocumentationDatabaseVersion row (ordered as the app orders them) declares
major 3 or later, and read as plain text below it or with no version. A
template that does not decode refuses the run, writing nothing.

Four tests: a compressed template's link refuses from version 3, a plain
one's still refuses below it, a template that does not decode refuses, and
a version-3 database with compressed templates deletes the same rows. The
first and third fail on 6c33cac (exit 0).

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

Copy link
Copy Markdown
Collaborator Author

New commit e31aa00 answers R50-05 from the review on #50: from documentation version 3, the step decodes each template before reading its links. A template that doesn't decode refuses the run. Below 3, templates are read as plain text, as before.

  • 4 new tests (48 in all). The two refusal tests fail on 6c33cac. The other two pass on it, as they should: one checks plain templates below 3, the other that a 3.0.0 database still loses the expected rows.
  • 17 new mutants, all killed (92 in all).
  • On the ship-time build, the link-like strings read from templates go from 0 to 29,210. A copy whose page.peb links to an x/ row refuses; before, it exited 0.

The body is updated. #51 is rebased onto this commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3

davidschachterADFA added a commit that referenced this pull request Oct 19, 2026
#46's drop_unread_rows.py says it runs after every step that writes
page content, links or tooltips. This step writes page content, and its
docstring put it after drop_unread_rows.py, which also listed the row
drops among the steps that write a/ pages. The end-to-end build used
that order and verified it, but the two docstrings contradicted each
other.

The docstring and the CLAUDE.md entry now name the order as an
exception to #46's rule, and say why it is safe: every page this step
converts renders back byte for byte, so it links to exactly what the row
drops already checked in it as HTML. The includes do hold links (41,153
rooted ones, 40,991 of them under /a/, and about 15,400 absolute ones in
the ship-time build's), but none that its pages did not hold before. drop_unread_rows.py
reads templates and templated rows' JSON too, so it could also run
after this step. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
CLAUDE.md conflicted where both sides add a script to the list of live
scripts; both entries are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
davidschachterADFA added a commit that referenced this pull request Oct 19, 2026
…-and-unlinked-rows

Its base took origin/main (#52) through #46; CLAUDE.md conflicted where this
branch extends the drop_unread_rows.py entry and the base adds
svg_images_to_webp.py to the same sentence. Both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
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.

2 participants