Repository navigation
ADFA-3305: Merge duplicate media and drop unlinked rows - #54
davidschachterADFA wants to merge 7 commits into
Conversation
… the kept one A new drop_unread_rows.py group, duplicate-media. Rows of an image, video or audio type with the same type, language and stored bytes (chunks joined) are copies; one of each set is kept: the copy the most rows link to (kept text rows, templates, tooltip buttons and Bookshelf entries), then the shortest path, then the lowest id. Every link to a dropped copy is pointed at the kept one, keeping its own directory prefix and percent-encoding: in kept text rows, templated rows' JSON included, and in tooltip button uris (TooltipButtonUris at schema 3, TooltipButtons at 2). Templates are not rewritten, so a template link to a dropped copy refuses the run, as does any link the rewrite misses and a JSON row that would stop parsing. PDFs are not merged (the Bookshelf placeholder books share their bytes but are separate entries, and deleting one fires DeleteBook), nor text (the only identical text rows are licence notices). On the 2026-10-19 item-B build: 53 copies in 52 sets, 881,962 stored bytes, 98 links in 13 rows pointed at the kept copies. 44 are the Help+Manual zoom images that became identical to their small ones when resized, ADFA-6645's item 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
A new drop_unread_rows.py group, unlinked. UNLINKED names each row and why it is unread; the group deletes those and nothing else, and the reference check still refuses the run if any kept row links to one. The list came from the item-B analysis's 113 candidates (its 41 pdf.js rows are item C and stay). A check sharing no code with the step read every text row, template, tooltip text and button uri of the 2026-10-19 item-B build and kept a candidate out if anything not itself being deleted named it: by file name (other than an absolute URL to another host), by path without the extension, or by file name without the extension. The second rule matters: nav.peb and page.peb build their links from page ids with no ".html" in the data, so 20 of the analysis's 21 Kotlin topic pages are linked from the sidebar after all; and Help+Manual's scripts load files by name. Repeated until nothing changed: 31 rows. None is among the app's own documentation URLs (CodeOnTheGo stage, grepped). Left out by policy as well: the playground pages of WebServer's disabled playground endpoint, the PUCC example data, x.html and x.pdf, the t/ placeholder book, the jQuery licence notices and the two index pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
Measured on the 2026-10-19 item-B build against the same build repacked, both with --pack Content,TooltipTexts --page-size 4096, .br with brotli -q 11 -w 24. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
Independent Claude review (head 5b7f910)On today's data the two groups do what the body says, and nothing they delete or repoint breaks a page, image, script or tooltip.
The two findings are latent: a type the merge rule admits but shouldn't (SVG), and a lookup-table invariant at schema 3.
RG-01. Probe, through the test file's own fixtures:
None of the 53 shipped copies is an SVG, and no shipped SVG has a relative reference (all 196 checked), so nothing changes today. A later build with identical SVGs in two directories would ship this silently. Fix: take RG-02. At schema 3, a rewritten URI is changed in place: Probe, schema 3, buttons on The lookup tables are meant to store each distinct string once. They have no Fix: point the buttons at the existing row's id and delete the old row, or update only when the new string isn't already there, with a schema-3 test. Checked and sound
🤖 Generated with Claude Code |
An SVG is a document: shown through <object> or opened directly, its relative href, xlink:href and url() resolve against its own URL. Merging byte-identical SVGs from two directories therefore changes what the dropped copy's pages load: in the review's probe, a/page.html ended up drawing k/art/pic.webp instead of a/media/pic.webp, and nothing refused. SVGs are left out entirely rather than merged within one directory only: that is simpler, and none of the 53 shipped copies is an SVG, so today's output is unchanged. The new test is the review's probe; it fails on 5b7f910 with the SVG merged. (RG-01) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
At schema 3, TooltipButtonUris stores each distinct uri once, and its writers (PluginTooltipTables.kt, docs/documentation-database.md) look a string up before inserting it. duplicate-media rewrote a dropped copy's uri in place, so when the kept copy's uri already had a row, two rows held the same string. Now the buttons are pointed at the existing row and the old row is deleted; a uri whose new string is not stored yet is still rewritten in place. Schema 2's TooltipButtons.uri is unchanged. The reuse test fails on 5b7f910 with two rows for one string; the in-place test passes there and pins that path. No shipped button uri names a copy, so today's output is unchanged. (RG-02) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
…rges test_copies_ignore_rows_other_groups_delete proved that a row another group deletes is no copy using an SVG, the module graph javadoc-use deletes. Since SVGs are never merged (RG-01), that case no longer reaches the guard, and the mutant that drops `path not in doomed` survived. The test now adds a WebP that unlinked deletes beside an identical WebP elsewhere; without the guard the run deletes the second one too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
…-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
Response to the reviewBoth findings are fixed (5b7f910..84a3126).
🤖 Generated with Claude Code |
ADFA-3305
Two more
drop_unread_rows.pygroups, from the item-B size analysis (G05 and G09):duplicate-mediamerges byte-identical media copies, pointing their links at the kept one, andunlinkeddeletes 31 rows nothing reads, from a closed list. On the 2026-10-19 item-B ship-time build they save 983,040 bytes of file and 259,386 of.br. Stacked on #51, whose step they extend.duplicate-mediaalso does item 2 of ADFA-6645 (the Help+Manual zoom images identical to their small ones), as #52 does its item 3.duplicate-media
http://localhost:6174/) and its percent-encoding. That covers kept text rows, the JSON of templated rows (the Help+Manual toggles'href,src,data-src0anddata-src1), and tooltip button uris (TooltipButtonUrisat schema 3,TooltipButtons.uriat 2). At schema 3, where each distinct uri is stored once, a button whose new uri already has a row is pointed at that row and the old row is deleted; otherwise the uri is rewritten in place.bookshelf/…PDFs andt/textbookPlaceholder.pdf) but are separate entries, and deleting one firesDeleteBook; text rows, since the only identical ones are the jQuery licence notices; and SVGs, because an SVG is a document whose relative references resolve against its own URL, so a copy kept in another directory could load other files. None of the shipped copies is an SVG._zoom25/_zoom10images, 8 area/devsiteimages, andi/images/closehelp.webpgoes in favour ofZoomClose.webp, which two scripts name.unlinked
The list:
UNLINKEDnames each of the 31 rows and why it is unread. The group deletes those and nothing else, and the reference check still refuses the run if any kept row links to one.How it was made: from the analysis's 113 candidates (its 41 pdf.js rows are item C and stay). A check that shares no code with the step read every text row, template, tooltip text and button uri of the item-B build. It kept a candidate out if anything not itself being deleted named it:
It repeated that until nothing changed. None of the 31 is among the app's own documentation URLs (CodeOnTheGo
stage, grepped).Why the stricter rules:
nav.pebandpage.pebbuild their links as/{{ node.id }}.htmlfrom page ids with no.htmlin the data. So 20 of the analysis's 21 Kotlin topic pages are linked from the sidebar after all, and the 21st fromkotlin-brand-assets.html. Help+Manual'shm_webhelp.jsloads its scripts and images by name. ItsEmbedstrings arehmWebHelp.doFullEmbedWindow, not a load ofhmEmbedHelp.js.What goes:
GoogleFonts.cssstub ADFA-4480 left;doc-filesimages no page shows;overview-summary.htmlredirect stub, jQuery 3.6.1 (the pages load 3.7.1),jdk-default.css, the JDWP spec page andjava_keywords.html;package-listfiles;k/not-found-version.html.Left out: 65 candidates the check could not prove unread, among them the sidebar's Kotlin pages, the Help+Manual scripts and images loaded by name, and
k/navigation.html. Also left out by policy:x.htmlandx.pdf, which ADFA-3305: Drop documentation rows nothing reads, and tooltip details that repeat the summary #46 keeps;t/placeholder book;Real data (the 2026-10-19 item-B ship-time build, 85,708,800 bytes)
Each result and the baseline packed with
rebuild_database.py --pack Content,TooltipTexts --page-size 4096(#38) and compressed withbrotli -q 11 -w 24. The baseline, the same build repacked, is 85,708,800 bytes,.br67,290,641..brsavedThe independent check (no code shared with the step) passes on the both-groups run:
It catches a run without
unlinkedand a row with one link left broken.Re-runs: a dry run of all seven groups on the result reports nothing to do.
The whole ship-time order with this step in place of ADFA-3305: Point javadoc's Index links at the split index; drop index-all.html #51's, from the 2026-10-18 database: 84,828,160 bytes,
.br67,163,662. The step found the same 53 copies and 31 rows there, early in the order, and refused nothing. The re-mint verified 22,720 of 22,720 items and 1,126 of 1,126 templates. Against the item-B build (85,708,800 and 67,133,725) that is 880,640 bytes less file but 29,937 more.br: the re-mint kept another seed (20822, not 20819), whose compressed rows store 27,790 bytes more, so the two full builds differ by more than these groups. The table above, on one dictionary, is the like-for-like measure.Tests
70 tests (18 new), passing under pytest (Python 3.11 and 3.14) and as a plain script. The first 15 fail on #51's head, where neither group exists, so the mutants carry the proof for them:
duplicate-media: 28 mutants, 26 killed. They cover:
The 2 survivors are equivalent: each removes a prefilter that skips rows not naming a copy.
unlinked: 4 mutants (selecting by prefix, by file name, nothing, or losing a path), 4 killed.
The review fixes add 3 tests and strengthen 1:
pic.webp, stay unmerged. This fails on 5b7f910, which merges them;unlinkeddeletes beside an identical WebP elsewhere. Its SVG case stopped reaching the guard once SVGs were excluded.Mutants after the fixes: the review's 25 (one adjusted to the changed line) and 10 of the fixes, 35 in all: 34 killed. The survivor,
MAX(id)instead ofMIN(id)in the uri lookup, is equivalent, sinceTooltipButtonUrisholds each string once.Real data, after the review fixes
On a copy of the item-B build, the fixed step deletes the same 84 rows and points the same 98 links in 13 rows, and the result is byte-identical to 5b7f910's (sha256 e3f59f64...). No shipped copy is an SVG, and no shipped button uri names a copy. A dry run on the result has nothing to do.
Pipeline position
Unchanged from #46: after the steps that write page content, links or tooltips, then
rebuild_database.py --pack, which must follow. Both rewrites change row sizes, which loosens a packed file.Not covered
a/devsitepages show the kept copy of each identical image.🤖 Generated with Claude Code
https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3