@ketecode/loom 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +111 -11
  2. package/agents/code-reviewer/evals/behavior.json +60 -0
  3. package/agents/inbox-triager/AGENT.md +98 -0
  4. package/agents/inbox-triager/evals/triggers.json +16 -0
  5. package/agents/spec-reviewer/AGENT.md +95 -0
  6. package/agents/spec-reviewer/evals/behavior.json +57 -0
  7. package/agents/spec-reviewer/evals/triggers.json +16 -0
  8. package/agents/task-implementer/AGENT.md +89 -0
  9. package/agents/task-implementer/evals/behavior.json +47 -0
  10. package/agents/task-implementer/evals/triggers.json +16 -0
  11. package/bin/kete-loom.js +46 -6
  12. package/commands/brainstorm.md +25 -0
  13. package/commands/debug.md +27 -0
  14. package/commands/dual-review.md +34 -0
  15. package/commands/feature-dev.md +8 -7
  16. package/commands/finish-branch.md +26 -0
  17. package/commands/fix-bug.md +38 -0
  18. package/commands/production-readiness.md +45 -0
  19. package/commands/usage-report.md +43 -0
  20. package/hooks/block-foreground-servers/HOOK.md +34 -0
  21. package/hooks/block-foreground-servers/cases.json +31 -0
  22. package/hooks/block-foreground-servers/hook.js +41 -0
  23. package/hooks/capture-lessons/HOOK.md +24 -0
  24. package/hooks/capture-lessons/hook.js +48 -0
  25. package/hooks/notify-on-stop/HOOK.md +25 -0
  26. package/hooks/notify-on-stop/hook.js +27 -0
  27. package/hooks/protect-lint-config/HOOK.md +29 -0
  28. package/hooks/protect-lint-config/cases.json +18 -0
  29. package/hooks/protect-lint-config/fixtures/.eslintrc.json +1 -0
  30. package/hooks/protect-lint-config/fixtures/.golangci.yml +1 -0
  31. package/hooks/protect-lint-config/fixtures/.prettierrc +1 -0
  32. package/hooks/protect-lint-config/fixtures/.rubocop.yml +1 -0
  33. package/hooks/protect-lint-config/fixtures/biome.json +1 -0
  34. package/hooks/protect-lint-config/fixtures/eslint.config.mjs +1 -0
  35. package/hooks/protect-lint-config/fixtures/ruff.toml +1 -0
  36. package/hooks/protect-lint-config/hook.js +25 -0
  37. package/hooks/read-before-edit/HOOK.md +26 -0
  38. package/hooks/read-before-edit/cases.json +15 -0
  39. package/hooks/read-before-edit/hook.js +50 -0
  40. package/hooks/run.js +22 -6
  41. package/hooks/verify-before-stop/HOOK.md +32 -0
  42. package/hooks/verify-before-stop/hook.js +70 -0
  43. package/mcp/clickhouse.json +11 -0
  44. package/mcp/exa.json +11 -0
  45. package/mcp/firecrawl.json +11 -0
  46. package/mcp/vercel.json +11 -0
  47. package/package.json +6 -4
  48. package/profiles.json +54 -5
  49. package/rules/use-skills.md +23 -0
  50. package/scripts/lib/behavior.js +52 -0
  51. package/scripts/lib/hooks-install.js +9 -7
  52. package/scripts/lib/lint.js +24 -2
  53. package/scripts/lib/mcp-install.js +5 -1
  54. package/scripts/lib/render.js +6 -0
  55. package/scripts/lib/skills.js +1 -0
  56. package/scripts/lib/targets.js +10 -0
  57. package/skills/agent-failure-debugging/SKILL.md +92 -0
  58. package/skills/agent-failure-debugging/evals/triggers.json +16 -0
  59. package/skills/agent-outbound-messaging/SKILL.md +92 -0
  60. package/skills/agent-outbound-messaging/evals/triggers.json +16 -0
  61. package/skills/agent-security/SKILL.md +117 -0
  62. package/skills/agent-security/evals/triggers.json +16 -0
  63. package/skills/agent-security/references/host-configs.md +37 -0
  64. package/skills/api-client-integration/SKILL.md +86 -0
  65. package/skills/api-client-integration/evals/triggers.json +16 -0
  66. package/skills/api-client-integration/references/resilience.md +57 -0
  67. package/skills/blender-automation/SKILL.md +100 -0
  68. package/skills/blender-automation/evals/triggers.json +16 -0
  69. package/skills/blender-automation/references/batch-and-render.md +77 -0
  70. package/skills/blender-automation/references/bpy-inspection.md +91 -0
  71. package/skills/blender-automation/references/rig-and-retarget-checks.md +23 -0
  72. package/skills/brand-strategy/SKILL.md +83 -0
  73. package/skills/brand-strategy/evals/triggers.json +16 -0
  74. package/skills/change-impact-analysis/SKILL.md +89 -0
  75. package/skills/change-impact-analysis/evals/triggers.json +16 -0
  76. package/skills/change-impact-analysis/references/graph-commands.md +28 -0
  77. package/skills/contract-drafting/SKILL.md +96 -0
  78. package/skills/contract-drafting/evals/triggers.json +16 -0
  79. package/skills/contract-drafting/references/term-sheets.md +68 -0
  80. package/skills/contract-first-development/SKILL.md +86 -0
  81. package/skills/contract-first-development/evals/triggers.json +16 -0
  82. package/skills/contract-first-development/references/tooling.md +46 -0
  83. package/skills/decision-council/SKILL.md +98 -0
  84. package/skills/decision-council/evals/triggers.json +16 -0
  85. package/skills/design-brainstorming/SKILL.md +132 -0
  86. package/skills/design-brainstorming/evals/behavior.json +55 -0
  87. package/skills/design-brainstorming/evals/triggers.json +16 -0
  88. package/skills/design-brainstorming/references/visual-mockups.md +34 -0
  89. package/skills/desktop-app-testing/SKILL.md +104 -0
  90. package/skills/desktop-app-testing/evals/triggers.json +16 -0
  91. package/skills/desktop-app-testing/references/drivers.md +81 -0
  92. package/skills/document-processing/SKILL.md +100 -0
  93. package/skills/document-processing/evals/triggers.json +17 -0
  94. package/skills/document-processing/references/extraction-and-ocr.md +71 -0
  95. package/skills/document-processing/references/forms-and-conversion.md +69 -0
  96. package/skills/document-processing/references/redaction.md +61 -0
  97. package/skills/energy-procurement/SKILL.md +106 -0
  98. package/skills/energy-procurement/evals/triggers.json +16 -0
  99. package/skills/finishing-a-branch/SKILL.md +127 -0
  100. package/skills/finishing-a-branch/evals/behavior.json +49 -0
  101. package/skills/finishing-a-branch/evals/triggers.json +16 -0
  102. package/skills/freight-carrier-management/SKILL.md +107 -0
  103. package/skills/freight-carrier-management/evals/triggers.json +16 -0
  104. package/skills/git-worktrees/SKILL.md +130 -0
  105. package/skills/git-worktrees/evals/behavior.json +56 -0
  106. package/skills/git-worktrees/evals/triggers.json +16 -0
  107. package/skills/google-workspace-integration/SKILL.md +65 -0
  108. package/skills/google-workspace-integration/evals/triggers.json +17 -0
  109. package/skills/google-workspace-integration/references/api.md +121 -0
  110. package/skills/harmonyos-arkts/SKILL.md +117 -0
  111. package/skills/harmonyos-arkts/evals/triggers.json +16 -0
  112. package/skills/harmonyos-arkts/references/build-errors.md +42 -0
  113. package/skills/implementation-plans/SKILL.md +117 -0
  114. package/skills/implementation-plans/evals/behavior.json +54 -0
  115. package/skills/implementation-plans/evals/triggers.json +16 -0
  116. package/skills/ios-platform-features/SKILL.md +99 -0
  117. package/skills/ios-platform-features/evals/triggers.json +16 -0
  118. package/skills/ios-platform-features/references/foundation-models.md +90 -0
  119. package/skills/jvm-sql-mappers/SKILL.md +98 -0
  120. package/skills/jvm-sql-mappers/evals/triggers.json +16 -0
  121. package/skills/jvm-sql-mappers/references/examples.md +129 -0
  122. package/skills/mobile-accessibility/SKILL.md +94 -0
  123. package/skills/mobile-accessibility/evals/triggers.json +16 -0
  124. package/skills/network-design/SKILL.md +98 -0
  125. package/skills/network-design/evals/triggers.json +16 -0
  126. package/skills/observability/SKILL.md +125 -0
  127. package/skills/observability/evals/triggers.json +17 -0
  128. package/skills/observability/references/otel-setup.md +87 -0
  129. package/skills/observability/references/slo-alerts.md +70 -0
  130. package/skills/open-source-release/SKILL.md +107 -0
  131. package/skills/open-source-release/evals/triggers.json +15 -0
  132. package/skills/open-source-release/references/history-scrub.md +65 -0
  133. package/skills/patent-research/SKILL.md +94 -0
  134. package/skills/patent-research/evals/triggers.json +16 -0
  135. package/skills/patent-research/references/databases-and-queries.md +52 -0
  136. package/skills/prospect-research/SKILL.md +87 -0
  137. package/skills/prospect-research/evals/triggers.json +16 -0
  138. package/skills/python-app-distribution/SKILL.md +107 -0
  139. package/skills/python-app-distribution/evals/triggers.json +16 -0
  140. package/skills/python-app-distribution/references/pypi-trusted-publishing.md +55 -0
  141. package/skills/python-app-distribution/references/windows-macos-signing.md +70 -0
  142. package/skills/quality-nonconformance/SKILL.md +107 -0
  143. package/skills/quality-nonconformance/evals/triggers.json +16 -0
  144. package/skills/quality-nonconformance/references/8d-and-capa.md +46 -0
  145. package/skills/react-testing/SKILL.md +121 -0
  146. package/skills/react-testing/evals/triggers.json +18 -0
  147. package/skills/react-testing/references/msw.md +64 -0
  148. package/skills/react-testing/references/queries.md +41 -0
  149. package/skills/react-testing/references/setup.md +74 -0
  150. package/skills/recommender-systems/SKILL.md +119 -0
  151. package/skills/recommender-systems/evals/triggers.json +16 -0
  152. package/skills/recommender-systems/references/evaluation.md +39 -0
  153. package/skills/refactoring-safely/evals/behavior.json +57 -0
  154. package/skills/reproducible-dev-environments/SKILL.md +132 -0
  155. package/skills/reproducible-dev-environments/evals/triggers.json +16 -0
  156. package/skills/responding-to-code-review/SKILL.md +124 -0
  157. package/skills/responding-to-code-review/evals/behavior.json +54 -0
  158. package/skills/responding-to-code-review/evals/triggers.json +16 -0
  159. package/skills/security-review/evals/behavior.json +56 -0
  160. package/skills/session-handoff/evals/behavior.json +55 -0
  161. package/skills/skill-authoring/SKILL.md +13 -2
  162. package/skills/skill-authoring/evals/triggers.json +2 -1
  163. package/skills/skill-authoring/references/pressure-testing.md +87 -0
  164. package/skills/slide-decks/SKILL.md +107 -0
  165. package/skills/slide-decks/evals/triggers.json +18 -0
  166. package/skills/slide-decks/references/html-decks.md +86 -0
  167. package/skills/slide-decks/references/layouts.md +45 -0
  168. package/skills/slide-decks/references/pptx.md +87 -0
  169. package/skills/subagent-driven-development/SKILL.md +109 -0
  170. package/skills/subagent-driven-development/evals/behavior.json +53 -0
  171. package/skills/subagent-driven-development/evals/triggers.json +15 -0
  172. package/skills/subagent-driven-development/references/implementer-brief.md +60 -0
  173. package/skills/subagent-driven-development/references/reviewer-briefs.md +52 -0
  174. package/skills/systematic-debugging/SKILL.md +92 -0
  175. package/skills/systematic-debugging/evals/behavior.json +59 -0
  176. package/skills/systematic-debugging/evals/triggers.json +16 -0
  177. package/skills/systematic-debugging/references/condition-waiting.md +52 -0
  178. package/skills/systematic-debugging/references/root-cause-tracing.md +49 -0
  179. package/skills/systematic-debugging/references/test-pollution.md +46 -0
  180. package/skills/tdd-workflow/SKILL.md +3 -1
  181. package/skills/tdd-workflow/evals/behavior.json +55 -0
  182. package/skills/tdd-workflow/evals/triggers.json +2 -1
  183. package/skills/tdd-workflow/references/testing-anti-patterns.md +87 -0
  184. package/skills/transactional-email/SKILL.md +68 -0
  185. package/skills/transactional-email/evals/triggers.json +17 -0
  186. package/skills/transactional-email/references/providers.md +63 -0
  187. package/skills/ui-demo-recording/SKILL.md +93 -0
  188. package/skills/ui-demo-recording/evals/triggers.json +16 -0
  189. package/skills/ui-demo-recording/references/cursor-and-captions.md +48 -0
  190. package/skills/ui-demo-recording/references/playwright-recording.md +63 -0
  191. package/skills/ui-demo-recording/references/post-processing.md +57 -0
  192. package/skills/ui-design-polish/SKILL.md +113 -0
  193. package/skills/ui-design-polish/evals/triggers.json +18 -0
  194. package/skills/ui-design-polish/references/detail-checklist.md +45 -0
  195. package/skills/ui-design-polish/references/tokens.md +68 -0
  196. package/skills/ui-flow-audit/SKILL.md +112 -0
  197. package/skills/ui-flow-audit/evals/triggers.json +17 -0
  198. package/skills/ui-motion/SKILL.md +103 -0
  199. package/skills/ui-motion/evals/triggers.json +18 -0
  200. package/skills/ui-motion/references/patterns.md +138 -0
  201. package/skills/ui-motion/references/tokens.md +52 -0
  202. package/skills/unicode-text-processing/SKILL.md +103 -0
  203. package/skills/unicode-text-processing/evals/triggers.json +17 -0
  204. package/skills/unicode-text-processing/references/apis.md +89 -0
  205. package/skills/unicode-text-processing/references/script-notes.md +50 -0
  206. package/skills/verification-loop/evals/behavior.json +52 -0
  207. package/skills/video-editing/SKILL.md +111 -0
  208. package/skills/video-editing/evals/triggers.json +16 -0
  209. package/skills/video-editing/references/edl-and-logging.md +44 -0
  210. package/skills/video-editing/references/ffmpeg-edit-recipes.md +92 -0
  211. package/skills/video-editing/references/resolve-scripting.md +53 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "should_trigger": [
3
+ "I want to add some kind of team sharing to our notes app but I haven't worked out how it should behave. Can we think it through together first?",
4
+ "Let's brainstorm the design for a recurring-invoices feature before anyone writes code",
5
+ "I have a rough idea for an offline mode in our field-inspection app, help me shape it into a design we agree on",
6
+ "Before we build the new notification preferences, ask me what you need to know and propose a couple of approaches",
7
+ "Help me figure out what our internal deploy dashboard should actually do, then write the design down in the repo"
8
+ ],
9
+ "should_not_trigger": [
10
+ "Write a PRD for the recurring invoices feature with goals, metrics and rollout for the leadership review",
11
+ "Here's the approved design doc for offline mode, break it into bite-sized implementation tasks",
12
+ "Should our notifications live in a separate service or stay in the monolith? Weigh the options",
13
+ "Record our decision to use Postgres row-level security as an architecture decision record",
14
+ "Implement the team sharing endpoints from the plan in docs/plans, test first"
15
+ ]
16
+ }
@@ -0,0 +1,34 @@
1
+ # Visual mockups during design
2
+
3
+ Use these when the user has to choose between layouts, flows or visual treatments, and words would leave them guessing. A mockup is there to help them decide, not to be the product: throw it away once the design doc records the choice.
4
+
5
+ ## When to offer one
6
+
7
+ - The question is about a screen, a flow across screens, a chart or dashboard, or the density and hierarchy of a page.
8
+ - Two approaches differ mainly in how they look or feel.
9
+ - The user says "show me", or keeps asking what something would look like.
10
+
11
+ Don't offer one for back-end, data or API decisions; a short table or sequence of steps is clearer there. Ask before making mockups: "Want me to sketch A and B as HTML pages you can open side by side?"
12
+
13
+ ## How to make them
14
+
15
+ 1. Put them outside the source tree, in a folder that's git-ignored, such as `.design/mockups/<topic>/`. Check with `git check-ignore -q .design/` first. If it isn't ignored, ask before adding it to `.gitignore`, or use the system temp folder.
16
+ 2. Write **one self-contained HTML file per option**: inline CSS, no build step, no framework, no external scripts, fonts or images. Name the files `a-<short-name>.html`, `b-<short-name>.html`.
17
+ 3. Use realistic but obviously fake content: plausible names and numbers that are not real people or data. Show the awkward cases too: a long name, an empty list, an error state.
18
+ 4. Match only what the decision needs. Use grey boxes for anything that isn't being decided, and the project's real colours and type only if the choice depends on them.
19
+ 5. Add a short header on each page: the option letter, a one-line summary, and what to look at ("compare how the filters sit relative to the table").
20
+ 6. When there are more than two options, or one flow across several screens, add an `index.html` that links them, or shows them side by side in iframes.
21
+ 7. Tell the user the paths and how to open them (`open` on macOS, `xdg-open` on Linux, `start` on Windows, or "open this file in your browser"). Opening a browser yourself is fine when the user asked for it.
22
+
23
+ ## Taking feedback
24
+
25
+ - Ask one question per round: "Which layout, A or B?" then "What would you change about B?"
26
+ - Edit the mockup the user picked, and keep the earlier versions (`b-v2.html`) so you can go back.
27
+ - Once a section of the design is agreed, record the decision in words, with a screenshot if the user wants one. Never put the mockup HTML into the design doc or the source tree.
28
+ - When the design doc is written, offer to delete the mockup folder.
29
+
30
+ ## Never
31
+
32
+ - Wire mockups to real APIs, real customer data or production credentials.
33
+ - Turn a mockup into production code by copying it into the app. Build the real thing from the design doc, test-first.
34
+ - Run a local server for mockups unless the user asks. Static files are enough, and leave nothing running.
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: desktop-app-testing
3
+ description: Automates end-to-end UI tests for native desktop applications (WPF, WinForms, Win32, Qt and Electron) by driving them through UI Automation with pywinauto, through the Appium Windows driver backed by WinAppDriver, or through Playwright's Electron support, with stable automation IDs instead of screen coordinates, isolated app data per test, screenshots on failure, and CI runners that have a real interactive desktop session or a virtual display. Use when writing UI tests for a Windows or cross-platform desktop app, a desktop test passes locally but cannot find windows in CI, choosing between pywinauto, Appium and Playwright for a desktop app, or replacing coordinate-click scripts that break on every resolution change.
4
+ license: MIT
5
+ compatibility: Any Agent Skills host. Windows targets need a Windows machine or runner with an unlocked interactive session; Electron tests run on Windows, macOS or Linux (Linux needs a display, for example Xvfb).
6
+ metadata:
7
+ kete-loom/version: "0.1.0"
8
+ kete-loom/category: testing
9
+ kete-loom/risk: runs-commands
10
+ kete-loom/owner: "Kete-org"
11
+ ---
12
+
13
+ # Desktop app testing
14
+
15
+ Write desktop UI tests that find controls by identity, start from a known state, and give a screenshot and log when they fail, in CI as well as on a developer machine.
16
+
17
+ Browser apps belong to `e2e-testing-playwright`. Unit and component tests belong to the language's testing skill (`csharp-testing`, `python-testing`, `cpp-testing`). This skill is for driving the packaged, running desktop app.
18
+
19
+ ## 0. Before you start
20
+
21
+ 1. Identify the UI framework and target OS: WPF, WinForms, Win32/MFC, WinUI/UWP, Qt (Widgets or QML), Electron, or other.
22
+ 2. Find how the app is built and launched (path to the built `.exe`, command-line flags, config or data directories).
23
+ 3. Check for existing UI tests and their tool. Extend that tool unless it is unworkable; tell the user before switching.
24
+ 4. Inspect the UI tree with Accessibility Insights for Windows or `Inspect.exe` (Windows SDK) to see which controls expose `AutomationId`, `Name` and `ControlType`. For Electron, use DevTools.
25
+
26
+ ## 1. Pick the driver
27
+
28
+ | App | Tool | Why |
29
+ |---|---|---|
30
+ | Electron | Playwright `_electron.launch` | Same API as web tests, access to the main process |
31
+ | WPF, WinForms, WinUI, Win32, Qt on Windows, test code in Python | pywinauto with `backend="uia"` | No server process, direct UI Automation |
32
+ | Same apps, team wants WebDriver or non-Python language | Appium with the Windows driver (`appium driver install windows`) | WebDriver protocol, any client language; needs WinAppDriver and Windows Developer Mode |
33
+ | Old Win32/MFC controls with poor UIA support | pywinauto `backend="win32"` | Reads native Win32 controls |
34
+ | Qt on Linux or macOS | the platform accessibility layer, or Squish (commercial) if the team owns it; **stop and ask** | Few free options; confirm the choice |
35
+ | macOS native (AppKit/SwiftUI) | XCUITest, or Appium Mac2 driver | Out of scope here beyond this pointer |
36
+
37
+ WinAppDriver has seen no active development for years; it still works for many apps but check its status and the Appium Windows driver's current requirements before committing a new suite to it. Prefer pywinauto for new Python suites.
38
+
39
+ ## 2. Make the app testable
40
+
41
+ 1. Give every control a test needs a stable automation identifier:
42
+ - WPF: `AutomationProperties.AutomationId="SaveButton"` (or `x:Name`, which UIA exposes).
43
+ - WinForms: the control's `Name` property becomes the `AutomationId`.
44
+ - Qt: `setObjectName("saveButton")` and accessible names; check what UIA shows.
45
+ - Electron: `data-testid` or accessible roles and names in the renderer.
46
+ 2. Add a launch flag or environment variable for tests: a separate data directory, disabled auto-update, telemetry off, animations off, a fixed locale. Never point tests at the user's real profile.
47
+ 3. Where a test needs data, seed it through files or an API before launch, not by clicking through setup screens.
48
+
49
+ Changing app code to add IDs is in scope; ask before changing behaviour.
50
+
51
+ ## 3. Write the tests
52
+
53
+ ### Locators
54
+
55
+ - Order of preference: automation ID, then control type plus accessible name, then a path inside a uniquely identified parent. Never screen coordinates, image matching or tab-order key presses as the main strategy.
56
+ - Scope searches to the app's top window, not the desktop root; desktop-wide searches are slow and pick up other apps.
57
+
58
+ ### Waiting
59
+
60
+ - Wait for a condition (window exists, control enabled, text equals) with a timeout. pywinauto: `.wait("visible enabled", timeout=10)`; Appium: explicit WebDriver waits; Playwright: web-first `expect` assertions.
61
+ - Never use fixed sleeps except as a last resort with a comment explaining what is missing.
62
+
63
+ ### Structure
64
+
65
+ - One fresh app instance per test or per test class, launched with a clean temp data directory; close it in teardown even when the test fails, and kill leftover processes by PID.
66
+ - Page-object style: one class per window or dialog wrapping its locators and actions.
67
+ - Assert on visible outcomes (a list row appears, a file is written, a status label changes), not on internal state.
68
+ - On failure, save a screenshot of the app window and the app's log file as test artifacts.
69
+
70
+ Code templates for each tool are in `references/drivers.md`.
71
+
72
+ ## 4. CI
73
+
74
+ 1. Windows UI automation needs an **interactive, unlocked desktop session**. A runner installed as a Windows service, a locked screen or a disconnected RDP session makes window searches fail or hang.
75
+ - GitHub-hosted Windows runners: try them first; check that windows are found and screenshots are not black.
76
+ - Self-hosted: run the runner as a normal user with auto-logon and the screensaver and lock disabled, not as a service. Keep the screen resolution fixed.
77
+ 2. Linux (Electron, Qt): run under a virtual display, `xvfb-run -a <test command>`.
78
+ 3. Install the app from the same build artifact the release uses, not a debug build, at least for the smoke subset.
79
+ 4. Run UI tests serially on one desktop; parallel instances fight over focus. Shard across machines instead.
80
+ 5. Upload screenshots, app logs and test reports on failure.
81
+
82
+ ## 5. Flakiness
83
+
84
+ When a test is flaky, find the cause before adding retries: a missing wait, focus stolen by another window (update prompts, notifications), shared state between tests, or animations. One CI retry is acceptable while the cause is investigated; report each flaky test to the user.
85
+
86
+ ## Never
87
+
88
+ - Never automate the user's real profile, real accounts or production back ends.
89
+ - Never rely on screen coordinates, pixel matching or fixed sleeps as the main technique.
90
+ - Never disable OS security features (UAC, Defender) on shared machines to make tests pass; use a dedicated test VM.
91
+ - Never leave app processes running after the suite.
92
+
93
+ ## Report back
94
+
95
+ ```
96
+ ## Desktop UI tests: <app>
97
+ Framework / OS: <WPF on Windows 11, ...>
98
+ Tool: <pywinauto uia | Appium Windows | Playwright Electron> and why
99
+ Tests added: <names, what each covers>
100
+ App changes: <automation IDs or test flags added>
101
+ CI: <runner type, session setup, artifacts>
102
+ Result: <command, pass/fail counts, runs repeated for flakiness>
103
+ Gaps: <controls without IDs, untestable areas>
104
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "should_trigger": [
3
+ "Write UI tests for our WPF invoicing app that click through creating a customer",
4
+ "Our pywinauto tests pass locally but can't find the main window on the CI Windows runner",
5
+ "Should we use pywinauto or Appium with WinAppDriver to test our WinForms client?",
6
+ "Add end-to-end tests for our Electron app that launch it and check the main window",
7
+ "Replace our AutoIt scripts that click screen coordinates with something that survives a resolution change"
8
+ ],
9
+ "should_not_trigger": [
10
+ "Write Playwright tests for the login page of our React web app",
11
+ "Add xUnit unit tests for the tax calculation class in our C# library",
12
+ "Package our Python Qt app into a signed Windows installer",
13
+ "Check our web app's forms for screen reader and keyboard accessibility",
14
+ "Test our iOS app's onboarding flow with XCUITest on a simulator"
15
+ ]
16
+ }
@@ -0,0 +1,81 @@
1
+ # Desktop driver templates
2
+
3
+ Read this when writing the first test with a tool. Check the tool's current documentation for version-specific capability names.
4
+
5
+ ## pywinauto (Python, UI Automation backend)
6
+
7
+ ```python
8
+ import subprocess, tempfile, pytest
9
+ from pywinauto import Application
10
+
11
+ APP = r"C:\build\MyApp\MyApp.exe"
12
+
13
+ @pytest.fixture
14
+ def app():
15
+ data_dir = tempfile.mkdtemp()
16
+ application = Application(backend="uia").start(f'"{APP}" --data-dir "{data_dir}" --no-update')
17
+ main = application.window(title_re="MyApp.*")
18
+ main.wait("visible ready", timeout=20)
19
+ yield application, main
20
+ application.kill()
21
+
22
+ def test_save_creates_record(app, request):
23
+ _, main = app
24
+ try:
25
+ main.child_window(auto_id="NameBox", control_type="Edit").set_edit_text("Ama")
26
+ main.child_window(auto_id="SaveButton", control_type="Button").click_input()
27
+ row = main.child_window(title="Ama", control_type="DataItem")
28
+ row.wait("visible", timeout=10)
29
+ except Exception:
30
+ main.capture_as_image().save(f"{request.node.name}.png")
31
+ raise
32
+ ```
33
+
34
+ Use `print_control_identifiers()` on a window once to explore its tree; do not leave it in tests. `click_input()` moves the real mouse (needs focus); `invoke()` uses the UIA Invoke pattern and is often steadier.
35
+
36
+ ## Appium Windows driver (WebDriver)
37
+
38
+ Install: `npm install -g appium`, `appium driver install windows`, install WinAppDriver, enable Windows Developer Mode, start `appium`.
39
+
40
+ ```python
41
+ from appium import webdriver
42
+ from appium.options.windows import WindowsOptions
43
+ from appium.webdriver.common.appiumby import AppiumBy
44
+
45
+ opts = WindowsOptions()
46
+ opts.app = r"C:\build\MyApp\MyApp.exe"
47
+ opts.set_capability("appium:appArguments", "--data-dir C:\\temp\\t1")
48
+ driver = webdriver.Remote("http://127.0.0.1:4723", options=opts)
49
+ try:
50
+ driver.find_element(AppiumBy.ACCESSIBILITY_ID, "NameBox").send_keys("Ama")
51
+ driver.find_element(AppiumBy.ACCESSIBILITY_ID, "SaveButton").click()
52
+ finally:
53
+ driver.quit()
54
+ ```
55
+
56
+ `ACCESSIBILITY_ID` maps to UIA `AutomationId`. Use `"appium:app": "Root"` to attach to the desktop for apps you start yourself, then find the window by name.
57
+
58
+ ## Playwright for Electron (TypeScript)
59
+
60
+ ```ts
61
+ import { test, expect, _electron as electron } from '@playwright/test';
62
+ import os from 'node:os'; import path from 'node:path'; import fs from 'node:fs';
63
+
64
+ test('save creates a record', async () => {
65
+ const userData = fs.mkdtempSync(path.join(os.tmpdir(), 'app-'));
66
+ // Pass the temp dir however the app accepts it (a flag or env var that calls app.setPath('userData', ...)).
67
+ const app = await electron.launch({ args: ['.'], env: { ...process.env, MYAPP_USER_DATA: userData } });
68
+ try {
69
+ const win = await app.firstWindow();
70
+ await win.getByLabel('Name').fill('Ama');
71
+ await win.getByRole('button', { name: 'Save' }).click();
72
+ await expect(win.getByRole('row', { name: /Ama/ })).toBeVisible();
73
+ const version = await app.evaluate(({ app }) => app.getVersion());
74
+ expect(version).toMatch(/\d+\.\d+\.\d+/);
75
+ } finally {
76
+ await app.close();
77
+ }
78
+ });
79
+ ```
80
+
81
+ For a packaged build pass `executablePath` to the built binary. The config's `use.trace` option does not cover a context you launch yourself; record a trace with `await app.context().tracing.start({ screenshots: true, snapshots: true })` after launch and `tracing.stop({ path })` in the `finally` block when the test failed.
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: document-processing
3
+ description: Pipelines for PDFs and scanned paperwork built on open tools - triage each file as born-digital, scanned or mixed, pull text and tables with pdfplumber or pdftotext, add a searchable text layer with OCRmyPDF and Tesseract language packs, split, merge, rotate, decrypt and repair with qpdf, read and fill AcroForm fields with pypdf or pdftk, redact so the underlying text and metadata are truly gone and prove it, and translate a document while keeping its layout. Use when someone has a folder of PDFs or scans to extract data from, needs a scanned contract made searchable, wants a PDF form filled from a spreadsheet, or must hand over a redacted document that has to survive copy-paste and text extraction.
4
+ license: MIT
5
+ compatibility: Any Agent Skills host. Uses Python 3 with pdfplumber and pypdf, plus the qpdf, ocrmypdf, tesseract (with the needed language data), pdftotext/pdfinfo (poppler-utils) and optionally pdftk CLIs. Redaction examples use PyMuPDF, which is AGPL-licensed.
6
+ metadata:
7
+ kete-loom/version: "0.1.0"
8
+ kete-loom/category: data
9
+ kete-loom/risk: runs-commands
10
+ kete-loom/requires: "python3, qpdf, ocrmypdf, tesseract, pdftotext"
11
+ kete-loom/owner: "Kete-org"
12
+ ---
13
+
14
+ # Document processing
15
+
16
+ Treat every document as evidence: never change the original, keep each step's output, and check results against the page images, not just the extracted text. Command and code detail is in `references/extraction-and-ocr.md`, `references/forms-and-conversion.md` and `references/redaction.md`.
17
+
18
+ ## 0. Before you start
19
+
20
+ 1. Copy inputs to `work/in/` and treat them as read-only. Outputs go to `work/out/`.
21
+ 2. Ask the user what the result is for (search, a spreadsheet, a filled form, a redacted copy for disclosure, a translation) and the languages in the documents.
22
+ 3. **Sensitive content** (IDs, health, financial, legal): confirm the user may process it and that no cloud OCR, translation or AI service will be used without their approval. Default to local tools.
23
+ 4. Do not open files from untrusted sources with tools that execute embedded content; work with command-line tools and libraries, not by opening JavaScript-enabled viewers.
24
+
25
+ ## 1. Triage each file
26
+
27
+ Run `pdfinfo file.pdf` (pages, encryption, PDF version, tagged or not) and `pdffonts file.pdf`, then sample text with `pdftotext -f 1 -l 3 -layout file.pdf -`.
28
+
29
+ | Finding | Class | Next step |
30
+ |---|---|---|
31
+ | Text comes out readable, fonts listed | Born-digital | Extract directly (step 2) |
32
+ | No text, no fonts, pages are images | Scanned | OCR first (step 3) |
33
+ | Some pages have text, others not | Mixed | OCR with skip mode for pages that already have text |
34
+ | Text is garbled (wrong glyphs, `(cid:123)`) | Broken font encoding | OCR with force mode, or redo |
35
+ | `qpdf --check` reports damage | Damaged | Repair with `qpdf in.pdf repaired.pdf`, then re-triage |
36
+ | Encrypted | Protected | Decrypt with `qpdf --decrypt` only with the password and permission from the user |
37
+
38
+ ## 2. Extract text and tables (born-digital)
39
+
40
+ 1. Plain text in reading order: `pdftotext -layout` for column-preserving text, or pdfplumber `page.extract_text()` for per-page control.
41
+ 2. Tables: pdfplumber `page.extract_tables()` with explicit `table_settings` (lines strategy for ruled tables, text strategy for whitespace tables). Crop to the table region with `page.crop(bbox)` when a page has more than one table.
42
+ 3. Keep provenance on every row: file, page number and, for tables, the bounding box.
43
+ 4. Validate: row counts per page, column totals against the printed totals, and a manual spot check of at least three rows against the page image. Report any mismatch rather than "fixing" numbers.
44
+
45
+ ## 3. OCR scans
46
+
47
+ 1. Run OCRmyPDF with the right languages (`-l eng+fra`; install the Tesseract language data first, check with `tesseract --list-langs`).
48
+ 2. For phone photos or skewed scans add `--rotate-pages --deskew --clean`. These cannot be combined with redo mode.
49
+ 3. For mixed files use skip mode (`--skip-text`, or `--mode skip` in OCRmyPDF 17+); for an existing bad text layer use `--redo-ocr` (`--mode redo`); `--force-ocr` (`--mode force`) rasterises everything and loses vector quality.
50
+ 4. Write PDF/A for archiving (`--output-type pdfa`) and a `--sidecar out.txt` for the plain text.
51
+ 5. Check quality: sample pages, compare OCR text with the image, and note low-confidence areas (handwriting, stamps, tables). OCR is never 100% accurate; say so in the report and keep the images as the source of truth.
52
+
53
+ ## 4. Convert, split, merge, rotate
54
+
55
+ Use qpdf for structural work (no re-rendering, so quality is kept): `--pages` to select, split or merge, `--rotate`, `--linearize` for web viewing, `--split-pages` for one file per page. Office formats to PDF via `libreoffice --headless --convert-to pdf` if installed. Recipes in `references/forms-and-conversion.md`.
56
+
57
+ ## 5. Fill forms
58
+
59
+ 1. List the fields first (`pypdf` `reader.get_fields()` or `pdftk in.pdf dump_data_fields_utf8`) and map them to the user's data. **Show the user the mapping** before filling.
60
+ 2. Fill with pypdf (`writer.update_page_form_field_values`) or pdftk (`fill_form data.fdf output out.pdf`), one output per record.
61
+ 3. Checkboxes and radio buttons need their export value (often `/Yes` or `/On`), not `True`.
62
+ 4. Flatten (`pdftk ... flatten`, or render) only when the user wants a non-editable copy; keep the unflattened version.
63
+ 5. XFA forms (dynamic forms from Adobe LiveCycle) are not AcroForms; most open tools cannot fill them. Stop and tell the user.
64
+ 6. Open each output and check a few fields by eye.
65
+
66
+ ## 6. Redact (must be real)
67
+
68
+ Drawing a black box over text does **not** redact it: the text stays in the content stream and is copied, searched and extracted. Follow `references/redaction.md` exactly:
69
+
70
+ 1. Find every occurrence (search the text layer and the OCR layer; patterns for IDs, emails, phone numbers; ask the user for names and terms).
71
+ 2. Apply true redaction that removes the text, vector graphics and image pixels under each area (PyMuPDF `add_redact_annot` then `apply_redactions`), or rasterise the pages and re-OCR the result.
72
+ 3. Remove metadata, XMP, attachments, annotations, form field values, bookmarks and earlier revisions (incremental updates) by rewriting the file.
73
+ 4. **Verify**: extract all text from the output and search for every redacted term; inspect metadata; check that the file has no incremental updates left. Any hit means the redaction failed.
74
+ 5. A person must review redactions before disclosure. Legal, court or regulatory redaction rules (what must be withheld, how it is marked) are outside this skill; the user's legal team decides.
75
+
76
+ ## 7. Translate with layout
77
+
78
+ 1. Extract text per block with its position, font size and page (pdfplumber words or PyMuPDF blocks).
79
+ 2. Translate block by block, with the tool or service the user approves; keep numbers, names, codes and dates unchanged and give the translator a glossary.
80
+ 3. Rebuild: either an overlay PDF that writes translated text into each block's box (shrink font to fit, flag blocks that overflow) or a bilingual table (source, translation, page) when layout matters less than accuracy.
81
+ 4. Fonts must cover the target script (for example Noto fonts for Bengali, Arabic, CJK); check that no characters render as boxes. Complex scripts need a shaping-capable text engine.
82
+ 5. Machine translation of legal, medical or official documents needs review by a qualified translator before use; certified translations are out of scope.
83
+
84
+ ## Never
85
+
86
+ - Never modify or delete the original files.
87
+ - Never present OCR or extracted numbers as exact without a spot check.
88
+ - Never send documents to an online service without the user's explicit approval.
89
+ - Never call a document redacted until the verification step passes.
90
+
91
+ ## Report back
92
+
93
+ ```
94
+ Files: n (born-digital / scanned / mixed / damaged)
95
+ Outputs: <paths> Tools and versions: ...
96
+ Extraction: rows or pages extracted, spot checks done, mismatches
97
+ OCR: languages, pages OCRed, known weak areas
98
+ Redaction: terms and patterns, hits after verification (must be 0), metadata cleared
99
+ Needs a human: legal review of redactions, translation review, unreadable pages
100
+ ```
@@ -0,0 +1,17 @@
1
+ {
2
+ "should_trigger": [
3
+ "Pull the transaction tables out of these 200 bank statement PDFs into one CSV",
4
+ "Make this folder of scanned contracts searchable with OCR, some are in French",
5
+ "Fill this PDF application form for each of the 50 rows in our spreadsheet",
6
+ "Redact every name and account number from this PDF and prove the text can't be copied back out",
7
+ "Translate this German PDF brochure into English but keep the layout of each page",
8
+ "The text I extract from this PDF comes out as (cid:34) gibberish, how do I get real text"
9
+ ],
10
+ "should_not_trigger": [
11
+ "Create a Word document report with a table of contents and page numbers",
12
+ "Translate the app's UI strings into French and German with message catalogs",
13
+ "Write the user guide for our CLI tool",
14
+ "Build a RAG pipeline that chunks and embeds our product manuals",
15
+ "Normalise the Bengali text in our search index so the same word always matches"
16
+ ]
17
+ }
@@ -0,0 +1,71 @@
1
+ # Extraction and OCR
2
+
3
+ ## Triage commands
4
+
5
+ ```bash
6
+ pdfinfo in.pdf # pages, encryption, producer, PDF version
7
+ pdffonts in.pdf # no fonts => likely scanned
8
+ pdftotext -f 1 -l 3 -layout in.pdf - | head -50
9
+ qpdf --check in.pdf # structural problems
10
+ pdfimages -list in.pdf | head # image resolution per page (OCR needs ~300 dpi)
11
+ ```
12
+
13
+ ## Text with pdfplumber
14
+
15
+ ```python
16
+ import pdfplumber, json
17
+ rows = []
18
+ with pdfplumber.open("work/in/report.pdf") as pdf:
19
+ for i, page in enumerate(pdf.pages, start=1):
20
+ text = page.extract_text(layout=False) or ""
21
+ rows.append({"page": i, "text": text})
22
+ json.dump(rows, open("work/out/report.text.json", "w"), ensure_ascii=False, indent=1)
23
+ ```
24
+
25
+ Words with positions (for layout-aware work): `page.extract_words(keep_blank_chars=False, use_text_flow=True)` returns `x0, top, x1, bottom, text`.
26
+
27
+ ## Tables with pdfplumber
28
+
29
+ ```python
30
+ ruled = {"vertical_strategy": "lines", "horizontal_strategy": "lines"}
31
+ whitespace = {"vertical_strategy": "text", "horizontal_strategy": "text", "snap_tolerance": 3}
32
+
33
+ with pdfplumber.open("work/in/statement.pdf") as pdf:
34
+ page = pdf.pages[0]
35
+ region = page.crop((0, 120, page.width, 700)) # x0, top, x1, bottom in PDF points
36
+ for table in region.extract_tables(ruled):
37
+ for row in table:
38
+ print(row)
39
+ ```
40
+
41
+ Debug a table: `page.to_image(resolution=150).debug_tablefinder(ruled).save("debug.png")` and look at the detected lines.
42
+
43
+ Tips:
44
+ - Header rows that repeat on every page: drop them after the first page by matching the header.
45
+ - Cells that wrap onto two lines become `"line1\nline2"`; join them, do not split into rows.
46
+ - Numbers: strip thousands separators according to the document's locale (1.234,56 vs 1,234.56); keep the original string next to the parsed value.
47
+
48
+ ## OCRmyPDF
49
+
50
+ ```bash
51
+ tesseract --list-langs # confirm language data is installed
52
+ ocrmypdf -l eng --rotate-pages --deskew --clean --output-type pdfa --sidecar out.txt in.pdf out.pdf
53
+ ocrmypdf -l eng+deu --skip-text in.pdf out.pdf # mixed: only OCR pages without text
54
+ ocrmypdf -l eng --redo-ocr in.pdf out.pdf # replace a bad hidden text layer
55
+ ocrmypdf -l ben --force-ocr in.pdf out.pdf # rasterise and OCR everything
56
+ ```
57
+
58
+ - OCRmyPDF 17 added `--mode skip|redo|force`; the legacy flags still work.
59
+ - `--clean` needs the `unpaper` program installed; it only cleans the image Tesseract sees, not the output page (that is `--clean-final`).
60
+ - `--redo-ocr` cannot be combined with `--deskew`, `--rotate-pages` or `--clean`.
61
+ - Tesseract language codes are ISO 639-2 style (`eng`, `fra`, `deu`, `ben`, `hin`, `ara`, `chi_sim`, `jpn`). Choose `script/` models (for example `script/Devanagari`) when the language is unknown but the script is.
62
+ - Very low-resolution scans: upscale is no substitute for a rescan at 300 dpi; tell the user.
63
+
64
+ ## Plain Tesseract on images
65
+
66
+ ```bash
67
+ tesseract page.png out -l eng --psm 6 # writes out.txt; --psm 6 = single uniform block
68
+ tesseract page.png out -l eng tsv # word-level confidence in out.tsv
69
+ ```
70
+
71
+ Use the `conf` column of the TSV to flag words under about 60 for human checking.
@@ -0,0 +1,69 @@
1
+ # Forms and structural conversion
2
+
3
+ ## qpdf
4
+
5
+ ```bash
6
+ qpdf in.pdf repaired.pdf # rewrite, fixes many damaged files
7
+ qpdf --decrypt --password-file=pw.txt locked.pdf open.pdf # only with the owner's permission; a file keeps the password out of shell history
8
+ qpdf --empty --pages a.pdf b.pdf 1-3 -- merged.pdf # all of a, pages 1-3 of b
9
+ qpdf in.pdf --pages . 5-10 -- part.pdf # extract pages
10
+ qpdf --split-pages in.pdf page-%d.pdf # one file per page
11
+ qpdf in.pdf rotated.pdf --rotate=+90:2,4 # rotate pages 2 and 4 by 90 degrees clockwise
12
+ qpdf --linearize in.pdf web.pdf # fast web view
13
+ qpdf --qdf --object-streams=disable in.pdf inspect.pdf # human-readable objects for debugging
14
+ ```
15
+
16
+ Office documents to PDF (if LibreOffice is installed):
17
+
18
+ ```bash
19
+ libreoffice --headless --convert-to pdf --outdir work/out work/in/*.docx
20
+ ```
21
+
22
+ ## List form fields
23
+
24
+ ```bash
25
+ pdftk form.pdf dump_data_fields_utf8
26
+ ```
27
+
28
+ ```python
29
+ from pypdf import PdfReader
30
+ reader = PdfReader("form.pdf")
31
+ for name, f in (reader.get_fields() or {}).items():
32
+ print(name, f.get("/FT"), f.get("/V"), f.get("/_States_", "")) # /_States_ lists checkbox values
33
+ ```
34
+
35
+ If `get_fields()` is empty but the PDF looks like a form, it is either flat (no fields) or XFA. Check with `acro = reader.trailer["/Root"].get("/AcroForm")` then `acro is not None and "/XFA" in acro.get_object()`. Report which.
36
+
37
+ ## Fill with pypdf
38
+
39
+ ```python
40
+ from pypdf import PdfReader, PdfWriter
41
+ reader = PdfReader("form.pdf")
42
+ writer = PdfWriter(clone_from=reader)
43
+ values = {"FullName": "Ama Mensah", "Date": "2026-03-02", "Agree": "/Yes"}
44
+ for page in writer.pages:
45
+ writer.update_page_form_field_values(page, values, auto_regenerate=False)
46
+ writer.set_need_appearances_writer(True) # ask viewers to regenerate appearances
47
+ with open("filled.pdf", "wb") as fh:
48
+ writer.write(fh)
49
+ ```
50
+
51
+ Check the pypdf version's docs: the signature of `update_page_form_field_values` has gained options (such as `flatten`) over releases.
52
+
53
+ ## Fill with pdftk
54
+
55
+ Write an FDF or XFDF with the values, then:
56
+
57
+ ```bash
58
+ pdftk form.pdf fill_form data.xfdf output filled.pdf # keeps fields editable
59
+ pdftk form.pdf fill_form data.xfdf output filled_flat.pdf flatten
60
+ ```
61
+
62
+ Use `need_appearances` if the viewer shows empty fields after filling.
63
+
64
+ ## Batch filling from a CSV
65
+
66
+ 1. Read the CSV with the csv module (UTF-8, keep leading zeros as strings).
67
+ 2. Map CSV columns to field names with an explicit dict the user approved.
68
+ 3. One output per row named from a key column, sanitised to a safe filename.
69
+ 4. Log each row: output path, fields filled, fields missing.
@@ -0,0 +1,61 @@
1
+ # Redaction that holds up
2
+
3
+ A redaction has failed if any redacted value can be recovered by selecting, copying, searching, extracting text, reading metadata, opening an earlier revision or lifting an overlay. Check every one.
4
+
5
+ ## 1. Find targets
6
+
7
+ ```python
8
+ import fitz, re # PyMuPDF (AGPL; confirm the licence suits the user)
9
+ patterns = [r"\b\d{3}-\d{2}-\d{4}\b", r"[\w.+-]+@[\w-]+\.[\w.]+"]
10
+ terms = ["Ama Mensah", "Account 0012345678"] # from the user
11
+ doc = fitz.open("work/in/case.pdf")
12
+ hits = []
13
+ for page in doc:
14
+ text = page.get_text()
15
+ found = set(terms) | {m.group(0) for p in patterns for m in re.finditer(p, text)}
16
+ for t in found:
17
+ for rect in page.search_for(t):
18
+ hits.append((page.number + 1, t, rect))
19
+ print(len(hits), "hits")
20
+ ```
21
+
22
+ Scanned pages have no text to search: OCR them first, then search the OCR layer, and redact the image region as well as the hidden text.
23
+
24
+ Show the user the hit list (page, term, count) before applying.
25
+
26
+ ## 2. Apply
27
+
28
+ ```python
29
+ for page_no, _term, rect in hits: # the approved hit list from step 1
30
+ doc[page_no - 1].add_redact_annot(rect, fill=(0, 0, 0))
31
+ for page in doc:
32
+ page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_PIXELS) # removes text and the image pixels under each box; be explicit, defaults changed across versions
33
+ doc.set_metadata({})
34
+ doc.del_xml_metadata()
35
+ for name in doc.embfile_names():
36
+ doc.embfile_del(name)
37
+ doc.save("work/out/case.redacted.pdf", garbage=4, deflate=True, clean=True) # full rewrite, no incremental update
38
+ ```
39
+
40
+ Also remove, where present: form field values, annotations and comments, bookmarks that quote redacted text, and document-level JavaScript.
41
+
42
+ Stronger alternative when the layout does not need to stay searchable: rasterise each page to an image at 200 to 300 dpi after drawing the boxes, build a new PDF from the images, then OCR it if search is needed. This leaves no original text objects at all.
43
+
44
+ ## 3. Verify (all must pass)
45
+
46
+ ```bash
47
+ pdftotext work/out/case.redacted.pdf - | grep -i -E "Ama Mensah|0012345678|[0-9]{3}-[0-9]{2}-[0-9]{4}" && echo "FAIL: text still present"
48
+ pdfinfo work/out/case.redacted.pdf # author, title, subject, keywords cleared
49
+ exiftool work/out/case.redacted.pdf # if installed: XMP and other metadata
50
+ grep -c "%%EOF" work/out/case.redacted.pdf # more than 1 suggests incremental updates remain (a linearized file also has 2)
51
+ qpdf --check work/out/case.redacted.pdf
52
+ ```
53
+
54
+ pdftotext does not read annotation or form-field text: also list fields with pypdf `get_fields()` and annotations with PyMuPDF `page.annots()` and confirm none hold a redacted value.
55
+
56
+ Then open the output, select all text on a redacted page, and paste it into a text file; search again. Check a few redacted areas zoomed in for half-covered characters.
57
+
58
+ ## 4. Hand-off
59
+
60
+ - Report: terms and patterns used, hits found, hits after verification (must be zero), what metadata was removed.
61
+ - A person must review every redacted page before release. What must legally be withheld, and how redactions must be labelled (for example exemption codes), is decided by the user's legal or records team, not by this process.