Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
150 commits
Select commit Hold shift + click to select a range
faf50d6
chore(test): add test:unit script and document fixture-dependent inte…
devanfer02 May 24, 2026
f8ee671
fix(results): show 404 page when results jobId does not exist
devanfer02 May 24, 2026
cc5d75f
fix(router): wire NotFoundPage/ErrorPage at the router level, not jus…
devanfer02 May 24, 2026
7caa251
chore: remove claude skill
devanfer02 May 24, 2026
6ff3d19
perf(kbbi): 110× p99 cut on evaluation + IP-rotation bypass for daily…
devanfer02 May 24, 2026
8e91b07
fix(evaluation): show every page chip in findings table
devanfer02 May 24, 2026
4494a2a
feat(history): redesign cards with offset 3D shadow and inline stats
devanfer02 May 24, 2026
be76449
feat(ui): extend grid background and 3D card shadows across landing p…
devanfer02 May 24, 2026
026d641
feat(track): multilingual semantic passage matching with runtime-sele…
devanfer02 May 24, 2026
a75aa2a
docs(spec): third-party API logs design
devanfer02 May 24, 2026
c00fa80
feat(track): queue passage matching per source PDF with resumable bat…
devanfer02 May 24, 2026
4daf21f
feat(admin): log 3rd-party API events and surface them on /admin/api-…
devanfer02 May 24, 2026
35bf974
fix(logs): split API constants into server-safe module
devanfer02 May 24, 2026
730aa8b
feat(nav): expose /admin/api-logs in the header
devanfer02 May 24, 2026
455f83d
feat(track): live elapsed timer + confidence-desc sort on results
devanfer02 May 24, 2026
03dcc9b
fix(history): touch jobs.updatedAt when a passage batch finishes
devanfer02 May 24, 2026
43f92f0
fix(results): wrap expanded passage text instead of overflowing the row
devanfer02 May 24, 2026
23bddad
fix(matcher): strip journal masthead before scoring passage windows
devanfer02 May 24, 2026
ddf94ec
fix(track): clarify source-fetch vs passage-match CTAs on upload panel
devanfer02 May 24, 2026
17ef026
feat(env): wire PUBLIC_MODE flag and prod deployment knobs
devanfer02 May 24, 2026
2bfd45b
feat(history): scope by anonymous session UUID
devanfer02 May 24, 2026
0370043
feat(jobs): in-process concurrency limit + PDF page cap
devanfer02 May 24, 2026
9219655
feat(db): tune postgres for 2cpu/2gb VPS + add retention cron
devanfer02 May 24, 2026
6e945a1
feat(measurement): polite-pool helper + api_call_logs cache flag + lo…
devanfer02 May 24, 2026
fb61e85
chore(scripts): fix psql separator on measure-api-load
devanfer02 May 24, 2026
7c85c98
fix(results): readable needs-review and mid confidence badges
devanfer02 May 24, 2026
a9e9757
style(routes): align settings + api-logs heroes with design system
devanfer02 May 24, 2026
cf82cb9
docs(skills): add project-local testing skill
devanfer02 May 24, 2026
fa6ae10
Revert "feat(history): scope by anonymous session UUID"
devanfer02 May 25, 2026
c3cc9de
chore(db): drop session_id columns and indexes (migration 0011)
devanfer02 May 25, 2026
9235f0f
fix(routes): SSR-load settings + admin/api-logs to dodge client-rpc hang
devanfer02 May 25, 2026
e497ac0
style(routes): design polish on settings, api-logs, results-table
devanfer02 May 25, 2026
2cb977d
fix(env): split PUBLIC_MODE into client+server flags
devanfer02 May 25, 2026
1467616
fix(ui): resolve console warnings on /track
devanfer02 May 25, 2026
47eeb15
feat(api-logs): add date filter with presets + custom range
devanfer02 May 25, 2026
30e6d5e
fix(track): replace numeric source-pdf fallback with "Sumber tanpa nama"
devanfer02 May 25, 2026
0dab808
refactor(routes): hoist loaders to ensureQueryData so client and SSR …
devanfer02 May 25, 2026
4ced47e
feat(evaluation): expose dev fixture button on the upload step for fa…
devanfer02 May 25, 2026
cb4b74b
feat(kbbi): expire dictionary cache entries after 15 minutes so re-sc…
devanfer02 May 25, 2026
574172d
chore(header): rename "Log API" nav item to "3rd Party Logs" for clarity
devanfer02 May 25, 2026
2122869
docs(skills): add TanStack Start reference skill covering routes, ser…
devanfer02 May 25, 2026
2310655
fix(autofetch): write PDFs atomically via temp+rename, isolate per-re…
devanfer02 May 25, 2026
06eb9c2
feat(track): auto-resume passage matching after navigation by replayi…
devanfer02 May 25, 2026
028882d
docs(skills): refine drizzle-orm for CiteTrack, prune off-stack and o…
devanfer02 May 25, 2026
53fb115
docs(claude): forbid silent truncation of user-visible content
devanfer02 May 25, 2026
ad553e8
feat(eval): clickable nav, wrapped cover, tighter highlights in annot…
devanfer02 May 25, 2026
305582e
docs(specs): add public-mode hardening spec
devanfer02 May 25, 2026
121b50e
feat(public-mode): add policy module with overrides and repo URL
devanfer02 May 25, 2026
8952c46
feat(config): apply PUBLIC_MODE overrides at getConfig read time
devanfer02 May 25, 2026
c40e083
refactor(retention): read purge.retention_days from DB, env becomes seed
devanfer02 May 25, 2026
8d338b7
feat(header): show "Demo publik" badge when PUBLIC_MODE is on
devanfer02 May 25, 2026
8c10dfe
feat(public-mode): add privacy callout above upload dropzones
devanfer02 May 25, 2026
d44f3f2
chore(public-mode): point CITETRACK_REPO_URL at devanfer02/citetrack
devanfer02 May 25, 2026
dab5b4e
feat(settings): dev-only toggle to preview public mode in the browser
devanfer02 May 25, 2026
a95c00f
copy(privacy): humanize prose, drop em-dashes, fix bullet label
devanfer02 May 25, 2026
a7fafec
fix(public-mode): stop env.PUBLIC_MODE leaking into the client bundle
devanfer02 May 25, 2026
e400420
copy(privacy): add public-mode-only sections on demo terms and liability
devanfer02 May 25, 2026
24326df
chore(tests): restructure into unit/integration/perf dirs
devanfer02 May 25, 2026
c0aa0bc
test(suite): repair stale assertions and fill load-bearing coverage gaps
devanfer02 May 25, 2026
927b8b3
fix(parser): reject Title Case run as narrative citation author
devanfer02 May 25, 2026
9fdca35
feat(throttle): shared per-host throttle + 429 cooldown for all outbo…
devanfer02 May 25, 2026
aded7fb
fix(extractor): detect italic via font-usage heuristic for CID-subset…
devanfer02 May 26, 2026
42002f8
fix(kbbi): skip small Roman numerals so front-matter pages don't surf…
devanfer02 May 26, 2026
14a34ec
feat(kbbi): add disable_local_dump admin toggle to force external API…
devanfer02 May 26, 2026
ff00f5a
fix(kbbi): always fall back to external API on dump miss, drop 150-lo…
devanfer02 May 26, 2026
be4d7b3
docs(evaluation): add comparison feature design spec
devanfer02 May 26, 2026
423c624
docs(evaluation): add comparison feature implementation plan
devanfer02 May 26, 2026
cb515a4
feat(evaluation): add compareEvaluations pure function
devanfer02 May 26, 2026
73b67e7
feat(evaluation): add getEvaluationComparison server function
devanfer02 May 26, 2026
dd5725d
feat(evaluation): scaffold compare route with header and scoreboard
devanfer02 May 26, 2026
3f5bc12
feat(evaluation): add finding buckets and rule deltas to compare view
devanfer02 May 26, 2026
1fb3a60
feat(history): multi-select evaluations and compare entry point
devanfer02 May 26, 2026
7b81a13
fix(history): make selected search param idempotent + add compare err…
devanfer02 May 26, 2026
297e052
test(evaluation): add browsermcp e2e scaffolding for comparison feature
devanfer02 May 26, 2026
98d0d14
fix(evaluation): clarify compare severity strip
devanfer02 May 26, 2026
5d4b558
feat(jobs): detached job runner with heartbeat and crash recovery
devanfer02 May 27, 2026
5ae70dc
feat(kbbi): add lemma supplement and surface verification source
devanfer02 May 27, 2026
647f5e4
chore(tooling): add react-doctor scanner with scoped ignores
devanfer02 May 27, 2026
42bdf74
refactor(upload): extract PdfDropzoneCard atomic component
devanfer02 May 28, 2026
df087e7
feat(kbbi): restore per-job external-lookup cap as a config
devanfer02 May 28, 2026
1527ecf
chore(seed): move kbbi dictionary and lemma supplement under deploy/s…
devanfer02 May 28, 2026
d6f7e79
feat(seed): add multi-source KBBI dumps and idempotent core_kbbi.sql
devanfer02 May 28, 2026
71b9c41
feat(kbbi): add raf555 KBBI VI JSON source
devanfer02 May 28, 2026
72c0417
feat(settings): per-source toggles for KBBI providers
devanfer02 May 28, 2026
5441fff
fix(evaluation): swap KBBI/EYD pipeline order and sync per-stage live…
devanfer02 May 28, 2026
c71d882
fix(evaluation): make category header sticky while scrolling findings
devanfer02 May 28, 2026
15a04ff
refactor(settings): split configuration cards across tabs
devanfer02 May 28, 2026
97f0728
chore(lint): resolve react-doctor diagnostics in touched route files
devanfer02 May 28, 2026
dd62b36
feat(a11y): WCAG 2.2 AA pass across the UI (#7)
devanfer02 May 28, 2026
a0a0233
refactor(settings): consolidate KBBI source toggles into one card
devanfer02 May 28, 2026
7ddac6a
feat(admin/api-logs): page-indexed pagination and stats dashboard
devanfer02 May 28, 2026
ba297c3
fix(admin/api-logs): clarify duration label and unit-switch ms ↔ s
devanfer02 May 28, 2026
7bc0007
refactor(compare): tab the delta lists and group rows by rule
devanfer02 May 28, 2026
af4040f
refactor(admin/api-logs): use canonical duration formatter, drop loca…
devanfer02 May 28, 2026
5269c24
feat(compare): swap-and-recompare button + tighter, bolder tab strip
devanfer02 May 28, 2026
d4e89bf
docs(kbbi): design spec for AJAX/Cloudflare bypass + self-limit log tag
devanfer02 May 28, 2026
6a83e29
feat(compare): animate the swap with the View Transitions API
devanfer02 May 28, 2026
7a5512f
i18n(track): translate pipeline labels and result tables to Indonesian
devanfer02 May 28, 2026
3fb73d7
i18n(evaluation): drop English labels and fix awkward translations
devanfer02 May 28, 2026
b373c61
refactor(error-pages): humanize ErrorPage and 404 prose
devanfer02 May 28, 2026
1c003a4
refactor(prose): humanize landing copy, modals, and admin-jargon leaks
devanfer02 May 28, 2026
8641390
chore(deps): pin @tanstack/* to current versions instead of "latest"
devanfer02 May 28, 2026
89866a0
chore(deps): bump @tanstack/react-form and react-start to current npm…
devanfer02 May 28, 2026
75f7082
fix(kbbi): revive web.id + typoonline sources, tag self-limit timeout…
devanfer02 May 28, 2026
bd436c9
fix(admin/api-logs): treat aborted as its own outcome bucket, not info
devanfer02 May 28, 2026
2e41f02
fix(evaluation): restore auto-rotation on EYD tips and expand the pool
devanfer02 May 28, 2026
0bfecd7
fix(admin/api-logs): point KBBI timeout error at the right nav label …
devanfer02 May 28, 2026
2b563ce
fix(admin/api-logs): make the provider 'rincian' breakdown readable
devanfer02 May 28, 2026
2c323a6
docs(readme): point contributors and bug reporters at CONTRIBUTING.md
devanfer02 May 28, 2026
8c83f30
feat(evaluation): add tier-flow explainer with real local-vs-daring s…
devanfer02 May 28, 2026
566f924
feat(evaluation): pick a report to compare straight from the result page
devanfer02 May 29, 2026
0b1fa58
feat(settings): warn about resource and UX impact of heavier knobs
devanfer02 May 29, 2026
f05e003
feat(track): explain the tracking pipeline below the upload form
devanfer02 May 29, 2026
031639e
feat(evaluation): add local-only KBBI mode to skip online lookups
devanfer02 May 29, 2026
fa6b443
feat(evaluation): finish tier-flow explainer and bundle in-flight work
devanfer02 May 29, 2026
14b8d0e
refactor: extract shared download/clipboard/callout helpers (#9)
devanfer02 May 29, 2026
5aa1f7a
ci: run lint and unit tests on every PR with a 95% pass-rate gate (#10)
devanfer02 May 29, 2026
cbe41c9
feat(settings): horizontal-scroll config cards with global search
devanfer02 May 29, 2026
641bfce
feat(settings): wheel-to-horizontal scroll on card row, wider shorter…
devanfer02 May 29, 2026
e711574
fix(settings): normalise wheel deltaMode so card row scrolls in Firefox
devanfer02 May 29, 2026
2e37d76
fix(settings): keep scroll on search input; snappier wheel scroll
devanfer02 May 29, 2026
33b4153
refactor(settings): drop wheel hijack for a visible native scrollbar
devanfer02 May 29, 2026
213ed2f
feat(public-mode): force local-only KBBI lookup for faster demo
devanfer02 May 31, 2026
618670d
refactor(settings): extract reusable config card components
devanfer02 May 31, 2026
60b8998
feat(settings): add reusable local-env admin settings panel
devanfer02 May 31, 2026
7660eb6
refactor(settings): 2-col grid global page with upload + purge only
devanfer02 May 31, 2026
b31977a
feat(track): inline admin settings panel for autofetch + passage
devanfer02 May 31, 2026
db6f988
feat(evaluation): inline admin settings panel for KBBI
devanfer02 May 31, 2026
2336082
chore(settings): remove dead tab/search code
devanfer02 May 31, 2026
38ab6bd
feat(api-logs): keep stats panel visible with a clean zero state
devanfer02 May 31, 2026
9f33372
feat(hero): add Setelan pill jumping to inline admin settings panel
devanfer02 May 31, 2026
02387bd
fix(track): resume incomplete jobs from history instead of opening em…
devanfer02 May 31, 2026
a5d9b39
fix(track): wrap expanded passage text and make needs-review icon vis…
devanfer02 May 31, 2026
1ae830c
docs(apply): design spec for auto-apply after evaluation
devanfer02 May 31, 2026
2ef27af
chore(deps): add docx and pizzip for auto-apply
devanfer02 May 31, 2026
01afe4b
feat(landing): lead with evaluation, keep CiteTrack name
devanfer02 May 31, 2026
ffcb170
feat(apply): eligibility + change-log shared modules
devanfer02 May 31, 2026
3ad226f
feat(apply): rebuild corrected docx from page offsets (PDF-only path)
devanfer02 May 31, 2026
3a80c37
feat(apply): context-anchored docx patching (has-docx path)
devanfer02 May 31, 2026
0652363
feat(apply): applyEvaluationFixes server fn + paths + schema + downlo…
devanfer02 May 31, 2026
1b988a1
feat(evaluation): apply-panel UI on results page
devanfer02 May 31, 2026
9ecb542
test(apply): unit tests for splice, eligibility, change-log, patch
devanfer02 May 31, 2026
5277ca4
chore(scripts): test-apply diagnostic for apply engines
devanfer02 May 31, 2026
4d20d43
fix(apply): keep server-only deps out of the client bundle
devanfer02 May 31, 2026
3dc0c1b
feat(evaluation): move apply into header modal, consolidate downloads…
devanfer02 Jun 1, 2026
6f6fa7d
fix(eyd): require 2+ space gap for space-before-punct to drop extract…
devanfer02 Jun 1, 2026
2f8fff6
feat(apply): italicize foreign-word findings in the rebuilt docx
devanfer02 Jun 1, 2026
7876242
feat(evaluation): "Jadikan miring" group in the apply panel
devanfer02 Jun 1, 2026
6c4a730
docs(evaluation): spec for exclude-pages filter
devanfer02 Jul 6, 2026
b5496f9
feat(evaluation): exclude-pages filter for KBBI/EYD results
devanfer02 Jul 6, 2026
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
46 changes: 46 additions & 0 deletions .agents/skills/react-doctor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: react-doctor
description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook.
version: "1.1.0"
---

# React Doctor

Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score.

## After making React code changes:

Run `npx react-doctor@latest --verbose --diff` and check the score did not regress.

If the score dropped, fix the regressions before committing.

## For general cleanup or code improvement:

Run `npx react-doctor@latest --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings.

## /doctor — full local triage workflow

When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it:

```bash
curl --fail --silent --show-error \
--header 'Cache-Control: no-cache' \
https://www.react.doctor/prompts/react-doctor-agent.md
```

The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed.

Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe.

## Command

```bash
npx react-doctor@latest --verbose --diff
```

| Flag | Purpose |
| ----------- | --------------------------------------------- |
| `.` | Scan current directory |
| `--verbose` | Show affected files and line numbers per rule |
| `--diff` | Only scan changed files vs base branch |
| `--score` | Output only the numeric score |
13 changes: 13 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
- Use Bun to manage packages and dev tooling
- No `any` or `unknown` — always use precise types. Only use `any`/`unknown` when strictly unavoidable (e.g. third-party lib gaps, type assertion boundaries) and add a `// eslint-disable-next-line` comment explaining why.

### Don't Truncate Content the User Might Need

**DO NOT TRUNCATE CONTENT THAT THE USER MIGHT NEED.** Applies everywhere user-visible text is rendered — UI cells, tables, tooltips, exported PDFs / spreadsheets / CSVs, downloads, copy-to-clipboard payloads, share previews, error banners, anywhere.

- **Wrap, don't cut.** If a line is too wide, break to the next line. If a column is too narrow, let the cell grow vertically. If a page is too short, flow to the next page. Never replace meaning with `…` / `...` / "show more".
- **Show all items, not a sample.** Don't cap lists at N with "… and K more". The reader opened the artifact because they want every entry — show every entry. Pagination is fine where it exists for performance; silent omission is not.
- **Exceptions** (the only ones):
- Decorative or duplicative metadata (file extensions in a column whose header is "Type", relative-time strings ≤ 1 line).
- Inputs the user actively typed (an `<input maxlength>` is the user's own constraint).
- Hard physical limits where the alternative is worse (a 1-line `<title>` for SEO, a single-line OS notification). In those cases, prefer a tooltip / hover / "click for full" rather than silent `…`.
- **PDF / exported artifacts**: messages, citations, finding text, etc. must wrap across lines; lists of findings must show every finding even if the cover spans more pages. Never cap a page-finding list at N and append "… X lainnya".
- **If in doubt, wrap.** The cost of one extra line is tiny. The cost of a missing word in a thesis-evaluation report is the student not knowing why their writing was flagged.

### Documentation Lookup

Always check context7 MCP (`resolve-library-id` then `query-docs`) for up-to-date documentation before implementing with TanStack libraries (Start, Router, Query, Form) — don't rely on training knowledge alone as APIs change frequently.
Expand Down
131 changes: 131 additions & 0 deletions .claude/E2E_BROWSERMCP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Browser E2E Testing (browsermcp) — Agent Runbook

This is the self-serve guide for end-to-end testing CiteTrack in a real Chrome
browser via the **browsermcp** MCP server, in `dev` mode. It exists so the
agent can run E2E flows **without asking the user for step-by-step prompts**.

## Division of labor

- **The user does exactly one thing:** opens Chrome and clicks the browsermcp
extension's **Connect** button on a tab pointed at `http://localhost:3000`.
browsermcp drives whatever tab is connected; it cannot open or connect a tab
on its own.
- **The agent does everything else:** start the dev server, seed data, navigate,
click, type, screenshot, read console logs, assert.

If a `browser_*` tool returns `No connection to browser extension`, that means
the user has not connected a tab yet. Pause and ask them to click **Connect**,
then continue — do not treat it as a code failure.

## One-time environment (agent runs these)

```bash
# 1. Start the dev server (port 3000). Background it; don't block.
bun run dev # via Bash run_in_background: true

# 2. Seed deterministic data for the feature under test.
# For the evaluation-comparison feature, this ensures one finished
# evaluation exists for EACH thesis example PDF:
bun .claude/scripts/seed-compare-e2e.ts
# -> prints the two eval IDs + ready-to-open /history and /compare URLs.
# Add FRESH=1 to force a fresh analysis run instead of reusing.
```

Permissions for `bun run dev`, `bun run build`, the seed script, and every
`mcp__browsermcp__browser_*` tool are pre-approved in
`.claude/settings.local.json`, so none of these prompt.

## browsermcp tool cheat-sheet

| Tool | Use for |
|------|---------|
| `browser_navigate({ url })` | Go to a URL. |
| `browser_snapshot()` | Accessibility tree of the page. **Get element `ref`s here before clicking.** |
| `browser_click({ element, ref })` | Click. `element` is a human description, `ref` comes from the latest snapshot. |
| `browser_type({ element, ref, text, submit })` | Type into a field. |
| `browser_hover` / `browser_select_option` / `browser_press_key` | Hover, select, keypress. |
| `browser_screenshot()` | Visual capture — use to confirm layout/rendering for the user. |
| `browser_get_console_logs()` | Pull console output — check for client errors after each step. |
| `browser_wait({ time })` | Wait for async UI (loaders, query refetch). |

**Golden loop:** `navigate` → `snapshot` (grab refs) → act (`click`/`type`) →
`snapshot` again (refs are invalidated by navigation/DOM change) → assert →
`screenshot` for the user. Re-snapshot after every navigation; never reuse a
stale `ref`.

## Flow under test: Evaluation Comparison

**Goal:** verify a student can compare two evaluations and see the scoreboard +
resolved / still-present / newly-introduced buckets.

Preconditions: dev server up, seed run (two thesis evals exist), tab connected.

1. `browser_navigate({ url: "http://localhost:3000/history?kind=evaluation" })`
2. `browser_snapshot()` — confirm the Riwayat list renders and that `done`
evaluation rows show a checkbox (aria-label `Pilih <filename> untuk dibandingkan`).
Assert: checkboxes appear only on finished rows.
3. Click the checkbox on the `thesis_example.pdf` row → `browser_click`.
4. `browser_snapshot()` — assert a sticky bar appears reading
"Pilih satu lagi untuk membandingkan." and the "Bandingkan dipilih" button
is disabled.
5. Click the checkbox on the `thesis_example_2.pdf` row.
6. `browser_snapshot()` — assert the sticky bar now reads "Dua evaluation
dipilih." and "Bandingkan dipilih" is enabled.
7. Click "Bandingkan dipilih".
8. `browser_wait({ time: 1 })` then `browser_snapshot()` — assert URL is
`/evaluation/compare/<older>/<newer>` (older→newer canonical order) and the
page shows, in order:
- Header "Sebelum dan sesudah." with two filename pills.
- "Ringkasan perubahan" scoreboard cards (Skor keseluruhan, Total temuan,
Temuan KBBI, Temuan EYD) + the severity strip.
- "Yang sudah beres." (resolved), "Yang masih perlu disentuh."
(still present), "Yang baru muncul." (introduced) sections with counts.
- "Per aturan" reductions/regressions.
9. `browser_screenshot()` — capture the full comparison for the user to validate.
10. `browser_get_console_logs()` — assert no client-side errors.
11. (If still-present has rows) click a still-present row → assert it navigates
to `/evaluation/<afterId>?highlights=p.<n>;<token>` and the report opens on
that page.

### Error-path checks (optional, fast)

- `browser_navigate` to `/evaluation/compare/<id>/<id>` (same id twice) → assert
the friendly "Perbandingan gagal dibuka" view with "Kembali ke riwayat" link.
- `/evaluation/compare/<bogus-uuid>/<real-id>` → assert "tidak ditemukan" message.

## Reporting back

After the flow, summarize for the user: which assertions passed, attach the
screenshot, surface any console errors. **Final validation is the user's call** —
present the result and let them confirm the feature works.

## Known gotcha: flaky `click` / `screenshot` (this setup)

On this machine, `browser_click` and `browser_screenshot` frequently fail with
`WebSocket response timeout after 30000ms` — the action may or may not apply and
the call never returns. `browser_navigate`, `browser_snapshot`, and
`browser_get_console_logs` are reliable.

**Workaround — drive by URL state, not by clicking:**

- Selection-on-history is URL-backed, so instead of clicking checkboxes,
navigate straight to the pre-selected URL:
`/history?kind=evaluation&selected=%5B%22<id1>%22%2C%22<id2>%22%5D`
then `snapshot` to assert both rows show `[checked]` and the sticky bar reads
"Dua evaluation dipilih." with an enabled "Bandingkan dipilih" button.
- For the compare result, navigate directly to the destination the button
targets: `/evaluation/compare/<id1>/<id2>` (it self-canonicalizes older→newer).
- The compare page's accessibility snapshot can exceed the tool's token limit
because it renders **every** finding bucket (no truncation). When that happens
the result is written to a file — `grep` it for headings/counts/numbers rather
than reading the whole thing.
- Skip `screenshot`; rely on the snapshot text + `get_console_logs` for
assertions, and tell the user a visual capture wasn't possible.

## Notes / gotchas

- The compare route canonicalizes URL order (older→newer by `createdAt`) via a
redirect, so the final URL may swap the two IDs you passed. That's expected.
- Selection state lives in the URL (`?selected=[...]` JSON array) and survives
pagination.
- History/compare are local-only (`isLocalEnv`); they 404 in public mode.
Loading
Loading