Skip to content

Commit 46235fb

Browse files
alexmmillerclaude
andcommitted
Fix the 14 findings from Hal's second PR #24 review
Several of these are regressions from the previous round's fixes; the review grouped them by root cause, and both groups are addressed at that level rather than site by site. Group 1 - one chunking protocol, four implementations (F01/F02/F08/F13) WebServer.kt reassembles a page by reading `path`, and if that blob is exactly CHUNK_SIZE, appending `path-1`, `path-2`, ... until a fragment comes back shorter. Two rules follow: a "<base>-<N>" row is a continuation only if the base is exactly CHUNK_SIZE (a base merely existing proves nothing - "X" and "X-1" are legal as unrelated pages), and a short fragment terminates the chain. Both were being re-derived per call site, each wrong differently. New content_chunking.py owns both rules and distinguishes the two questions they answer: owned_fragment_paths (ownership - what a delete or replace must take, including an orphaned tail past a gap) and served_fragment_paths / reassemble (what the server actually concatenates). populate_db, insert_optimized_media, migrate_content_to_dictionary_brotli, renumber_misnumbered_fragments and sync_kdoc_json_to_db all now call it; the two duplicate CHUNK_SIZE definitions and eight ad-hoc fragment helpers are gone. * F01 unmatched_source_pages was handed {row[0] ...} - integer Content ids - so the membership test never matched and every source page was reported unmatched. all_paths, already computed 38 lines above, is what it wanted. * F02 write_content ran an unindexed "LIKE '<path>-%'" scan per updated row (~1e9 comparisons across kotlin-stdlib) for a lookup that can only return rows when the base is CHUNK_SIZE. Now gated on that first. * F08 find_fragment_paths treated "base exists" as sufficient, so an ordinary page at guide.html hid a genuinely misnumbered chain at guide.html-1 from the tool written to repair it. Same defect fixed in sync's is_fragment_path. * F13 reassemble_content had dropped the short-fragment terminator, so on a gapped chain it rebuilt a blob the server never serves - which rewrite_pages would then re-compress and store. Discovering a chain now depends on the base row's length, so three call sites that deleted the base before resolving its chain had to be reordered. Group 2 - optimize_media's name-planning pre-pass (F04/F05/F10) The pre-pass planned from source-side attributes and never reconciled that against the predicted output name or whether the source was readable. * F04 keyed collisions on (parent, stem), but insert_optimized_media addresses images by bare basename - so sub-a/logo.png and sub-b/logo.jpg both became logo.webp, one image lost, one page rendering the wrong picture. * F05 the mirror image: it renamed pairs that could not collide (no --webp means no extension rewrite), churning stored URLs for nothing. * F10 "not is_dir()" keeps broken symlinks, and process_file stat()'d outside its try, so one dangling link killed the run with an unhandled traceback. Now de-conflicted on possible_output_names() keyed by basename alone, and collect_sources() drops unreadable entries while stat() moved inside the try. Remaining findings * F03 the previous round's comment was wrong: nav.peb branches on `{% if node.id %}` and renders a *coloured link*, not a non-link, so the sidebar entry for an unconverted *.topic still 404'd. Clearing the id is what actually fixes it, and drops it from the pager for free. Verified on the live corpus: one such node (api-references), zero nav ids now without a Content row. * F06 the strict= dictionary lookup ran inside the import's own write transaction, so a self-inflicted lock aborted the whole import. Resolved and cached before the write connection opens. * F07 the INT/TERM/HUP handler restored state but never exited, so Ctrl-C carried on into the rest of the script and the EXIT trap then re-ran a cp of an already-removed file. Handlers exit 130; restore is idempotent. * F09 sidebar.js never checked response.ok, so a 404/500 body was injected and rendered as the navigation. * F11 the act script still documented "build complete" as gated on dry_run after the workflow moved to if: always(). * F12 all three workflows moved off pip to the repo's uv convention (a job-level $UV_RUN mirroring run_e2e_pipeline_test.sh); the dead "pip install markdown-it-py" is gone, it having been in requirements.txt. * F15 is_contiguous_from_one was dead after find_chains was rewritten. F14 (add check-tools to CI) is NOT done, deliberately: 6 of its 13 tests already fail on main, in code this PR doesn't touch, so the step would make every PR red for an unrelated reason. The header now says so and carries the one-liner to add once they pass. 11 new regression tests over the inputs the review identified; the 5 behavioural ones fail against the pre-fix code. 181 + 189 pass. The local workflow runs green end-to-end under act on the uv conversion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e6786a8 commit 46235fb

