{"owner":"Stirling-Tools","repo":"Stirling-PDF","hasSkills":true,"totalSkillsCount":57,"totalTokensCount":40666,"categories":["claude-rule","anthropic-skill","root-instruction","subagent-persona"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/comment-review/added-comments.mjs",".claude/skills/ui-before-after/capture-spec.template.ts",".claude/skills/ui-before-after/make_example.py",".claude/skills/ui-before-after/SKILL.md",".claude/skills/pr-quiz/SKILL.md",".claude/skills/ui-before-after/diff-shots.mjs",".claude/skills/comment-review/SKILL.md","AGENTS.md",".claude/skills/feature-walkthrough/SKILL.md",".claude/skills/ui-walkthrough/SKILL.md","CLAUDE.md",".claude/skills/ui-before-after/shoot-sections.mjs",".claude/skills/ui-walkthrough/make_example.py",".claude/skills/ui-walkthrough/report-template.html","frontend/.claude/agents/playwright-test-generator.md","frontend/.claude/agents/playwright-test-healer.md","frontend/.claude/agents/playwright-test-planner.md","frontend/src/editor/proprietary/services/heuristic/rules/core.json","frontend/src/editor/proprietary/services/heuristic/rules/languages.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json","frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json"],"skills":{".claude/skills/comment-review/added-comments.mjs":"#!/usr/bin/env node\n\n// Every comment line a range adds, as `file:line text`, for the review checklist.\n//\n//   node .claude/skills/comment-review/added-comments.mjs <base-ref> [head-ref]\n//\n// Line-based on purpose: it over-reports (a `//` inside a string, a `*` opening a\n// wrapped expression) rather than miss one, and the reviewer opens the file anyway.\n// A shell one-liner cannot do this job: `$0` and `$1` in an awk or perl script are\n// rewritten by skill argument interpolation before the shell ever sees them.\n\nimport { execFileSync } from \"node:child_process\";\n\nconst [base, head = \"HEAD\"] = process.argv.slice(2);\nif (!base) {\n  process.stderr.write(\"usage: added-comments.mjs <base-ref> [head-ref]\\n\");\n  process.exit(2);\n}\n\nconst diff = execFileSync(\"git\", [\"diff\", \"-U0\", \"--no-color\", `${base}...${head}`], {\n  encoding: \"utf8\",\n  maxBuffer: 1 << 28,\n});\n\nconst OPENS_COMMENT = /^\\+\\s*(\\/\\/|\\/\\*|\\*|#|\"\"\"|'''|\\{\\/\\*)/;\n\nlet file = null;\nlet line = 0;\nlet count = 0;\n\nfor (const raw of diff.split(\"\\n\")) {\n  if (raw.startsWith(\"+++ \")) {\n    file = raw.slice(4).replace(/^b\\//, \"\").trim();\n    continue;\n  }\n  // The new-file start of the hunk. Single-line hunks omit the count, so read the\n  // number rather than splitting on the comma that may not be there.\n  const hunk = /^@@ .*?\\+(\\d+)/.exec(raw);\n  if (hunk) {\n    line = Number(hunk[1]);\n    continue;\n  }\n  if (!raw.startsWith(\"+\")) continue;\n  if (OPENS_COMMENT.test(raw)) {\n    process.stdout.write(`${file}:${line} ${raw.slice(1).trim()}\\n`);\n    count += 1;\n  }\n  line += 1;\n}\n\nprocess.stderr.write(`${count} added comment lines\\n`);\n",".claude/skills/ui-before-after/capture-spec.template.ts":"// Generic before/after capturer. NOT app-specific: it walks a targets.json that\n// the ui-before-after skill generates from the branch/PR diff, so nothing here is\n// hand-listed. Copy to src/core/tests/stubbed/ui-before-after.spec.ts, then run\n// once per (side, theme):\n//   PR_SHOT_SIDE=after  PR_SHOT_THEME=light \\\n//     npx playwright test --project=stubbed ui-before-after.spec.ts\n//\n// targets.json shape: [{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\",\n//                        \"needsFile\": true }]\nimport { test } from \"@app/tests/helpers/stub-test-base\";\nimport type { Page } from \"@playwright/test\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst SIDE = process.env.PR_SHOT_SIDE ?? \"after\";\nconst THEME = process.env.PR_SHOT_THEME ?? \"light\";\n// Capture the full viewport by default so the affected region is in frame\n// wherever it is; diff-shots.mjs crops each comparison to what actually changed.\n// Set PR_SHOT_SCOPE to a selector to narrow the capture to one container.\nconst SCOPE = process.env.PR_SHOT_SCOPE ?? \"\";\nconst ROOT = path.resolve(process.cwd(), \"screenshots\", \"ui-diff\");\nconst OUT = path.join(ROOT, SIDE);\n// A tiny sample PDF so file-dependent tool panels render. Point at a real fixture.\nconst SAMPLE_PDF = process.env.PR_SHOT_SAMPLE ?? \"src/core/tests/test-fixtures/sample.pdf\";\n\ntype Target = { id: string; url: string; name?: string; needsFile?: boolean };\nconst targets: Target[] = JSON.parse(fs.readFileSync(path.join(ROOT, \"targets.json\"), \"utf-8\"));\n\ntest.use({ autoGoto: false, viewport: { width: 1600, height: 900 }, seedJwt: true });\n\nasync function applyTheme(page: Page): Promise<void> {\n  if (THEME !== \"dark\") return;\n  await page.addInitScript(() => {\n    localStorage.setItem(\"mantine-color-scheme\", \"dark\");\n    localStorage.setItem(\"mantine-color-scheme-value\", \"dark\");\n  });\n  await page.emulateMedia({ colorScheme: \"dark\" });\n}\n\nasync function seedFile(page: Page): Promise<void> {\n  if (!fs.existsSync(SAMPLE_PDF)) return;\n  await page.goto(\"/\", { waitUntil: \"domcontentloaded\" });\n  await page.getByTestId(\"files-button\").click().catch(() => {});\n  await page.locator('[data-testid=\"file-input\"]').setInputFiles(SAMPLE_PDF).catch(() => {});\n  await page.locator(\".file-sidebar-file-item\").first().isVisible({ timeout: 8_000 }).catch(() => {});\n}\n\nfor (const t of targets) {\n  // One test per target so a single failure doesn't drop the rest.\n  test(`${SIDE}/${THEME} ${t.id}`, async ({ page }) => {\n    fs.mkdirSync(OUT, { recursive: true });\n    await applyTheme(page);\n    if (t.needsFile !== false) await seedFile(page);\n    await page.goto(t.url, { waitUntil: \"domcontentloaded\" });\n    await page.waitForTimeout(400); // settle Mantine portals/transitions\n    const shot = path.join(OUT, `${t.id}__${THEME}.png`);\n    if (SCOPE) {\n      const scope = page.locator(SCOPE).first();\n      if (await scope.isVisible({ timeout: 8_000 }).catch(() => false)) {\n        await scope.screenshot({ path: shot });\n        return;\n      }\n    }\n    // Full viewport (fixed size → stable dimensions for pixel diffing).\n    await page.screenshot({ path: shot });\n  });\n}\n",".claude/skills/ui-before-after/make_example.py":"\"\"\"Build EXAMPLE.html from montage-template.html using REAL files-page shots as\nstand-in before/after pairs (layout demo, not an actual PR diff). Inlines PNGs as\ndata URIs so the HTML is portable. Run: python make_example.py\"\"\"\nimport base64\nimport json\nimport pathlib\nimport re\n\nHERE = pathlib.Path(__file__).parent\nSHOTS = pathlib.Path(\n    r\"C:\\Users\\systo\\git\\Stirling-PDFNew\\.claude\\worktrees\\kind-faraday-522a30\"\n    r\"\\frontend\\editor\\screenshots\\files-page\"\n)\n\n\ndef uri(fname):\n    p = SHOTS / fname\n    return \"data:image/png;base64,\" + base64.b64encode(p.read_bytes()).decode() if p.exists() else None\n\n\ndata = {\n    \"pr\": \"DEMO\",\n    \"title\": \"EXAMPLE — before/after montage (layout demo, real Files-page shots; not a real PR diff)\",\n    \"base\": \"main\", \"head\": \"demo-branch\",\n    \"cropSelector\": \"[data-sidebar=\\\"tool-panel\\\"]  (real runs crop to the side; these demo shots are full-page)\",\n    \"tabs\": [\n        {\"id\": \"files\", \"title\": \"Files page\", \"ctx\": \"Each row = one flow state; left = base branch, right = this PR.\",\n         \"states\": [\n             {\"name\": \"Empty folder\", \"before\": uri(\"01_empty_state_ctas.png\"), \"after\": uri(\"02_empty_state_storage_off.png\")},\n             {\"name\": \"Files + details panel\", \"before\": uri(\"03_subtoolbar_with_files.png\"), \"after\": uri(\"06_details_panel_save_to_server.png\")},\n             {\"name\": \"Delete folder confirm\", \"before\": None, \"after\": uri(\"19_delete_folder_dialog.png\"), \"note\": \"New in this PR\"},\n         ]},\n        {\"id\": \"move\", \"title\": \"Move-to-folder dialog\",\n         \"states\": [\n             {\"name\": \"Dialog opened\", \"before\": uri(\"07_move_dialog_collapsed.png\"), \"after\": uri(\"08_move_dialog_create_folder_expanded.png\")},\n             {\"name\": \"After folder created\", \"before\": None, \"after\": uri(\"08b_move_dialog_after_create_folder.png\"), \"note\": \"New flow\"},\n         ]},\n    ],\n}\n\ntpl = (HERE / \"montage-template.html\").read_text(encoding=\"utf-8\")\nout = re.sub(\n    r\"/\\*__DATA__\\*/.*?/\\*__END__\\*/\",\n    lambda _m: \"/*__DATA__*/\" + json.dumps(data) + \"/*__END__*/\",\n    tpl, count=1, flags=re.S,\n)\n(HERE / \"EXAMPLE.html\").write_text(out, encoding=\"utf-8\")\nprint(\"wrote\", HERE / \"EXAMPLE.html\", \"(\", (HERE / \"EXAMPLE.html\").stat().st_size // 1024, \"KB )\")\n",".claude/skills/ui-before-after/SKILL.md":"---\nname: ui-before-after\ndescription: >-\n  Analyse a branch or PR and automatically capture before/after screenshots of\n  every UI surface its changes touch, then pixel-diff the pairs to surface what\n  actually changed and assemble PR-ready before/after montage images. Generic and\n  diff-driven: it derives the capture targets from the diff (changed tools/routes →\n  URLs) instead of hand-listing screens, captures \"before\" from the base branch and\n  \"after\" from the head, then keeps only the views that visually differ. Each\n  comparison is auto-cropped to the region that actually changed (the bounding box of\n  differing pixels), falling back to the full page only when the change spans most of\n  it. Use for before/after shots, a visual diff of a branch/PR, \"screenshots for the\n  PR description\", \"show what changed in the UI\", or a side-by-side of UI changes.\n  Takes a PR number/URL (resolved via gh) or a branch; defaults to the current branch\n  vs its base. Flags: --scope <selector>, --base <ref|merge-base>, --theme\n  light|dark|both, --all (capture every route, not just changed), --no-autocrop,\n  --pagewide <n>, --threshold <n>.\nargument-hint: \"[PR# | PR-url | branch] [--scope <sel>] [--base <ref>] [--theme both] [--all] [--no-autocrop]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Before / After (generic visual diff)\n\nPoint it at a branch or PR; it figures out which UI changed, screenshots every\naffected surface **before** (base) and **after** (head), pixel-diffs the pairs, and\nmontages the ones that actually changed into images for the PR description.\n\n`$ARGUMENTS`: a PR number/URL, a branch, or nothing (current branch vs base).\nBy default it captures the full viewport and auto-crops each comparison to the region\nthat changed. Flags: `--scope <css>` (narrow the *capture* to a container, e.g.\n`[data-sidebar=\"tool-panel\"]`, when you already know where the change is),\n`--no-autocrop` (keep full frames), `--pagewide <fraction>` (above this share of the\npage, skip cropping; default 0.6), `--base <ref|merge-base>`,\n`--theme light|dark|both`, `--all` (walk every route, not just changed),\n`--threshold <fraction>` (diff sensitivity, default 0.001).\n\nShares the capture harness with **ui-walkthrough** - read its SKILL.md for the\nstubbed-Playwright setup, worktree node_modules, the\nstale-`:5173` gotcha, and the dark-mode init-script. Bundled helpers:\n[capture-spec.template.ts](capture-spec.template.ts), [diff-shots.mjs](diff-shots.mjs),\n[montage-template.html](montage-template.html), [shoot-sections.mjs](shoot-sections.mjs).\n\n## Process\n\n### 1. Resolve target + base\n```\ngh pr view <pr> --json number,title,headRefName,baseRefName,url,files   # PR\n# or branch: base = merge-base(main, HEAD); head = HEAD\ngh pr diff <pr> --name-only        # or: git diff --name-only <base>...HEAD\n```\n\n### 2. Derive capture targets from the diff (the \"analyse\" step - no hand-listing)\nMap changed frontend files to URLs generically:\n- **Tools**: a changed `components/tools/<toolDir>/…` or `hooks/tools/<tool>/…` →\n  toolId → URL via the repo's own rule `getToolUrlPath` in\n  [toolsTaxonomy.ts:200](../../../frontend/src/editor/core/data/toolsTaxonomy.ts): `/` + the\n  id kebab-cased (`addPageNumbers` → `/add-page-numbers`).\n- **Pages/routes**: changed `filesPage/*` → `/files`, etc.\n- `--all`: enumerate every tool in the registry instead of just changed ones.\nWrite `frontend/screenshots/ui-diff/targets.json` =\n`[{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\" }]`. This is what makes\nit generic - the spec never names a tool.\n\n### 3. Capture AFTER (head) then BEFORE (base)\nCopy [capture-spec.template.ts](capture-spec.template.ts) →\n`src/core/tests/stubbed/ui-before-after.spec.ts` (it loops `targets.json`, seeds a\nsample PDF so file-dependent panels render, navigates to each URL, and screenshots\nthe full viewport - or the `--scope` container if given). Ensure the harness is ready\n(node_modules + icons).\n```\n# after = current head\ncd frontend && PR_SHOT_SIDE=after PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n# before = base, in an isolated worktree (copy the spec + targets.json in)\ngit worktree add ../ba-base origin/<baseRefName>        # or the merge-base\n#   set up its frontend, copy spec + screenshots/ui-diff/targets.json across, then:\ncd ../ba-base/frontend && PR_SHOT_SIDE=before PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n#   copy its screenshots/ui-diff/before/ back next to after/. Repeat with\n#   PR_SHOT_THEME=dark if --theme includes dark. Remove worktree when done.\n```\n\n### 4. Auto-diff (surface what changed)\n```\ncd frontend && node <skill>/diff-shots.mjs \\\n  screenshots/ui-diff/before screenshots/ui-diff/after screenshots/ui-diff\n```\nProduces `diff-report.json` classifying each view `unchanged | changed | added |\nremoved`. For each changed view it computes the bounding box of differing pixels and\nwrites cropped `__before_crop.png` / `__after_crop.png` / `__diff.png` to that region\n(+ padding) - **unless** the change covers more than `--pagewide` of the frame, where\nit keeps the full frame (`pageWide:true`). Drop `unchanged` - that's the noise the\nuser doesn't want.\n\n### 5. Montage the changes\nBuild the manifest from the non-unchanged entries (group by tab/tool; each becomes a\nstate row with before/after). For changed views use the cropped `cropBefore` /\n`cropAfter` from `diff-report.json` (tight on the affected region; full frame when\n`pageWide`); `added`/`removed` render the \"not present\" placeholder. Fill\n[montage-template.html](montage-template.html) (replace the `window.__BA__` data\nblock; base64-inline the PNGs for portability), then render one PNG per section with\n[shoot-sections.mjs](shoot-sections.mjs). Optionally include the `__diff.png` overlay\nas a third column.\n\n### 6. Deliver\nOutput the `montage_<tab>.png` files + a short summary (N changed / added / removed,\nM unchanged skipped) and a paste-ready Markdown block. GitHub has no PR-body image\nAPI, so tell the user to drag the PNGs into the description. Do **not** post to the\nPR.\n\n## Gotchas\n- Two installs (base worktree + head); junction main's node_modules only if its deps\n  match that ref, else `npm ci` (see ui-walkthrough's stale-dep note).\n- A view that errors on one side (refactored/removed) → that side is missing; the\n  diff marks it added/removed rather than failing the run.\n- Pixel diff needs equal dimensions, so capture at a fixed viewport (the template\n  does); a view whose size changed is reported as \"changed (dimensions differ)\",\n  uncropped.\n- Auto-crop uses a single bounding box, so two far-apart changes give one large crop\n  (or trip `--pagewide`); narrow with `--scope` if that happens.\n- `getToolUrlPath` is the source of truth for tool URLs - use it, don't guess slugs.\n- Don't commit `screenshots/`, the throwaway spec, or the base worktree.\n",".claude/skills/pr-quiz/SKILL.md":"---\nname: pr-quiz\ndescription: >-\n  Quiz the PR author on their own branch before they request review, to prove they\n  actually understand the change - especially code an AI wrote for them. Scopes the\n  branch diff vs its base, reads the changed code, then asks graded questions about\n  what changed, why, how it works, what it could break, and which edge cases it must\n  handle. Presents all questions first, waits for the author's answers, then grades\n  each honestly against the real code (Correct / Partial / Incorrect with the true\n  answer and file:line), scores it, and gives a readiness verdict that names the\n  areas to re-study before asking humans to review. Use when asked to quiz me on my\n  PR/branch, \"test my understanding before review\", a self-check gate before opening\n  a PR, or before requesting reviewers. Administered as an interactive\n  multiple-choice quiz (clickable options) by default; pass --free-text for\n  written answers, --questions N to set count, --save to write a scorecard.\nargument-hint: \"[branch-or-base-ref] [--questions N] [--free-text] [--save]\"\nallowed-tools: Bash, Read, Grep, Glob, Write, AskUserQuestion\n---\n\n# PR Quiz\n\nTest whether the **author** genuinely understands their own branch before they ask\nother people to spend time reviewing it. This is a self-check gate: the point is to\ncatch changes - often AI-written - that the author would not be able to explain or\ndefend in review. Be a fair but honest examiner, not a pushover.\n\n`$ARGUMENTS` may name a base ref or branch to diff against; default is this branch\nvs where it forked from the main line. Flags:\n- `--questions N` - target N questions (else scale to diff size, see below).\n- `--free-text` - administer as a written numbered list instead of the default\n  interactive multiple-choice.\n- `--save` - also write a scorecard file after grading.\n\n## Integrity rules (read first - the whole skill depends on these)\n\n1. **Present every question before revealing any answer.** Ask, then wait. Never\n   show the answer key alongside the questions.\n2. **Do not give hints or the answer while the quiz is open.** If the author asks\n   \"what's the answer?\" or \"is it X?\" before committing, decline warmly and tell\n   them to give their best answer first - guessing is part of the signal.\n3. **Grade truthfully.** Vague, hand-wavy, or \"the AI did it\" non-answers are\n   Partial or Incorrect, not Correct. Do not inflate the score to be nice; a false\n   pass defeats the entire purpose.\n4. **Ground everything in code you actually read.** Every question and every model\n   answer must trace to a real line in the diff. Cite `path:line`. No trivia\n   (\"how many lines?\"), no invented behavior.\n5. **Credit real understanding.** If the author explains it correctly in their own\n   words, mark it Correct even if worded differently than your key.\n\n## Process\n\n### 1. Scope the change (silently)\n- Find the base. Prefer the fork point off the main line so the quiz covers only\n  this branch's work:\n  ```bash\n  git fetch -q origin 2>/dev/null; \\\n  BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main); \\\n  git diff --stat \"$BASE\"...HEAD\n  ```\n  If `$ARGUMENTS` names a ref, diff against that instead.\n- If the diff is empty, stop and say there's nothing to quiz on.\n- Read commit messages / PR description for the *stated* intent, but verify it\n  against the actual diff - a mismatch is itself a good question.\n\n### 2. Understand the code well enough to examine on it\nRead the full diff plus enough surrounding context and related files to answer\nevery question you plan to ask. You cannot grade understanding you don't have.\nNote the non-obvious parts: the design decisions, the risky lines, the edge cases,\nthe cross-file ripples, and anything that violates or upholds repo conventions\n(for this repo e.g. `@app/*` import layering, all file ops via FileContext,\nJackson 3 / Spring Boot 4 APIs, engine typed-contract boundaries).\n\n### 3. Build the question set\nScale count to the change unless `--questions N` is given:\nsmall (< ~50 changed lines) 3-4, medium 5-8, large 9-12. Cap at 12.\nDraw from these categories - weight toward the ones the diff actually exercises:\n- **Intent** - what problem this solves; why it was needed now.\n- **Mechanism** - how a specific non-trivial piece actually works (\"walk me\n  through what `foo()` does when called with X\").\n- **Decisions & alternatives** - why this approach over an obvious alternative;\n  what a reviewer would reasonably push back on.\n- **Blast radius** - what else this touches or could break; what you'd retest.\n- **Edge cases** - inputs/states the change must handle (null, empty, large,\n  concurrent, error paths).\n- **Conventions & correctness** - does it follow the repo's rules; is there a\n  latent bug the author should be able to spot.\nPrefer questions the author can only answer if they read and understood the code.\nKeep a private answer key with `path:line` for each - do **not** show it yet.\n\n### 4. Administer the quiz\n- **Default (multiple choice):** use the `AskUserQuestion` tool. Per question write\n  3-4 options where **every** option is independently plausible - each distractor a\n  real-but-wrong reading of the code, not filler. Two hard rules so the answer\n  can't be spotted by shape rather than knowledge:\n  - **Randomise the correct option's position** across questions - never default\n    it to first. Spread it roughly evenly over the slots.\n  - **Keep all options the same depth and length.** Do not describe the correct\n    one more fully than the distractors - a longer or more-detailed option is a\n    dead giveaway. Trim the right answer or flesh out the wrong ones until a\n    reader can't tell them apart by size.\n  The tool caps a call at 4 questions, so ask in batches of 4 - but run them as\n  one continuous flow: fire the next batch immediately after the previous\n  returns, with no narration (\"Round 2 of 3\") and no grading between batches.\n  The author always has an \"Other\" free-text escape, which is fine.\n- **`--free-text`:** present all questions in one numbered list, then say\n  \"Answer in one reply; number your answers. I won't grade until you're done.\"\n  Wait for the author's answers.\n- Do not proceed to grading until every answer is in.\n\n### 5. Grade\nFor each question, in order:\n- Verdict: **Correct** / **Partial** / **Incorrect**.\n- The model answer in one or two sentences, citing the real `path:line`.\n- One line on the gap when Partial/Incorrect - what they missed and where to look.\nThen a **Score** (e.g. 6/8, counting Partial as half) and a one-line summary of\nthe pattern (e.g. \"solid on intent, shaky on the error paths\").\n\n### 6. Readiness verdict\nEnd with a clear call:\n- **Ready for review** - understanding is sound; note anything to mention to\n  reviewers proactively.\n- **Study first** - list the specific files/concepts to re-read before requesting\n  review, each as a clickable `path:line`. Be concrete: \"re-read the null handling\n  in X before you send this out.\"\nKeep it honest - if they'd get grilled in review on something, say so now.\n\n### 7. If `--save`\nWrite `pr-quiz/<branch>-scorecard.md`: the questions, their answers, your grades\nand model answers, the score, and the verdict. Don't commit it unless asked.\n\n## Principles\n- **The author is the examinee, not the collaborator.** During the quiz you withhold\n  answers; you're measuring them, not helping them pass.\n- **A failed quiz is a successful outcome** - it caught a gap before a human's time\n  was spent. Frame it that way, not as a scolding.\n- **True to the code.** Every question, answer, and grade traces to a line you read.\n- **Terse and direct** in chat - the questions and the verdict, minimal preamble.\n",".claude/skills/ui-before-after/diff-shots.mjs":"// Auto-diff before/ vs after/ screenshots, classify each as\n// unchanged | changed | added | removed, and CROP each changed pair to the\n// affected region (bounding box of differing pixels + padding) - unless the\n// change spans most of the page, in which case the full frame is kept.\n// Run from frontend (so deps resolve):\n//   node <skill>/diff-shots.mjs <beforeDir> <afterDir> [outDir]\n// Env:\n//   DIFF_THRESHOLD  min fraction of differing pixels to count as changed (default 0.001)\n//   DIFF_PAD        padding px around the affected region (default 24)\n//   DIFF_PAGEWIDE   if affected bbox area / image area exceeds this, keep full frame (default 0.6)\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(path.join(process.cwd(), \"noop.js\"));\nconst pm = require(\"pixelmatch\");\nconst pixelmatch = pm.default || pm;\nconst { PNG } = require(\"pngjs\");\n\nconst beforeDir = path.resolve(process.argv[2]);\nconst afterDir = path.resolve(process.argv[3]);\nconst outDir = path.resolve(process.argv[4] || afterDir);\nconst THRESHOLD = Number(process.env.DIFF_THRESHOLD ?? \"0.001\");\nconst PAD = Number(process.env.DIFF_PAD ?? \"24\");\nconst PAGEWIDE = Number(process.env.DIFF_PAGEWIDE ?? \"0.6\");\n\nconst read = (p) => PNG.sync.read(fs.readFileSync(p));\nconst isShot = (f) => f.endsWith(\".png\") && !/__(diff|before_crop|after_crop)\\.png$/.test(f);\nconst list = (d) => (fs.existsSync(d) ? fs.readdirSync(d).filter(isShot) : []);\nconst names = [...new Set([...list(beforeDir), ...list(afterDir)])].sort();\nfs.mkdirSync(outDir, { recursive: true });\n\nfunction cropPNG(src, x, y, w, h) {\n  const out = new PNG({ width: w, height: h });\n  PNG.bitblt(src, out, x, y, w, h, 0, 0);\n  return out;\n}\nconst writePNG = (p, png) => fs.writeFileSync(p, PNG.sync.write(png));\n\n// Bounding box of differing pixels using a diff mask (alpha>0 where changed).\nfunction changedBBox(before, after, w, h) {\n  const mask = new PNG({ width: w, height: h });\n  pixelmatch(before.data, after.data, mask.data, w, h, { threshold: 0.1, diffMask: true });\n  let minX = w, minY = h, maxX = -1, maxY = -1, count = 0;\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) {\n      if (mask.data[(y * w + x) * 4 + 3] > 0) {\n        count++;\n        if (x < minX) minX = x; if (x > maxX) maxX = x;\n        if (y < minY) minY = y; if (y > maxY) maxY = y;\n      }\n    }\n  }\n  return maxX < 0 ? null : { minX, minY, maxX, maxY, count };\n}\n\nconst report = [];\nfor (const name of names) {\n  const id = name.replace(/\\.png$/, \"\");\n  const bp = path.join(beforeDir, name), ap = path.join(afterDir, name);\n  const hasB = fs.existsSync(bp), hasA = fs.existsSync(ap);\n  if (hasB && !hasA) { report.push({ id, status: \"removed\", before: bp }); continue; }\n  if (!hasB && hasA) { report.push({ id, status: \"added\", after: ap }); continue; }\n\n  const before = read(bp), after = read(ap);\n  if (before.width !== after.width || before.height !== after.height) {\n    report.push({ id, status: \"changed\", note: \"dimensions differ\", before: bp, after: ap });\n    continue;\n  }\n  const w = after.width, h = after.height;\n  const overlay = new PNG({ width: w, height: h });\n  const px = pixelmatch(before.data, after.data, overlay.data, w, h, { threshold: 0.1 });\n  const ratio = px / (w * h);\n  if (ratio <= THRESHOLD) { report.push({ id, status: \"unchanged\", ratio: Number(ratio.toFixed(5)), before: bp, after: ap }); continue; }\n\n  const box = changedBBox(before, after, w, h);\n  // Pad + clamp the affected region.\n  const x = Math.max(0, box.minX - PAD), y = Math.max(0, box.minY - PAD);\n  const x2 = Math.min(w, box.maxX + 1 + PAD), y2 = Math.min(h, box.maxY + 1 + PAD);\n  const bw = x2 - x, bh = y2 - y;\n  const pageWide = (bw * bh) / (w * h) > PAGEWIDE;\n\n  const entry = { id, status: \"changed\", ratio: Number(ratio.toFixed(5)), before: bp, after: ap, pageWide };\n  if (pageWide) {\n    // Change spans most of the page - keep the full frame, full overlay.\n    const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, overlay);\n    entry.diff = dp;\n  } else {\n    entry.bbox = { x, y, w: bw, h: bh };\n    const cb = path.join(outDir, `${id}__before_crop.png`); writePNG(cb, cropPNG(before, x, y, bw, bh));\n    const ca = path.join(outDir, `${id}__after_crop.png`); writePNG(ca, cropPNG(after, x, y, bw, bh));\n    const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, cropPNG(overlay, x, y, bw, bh));\n    entry.cropBefore = cb; entry.cropAfter = ca; entry.diff = dp;\n  }\n  report.push(entry);\n}\n\nfs.writeFileSync(path.join(outDir, \"diff-report.json\"), JSON.stringify(report, null, 2));\nconst changed = report.filter((r) => r.status !== \"unchanged\");\nconsole.log(`diffed ${report.length} view(s): ${changed.length} changed/added/removed, ${report.length - changed.length} unchanged`);\nfor (const r of changed) {\n  const tail = r.status !== \"changed\" ? \"\"\n    : r.pageWide ? \" (page-wide → full frame)\"\n    : ` (${(r.ratio * 100).toFixed(2)}%, cropped to ${r.bbox.w}×${r.bbox.h})`;\n  console.log(`  ${r.status.padEnd(9)} ${r.id}${tail}${r.note ? \" - \" + r.note : \"\"}`);\n}\n",".claude/skills/comment-review/SKILL.md":"---\nname: comment-review\ndescription: >-\n  Review and improve the code comments a PR or branch adds, against the standard in\n  docs/developer/CODE_COMMENTS.md: cut the ones that restate the code, tighten the verbose\n  ones, reframe what-comments into the why or the constraint, and add the contract\n  docs the change left missing. Runs comment-lint for the mechanical rules first,\n  then judges what a linter cannot: padding around one real fact, facts already\n  stated by a type or a name, comments that will go stale on the next unrelated\n  change, and new public surfaces left undocumented. Applies comment-only edits and\n  reports every verdict in chat. Use when asked to review, clean up, trim, thin out\n  or improve the comments on a PR or branch, to strip AI-looking narration before\n  requesting review, or when comment-lint findings need judgment rather than a\n  mechanical fix. Pass --report-only to change nothing.\nargument-hint: \"[pr-number|branch|base-ref] [--report-only]\"\nallowed-tools: Bash, Read, Grep, Glob, Edit\n---\n\n# Comment review\n\nMake the comments this branch adds fewer and worth more. Every comment kept is a line\nsomeone has to maintain and eventually reconcile with the code, so it has to pay for\nitself. A pass that only deletes is half the job: the same read should find the\ncontract, hazard and unit the change introduced and left unsaid.\n\n`$ARGUMENTS` may name a PR number, a branch, or a base ref; default is this branch\nagainst where it forked from the main line. `--report-only` reports without editing.\n\n## The standard\n\n`AGENTS.md` section \"Comments\" is the short form; `docs/developer/CODE_COMMENTS.md` is the\nauthority, with the reasoning and the worked examples. **Read\n`docs/developer/CODE_COMMENTS.md` before judging anything.** Do not work from memory of what\na good comment is: the repo has already decided, and the four jobs (contract, why,\nhazard, map) are the whole permitted set.\n\n## Hard rules\n\n1. **Comment-only diff.** Never rename, extract, reorder or restructure code in this\n   pass. Where the right fix is a code change, report it as a suggestion.\n2. **Scope is what the branch adds or edits.** Comments the branch never touched are\n   the standing backlog (`task pre-commit:comment-lint:all`), not this review.\n3. **Never delete information you cannot re-derive from the code in front of you.** If\n   verifying the comment sent you into another file, a spec, or the git log, it is\n   carrying information. Keep it; tighten at most.\n4. **Never invent a rationale.** If the why is not determinable from the code, cut the\n   comment or flag it for the author. A fabricated why gets trusted, which is worse\n   than silence.\n5. **No suppressions.** Do not add `comment-lint-allow` to make a finding go away. If a\n   rule is genuinely wrong, say which and why in the report.\n6. **Report in chat.** Never post to the PR, never reply to threads, never resolve\n   anything, unless the user asks in this session.\n7. **No ratio targets.** Volume is not the metric. A dense header on a genuinely\n   complex file can be exactly right, and the repo's highest comment-ratio commits are\n   its best-documented work. Judge one comment at a time.\n\n## Process\n\n### 1. Scope the change\n\nRead the PR's own base branch rather than assuming main, because a stacked PR whose\nbase is another PR would otherwise be reviewed together with its parent:\n\n```bash\ngh pr view <n> --json baseRefName,headRefName,title\n```\n\nThen get the head onto disk. `gh pr checkout <n>` normally, but it fails when\nanother worktree already holds that branch, and a review needs no branch of its own:\n\n```bash\ngit fetch -f origin <headRefName>:refs/review/pr<n>-head <baseRefName>:refs/review/pr<n>-base\ngit checkout --detach refs/review/pr<n>-head\nBASE=$(git merge-base HEAD refs/review/pr<n>-base)\n```\n\nA stale remote ref can fail the fetch entirely (\"incorrect old value provided\"). Fetch\nthe two branches by name as above rather than fetching everything, and confirm the\nhead commit is the PR's before reading anything.\n\nWith no PR number, review this branch against where it forked from the main line:\n`BASE=$(git merge-base HEAD origin/main)`.\n\n### 2. Mechanical pass\n\n```bash\nnode scripts/lint/comment-lint.mjs --since \"$BASE\"\n```\n\nIf it warns that oxlint is missing, run `task frontend:install` first or the TS and TSX\nhalf is silently unchecked. Each finding names a rule in\n`scripts/lint/comment-rules.mjs`; the fix is the rewrite the rule implies. The rules\nare deliberately narrow, they do not judge trailing comments at all, and each one\nexcludes readings that had an innocent form. So a clean run says nothing about\nverbosity: it is the floor, not the review.\n\n### 3. Inventory every comment the branch adds\n\n```bash\nnode .claude/skills/comment-review/added-comments.mjs \"$BASE\"\n```\n\nThis is the checklist, and it over-reports slightly by design. Every entry on it gets\na verdict. Do not rewrite it as an inline awk or perl one-liner: `$0` and `$1` in the\nscript body are rewritten by argument interpolation before the shell sees them, which\nsilently produces an empty inventory and a review that judged nothing.\n\nAlso read what the branch **removed**:\n\n```bash\ngit diff \"$BASE\"...HEAD | grep -E \"^-\\s*(//|/\\*|\\*|#)\" | head -40\n```\n\nA rewritten comment shows up as one line added and one removed, and the removed half\nis where a fact goes missing. A branch that has already had a tightening pass is the\nlikeliest place to find one.\n\n### 4. Read the code, not the diff\n\nOpen each file at the comment. A diff hunk is not enough to judge one: you need the\ncode it introduces, the signature it claims to document, and whether the fact it states\nis already carried by a type, a name, a constant, an enum's own cases, or a doc block\ntwo lines up. Two copies of one fact drift apart, which is the maintenance cost with\nnone of the value.\n\n### 5. Judge\n\nFive tests, cheapest first:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Altitude.** Does it sit at a different level of detail than the code below, either\n  lower (a precise fact the code implies but does not say) or higher (intent a reader\n  would otherwise assemble from ten lines)? Same altitude is the definition of\n  redundant.\n- **Name.** Could a better identifier, an extracted function or a named constant carry\n  it instead? Then the comment is the wrong fix.\n- **Staleness.** Will this need editing the next time the code changes for an unrelated\n  reason? A comment that tracks the code rather than constrains it will drift and then\n  mislead.\n- **Duplication.** Is this fact stated somewhere else already? Delete the copy.\n\nGive every inventoried comment exactly one verdict:\n\n- **Cut.** Restates the code, banners a section, narrates steps or history, is\n  commented-out code, duplicates a fact, or pads one real fact with three sentences\n  that only set each other up.\n- **Tighten.** The fact is real and the prose is long. Keep the fact, drop the words.\n  One fact per comment; no sentence whose only job is to introduce the next one.\n- **Reframe.** It says what the code does. State the why, the hazard or the contract\n  instead, subject to rule 4.\n- **Add.** The change introduced something a caller outside the file can reach, or an\n  invariant, unit, ordering requirement, lifetime, thread-safety or error semantic, and\n  left it undocumented. Also the map comment a genuinely complex new file needs.\n- **Keep.** Earns its place. Count these; do not list them one by one.\n\n**Do not cut:**\n\n- Hazards: \"must stay in sync with X\", \"order matters because Y\", \"do not remove, it\n  prevents Z\".\n- Units, ranges, encodings, magic-byte decodes, currency and timezone assumptions.\n  These are mostly trailing comments, which the linter leaves alone because on this\n  codebase most of them are right.\n- The alternative that was rejected, and the reason.\n- Spec, RFC and CVE references, and TODOs that carry an issue.\n- Security and failure semantics: fail-closed ordering, what is deliberately not\n  defended, ownership and lifetime.\n- Anything whose truth you had to leave the file to verify.\n\n### 6. Apply\n\nSkip this step entirely under `--report-only`.\n\nEdit comments only. Match the file's convention: Javadoc summary fragments rather than\n\"This method returns\", JSDoc on the `@app/*` seams and layer boundaries with no\n`@param` restating a typed signature, Python docstrings where the type does not carry\nthe contract. Plain ASCII prose, no em dashes, no repeated bold lead-ins.\n\nThen run the formatter for each language touched, because rewrapping a doc block\nchanges line wrapping and the format gate will fail otherwise:\n\n- Java: `task backend:format`\n- TS and TSX: `task frontend:format`\n- Python: `task engine:fix`\n\nRe-run `node scripts/lint/comment-lint.mjs --since \"$BASE\"` and confirm it is clean. Do\nnot commit unless asked. The diff is comment-only, so `git diff` is the entire review\nsurface.\n\n### 7. Report\n\nTerse, in chat:\n\n- One counts line: `N in scope: X cut, Y tightened, Z reframed, A added, K kept`.\n- Per file, one line per changed comment: a clickable `path:line`, the verdict, and the\n  reason in a few words. No before/after quoting unless it is genuinely unclear.\n- **Needs your call**, listing what you deliberately did not do: comments whose truth\n  you could not verify, rationale you would have had to invent, and the code changes a\n  comment was papering over (extract this, rename that, split the file).\n- Any linter finding you believe is wrong, naming the rule.\n\n## Principles\n\n- Fewer comments, more information in each.\n- The reader is a new contributor with this file open, not someone who read the PR.\n- Deleting a good comment costs more than keeping a mediocre one. Uncertainty means\n  keep it and flag it.\n- The additions matter as much as the cuts. A branch that leaves a new public surface\n  undocumented is not improved by trimming its narration.\n","AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n",".claude/skills/feature-walkthrough/SKILL.md":"---\nname: feature-walkthrough\ndescription: >-\n  Explain the full logic and process of the current branch end-to-end so someone\n  with no prior knowledge of the task can understand, review, and reproduce it.\n  Scopes the change from the branch diff, traces the flow across every layer it\n  touches (frontend tool/hook/component, Java controller/service/endpoint, Python\n  engine, config, i18n, tests), and produces a self-contained walkthrough document\n  with Mermaid diagrams (sequence/flow/architecture), annotated file map with\n  clickable references, before/after behavior, screenshots where a UI is involved,\n  a \"try it locally\" section, and edge cases/risks. Use when asked for a feature or\n  branch walkthrough, \"explain what this branch does\", a design/logic writeup, PR\n  reviewer onboarding, or a hand-off doc. Pass --html to also emit a rendered HTML\n  version; --no-screens to skip screenshots.\nargument-hint: \"[branch-or-area] [--html] [--no-screens]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# Feature / Branch Walkthrough\n\nTurn the current branch into a walkthrough a newcomer can follow. Audience:\n**someone who has never seen this task**. Explain the *why*, the *flow*, and *how to\ntry it* - not just a diff summary.\n\n`$ARGUMENTS` may name a branch or area to focus on; default is the current branch\nvs `main`. Flags: `--html` (also emit a rendered HTML twin), `--no-screens`.\n\n## Process\n\n### 1. Scope the change\n- `git log --oneline main..HEAD` and `git diff --stat main...HEAD` for the shape.\n- Read the PR description / commit messages for stated intent. Do **not** invent\n  history or motivation that isn't evidenced (state current behavior in present tense).\n- Classify touched files by layer:\n  - **Frontend**: tools (`frontend/src/editor/core/components/tools/*` or `.../core/tools/*`),\n    hooks (`core/hooks/tools/*`, `useToolOperation`), contexts, routes, i18n\n    (`public/locales/en-US`).\n  - **Java backend**: controllers (`.../controller/api/...`), services, models, config.\n  - **Engine**: `engine/src/stirling/{agents,contracts,api,services}`.\n  - **Config / build / docker / tests.**\n\n### 2. Trace the flow end-to-end\nFollow one real path from user action to result. For a typical PDF tool that's:\nUI control → `useToolOperation` hook → `POST /api/v1/...` → Spring controller →\nservice (PDFBox / LibreOffice / engine call) → response → review panel → download.\nRead the actual files so the narrative is true to the code, and collect the exact\nfile:line anchors you'll cite.\n\n### 3. Draw the diagrams (Mermaid)\nPick what fits; usually 2-3 of:\n- **Sequence diagram** - request/response across frontend → backend → engine.\n- **Flowchart** - the core decision/branching logic of the feature.\n- **Architecture/component** - new pieces and how they wire to existing ones.\n- **State** - if the feature has modes/steps.\nKeep nodes labeled in plain language. Validate the Mermaid parses before shipping.\n\n### 4. Screenshots (unless --no-screens)\nIf a UI is involved, capture key states with the stubbed Playwright harness\n(see the **ui-walkthrough** skill and `files-page-screenshots.spec.ts` for the\npattern) or, for before/after, capture `main` then the branch. Drop PNGs in\n`walkthrough/<feature>/` and reference them from the doc. For backend-only\nchanges, show request/response examples (curl + JSON) instead.\n\n### 5. Write the walkthrough\nCreate `walkthrough/<feature>/FEATURE-WALKTHROUGH.md` with:\n1. **TL;DR** - what the branch does and who it's for, in 3-4 sentences.\n2. **Problem & approach** - what wasn't possible before; the chosen solution.\n3. **Architecture diagram** + 1-paragraph orientation.\n4. **End-to-end flow** - the sequence diagram + a numbered walk of each step,\n   each citing the real file (clickable `path:line`).\n5. **Key files** - annotated map (path → one line on its role).\n6. **Logic deep-dive** - the flowchart + prose for the non-obvious decisions.\n7. **Behavior** - before vs after; screenshots or request/response examples.\n8. **Try it locally** - exact steps (`task dev` / `task dev:all`, the route to\n   open or the curl to run, any env like `DOCKER_ENABLE_SECURITY` or a test\n   license key). Make it copy-pasteable.\n9. **Edge cases, risks, follow-ups** - what's untested, known limits, gotchas.\n\nMarkdown is the primary deliverable - it renders with diagrams in GitHub PRs and\nIDEs, no build step, ideal for review.\n\n### 6. If `--html`\nAlso emit `walkthrough/<feature>/walkthrough.html`: the same content with Mermaid\nrendered via `mermaid.initialize({startOnLoad:true})` (script from CDN; note in\nthe file that rendering diagrams needs network, the `.md` is the offline copy) and\nscreenshots inline. Keep it self-contained otherwise.\n\n### 7. Deliver\nGive the doc path and a short chat summary. Offer to `SendUserFile` it.\n\n## Principles\n- **True to the code.** Every claim traces to a file you read; cite `path:line`.\n  No fabricated migration/version history.\n- **Newcomer-first.** Define repo-specific terms (FileContext, `useToolOperation`,\n  the `@app/*` layer cascade, stubbed vs live tests) on first use.\n- **Show, don't assert.** Prefer a diagram + a real example over adjectives.\n- Don't commit the `walkthrough/` output unless asked.\n",".claude/skills/ui-walkthrough/SKILL.md":"---\nname: ui-walkthrough\ndescription: >-\n  Full UI investigation of the current branch's feature. Enumerates every view\n  and state (empty, populated, loading, error, each dialog/menu/panel, responsive\n  breakpoints, light + dark + RTL), captures them with the stubbed Playwright\n  harness, assembles a single-image HTML walkthrough with a global light/dark\n  toggle slider, then runs two review passes: visual/consistency (alignment,\n  spacing, professionalism, dark/light parity, contrast, truncation) and\n  UX/ease-of-use (flow, discoverability, affordances, empty/error states,\n  expectations). Use when asked for a UI walkthrough, screenshot review, design\n  or QA pass, \"find anywhere to make it easier/better for users\", or before\n  merging frontend work. Pass --fix to auto-apply safe frontend fixes and\n  re-capture; --theme to limit themes; --no-rtl to skip RTL.\nargument-hint: \"[feature/area] [--fix] [--theme light|dark|both] [--no-rtl] [--breakpoints]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Walkthrough\n\nProduce a reviewable HTML walkthrough of a feature's UI in every state and theme,\nthen critique it. Optionally auto-fix and re-capture.\n\n`$ARGUMENTS` may name the feature/area to focus on. If empty, scope from the\ncurrent branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),\n`--no-rtl`, `--breakpoints` (also capture phone/narrow widths).\n\n## What this repo gives you (use it, don't reinvent)\n\n- **Stubbed Playwright project** = backend-free screenshots via `page.route()` mocks.\n  Reference implementation: `frontend/src/editor/core/tests/stubbed/files-page-screenshots.spec.ts`.\n  It already shows the light / **dark** / **RTL** passes, JWT seeding, IndexedDB\n  seeding, and dumping PNGs to a `screenshots/<area>/` folder. Copy its shape.\n- Helpers: `frontend/src/editor/core/tests/helpers/ui-helpers.ts`\n  (`uploadFiles`, `openSettings`, `waitForModalOpen`, `dismissTourTooltip`, …)\n  and the `stub-test-base` fixtures (`autoGoto`, `seedJwt`, `viewport`).\n- Config: `frontend/playwright.config.ts` (run from `frontend/`).\n- Report template: [report-template.html](report-template.html) - self-contained,\n  one big image at a time, a global light/dark slider that flips every shot,\n  thumbnail rail, prev/next + arrow keys, and a Findings tab.\n\n## Process\n\n### 1. Scope the feature\n- If `$ARGUMENTS` is empty: `git diff --name-only main...HEAD` and read the PR/commits.\n  Identify changed pages, tools (`core/components/tools/<tool>` or `core/tools/<tool>`),\n  dialogs, panels, and routes.\n- Enumerate **every view and state** to capture, e.g.:\n  empty / populated / loading / error / disabled; each dialog, menu, popover, tooltip;\n  each tab or step; selection + multi-select; success/result panel; and (if relevant)\n  permission/role variants. Write the list down before capturing - it's the report's spine.\n\n### 2. Prepare the harness (worktree-safe)\nWorktrees have no `node_modules`. From repo root:\n```\ncd frontend && npm ci                       # or junction main's node_modules (see memory)\n```\nKill any stale dev server first (it serves old modules):\n`Get-NetTCPConnection -LocalPort 5173 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`\n\n### 3. Write the capture spec\nCreate `frontend/src/editor/core/tests/stubbed/<feature>-walkthrough.spec.ts`,\nmodeled on `files-page-screenshots.spec.ts`. For each enumerated view:\n- stub the APIs it needs, drive the UI to that state, wait on a real locator\n  (not a fixed sleep), `await settle(page)` for Mantine portals, then\n  `page.screenshot({ path: shotPath(\"NN_name_<theme>\") })`.\n- Capture each view in **light and dark** (and RTL unless `--no-rtl`). Reuse the\n  `enableDarkMode` / `enableRtl` init-script pattern from the reference spec\n  (`localStorage[\"mantine-color-scheme\"]=\"dark\"` + `emulateMedia({colorScheme:\"dark\"})`).\n- Name shots `NN_<view>_<theme>.png` so light/dark pair up by suffix.\n- Prefer **stable test-ids** over translated accessible names (RTL/i18n breaks text locators).\n\nRun it: `cd frontend && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.\nAdd `--project=stubbed-firefox`/`-webkit` only if cross-browser layout matters.\n\n### 4. Build the report\n- Copy `report-template.html` to `screenshots/<feature>/walkthrough.html` (so the\n  relative `screenshots/...` image paths resolve, or rewrite paths to sit beside it).\n- Build the manifest and inject it: replace the JSON between the\n  `/*__DATA__*/` … `/*__END__*/` markers with one `views[]` entry per view\n  (`{id,title,light,dark,viewport,notes}`) and an empty `findings` object you'll\n  fill in step 5. Keep `light`/`dark` as relative paths.\n- The toggle slider answers the \"one big image + flip light/dark for all\" request:\n  it shows a single large screenshot, and switching the slider re-themes every view.\n\n### 5. Review pass 1 - visual & consistency\nOpen each screenshot (Read the PNG) and judge against the others:\nalignment & spacing rhythm, control placement, button hierarchy, typography,\n**light/dark parity** (contrast, invisible borders, washed-out text, wrong tokens),\ntruncation/overflow, RTL mirroring, focus states, icon consistency, professional polish.\nRecord each issue as a finding `{severity:high|med|low, view, title, detail, fix}`.\n\n### 6. Review pass 2 - UX & ease of use\nWalk the flow as a first-time user: discoverability, number of steps, affordance\nclarity, empty-state guidance, error recovery, destructive-action confirmation,\ndefaults, loading feedback, mobile reachability, accessible names, and whether the\nUI matches user expectations for this kind of tool. Record findings the same way.\n\nWrite both finding lists into the report's `findings.visual` / `findings.ux`,\nand add short per-view `notes`. Re-inject the manifest.\n\n### 7. If `--fix`\nOnly safe, self-contained frontend fixes (spacing, alignment, tokens, missing\ndark-mode colors, labels, aria, obvious copy). For each: edit the component/CSS,\nmark the finding `fixed:true` with what changed, then **re-run the spec** to\nre-capture the affected shots and regenerate the report. Run `task frontend:check`.\nLeave anything risky or ambiguous as a finding, not a change.\n\n### 8. Deliver\nTell the user the report path and give a tight chat summary: N views ×\nthemes captured, top findings by severity, and (if `--fix`) what changed.\nOptionally `SendUserFile` the `walkthrough.html`.\n\n## Gotchas\n- Stale `:5173` server serves old bundles - kill it before capturing (see step 2).\n- `await settle(page)` before shots or portals/transitions tear mid-capture.\n- Don't commit the generated `screenshots/` or the throwaway spec unless asked.\n","CLAUDE.md":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n",".claude/skills/ui-before-after/shoot-sections.mjs":"# Claude Directive: shoot-sections.mjs\n\nPath: `.claude/skills/ui-before-after/shoot-sections.mjs`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/shoot-sections.mjs)",".claude/skills/ui-walkthrough/make_example.py":"# Claude Directive: make_example.py\n\nPath: `.claude/skills/ui-walkthrough/make_example.py`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/make_example.py)",".claude/skills/ui-walkthrough/report-template.html":"# Claude Directive: report-template.html\n\nPath: `.claude/skills/ui-walkthrough/report-template.html`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/report-template.html)","frontend/.claude/agents/playwright-test-generator.md":"# Subagent: playwright-test-generator\n\nPath: `frontend/.claude/agents/playwright-test-generator.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-generator.md)","frontend/.claude/agents/playwright-test-healer.md":"# Subagent: playwright-test-healer\n\nPath: `frontend/.claude/agents/playwright-test-healer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-healer.md)","frontend/.claude/agents/playwright-test-planner.md":"# Subagent: playwright-test-planner\n\nPath: `frontend/.claude/agents/playwright-test-planner.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-planner.md)","frontend/src/editor/proprietary/services/heuristic/rules/core.json":"# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/core.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/core.json)","frontend/src/editor/proprietary/services/heuristic/rules/languages.json":"# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/languages.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/languages.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json)"},"files":{".claude/skills/comment-review/added-comments.mjs":"#!/usr/bin/env node\n\n// Every comment line a range adds, as `file:line text`, for the review checklist.\n//\n//   node .claude/skills/comment-review/added-comments.mjs <base-ref> [head-ref]\n//\n// Line-based on purpose: it over-reports (a `//` inside a string, a `*` opening a\n// wrapped expression) rather than miss one, and the reviewer opens the file anyway.\n// A shell one-liner cannot do this job: `$0` and `$1` in an awk or perl script are\n// rewritten by skill argument interpolation before the shell ever sees them.\n\nimport { execFileSync } from \"node:child_process\";\n\nconst [base, head = \"HEAD\"] = process.argv.slice(2);\nif (!base) {\n  process.stderr.write(\"usage: added-comments.mjs <base-ref> [head-ref]\\n\");\n  process.exit(2);\n}\n\nconst diff = execFileSync(\"git\", [\"diff\", \"-U0\", \"--no-color\", `${base}...${head}`], {\n  encoding: \"utf8\",\n  maxBuffer: 1 << 28,\n});\n\nconst OPENS_COMMENT = /^\\+\\s*(\\/\\/|\\/\\*|\\*|#|\"\"\"|'''|\\{\\/\\*)/;\n\nlet file = null;\nlet line = 0;\nlet count = 0;\n\nfor (const raw of diff.split(\"\\n\")) {\n  if (raw.startsWith(\"+++ \")) {\n    file = raw.slice(4).replace(/^b\\//, \"\").trim();\n    continue;\n  }\n  // The new-file start of the hunk. Single-line hunks omit the count, so read the\n  // number rather than splitting on the comma that may not be there.\n  const hunk = /^@@ .*?\\+(\\d+)/.exec(raw);\n  if (hunk) {\n    line = Number(hunk[1]);\n    continue;\n  }\n  if (!raw.startsWith(\"+\")) continue;\n  if (OPENS_COMMENT.test(raw)) {\n    process.stdout.write(`${file}:${line} ${raw.slice(1).trim()}\\n`);\n    count += 1;\n  }\n  line += 1;\n}\n\nprocess.stderr.write(`${count} added comment lines\\n`);\n",".claude/skills/ui-before-after/capture-spec.template.ts":"// Generic before/after capturer. NOT app-specific: it walks a targets.json that\n// the ui-before-after skill generates from the branch/PR diff, so nothing here is\n// hand-listed. Copy to src/core/tests/stubbed/ui-before-after.spec.ts, then run\n// once per (side, theme):\n//   PR_SHOT_SIDE=after  PR_SHOT_THEME=light \\\n//     npx playwright test --project=stubbed ui-before-after.spec.ts\n//\n// targets.json shape: [{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\",\n//                        \"needsFile\": true }]\nimport { test } from \"@app/tests/helpers/stub-test-base\";\nimport type { Page } from \"@playwright/test\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst SIDE = process.env.PR_SHOT_SIDE ?? \"after\";\nconst THEME = process.env.PR_SHOT_THEME ?? \"light\";\n// Capture the full viewport by default so the affected region is in frame\n// wherever it is; diff-shots.mjs crops each comparison to what actually changed.\n// Set PR_SHOT_SCOPE to a selector to narrow the capture to one container.\nconst SCOPE = process.env.PR_SHOT_SCOPE ?? \"\";\nconst ROOT = path.resolve(process.cwd(), \"screenshots\", \"ui-diff\");\nconst OUT = path.join(ROOT, SIDE);\n// A tiny sample PDF so file-dependent tool panels render. Point at a real fixture.\nconst SAMPLE_PDF = process.env.PR_SHOT_SAMPLE ?? \"src/core/tests/test-fixtures/sample.pdf\";\n\ntype Target = { id: string; url: string; name?: string; needsFile?: boolean };\nconst targets: Target[] = JSON.parse(fs.readFileSync(path.join(ROOT, \"targets.json\"), \"utf-8\"));\n\ntest.use({ autoGoto: false, viewport: { width: 1600, height: 900 }, seedJwt: true });\n\nasync function applyTheme(page: Page): Promise<void> {\n  if (THEME !== \"dark\") return;\n  await page.addInitScript(() => {\n    localStorage.setItem(\"mantine-color-scheme\", \"dark\");\n    localStorage.setItem(\"mantine-color-scheme-value\", \"dark\");\n  });\n  await page.emulateMedia({ colorScheme: \"dark\" });\n}\n\nasync function seedFile(page: Page): Promise<void> {\n  if (!fs.existsSync(SAMPLE_PDF)) return;\n  await page.goto(\"/\", { waitUntil: \"domcontentloaded\" });\n  await page.getByTestId(\"files-button\").click().catch(() => {});\n  await page.locator('[data-testid=\"file-input\"]').setInputFiles(SAMPLE_PDF).catch(() => {});\n  await page.locator(\".file-sidebar-file-item\").first().isVisible({ timeout: 8_000 }).catch(() => {});\n}\n\nfor (const t of targets) {\n  // One test per target so a single failure doesn't drop the rest.\n  test(`${SIDE}/${THEME} ${t.id}`, async ({ page }) => {\n    fs.mkdirSync(OUT, { recursive: true });\n    await applyTheme(page);\n    if (t.needsFile !== false) await seedFile(page);\n    await page.goto(t.url, { waitUntil: \"domcontentloaded\" });\n    await page.waitForTimeout(400); // settle Mantine portals/transitions\n    const shot = path.join(OUT, `${t.id}__${THEME}.png`);\n    if (SCOPE) {\n      const scope = page.locator(SCOPE).first();\n      if (await scope.isVisible({ timeout: 8_000 }).catch(() => false)) {\n        await scope.screenshot({ path: shot });\n        return;\n      }\n    }\n    // Full viewport (fixed size → stable dimensions for pixel diffing).\n    await page.screenshot({ path: shot });\n  });\n}\n",".claude/skills/ui-before-after/make_example.py":"\"\"\"Build EXAMPLE.html from montage-template.html using REAL files-page shots as\nstand-in before/after pairs (layout demo, not an actual PR diff). Inlines PNGs as\ndata URIs so the HTML is portable. Run: python make_example.py\"\"\"\nimport base64\nimport json\nimport pathlib\nimport re\n\nHERE = pathlib.Path(__file__).parent\nSHOTS = pathlib.Path(\n    r\"C:\\Users\\systo\\git\\Stirling-PDFNew\\.claude\\worktrees\\kind-faraday-522a30\"\n    r\"\\frontend\\editor\\screenshots\\files-page\"\n)\n\n\ndef uri(fname):\n    p = SHOTS / fname\n    return \"data:image/png;base64,\" + base64.b64encode(p.read_bytes()).decode() if p.exists() else None\n\n\ndata = {\n    \"pr\": \"DEMO\",\n    \"title\": \"EXAMPLE — before/after montage (layout demo, real Files-page shots; not a real PR diff)\",\n    \"base\": \"main\", \"head\": \"demo-branch\",\n    \"cropSelector\": \"[data-sidebar=\\\"tool-panel\\\"]  (real runs crop to the side; these demo shots are full-page)\",\n    \"tabs\": [\n        {\"id\": \"files\", \"title\": \"Files page\", \"ctx\": \"Each row = one flow state; left = base branch, right = this PR.\",\n         \"states\": [\n             {\"name\": \"Empty folder\", \"before\": uri(\"01_empty_state_ctas.png\"), \"after\": uri(\"02_empty_state_storage_off.png\")},\n             {\"name\": \"Files + details panel\", \"before\": uri(\"03_subtoolbar_with_files.png\"), \"after\": uri(\"06_details_panel_save_to_server.png\")},\n             {\"name\": \"Delete folder confirm\", \"before\": None, \"after\": uri(\"19_delete_folder_dialog.png\"), \"note\": \"New in this PR\"},\n         ]},\n        {\"id\": \"move\", \"title\": \"Move-to-folder dialog\",\n         \"states\": [\n             {\"name\": \"Dialog opened\", \"before\": uri(\"07_move_dialog_collapsed.png\"), \"after\": uri(\"08_move_dialog_create_folder_expanded.png\")},\n             {\"name\": \"After folder created\", \"before\": None, \"after\": uri(\"08b_move_dialog_after_create_folder.png\"), \"note\": \"New flow\"},\n         ]},\n    ],\n}\n\ntpl = (HERE / \"montage-template.html\").read_text(encoding=\"utf-8\")\nout = re.sub(\n    r\"/\\*__DATA__\\*/.*?/\\*__END__\\*/\",\n    lambda _m: \"/*__DATA__*/\" + json.dumps(data) + \"/*__END__*/\",\n    tpl, count=1, flags=re.S,\n)\n(HERE / \"EXAMPLE.html\").write_text(out, encoding=\"utf-8\")\nprint(\"wrote\", HERE / \"EXAMPLE.html\", \"(\", (HERE / \"EXAMPLE.html\").stat().st_size // 1024, \"KB )\")\n",".claude/skills/ui-before-after/SKILL.md":"---\nname: ui-before-after\ndescription: >-\n  Analyse a branch or PR and automatically capture before/after screenshots of\n  every UI surface its changes touch, then pixel-diff the pairs to surface what\n  actually changed and assemble PR-ready before/after montage images. Generic and\n  diff-driven: it derives the capture targets from the diff (changed tools/routes →\n  URLs) instead of hand-listing screens, captures \"before\" from the base branch and\n  \"after\" from the head, then keeps only the views that visually differ. Each\n  comparison is auto-cropped to the region that actually changed (the bounding box of\n  differing pixels), falling back to the full page only when the change spans most of\n  it. Use for before/after shots, a visual diff of a branch/PR, \"screenshots for the\n  PR description\", \"show what changed in the UI\", or a side-by-side of UI changes.\n  Takes a PR number/URL (resolved via gh) or a branch; defaults to the current branch\n  vs its base. Flags: --scope <selector>, --base <ref|merge-base>, --theme\n  light|dark|both, --all (capture every route, not just changed), --no-autocrop,\n  --pagewide <n>, --threshold <n>.\nargument-hint: \"[PR# | PR-url | branch] [--scope <sel>] [--base <ref>] [--theme both] [--all] [--no-autocrop]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Before / After (generic visual diff)\n\nPoint it at a branch or PR; it figures out which UI changed, screenshots every\naffected surface **before** (base) and **after** (head), pixel-diffs the pairs, and\nmontages the ones that actually changed into images for the PR description.\n\n`$ARGUMENTS`: a PR number/URL, a branch, or nothing (current branch vs base).\nBy default it captures the full viewport and auto-crops each comparison to the region\nthat changed. Flags: `--scope <css>` (narrow the *capture* to a container, e.g.\n`[data-sidebar=\"tool-panel\"]`, when you already know where the change is),\n`--no-autocrop` (keep full frames), `--pagewide <fraction>` (above this share of the\npage, skip cropping; default 0.6), `--base <ref|merge-base>`,\n`--theme light|dark|both`, `--all` (walk every route, not just changed),\n`--threshold <fraction>` (diff sensitivity, default 0.001).\n\nShares the capture harness with **ui-walkthrough** - read its SKILL.md for the\nstubbed-Playwright setup, worktree node_modules, the\nstale-`:5173` gotcha, and the dark-mode init-script. Bundled helpers:\n[capture-spec.template.ts](capture-spec.template.ts), [diff-shots.mjs](diff-shots.mjs),\n[montage-template.html](montage-template.html), [shoot-sections.mjs](shoot-sections.mjs).\n\n## Process\n\n### 1. Resolve target + base\n```\ngh pr view <pr> --json number,title,headRefName,baseRefName,url,files   # PR\n# or branch: base = merge-base(main, HEAD); head = HEAD\ngh pr diff <pr> --name-only        # or: git diff --name-only <base>...HEAD\n```\n\n### 2. Derive capture targets from the diff (the \"analyse\" step - no hand-listing)\nMap changed frontend files to URLs generically:\n- **Tools**: a changed `components/tools/<toolDir>/…` or `hooks/tools/<tool>/…` →\n  toolId → URL via the repo's own rule `getToolUrlPath` in\n  [toolsTaxonomy.ts:200](../../../frontend/src/editor/core/data/toolsTaxonomy.ts): `/` + the\n  id kebab-cased (`addPageNumbers` → `/add-page-numbers`).\n- **Pages/routes**: changed `filesPage/*` → `/files`, etc.\n- `--all`: enumerate every tool in the registry instead of just changed ones.\nWrite `frontend/screenshots/ui-diff/targets.json` =\n`[{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\" }]`. This is what makes\nit generic - the spec never names a tool.\n\n### 3. Capture AFTER (head) then BEFORE (base)\nCopy [capture-spec.template.ts](capture-spec.template.ts) →\n`src/core/tests/stubbed/ui-before-after.spec.ts` (it loops `targets.json`, seeds a\nsample PDF so file-dependent panels render, navigates to each URL, and screenshots\nthe full viewport - or the `--scope` container if given). Ensure the harness is ready\n(node_modules + icons).\n```\n# after = current head\ncd frontend && PR_SHOT_SIDE=after PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n# before = base, in an isolated worktree (copy the spec + targets.json in)\ngit worktree add ../ba-base origin/<baseRefName>        # or the merge-base\n#   set up its frontend, copy spec + screenshots/ui-diff/targets.json across, then:\ncd ../ba-base/frontend && PR_SHOT_SIDE=before PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n#   copy its screenshots/ui-diff/before/ back next to after/. Repeat with\n#   PR_SHOT_THEME=dark if --theme includes dark. Remove worktree when done.\n```\n\n### 4. Auto-diff (surface what changed)\n```\ncd frontend && node <skill>/diff-shots.mjs \\\n  screenshots/ui-diff/before screenshots/ui-diff/after screenshots/ui-diff\n```\nProduces `diff-report.json` classifying each view `unchanged | changed | added |\nremoved`. For each changed view it computes the bounding box of differing pixels and\nwrites cropped `__before_crop.png` / `__after_crop.png` / `__diff.png` to that region\n(+ padding) - **unless** the change covers more than `--pagewide` of the frame, where\nit keeps the full frame (`pageWide:true`). Drop `unchanged` - that's the noise the\nuser doesn't want.\n\n### 5. Montage the changes\nBuild the manifest from the non-unchanged entries (group by tab/tool; each becomes a\nstate row with before/after). For changed views use the cropped `cropBefore` /\n`cropAfter` from `diff-report.json` (tight on the affected region; full frame when\n`pageWide`); `added`/`removed` render the \"not present\" placeholder. Fill\n[montage-template.html](montage-template.html) (replace the `window.__BA__` data\nblock; base64-inline the PNGs for portability), then render one PNG per section with\n[shoot-sections.mjs](shoot-sections.mjs). Optionally include the `__diff.png` overlay\nas a third column.\n\n### 6. Deliver\nOutput the `montage_<tab>.png` files + a short summary (N changed / added / removed,\nM unchanged skipped) and a paste-ready Markdown block. GitHub has no PR-body image\nAPI, so tell the user to drag the PNGs into the description. Do **not** post to the\nPR.\n\n## Gotchas\n- Two installs (base worktree + head); junction main's node_modules only if its deps\n  match that ref, else `npm ci` (see ui-walkthrough's stale-dep note).\n- A view that errors on one side (refactored/removed) → that side is missing; the\n  diff marks it added/removed rather than failing the run.\n- Pixel diff needs equal dimensions, so capture at a fixed viewport (the template\n  does); a view whose size changed is reported as \"changed (dimensions differ)\",\n  uncropped.\n- Auto-crop uses a single bounding box, so two far-apart changes give one large crop\n  (or trip `--pagewide`); narrow with `--scope` if that happens.\n- `getToolUrlPath` is the source of truth for tool URLs - use it, don't guess slugs.\n- Don't commit `screenshots/`, the throwaway spec, or the base worktree.\n",".claude/skills/pr-quiz/SKILL.md":"---\nname: pr-quiz\ndescription: >-\n  Quiz the PR author on their own branch before they request review, to prove they\n  actually understand the change - especially code an AI wrote for them. Scopes the\n  branch diff vs its base, reads the changed code, then asks graded questions about\n  what changed, why, how it works, what it could break, and which edge cases it must\n  handle. Presents all questions first, waits for the author's answers, then grades\n  each honestly against the real code (Correct / Partial / Incorrect with the true\n  answer and file:line), scores it, and gives a readiness verdict that names the\n  areas to re-study before asking humans to review. Use when asked to quiz me on my\n  PR/branch, \"test my understanding before review\", a self-check gate before opening\n  a PR, or before requesting reviewers. Administered as an interactive\n  multiple-choice quiz (clickable options) by default; pass --free-text for\n  written answers, --questions N to set count, --save to write a scorecard.\nargument-hint: \"[branch-or-base-ref] [--questions N] [--free-text] [--save]\"\nallowed-tools: Bash, Read, Grep, Glob, Write, AskUserQuestion\n---\n\n# PR Quiz\n\nTest whether the **author** genuinely understands their own branch before they ask\nother people to spend time reviewing it. This is a self-check gate: the point is to\ncatch changes - often AI-written - that the author would not be able to explain or\ndefend in review. Be a fair but honest examiner, not a pushover.\n\n`$ARGUMENTS` may name a base ref or branch to diff against; default is this branch\nvs where it forked from the main line. Flags:\n- `--questions N` - target N questions (else scale to diff size, see below).\n- `--free-text` - administer as a written numbered list instead of the default\n  interactive multiple-choice.\n- `--save` - also write a scorecard file after grading.\n\n## Integrity rules (read first - the whole skill depends on these)\n\n1. **Present every question before revealing any answer.** Ask, then wait. Never\n   show the answer key alongside the questions.\n2. **Do not give hints or the answer while the quiz is open.** If the author asks\n   \"what's the answer?\" or \"is it X?\" before committing, decline warmly and tell\n   them to give their best answer first - guessing is part of the signal.\n3. **Grade truthfully.** Vague, hand-wavy, or \"the AI did it\" non-answers are\n   Partial or Incorrect, not Correct. Do not inflate the score to be nice; a false\n   pass defeats the entire purpose.\n4. **Ground everything in code you actually read.** Every question and every model\n   answer must trace to a real line in the diff. Cite `path:line`. No trivia\n   (\"how many lines?\"), no invented behavior.\n5. **Credit real understanding.** If the author explains it correctly in their own\n   words, mark it Correct even if worded differently than your key.\n\n## Process\n\n### 1. Scope the change (silently)\n- Find the base. Prefer the fork point off the main line so the quiz covers only\n  this branch's work:\n  ```bash\n  git fetch -q origin 2>/dev/null; \\\n  BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main); \\\n  git diff --stat \"$BASE\"...HEAD\n  ```\n  If `$ARGUMENTS` names a ref, diff against that instead.\n- If the diff is empty, stop and say there's nothing to quiz on.\n- Read commit messages / PR description for the *stated* intent, but verify it\n  against the actual diff - a mismatch is itself a good question.\n\n### 2. Understand the code well enough to examine on it\nRead the full diff plus enough surrounding context and related files to answer\nevery question you plan to ask. You cannot grade understanding you don't have.\nNote the non-obvious parts: the design decisions, the risky lines, the edge cases,\nthe cross-file ripples, and anything that violates or upholds repo conventions\n(for this repo e.g. `@app/*` import layering, all file ops via FileContext,\nJackson 3 / Spring Boot 4 APIs, engine typed-contract boundaries).\n\n### 3. Build the question set\nScale count to the change unless `--questions N` is given:\nsmall (< ~50 changed lines) 3-4, medium 5-8, large 9-12. Cap at 12.\nDraw from these categories - weight toward the ones the diff actually exercises:\n- **Intent** - what problem this solves; why it was needed now.\n- **Mechanism** - how a specific non-trivial piece actually works (\"walk me\n  through what `foo()` does when called with X\").\n- **Decisions & alternatives** - why this approach over an obvious alternative;\n  what a reviewer would reasonably push back on.\n- **Blast radius** - what else this touches or could break; what you'd retest.\n- **Edge cases** - inputs/states the change must handle (null, empty, large,\n  concurrent, error paths).\n- **Conventions & correctness** - does it follow the repo's rules; is there a\n  latent bug the author should be able to spot.\nPrefer questions the author can only answer if they read and understood the code.\nKeep a private answer key with `path:line` for each - do **not** show it yet.\n\n### 4. Administer the quiz\n- **Default (multiple choice):** use the `AskUserQuestion` tool. Per question write\n  3-4 options where **every** option is independently plausible - each distractor a\n  real-but-wrong reading of the code, not filler. Two hard rules so the answer\n  can't be spotted by shape rather than knowledge:\n  - **Randomise the correct option's position** across questions - never default\n    it to first. Spread it roughly evenly over the slots.\n  - **Keep all options the same depth and length.** Do not describe the correct\n    one more fully than the distractors - a longer or more-detailed option is a\n    dead giveaway. Trim the right answer or flesh out the wrong ones until a\n    reader can't tell them apart by size.\n  The tool caps a call at 4 questions, so ask in batches of 4 - but run them as\n  one continuous flow: fire the next batch immediately after the previous\n  returns, with no narration (\"Round 2 of 3\") and no grading between batches.\n  The author always has an \"Other\" free-text escape, which is fine.\n- **`--free-text`:** present all questions in one numbered list, then say\n  \"Answer in one reply; number your answers. I won't grade until you're done.\"\n  Wait for the author's answers.\n- Do not proceed to grading until every answer is in.\n\n### 5. Grade\nFor each question, in order:\n- Verdict: **Correct** / **Partial** / **Incorrect**.\n- The model answer in one or two sentences, citing the real `path:line`.\n- One line on the gap when Partial/Incorrect - what they missed and where to look.\nThen a **Score** (e.g. 6/8, counting Partial as half) and a one-line summary of\nthe pattern (e.g. \"solid on intent, shaky on the error paths\").\n\n### 6. Readiness verdict\nEnd with a clear call:\n- **Ready for review** - understanding is sound; note anything to mention to\n  reviewers proactively.\n- **Study first** - list the specific files/concepts to re-read before requesting\n  review, each as a clickable `path:line`. Be concrete: \"re-read the null handling\n  in X before you send this out.\"\nKeep it honest - if they'd get grilled in review on something, say so now.\n\n### 7. If `--save`\nWrite `pr-quiz/<branch>-scorecard.md`: the questions, their answers, your grades\nand model answers, the score, and the verdict. Don't commit it unless asked.\n\n## Principles\n- **The author is the examinee, not the collaborator.** During the quiz you withhold\n  answers; you're measuring them, not helping them pass.\n- **A failed quiz is a successful outcome** - it caught a gap before a human's time\n  was spent. Frame it that way, not as a scolding.\n- **True to the code.** Every question, answer, and grade traces to a line you read.\n- **Terse and direct** in chat - the questions and the verdict, minimal preamble.\n",".claude/skills/ui-before-after/diff-shots.mjs":"// Auto-diff before/ vs after/ screenshots, classify each as\n// unchanged | changed | added | removed, and CROP each changed pair to the\n// affected region (bounding box of differing pixels + padding) - unless the\n// change spans most of the page, in which case the full frame is kept.\n// Run from frontend (so deps resolve):\n//   node <skill>/diff-shots.mjs <beforeDir> <afterDir> [outDir]\n// Env:\n//   DIFF_THRESHOLD  min fraction of differing pixels to count as changed (default 0.001)\n//   DIFF_PAD        padding px around the affected region (default 24)\n//   DIFF_PAGEWIDE   if affected bbox area / image area exceeds this, keep full frame (default 0.6)\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(path.join(process.cwd(), \"noop.js\"));\nconst pm = require(\"pixelmatch\");\nconst pixelmatch = pm.default || pm;\nconst { PNG } = require(\"pngjs\");\n\nconst beforeDir = path.resolve(process.argv[2]);\nconst afterDir = path.resolve(process.argv[3]);\nconst outDir = path.resolve(process.argv[4] || afterDir);\nconst THRESHOLD = Number(process.env.DIFF_THRESHOLD ?? \"0.001\");\nconst PAD = Number(process.env.DIFF_PAD ?? \"24\");\nconst PAGEWIDE = Number(process.env.DIFF_PAGEWIDE ?? \"0.6\");\n\nconst read = (p) => PNG.sync.read(fs.readFileSync(p));\nconst isShot = (f) => f.endsWith(\".png\") && !/__(diff|before_crop|after_crop)\\.png$/.test(f);\nconst list = (d) => (fs.existsSync(d) ? fs.readdirSync(d).filter(isShot) : []);\nconst names = [...new Set([...list(beforeDir), ...list(afterDir)])].sort();\nfs.mkdirSync(outDir, { recursive: true });\n\nfunction cropPNG(src, x, y, w, h) {\n  const out = new PNG({ width: w, height: h });\n  PNG.bitblt(src, out, x, y, w, h, 0, 0);\n  return out;\n}\nconst writePNG = (p, png) => fs.writeFileSync(p, PNG.sync.write(png));\n\n// Bounding box of differing pixels using a diff mask (alpha>0 where changed).\nfunction changedBBox(before, after, w, h) {\n  const mask = new PNG({ width: w, height: h });\n  pixelmatch(before.data, after.data, mask.data, w, h, { threshold: 0.1, diffMask: true });\n  let minX = w, minY = h, maxX = -1, maxY = -1, count = 0;\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) {\n      if (mask.data[(y * w + x) * 4 + 3] > 0) {\n        count++;\n        if (x < minX) minX = x; if (x > maxX) maxX = x;\n        if (y < minY) minY = y; if (y > maxY) maxY = y;\n      }\n    }\n  }\n  return maxX < 0 ? null : { minX, minY, maxX, maxY, count };\n}\n\nconst report = [];\nfor (const name of names) {\n  const id = name.replace(/\\.png$/, \"\");\n  const bp = path.join(beforeDir, name), ap = path.join(afterDir, name);\n  const hasB = fs.existsSync(bp), hasA = fs.existsSync(ap);\n  if (hasB && !hasA) { report.push({ id, status: \"removed\", before: bp }); continue; }\n  if (!hasB && hasA) { report.push({ id, status: \"added\", after: ap }); continue; }\n\n  const before = read(bp), after = read(ap);\n  if (before.width !== after.width || before.height !== after.height) {\n    report.push({ id, status: \"changed\", note: \"dimensions differ\", before: bp, after: ap });\n    continue;\n  }\n  const w = after.width, h = after.height;\n  const overlay = new PNG({ width: w, height: h });\n  const px = pixelmatch(before.data, after.data, overlay.data, w, h, { threshold: 0.1 });\n  const ratio = px / (w * h);\n  if (ratio <= THRESHOLD) { report.push({ id, status: \"unchanged\", ratio: Number(ratio.toFixed(5)), before: bp, after: ap }); continue; }\n\n  const box = changedBBox(before, after, w, h);\n  // Pad + clamp the affected region.\n  const x = Math.max(0, box.minX - PAD), y = Math.max(0, box.minY - PAD);\n  const x2 = Math.min(w, box.maxX + 1 + PAD), y2 = Math.min(h, box.maxY + 1 + PAD);\n  const bw = x2 - x, bh = y2 - y;\n  const pageWide = (bw * bh) / (w * h) > PAGEWIDE;\n\n  const entry = { id, status: \"changed\", ratio: Number(ratio.toFixed(5)), before: bp, after: ap, pageWide };\n  if (pageWide) {\n    // Change spans most of the page - keep the full frame, full overlay.\n    const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, overlay);\n    entry.diff = dp;\n  } else {\n    entry.bbox = { x, y, w: bw, h: bh };\n    const cb = path.join(outDir, `${id}__before_crop.png`); writePNG(cb, cropPNG(before, x, y, bw, bh));\n    const ca = path.join(outDir, `${id}__after_crop.png`); writePNG(ca, cropPNG(after, x, y, bw, bh));\n    const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, cropPNG(overlay, x, y, bw, bh));\n    entry.cropBefore = cb; entry.cropAfter = ca; entry.diff = dp;\n  }\n  report.push(entry);\n}\n\nfs.writeFileSync(path.join(outDir, \"diff-report.json\"), JSON.stringify(report, null, 2));\nconst changed = report.filter((r) => r.status !== \"unchanged\");\nconsole.log(`diffed ${report.length} view(s): ${changed.length} changed/added/removed, ${report.length - changed.length} unchanged`);\nfor (const r of changed) {\n  const tail = r.status !== \"changed\" ? \"\"\n    : r.pageWide ? \" (page-wide → full frame)\"\n    : ` (${(r.ratio * 100).toFixed(2)}%, cropped to ${r.bbox.w}×${r.bbox.h})`;\n  console.log(`  ${r.status.padEnd(9)} ${r.id}${tail}${r.note ? \" - \" + r.note : \"\"}`);\n}\n",".claude/skills/comment-review/SKILL.md":"---\nname: comment-review\ndescription: >-\n  Review and improve the code comments a PR or branch adds, against the standard in\n  docs/developer/CODE_COMMENTS.md: cut the ones that restate the code, tighten the verbose\n  ones, reframe what-comments into the why or the constraint, and add the contract\n  docs the change left missing. Runs comment-lint for the mechanical rules first,\n  then judges what a linter cannot: padding around one real fact, facts already\n  stated by a type or a name, comments that will go stale on the next unrelated\n  change, and new public surfaces left undocumented. Applies comment-only edits and\n  reports every verdict in chat. Use when asked to review, clean up, trim, thin out\n  or improve the comments on a PR or branch, to strip AI-looking narration before\n  requesting review, or when comment-lint findings need judgment rather than a\n  mechanical fix. Pass --report-only to change nothing.\nargument-hint: \"[pr-number|branch|base-ref] [--report-only]\"\nallowed-tools: Bash, Read, Grep, Glob, Edit\n---\n\n# Comment review\n\nMake the comments this branch adds fewer and worth more. Every comment kept is a line\nsomeone has to maintain and eventually reconcile with the code, so it has to pay for\nitself. A pass that only deletes is half the job: the same read should find the\ncontract, hazard and unit the change introduced and left unsaid.\n\n`$ARGUMENTS` may name a PR number, a branch, or a base ref; default is this branch\nagainst where it forked from the main line. `--report-only` reports without editing.\n\n## The standard\n\n`AGENTS.md` section \"Comments\" is the short form; `docs/developer/CODE_COMMENTS.md` is the\nauthority, with the reasoning and the worked examples. **Read\n`docs/developer/CODE_COMMENTS.md` before judging anything.** Do not work from memory of what\na good comment is: the repo has already decided, and the four jobs (contract, why,\nhazard, map) are the whole permitted set.\n\n## Hard rules\n\n1. **Comment-only diff.** Never rename, extract, reorder or restructure code in this\n   pass. Where the right fix is a code change, report it as a suggestion.\n2. **Scope is what the branch adds or edits.** Comments the branch never touched are\n   the standing backlog (`task pre-commit:comment-lint:all`), not this review.\n3. **Never delete information you cannot re-derive from the code in front of you.** If\n   verifying the comment sent you into another file, a spec, or the git log, it is\n   carrying information. Keep it; tighten at most.\n4. **Never invent a rationale.** If the why is not determinable from the code, cut the\n   comment or flag it for the author. A fabricated why gets trusted, which is worse\n   than silence.\n5. **No suppressions.** Do not add `comment-lint-allow` to make a finding go away. If a\n   rule is genuinely wrong, say which and why in the report.\n6. **Report in chat.** Never post to the PR, never reply to threads, never resolve\n   anything, unless the user asks in this session.\n7. **No ratio targets.** Volume is not the metric. A dense header on a genuinely\n   complex file can be exactly right, and the repo's highest comment-ratio commits are\n   its best-documented work. Judge one comment at a time.\n\n## Process\n\n### 1. Scope the change\n\nRead the PR's own base branch rather than assuming main, because a stacked PR whose\nbase is another PR would otherwise be reviewed together with its parent:\n\n```bash\ngh pr view <n> --json baseRefName,headRefName,title\n```\n\nThen get the head onto disk. `gh pr checkout <n>` normally, but it fails when\nanother worktree already holds that branch, and a review needs no branch of its own:\n\n```bash\ngit fetch -f origin <headRefName>:refs/review/pr<n>-head <baseRefName>:refs/review/pr<n>-base\ngit checkout --detach refs/review/pr<n>-head\nBASE=$(git merge-base HEAD refs/review/pr<n>-base)\n```\n\nA stale remote ref can fail the fetch entirely (\"incorrect old value provided\"). Fetch\nthe two branches by name as above rather than fetching everything, and confirm the\nhead commit is the PR's before reading anything.\n\nWith no PR number, review this branch against where it forked from the main line:\n`BASE=$(git merge-base HEAD origin/main)`.\n\n### 2. Mechanical pass\n\n```bash\nnode scripts/lint/comment-lint.mjs --since \"$BASE\"\n```\n\nIf it warns that oxlint is missing, run `task frontend:install` first or the TS and TSX\nhalf is silently unchecked. Each finding names a rule in\n`scripts/lint/comment-rules.mjs`; the fix is the rewrite the rule implies. The rules\nare deliberately narrow, they do not judge trailing comments at all, and each one\nexcludes readings that had an innocent form. So a clean run says nothing about\nverbosity: it is the floor, not the review.\n\n### 3. Inventory every comment the branch adds\n\n```bash\nnode .claude/skills/comment-review/added-comments.mjs \"$BASE\"\n```\n\nThis is the checklist, and it over-reports slightly by design. Every entry on it gets\na verdict. Do not rewrite it as an inline awk or perl one-liner: `$0` and `$1` in the\nscript body are rewritten by argument interpolation before the shell sees them, which\nsilently produces an empty inventory and a review that judged nothing.\n\nAlso read what the branch **removed**:\n\n```bash\ngit diff \"$BASE\"...HEAD | grep -E \"^-\\s*(//|/\\*|\\*|#)\" | head -40\n```\n\nA rewritten comment shows up as one line added and one removed, and the removed half\nis where a fact goes missing. A branch that has already had a tightening pass is the\nlikeliest place to find one.\n\n### 4. Read the code, not the diff\n\nOpen each file at the comment. A diff hunk is not enough to judge one: you need the\ncode it introduces, the signature it claims to document, and whether the fact it states\nis already carried by a type, a name, a constant, an enum's own cases, or a doc block\ntwo lines up. Two copies of one fact drift apart, which is the maintenance cost with\nnone of the value.\n\n### 5. Judge\n\nFive tests, cheapest first:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Altitude.** Does it sit at a different level of detail than the code below, either\n  lower (a precise fact the code implies but does not say) or higher (intent a reader\n  would otherwise assemble from ten lines)? Same altitude is the definition of\n  redundant.\n- **Name.** Could a better identifier, an extracted function or a named constant carry\n  it instead? Then the comment is the wrong fix.\n- **Staleness.** Will this need editing the next time the code changes for an unrelated\n  reason? A comment that tracks the code rather than constrains it will drift and then\n  mislead.\n- **Duplication.** Is this fact stated somewhere else already? Delete the copy.\n\nGive every inventoried comment exactly one verdict:\n\n- **Cut.** Restates the code, banners a section, narrates steps or history, is\n  commented-out code, duplicates a fact, or pads one real fact with three sentences\n  that only set each other up.\n- **Tighten.** The fact is real and the prose is long. Keep the fact, drop the words.\n  One fact per comment; no sentence whose only job is to introduce the next one.\n- **Reframe.** It says what the code does. State the why, the hazard or the contract\n  instead, subject to rule 4.\n- **Add.** The change introduced something a caller outside the file can reach, or an\n  invariant, unit, ordering requirement, lifetime, thread-safety or error semantic, and\n  left it undocumented. Also the map comment a genuinely complex new file needs.\n- **Keep.** Earns its place. Count these; do not list them one by one.\n\n**Do not cut:**\n\n- Hazards: \"must stay in sync with X\", \"order matters because Y\", \"do not remove, it\n  prevents Z\".\n- Units, ranges, encodings, magic-byte decodes, currency and timezone assumptions.\n  These are mostly trailing comments, which the linter leaves alone because on this\n  codebase most of them are right.\n- The alternative that was rejected, and the reason.\n- Spec, RFC and CVE references, and TODOs that carry an issue.\n- Security and failure semantics: fail-closed ordering, what is deliberately not\n  defended, ownership and lifetime.\n- Anything whose truth you had to leave the file to verify.\n\n### 6. Apply\n\nSkip this step entirely under `--report-only`.\n\nEdit comments only. Match the file's convention: Javadoc summary fragments rather than\n\"This method returns\", JSDoc on the `@app/*` seams and layer boundaries with no\n`@param` restating a typed signature, Python docstrings where the type does not carry\nthe contract. Plain ASCII prose, no em dashes, no repeated bold lead-ins.\n\nThen run the formatter for each language touched, because rewrapping a doc block\nchanges line wrapping and the format gate will fail otherwise:\n\n- Java: `task backend:format`\n- TS and TSX: `task frontend:format`\n- Python: `task engine:fix`\n\nRe-run `node scripts/lint/comment-lint.mjs --since \"$BASE\"` and confirm it is clean. Do\nnot commit unless asked. The diff is comment-only, so `git diff` is the entire review\nsurface.\n\n### 7. Report\n\nTerse, in chat:\n\n- One counts line: `N in scope: X cut, Y tightened, Z reframed, A added, K kept`.\n- Per file, one line per changed comment: a clickable `path:line`, the verdict, and the\n  reason in a few words. No before/after quoting unless it is genuinely unclear.\n- **Needs your call**, listing what you deliberately did not do: comments whose truth\n  you could not verify, rationale you would have had to invent, and the code changes a\n  comment was papering over (extract this, rename that, split the file).\n- Any linter finding you believe is wrong, naming the rule.\n\n## Principles\n\n- Fewer comments, more information in each.\n- The reader is a new contributor with this file open, not someone who read the PR.\n- Deleting a good comment costs more than keeping a mediocre one. Uncertainty means\n  keep it and flag it.\n- The additions matter as much as the cuts. A branch that leaves a new public surface\n  undocumented is not improved by trimming its narration.\n","AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n",".claude/skills/feature-walkthrough/SKILL.md":"---\nname: feature-walkthrough\ndescription: >-\n  Explain the full logic and process of the current branch end-to-end so someone\n  with no prior knowledge of the task can understand, review, and reproduce it.\n  Scopes the change from the branch diff, traces the flow across every layer it\n  touches (frontend tool/hook/component, Java controller/service/endpoint, Python\n  engine, config, i18n, tests), and produces a self-contained walkthrough document\n  with Mermaid diagrams (sequence/flow/architecture), annotated file map with\n  clickable references, before/after behavior, screenshots where a UI is involved,\n  a \"try it locally\" section, and edge cases/risks. Use when asked for a feature or\n  branch walkthrough, \"explain what this branch does\", a design/logic writeup, PR\n  reviewer onboarding, or a hand-off doc. Pass --html to also emit a rendered HTML\n  version; --no-screens to skip screenshots.\nargument-hint: \"[branch-or-area] [--html] [--no-screens]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# Feature / Branch Walkthrough\n\nTurn the current branch into a walkthrough a newcomer can follow. Audience:\n**someone who has never seen this task**. Explain the *why*, the *flow*, and *how to\ntry it* - not just a diff summary.\n\n`$ARGUMENTS` may name a branch or area to focus on; default is the current branch\nvs `main`. Flags: `--html` (also emit a rendered HTML twin), `--no-screens`.\n\n## Process\n\n### 1. Scope the change\n- `git log --oneline main..HEAD` and `git diff --stat main...HEAD` for the shape.\n- Read the PR description / commit messages for stated intent. Do **not** invent\n  history or motivation that isn't evidenced (state current behavior in present tense).\n- Classify touched files by layer:\n  - **Frontend**: tools (`frontend/src/editor/core/components/tools/*` or `.../core/tools/*`),\n    hooks (`core/hooks/tools/*`, `useToolOperation`), contexts, routes, i18n\n    (`public/locales/en-US`).\n  - **Java backend**: controllers (`.../controller/api/...`), services, models, config.\n  - **Engine**: `engine/src/stirling/{agents,contracts,api,services}`.\n  - **Config / build / docker / tests.**\n\n### 2. Trace the flow end-to-end\nFollow one real path from user action to result. For a typical PDF tool that's:\nUI control → `useToolOperation` hook → `POST /api/v1/...` → Spring controller →\nservice (PDFBox / LibreOffice / engine call) → response → review panel → download.\nRead the actual files so the narrative is true to the code, and collect the exact\nfile:line anchors you'll cite.\n\n### 3. Draw the diagrams (Mermaid)\nPick what fits; usually 2-3 of:\n- **Sequence diagram** - request/response across frontend → backend → engine.\n- **Flowchart** - the core decision/branching logic of the feature.\n- **Architecture/component** - new pieces and how they wire to existing ones.\n- **State** - if the feature has modes/steps.\nKeep nodes labeled in plain language. Validate the Mermaid parses before shipping.\n\n### 4. Screenshots (unless --no-screens)\nIf a UI is involved, capture key states with the stubbed Playwright harness\n(see the **ui-walkthrough** skill and `files-page-screenshots.spec.ts` for the\npattern) or, for before/after, capture `main` then the branch. Drop PNGs in\n`walkthrough/<feature>/` and reference them from the doc. For backend-only\nchanges, show request/response examples (curl + JSON) instead.\n\n### 5. Write the walkthrough\nCreate `walkthrough/<feature>/FEATURE-WALKTHROUGH.md` with:\n1. **TL;DR** - what the branch does and who it's for, in 3-4 sentences.\n2. **Problem & approach** - what wasn't possible before; the chosen solution.\n3. **Architecture diagram** + 1-paragraph orientation.\n4. **End-to-end flow** - the sequence diagram + a numbered walk of each step,\n   each citing the real file (clickable `path:line`).\n5. **Key files** - annotated map (path → one line on its role).\n6. **Logic deep-dive** - the flowchart + prose for the non-obvious decisions.\n7. **Behavior** - before vs after; screenshots or request/response examples.\n8. **Try it locally** - exact steps (`task dev` / `task dev:all`, the route to\n   open or the curl to run, any env like `DOCKER_ENABLE_SECURITY` or a test\n   license key). Make it copy-pasteable.\n9. **Edge cases, risks, follow-ups** - what's untested, known limits, gotchas.\n\nMarkdown is the primary deliverable - it renders with diagrams in GitHub PRs and\nIDEs, no build step, ideal for review.\n\n### 6. If `--html`\nAlso emit `walkthrough/<feature>/walkthrough.html`: the same content with Mermaid\nrendered via `mermaid.initialize({startOnLoad:true})` (script from CDN; note in\nthe file that rendering diagrams needs network, the `.md` is the offline copy) and\nscreenshots inline. Keep it self-contained otherwise.\n\n### 7. Deliver\nGive the doc path and a short chat summary. Offer to `SendUserFile` it.\n\n## Principles\n- **True to the code.** Every claim traces to a file you read; cite `path:line`.\n  No fabricated migration/version history.\n- **Newcomer-first.** Define repo-specific terms (FileContext, `useToolOperation`,\n  the `@app/*` layer cascade, stubbed vs live tests) on first use.\n- **Show, don't assert.** Prefer a diagram + a real example over adjectives.\n- Don't commit the `walkthrough/` output unless asked.\n",".claude/skills/ui-walkthrough/SKILL.md":"---\nname: ui-walkthrough\ndescription: >-\n  Full UI investigation of the current branch's feature. Enumerates every view\n  and state (empty, populated, loading, error, each dialog/menu/panel, responsive\n  breakpoints, light + dark + RTL), captures them with the stubbed Playwright\n  harness, assembles a single-image HTML walkthrough with a global light/dark\n  toggle slider, then runs two review passes: visual/consistency (alignment,\n  spacing, professionalism, dark/light parity, contrast, truncation) and\n  UX/ease-of-use (flow, discoverability, affordances, empty/error states,\n  expectations). Use when asked for a UI walkthrough, screenshot review, design\n  or QA pass, \"find anywhere to make it easier/better for users\", or before\n  merging frontend work. Pass --fix to auto-apply safe frontend fixes and\n  re-capture; --theme to limit themes; --no-rtl to skip RTL.\nargument-hint: \"[feature/area] [--fix] [--theme light|dark|both] [--no-rtl] [--breakpoints]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Walkthrough\n\nProduce a reviewable HTML walkthrough of a feature's UI in every state and theme,\nthen critique it. Optionally auto-fix and re-capture.\n\n`$ARGUMENTS` may name the feature/area to focus on. If empty, scope from the\ncurrent branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),\n`--no-rtl`, `--breakpoints` (also capture phone/narrow widths).\n\n## What this repo gives you (use it, don't reinvent)\n\n- **Stubbed Playwright project** = backend-free screenshots via `page.route()` mocks.\n  Reference implementation: `frontend/src/editor/core/tests/stubbed/files-page-screenshots.spec.ts`.\n  It already shows the light / **dark** / **RTL** passes, JWT seeding, IndexedDB\n  seeding, and dumping PNGs to a `screenshots/<area>/` folder. Copy its shape.\n- Helpers: `frontend/src/editor/core/tests/helpers/ui-helpers.ts`\n  (`uploadFiles`, `openSettings`, `waitForModalOpen`, `dismissTourTooltip`, …)\n  and the `stub-test-base` fixtures (`autoGoto`, `seedJwt`, `viewport`).\n- Config: `frontend/playwright.config.ts` (run from `frontend/`).\n- Report template: [report-template.html](report-template.html) - self-contained,\n  one big image at a time, a global light/dark slider that flips every shot,\n  thumbnail rail, prev/next + arrow keys, and a Findings tab.\n\n## Process\n\n### 1. Scope the feature\n- If `$ARGUMENTS` is empty: `git diff --name-only main...HEAD` and read the PR/commits.\n  Identify changed pages, tools (`core/components/tools/<tool>` or `core/tools/<tool>`),\n  dialogs, panels, and routes.\n- Enumerate **every view and state** to capture, e.g.:\n  empty / populated / loading / error / disabled; each dialog, menu, popover, tooltip;\n  each tab or step; selection + multi-select; success/result panel; and (if relevant)\n  permission/role variants. Write the list down before capturing - it's the report's spine.\n\n### 2. Prepare the harness (worktree-safe)\nWorktrees have no `node_modules`. From repo root:\n```\ncd frontend && npm ci                       # or junction main's node_modules (see memory)\n```\nKill any stale dev server first (it serves old modules):\n`Get-NetTCPConnection -LocalPort 5173 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`\n\n### 3. Write the capture spec\nCreate `frontend/src/editor/core/tests/stubbed/<feature>-walkthrough.spec.ts`,\nmodeled on `files-page-screenshots.spec.ts`. For each enumerated view:\n- stub the APIs it needs, drive the UI to that state, wait on a real locator\n  (not a fixed sleep), `await settle(page)` for Mantine portals, then\n  `page.screenshot({ path: shotPath(\"NN_name_<theme>\") })`.\n- Capture each view in **light and dark** (and RTL unless `--no-rtl`). Reuse the\n  `enableDarkMode` / `enableRtl` init-script pattern from the reference spec\n  (`localStorage[\"mantine-color-scheme\"]=\"dark\"` + `emulateMedia({colorScheme:\"dark\"})`).\n- Name shots `NN_<view>_<theme>.png` so light/dark pair up by suffix.\n- Prefer **stable test-ids** over translated accessible names (RTL/i18n breaks text locators).\n\nRun it: `cd frontend && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.\nAdd `--project=stubbed-firefox`/`-webkit` only if cross-browser layout matters.\n\n### 4. Build the report\n- Copy `report-template.html` to `screenshots/<feature>/walkthrough.html` (so the\n  relative `screenshots/...` image paths resolve, or rewrite paths to sit beside it).\n- Build the manifest and inject it: replace the JSON between the\n  `/*__DATA__*/` … `/*__END__*/` markers with one `views[]` entry per view\n  (`{id,title,light,dark,viewport,notes}`) and an empty `findings` object you'll\n  fill in step 5. Keep `light`/`dark` as relative paths.\n- The toggle slider answers the \"one big image + flip light/dark for all\" request:\n  it shows a single large screenshot, and switching the slider re-themes every view.\n\n### 5. Review pass 1 - visual & consistency\nOpen each screenshot (Read the PNG) and judge against the others:\nalignment & spacing rhythm, control placement, button hierarchy, typography,\n**light/dark parity** (contrast, invisible borders, washed-out text, wrong tokens),\ntruncation/overflow, RTL mirroring, focus states, icon consistency, professional polish.\nRecord each issue as a finding `{severity:high|med|low, view, title, detail, fix}`.\n\n### 6. Review pass 2 - UX & ease of use\nWalk the flow as a first-time user: discoverability, number of steps, affordance\nclarity, empty-state guidance, error recovery, destructive-action confirmation,\ndefaults, loading feedback, mobile reachability, accessible names, and whether the\nUI matches user expectations for this kind of tool. Record findings the same way.\n\nWrite both finding lists into the report's `findings.visual` / `findings.ux`,\nand add short per-view `notes`. Re-inject the manifest.\n\n### 7. If `--fix`\nOnly safe, self-contained frontend fixes (spacing, alignment, tokens, missing\ndark-mode colors, labels, aria, obvious copy). For each: edit the component/CSS,\nmark the finding `fixed:true` with what changed, then **re-run the spec** to\nre-capture the affected shots and regenerate the report. Run `task frontend:check`.\nLeave anything risky or ambiguous as a finding, not a change.\n\n### 8. Deliver\nTell the user the report path and give a tight chat summary: N views ×\nthemes captured, top findings by severity, and (if `--fix`) what changed.\nOptionally `SendUserFile` the `walkthrough.html`.\n\n## Gotchas\n- Stale `:5173` server serves old bundles - kill it before capturing (see step 2).\n- `await settle(page)` before shots or portals/transitions tear mid-capture.\n- Don't commit the generated `screenshots/` or the throwaway spec unless asked.\n","CLAUDE.md":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n",".claude/skills/ui-before-after/shoot-sections.mjs":"# Claude Directive: shoot-sections.mjs\n\nPath: `.claude/skills/ui-before-after/shoot-sections.mjs`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/shoot-sections.mjs)",".claude/skills/ui-walkthrough/make_example.py":"# Claude Directive: make_example.py\n\nPath: `.claude/skills/ui-walkthrough/make_example.py`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/make_example.py)",".claude/skills/ui-walkthrough/report-template.html":"# Claude Directive: report-template.html\n\nPath: `.claude/skills/ui-walkthrough/report-template.html`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/report-template.html)","frontend/.claude/agents/playwright-test-generator.md":"# Subagent: playwright-test-generator\n\nPath: `frontend/.claude/agents/playwright-test-generator.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-generator.md)","frontend/.claude/agents/playwright-test-healer.md":"# Subagent: playwright-test-healer\n\nPath: `frontend/.claude/agents/playwright-test-healer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-healer.md)","frontend/.claude/agents/playwright-test-planner.md":"# Subagent: playwright-test-planner\n\nPath: `frontend/.claude/agents/playwright-test-planner.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-planner.md)","frontend/src/editor/proprietary/services/heuristic/rules/core.json":"# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/core.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/core.json)","frontend/src/editor/proprietary/services/heuristic/rules/languages.json":"# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/languages.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/languages.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json)","frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n","isInternal":false,"tokens":7181,"sizeBytes":31586},{"name":"SKILL.md","path":".claude/skills/comment-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/comment-review/SKILL.md","title":"comment-review","category":"anthropic-skill","format":"markdown","content":"---\nname: comment-review\ndescription: >-\n  Review and improve the code comments a PR or branch adds, against the standard in\n  docs/developer/CODE_COMMENTS.md: cut the ones that restate the code, tighten the verbose\n  ones, reframe what-comments into the why or the constraint, and add the contract\n  docs the change left missing. Runs comment-lint for the mechanical rules first,\n  then judges what a linter cannot: padding around one real fact, facts already\n  stated by a type or a name, comments that will go stale on the next unrelated\n  change, and new public surfaces left undocumented. Applies comment-only edits and\n  reports every verdict in chat. Use when asked to review, clean up, trim, thin out\n  or improve the comments on a PR or branch, to strip AI-looking narration before\n  requesting review, or when comment-lint findings need judgment rather than a\n  mechanical fix. Pass --report-only to change nothing.\nargument-hint: \"[pr-number|branch|base-ref] [--report-only]\"\nallowed-tools: Bash, Read, Grep, Glob, Edit\n---\n\n# Comment review\n\nMake the comments this branch adds fewer and worth more. Every comment kept is a line\nsomeone has to maintain and eventually reconcile with the code, so it has to pay for\nitself. A pass that only deletes is half the job: the same read should find the\ncontract, hazard and unit the change introduced and left unsaid.\n\n`$ARGUMENTS` may name a PR number, a branch, or a base ref; default is this branch\nagainst where it forked from the main line. `--report-only` reports without editing.\n\n## The standard\n\n`AGENTS.md` section \"Comments\" is the short form; `docs/developer/CODE_COMMENTS.md` is the\nauthority, with the reasoning and the worked examples. **Read\n`docs/developer/CODE_COMMENTS.md` before judging anything.** Do not work from memory of what\na good comment is: the repo has already decided, and the four jobs (contract, why,\nhazard, map) are the whole permitted set.\n\n## Hard rules\n\n1. **Comment-only diff.** Never rename, extract, reorder or restructure code in this\n   pass. Where the right fix is a code change, report it as a suggestion.\n2. **Scope is what the branch adds or edits.** Comments the branch never touched are\n   the standing backlog (`task pre-commit:comment-lint:all`), not this review.\n3. **Never delete information you cannot re-derive from the code in front of you.** If\n   verifying the comment sent you into another file, a spec, or the git log, it is\n   carrying information. Keep it; tighten at most.\n4. **Never invent a rationale.** If the why is not determinable from the code, cut the\n   comment or flag it for the author. A fabricated why gets trusted, which is worse\n   than silence.\n5. **No suppressions.** Do not add `comment-lint-allow` to make a finding go away. If a\n   rule is genuinely wrong, say which and why in the report.\n6. **Report in chat.** Never post to the PR, never reply to threads, never resolve\n   anything, unless the user asks in this session.\n7. **No ratio targets.** Volume is not the metric. A dense header on a genuinely\n   complex file can be exactly right, and the repo's highest comment-ratio commits are\n   its best-documented work. Judge one comment at a time.\n\n## Process\n\n### 1. Scope the change\n\nRead the PR's own base branch rather than assuming main, because a stacked PR whose\nbase is another PR would otherwise be reviewed together with its parent:\n\n```bash\ngh pr view <n> --json baseRefName,headRefName,title\n```\n\nThen get the head onto disk. `gh pr checkout <n>` normally, but it fails when\nanother worktree already holds that branch, and a review needs no branch of its own:\n\n```bash\ngit fetch -f origin <headRefName>:refs/review/pr<n>-head <baseRefName>:refs/review/pr<n>-base\ngit checkout --detach refs/review/pr<n>-head\nBASE=$(git merge-base HEAD refs/review/pr<n>-base)\n```\n\nA stale remote ref can fail the fetch entirely (\"incorrect old value provided\"). Fetch\nthe two branches by name as above rather than fetching everything, and confirm the\nhead commit is the PR's before reading anything.\n\nWith no PR number, review this branch against where it forked from the main line:\n`BASE=$(git merge-base HEAD origin/main)`.\n\n### 2. Mechanical pass\n\n```bash\nnode scripts/lint/comment-lint.mjs --since \"$BASE\"\n```\n\nIf it warns that oxlint is missing, run `task frontend:install` first or the TS and TSX\nhalf is silently unchecked. Each finding names a rule in\n`scripts/lint/comment-rules.mjs`; the fix is the rewrite the rule implies. The rules\nare deliberately narrow, they do not judge trailing comments at all, and each one\nexcludes readings that had an innocent form. So a clean run says nothing about\nverbosity: it is the floor, not the review.\n\n### 3. Inventory every comment the branch adds\n\n```bash\nnode .claude/skills/comment-review/added-comments.mjs \"$BASE\"\n```\n\nThis is the checklist, and it over-reports slightly by design. Every entry on it gets\na verdict. Do not rewrite it as an inline awk or perl one-liner: `$0` and `$1` in the\nscript body are rewritten by argument interpolation before the shell sees them, which\nsilently produces an empty inventory and a review that judged nothing.\n\nAlso read what the branch **removed**:\n\n```bash\ngit diff \"$BASE\"...HEAD | grep -E \"^-\\s*(//|/\\*|\\*|#)\" | head -40\n```\n\nA rewritten comment shows up as one line added and one removed, and the removed half\nis where a fact goes missing. A branch that has already had a tightening pass is the\nlikeliest place to find one.\n\n### 4. Read the code, not the diff\n\nOpen each file at the comment. A diff hunk is not enough to judge one: you need the\ncode it introduces, the signature it claims to document, and whether the fact it states\nis already carried by a type, a name, a constant, an enum's own cases, or a doc block\ntwo lines up. Two copies of one fact drift apart, which is the maintenance cost with\nnone of the value.\n\n### 5. Judge\n\nFive tests, cheapest first:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Altitude.** Does it sit at a different level of detail than the code below, either\n  lower (a precise fact the code implies but does not say) or higher (intent a reader\n  would otherwise assemble from ten lines)? Same altitude is the definition of\n  redundant.\n- **Name.** Could a better identifier, an extracted function or a named constant carry\n  it instead? Then the comment is the wrong fix.\n- **Staleness.** Will this need editing the next time the code changes for an unrelated\n  reason? A comment that tracks the code rather than constrains it will drift and then\n  mislead.\n- **Duplication.** Is this fact stated somewhere else already? Delete the copy.\n\nGive every inventoried comment exactly one verdict:\n\n- **Cut.** Restates the code, banners a section, narrates steps or history, is\n  commented-out code, duplicates a fact, or pads one real fact with three sentences\n  that only set each other up.\n- **Tighten.** The fact is real and the prose is long. Keep the fact, drop the words.\n  One fact per comment; no sentence whose only job is to introduce the next one.\n- **Reframe.** It says what the code does. State the why, the hazard or the contract\n  instead, subject to rule 4.\n- **Add.** The change introduced something a caller outside the file can reach, or an\n  invariant, unit, ordering requirement, lifetime, thread-safety or error semantic, and\n  left it undocumented. Also the map comment a genuinely complex new file needs.\n- **Keep.** Earns its place. Count these; do not list them one by one.\n\n**Do not cut:**\n\n- Hazards: \"must stay in sync with X\", \"order matters because Y\", \"do not remove, it\n  prevents Z\".\n- Units, ranges, encodings, magic-byte decodes, currency and timezone assumptions.\n  These are mostly trailing comments, which the linter leaves alone because on this\n  codebase most of them are right.\n- The alternative that was rejected, and the reason.\n- Spec, RFC and CVE references, and TODOs that carry an issue.\n- Security and failure semantics: fail-closed ordering, what is deliberately not\n  defended, ownership and lifetime.\n- Anything whose truth you had to leave the file to verify.\n\n### 6. Apply\n\nSkip this step entirely under `--report-only`.\n\nEdit comments only. Match the file's convention: Javadoc summary fragments rather than\n\"This method returns\", JSDoc on the `@app/*` seams and layer boundaries with no\n`@param` restating a typed signature, Python docstrings where the type does not carry\nthe contract. Plain ASCII prose, no em dashes, no repeated bold lead-ins.\n\nThen run the formatter for each language touched, because rewrapping a doc block\nchanges line wrapping and the format gate will fail otherwise:\n\n- Java: `task backend:format`\n- TS and TSX: `task frontend:format`\n- Python: `task engine:fix`\n\nRe-run `node scripts/lint/comment-lint.mjs --since \"$BASE\"` and confirm it is clean. Do\nnot commit unless asked. The diff is comment-only, so `git diff` is the entire review\nsurface.\n\n### 7. Report\n\nTerse, in chat:\n\n- One counts line: `N in scope: X cut, Y tightened, Z reframed, A added, K kept`.\n- Per file, one line per changed comment: a clickable `path:line`, the verdict, and the\n  reason in a few words. No before/after quoting unless it is genuinely unclear.\n- **Needs your call**, listing what you deliberately did not do: comments whose truth\n  you could not verify, rationale you would have had to invent, and the code changes a\n  comment was papering over (extract this, rename that, split the file).\n- Any linter finding you believe is wrong, naming the rule.\n\n## Principles\n\n- Fewer comments, more information in each.\n- The reader is a new contributor with this file open, not someone who read the PR.\n- Deleting a good comment costs more than keeping a mediocre one. Uncertainty means\n  keep it and flag it.\n- The additions matter as much as the cuts. A branch that leaves a new public surface\n  undocumented is not improved by trimming its narration.\n","frontmatter":{"name":"comment-review","description":"Review and improve the code comments a PR or branch adds, against the standard in docs/developer/CODE_COMMENTS.md: cut the ones that restate the code, tighten the verbose ones, reframe what-comments into the why or the constraint, and add the contract docs the change left missing. Runs comment-lint for the mechanical rules first, then judges what a linter cannot: padding around one real fact, facts already stated by a type or a name, comments that will go stale on the next unrelated change, and new public surfaces left undocumented. Applies comment-only edits and reports every verdict in chat. Use when asked to review, clean up, trim, thin out or improve the comments on a PR or branch, to strip AI-looking narration before requesting review, or when comment-lint findings need judgment rather than a mechanical fix. Pass --report-only to change nothing.","argument-hint":"[pr-number|branch|base-ref] [--report-only]","allowed-tools":"Bash, Read, Grep, Glob, Edit","allowedTools":["Bash,","Read,","Grep,","Glob,","Edit"]},"allowedTools":["Bash,","Read,","Grep,","Glob,","Edit"],"isInternal":false,"tokens":2388,"sizeBytes":9901},{"name":"SKILL.md","path":".claude/skills/feature-walkthrough/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/feature-walkthrough/SKILL.md","title":"feature-walkthrough","category":"anthropic-skill","format":"markdown","content":"---\nname: feature-walkthrough\ndescription: >-\n  Explain the full logic and process of the current branch end-to-end so someone\n  with no prior knowledge of the task can understand, review, and reproduce it.\n  Scopes the change from the branch diff, traces the flow across every layer it\n  touches (frontend tool/hook/component, Java controller/service/endpoint, Python\n  engine, config, i18n, tests), and produces a self-contained walkthrough document\n  with Mermaid diagrams (sequence/flow/architecture), annotated file map with\n  clickable references, before/after behavior, screenshots where a UI is involved,\n  a \"try it locally\" section, and edge cases/risks. Use when asked for a feature or\n  branch walkthrough, \"explain what this branch does\", a design/logic writeup, PR\n  reviewer onboarding, or a hand-off doc. Pass --html to also emit a rendered HTML\n  version; --no-screens to skip screenshots.\nargument-hint: \"[branch-or-area] [--html] [--no-screens]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# Feature / Branch Walkthrough\n\nTurn the current branch into a walkthrough a newcomer can follow. Audience:\n**someone who has never seen this task**. Explain the *why*, the *flow*, and *how to\ntry it* - not just a diff summary.\n\n`$ARGUMENTS` may name a branch or area to focus on; default is the current branch\nvs `main`. Flags: `--html` (also emit a rendered HTML twin), `--no-screens`.\n\n## Process\n\n### 1. Scope the change\n- `git log --oneline main..HEAD` and `git diff --stat main...HEAD` for the shape.\n- Read the PR description / commit messages for stated intent. Do **not** invent\n  history or motivation that isn't evidenced (state current behavior in present tense).\n- Classify touched files by layer:\n  - **Frontend**: tools (`frontend/src/editor/core/components/tools/*` or `.../core/tools/*`),\n    hooks (`core/hooks/tools/*`, `useToolOperation`), contexts, routes, i18n\n    (`public/locales/en-US`).\n  - **Java backend**: controllers (`.../controller/api/...`), services, models, config.\n  - **Engine**: `engine/src/stirling/{agents,contracts,api,services}`.\n  - **Config / build / docker / tests.**\n\n### 2. Trace the flow end-to-end\nFollow one real path from user action to result. For a typical PDF tool that's:\nUI control → `useToolOperation` hook → `POST /api/v1/...` → Spring controller →\nservice (PDFBox / LibreOffice / engine call) → response → review panel → download.\nRead the actual files so the narrative is true to the code, and collect the exact\nfile:line anchors you'll cite.\n\n### 3. Draw the diagrams (Mermaid)\nPick what fits; usually 2-3 of:\n- **Sequence diagram** - request/response across frontend → backend → engine.\n- **Flowchart** - the core decision/branching logic of the feature.\n- **Architecture/component** - new pieces and how they wire to existing ones.\n- **State** - if the feature has modes/steps.\nKeep nodes labeled in plain language. Validate the Mermaid parses before shipping.\n\n### 4. Screenshots (unless --no-screens)\nIf a UI is involved, capture key states with the stubbed Playwright harness\n(see the **ui-walkthrough** skill and `files-page-screenshots.spec.ts` for the\npattern) or, for before/after, capture `main` then the branch. Drop PNGs in\n`walkthrough/<feature>/` and reference them from the doc. For backend-only\nchanges, show request/response examples (curl + JSON) instead.\n\n### 5. Write the walkthrough\nCreate `walkthrough/<feature>/FEATURE-WALKTHROUGH.md` with:\n1. **TL;DR** - what the branch does and who it's for, in 3-4 sentences.\n2. **Problem & approach** - what wasn't possible before; the chosen solution.\n3. **Architecture diagram** + 1-paragraph orientation.\n4. **End-to-end flow** - the sequence diagram + a numbered walk of each step,\n   each citing the real file (clickable `path:line`).\n5. **Key files** - annotated map (path → one line on its role).\n6. **Logic deep-dive** - the flowchart + prose for the non-obvious decisions.\n7. **Behavior** - before vs after; screenshots or request/response examples.\n8. **Try it locally** - exact steps (`task dev` / `task dev:all`, the route to\n   open or the curl to run, any env like `DOCKER_ENABLE_SECURITY` or a test\n   license key). Make it copy-pasteable.\n9. **Edge cases, risks, follow-ups** - what's untested, known limits, gotchas.\n\nMarkdown is the primary deliverable - it renders with diagrams in GitHub PRs and\nIDEs, no build step, ideal for review.\n\n### 6. If `--html`\nAlso emit `walkthrough/<feature>/walkthrough.html`: the same content with Mermaid\nrendered via `mermaid.initialize({startOnLoad:true})` (script from CDN; note in\nthe file that rendering diagrams needs network, the `.md` is the offline copy) and\nscreenshots inline. Keep it self-contained otherwise.\n\n### 7. Deliver\nGive the doc path and a short chat summary. Offer to `SendUserFile` it.\n\n## Principles\n- **True to the code.** Every claim traces to a file you read; cite `path:line`.\n  No fabricated migration/version history.\n- **Newcomer-first.** Define repo-specific terms (FileContext, `useToolOperation`,\n  the `@app/*` layer cascade, stubbed vs live tests) on first use.\n- **Show, don't assert.** Prefer a diagram + a real example over adjectives.\n- Don't commit the `walkthrough/` output unless asked.\n","frontmatter":{"name":"feature-walkthrough","description":"Explain the full logic and process of the current branch end-to-end so someone with no prior knowledge of the task can understand, review, and reproduce it. Scopes the change from the branch diff, traces the flow across every layer it touches (frontend tool/hook/component, Java controller/service/endpoint, Python engine, config, i18n, tests), and produces a self-contained walkthrough document with Mermaid diagrams (sequence/flow/architecture), annotated file map with clickable references, before/after behavior, screenshots where a UI is involved, a \"try it locally\" section, and edge cases/risks. Use when asked for a feature or branch walkthrough, \"explain what this branch does\", a design/logic writeup, PR reviewer onboarding, or a hand-off doc. Pass --html to also emit a rendered HTML version; --no-screens to skip screenshots.","argument-hint":"[branch-or-area] [--html] [--no-screens]","allowed-tools":"Read, Write, Edit, Glob, Grep, Bash","allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash"]},"allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash"],"isInternal":false,"tokens":1322,"sizeBytes":5246},{"name":"SKILL.md","path":".claude/skills/pr-quiz/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/pr-quiz/SKILL.md","title":"pr-quiz","category":"anthropic-skill","format":"markdown","content":"---\nname: pr-quiz\ndescription: >-\n  Quiz the PR author on their own branch before they request review, to prove they\n  actually understand the change - especially code an AI wrote for them. Scopes the\n  branch diff vs its base, reads the changed code, then asks graded questions about\n  what changed, why, how it works, what it could break, and which edge cases it must\n  handle. Presents all questions first, waits for the author's answers, then grades\n  each honestly against the real code (Correct / Partial / Incorrect with the true\n  answer and file:line), scores it, and gives a readiness verdict that names the\n  areas to re-study before asking humans to review. Use when asked to quiz me on my\n  PR/branch, \"test my understanding before review\", a self-check gate before opening\n  a PR, or before requesting reviewers. Administered as an interactive\n  multiple-choice quiz (clickable options) by default; pass --free-text for\n  written answers, --questions N to set count, --save to write a scorecard.\nargument-hint: \"[branch-or-base-ref] [--questions N] [--free-text] [--save]\"\nallowed-tools: Bash, Read, Grep, Glob, Write, AskUserQuestion\n---\n\n# PR Quiz\n\nTest whether the **author** genuinely understands their own branch before they ask\nother people to spend time reviewing it. This is a self-check gate: the point is to\ncatch changes - often AI-written - that the author would not be able to explain or\ndefend in review. Be a fair but honest examiner, not a pushover.\n\n`$ARGUMENTS` may name a base ref or branch to diff against; default is this branch\nvs where it forked from the main line. Flags:\n- `--questions N` - target N questions (else scale to diff size, see below).\n- `--free-text` - administer as a written numbered list instead of the default\n  interactive multiple-choice.\n- `--save` - also write a scorecard file after grading.\n\n## Integrity rules (read first - the whole skill depends on these)\n\n1. **Present every question before revealing any answer.** Ask, then wait. Never\n   show the answer key alongside the questions.\n2. **Do not give hints or the answer while the quiz is open.** If the author asks\n   \"what's the answer?\" or \"is it X?\" before committing, decline warmly and tell\n   them to give their best answer first - guessing is part of the signal.\n3. **Grade truthfully.** Vague, hand-wavy, or \"the AI did it\" non-answers are\n   Partial or Incorrect, not Correct. Do not inflate the score to be nice; a false\n   pass defeats the entire purpose.\n4. **Ground everything in code you actually read.** Every question and every model\n   answer must trace to a real line in the diff. Cite `path:line`. No trivia\n   (\"how many lines?\"), no invented behavior.\n5. **Credit real understanding.** If the author explains it correctly in their own\n   words, mark it Correct even if worded differently than your key.\n\n## Process\n\n### 1. Scope the change (silently)\n- Find the base. Prefer the fork point off the main line so the quiz covers only\n  this branch's work:\n  ```bash\n  git fetch -q origin 2>/dev/null; \\\n  BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main); \\\n  git diff --stat \"$BASE\"...HEAD\n  ```\n  If `$ARGUMENTS` names a ref, diff against that instead.\n- If the diff is empty, stop and say there's nothing to quiz on.\n- Read commit messages / PR description for the *stated* intent, but verify it\n  against the actual diff - a mismatch is itself a good question.\n\n### 2. Understand the code well enough to examine on it\nRead the full diff plus enough surrounding context and related files to answer\nevery question you plan to ask. You cannot grade understanding you don't have.\nNote the non-obvious parts: the design decisions, the risky lines, the edge cases,\nthe cross-file ripples, and anything that violates or upholds repo conventions\n(for this repo e.g. `@app/*` import layering, all file ops via FileContext,\nJackson 3 / Spring Boot 4 APIs, engine typed-contract boundaries).\n\n### 3. Build the question set\nScale count to the change unless `--questions N` is given:\nsmall (< ~50 changed lines) 3-4, medium 5-8, large 9-12. Cap at 12.\nDraw from these categories - weight toward the ones the diff actually exercises:\n- **Intent** - what problem this solves; why it was needed now.\n- **Mechanism** - how a specific non-trivial piece actually works (\"walk me\n  through what `foo()` does when called with X\").\n- **Decisions & alternatives** - why this approach over an obvious alternative;\n  what a reviewer would reasonably push back on.\n- **Blast radius** - what else this touches or could break; what you'd retest.\n- **Edge cases** - inputs/states the change must handle (null, empty, large,\n  concurrent, error paths).\n- **Conventions & correctness** - does it follow the repo's rules; is there a\n  latent bug the author should be able to spot.\nPrefer questions the author can only answer if they read and understood the code.\nKeep a private answer key with `path:line` for each - do **not** show it yet.\n\n### 4. Administer the quiz\n- **Default (multiple choice):** use the `AskUserQuestion` tool. Per question write\n  3-4 options where **every** option is independently plausible - each distractor a\n  real-but-wrong reading of the code, not filler. Two hard rules so the answer\n  can't be spotted by shape rather than knowledge:\n  - **Randomise the correct option's position** across questions - never default\n    it to first. Spread it roughly evenly over the slots.\n  - **Keep all options the same depth and length.** Do not describe the correct\n    one more fully than the distractors - a longer or more-detailed option is a\n    dead giveaway. Trim the right answer or flesh out the wrong ones until a\n    reader can't tell them apart by size.\n  The tool caps a call at 4 questions, so ask in batches of 4 - but run them as\n  one continuous flow: fire the next batch immediately after the previous\n  returns, with no narration (\"Round 2 of 3\") and no grading between batches.\n  The author always has an \"Other\" free-text escape, which is fine.\n- **`--free-text`:** present all questions in one numbered list, then say\n  \"Answer in one reply; number your answers. I won't grade until you're done.\"\n  Wait for the author's answers.\n- Do not proceed to grading until every answer is in.\n\n### 5. Grade\nFor each question, in order:\n- Verdict: **Correct** / **Partial** / **Incorrect**.\n- The model answer in one or two sentences, citing the real `path:line`.\n- One line on the gap when Partial/Incorrect - what they missed and where to look.\nThen a **Score** (e.g. 6/8, counting Partial as half) and a one-line summary of\nthe pattern (e.g. \"solid on intent, shaky on the error paths\").\n\n### 6. Readiness verdict\nEnd with a clear call:\n- **Ready for review** - understanding is sound; note anything to mention to\n  reviewers proactively.\n- **Study first** - list the specific files/concepts to re-read before requesting\n  review, each as a clickable `path:line`. Be concrete: \"re-read the null handling\n  in X before you send this out.\"\nKeep it honest - if they'd get grilled in review on something, say so now.\n\n### 7. If `--save`\nWrite `pr-quiz/<branch>-scorecard.md`: the questions, their answers, your grades\nand model answers, the score, and the verdict. Don't commit it unless asked.\n\n## Principles\n- **The author is the examinee, not the collaborator.** During the quiz you withhold\n  answers; you're measuring them, not helping them pass.\n- **A failed quiz is a successful outcome** - it caught a gap before a human's time\n  was spent. Frame it that way, not as a scolding.\n- **True to the code.** Every question, answer, and grade traces to a line you read.\n- **Terse and direct** in chat - the questions and the verdict, minimal preamble.\n","frontmatter":{"name":"pr-quiz","description":"Quiz the PR author on their own branch before they request review, to prove they actually understand the change - especially code an AI wrote for them. Scopes the branch diff vs its base, reads the changed code, then asks graded questions about what changed, why, how it works, what it could break, and which edge cases it must handle. Presents all questions first, waits for the author's answers, then grades each honestly against the real code (Correct / Partial / Incorrect with the true answer and file:line), scores it, and gives a readiness verdict that names the areas to re-study before asking humans to review. Use when asked to quiz me on my PR/branch, \"test my understanding before review\", a self-check gate before opening a PR, or before requesting reviewers. Administered as an interactive multiple-choice quiz (clickable options) by default; pass --free-text for written answers, --questions N to set count, --save to write a scorecard.","argument-hint":"[branch-or-base-ref] [--questions N] [--free-text] [--save]","allowed-tools":"Bash, Read, Grep, Glob, Write, AskUserQuestion","allowedTools":["Bash,","Read,","Grep,","Glob,","Write,","AskUserQuestion"]},"allowedTools":["Bash,","Read,","Grep,","Glob,","Write,","AskUserQuestion"],"isInternal":false,"tokens":1891,"sizeBytes":7723},{"name":"SKILL.md","path":".claude/skills/ui-before-after/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/SKILL.md","title":"ui-before-after","category":"anthropic-skill","format":"markdown","content":"---\nname: ui-before-after\ndescription: >-\n  Analyse a branch or PR and automatically capture before/after screenshots of\n  every UI surface its changes touch, then pixel-diff the pairs to surface what\n  actually changed and assemble PR-ready before/after montage images. Generic and\n  diff-driven: it derives the capture targets from the diff (changed tools/routes →\n  URLs) instead of hand-listing screens, captures \"before\" from the base branch and\n  \"after\" from the head, then keeps only the views that visually differ. Each\n  comparison is auto-cropped to the region that actually changed (the bounding box of\n  differing pixels), falling back to the full page only when the change spans most of\n  it. Use for before/after shots, a visual diff of a branch/PR, \"screenshots for the\n  PR description\", \"show what changed in the UI\", or a side-by-side of UI changes.\n  Takes a PR number/URL (resolved via gh) or a branch; defaults to the current branch\n  vs its base. Flags: --scope <selector>, --base <ref|merge-base>, --theme\n  light|dark|both, --all (capture every route, not just changed), --no-autocrop,\n  --pagewide <n>, --threshold <n>.\nargument-hint: \"[PR# | PR-url | branch] [--scope <sel>] [--base <ref>] [--theme both] [--all] [--no-autocrop]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Before / After (generic visual diff)\n\nPoint it at a branch or PR; it figures out which UI changed, screenshots every\naffected surface **before** (base) and **after** (head), pixel-diffs the pairs, and\nmontages the ones that actually changed into images for the PR description.\n\n`$ARGUMENTS`: a PR number/URL, a branch, or nothing (current branch vs base).\nBy default it captures the full viewport and auto-crops each comparison to the region\nthat changed. Flags: `--scope <css>` (narrow the *capture* to a container, e.g.\n`[data-sidebar=\"tool-panel\"]`, when you already know where the change is),\n`--no-autocrop` (keep full frames), `--pagewide <fraction>` (above this share of the\npage, skip cropping; default 0.6), `--base <ref|merge-base>`,\n`--theme light|dark|both`, `--all` (walk every route, not just changed),\n`--threshold <fraction>` (diff sensitivity, default 0.001).\n\nShares the capture harness with **ui-walkthrough** - read its SKILL.md for the\nstubbed-Playwright setup, worktree node_modules, the\nstale-`:5173` gotcha, and the dark-mode init-script. Bundled helpers:\n[capture-spec.template.ts](capture-spec.template.ts), [diff-shots.mjs](diff-shots.mjs),\n[montage-template.html](montage-template.html), [shoot-sections.mjs](shoot-sections.mjs).\n\n## Process\n\n### 1. Resolve target + base\n```\ngh pr view <pr> --json number,title,headRefName,baseRefName,url,files   # PR\n# or branch: base = merge-base(main, HEAD); head = HEAD\ngh pr diff <pr> --name-only        # or: git diff --name-only <base>...HEAD\n```\n\n### 2. Derive capture targets from the diff (the \"analyse\" step - no hand-listing)\nMap changed frontend files to URLs generically:\n- **Tools**: a changed `components/tools/<toolDir>/…` or `hooks/tools/<tool>/…` →\n  toolId → URL via the repo's own rule `getToolUrlPath` in\n  [toolsTaxonomy.ts:200](../../../frontend/src/editor/core/data/toolsTaxonomy.ts): `/` + the\n  id kebab-cased (`addPageNumbers` → `/add-page-numbers`).\n- **Pages/routes**: changed `filesPage/*` → `/files`, etc.\n- `--all`: enumerate every tool in the registry instead of just changed ones.\nWrite `frontend/screenshots/ui-diff/targets.json` =\n`[{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\" }]`. This is what makes\nit generic - the spec never names a tool.\n\n### 3. Capture AFTER (head) then BEFORE (base)\nCopy [capture-spec.template.ts](capture-spec.template.ts) →\n`src/core/tests/stubbed/ui-before-after.spec.ts` (it loops `targets.json`, seeds a\nsample PDF so file-dependent panels render, navigates to each URL, and screenshots\nthe full viewport - or the `--scope` container if given). Ensure the harness is ready\n(node_modules + icons).\n```\n# after = current head\ncd frontend && PR_SHOT_SIDE=after PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n# before = base, in an isolated worktree (copy the spec + targets.json in)\ngit worktree add ../ba-base origin/<baseRefName>        # or the merge-base\n#   set up its frontend, copy spec + screenshots/ui-diff/targets.json across, then:\ncd ../ba-base/frontend && PR_SHOT_SIDE=before PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n#   copy its screenshots/ui-diff/before/ back next to after/. Repeat with\n#   PR_SHOT_THEME=dark if --theme includes dark. Remove worktree when done.\n```\n\n### 4. Auto-diff (surface what changed)\n```\ncd frontend && node <skill>/diff-shots.mjs \\\n  screenshots/ui-diff/before screenshots/ui-diff/after screenshots/ui-diff\n```\nProduces `diff-report.json` classifying each view `unchanged | changed | added |\nremoved`. For each changed view it computes the bounding box of differing pixels and\nwrites cropped `__before_crop.png` / `__after_crop.png` / `__diff.png` to that region\n(+ padding) - **unless** the change covers more than `--pagewide` of the frame, where\nit keeps the full frame (`pageWide:true`). Drop `unchanged` - that's the noise the\nuser doesn't want.\n\n### 5. Montage the changes\nBuild the manifest from the non-unchanged entries (group by tab/tool; each becomes a\nstate row with before/after). For changed views use the cropped `cropBefore` /\n`cropAfter` from `diff-report.json` (tight on the affected region; full frame when\n`pageWide`); `added`/`removed` render the \"not present\" placeholder. Fill\n[montage-template.html](montage-template.html) (replace the `window.__BA__` data\nblock; base64-inline the PNGs for portability), then render one PNG per section with\n[shoot-sections.mjs](shoot-sections.mjs). Optionally include the `__diff.png` overlay\nas a third column.\n\n### 6. Deliver\nOutput the `montage_<tab>.png` files + a short summary (N changed / added / removed,\nM unchanged skipped) and a paste-ready Markdown block. GitHub has no PR-body image\nAPI, so tell the user to drag the PNGs into the description. Do **not** post to the\nPR.\n\n## Gotchas\n- Two installs (base worktree + head); junction main's node_modules only if its deps\n  match that ref, else `npm ci` (see ui-walkthrough's stale-dep note).\n- A view that errors on one side (refactored/removed) → that side is missing; the\n  diff marks it added/removed rather than failing the run.\n- Pixel diff needs equal dimensions, so capture at a fixed viewport (the template\n  does); a view whose size changed is reported as \"changed (dimensions differ)\",\n  uncropped.\n- Auto-crop uses a single bounding box, so two far-apart changes give one large crop\n  (or trip `--pagewide`); narrow with `--scope` if that happens.\n- `getToolUrlPath` is the source of truth for tool URLs - use it, don't guess slugs.\n- Don't commit `screenshots/`, the throwaway spec, or the base worktree.\n","frontmatter":{"name":"ui-before-after","description":"Analyse a branch or PR and automatically capture before/after screenshots of every UI surface its changes touch, then pixel-diff the pairs to surface what actually changed and assemble PR-ready before/after montage images. Generic and diff-driven: it derives the capture targets from the diff (changed tools/routes → URLs) instead of hand-listing screens, captures \"before\" from the base branch and \"after\" from the head, then keeps only the views that visually differ. Each comparison is auto-cropped to the region that actually changed (the bounding box of differing pixels), falling back to the full page only when the change spans most of it. Use for before/after shots, a visual diff of a branch/PR, \"screenshots for the PR description\", \"show what changed in the UI\", or a side-by-side of UI changes. Takes a PR number/URL (resolved via gh) or a branch; defaults to the current branch vs its base. Flags: --scope <selector>, --base <ref|merge-base>, --theme light|dark|both, --all (capture every route, not just changed), --no-autocrop, --pagewide <n>, --threshold <n>.","argument-hint":"[PR# | PR-url | branch] [--scope <sel>] [--base <ref>] [--theme both] [--all] [--no-autocrop]","allowed-tools":"Read, Write, Edit, Glob, Grep, Bash","allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash"]},"allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash"],"isInternal":false,"tokens":1822,"sizeBytes":6937},{"name":"SKILL.md","path":".claude/skills/ui-walkthrough/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/SKILL.md","title":"ui-walkthrough","category":"anthropic-skill","format":"markdown","content":"---\nname: ui-walkthrough\ndescription: >-\n  Full UI investigation of the current branch's feature. Enumerates every view\n  and state (empty, populated, loading, error, each dialog/menu/panel, responsive\n  breakpoints, light + dark + RTL), captures them with the stubbed Playwright\n  harness, assembles a single-image HTML walkthrough with a global light/dark\n  toggle slider, then runs two review passes: visual/consistency (alignment,\n  spacing, professionalism, dark/light parity, contrast, truncation) and\n  UX/ease-of-use (flow, discoverability, affordances, empty/error states,\n  expectations). Use when asked for a UI walkthrough, screenshot review, design\n  or QA pass, \"find anywhere to make it easier/better for users\", or before\n  merging frontend work. Pass --fix to auto-apply safe frontend fixes and\n  re-capture; --theme to limit themes; --no-rtl to skip RTL.\nargument-hint: \"[feature/area] [--fix] [--theme light|dark|both] [--no-rtl] [--breakpoints]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Walkthrough\n\nProduce a reviewable HTML walkthrough of a feature's UI in every state and theme,\nthen critique it. Optionally auto-fix and re-capture.\n\n`$ARGUMENTS` may name the feature/area to focus on. If empty, scope from the\ncurrent branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),\n`--no-rtl`, `--breakpoints` (also capture phone/narrow widths).\n\n## What this repo gives you (use it, don't reinvent)\n\n- **Stubbed Playwright project** = backend-free screenshots via `page.route()` mocks.\n  Reference implementation: `frontend/src/editor/core/tests/stubbed/files-page-screenshots.spec.ts`.\n  It already shows the light / **dark** / **RTL** passes, JWT seeding, IndexedDB\n  seeding, and dumping PNGs to a `screenshots/<area>/` folder. Copy its shape.\n- Helpers: `frontend/src/editor/core/tests/helpers/ui-helpers.ts`\n  (`uploadFiles`, `openSettings`, `waitForModalOpen`, `dismissTourTooltip`, …)\n  and the `stub-test-base` fixtures (`autoGoto`, `seedJwt`, `viewport`).\n- Config: `frontend/playwright.config.ts` (run from `frontend/`).\n- Report template: [report-template.html](report-template.html) - self-contained,\n  one big image at a time, a global light/dark slider that flips every shot,\n  thumbnail rail, prev/next + arrow keys, and a Findings tab.\n\n## Process\n\n### 1. Scope the feature\n- If `$ARGUMENTS` is empty: `git diff --name-only main...HEAD` and read the PR/commits.\n  Identify changed pages, tools (`core/components/tools/<tool>` or `core/tools/<tool>`),\n  dialogs, panels, and routes.\n- Enumerate **every view and state** to capture, e.g.:\n  empty / populated / loading / error / disabled; each dialog, menu, popover, tooltip;\n  each tab or step; selection + multi-select; success/result panel; and (if relevant)\n  permission/role variants. Write the list down before capturing - it's the report's spine.\n\n### 2. Prepare the harness (worktree-safe)\nWorktrees have no `node_modules`. From repo root:\n```\ncd frontend && npm ci                       # or junction main's node_modules (see memory)\n```\nKill any stale dev server first (it serves old modules):\n`Get-NetTCPConnection -LocalPort 5173 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`\n\n### 3. Write the capture spec\nCreate `frontend/src/editor/core/tests/stubbed/<feature>-walkthrough.spec.ts`,\nmodeled on `files-page-screenshots.spec.ts`. For each enumerated view:\n- stub the APIs it needs, drive the UI to that state, wait on a real locator\n  (not a fixed sleep), `await settle(page)` for Mantine portals, then\n  `page.screenshot({ path: shotPath(\"NN_name_<theme>\") })`.\n- Capture each view in **light and dark** (and RTL unless `--no-rtl`). Reuse the\n  `enableDarkMode` / `enableRtl` init-script pattern from the reference spec\n  (`localStorage[\"mantine-color-scheme\"]=\"dark\"` + `emulateMedia({colorScheme:\"dark\"})`).\n- Name shots `NN_<view>_<theme>.png` so light/dark pair up by suffix.\n- Prefer **stable test-ids** over translated accessible names (RTL/i18n breaks text locators).\n\nRun it: `cd frontend && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.\nAdd `--project=stubbed-firefox`/`-webkit` only if cross-browser layout matters.\n\n### 4. Build the report\n- Copy `report-template.html` to `screenshots/<feature>/walkthrough.html` (so the\n  relative `screenshots/...` image paths resolve, or rewrite paths to sit beside it).\n- Build the manifest and inject it: replace the JSON between the\n  `/*__DATA__*/` … `/*__END__*/` markers with one `views[]` entry per view\n  (`{id,title,light,dark,viewport,notes}`) and an empty `findings` object you'll\n  fill in step 5. Keep `light`/`dark` as relative paths.\n- The toggle slider answers the \"one big image + flip light/dark for all\" request:\n  it shows a single large screenshot, and switching the slider re-themes every view.\n\n### 5. Review pass 1 - visual & consistency\nOpen each screenshot (Read the PNG) and judge against the others:\nalignment & spacing rhythm, control placement, button hierarchy, typography,\n**light/dark parity** (contrast, invisible borders, washed-out text, wrong tokens),\ntruncation/overflow, RTL mirroring, focus states, icon consistency, professional polish.\nRecord each issue as a finding `{severity:high|med|low, view, title, detail, fix}`.\n\n### 6. Review pass 2 - UX & ease of use\nWalk the flow as a first-time user: discoverability, number of steps, affordance\nclarity, empty-state guidance, error recovery, destructive-action confirmation,\ndefaults, loading feedback, mobile reachability, accessible names, and whether the\nUI matches user expectations for this kind of tool. Record findings the same way.\n\nWrite both finding lists into the report's `findings.visual` / `findings.ux`,\nand add short per-view `notes`. Re-inject the manifest.\n\n### 7. If `--fix`\nOnly safe, self-contained frontend fixes (spacing, alignment, tokens, missing\ndark-mode colors, labels, aria, obvious copy). For each: edit the component/CSS,\nmark the finding `fixed:true` with what changed, then **re-run the spec** to\nre-capture the affected shots and regenerate the report. Run `task frontend:check`.\nLeave anything risky or ambiguous as a finding, not a change.\n\n### 8. Deliver\nTell the user the report path and give a tight chat summary: N views ×\nthemes captured, top findings by severity, and (if `--fix`) what changed.\nOptionally `SendUserFile` the `walkthrough.html`.\n\n## Gotchas\n- Stale `:5173` server serves old bundles - kill it before capturing (see step 2).\n- `await settle(page)` before shots or portals/transitions tear mid-capture.\n- Don't commit the generated `screenshots/` or the throwaway spec unless asked.\n","frontmatter":{"name":"ui-walkthrough","description":"Full UI investigation of the current branch's feature. Enumerates every view and state (empty, populated, loading, error, each dialog/menu/panel, responsive breakpoints, light + dark + RTL), captures them with the stubbed Playwright harness, assembles a single-image HTML walkthrough with a global light/dark toggle slider, then runs two review passes: visual/consistency (alignment, spacing, professionalism, dark/light parity, contrast, truncation) and UX/ease-of-use (flow, discoverability, affordances, empty/error states, expectations). Use when asked for a UI walkthrough, screenshot review, design or QA pass, \"find anywhere to make it easier/better for users\", or before merging frontend work. Pass --fix to auto-apply safe frontend fixes and re-capture; --theme to limit themes; --no-rtl to skip RTL.","argument-hint":"[feature/area] [--fix] [--theme light|dark|both] [--no-rtl] [--breakpoints]","allowed-tools":"Read, Write, Edit, Glob, Grep, Bash","allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash"]},"allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash"],"isInternal":false,"tokens":1676,"sizeBytes":6652},{"name":"core.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/core.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/core.json","title":"Rules Skill","category":"anthropic-skill","format":"json","content":"# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/core.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/core.json)","tokens":300,"sizeBytes":254},{"name":"languages.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/languages.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/languages.json","title":"Rules Skill","category":"anthropic-skill","format":"json","content":"# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/languages.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/languages.json)","tokens":300,"sizeBytes":264},{"name":"ar.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json)","tokens":300,"sizeBytes":262},{"name":"az.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json)","tokens":300,"sizeBytes":262},{"name":"bg.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json)","tokens":300,"sizeBytes":262},{"name":"bo.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json)","tokens":300,"sizeBytes":262},{"name":"ca.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json)","tokens":300,"sizeBytes":262},{"name":"cs.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json)","tokens":300,"sizeBytes":262},{"name":"da.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json)","tokens":300,"sizeBytes":262},{"name":"de.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json)","tokens":300,"sizeBytes":262},{"name":"el.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json)","tokens":300,"sizeBytes":262},{"name":"en.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json)","tokens":300,"sizeBytes":262},{"name":"es.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json)","tokens":300,"sizeBytes":262},{"name":"eu.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json)","tokens":300,"sizeBytes":262},{"name":"fa.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json)","tokens":300,"sizeBytes":262},{"name":"fr.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json)","tokens":300,"sizeBytes":262},{"name":"ga.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json)","tokens":300,"sizeBytes":262},{"name":"hi.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json)","tokens":300,"sizeBytes":262},{"name":"hr.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json)","tokens":300,"sizeBytes":262},{"name":"hu.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json)","tokens":300,"sizeBytes":262},{"name":"id.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json)","tokens":300,"sizeBytes":262},{"name":"it.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json)","tokens":300,"sizeBytes":262},{"name":"ja.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json)","tokens":300,"sizeBytes":262},{"name":"ko.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json)","tokens":300,"sizeBytes":262},{"name":"ml.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ml.json)","tokens":300,"sizeBytes":262},{"name":"nl.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/nl.json)","tokens":300,"sizeBytes":262},{"name":"no.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/no.json)","tokens":300,"sizeBytes":262},{"name":"pl.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pl.json)","tokens":300,"sizeBytes":262},{"name":"pt.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/pt.json)","tokens":300,"sizeBytes":262},{"name":"ro.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ro.json)","tokens":300,"sizeBytes":262},{"name":"ru.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ru.json)","tokens":300,"sizeBytes":262},{"name":"sk.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sk.json)","tokens":300,"sizeBytes":262},{"name":"sl.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sl.json)","tokens":300,"sizeBytes":262},{"name":"sr.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sr.json)","tokens":300,"sizeBytes":262},{"name":"sv.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/sv.json)","tokens":300,"sizeBytes":262},{"name":"th.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/th.json)","tokens":300,"sizeBytes":262},{"name":"tr.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/tr.json)","tokens":300,"sizeBytes":262},{"name":"uk.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/uk.json)","tokens":300,"sizeBytes":262},{"name":"vi.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/vi.json)","tokens":300,"sizeBytes":262},{"name":"zh.json","path":"frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json","title":"Packs Skill","category":"anthropic-skill","format":"json","content":"# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/zh.json)","tokens":300,"sizeBytes":262},{"name":"added-comments.mjs","path":".claude/skills/comment-review/added-comments.mjs","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/comment-review/added-comments.mjs","title":"Claude Directive: added-comments.mjs","category":"claude-rule","format":"markdown","content":"#!/usr/bin/env node\n\n// Every comment line a range adds, as `file:line text`, for the review checklist.\n//\n//   node .claude/skills/comment-review/added-comments.mjs <base-ref> [head-ref]\n//\n// Line-based on purpose: it over-reports (a `//` inside a string, a `*` opening a\n// wrapped expression) rather than miss one, and the reviewer opens the file anyway.\n// A shell one-liner cannot do this job: `$0` and `$1` in an awk or perl script are\n// rewritten by skill argument interpolation before the shell ever sees them.\n\nimport { execFileSync } from \"node:child_process\";\n\nconst [base, head = \"HEAD\"] = process.argv.slice(2);\nif (!base) {\n  process.stderr.write(\"usage: added-comments.mjs <base-ref> [head-ref]\\n\");\n  process.exit(2);\n}\n\nconst diff = execFileSync(\"git\", [\"diff\", \"-U0\", \"--no-color\", `${base}...${head}`], {\n  encoding: \"utf8\",\n  maxBuffer: 1 << 28,\n});\n\nconst OPENS_COMMENT = /^\\+\\s*(\\/\\/|\\/\\*|\\*|#|\"\"\"|'''|\\{\\/\\*)/;\n\nlet file = null;\nlet line = 0;\nlet count = 0;\n\nfor (const raw of diff.split(\"\\n\")) {\n  if (raw.startsWith(\"+++ \")) {\n    file = raw.slice(4).replace(/^b\\//, \"\").trim();\n    continue;\n  }\n  // The new-file start of the hunk. Single-line hunks omit the count, so read the\n  // number rather than splitting on the comma that may not be there.\n  const hunk = /^@@ .*?\\+(\\d+)/.exec(raw);\n  if (hunk) {\n    line = Number(hunk[1]);\n    continue;\n  }\n  if (!raw.startsWith(\"+\")) continue;\n  if (OPENS_COMMENT.test(raw)) {\n    process.stdout.write(`${file}:${line} ${raw.slice(1).trim()}\\n`);\n    count += 1;\n  }\n  line += 1;\n}\n\nprocess.stderr.write(`${count} added comment lines\\n`);\n","isInternal":false,"tokens":467,"sizeBytes":1613},{"name":"capture-spec.template.ts","path":".claude/skills/ui-before-after/capture-spec.template.ts","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/capture-spec.template.ts","title":"Claude Directive: capture-spec.template.ts","category":"claude-rule","format":"markdown","content":"// Generic before/after capturer. NOT app-specific: it walks a targets.json that\n// the ui-before-after skill generates from the branch/PR diff, so nothing here is\n// hand-listed. Copy to src/core/tests/stubbed/ui-before-after.spec.ts, then run\n// once per (side, theme):\n//   PR_SHOT_SIDE=after  PR_SHOT_THEME=light \\\n//     npx playwright test --project=stubbed ui-before-after.spec.ts\n//\n// targets.json shape: [{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\",\n//                        \"needsFile\": true }]\nimport { test } from \"@app/tests/helpers/stub-test-base\";\nimport type { Page } from \"@playwright/test\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst SIDE = process.env.PR_SHOT_SIDE ?? \"after\";\nconst THEME = process.env.PR_SHOT_THEME ?? \"light\";\n// Capture the full viewport by default so the affected region is in frame\n// wherever it is; diff-shots.mjs crops each comparison to what actually changed.\n// Set PR_SHOT_SCOPE to a selector to narrow the capture to one container.\nconst SCOPE = process.env.PR_SHOT_SCOPE ?? \"\";\nconst ROOT = path.resolve(process.cwd(), \"screenshots\", \"ui-diff\");\nconst OUT = path.join(ROOT, SIDE);\n// A tiny sample PDF so file-dependent tool panels render. Point at a real fixture.\nconst SAMPLE_PDF = process.env.PR_SHOT_SAMPLE ?? \"src/core/tests/test-fixtures/sample.pdf\";\n\ntype Target = { id: string; url: string; name?: string; needsFile?: boolean };\nconst targets: Target[] = JSON.parse(fs.readFileSync(path.join(ROOT, \"targets.json\"), \"utf-8\"));\n\ntest.use({ autoGoto: false, viewport: { width: 1600, height: 900 }, seedJwt: true });\n\nasync function applyTheme(page: Page): Promise<void> {\n  if (THEME !== \"dark\") return;\n  await page.addInitScript(() => {\n    localStorage.setItem(\"mantine-color-scheme\", \"dark\");\n    localStorage.setItem(\"mantine-color-scheme-value\", \"dark\");\n  });\n  await page.emulateMedia({ colorScheme: \"dark\" });\n}\n\nasync function seedFile(page: Page): Promise<void> {\n  if (!fs.existsSync(SAMPLE_PDF)) return;\n  await page.goto(\"/\", { waitUntil: \"domcontentloaded\" });\n  await page.getByTestId(\"files-button\").click().catch(() => {});\n  await page.locator('[data-testid=\"file-input\"]').setInputFiles(SAMPLE_PDF).catch(() => {});\n  await page.locator(\".file-sidebar-file-item\").first().isVisible({ timeout: 8_000 }).catch(() => {});\n}\n\nfor (const t of targets) {\n  // One test per target so a single failure doesn't drop the rest.\n  test(`${SIDE}/${THEME} ${t.id}`, async ({ page }) => {\n    fs.mkdirSync(OUT, { recursive: true });\n    await applyTheme(page);\n    if (t.needsFile !== false) await seedFile(page);\n    await page.goto(t.url, { waitUntil: \"domcontentloaded\" });\n    await page.waitForTimeout(400); // settle Mantine portals/transitions\n    const shot = path.join(OUT, `${t.id}__${THEME}.png`);\n    if (SCOPE) {\n      const scope = page.locator(SCOPE).first();\n      if (await scope.isVisible({ timeout: 8_000 }).catch(() => false)) {\n        await scope.screenshot({ path: shot });\n        return;\n      }\n    }\n    // Full viewport (fixed size → stable dimensions for pixel diffing).\n    await page.screenshot({ path: shot });\n  });\n}\n","isInternal":false,"tokens":792,"sizeBytes":3146},{"name":"diff-shots.mjs","path":".claude/skills/ui-before-after/diff-shots.mjs","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/diff-shots.mjs","title":"Claude Directive: diff-shots.mjs","category":"claude-rule","format":"markdown","content":"// Auto-diff before/ vs after/ screenshots, classify each as\n// unchanged | changed | added | removed, and CROP each changed pair to the\n// affected region (bounding box of differing pixels + padding) - unless the\n// change spans most of the page, in which case the full frame is kept.\n// Run from frontend (so deps resolve):\n//   node <skill>/diff-shots.mjs <beforeDir> <afterDir> [outDir]\n// Env:\n//   DIFF_THRESHOLD  min fraction of differing pixels to count as changed (default 0.001)\n//   DIFF_PAD        padding px around the affected region (default 24)\n//   DIFF_PAGEWIDE   if affected bbox area / image area exceeds this, keep full frame (default 0.6)\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(path.join(process.cwd(), \"noop.js\"));\nconst pm = require(\"pixelmatch\");\nconst pixelmatch = pm.default || pm;\nconst { PNG } = require(\"pngjs\");\n\nconst beforeDir = path.resolve(process.argv[2]);\nconst afterDir = path.resolve(process.argv[3]);\nconst outDir = path.resolve(process.argv[4] || afterDir);\nconst THRESHOLD = Number(process.env.DIFF_THRESHOLD ?? \"0.001\");\nconst PAD = Number(process.env.DIFF_PAD ?? \"24\");\nconst PAGEWIDE = Number(process.env.DIFF_PAGEWIDE ?? \"0.6\");\n\nconst read = (p) => PNG.sync.read(fs.readFileSync(p));\nconst isShot = (f) => f.endsWith(\".png\") && !/__(diff|before_crop|after_crop)\\.png$/.test(f);\nconst list = (d) => (fs.existsSync(d) ? fs.readdirSync(d).filter(isShot) : []);\nconst names = [...new Set([...list(beforeDir), ...list(afterDir)])].sort();\nfs.mkdirSync(outDir, { recursive: true });\n\nfunction cropPNG(src, x, y, w, h) {\n  const out = new PNG({ width: w, height: h });\n  PNG.bitblt(src, out, x, y, w, h, 0, 0);\n  return out;\n}\nconst writePNG = (p, png) => fs.writeFileSync(p, PNG.sync.write(png));\n\n// Bounding box of differing pixels using a diff mask (alpha>0 where changed).\nfunction changedBBox(before, after, w, h) {\n  const mask = new PNG({ width: w, height: h });\n  pixelmatch(before.data, after.data, mask.data, w, h, { threshold: 0.1, diffMask: true });\n  let minX = w, minY = h, maxX = -1, maxY = -1, count = 0;\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) {\n      if (mask.data[(y * w + x) * 4 + 3] > 0) {\n        count++;\n        if (x < minX) minX = x; if (x > maxX) maxX = x;\n        if (y < minY) minY = y; if (y > maxY) maxY = y;\n      }\n    }\n  }\n  return maxX < 0 ? null : { minX, minY, maxX, maxY, count };\n}\n\nconst report = [];\nfor (const name of names) {\n  const id = name.replace(/\\.png$/, \"\");\n  const bp = path.join(beforeDir, name), ap = path.join(afterDir, name);\n  const hasB = fs.existsSync(bp), hasA = fs.existsSync(ap);\n  if (hasB && !hasA) { report.push({ id, status: \"removed\", before: bp }); continue; }\n  if (!hasB && hasA) { report.push({ id, status: \"added\", after: ap }); continue; }\n\n  const before = read(bp), after = read(ap);\n  if (before.width !== after.width || before.height !== after.height) {\n    report.push({ id, status: \"changed\", note: \"dimensions differ\", before: bp, after: ap });\n    continue;\n  }\n  const w = after.width, h = after.height;\n  const overlay = new PNG({ width: w, height: h });\n  const px = pixelmatch(before.data, after.data, overlay.data, w, h, { threshold: 0.1 });\n  const ratio = px / (w * h);\n  if (ratio <= THRESHOLD) { report.push({ id, status: \"unchanged\", ratio: Number(ratio.toFixed(5)), before: bp, after: ap }); continue; }\n\n  const box = changedBBox(before, after, w, h);\n  // Pad + clamp the affected region.\n  const x = Math.max(0, box.minX - PAD), y = Math.max(0, box.minY - PAD);\n  const x2 = Math.min(w, box.maxX + 1 + PAD), y2 = Math.min(h, box.maxY + 1 + PAD);\n  const bw = x2 - x, bh = y2 - y;\n  const pageWide = (bw * bh) / (w * h) > PAGEWIDE;\n\n  const entry = { id, status: \"changed\", ratio: Number(ratio.toFixed(5)), before: bp, after: ap, pageWide };\n  if (pageWide) {\n    // Change spans most of the page - keep the full frame, full overlay.\n    const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, overlay);\n    entry.diff = dp;\n  } else {\n    entry.bbox = { x, y, w: bw, h: bh };\n    const cb = path.join(outDir, `${id}__before_crop.png`); writePNG(cb, cropPNG(before, x, y, bw, bh));\n    const ca = path.join(outDir, `${id}__after_crop.png`); writePNG(ca, cropPNG(after, x, y, bw, bh));\n    const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, cropPNG(overlay, x, y, bw, bh));\n    entry.cropBefore = cb; entry.cropAfter = ca; entry.diff = dp;\n  }\n  report.push(entry);\n}\n\nfs.writeFileSync(path.join(outDir, \"diff-report.json\"), JSON.stringify(report, null, 2));\nconst changed = report.filter((r) => r.status !== \"unchanged\");\nconsole.log(`diffed ${report.length} view(s): ${changed.length} changed/added/removed, ${report.length - changed.length} unchanged`);\nfor (const r of changed) {\n  const tail = r.status !== \"changed\" ? \"\"\n    : r.pageWide ? \" (page-wide → full frame)\"\n    : ` (${(r.ratio * 100).toFixed(2)}%, cropped to ${r.bbox.w}×${r.bbox.h})`;\n  console.log(`  ${r.status.padEnd(9)} ${r.id}${tail}${r.note ? \" - \" + r.note : \"\"}`);\n}\n","isInternal":false,"tokens":1525,"sizeBytes":5116},{"name":"make_example.py","path":".claude/skills/ui-before-after/make_example.py","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/make_example.py","title":"Claude Directive: make_example.py","category":"claude-rule","format":"markdown","content":"\"\"\"Build EXAMPLE.html from montage-template.html using REAL files-page shots as\nstand-in before/after pairs (layout demo, not an actual PR diff). Inlines PNGs as\ndata URIs so the HTML is portable. Run: python make_example.py\"\"\"\nimport base64\nimport json\nimport pathlib\nimport re\n\nHERE = pathlib.Path(__file__).parent\nSHOTS = pathlib.Path(\n    r\"C:\\Users\\systo\\git\\Stirling-PDFNew\\.claude\\worktrees\\kind-faraday-522a30\"\n    r\"\\frontend\\editor\\screenshots\\files-page\"\n)\n\n\ndef uri(fname):\n    p = SHOTS / fname\n    return \"data:image/png;base64,\" + base64.b64encode(p.read_bytes()).decode() if p.exists() else None\n\n\ndata = {\n    \"pr\": \"DEMO\",\n    \"title\": \"EXAMPLE — before/after montage (layout demo, real Files-page shots; not a real PR diff)\",\n    \"base\": \"main\", \"head\": \"demo-branch\",\n    \"cropSelector\": \"[data-sidebar=\\\"tool-panel\\\"]  (real runs crop to the side; these demo shots are full-page)\",\n    \"tabs\": [\n        {\"id\": \"files\", \"title\": \"Files page\", \"ctx\": \"Each row = one flow state; left = base branch, right = this PR.\",\n         \"states\": [\n             {\"name\": \"Empty folder\", \"before\": uri(\"01_empty_state_ctas.png\"), \"after\": uri(\"02_empty_state_storage_off.png\")},\n             {\"name\": \"Files + details panel\", \"before\": uri(\"03_subtoolbar_with_files.png\"), \"after\": uri(\"06_details_panel_save_to_server.png\")},\n             {\"name\": \"Delete folder confirm\", \"before\": None, \"after\": uri(\"19_delete_folder_dialog.png\"), \"note\": \"New in this PR\"},\n         ]},\n        {\"id\": \"move\", \"title\": \"Move-to-folder dialog\",\n         \"states\": [\n             {\"name\": \"Dialog opened\", \"before\": uri(\"07_move_dialog_collapsed.png\"), \"after\": uri(\"08_move_dialog_create_folder_expanded.png\")},\n             {\"name\": \"After folder created\", \"before\": None, \"after\": uri(\"08b_move_dialog_after_create_folder.png\"), \"note\": \"New flow\"},\n         ]},\n    ],\n}\n\ntpl = (HERE / \"montage-template.html\").read_text(encoding=\"utf-8\")\nout = re.sub(\n    r\"/\\*__DATA__\\*/.*?/\\*__END__\\*/\",\n    lambda _m: \"/*__DATA__*/\" + json.dumps(data) + \"/*__END__*/\",\n    tpl, count=1, flags=re.S,\n)\n(HERE / \"EXAMPLE.html\").write_text(out, encoding=\"utf-8\")\nprint(\"wrote\", HERE / \"EXAMPLE.html\", \"(\", (HERE / \"EXAMPLE.html\").stat().st_size // 1024, \"KB )\")\n","isInternal":false,"tokens":621,"sizeBytes":2248},{"name":"shoot-sections.mjs","path":".claude/skills/ui-before-after/shoot-sections.mjs","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/shoot-sections.mjs","title":"Claude Directive: shoot-sections.mjs","category":"claude-rule","format":"markdown","content":"# Claude Directive: shoot-sections.mjs\n\nPath: `.claude/skills/ui-before-after/shoot-sections.mjs`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-before-after/shoot-sections.mjs)","tokens":300,"sizeBytes":245},{"name":"make_example.py","path":".claude/skills/ui-walkthrough/make_example.py","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/make_example.py","title":"Claude Directive: make_example.py","category":"claude-rule","format":"markdown","content":"# Claude Directive: make_example.py\n\nPath: `.claude/skills/ui-walkthrough/make_example.py`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/make_example.py)","tokens":300,"sizeBytes":234},{"name":"report-template.html","path":".claude/skills/ui-walkthrough/report-template.html","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/report-template.html","title":"Claude Directive: report-template.html","category":"claude-rule","format":"markdown","content":"# Claude Directive: report-template.html\n\nPath: `.claude/skills/ui-walkthrough/report-template.html`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/.claude/skills/ui-walkthrough/report-template.html)","tokens":300,"sizeBytes":249},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n","isInternal":false,"tokens":7181,"sizeBytes":31586},{"name":"playwright-test-generator.md","path":"frontend/.claude/agents/playwright-test-generator.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-generator.md","title":"Subagent: playwright-test-generator","category":"subagent-persona","format":"markdown","content":"# Subagent: playwright-test-generator\n\nPath: `frontend/.claude/agents/playwright-test-generator.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-generator.md)","tokens":300,"sizeBytes":250},{"name":"playwright-test-healer.md","path":"frontend/.claude/agents/playwright-test-healer.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-healer.md","title":"Subagent: playwright-test-healer","category":"subagent-persona","format":"markdown","content":"# Subagent: playwright-test-healer\n\nPath: `frontend/.claude/agents/playwright-test-healer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-healer.md)","tokens":300,"sizeBytes":241},{"name":"playwright-test-planner.md","path":"frontend/.claude/agents/playwright-test-planner.md","rawUrl":"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-planner.md","title":"Subagent: playwright-test-planner","category":"subagent-persona","format":"markdown","content":"# Subagent: playwright-test-planner\n\nPath: `frontend/.claude/agents/playwright-test-planner.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/.claude/agents/playwright-test-planner.md)","tokens":300,"sizeBytes":244}],"systemPromptSnippet":"<yakaai_skills repo=\"Stirling-Tools/Stirling-PDF\">\n<!-- File: AGENTS.md (Tokens: ~7181 | Category: root-instruction) -->\n# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\n## Taskfile (Recommended)\n\nThis project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.\n\nTask `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.\n\n### Quick Reference\n- `task install` — install all dependencies\n- `task dev` — start backend + frontend concurrently\n- `task dev:all` — start backend + frontend + engine concurrently\n- `task build` — build all components\n- `task test` — run all tests (backend + frontend + engine)\n- `task lint` — run all linters\n- `task format` — auto-fix formatting across all components\n- `task check` — full quality gate (lint + typecheck + test)\n- `task clean` — clean all build artifacts\n- `task docker:build` — build standard Docker image\n- `task docker:up` — start Docker compose stack\n\n## Comments\n\nA comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.\n\nComment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so \"this used to reimplement the modal internals\" becomes \"thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart\". Future state goes in a TODO with an issue.\n\nWrite a comment when it does one of these four jobs:\n\n- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.\n- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.\n- **Hazard.** \"Must stay in sync with X\", \"order matters because Y\", \"do not remove, it prevents Z\".\n- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.\n\nNever write:\n\n- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.\n- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.\n- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.\n- Commented-out code. Delete it.\n- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.\n- Docs on self-explanatory members with no constraint to state.\n\nTwo tests before keeping a comment:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.\n\nA comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // \"%PDF\"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.\n\nA reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.\n\nA TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.\n\nA comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.\n\n`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @docs/developer/CODE_COMMENTS.md\n\n## Common Development Commands\n\n### Build and Test\n- **Build project**: `task build`\n- **Run backend locally**: `task backend:dev`\n- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)\n- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)\n- **Code formatting**: `task format` (or `task backend:format` for Java only)\n- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)\n\nAfter modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.\n\n### Docker Development\n- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)\n- **Build fat version**: `task docker:build:fat`\n- **Build ultra-lite**: `task docker:build:ultra-lite`\n- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)\n- **Stop compose stack**: `task docker:down`\n- **View logs**: `task docker:logs`\n- **Example compose files**: Located in `exampleYmlFiles/` directory\n\n### Security Mode Development\nSet `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.\n\n### Python Development (AI Engine)\n\nThe engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.\n\n#### Python Commands\nAll engine commands run from the repo root using Task:\n- `task engine:check` — run all checks (typecheck + lint + format-check + test)\n- `task engine:fix` — auto-fix lint + formatting\n- `task engine:install` — install Python dependencies via uv\n- `task engine:dev` — start FastAPI with hot reload (localhost:5001)\n- `task engine:test` — run pytest\n- `task engine:lint` — run ruff linting\n- `task engine:typecheck` — run pyright\n- `task engine:format` — format code with ruff\n- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec\n\nThe project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.\n\n#### Python Code Style\n- Keep `task engine:check` passing.\n- Use modern Python when it improves clarity.\n- Prefer explicit names to cleverness.\n- Avoid nested functions and nested classes unless the language construct requires them.\n- Prefer composition to inheritance when combining concepts.\n- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.\n- Comments follow the repo-wide rules in the \"Comments\" section above.\n\n#### Python Typing and Models\n- Deserialize into Pydantic models as early as possible.\n- Serialize from Pydantic models as late as possible.\n- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.\n- Avoid `Any` wherever possible.\n- Avoid `cast()` wherever possible (reconsider the structure first).\n- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.\n- Do not use string literals for any type annotations, including `cast()`.\n\n#### Python Configuration\n- Keep application-owned configuration in `stirling.config`.\n- Only add `STIRLING_*` environment variables that the engine itself truly owns.\n- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.\n- Let `pydantic-ai` own provider authentication configuration when possible.\n\n#### Python Architecture\n\n**Package roles:**\n- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.\n- `stirling.models`: shared model primitives and generated tool models.\n- `stirling.agents`: reasoning modules for individual capabilities.\n- `stirling.api`: HTTP layer, dependency access, and app startup wiring.\n- `stirling.services`: shared runtime and non-AI infrastructure.\n- `stirling.config`: application-owned settings.\n\n**Source of truth:**\n- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.\n- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.\n- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.\n- If a tool ID must match a parameter model, validate that relationship explicitly in code.\n\n**Boundaries:**\n- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.\n- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.\n- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.\n- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.\n\n#### Python AI Usage\n- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.\n- Use AI for reasoning-heavy outputs, not deterministic glue.\n- Do not ask the model to invent data that Python can derive safely.\n- Do not fabricate fallback user-facing copy in code to hide incomplete model output.\n- AI output schemas should be impossible to instantiate incorrectly.\n  - Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.\n  - Prefer Python to derive deterministic follow-up structure from a valid AI result.\n- Use `NativeOutput(...)` for structured model outputs.\n- Use `ToolOutput(...)` when the model should select and call delegate functions.\n\n#### Python Testing\n- Test contracts directly.\n- Test agents directly where behaviour matters.\n- Test API routes as thin integration points.\n- Prefer dependency overrides or startup-state seams to monkeypatching random globals.\n\n### Frontend Development\n- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080\n- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS\n- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)\n- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines\n- **Package Installation**: `task frontend:install`\n- **Deployment Options**:\n  - **Desktop App**: `task desktop:build`\n  - **Web Server**: `task frontend:build` then serve dist/ folder\n  - **Development**: `task desktop:dev` for desktop dev mode\n\n#### Environment Variables\n- All `VITE_*` variables must be declared in the appropriate committed env file:\n  - `frontend/.env` — core and shared vars (base, loaded in every mode)\n  - `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)\n  - `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)\n  - `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)\n- These files are committed to Git and must not contain private keys\n- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top\n- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files\n- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file\n- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks\n- See `frontend/README.md#environment-variables` for full documentation\n\n#### Import Paths - CRITICAL\n**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.\n\nFor a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md\n\nBefore touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.\n\nBefore adding or styling an icon, read @frontend/src/editor/core/icons/README.md — `<Icon name=\"…\" />` comes from `@app/ui/Icon`, icon sources live as `.svg` files in `core/icons/svg/`, and inline `<svg>` in TS/TSX is linted out.\n\n```typescript\n// ✅ CORRECT - Use @app/* for all imports\nimport { AppLayout } from \"@app/components/AppLayout\";\nimport { useFileContext } from \"@app/contexts/FileContext\";\nimport { FileContext } from \"@app/contexts/FileContext\";\n\n// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code\nimport { AppLayout } from \"@core/components/AppLayout\";\nimport { useFileContext } from \"@proprietary/contexts/FileContext\";\n```\n\n**Only use explicit aliases when:**\n- Building layer-specific override that wraps a lower layer's component\n- Example: `import { AppProviders as CoreAppProviders } from \"@core/components/AppProviders\"` when creating proprietary/AppProviders.tsx that extends the core version\n\nThe `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see \"Frontend `cloud/` Layer\" below for the full per-flavor order.\n\n#### Frontend `cloud/` Layer\n\n`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):\n\n- **core** → core\n- **proprietary** → proprietary → core\n- **saas** → saas → cloud → proprietary → core\n- **desktop** → desktop → cloud → proprietary → core\n- **cloud** → cloud → proprietary → core\n\nWhat goes where:\n\n- **core** — OSS base.\n- **proprietary** — licensed / offline features.\n- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.\n- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.\n- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.\n\n`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.\n\nRule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).\n\n**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local mode and the server keeps the on/off switch (no desktop release needed to flip a flag). In self-hosted mode the connected server's own `AppConfigContext` is the authority instead. Gate a feature behind a per-platform seam - e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useAppConfig()` when signed in to a self-hosted server and `useSaasAppConfig()` otherwise) - rather than hardcoding the flag on.\n\n#### Component Override Pattern (Stub/Shadow)\nUse this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.\n\n**How it works:**\n1. Core defines stub component (returns null or no-op)\n2. Desktop/proprietary overrides with same path/name\n3. Core imports via `@app/*` - higher layer \"shadows\" core in those builds\n4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!\n\n**Example - Desktop-specific footer:**\n\n```typescript\n// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {\n  return null; // Stub - does nothing in web builds\n}\n```\n\n```tsx\n// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)\nimport { Box } from '@mantine/core';\nimport { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';\n\ninterface WorkbenchBarFooterExtensionsProps {\n  className?: string;\n}\n\nexport function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {\n  return (\n    <Box className={className}>\n      <BackendHealthIndicator />\n    </Box>\n  );\n}\n```\n\n```tsx\n// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)\nimport { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';\n\nexport function WorkbenchBar() {\n  return (\n    <div>\n      {/* In web builds: renders nothing (stub returns null) */}\n      {/* In desktop builds: renders BackendHealthIndicator */}\n      <WorkbenchBarFooterExtensions className=\"workbench-bar-footer\" />\n    </div>\n  );\n}\n```\n\n**Build resolution:**\n- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)\n- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)\n\n**Benefits:**\n- No runtime checks or feature flags\n- Type-safe across all builds\n- Clean, readable code\n- Build-time optimization (dead code elimination)\n\n#### Multi-Tool Workflow Architecture\nFrontend designed for **stateful document processing**:\n- Users upload PDFs once, then chain tools (split → merge → compress → view)\n- File state and processing results persist across tool switches\n- No file reloading between tools - performance critical for large PDFs (up to 100GB+)\n\n#### FileContext - Central State Management\n**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`\n- **Active files**: Currently loaded PDFs and their variants\n- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)\n- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management\n- **IndexedDB persistence**: File storage with thumbnail caching\n- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution\n\n**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.\n\n#### Processing Services\n- **enhancedPDFProcessingService**: Background PDF parsing and manipulation\n- **thumbnailGenerationService**: Web Worker-based with main-thread fallback\n- **fileStorage**: IndexedDB with LRU cache management\n\n#### Memory Management Strategy\n**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:\n- PDF.js documents that need explicit .destroy() calls\n- Blob URLs from tool outputs that need revocation\n- Web Workers that need termination\nWithout cleanup: browser crashes with memory leaks.\n\n#### Tool Development\n\n**Architecture**: Modular hook-based system with clear separation of concerns:\n\n- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook\n  - Coordinates all tool operations with consistent interface\n  - Integrates with FileContext for operation tracking\n  - Handles validation, error handling, and UI state management\n\n- **Supporting Hooks**:\n  - **useToolState**: UI state management (loading, progress, error, files)\n  - **useToolApiCalls**: HTTP requests and file processing\n  - **useToolResources**: Blob URLs, thumbnails, ZIP downloads\n\n- **Utilities**:\n  - **toolErrorHandler**: Standardized error extraction and i18n support\n  - **toolResponseProcessor**: API response handling (single/zip/custom)\n  - **toolOperationTracker**: FileContext integration utilities\n\n**Three Tool Patterns**:\n\n**Pattern 1: Single-File Tools** (Individual processing)\n- Backend processes one file per API call\n- Set `multiFileEndpoint: false`\n- Examples: Compress, Rotate\n```typescript\nreturn useToolOperation({\n  operationType: 'compress',\n  endpoint: '/api/v1/misc/compress-pdf',\n  buildFormData: (params, file: File) => { /* single file */ },\n  multiFileEndpoint: false,\n});\n```\n\n**Pattern 2: Multi-File Tools** (Batch processing)\n- Backend accepts `MultipartFile[]` arrays in single API call\n- Set `multiFileEndpoint: true`\n- Examples: Split, Merge, Overlay\n```typescript\nreturn useToolOperation({\n  operationType: 'split',\n  endpoint: '/api/v1/general/split-pages',\n  buildFormData: (params, files: File[]) => { /* all files */ },\n  multiFileEndpoint: true,\n  filePrefix: 'split_',\n});\n```\n\n**Pattern 3: Complex Tools** (Custom processing)\n- Tools with complex routing logic or non-standard processing\n- Provide `customProcessor` for full control\n- Examples: Convert, OCR\n```typescript\nreturn useToolOperation({\n  operationType: 'convert',\n  customProcessor: async (params, files) => { /* custom logic */ },\n});\n```\n\n**Benefits**:\n- **No Timeouts**: Operations run until completion (supports 100GB+ files)\n- **Consistent**: All tools follow same pattern and interface\n- **Maintainable**: Single responsibility hooks, easy to test and modify\n- **i18n Ready**: Built-in internationalization support\n- **Type Safe**: Full TypeScript support with generic interfaces\n- **Memory Safe**: Automatic resource cleanup and blob URL management\n\n## Architecture Overview\n\n### Project Structure\n- **Backend**: Spring Boot application\n- **Frontend**: React-based SPA in `/frontend` directory\n  - **File Storage**: IndexedDB for client-side file persistence and thumbnails\n  - **Internationalization**: JSON-based translations (converted from backend .properties)\n- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering\n- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)\n- **Configuration**: YAML-based configuration with environment variable overrides\n\n### Controller Architecture\n- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations\n  - Organized by function: converters, security, misc, pipeline\n  - Follow pattern: `@RestController` + `@RequestMapping(\"/api/v1/...\")`\n\n### Key Components\n- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic\n- **ConfigInitializer**: Handles runtime configuration and settings files\n- **Pipeline System**: Automated PDF processing workflows via `PipelineController`\n- **Security Layer**: Authentication, authorization, and user management (when enabled)\n\n### Frontend Directory Structure\nThe frontend is organized with a clear separation of concerns:\n\n- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)\n  - **`core/components/`**: React components organized by feature\n    - `core/components/tools/`: Individual PDF tool implementations\n    - `core/components/viewer/`: PDF viewer components\n    - `core/components/pageTracks/`: Page editor (every open PDF as a track of pages)\n    - `core/components/tooltips/`: Help tooltips for tools\n    - `core/components/shared/`: Reusable UI components\n  - **`core/contexts/`**: React Context providers\n    - `FileContext.tsx`: Central file state management\n    - `file/`: File reducer and selectors\n    - `toolWorkflow/`: Tool workflow state\n  - **`core/hooks/`**: Custom React hooks\n    - `hooks/tools/`: Tool-specific operation hooks (one directory per tool)\n    - `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)\n  - **`core/constants/`**: Application constants and configuration\n  - **`core/data/`**: Static data (tool taxonomy, etc.)\n  - **`core/services/`**: Business logic services (PDF processing, storage, etc.)\n\n- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code\n- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features\n- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code\n- **`frontend/public/`**: Static assets served directly\n  - `public/locales/`: Translation JSON files\n\n### Component Architecture\n- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)\n- **Internationalization**:\n  - Backend: `messages_*.properties` files\n  - Frontend: JSON files in `frontend/public/locales/` (converted from .properties)\n  - Conversion Script: `scripts/convert_properties_to_json.py`\n\n### Configuration Modes\n- **Ultra-lite**: Basic PDF operations only\n- **Standard**: Full feature set\n- **Fat**: Pre-downloaded dependencies for air-gapped environments\n- **Security Mode**: Adds authentication, user management, and enterprise features\n\n### Testing Strategy\n- **Integration Tests**: Cucumber tests in `testing/cucumber/`\n- **Docker Testing**: `test.sh` validates all Docker variants\n- **Manual Testing**: No unit tests currently - relies on UI and API testing\n\n#### No fixed waits in tests\nNever sleep in tests (`page.waitForTimeout`, `Thread.sleep`, etc.) - it causes flakes. Wait for the actual condition instead (`expect(...)`, `expect.poll`, `waitForFunction`, `vi.waitFor`), or use fake timers for time-based behaviour.\n\n## Development Workflow\n\n1. **Local Development** (using Taskfile):\n   - Backend + frontend: `task dev`\n   - All services (including AI engine): `task dev:all`\n   - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)\n2. **Quality Gate**: Run `task check` before submitting PRs\n3. **Docker Testing**: Use `./test.sh` for full Docker integration tests\n4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)\n5. **Translations**:\n   - Backend: Use helper scripts in `/scripts` for multi-language updates\n   - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script\n6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`\n\n## Frontend Architecture Status\n\n- **Core Status**: React SPA architecture complete with multi-tool workflow support\n- **State Management**: FileContext handles all file operations and tool navigation\n- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)\n- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator\n  - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`\n  - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`\n  - Pattern: Each tool creates focused operation hook, UI consumes state/actions\n- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)\n- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing\n\n## Translation Rules\n\n- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately\n- Translation files are located in `frontend/public/locales/`\n- After changing any translation file, run `task pre-commit:fix`\n\n## Important Notes\n\n- **Java Version**: Requires JDK 25.\n- **Lombok**: Used extensively - ensure IDE plugin is installed\n- **File Persistence**:\n  - **Backend**: Designed to be stateless - files are processed in memory/temp locations only\n  - **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)\n- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation\n- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer\n- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling\n- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code\n- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)\n- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes\n- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)\n- **Adding Tools**: See `docs/developer/ADDING_TOOLS.md` for complete guide to creating new PDF tools\n\n## Communication Style\n- Be direct and to the point\n- No apologies or conversational filler\n- Answer questions directly without preamble\n- Explain reasoning concisely when asked\n- Avoid unnecessary elaboration\n\n## Decision Making\n- Ask clarifying questions before making assumptions\n- Stop and ask when uncertain about project-specific details\n- Confirm approach before making structural changes\n- Request guidance on preferences (cross-platform vs specific tools, etc.)\n- Verify understanding of requirements before proceeding\n\n\n## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->\n\nThis codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.1.1**,\n**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.\nAll three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and\nJackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no\nlonger exist.\n\nBefore writing or editing Spring / Jackson / JDK code:\n\n1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being\n   used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new\n   `org.springframework.boot` 4.x package layout.\n2. If you're not sure whether an API exists in this stack version, **check the source on disk\n   first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).\n3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something\n   doesn't work, surface it to the human — don't guess.\n\nSame goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new\n`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's\nactual imports, not what worked three years ago.\n\n<!-- File: .claude/skills/comment-review/SKILL.md (Tokens: ~2388 | Category: anthropic-skill) -->\n---\nname: comment-review\ndescription: >-\n  Review and improve the code comments a PR or branch adds, against the standard in\n  docs/developer/CODE_COMMENTS.md: cut the ones that restate the code, tighten the verbose\n  ones, reframe what-comments into the why or the constraint, and add the contract\n  docs the change left missing. Runs comment-lint for the mechanical rules first,\n  then judges what a linter cannot: padding around one real fact, facts already\n  stated by a type or a name, comments that will go stale on the next unrelated\n  change, and new public surfaces left undocumented. Applies comment-only edits and\n  reports every verdict in chat. Use when asked to review, clean up, trim, thin out\n  or improve the comments on a PR or branch, to strip AI-looking narration before\n  requesting review, or when comment-lint findings need judgment rather than a\n  mechanical fix. Pass --report-only to change nothing.\nargument-hint: \"[pr-number|branch|base-ref] [--report-only]\"\nallowed-tools: Bash, Read, Grep, Glob, Edit\n---\n\n# Comment review\n\nMake the comments this branch adds fewer and worth more. Every comment kept is a line\nsomeone has to maintain and eventually reconcile with the code, so it has to pay for\nitself. A pass that only deletes is half the job: the same read should find the\ncontract, hazard and unit the change introduced and left unsaid.\n\n`$ARGUMENTS` may name a PR number, a branch, or a base ref; default is this branch\nagainst where it forked from the main line. `--report-only` reports without editing.\n\n## The standard\n\n`AGENTS.md` section \"Comments\" is the short form; `docs/developer/CODE_COMMENTS.md` is the\nauthority, with the reasoning and the worked examples. **Read\n`docs/developer/CODE_COMMENTS.md` before judging anything.** Do not work from memory of what\na good comment is: the repo has already decided, and the four jobs (contract, why,\nhazard, map) are the whole permitted set.\n\n## Hard rules\n\n1. **Comment-only diff.** Never rename, extract, reorder or restructure code in this\n   pass. Where the right fix is a code change, report it as a suggestion.\n2. **Scope is what the branch adds or edits.** Comments the branch never touched are\n   the standing backlog (`task pre-commit:comment-lint:all`), not this review.\n3. **Never delete information you cannot re-derive from the code in front of you.** If\n   verifying the comment sent you into another file, a spec, or the git log, it is\n   carrying information. Keep it; tighten at most.\n4. **Never invent a rationale.** If the why is not determinable from the code, cut the\n   comment or flag it for the author. A fabricated why gets trusted, which is worse\n   than silence.\n5. **No suppressions.** Do not add `comment-lint-allow` to make a finding go away. If a\n   rule is genuinely wrong, say which and why in the report.\n6. **Report in chat.** Never post to the PR, never reply to threads, never resolve\n   anything, unless the user asks in this session.\n7. **No ratio targets.** Volume is not the metric. A dense header on a genuinely\n   complex file can be exactly right, and the repo's highest comment-ratio commits are\n   its best-documented work. Judge one comment at a time.\n\n## Process\n\n### 1. Scope the change\n\nRead the PR's own base branch rather than assuming main, because a stacked PR whose\nbase is another PR would otherwise be reviewed together with its parent:\n\n```bash\ngh pr view <n> --json baseRefName,headRefName,title\n```\n\nThen get the head onto disk. `gh pr checkout <n>` normally, but it fails when\nanother worktree already holds that branch, and a review needs no branch of its own:\n\n```bash\ngit fetch -f origin <headRefName>:refs/review/pr<n>-head <baseRefName>:refs/review/pr<n>-base\ngit checkout --detach refs/review/pr<n>-head\nBASE=$(git merge-base HEAD refs/review/pr<n>-base)\n```\n\nA stale remote ref can fail the fetch entirely (\"incorrect old value provided\"). Fetch\nthe two branches by name as above rather than fetching everything, and confirm the\nhead commit is the PR's before reading anything.\n\nWith no PR number, review this branch against where it forked from the main line:\n`BASE=$(git merge-base HEAD origin/main)`.\n\n### 2. Mechanical pass\n\n```bash\nnode scripts/lint/comment-lint.mjs --since \"$BASE\"\n```\n\nIf it warns that oxlint is missing, run `task frontend:install` first or the TS and TSX\nhalf is silently unchecked. Each finding names a rule in\n`scripts/lint/comment-rules.mjs`; the fix is the rewrite the rule implies. The rules\nare deliberately narrow, they do not judge trailing comments at all, and each one\nexcludes readings that had an innocent form. So a clean run says nothing about\nverbosity: it is the floor, not the review.\n\n### 3. Inventory every comment the branch adds\n\n```bash\nnode .claude/skills/comment-review/added-comments.mjs \"$BASE\"\n```\n\nThis is the checklist, and it over-reports slightly by design. Every entry on it gets\na verdict. Do not rewrite it as an inline awk or perl one-liner: `$0` and `$1` in the\nscript body are rewritten by argument interpolation before the shell sees them, which\nsilently produces an empty inventory and a review that judged nothing.\n\nAlso read what the branch **removed**:\n\n```bash\ngit diff \"$BASE\"...HEAD | grep -E \"^-\\s*(//|/\\*|\\*|#)\" | head -40\n```\n\nA rewritten comment shows up as one line added and one removed, and the removed half\nis where a fact goes missing. A branch that has already had a tightening pass is the\nlikeliest place to find one.\n\n### 4. Read the code, not the diff\n\nOpen each file at the comment. A diff hunk is not enough to judge one: you need the\ncode it introduces, the signature it claims to document, and whether the fact it states\nis already carried by a type, a name, a constant, an enum's own cases, or a doc block\ntwo lines up. Two copies of one fact drift apart, which is the maintenance cost with\nnone of the value.\n\n### 5. Judge\n\nFive tests, cheapest first:\n\n- **Delete it.** Is any information lost? If not, it stays deleted.\n- **Altitude.** Does it sit at a different level of detail than the code below, either\n  lower (a precise fact the code implies but does not say) or higher (intent a reader\n  would otherwise assemble from ten lines)? Same altitude is the definition of\n  redundant.\n- **Name.** Could a better identifier, an extracted function or a named constant carry\n  it instead? Then the comment is the wrong fix.\n- **Staleness.** Will this need editing the next time the code changes for an unrelated\n  reason? A comment that tracks the code rather than constrains it will drift and then\n  mislead.\n- **Duplication.** Is this fact stated somewhere else already? Delete the copy.\n\nGive every inventoried comment exactly one verdict:\n\n- **Cut.** Restates the code, banners a section, narrates steps or history, is\n  commented-out code, duplicates a fact, or pads one real fact with three sentences\n  that only set each other up.\n- **Tighten.** The fact is real and the prose is long. Keep the fact, drop the words.\n  One fact per comment; no sentence whose only job is to introduce the next one.\n- **Reframe.** It says what the code does. State the why, the hazard or the contract\n  instead, subject to rule 4.\n- **Add.** The change introduced something a caller outside the file can reach, or an\n  invariant, unit, ordering requirement, lifetime, thread-safety or error semantic, and\n  left it undocumented. Also the map comment a genuinely complex new file needs.\n- **Keep.** Earns its place. Count these; do not list them one by one.\n\n**Do not cut:**\n\n- Hazards: \"must stay in sync with X\", \"order matters because Y\", \"do not remove, it\n  prevents Z\".\n- Units, ranges, encodings, magic-byte decodes, currency and timezone assumptions.\n  These are mostly trailing comments, which the linter leaves alone because on this\n  codebase most of them are right.\n- The alternative that was rejected, and the reason.\n- Spec, RFC and CVE references, and TODOs that carry an issue.\n- Security and failure semantics: fail-closed ordering, what is deliberately not\n  defended, ownership and lifetime.\n- Anything whose truth you had to leave the file to verify.\n\n### 6. Apply\n\nSkip this step entirely under `--report-only`.\n\nEdit comments only. Match the file's convention: Javadoc summary fragments rather than\n\"This method returns\", JSDoc on the `@app/*` seams and layer boundaries with no\n`@param` restating a typed signature, Python docstrings where the type does not carry\nthe contract. Plain ASCII prose, no em dashes, no repeated bold lead-ins.\n\nThen run the formatter for each language touched, because rewrapping a doc block\nchanges line wrapping and the format gate will fail otherwise:\n\n- Java: `task backend:format`\n- TS and TSX: `task frontend:format`\n- Python: `task engine:fix`\n\nRe-run `node scripts/lint/comment-lint.mjs --since \"$BASE\"` and confirm it is clean. Do\nnot commit unless asked. The diff is comment-only, so `git diff` is the entire review\nsurface.\n\n### 7. Report\n\nTerse, in chat:\n\n- One counts line: `N in scope: X cut, Y tightened, Z reframed, A added, K kept`.\n- Per file, one line per changed comment: a clickable `path:line`, the verdict, and the\n  reason in a few words. No before/after quoting unless it is genuinely unclear.\n- **Needs your call**, listing what you deliberately did not do: comments whose truth\n  you could not verify, rationale you would have had to invent, and the code changes a\n  comment was papering over (extract this, rename that, split the file).\n- Any linter finding you believe is wrong, naming the rule.\n\n## Principles\n\n- Fewer comments, more information in each.\n- The reader is a new contributor with this file open, not someone who read the PR.\n- Deleting a good comment costs more than keeping a mediocre one. Uncertainty means\n  keep it and flag it.\n- The additions matter as much as the cuts. A branch that leaves a new public surface\n  undocumented is not improved by trimming its narration.\n\n<!-- File: .claude/skills/feature-walkthrough/SKILL.md (Tokens: ~1322 | Category: anthropic-skill) -->\n---\nname: feature-walkthrough\ndescription: >-\n  Explain the full logic and process of the current branch end-to-end so someone\n  with no prior knowledge of the task can understand, review, and reproduce it.\n  Scopes the change from the branch diff, traces the flow across every layer it\n  touches (frontend tool/hook/component, Java controller/service/endpoint, Python\n  engine, config, i18n, tests), and produces a self-contained walkthrough document\n  with Mermaid diagrams (sequence/flow/architecture), annotated file map with\n  clickable references, before/after behavior, screenshots where a UI is involved,\n  a \"try it locally\" section, and edge cases/risks. Use when asked for a feature or\n  branch walkthrough, \"explain what this branch does\", a design/logic writeup, PR\n  reviewer onboarding, or a hand-off doc. Pass --html to also emit a rendered HTML\n  version; --no-screens to skip screenshots.\nargument-hint: \"[branch-or-area] [--html] [--no-screens]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# Feature / Branch Walkthrough\n\nTurn the current branch into a walkthrough a newcomer can follow. Audience:\n**someone who has never seen this task**. Explain the *why*, the *flow*, and *how to\ntry it* - not just a diff summary.\n\n`$ARGUMENTS` may name a branch or area to focus on; default is the current branch\nvs `main`. Flags: `--html` (also emit a rendered HTML twin), `--no-screens`.\n\n## Process\n\n### 1. Scope the change\n- `git log --oneline main..HEAD` and `git diff --stat main...HEAD` for the shape.\n- Read the PR description / commit messages for stated intent. Do **not** invent\n  history or motivation that isn't evidenced (state current behavior in present tense).\n- Classify touched files by layer:\n  - **Frontend**: tools (`frontend/src/editor/core/components/tools/*` or `.../core/tools/*`),\n    hooks (`core/hooks/tools/*`, `useToolOperation`), contexts, routes, i18n\n    (`public/locales/en-US`).\n  - **Java backend**: controllers (`.../controller/api/...`), services, models, config.\n  - **Engine**: `engine/src/stirling/{agents,contracts,api,services}`.\n  - **Config / build / docker / tests.**\n\n### 2. Trace the flow end-to-end\nFollow one real path from user action to result. For a typical PDF tool that's:\nUI control → `useToolOperation` hook → `POST /api/v1/...` → Spring controller →\nservice (PDFBox / LibreOffice / engine call) → response → review panel → download.\nRead the actual files so the narrative is true to the code, and collect the exact\nfile:line anchors you'll cite.\n\n### 3. Draw the diagrams (Mermaid)\nPick what fits; usually 2-3 of:\n- **Sequence diagram** - request/response across frontend → backend → engine.\n- **Flowchart** - the core decision/branching logic of the feature.\n- **Architecture/component** - new pieces and how they wire to existing ones.\n- **State** - if the feature has modes/steps.\nKeep nodes labeled in plain language. Validate the Mermaid parses before shipping.\n\n### 4. Screenshots (unless --no-screens)\nIf a UI is involved, capture key states with the stubbed Playwright harness\n(see the **ui-walkthrough** skill and `files-page-screenshots.spec.ts` for the\npattern) or, for before/after, capture `main` then the branch. Drop PNGs in\n`walkthrough/<feature>/` and reference them from the doc. For backend-only\nchanges, show request/response examples (curl + JSON) instead.\n\n### 5. Write the walkthrough\nCreate `walkthrough/<feature>/FEATURE-WALKTHROUGH.md` with:\n1. **TL;DR** - what the branch does and who it's for, in 3-4 sentences.\n2. **Problem & approach** - what wasn't possible before; the chosen solution.\n3. **Architecture diagram** + 1-paragraph orientation.\n4. **End-to-end flow** - the sequence diagram + a numbered walk of each step,\n   each citing the real file (clickable `path:line`).\n5. **Key files** - annotated map (path → one line on its role).\n6. **Logic deep-dive** - the flowchart + prose for the non-obvious decisions.\n7. **Behavior** - before vs after; screenshots or request/response examples.\n8. **Try it locally** - exact steps (`task dev` / `task dev:all`, the route to\n   open or the curl to run, any env like `DOCKER_ENABLE_SECURITY` or a test\n   license key). Make it copy-pasteable.\n9. **Edge cases, risks, follow-ups** - what's untested, known limits, gotchas.\n\nMarkdown is the primary deliverable - it renders with diagrams in GitHub PRs and\nIDEs, no build step, ideal for review.\n\n### 6. If `--html`\nAlso emit `walkthrough/<feature>/walkthrough.html`: the same content with Mermaid\nrendered via `mermaid.initialize({startOnLoad:true})` (script from CDN; note in\nthe file that rendering diagrams needs network, the `.md` is the offline copy) and\nscreenshots inline. Keep it self-contained otherwise.\n\n### 7. Deliver\nGive the doc path and a short chat summary. Offer to `SendUserFile` it.\n\n## Principles\n- **True to the code.** Every claim traces to a file you read; cite `path:line`.\n  No fabricated migration/version history.\n- **Newcomer-first.** Define repo-specific terms (FileContext, `useToolOperation`,\n  the `@app/*` layer cascade, stubbed vs live tests) on first use.\n- **Show, don't assert.** Prefer a diagram + a real example over adjectives.\n- Don't commit the `walkthrough/` output unless asked.\n\n<!-- File: .claude/skills/pr-quiz/SKILL.md (Tokens: ~1891 | Category: anthropic-skill) -->\n---\nname: pr-quiz\ndescription: >-\n  Quiz the PR author on their own branch before they request review, to prove they\n  actually understand the change - especially code an AI wrote for them. Scopes the\n  branch diff vs its base, reads the changed code, then asks graded questions about\n  what changed, why, how it works, what it could break, and which edge cases it must\n  handle. Presents all questions first, waits for the author's answers, then grades\n  each honestly against the real code (Correct / Partial / Incorrect with the true\n  answer and file:line), scores it, and gives a readiness verdict that names the\n  areas to re-study before asking humans to review. Use when asked to quiz me on my\n  PR/branch, \"test my understanding before review\", a self-check gate before opening\n  a PR, or before requesting reviewers. Administered as an interactive\n  multiple-choice quiz (clickable options) by default; pass --free-text for\n  written answers, --questions N to set count, --save to write a scorecard.\nargument-hint: \"[branch-or-base-ref] [--questions N] [--free-text] [--save]\"\nallowed-tools: Bash, Read, Grep, Glob, Write, AskUserQuestion\n---\n\n# PR Quiz\n\nTest whether the **author** genuinely understands their own branch before they ask\nother people to spend time reviewing it. This is a self-check gate: the point is to\ncatch changes - often AI-written - that the author would not be able to explain or\ndefend in review. Be a fair but honest examiner, not a pushover.\n\n`$ARGUMENTS` may name a base ref or branch to diff against; default is this branch\nvs where it forked from the main line. Flags:\n- `--questions N` - target N questions (else scale to diff size, see below).\n- `--free-text` - administer as a written numbered list instead of the default\n  interactive multiple-choice.\n- `--save` - also write a scorecard file after grading.\n\n## Integrity rules (read first - the whole skill depends on these)\n\n1. **Present every question before revealing any answer.** Ask, then wait. Never\n   show the answer key alongside the questions.\n2. **Do not give hints or the answer while the quiz is open.** If the author asks\n   \"what's the answer?\" or \"is it X?\" before committing, decline warmly and tell\n   them to give their best answer first - guessing is part of the signal.\n3. **Grade truthfully.** Vague, hand-wavy, or \"the AI did it\" non-answers are\n   Partial or Incorrect, not Correct. Do not inflate the score to be nice; a false\n   pass defeats the entire purpose.\n4. **Ground everything in code you actually read.** Every question and every model\n   answer must trace to a real line in the diff. Cite `path:line`. No trivia\n   (\"how many lines?\"), no invented behavior.\n5. **Credit real understanding.** If the author explains it correctly in their own\n   words, mark it Correct even if worded differently than your key.\n\n## Process\n\n### 1. Scope the change (silently)\n- Find the base. Prefer the fork point off the main line so the quiz covers only\n  this branch's work:\n  ```bash\n  git fetch -q origin 2>/dev/null; \\\n  BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main); \\\n  git diff --stat \"$BASE\"...HEAD\n  ```\n  If `$ARGUMENTS` names a ref, diff against that instead.\n- If the diff is empty, stop and say there's nothing to quiz on.\n- Read commit messages / PR description for the *stated* intent, but verify it\n  against the actual diff - a mismatch is itself a good question.\n\n### 2. Understand the code well enough to examine on it\nRead the full diff plus enough surrounding context and related files to answer\nevery question you plan to ask. You cannot grade understanding you don't have.\nNote the non-obvious parts: the design decisions, the risky lines, the edge cases,\nthe cross-file ripples, and anything that violates or upholds repo conventions\n(for this repo e.g. `@app/*` import layering, all file ops via FileContext,\nJackson 3 / Spring Boot 4 APIs, engine typed-contract boundaries).\n\n### 3. Build the question set\nScale count to the change unless `--questions N` is given:\nsmall (< ~50 changed lines) 3-4, medium 5-8, large 9-12. Cap at 12.\nDraw from these categories - weight toward the ones the diff actually exercises:\n- **Intent** - what problem this solves; why it was needed now.\n- **Mechanism** - how a specific non-trivial piece actually works (\"walk me\n  through what `foo()` does when called with X\").\n- **Decisions & alternatives** - why this approach over an obvious alternative;\n  what a reviewer would reasonably push back on.\n- **Blast radius** - what else this touches or could break; what you'd retest.\n- **Edge cases** - inputs/states the change must handle (null, empty, large,\n  concurrent, error paths).\n- **Conventions & correctness** - does it follow the repo's rules; is there a\n  latent bug the author should be able to spot.\nPrefer questions the author can only answer if they read and understood the code.\nKeep a private answer key with `path:line` for each - do **not** show it yet.\n\n### 4. Administer the quiz\n- **Default (multiple choice):** use the `AskUserQuestion` tool. Per question write\n  3-4 options where **every** option is independently plausible - each distractor a\n  real-but-wrong reading of the code, not filler. Two hard rules so the answer\n  can't be spotted by shape rather than knowledge:\n  - **Randomise the correct option's position** across questions - never default\n    it to first. Spread it roughly evenly over the slots.\n  - **Keep all options the same depth and length.** Do not describe the correct\n    one more fully than the distractors - a longer or more-detailed option is a\n    dead giveaway. Trim the right answer or flesh out the wrong ones until a\n    reader can't tell them apart by size.\n  The tool caps a call at 4 questions, so ask in batches of 4 - but run them as\n  one continuous flow: fire the next batch immediately after the previous\n  returns, with no narration (\"Round 2 of 3\") and no grading between batches.\n  The author always has an \"Other\" free-text escape, which is fine.\n- **`--free-text`:** present all questions in one numbered list, then say\n  \"Answer in one reply; number your answers. I won't grade until you're done.\"\n  Wait for the author's answers.\n- Do not proceed to grading until every answer is in.\n\n### 5. Grade\nFor each question, in order:\n- Verdict: **Correct** / **Partial** / **Incorrect**.\n- The model answer in one or two sentences, citing the real `path:line`.\n- One line on the gap when Partial/Incorrect - what they missed and where to look.\nThen a **Score** (e.g. 6/8, counting Partial as half) and a one-line summary of\nthe pattern (e.g. \"solid on intent, shaky on the error paths\").\n\n### 6. Readiness verdict\nEnd with a clear call:\n- **Ready for review** - understanding is sound; note anything to mention to\n  reviewers proactively.\n- **Study first** - list the specific files/concepts to re-read before requesting\n  review, each as a clickable `path:line`. Be concrete: \"re-read the null handling\n  in X before you send this out.\"\nKeep it honest - if they'd get grilled in review on something, say so now.\n\n### 7. If `--save`\nWrite `pr-quiz/<branch>-scorecard.md`: the questions, their answers, your grades\nand model answers, the score, and the verdict. Don't commit it unless asked.\n\n## Principles\n- **The author is the examinee, not the collaborator.** During the quiz you withhold\n  answers; you're measuring them, not helping them pass.\n- **A failed quiz is a successful outcome** - it caught a gap before a human's time\n  was spent. Frame it that way, not as a scolding.\n- **True to the code.** Every question, answer, and grade traces to a line you read.\n- **Terse and direct** in chat - the questions and the verdict, minimal preamble.\n\n<!-- File: .claude/skills/ui-before-after/SKILL.md (Tokens: ~1822 | Category: anthropic-skill) -->\n---\nname: ui-before-after\ndescription: >-\n  Analyse a branch or PR and automatically capture before/after screenshots of\n  every UI surface its changes touch, then pixel-diff the pairs to surface what\n  actually changed and assemble PR-ready before/after montage images. Generic and\n  diff-driven: it derives the capture targets from the diff (changed tools/routes →\n  URLs) instead of hand-listing screens, captures \"before\" from the base branch and\n  \"after\" from the head, then keeps only the views that visually differ. Each\n  comparison is auto-cropped to the region that actually changed (the bounding box of\n  differing pixels), falling back to the full page only when the change spans most of\n  it. Use for before/after shots, a visual diff of a branch/PR, \"screenshots for the\n  PR description\", \"show what changed in the UI\", or a side-by-side of UI changes.\n  Takes a PR number/URL (resolved via gh) or a branch; defaults to the current branch\n  vs its base. Flags: --scope <selector>, --base <ref|merge-base>, --theme\n  light|dark|both, --all (capture every route, not just changed), --no-autocrop,\n  --pagewide <n>, --threshold <n>.\nargument-hint: \"[PR# | PR-url | branch] [--scope <sel>] [--base <ref>] [--theme both] [--all] [--no-autocrop]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Before / After (generic visual diff)\n\nPoint it at a branch or PR; it figures out which UI changed, screenshots every\naffected surface **before** (base) and **after** (head), pixel-diffs the pairs, and\nmontages the ones that actually changed into images for the PR description.\n\n`$ARGUMENTS`: a PR number/URL, a branch, or nothing (current branch vs base).\nBy default it captures the full viewport and auto-crops each comparison to the region\nthat changed. Flags: `--scope <css>` (narrow the *capture* to a container, e.g.\n`[data-sidebar=\"tool-panel\"]`, when you already know where the change is),\n`--no-autocrop` (keep full frames), `--pagewide <fraction>` (above this share of the\npage, skip cropping; default 0.6), `--base <ref|merge-base>`,\n`--theme light|dark|both`, `--all` (walk every route, not just changed),\n`--threshold <fraction>` (diff sensitivity, default 0.001).\n\nShares the capture harness with **ui-walkthrough** - read its SKILL.md for the\nstubbed-Playwright setup, worktree node_modules, the\nstale-`:5173` gotcha, and the dark-mode init-script. Bundled helpers:\n[capture-spec.template.ts](capture-spec.template.ts), [diff-shots.mjs](diff-shots.mjs),\n[montage-template.html](montage-template.html), [shoot-sections.mjs](shoot-sections.mjs).\n\n## Process\n\n### 1. Resolve target + base\n```\ngh pr view <pr> --json number,title,headRefName,baseRefName,url,files   # PR\n# or branch: base = merge-base(main, HEAD); head = HEAD\ngh pr diff <pr> --name-only        # or: git diff --name-only <base>...HEAD\n```\n\n### 2. Derive capture targets from the diff (the \"analyse\" step - no hand-listing)\nMap changed frontend files to URLs generically:\n- **Tools**: a changed `components/tools/<toolDir>/…` or `hooks/tools/<tool>/…` →\n  toolId → URL via the repo's own rule `getToolUrlPath` in\n  [toolsTaxonomy.ts:200](../../../frontend/src/editor/core/data/toolsTaxonomy.ts): `/` + the\n  id kebab-cased (`addPageNumbers` → `/add-page-numbers`).\n- **Pages/routes**: changed `filesPage/*` → `/files`, etc.\n- `--all`: enumerate every tool in the registry instead of just changed ones.\nWrite `frontend/screenshots/ui-diff/targets.json` =\n`[{ \"id\":\"compress\", \"url\":\"/compress\", \"name\":\"Compress\" }]`. This is what makes\nit generic - the spec never names a tool.\n\n### 3. Capture AFTER (head) then BEFORE (base)\nCopy [capture-spec.template.ts](capture-spec.template.ts) →\n`src/core/tests/stubbed/ui-before-after.spec.ts` (it loops `targets.json`, seeds a\nsample PDF so file-dependent panels render, navigates to each URL, and screenshots\nthe full viewport - or the `--scope` container if given). Ensure the harness is ready\n(node_modules + icons).\n```\n# after = current head\ncd frontend && PR_SHOT_SIDE=after PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n# before = base, in an isolated worktree (copy the spec + targets.json in)\ngit worktree add ../ba-base origin/<baseRefName>        # or the merge-base\n#   set up its frontend, copy spec + screenshots/ui-diff/targets.json across, then:\ncd ../ba-base/frontend && PR_SHOT_SIDE=before PR_SHOT_THEME=light \\\n  npx playwright test --project=stubbed ui-before-after.spec.ts\n#   copy its screenshots/ui-diff/before/ back next to after/. Repeat with\n#   PR_SHOT_THEME=dark if --theme includes dark. Remove worktree when done.\n```\n\n### 4. Auto-diff (surface what changed)\n```\ncd frontend && node <skill>/diff-shots.mjs \\\n  screenshots/ui-diff/before screenshots/ui-diff/after screenshots/ui-diff\n```\nProduces `diff-report.json` classifying each view `unchanged | changed | added |\nremoved`. For each changed view it computes the bounding box of differing pixels and\nwrites cropped `__before_crop.png` / `__after_crop.png` / `__diff.png` to that region\n(+ padding) - **unless** the change covers more than `--pagewide` of the frame, where\nit keeps the full frame (`pageWide:true`). Drop `unchanged` - that's the noise the\nuser doesn't want.\n\n### 5. Montage the changes\nBuild the manifest from the non-unchanged entries (group by tab/tool; each becomes a\nstate row with before/after). For changed views use the cropped `cropBefore` /\n`cropAfter` from `diff-report.json` (tight on the affected region; full frame when\n`pageWide`); `added`/`removed` render the \"not present\" placeholder. Fill\n[montage-template.html](montage-template.html) (replace the `window.__BA__` data\nblock; base64-inline the PNGs for portability), then render one PNG per section with\n[shoot-sections.mjs](shoot-sections.mjs). Optionally include the `__diff.png` overlay\nas a third column.\n\n### 6. Deliver\nOutput the `montage_<tab>.png` files + a short summary (N changed / added / removed,\nM unchanged skipped) and a paste-ready Markdown block. GitHub has no PR-body image\nAPI, so tell the user to drag the PNGs into the description. Do **not** post to the\nPR.\n\n## Gotchas\n- Two installs (base worktree + head); junction main's node_modules only if its deps\n  match that ref, else `npm ci` (see ui-walkthrough's stale-dep note).\n- A view that errors on one side (refactored/removed) → that side is missing; the\n  diff marks it added/removed rather than failing the run.\n- Pixel diff needs equal dimensions, so capture at a fixed viewport (the template\n  does); a view whose size changed is reported as \"changed (dimensions differ)\",\n  uncropped.\n- Auto-crop uses a single bounding box, so two far-apart changes give one large crop\n  (or trip `--pagewide`); narrow with `--scope` if that happens.\n- `getToolUrlPath` is the source of truth for tool URLs - use it, don't guess slugs.\n- Don't commit `screenshots/`, the throwaway spec, or the base worktree.\n\n<!-- File: .claude/skills/ui-walkthrough/SKILL.md (Tokens: ~1676 | Category: anthropic-skill) -->\n---\nname: ui-walkthrough\ndescription: >-\n  Full UI investigation of the current branch's feature. Enumerates every view\n  and state (empty, populated, loading, error, each dialog/menu/panel, responsive\n  breakpoints, light + dark + RTL), captures them with the stubbed Playwright\n  harness, assembles a single-image HTML walkthrough with a global light/dark\n  toggle slider, then runs two review passes: visual/consistency (alignment,\n  spacing, professionalism, dark/light parity, contrast, truncation) and\n  UX/ease-of-use (flow, discoverability, affordances, empty/error states,\n  expectations). Use when asked for a UI walkthrough, screenshot review, design\n  or QA pass, \"find anywhere to make it easier/better for users\", or before\n  merging frontend work. Pass --fix to auto-apply safe frontend fixes and\n  re-capture; --theme to limit themes; --no-rtl to skip RTL.\nargument-hint: \"[feature/area] [--fix] [--theme light|dark|both] [--no-rtl] [--breakpoints]\"\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash\n---\n\n# UI Walkthrough\n\nProduce a reviewable HTML walkthrough of a feature's UI in every state and theme,\nthen critique it. Optionally auto-fix and re-capture.\n\n`$ARGUMENTS` may name the feature/area to focus on. If empty, scope from the\ncurrent branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),\n`--no-rtl`, `--breakpoints` (also capture phone/narrow widths).\n\n## What this repo gives you (use it, don't reinvent)\n\n- **Stubbed Playwright project** = backend-free screenshots via `page.route()` mocks.\n  Reference implementation: `frontend/src/editor/core/tests/stubbed/files-page-screenshots.spec.ts`.\n  It already shows the light / **dark** / **RTL** passes, JWT seeding, IndexedDB\n  seeding, and dumping PNGs to a `screenshots/<area>/` folder. Copy its shape.\n- Helpers: `frontend/src/editor/core/tests/helpers/ui-helpers.ts`\n  (`uploadFiles`, `openSettings`, `waitForModalOpen`, `dismissTourTooltip`, …)\n  and the `stub-test-base` fixtures (`autoGoto`, `seedJwt`, `viewport`).\n- Config: `frontend/playwright.config.ts` (run from `frontend/`).\n- Report template: [report-template.html](report-template.html) - self-contained,\n  one big image at a time, a global light/dark slider that flips every shot,\n  thumbnail rail, prev/next + arrow keys, and a Findings tab.\n\n## Process\n\n### 1. Scope the feature\n- If `$ARGUMENTS` is empty: `git diff --name-only main...HEAD` and read the PR/commits.\n  Identify changed pages, tools (`core/components/tools/<tool>` or `core/tools/<tool>`),\n  dialogs, panels, and routes.\n- Enumerate **every view and state** to capture, e.g.:\n  empty / populated / loading / error / disabled; each dialog, menu, popover, tooltip;\n  each tab or step; selection + multi-select; success/result panel; and (if relevant)\n  permission/role variants. Write the list down before capturing - it's the report's spine.\n\n### 2. Prepare the harness (worktree-safe)\nWorktrees have no `node_modules`. From repo root:\n```\ncd frontend && npm ci                       # or junction main's node_modules (see memory)\n```\nKill any stale dev server first (it serves old modules):\n`Get-NetTCPConnection -LocalPort 5173 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`\n\n### 3. Write the capture spec\nCreate `frontend/src/editor/core/tests/stubbed/<feature>-walkthrough.spec.ts`,\nmodeled on `files-page-screenshots.spec.ts`. For each enumerated view:\n- stub the APIs it needs, drive the UI to that state, wait on a real locator\n  (not a fixed sleep), `await settle(page)` for Mantine portals, then\n  `page.screenshot({ path: shotPath(\"NN_name_<theme>\") })`.\n- Capture each view in **light and dark** (and RTL unless `--no-rtl`). Reuse the\n  `enableDarkMode` / `enableRtl` init-script pattern from the reference spec\n  (`localStorage[\"mantine-color-scheme\"]=\"dark\"` + `emulateMedia({colorScheme:\"dark\"})`).\n- Name shots `NN_<view>_<theme>.png` so light/dark pair up by suffix.\n- Prefer **stable test-ids** over translated accessible names (RTL/i18n breaks text locators).\n\nRun it: `cd frontend && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.\nAdd `--project=stubbed-firefox`/`-webkit` only if cross-browser layout matters.\n\n### 4. Build the report\n- Copy `report-template.html` to `screenshots/<feature>/walkthrough.html` (so the\n  relative `screenshots/...` image paths resolve, or rewrite paths to sit beside it).\n- Build the manifest and inject it: replace the JSON between the\n  `/*__DATA__*/` … `/*__END__*/` markers with one `views[]` entry per view\n  (`{id,title,light,dark,viewport,notes}`) and an empty `findings` object you'll\n  fill in step 5. Keep `light`/`dark` as relative paths.\n- The toggle slider answers the \"one big image + flip light/dark for all\" request:\n  it shows a single large screenshot, and switching the slider re-themes every view.\n\n### 5. Review pass 1 - visual & consistency\nOpen each screenshot (Read the PNG) and judge against the others:\nalignment & spacing rhythm, control placement, button hierarchy, typography,\n**light/dark parity** (contrast, invisible borders, washed-out text, wrong tokens),\ntruncation/overflow, RTL mirroring, focus states, icon consistency, professional polish.\nRecord each issue as a finding `{severity:high|med|low, view, title, detail, fix}`.\n\n### 6. Review pass 2 - UX & ease of use\nWalk the flow as a first-time user: discoverability, number of steps, affordance\nclarity, empty-state guidance, error recovery, destructive-action confirmation,\ndefaults, loading feedback, mobile reachability, accessible names, and whether the\nUI matches user expectations for this kind of tool. Record findings the same way.\n\nWrite both finding lists into the report's `findings.visual` / `findings.ux`,\nand add short per-view `notes`. Re-inject the manifest.\n\n### 7. If `--fix`\nOnly safe, self-contained frontend fixes (spacing, alignment, tokens, missing\ndark-mode colors, labels, aria, obvious copy). For each: edit the component/CSS,\nmark the finding `fixed:true` with what changed, then **re-run the spec** to\nre-capture the affected shots and regenerate the report. Run `task frontend:check`.\nLeave anything risky or ambiguous as a finding, not a change.\n\n### 8. Deliver\nTell the user the report path and give a tight chat summary: N views ×\nthemes captured, top findings by severity, and (if `--fix`) what changed.\nOptionally `SendUserFile` the `walkthrough.html`.\n\n## Gotchas\n- Stale `:5173` server serves old bundles - kill it before capturing (see step 2).\n- `await settle(page)` before shots or portals/transitions tear mid-capture.\n- Don't commit the generated `screenshots/` or the throwaway spec unless asked.\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/core.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/core.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/core.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/languages.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Rules Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/languages.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/languages.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ar.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/az.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bg.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/bo.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ca.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/cs.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/da.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/de.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/el.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/en.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/es.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/eu.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fa.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/fr.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ga.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hi.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hr.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/hu.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/id.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/it.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ja.json)\n\n<!-- File: frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Packs Skill\n\nPath: `frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/HEAD/frontend/src/editor/proprietary/services/heuristic/rules/packs/ko.json)\n\n<!-- ... +27 more modular skills available via /api/skills/Stirling-Tools/Stirling-PDF?path={path} -->\n</yakaai_skills>"}