Skip to content
Merged
27 changes: 25 additions & 2 deletions .claude/hooks/session-start.sh
100755 → 100644
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,7 +48,27 @@ fi

if [ -x "$NODE_BIN/node" ]; then
export PATH="$NODE_BIN:$PATH"
echo "export PATH=\"$NODE_BIN:\$PATH\"" >> "$CLAUDE_ENV_FILE"
# CLAUDE_ENV_FILE and CLAUDE_PROJECT_DIR below are set by Claude Code when this
# runs as a hook, and unset when a human or agent runs it by hand. Under
# `set -u` an unguarded expansion aborts the script — and it aborts *here*,
# after the tarball has downloaded and after PATH is exported only inside this
# soon-to-exit child process, but before that PATH can be persisted for the
# caller and before the install below. The operator sees a failure and cannot
# tell the download actually succeeded.
#
# That matters because manual invocation is not a hypothetical: SessionStart
# hooks do not re-fire when a long-lived session re-bases onto a newer main, so
# a session that predates this file acquires it without ever running it, stays
# on the container's older Node, and then fails `check:runtime` — the first
# step of verify:pr-local — for every diff. Running this script is the remedy,
# so it has to work when run.
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo "export PATH=\"$NODE_BIN:\$PATH\"" >> "$CLAUDE_ENV_FILE"
else
echo "[session-start] CLAUDE_ENV_FILE is unset (run by hand?); PATH is active only inside this hook process."
echo "[session-start] To keep it in your invoking shell, run this command there:"
echo "export PATH=\"$NODE_BIN:\$PATH\""
fi
fi

if ! supported_runtime "$(node -v 2>/dev/null | sed -E 's/^v//' || true)"; then
Expand All@@ -57,7 +77,10 @@ fi

echo "[session-start] Using node $(node -v) / npm $(npm -v)"