17 files changed

Lines changed: 713 additions & 259 deletions

‎.github/workflows/build-kotlin-docs-local.yaml‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ jobs:
138138
runs-on: ubuntu-latest
139139
timeout-minutes: 180
140140
env:
141+
# The repo's uv convention, as one string so every step below reads the
142+
# same: `$UV_RUN python3 <script>`. Mirrors run_e2e_pipeline_test.sh's
143+
# own UV_RUN array. Expanded unquoted on purpose - it is a fixed literal
144+
# with no paths or user input in it.
145+
UV_RUN: uv run --with-requirements requirements.txt --
141146
KOTLIN_WEB_SITE_REF: ${{ inputs.kotlin_web_site_ref }}
142147
KOTLIN_REF: ${{ inputs.kotlin_ref }}
143148
DB_PATH: ${{ inputs.db_path }}
@@ -195,6 +200,9 @@ jobs:
195200
with:
196201
python-version: '3.11'
197202

203+
- name: Set up uv
204+
uses: astral-sh/setup-uv@v5
205+
198206
- name: Set up JDK (for the kdoc-to-json / kotlin-stdlib-docs Gradle builds)
199207
uses: actions/setup-java@v4
200208
with:
@@ -213,12 +221,15 @@ jobs:
213221
# no Python binding exposes a custom dictionary (ADFA-5153).
214222
sudo apt-get install -y pngquant unzip sqlite3 brotli
215223
216-
- name: Install Python dependencies
217-
run: |
218-
pip install -r requirements.txt
219-
# markdown-it-py: ProcessKotlinWebsiteJSON's own requirement (see
220-
# its README); scour/cairosvg are in requirements.txt already.
221-
pip install markdown-it-py
224+
# No install step: every Python invocation below runs through $UV_RUN
225+
# (see the job env), which resolves requirements.txt into a cached
226+
# ephemeral environment per call. That is this repo's convention - uv,
227+
# never pip or the `uv pip` shim - and is what ProcessKotlinWebsiteJSON's
228+
# README and run_e2e_pipeline_test.sh already document and use.
229+
#
230+
# The former "pip install markdown-it-py" here was dead work: it is in
231+
# requirements.txt (markdown-it-py>=2.0) and was already installed by the
232+
# line above it. scour/cairosvg are in there too.
222233

223234
- name: Copy documentation.db from local disk
224235
run: |
@@ -252,7 +263,7 @@ jobs:
252263
- name: 'Step 1/5: find_missing_assets.py (source QA report)'
253264
if: ${{ !inputs.skip_website_docs }}
254265
run: |
255-
python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/find_missing_assets.py \
266+
$UV_RUN python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/find_missing_assets.py \
256267
kotlin-web-site/docs missing-assets-report.md
257268
258269
- name: Write missing-assets report to output_dir
@@ -265,7 +276,7 @@ jobs:
265276
# BLACKLISTED_ELEMENT_TITLES is defined once in this job's env: block
266277
# (ADFA-4737); the verification step below reads the same variable.
267278
mapfile -t BLACKLIST <<< "$BLACKLISTED_ELEMENT_TITLES"
268-
python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/populate_db.py \
279+
$UV_RUN python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/populate_db.py \
269280
kotlin-web-site/docs \
270281
ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/config.json \
271282
webHelpImages.zip \
@@ -282,7 +293,7 @@ jobs:
282293
"INSERT OR IGNORE INTO ContentTypes (value, compression) VALUES ('image/webp', 'brotli');"
283294
mkdir -p media
284295
unzip -q webHelpImages.zip -d media
285-
python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/insert_optimized_media.py \
296+
$UV_RUN python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/insert_optimized_media.py \
286297
media documentation.db \
287298
--jpeg-quality 85 --webp --webp-quality 90 --verbose
288299
@@ -308,12 +319,12 @@ jobs:
308319
- name: 'Step 5/5: sync_kdoc_json_to_db.py (overwrite kotlin-stdlib/-reflect/-test content)'
309320
if: ${{ !inputs.skip_stdlib_docs }}
310321
run: |
311-
python3 scripts/sync_kotlin_stdlib_docs/sync_kdoc_json_to_db.py \
322+
$UV_RUN python3 scripts/sync_kotlin_stdlib_docs/sync_kdoc_json_to_db.py \
312323
"${{ steps.stdlib_docs.outputs.all_libs_dir }}" --db documentation.db
313324
314325
- name: Summary
315326
run: |
316-
python3 - documentation.db <<'PYEOF'
327+
$UV_RUN python3 - documentation.db <<'PYEOF'
317328
import sqlite3
318329
import sys
319330
@@ -339,7 +350,7 @@ jobs:
339350
# applied - read from the job env rather than restated here, so this
340351
# check can't silently verify a different list than the one used.
341352
mapfile -t BLACKLIST <<< "$BLACKLISTED_ELEMENT_TITLES"
342-
python3 - ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON kotlin-web-site/docs documentation.db \
353+
$UV_RUN python3 - ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON kotlin-web-site/docs documentation.db \
343354
"${BLACKLIST[@]}" <<'PYEOF'
344355
import sqlite3
345356
import sys

