Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,14 @@ uses. Any future Android/Java tooltip work should target the normalized `Tooltip
- **`scripts/`** — the original CLI toolbox. Live/current: `DocumentationDatabase.py` (Content
ingestion — see whitelist issue above), `ingest.py` (thin CLI over it, used by
`publish-doc-db.yaml`), `TooltipManager.py` (CSV ⇄ normalized-Tooltips round-trip),
`create_empty_database.py`, `list_database_documents.py`, `svg_images_to_webp.py` (ADFA-3305:
re-encodes the raster images SVG rows embed as data: URIs as WebP, image by image, by a quality
rule; run on the shipped copy before the final dictionary re-mint; `compare_svg_renders.py`
renders each changed SVG before and after in headless Chrome, through `<img>` and `<object>`, as its
visual check).
`create_empty_database.py`, `list_database_documents.py`, `drop_unread_rows.py` (deletes Content rows
nothing reads - javadoc Use pages, pdf.js CMaps, pdf.js fallbacks and scripting sandbox, x/
placeholders - after checking that nothing kept refers to them, before or after the schema-3
migration, whose templates it decodes), `clear_repeated_tooltip_detail.py` (empties a tooltip detail
that repeats its summary), `svg_images_to_webp.py` (ADFA-3305: re-encodes the raster images SVG rows
embed as data: URIs as WebP, image by image, by a quality rule; run on the shipped copy before the
final dictionary re-mint; `compare_svg_renders.py` renders each changed SVG before and after in
headless Chrome, through `<img>` and `<object>`, as its visual check).
**Deprecated/dead** (target the
removed `ide_tooltip_table` — see above, kept for reference only): `tooltips.py`,
`import_tooltips.py`, `android_tooltips.py`, `java_tooltips.py`, `android_html_page.py`,
Expand Down
140 changes: 140 additions & 0 deletions scripts/clear_repeated_tooltip_detail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "brotli",
# "Pillow",
# ]
# ///
"""
clear_repeated_tooltip_detail.py

Empties Tooltips.detail where it is the same text as Tooltips.summary.

In the 2026-10-18 database 1,605 tooltips (1,592 Kotlin, 13 Java) repeat their
summary as their detail, 151,274 bytes in all; 25,285 others already have an
empty detail.

What the user sees differently (ToolTipManager.kt, the only reader of detail):
- The first popup shows the summary, as before.
- "See More" shows when the detail is not blank or the tooltip has buttons.
For a tooltip with buttons it still shows, and the second popup now lists
just the buttons' links instead of the summary again, a blank line, then the
links. All 1,605 tooltips in that database have buttons.
- A tooltip with no buttons loses "See More", whose popup held only the
summary again (and the sponsor button every second popup shows). This step
counts such tooltips and says so.
- The info dialog and the feedback e-mail show an empty Detail.

The comparison is exact (Python string equality), whatever collation the
schema gives the columns. Tooltips are not Brotli-compressed, so the shared
dictionary does not need re-minting.

Measured on the 2026-10-18 database after ADFA-6627, packed with
rebuild_database.py --pack Content,Tooltips --page-size 4096: the file is
151,552 bytes smaller; its brotli -q 11 -w 24 is 10,049 bytes smaller, which is
within the 0.2 MB that repacking alone moves it.

Runs in one transaction (rolled back on any error), backs the database up
first, and VACUUMs afterwards. --dry-run reports what would change without
writing. A second run finds nothing to do and exits 0.

Pipeline position: after every step that writes tooltip text, and before
migrate_tooltip_schema.py, which moves summary and detail out of Tooltips into
TooltipTexts (documentation schema 3). This step reads only the schema-2 shape
and refuses a migrated database, writing nothing. rebuild_database.py comes
after both.

uv run scripts/clear_repeated_tooltip_detail.py documentation.db [--dry-run] [--verbose]
"""
from __future__ import annotations

import argparse
import sqlite3
import sys
from collections import Counter
from pathlib import Path

from optimize_db_media import backup_database, human


def run(cfg: dict) -> int:
db_path = cfg["db_path"]
if not db_path.is_file():
print(f"error: {db_path} does not exist", file=sys.stderr)
return 1

conn = sqlite3.connect(db_path)
try:
columns = {row[1] for row in conn.execute("PRAGMA table_info(Tooltips)")}
if conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'TooltipTexts'").fetchone():
print("error: this database's tooltip text is in TooltipTexts (documentation schema 3); run this step "
"before migrate_tooltip_schema.py", file=sys.stderr)
return 1
if not {"id", "categoryId", "tag", "summary", "detail"} <= columns:
print("error: this database has no Tooltips table with id, categoryId, tag, summary and detail",
file=sys.stderr)
return 1
rows = conn.execute(
"SELECT T.id, T.tag, T.summary, T.detail, COALESCE(TC.category, '?'), "
"EXISTS (SELECT 1 FROM TooltipButtons B WHERE B.tooltipId = T.id) "
"FROM Tooltips T LEFT JOIN TooltipCategories TC ON TC.id = T.categoryId ORDER BY T.id").fetchall()
repeated = [row for row in rows if isinstance(row[3], str) and row[3] != "" and row[3] == row[2]]
saved = sum(len(row[3].encode("utf-8")) for row in repeated)
by_category = Counter(row[4] for row in repeated)
no_buttons = [row for row in repeated if not row[5]]

if not repeated:
print(f"Nothing to do: none of {len(rows)} tooltip(s) repeats its summary as its detail.")
return 0
print(f"{len(repeated)} of {len(rows)} tooltip(s) repeat their summary as their detail "
f"({', '.join(f'{category} {count}' for category, count in sorted(by_category.items()))}); "
f"emptying it saves {human(saved)} byte(s).")
if no_buttons:
print(f" {len(no_buttons)} of them have no buttons, so their tooltip loses its See More link, "
"which led only to the same text again.")

if cfg["dry_run"]:
print("Dry run: nothing written.")
return 0
print(f"Backing up {db_path} ...")
print(f"Backup written to {backup_database(db_path)}")
conn.execute("BEGIN")
try:
for tooltip_id, tag, _summary, _detail, category, _buttons in repeated:
conn.execute("UPDATE Tooltips SET detail = '' WHERE id = ?", (tooltip_id,))
if cfg["verbose"]:
print(f" [CLEAR] {category} {tag}")
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

print("Vacuuming database...")
vac = sqlite3.connect(db_path)
try:
vac.execute("VACUUM")
finally:
vac.close()
print(f"\nDone. emptied the detail of {len(repeated)} tooltip(s), saving {human(saved)} byte(s).")
return 0


def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("db_path", type=Path, help="SQLite database to edit in place, e.g. documentation.db")
p.add_argument("--dry-run", action="store_true",
help="Report what would change, without writing or backing up")
p.add_argument("--verbose", action="store_true", help="Log every tooltip changed")
return p


def main() -> None:
args = build_parser().parse_args()
sys.exit(run({"db_path": args.db_path, "dry_run": args.dry_run, "verbose": args.verbose}))


if __name__ == "__main__":
main()
Loading
Loading