cd "$CLAUDE_PROJECT_DIR"
# Fall back to the repository this script lives in, derived from its own path
# rather than from the caller's cwd, so a manual run installs into the right tree
# from anywhere.
cd "${CLAUDE_PROJECT_DIR:-"$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"}"
# npm ci keeps the lockfile untouched (npm install rewrites peer/optional
# metadata and dirties the worktree). A bare "node_modules exists" check is not
# enough: a cached container keeps stale node_modules after dependency-bumping
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-13 | claude/viewer-ledger-truth-pass | 648b86ad079e563c7be830ef07dedf63a1ff91a5 | document-viewer ledger truth pass: queue five inbox requests (crop-overlay row, #278 done, #215 restated, #280 third acceptance item, stale-runtime provisioning gap) plus one plan-doc correction | PR #1930 opened. Inventory of remaining document-viewer work found four ledger rows stating things no longer true; every claim re-verified against main 2d270392 rather than the four-day-stale base the inventory began on. Crop to page overlay had NO row at all despite being the one unbuilt Phase 3 capability - its geometry is SELECTed at document-detail.ts and dropped before DocumentDetailImage and ImageRow. #294 and #283 left alone as deliberate deferrals. Queued as inbox requests under the new intake contract, canonical ledger untouched; an earlier attempt on the stale base had allocated #295, which is now taken on main by an unrelated row - reconciliation assigns ids instead. Opened from a fresh branch with user agreement: the designated branch holds dead #1777 history, force-push was blocked by check:ledger-write-discipline diffing against that stale tip, and remote branch deletion is refused by this session's transport. | verify:pr-local COMPLETE, failed: (none) - all 11 selected checks passed (check:runtime, installed-lock-parity, format:changed, sitemap:check, four docs checks, branch-review-ledger, outstanding-issues, ledger-write-discipline). Docs-only scope so build/lint/typecheck/unit/RAG skipped by risk routing, confirmed via --dry-run first. Gate only became runnable after provisioning Node 24.19.0 by hand - queued as its own finding. |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-13 | PR #1930 / claude/viewer-ledger-truth-pass | 2ac0f48a820be62947112efbb5d0845a702dad8e | heavy review-and-fix: ledger truth, SessionStart runtime remediation, and focused tests | Fixed three confirmed P2 review findings and one additional P2 test-isolation defect. Corrected bbox runtime-flow and #215 completion claims; completed caller-persistent Node remediation; removed the resolved queued runtime issue; no P0/P1 findings. Remaining viewer and ledger requests were source-validated. | bash -n; focused Node/shell regression harness PASS; TypeScript syntax PASS under Node strip-types; JSON parse PASS; distinct manual adversarial pass; full Node 24 npm gates deferred to exact-head CI because no authenticated checkout or compatible local runtime |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"id": "3f1a672f-9038-4a36-897f-5286e4fa028d",
"createdOn": "2026-08-13",
"action": "add",
"payload": {
"pri": "P3",
"type": "task",
"summary": "Crop-to-page overlay remains unbuilt; bbox already reaches viewer state at runtime but is untyped, unvalidated, and unused",
"detail": "**Outcome:** selecting an indexed table or diagram can highlight its region on the PDF page, or the capability is deliberately retired — either way it stops living only in a plan document. **Detail:** this is the one Phase 3 capability never built (docs/plans/document-viewer-redesign-plan.md, Phase 3 table, 'Out of scope'). It had no ledger row until now, which is how work disappears between sessions: the plan doc marks it out of scope and nothing in durable memory says it remains owed. **The data path is partially live, not dropped.** src/lib/document-detail.ts SELECTs bbox alongside the other image columns, and withImageTableMetadata spreads every selected field except metadata. bbox therefore survives the runtime response and reaches DocumentViewer's image state. The gap is static and behavioural: DocumentDetailImage in src/lib/document-detail-contract.ts does not declare bbox, ImageRow in src/components/document-viewer/types.ts aliases that contract, no normalisation validates the stored value, and no viewer code renders it. Verified against exact PR head 2ac0f48a820be62947112efbb5d0845a702dad8e on 2026-08-13. **Shape of the work, in order:** (1) establish the ingestion coordinate space and stored shape, add a normalised bbox field to DocumentDetailImage, and add a focused loader or route-serialization test proving bbox survives with the promised shape. Do not change the selected-field mapping unless that test demonstrates an actual loss. (2) Only then draw the highlight over the rendered page when a figure is selected, accounting for the virtualized page column, the per-page raster scale from resolveViewportScale, and rotation. **Why it was scoped out rather than overlooked:** the contract and normalisation work has a wider blast radius than the component-only Phase 3 diff, and crop geometry quality from ingestion is separate debt — the redesign plan's residual-risk section says not to block viewer UX on perfect crops. **Stop:** do not land the typed-contract and normalisation half inside a viewer-only PR; it changes what the document-detail API promises and needs its own review and governance preflight. Do not render raw, unvalidated bbox values — a highlight over the wrong region of a clinical source is worse than no highlight.",
"source": "session 2026-08-13 document-viewer remaining-work inventory; docs/plans/document-viewer-redesign-plan.md Phase 3 table; src/lib/document-detail.ts bbox projection"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
{
"version": 1,
"id": "89ac3ee8-550a-44aa-b411-88687e5935e4",
"createdOn": "2026-08-13",
"action": "update",
"payload": {
"id": "#280",
"detail": "**Outcome:** the two phone-only viewer fixes are confirmed on the device class they were written for. **Detail:** the viewer-optimisation branch revives pinch-to-zoom in fit mode (it was gated off in the default state, so a pinch reached neither the viewer nor the browser) and adds a canvas pixel budget so WebKit stops blanking the page above roughly 2.3x zoom on a dpr-3 display. Neither is verifiable in this container (see the Chromium/pdf.js row) and neither is a Chromium behaviour anyway — the canvas ceiling is a WebKit limit and the touch-action contention is a Safari gesture question. **Next:** on a real iPhone, in Safari and in the installed PWA: pinch a freshly opened document and confirm it zooms without first tapping a control; zoom to maximum and confirm the page stays painted rather than going blank; confirm a pinch that drifts vertically is not cancelled mid-gesture by the holder's 'touch-action: pan-y' (the mitigation if it is, is switching touch-action to none while two pointers are down — the gesture hook already tracks pointer count and exposes 'pinching'). Record the result against docs/phone-chrome-physical-acceptance.md. **Stop:** do not re-gate pinch on '!fitWidth' to resolve a gesture-contention finding — that restores the original defect. **THIRD ACCEPTANCE ITEM ADDED 2026-08-13 (document viewer Phase 3, PR #1772).** Multi-page documents now read as one continuous column inside a bounded reading pane: pdf-canvas-viewer.tsx gives the holder max-h-[72vh] sm:max-h-[80vh] when totalPages > 1 and not fullscreen, so the holder becomes the scroller, and its overscroll changes from overscroll-contain to overscroll-x-contain precisely so vertical scroll chains OUT of the pane at its ends rather than trapping the reader. Single-page documents keep their previous geometry exactly and need no re-check. A nested vertical scroller inside a page is a known iOS hazard and no Chromium gate says anything about it, so it belongs on this same device pass. **On a real iPhone, in Safari and the installed PWA:** open a multi-page document (the 2-page synthetic clozapine demo doc, or any real guideline), confirm pages scroll inside the pane, and confirm that reaching its top or bottom continues scrolling the page rather than dead-ending. **Stop:** if it does trap, do not fix it by removing the pane — the pane is what makes a long guideline readable; adjust the overscroll behaviour or the pane height instead."
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"id": "9ae893ef-7073-4dfa-8602-899d53b177b7",
"createdOn": "2026-08-13",
"action": "add",
"payload": {
"pri": "P3",
"type": "rec",
"summary": "A queued update request can silently clobber a row that changed after the request was written",
"detail": "**Outcome:** the inbox cannot apply a stale rewrite over someone else's newer content without anyone noticing. **Detail:** the inbox intake fixed ID allocation — ids are assigned at reconciliation, so two branches can no longer collide on a number, which was the sharper of the two hazards. It does not address content staleness. An 'update' request carries a full replacement '--detail' string written against whatever the author read at queue time; reconciliation applies it verbatim. If the target row changed on main between queueing and reconciling, the newer content is overwritten with no signal. The multiple-pending-mutations guard does not catch this: it fires only when two requests target the same id, not when one request is simply old. **Live near-miss, 2026-08-13:** a document-viewer ledger pass was drafted against a base four days stale, and its '#215' restatement was composed from that stale reading. It was caught only because the author re-read every row against current main before queueing — a discipline, not a gate. The same pass had already had to discard a directly-allocated '#295' because main had since claimed it; that half is now structurally impossible, this half is not. **Next:** consider fingerprinting the target row at queue time — the request schema is versioned ('version: 1'), so a 'baseRow' hash could be added to add/update/done payloads and compared at reconcile, refusing (or requiring an explicit override) when the row moved underneath. Weigh against just documenting the re-read discipline: this costs a schema bump plus writer, reconcile and self-test changes, and the failure needs a multi-day-stale base to bite. **Stop:** do not make reconciliation merge or three-way-diff detail text — a replacement that silently becomes a merge is harder to reason about than one that refuses.",
"source": "session 2026-08-13 document-viewer ledger truth pass, PR #1930; scripts/ledger-inbox.mjs request schema"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
{
"version": 1,
"id": "9db75fae-07e6-454d-b2c4-eb88b0e8ff06",
"createdOn": "2026-08-13",
"action": "done",
"payload": {
"id": "#278",
"outcome": "Resolved by PR #1743 (65cd909f, 'mask the pinned chrome in the document-viewer baseline, and add a baseline adopt helper'). Confirmed by inspection against main 2d270392 on 2026-08-13: tests/ui-visual-baseline.spec.ts carries mask: [\"[data-document-sticky-header]\", \".document-viewer-composer\"] on the document-viewer target, which is the second of the two remedies this row proposed. The header selector is the document-specific data attribute rather than .edge-glass-header, because the universal search header also carries that class and would have kept the fail-loud mask guard green after a DocumentViewer rename. The clip was NOT narrowed and fullPage was NOT used — the latter correctly avoided because ledger #093 leaves a hidden duplicate page root under CI load. Residual, deliberately not reopened: a mask is a hole in the gate, so those two elements' geometry is now covered only by the phone-chrome contracts in docs/search-chrome-behaviour.md, which is the trade the fix's own comment records."
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
{
"version": 1,
"id": "c15dfcea-2b4e-4c25-983e-c699ef7d1a52",
"createdOn": "2026-08-13",
"action": "update",
"payload": {
"id": "#215",
"detail": "**Outcome:** two of the four image-only findings from the 2026-08-02 audit are shipped; two remain open for an explicit implementation-or-drop decision. **RESTATED 2026-08-13 after inspection against main 2d270392 — two of the four items already shipped and the row no longer describes them as open.** DONE: src/components/clinical-dashboard/image-lightbox.tsx carries decoding=\"async\" (Phase 0, PR #1660), asserted by tests/signed-image.dom.test.tsx. DONE: SignedImage has the priority prop for above-fold evidence — it also skips the IntersectionObserver deferral entirely — and document viewer Phase 3 (PR #1772) added the other half of that pair: an explicit fetchPriority of high when priority is set and low otherwise, so a deferred rail figure does not contend with the page's own above-the-fold work. The document rail additionally passes a 240px observer root margin against the shared 640px default. REMAINING, both confirmed by inspection rather than inferred: (a) src/components/pwa-lifecycle.tsx still has no decoding attribute; (b) public/demo-documents/ still contains no .webp — the PNGs are ~80 KB each and served as-is, so the conversion with a PNG fallback has not been done. **Next:** apply decoding=async in pwa-lifecycle.tsx, and either convert the demo PNGs to WebP with a PNG fallback or record that an ~80 KB synthetic demo asset is not worth the build step. **Stop:** do not treat this row as covering the broader performance findings — those live under #016, #013, #117 and #147."
}
}
4 changes: 2 additions & 2 deletions docs/plans/document-viewer-redesign-plan.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
**Branch seed:** `cursor/document-viewer-redesign-plan-1db8`
**Flightplan evidence:** `.local/workflow-evidence/2026-08-06T17-27-05-553Z-flightplan.json`
**Related ledger:** `#214` / `#219` resolved; `#215` residual (PWA/demo WebP) optional
**Landed:** Phase 0 [#1660](https://github.com/BigSimmo/Database/pull/1660), Phase 1 [#1665](https://github.com/BigSimmo/Database/pull/1665), Phase 2 [#1741](https://github.com/BigSimmo/Database/pull/1741), Phase 3 (this branch)
**Landed:** Phase 0 [#1660](https://github.com/BigSimmo/Database/pull/1660), Phase 1 [#1665](https://github.com/BigSimmo/Database/pull/1665), Phase 2 [#1741](https://github.com/BigSimmo/Database/pull/1741), Phase 3 [#1772](https://github.com/BigSimmo/Database/pull/1772)
**Phase details:** [`document-viewer-phase2-unified-chrome.md`](./document-viewer-phase2-unified-chrome.md) · [`document-viewer-phase3-handover.md`](./document-viewer-phase3-handover.md)

This is the execution plan for a dramatic improvement of design, style, approach,
Expand DownExpand Up@@ -164,7 +164,7 @@ Execution brief: [`document-viewer-phase3-handover.md`](./document-viewer-phase3
| Capability | Status |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Multi-page virtualization | **Done** — windowed page column, document-wide live-canvas budget, idle ±1 render-ahead |
| Crop → page overlay | **Out of scope** — needs `bbox` through `DocumentDetailImage`; a wider contract change than this phase |
| Crop → page overlay | **Out of scope, now tracked as a ledger row** — needs `bbox` through `DocumentDetailImage` and `ImageRow` |
| Keyboard reading mode | **Done** — Page Up/Down, Home/End, `f` fit, `r` rotate; `docs/wiring-conventions.md` |
| Rail virtualization | **Done** — `DocumentImageList` windows `#source-images` and the audit list |
| Smarter signed-URL / decode priority | **Done** — explicit `fetchPriority`, tighter rail root margin; batch route still deferred (`#283`) |
Expand Down
10 changes: 9 additions & 1 deletion scripts/check-runtime.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,7 +74,15 @@ export function checkNodeRuntime(
ok: false,
expectedMajor,
actualVersion: version,
message: `Node ${version} is below the ${minimumVersion} floor this project requires (package.json engines.node). Install Node ${minimumVersion} or newer.`,
// Name the remedy the repo already ships. This check is the first step of
// verify:pr-local, so it is where a stale-runtime session lands for every
// diff — and "install Node yourself" sends the reader off to do by hand
// what `.claude/hooks/session-start.sh` does correctly, including the
// exclusive major ceiling that a manual install of "latest" would miss.
message:
`Node ${version} is below the ${minimumVersion} floor this project requires (package.json engines.node). ` +
`In a Claude Code remote session, run \`bash .claude/hooks/session-start.sh\`, then run the printed ` +
`\`export PATH=...\` command in your current shell before retrying. Otherwise install ${minimumVersion} or newer yourself.`,
};
}

Expand Down
3 changes: 3 additions & 0 deletions tests/check-runtime.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,9 @@ describe("runtime release gate", () => {
const result = checkNodeRuntime("24.13.0");
expect(result.ok).toBe(false);
expect(result.message).toContain(NODE_MINIMUM_VERSION);
expect(result.message).toContain("Claude Code remote");
expect(result.message).toContain("export PATH=");
expect(result.message).toContain("current shell");
});

it("accepts runtimes at or above the floor", () => {
Expand Down
Loading
Loading