‎.github/workflows/build-kotlin-docs.yaml‎

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ jobs:
124124
runs-on: ubuntu-latest
125125
timeout-minutes: 180
126126
env:
127+
# The repo's uv convention, as one string so every step below reads the
128+
# same: `$UV_RUN python3 <script>`. Mirrors run_e2e_pipeline_test.sh's
129+
# own UV_RUN array. Expanded unquoted on purpose - it is a fixed literal
130+
# with no paths or user input in it.
131+
UV_RUN: uv run --with-requirements requirements.txt --with google-api-python-client --with google-auth-httplib2 --with google-auth-oauthlib --
127132
KOTLIN_WEB_SITE_REF: ${{ inputs.kotlin_web_site_ref }}
128133
KOTLIN_REF: ${{ inputs.kotlin_ref }}
129134
DB_FILE_ID_SECRET: ${{ secrets.GOOGLE_DRIVE_FILE_ID }}
@@ -180,6 +185,9 @@ jobs:
180185
with:
181186
python-version: '3.11'
182187

188+
- name: Set up uv
189+
uses: astral-sh/setup-uv@v5
190+
183191
- name: Set up JDK (for the kdoc-to-json / kotlin-stdlib-docs Gradle builds)
184192
uses: actions/setup-java@v4
185193
with:
@@ -198,15 +206,15 @@ jobs:
198206
# no Python binding exposes a custom dictionary (ADFA-5153).
199207
sudo apt-get install -y pngquant unzip zip sqlite3 brotli
200208
201-
- name: Install Python dependencies
202-
run: |
203-
pip install -r requirements.txt
204-
# markdown-it-py: ProcessKotlinWebsiteJSON's own requirement (see
205-
# its README); scour/cairosvg are in requirements.txt already.
206-
# google-api-python-client & friends: Drive download/upload, same
207-
# libraries check-tools/download_database.py already depends on.
208-
pip install markdown-it-py \
209-
google-api-python-client google-auth-httplib2 google-auth-oauthlib
209+
# No install step: every Python invocation below runs through $UV_RUN
210+
# (see the job env), which resolves requirements.txt into a cached
211+
# ephemeral environment per call. That is this repo's convention - uv,
212+
# never pip or the `uv pip` shim - and is what ProcessKotlinWebsiteJSON's
213+
# README and run_e2e_pipeline_test.sh already document and use.
214+
#
215+
# The former "pip install markdown-it-py" here was dead work: it is in
216+
# requirements.txt (markdown-it-py>=2.0) and was already installed by the
217+
# line above it. scour/cairosvg are in there too.
210218

211219
- name: Authenticate to Google Cloud using Workload Identity Federation
212220
uses: google-github-actions/auth@v2
@@ -218,7 +226,7 @@ jobs:
218226
219227
- name: Download current documentation.db from Google Drive
220228
run: |
221-
python3 check-tools/download_database.py "$DB_FILE_ID" documentation.zip
229+
$UV_RUN python3 check-tools/download_database.py "$DB_FILE_ID" documentation.zip
222230
unzip -o documentation.zip
223231
if [ ! -f documentation.db ]; then
224232
found="$(find . -maxdepth 2 -name documentation.db | head -n1)"
@@ -258,12 +266,12 @@ jobs:
258266
# download_database.py is a generic Drive-file-by-ID downloader
259267
# despite its name - reused here rather than duplicating the
260268
# WIF/Drive-API download logic for a second file type.
261-
python3 check-tools/download_database.py "$IMG_FILE_ID" webHelpImages.zip
269+
$UV_RUN python3 check-tools/download_database.py "$IMG_FILE_ID" webHelpImages.zip
262270
263271
- name: 'Step 1/5: find_missing_assets.py (source QA report)'
264272
if: ${{ !inputs.skip_website_docs }}
265273
run: |
266-
python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/find_missing_assets.py \
274+
$UV_RUN python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/find_missing_assets.py \
267275
kotlin-web-site/docs missing-assets-report.md
268276
269277
- name: Upload missing-assets report
@@ -279,7 +287,7 @@ jobs:
279287
# BLACKLISTED_ELEMENT_TITLES is defined once in this job's env: block
280288
# (ADFA-4737); the verification step below reads the same variable.
281289
mapfile -t BLACKLIST <<< "$BLACKLISTED_ELEMENT_TITLES"
282-
python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/populate_db.py \
290+
$UV_RUN python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/populate_db.py \
283291
kotlin-web-site/docs \
284292
ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/config.json \
285293
webHelpImages.zip \
@@ -296,7 +304,7 @@ jobs:
296304
"INSERT OR IGNORE INTO ContentTypes (value, compression) VALUES ('image/webp', 'brotli');"
297305
mkdir -p media
298306
unzip -q webHelpImages.zip -d media
299-
python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/insert_optimized_media.py \
307+
$UV_RUN python3 ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/insert_optimized_media.py \
300308
media documentation.db \
301309
--jpeg-quality 85 --webp --webp-quality 90 --verbose
302310
@@ -322,12 +330,12 @@ jobs:
322330
- name: 'Step 5/5: sync_kdoc_json_to_db.py (overwrite kotlin-stdlib/-reflect/-test content)'
323331
if: ${{ !inputs.skip_stdlib_docs }}
324332
run: |
325-
python3 scripts/sync_kotlin_stdlib_docs/sync_kdoc_json_to_db.py \
333+
$UV_RUN python3 scripts/sync_kotlin_stdlib_docs/sync_kdoc_json_to_db.py \
326334
"${{ steps.stdlib_docs.outputs.all_libs_dir }}" --db documentation.db
327335
328336
- name: Summary
329337
run: |
330-
python3 - documentation.db <<'PYEOF'
338+
$UV_RUN python3 - documentation.db <<'PYEOF'
331339
import sqlite3
332340
import sys
333341
@@ -353,7 +361,7 @@ jobs:
353361
# applied - read from the job env rather than restated here, so this
354362
# check can't silently verify a different list than the one used.
355363
mapfile -t BLACKLIST <<< "$BLACKLISTED_ELEMENT_TITLES"
356-
python3 - ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON kotlin-web-site/docs documentation.db \
364+
$UV_RUN python3 - ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON kotlin-web-site/docs documentation.db \
357365
"${BLACKLIST[@]}" <<'PYEOF'
358366
import sqlite3
359367
import sys
@@ -408,7 +416,7 @@ jobs:
408416
- name: Upload updated database to Google Drive
409417
if: ${{ !inputs.dry_run }}
410418
run: |
411-
python3 - <<'PYEOF'
419+
$UV_RUN python3 - <<'PYEOF'
412420
import os
413421
from google.auth import default
414422
from googleapiclient.discovery import build

‎.github/workflows/python-tests.yaml‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ name: Python tests
1313
# The suites are separate because their dependencies are: docdb-studio and
1414
# check-tools each own a pyproject.toml + uv.lock, while ProcessKotlinWebsiteJSON
1515
# runs against the root requirements.txt.
16+
#
17+
# check-tools is NOT run here, deliberately. Its suite covers the health checker
18+
# behind the daily docdb-regression-test.yaml cron and genuinely ought to be
19+
# wired up - but 6 of its 13 tests already fail on main
20+
# (test_db_health_checker.py: the checker reports no issues where the tests
21+
# expect one), in code this PR doesn't touch. Adding the step as-is would make
22+
# every PR red for an unrelated reason. Fixing those belongs in its own change;
23+
# adding the step is a one-liner once they pass:
24+
# - name: check-tools
25+
# working-directory: check-tools
26+
# run: uv run --frozen -- python -m pytest -q
1627

1728
on:
1829
push:
@@ -51,20 +62,18 @@ jobs:
5162
# shared dictionary the migration tests need.
5263
sudo apt-get install -y pngquant brotli zstd
5364
54-
- name: Install Python dependencies
55-
run: |
56-
pip install -r requirements.txt pytest
65+
- name: Set up uv
66+
uses: astral-sh/setup-uv@v5
5767

5868
- name: 'ProcessKotlinWebsiteJSON + sync_kotlin_stdlib_docs'
5969
run: |
60-
python -m pytest \
61-
ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON \
62-
scripts/sync_kotlin_stdlib_docs \
63-
-q
64-
65-
- name: Install uv
66-
uses: astral-sh/setup-uv@v5
70+
uv run --with-requirements requirements.txt --with pytest -- \
71+
python -m pytest \
72+
ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON \
73+
scripts/sync_kotlin_stdlib_docs \
74+
-q
6775
6876
- name: docdb-studio
6977
working-directory: docdb-studio
7078
run: uv run --frozen -- python -m pytest -q
79+

‎Dokka-plugin-kdoc2json/scripts/kotlin/build-stdlib-json-docs.sh‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,11 @@ log "==> [1/2] Building and publishing a fresh copy of the kdoc-to-json plugin..
122122
log "==> Installing kdoc-to-json-enabled build.gradle.kts into $STDLIB_DOCS_DIR"
123123
ORIGINAL_BUILD_GRADLE="$(mktemp)"
124124
cp "$STDLIB_DOCS_DIR/build.gradle.kts" "$ORIGINAL_BUILD_GRADLE"
125+
# Idempotent: the EXIT trap fires after a signal handler has already run, so
126+
# this executes twice on Ctrl-C. Without the guard the second pass tries to cp
127+
# the mktemp file the first pass removed, which fails under `set -e`.
125128
restore_build_gradle() {
129+
[ -f "$ORIGINAL_BUILD_GRADLE" ] || return 0
126130
cp "$ORIGINAL_BUILD_GRADLE" "$STDLIB_DOCS_DIR/build.gradle.kts"
127131
rm -f "$ORIGINAL_BUILD_GRADLE"
128132
}
@@ -131,7 +135,15 @@ restore_build_gradle() {
131135
# build.gradle.kts sitting in the developer's kotlin clone and orphaned the
132136
# mktemp copy - contradicting this script's promise above that the checkout is
133137
# left exactly as it was found.
134-
trap restore_build_gradle EXIT INT TERM HUP
138+
#
139+
# The signal handlers must also *exit*. A handler that only restores returns,
140+
# and bash resumes at the next statement - so Ctrl-C would carry on into the
141+
# rest of the script with the original build.gradle.kts back in place, running
142+
# Gradle without the kdoc-to-json plugin and then reporting a baffling
143+
# "expected output ... but it wasn't created". 130 is the conventional
144+
# 128+SIGINT exit code.
145+
trap restore_build_gradle EXIT
146+
trap 'restore_build_gradle; exit 130' INT TERM HUP
135147
cp "$SCRIPT_DIR/build.gradle.kts" "$STDLIB_DOCS_DIR/build.gradle.kts"
136148

137149
log "==> [2/2] Generating JSON documentation via kdoc-to-json (dokka $DOKKA_VERSION)..."

‎ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/assets/sidebar.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@
113113
if (navSrc) {
114114
fetch(navSrc)
115115
.then(function (response) {
116+
// A 404/500 body is still a successful fetch, and its text is HTML or
117+
// plain prose. Without this check that error page gets injected into
118+
// <aside id="sidebar"> and rendered as the navigation, with nothing in
119+
// the console - the .catch below only fires for network-level
120+
// failures. Reachable whenever the nav row is missing: populate_db not
121+
// run, a partially-migrated database, a decompression failure.
122+
if (!response.ok) {
123+
throw new Error("HTTP " + response.status + " " + response.statusText);
124+
}
116125
return response.text();
117126
})
118127
.then(function (html) {

0 commit comments

Comments
 (0)