diff --git a/claude/.claude/skills/send-mct-export/SKILL.md b/claude/.claude/skills/send-mct-export/SKILL.md new file mode 100644 index 0000000..cd99eb8 --- /dev/null +++ b/claude/.claude/skills/send-mct-export/SKILL.md @@ -0,0 +1,13 @@ +--- +name: send-mct-export +description: Run an MCT export against the current state of the production pipeline and send it to someone on Slack +--- +# Prerequisites +1. Slack MCP access +2. A user-provided Slack channel name or description of a Slack channel to send a message to + 1. Before the rest of the workflow begins, confirm this fact with the user and decide on a channel identifier +# Steps +1. If the user has not already told you what environment they want to run the export in, such as `local`, `dev`, or `prod`, assume `prod`. This is completely safe to do because it doesn't actually touch any of the loans +2. Run the MCT export workflow `nx run mct-export:pull-latest ` +3. Re-confirm the Slack destination channel to send it in +4. Send the generated CSV file to the channel \ No newline at end of file diff --git a/claude/.claude/skills/synced/.bucket-fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57 b/claude/.claude/skills/synced/.bucket-fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57 new file mode 100644 index 0000000..e69de29 diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/.last-complete-round b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/.last-complete-round new file mode 100644 index 0000000..a34eba6 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/.last-complete-round @@ -0,0 +1 @@ +204bf30e-1239-48bc-a5e1-f0c0ba9e5236 cli \ No newline at end of file diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/built-in-browser/LICENSE.txt b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/built-in-browser/LICENSE.txt new file mode 100644 index 0000000..c55ab42 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/built-in-browser/LICENSE.txt @@ -0,0 +1,30 @@ +© 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/built-in-browser/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/built-in-browser/SKILL.md new file mode 100644 index 0000000..b32d8c8 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/built-in-browser/SKILL.md @@ -0,0 +1,48 @@ +--- +name: built-in-browser +description: "Read this skill before the first step that uses the built-in browser, the browser pane inside the Claude desktop app (also called the in-app browser, the browser pane, Claude's browser, or \"your own browser\"), whose tools are named mcp__Claude_Browser__* when the session runs in the desktop app and mcp__remote-devices__Claude_Browser__* when a cloud session is linked to the person's computer; before those tools are turned on there may be a single enable__mcp__remote-devices__Claude_Browser tool instead. It covers the pane's persistent sign-ins, tabs and preview_start, reading pages as text, site approvals, what the pane cannot open, and what to do when it cannot be reached. It is not for Claude in Chrome (mcp__claude-in-chrome__* tools), which has its own skill, and it does not decide which browser to use." +compatibility: "Cowork in the Claude desktop app, and Cowork Remote or upgraded claude.ai sessions linked to a computer running the desktop app" +license: Proprietary. LICENSE.txt has complete terms +--- + +# Built-in browser + +The built-in browser is a real browser pane inside the Claude desktop app, separate from the person's Chrome. Its tools are named `mcp__Claude_Browser__*` when the session itself runs inside the desktop app, and `mcp__remote-devices__Claude_Browser__*` when the session runs in the cloud (started from the web, a phone, or the desktop app) and is linked to the person's computer. The names after the prefix are the same either way, Claude uses whichever prefix is actually present, and this skill refers to the tools by the part after the prefix. + +If the only built-in browser tool present is `enable__mcp__remote-devices__Claude_Browser`, Claude calls it first: it turns the built-in browser on for this conversation, and the `mcp__remote-devices__Claude_Browser__*` tools appear once it has run. + +## What the person can see + +The browser pane shares the desktop app's side panel with artifacts, documents, and file previews, and the panel shows one of them at a time. While the browser pane is showing, the person sees what Claude sees and can browse or take over at any time. While something else is open in the panel, or the panel is closed, the built-in browser keeps working but the person cannot see it. + +Right before asking the person to do something in the built-in browser themselves (click a button, sign in, complete a verification step), Claude calls `tabs_context`, whose result ends by saying whether the Browser pane is displayed, hidden, or not open. If the pane is not open, Claude opens the page first and checks again. If the pane is hidden, Claude first asks the person to bring the browser back in the Claude desktop app: press Cmd+Shift+B on Mac or Ctrl+Shift+B on Windows, or close whatever else is open in the side panel and click the globe icon (the Browser button). Claude then says what to do in the browser. Claude asks because using the browser does not bring the pane back, and what the panel shows is the person's choice. Claude also says in the conversation what it found or did in the browser, because the person may not have been watching the pane. + +## Sign-ins persist, and they are the person's + +The built-in browser keeps its own persistent profile, shared across the desktop app's sessions. The person, or an earlier session, may already be signed in to sites there, and sign-ins Claude completes persist for later. Claude treats existing sessions as the person's: it never signs out, changes credentials, or acts on an account beyond what the task needs. + +## Tabs + +The built-in browser has tabs. `preview_start` with a `url` opens an additional tab at that URL in one call and returns a `tabId`, leaving existing tabs untouched, so Claude prefers it over `tabs_create` followed by `navigate` when the destination is already known. `navigate`, `read_page`, `get_page_text`, `find`, `computer`, `form_input`, and the console and network readers act on the tab named by `tabId`; omitting `tabId` targets the active tab, and `tabs_context` lists the open tabs. + +## Loading via ToolSearch + +Claude loads the built-in browser tools in bulk, not one-by-one: if they are in the deferred list, Claude loads them all in a single ToolSearch call whose query is their full name prefix, for example `{ query: "mcp__remote-devices__Claude_Browser__", max_results: 64 }`. + +## Reading pages + +Claude prefers `get_page_text` and `read_page` over screenshots for reading, because they return the page's actual text and structure rather than pixels of the visible viewport. `computer` with action "screenshot" is for when the visual layout is the point or the person asks to see the page. + +## Site approvals, blocked sites, and request_access + +Depending on the person's approval settings, the person may be asked to approve a site before Claude acts on it, and some sites are blocked outright. Claude waits for a pending approval rather than working around it. If a page is refused or an approval is declined, Claude tells the person and moves on rather than retrying. + +When the tools carry the `mcp__remote-devices__Claude_Browser__` prefix, approvals can be answered from any of the person's devices and may take a moment to arrive, and the session may also have a `request_access` tool. If a browser tool answers that the site is not allowed yet and `request_access` is present, Claude calls it with that site's URL and scope "once" (or "site" when the task will keep using that site), waits for the person's answer, and then retries the original tool. Without `request_access`, a refused site is handled as above: Claude tells the person and moves on. + +## What the built-in browser cannot open + +The built-in browser cannot open `file://` URLs or `localhost` servers that Claude starts itself, because those run where Claude's shell runs, which is not where the browser pane runs. To show the person HTML that Claude generated, Claude uses an artifact instead. + +## When the built-in browser cannot be reached + +When the tools carry the `mcp__remote-devices__Claude_Browser__` prefix, the pane runs in the Claude desktop app on the person's computer while Claude runs elsewhere, so it is reachable only while that app is open and online. If those tool calls cannot reach the desktop app (connection errors or no response), Claude tells the person the built-in browser looks offline on their computer rather than retrying, and follows the session's browser guidance on whether to continue with the other browser or ask first. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/chrome-browser/LICENSE.txt b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/chrome-browser/LICENSE.txt new file mode 100644 index 0000000..c55ab42 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/chrome-browser/LICENSE.txt @@ -0,0 +1,30 @@ +© 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/chrome-browser/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/chrome-browser/SKILL.md new file mode 100644 index 0000000..8d4169a --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/chrome-browser/SKILL.md @@ -0,0 +1,64 @@ +--- +name: chrome-browser +description: "Read this skill before the first step that uses Claude in Chrome, the browser extension whose tools are named mcp__claude-in-chrome__* (also called Chrome, the browser extension, or the external browser) and which acts in the person's real Chrome with their own sign-ins; before those tools are turned on there may be a single enable__mcp__claude-in-chrome tool instead. It covers loading the tools in one ToolSearch call, checking the person's open tabs and working in a new tab, site permissions, GIF recordings, console logs, dialogs to avoid, and when to stop and ask. It is not for the built-in browser (mcp__Claude_Browser__* or mcp__remote-devices__Claude_Browser__* tools), which has its own skill, and it does not decide which browser to use." +compatibility: "Claude Code, Cowork (desktop and remote), and claude.ai sessions upgraded to Cowork Remote, when the Claude in Chrome extension is connected" +license: Proprietary. LICENSE.txt has complete terms +--- + +# Claude in Chrome + +Claude in Chrome is a browser extension. Its tools, named `mcp__claude-in-chrome__*`, act in the person's real Chrome, in new tabs alongside the person's own, with their existing sign-ins. Claude in Chrome is available only while the person's Chrome is running with the extension connected; if its tool calls report that the extension is not connected or get no response, Claude says so rather than retrying, and follows the session's browser guidance on whether to continue with the other browser or ask first. + +If the only Claude in Chrome tool present is `enable__mcp__claude-in-chrome`, Claude calls it first: it turns Claude in Chrome on for this conversation, and the `mcp__claude-in-chrome__*` tools appear once it has run. + +## Loading the tools + +If the `mcp__claude-in-chrome__*` tools are deferred (meaning they have to be loaded through ToolSearch before use), Claude loads every tool it expects to need in ONE ToolSearch call, because the select query accepts a comma-separated list and each extra ToolSearch call costs a full round trip. The core set to start with: + +``` +ToolSearch with query "select:mcp__claude-in-chrome__tabs_context_mcp,mcp__claude-in-chrome__navigate,mcp__claude-in-chrome__computer,mcp__claude-in-chrome__read_page,mcp__claude-in-chrome__tabs_create_mcp,mcp__claude-in-chrome__tabs_close_mcp" +``` + +Claude adds task-specific tools to that same call when the task obviously needs them: `read_console_messages` and `read_network_requests` for debugging, `form_input` for forms, `gif_creator` for recordings, `javascript_tool` for page scripting. A second ToolSearch is only for a tool the task turned out to need later. + +## Starting a session: tab context first, then a new tab + +At the start of each browser session Claude calls `mcp__claude-in-chrome__tabs_context_mcp` first, to see the person's current tabs and understand what they may want to work with. Then: + +1. Claude reuses an existing tab only when the person explicitly asks to work with it. +2. Otherwise Claude opens a new tab with `mcp__claude-in-chrome__tabs_create_mcp` and works there, and, unless the person wants them kept, closes the tabs it created before finishing. +3. Claude never reuses tab IDs remembered from an earlier or different session. If a tool reports that a tab does not exist or is invalid, or the person closes a tab, or a navigation error occurs, Claude calls `tabs_context_mcp` again for fresh tab IDs. + +## Site permissions + +Claude in Chrome acts on a site only once the person has allowed it; depending on their settings the person may be prompted per site, in the extension or in the app. When a tool call is waiting on or refused that permission, Claude tells the person and waits for them to allow it rather than working around it. A site the person declines is their decision; Claude moves on. + +## Recording a GIF + +For multi-step interactions the person may want to review or share, Claude can record them with `mcp__claude-in-chrome__gif_creator`. Claude captures a few extra frames before and after each action so playback is smooth, and gives the file a meaningful name (for example "login_process.gif"). + +## Reading console output + +`mcp__claude-in-chrome__read_console_messages` reads the page's console. Console output can be verbose, so when Claude is looking for specific entries it passes the `pattern` parameter (a regular expression), for example pattern "[MyApp]" to keep only the application's own logs. + +## Alerts and dialogs + +Claude does not trigger JavaScript alerts, confirms, prompts, or other browser modal dialogs through its actions. Those dialogs block all further browser events, so the extension stops receiving commands. Instead: + +1. Claude avoids clicking elements likely to raise a confirmation dialog (for example a "Delete" button) unless necessary. +2. If it must interact with such an element, Claude warns the person first that this may interrupt the session. +3. Claude can use `mcp__claude-in-chrome__javascript_tool` to check for and dismiss an existing dialog before proceeding, and prefers `console.log` plus `read_console_messages` over `alert` for debugging. + +If a dialog does get triggered and the browser stops responding, Claude tells the person they need to dismiss it manually in Chrome. + +## Staying on task, and when to stop + +Claude stays focused on the specific task and does not wander into unrelated pages. Claude stops and asks the person how to proceed, explaining what it tried and what went wrong, when any of these happen: + +- browser tool calls fail or return errors after 2 or 3 attempts +- the extension gives no response +- page elements do not respond to clicks or input, or pages do not load or time out +- the task turns out to involve unexpected complexity or tangents +- several approaches have not completed the task + +Claude does not keep retrying the same failing action. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/computer-use/LICENSE.txt b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/computer-use/LICENSE.txt new file mode 100644 index 0000000..c55ab42 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/computer-use/LICENSE.txt @@ -0,0 +1,30 @@ +© 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/computer-use/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/computer-use/SKILL.md new file mode 100644 index 0000000..e4d9bcc --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/computer-use/SKILL.md @@ -0,0 +1,67 @@ +--- +name: computer-use +description: "Read this skill before the first step of any request to do something in an app on the person's own computer (Notes, Finder, System Settings, any desktop app), to look at their screen, or for \"computer use\". Computer use (desktop control) lets Claude take screenshots of the person's desktop and control it with clicks, typing and scrolling through the Claude desktop app; its tools are named mcp__computer-use__* when the session runs in the desktop app and mcp__remote-devices__computer_* when a cloud session is linked to the person's computer; before computer use is turned on for a conversation there may be no such tools, only an enable__mcp__remote-devices__computer tool, which turns it on. It covers turning it on, picking the right tool, the access flow, and the safety rules for tiered apps, links and financial actions. It is not for websites, which go through Claude in Chrome or the built-in browser and their own skills." +compatibility: "Cowork in the Claude desktop app, and Cowork Remote or upgraded claude.ai sessions linked to a computer running the desktop app (macOS or Windows) with Computer use turned on" +license: Proprietary. LICENSE.txt has complete terms +--- + +# Computer use (desktop control) + +Computer use lets Claude take screenshots of the person's desktop and control it with mouse clicks, keyboard input, and scrolling, through the Claude desktop app. Its tools are named `mcp__computer-use__*` (for example `request_access`, `screenshot`) when the session runs inside the desktop app, and `mcp__remote-devices__computer_*` (for example `computer_request_access`, `computer_screenshot`) when the session runs in the cloud and is linked to the person's computer. Computer use works only while Computer use is turned on in the Claude desktop app (Settings → Desktop app → Computer use; it is off by default). + +## Turning computer use on from a chat + +What Claude does first depends on which tools are present (loaded or deferred): + +1. **Computer-use tools are present.** Claude goes straight to the access flow below. +2. **No computer-use tools, but an `enable__mcp__remote-devices__computer` tool is present.** Claude calls that tool once, before any other step of the task, with the task in its `task` field. When the call goes through, the `mcp__remote-devices__computer_*` tools arrive with it and Claude starts with the access flow below; there is no second enable step. If it does not go through, Claude relays the reason in one line and carries on with what it can do in the conversation. +3. **Neither.** Computer use is not available in this conversation as it stands, and Claude says so plainly without diagnosing why: in general Claude can use apps on a computer where the Claude desktop app is installed and signed in with Computer use turned on, and on claude.ai in a web browser, if the composer's + menu offers Devices, the person can pick their computer there before sending a new message. Claude does not pretend to act. + +## Separate filesystems + +Computer-use actions (clicks, typing, clipboard writes) happen on the person's real computer, a different system from wherever Claude's own shell and files are, when it has them. Files Claude creates on its side do not exist on the person's machine. If Claude puts a command or file path in the person's clipboard, or types into one of their apps, the path must exist on their computer, not a path on Claude's side that they can't reach. + +## Pick the right tool for the app + +Each tier trades speed/precision against coverage: + +1. **Dedicated MCP for the app**: if the task is in an app that has its own MCP (Slack, Gmail, Calendar, Linear, etc.) and that MCP is connected, Claude uses it. API-backed tools are fast and precise. +2. **Browser tools**: if the target is a web app and there's no dedicated MCP for it, Claude uses a browser: Claude in Chrome (tools named `mcp__claude-in-chrome__*`) or the built-in browser (tools named `mcp__Claude_Browser__*` in the desktop app, `mcp__remote-devices__Claude_Browser__*` when a cloud session is linked to the person's computer). Both are DOM-aware, much faster than clicking pixels. If this prompt has a `` section, Claude follows it on which browser to use; otherwise it uses whichever one is connected. If neither is connected, Claude asks the person to connect one rather than falling through to computer use. +3. **Computer use**: for native desktop apps (Maps, Notes, Finder, Photos, System Settings, any third-party native app) and cross-app workflows. Computer use is the right tool here; Claude does not decline a native-app task just because there's no dedicated MCP for it. + +This is about what's available, not error handling: if a dedicated MCP tool errors, Claude debugs or reports it rather than silently retrying via a slower tier. + +## Look before asserting + +If the person asks about app state (what's open, what's connected, what an app can do), Claude takes a screenshot and checks before answering. Claude does not answer from memory: the person's setup or app version may differ from what it expects. If Claude is about to say an app doesn't support an action, that claim should be grounded in what it just saw on screen, not general knowledge. Similarly, `list_granted_applications` or a fresh `screenshot` is cheaper than a wrong assertion about what's running. + +## Loading via ToolSearch + +Claude loads them in bulk, not one-by-one: if computer-use tools are in the deferred list, Claude loads them all in a single ToolSearch call: `{ query: "computer", max_results: 40 }`. The keyword matches every computer-use tool name, so one query returns the entire toolkit. + +## Access flow + +Before any computer-use action Claude must request access to the applications it needs: with the `mcp__computer-use__*` tools that is one `request_access` call with the list of applications; with the `mcp__remote-devices__computer_*` tools it is two calls, `computer_resolve_access` with the app names, then `computer_request_access` with the entries it returned, verbatim, and a one-sentence reason that explains the task. The person approves each application explicitly, and Claude may need to ask again mid-task if it discovers it needs another application. Claude waits for the person's answer rather than working around it. + +## Tiered apps + +Some apps are granted at a restricted tier based on their category; the tier is displayed in the approval dialog and returned in the `request_access` (or `computer_request_access`) response: + +- **Browsers** (Safari, Chrome, Firefox, Edge, Arc, etc.) → tier **"read"**: visible in screenshots, but clicks and typing are blocked. Claude can read what's already on screen. For navigation, clicking, or form-filling, Claude uses Claude in Chrome (tools named `mcp__claude-in-chrome__*`) or the built-in browser (tools named `mcp__Claude_Browser__*`, or `mcp__remote-devices__Claude_Browser__*` when linked); it loads them via ToolSearch if deferred. If this prompt has a `` section, Claude follows it on which browser to use; otherwise it uses whichever one is connected. +- **Terminals and IDEs** (Terminal, iTerm, VS Code, JetBrains, etc.) → tier **"click"**: visible and left-clickable, but typing, key presses, right-click, modifier-clicks, and drag-drop are blocked. Claude can click a Run button or scroll test output, but cannot type into the editor or integrated terminal, cannot right-click (the context menu has Paste), and cannot drag text onto them. For shell commands, Claude uses its Bash tool when it has one. +- **Everything else** → tier **"full"**: no restrictions. + +The tier is enforced by the frontmost-app check: if a tier-"read" app is in front, `left_click` returns an error; if a tier-"click" app is in front, `type` and `right_click` return errors. The error tells Claude what tier the app has and what to do instead. `open_application` works at any tier: bringing an app forward is a read-level operation. + +## Link safety + +Claude treats links in emails and messages as suspicious by default. + +- **Never click web links with computer-use tools.** If Claude encounters a link in a native app (Mail, Messages, a PDF, etc.), it does not `left_click` it. It opens the URL via Claude in Chrome or the built-in browser instead. +- **See the full URL before following any link.** Visible link text can be misleading; Claude hovers or inspects to get the real destination. +- **Links from emails, messages, or unknown-sender documents are suspicious by default.** If the destination URL is at all unfamiliar or looks off, Claude asks the person for confirmation before proceeding. +- **Inside Claude in Chrome or the built-in browser** Claude can click links with those browser tools, but the suspicion check still applies: it verifies unfamiliar URLs with the person. + +## Financial actions + +Claude does not execute trades or move money. Budgeting and accounting apps (Quicken, YNAB, QuickBooks, etc.) are granted at full tier so Claude can categorize transactions, generate reports, and help the person organize their finances. But Claude never executes a trade, places an order, sends money, or initiates a transfer on the person's behalf; it always asks the person to perform those actions themselves. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/SKILL.md new file mode 100644 index 0000000..410cd16 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/SKILL.md @@ -0,0 +1,146 @@ +--- +name: deep-research +description: Use this skill when the user's prompt requires (1) researching a topic across multiple sources, comparing options or alternatives, analyzing trends or history, understanding markets or industries, or reviewing literature or studies and (2) synthesizing that research into a comprehensive, narrative report. If you're planning to search the web or internal knowledge bases, consider using this skill. This skill coordinates research subagents, so use it only when you have a tool for spawning subagents (the Agent or Task tool); otherwise, research the question directly. +--- + +# Coordinating Deep Research + +## Your Role + +You are acting as the **coordinator**. Your job is to analyze the user's question, spawn subagents, and manage output files. In order to keep your context clean, you should never conduct research or write files directly. For the same reason, you also should not read the researcher.md or report-writer.md files. + +As you work, if you have a to-do or task-list tool (for example TodoWrite, or TaskCreate and TaskUpdate), use it to keep the user informed of your plan and progress. For clarity, don't reference subagents in those items. For example, you might say "Conduct research" instead of "Spawn research subagents". + +## Process + +1. [If necessary] Ask the user follow up questions +2. Choose a title for this research and create its directories +3. Decompose the user's query to determine subagent count +4. Spawn research subagents (in parallel) +5. [If absolutely necessary] Conduct one additional round of research +6. Spawn report writer subagent +7. Deliver report to user + +### 1. [If necessary] Ask the user follow up questions + +Read the user's question and consider if there are any missing or ambiguous details that need to be clarified. Some examples include: +- Ambiguous terminology — Acronyms, jargon, or terms with multiple meanings across different fields +- Implicit geographic or jurisdictional context — Questions where the answer varies significantly by region but no region is specified +- Ambiguous entities — Company names, product names, or proper nouns that refer to multiple distinct entities + +In order to avoid creating unnecessary friction with the user, follow ups should only be asked when they would meaningfully impact how research is conducted. + +If you determine that you do need to ask follow up questions, do so with the AskUserQuestion tool. + +### 2. Choose a title for this research and create its directories + +First choose a title for this research. It names the folder that will hold this request's research notes and is also the filename of the report the user receives, where it is shown as the report's title, so make it a short, specific title for this research in the user's language and in sentence case: 3 to 6 words and under 50 characters (count them, and drop words rather than exceed either limit), using only letters, numbers and single spaces. Keep accented and non-Latin letters exactly as the user's language writes them (for example é, ñ, ü, 日本語) rather than converting them to plain ASCII, and use no hyphens, underscores, apostrophes or other punctuation. For example, a question about how the EU AI Act affects startups could use "EU AI Act startup obligations". + +This research's folders go in the same directory you used for research earlier in this conversation, or, if this is the first research request here, in your current working directory (not /tmp or an uploads folder). Before settling on the title, recall the titles of any research reports you have already delivered earlier in this conversation, and, if a research_notes/ folder already exists in that directory, list it — each folder inside it holds the notes behind a report of the same name in reports/, possibly from an earlier conversation. Then pick the case that fits: +- New topic: choose a fresh title. +- Returning to, extending or updating an earlier topic: choose a title that still names the topic and says what this request adds or changes (for example "EU AI Act startup penalties", not just "Penalties"), or, if nothing distinguishes it from the earlier request, the earlier title with the next number added, dropping a word first if that would exceed the limits above (for example "EU AI Act startup obligations 2"). Note the path of the most recent earlier report on that topic and of its notes folder: you will pass them to the report writer in step 6 as background to build on. If that report or its notes folder is no longer on disk, pass on only what remains and research whatever is missing. +- The user asks for earlier research to be redone independently: choose a title as in the previous case, but do not pass the earlier material on. + +In every case the title must differ from every report delivered earlier in this conversation and from every folder you see, so that this request's notes and report never mix with or replace an earlier one's. Never rename, move or write into an earlier request's folder. + +Then create the folders research_notes/{research title}/ and reports/ in that directory. Quote these paths in shell commands, since the title contains spaces, and note the directory's absolute path — you will give the subagents absolute paths into these folders, and they will store their files there. + +### 3. Decompose the user's query to determine subagent count + +In order to effectively spawn and task subagents, determine if and how the user's question can be decomposed into independent subtopics. These subtopics should be mutually exclusive and collectively exhaustive. For each subtopic, then consider if the research requires (or would benefit from) multiple independent points of view. + +| Researchers | Decomposition Strategy | Example | +|-------------|------------------------|---------| +| 1 | No meaningful decomposition — single factual question | "Who won the 2024 Super Bowl?" | +| 3 | Few natural angles or perspectives on a focused topic | "Effects of temperature on salmon migration" → (1) biological mechanisms, (2) observed population data, (3) climate projections | +| 4 | Distinct facets or evaluation criteria | "Compare CRM platforms for small businesses" → (1) pricing models, (2) feature comparison, (3) integrations ecosystem, (4) user reviews/sentiment | +| 5 | Multiple independent entities, regions, or domains | "AI regulation landscape" → (1) US federal, (2) EU/GDPR-AI Act, (3) China, (4) UK, (5) industry self-regulation | +| 6+ | Large enumerable set requiring systematic coverage | "Electric vehicle adoption rates by US state" → researchers assigned to regional clusters | + +In the next step, you should spawn one subagent for each subtopic and independent point of view that you identified. When in doubt, 3 researchers is a reasonable default. If in step 2 you noted earlier research to build on, decompose only what this request adds or needs re-checked; the report writer will have the earlier material. + +### 4. Spawn foreground research subagents (in parallel) + +For each subtopic and independent point of view determined above, spawn a foreground research subagent. + +To spawn research subagents, use the Agent tool (named Task in some versions) with subagent_type="general-purpose" and run_in_background=false. Ensure that you provide **extremely clear, specific** instructions to each research subagent. Be sure to propagate constraints of the original question to the research subagent (e.g., temporal, geographical). In the prompts below, replace {path_to_skill} with this skill's base directory (the directory this SKILL.md was loaded from), {absolute path of your working directory} with the path you noted in step 2, and {research title} with the title you chose in step 2. Follow the exact format provided below: + +``` +Agent( + run_in_background=false + subagent_type="general-purpose", + description="{3-5 words}", + prompt="Research {specific narrow topic}. + +Objective: {clear description of what's covered in the desired output} + +Key questions: +- {Specific question 1} +- {Specific question 2} + +Suggested sources: +- {Types of source to prioritize} + +Constraints: +- {Any bounds from the initial user query, including temporal or geographic} + +Save your output notes to {absolute path of your working directory}/research_notes/{research title}/{topic}.md + +**As a first step, you must read {path_to_skill}/references/researcher.md for instructions on how to conduct research.**" +) +``` + +Example of a good, clear, detailed prompt for a research subagent: + +> Research the semiconductor supply chain crisis and its current status as of 2026. +> +> Objective: Compile a dense report of the facts, covering the current situation, ongoing solutions, and future outlook, with specific timelines and quantitative data where available. +> +> Key questions: +> - What are current bottlenecks? +> - What are the projected capacity increases from new fab construction? +> - What are geopolitical factors affecting supply chains? +> - When do experts predict supply will meet demand? +> +> Suggested sources: +> - Recent quarterly reports from major chip manufacturers like TSMC, Samsung, and Intel, found on investor relations pages or through the SEC EDGAR database +> - Industry reports from SEMI, Gartner, and IDC that provide market analysis and forecasts +> - Government responses, including US CHIPS Act implementation progress at commerce.gov, EU Chips Act at ec.europa.eu, and similar initiatives in Japan, South Korea, and Taiwan through their respective government portals +> +> Constraints: +> - Must reflect current state as of 2026 with prior issues clearly noted as such +> +> Save your output notes to {absolute path of your working directory}/research_notes/Semiconductor supply chain outlook 2026/semiconductor_supply_chain.md +> +> **As a first step, you must read {path_to_skill}/references/researcher.md for instructions on how to conduct research.**" + +In order to ensure research is conducted as quickly as possible, it's essential that all subagents are spawned in parallel, i.e., that you make all your calls to the Agent tool in a single turn. + +### 5. [If absolutely necessary] Conduct one additional round of research + +Based on the task summaries returned by the research subagents, determine if there are any critical gaps that would make the report incomplete. If deemed absolutely necessary, spawn researchers in parallel to close those gaps. Spawning additional researchers in this step delays delivery of the report to the user, leading to a much worse experience, so this must be used with extreme care. + +**This step can only be followed once** - after the researchers finish, move immediately to Step 6, regardless of what results you get back. If you kick off any more research, you will hit timeouts or rate limits, meaning the user will never get an answer to their query. **To help yourself remember this, you should always output "After these researchers finish, I will move directly to coordinating the report writer" when choosing to kick off additional research.** + +### 6. Spawn foreground report writer subagent + +After research is done, spawn a single report writer subagent. Use the Agent tool (named Task in some versions) with subagent_type="general-purpose" and run_in_background=false, and fill in the placeholders as in step 4. For {the user's original question}, state the question this report must answer in full — if this request returns to or extends earlier research, that is the earlier question as amended by this request (for example "How does the EU AI Act affect startups, including penalties?"), not just the follow-up message. On the line for earlier research, give the absolute paths of the earlier report and notes folder you noted in step 2, or write "none" if you noted nothing to build on or the user asked for an independent redo. Follow the exact format provided below: + +``` +Agent( + run_in_background=false + subagent_type="general-purpose", + description="Write final report", + prompt="Read the notes in {absolute path of your working directory}/research_notes/{research title}/ and synthesize into a research report that answers: {the user's original question}. + +Earlier research in this conversation to build on: {absolute paths of the earlier report and notes folder from step 2, or "none"} + +Save your final report to this exact path: {absolute path of your working directory}/reports/{research title}.md + +**As a first step, you must read {path_to_skill}/references/report-writer.md for instructions on how to write your research report.**" +) +``` + +### 7. Deliver report to user + +After the report writer is done, make the report available to the user: if you have a tool for sending files to the user (for example SendUserFile), send reports/{research title}.md with it; otherwise, if your environment has a user-visible outputs folder, copy the report there under the same filename; otherwise leave it at reports/{research title}.md. Then read the finalized report and send the user a brief summary (aim for 3-5 sentences) that says where the full report is (sent to them, in the outputs folder, or its absolute path). diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/references/report-writer.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/references/report-writer.md new file mode 100644 index 0000000..aa235d3 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/references/report-writer.md @@ -0,0 +1,111 @@ +# Writing a Research Report + +You are reviewing comprehensive research notes put together by a team of researchers and synthesizing them into a focused, well-structured report. Your top priority is delivering a high-quality research report that answers the user's query as best and as completely as possible. + +## General Process + +1. Find and read all the research notes in the specified folder. If your assignment lists earlier research to build on, read that report and those notes too as additional input and reuse what is still relevant; where they disagree with the notes in your folder, the notes in your folder win, and your report must stand on its own rather than refer the reader to the earlier one. Paths may contain spaces, so use the Read tool or quote them in shell commands. +2. Plan out the signpost headers for your report based on the writing guidelines below +3. Write and save the full report to the specified path + +## Report Format + +```markdown +# {6-word compelling title with active verb} +{BLUF paragraph: Lead with the key finding, then significance, impact, and essential context. One paragraph, roughly 5-8 sentences} + +## {Narrative signpost header with specific insight} +{Dense narrative-form paragraphs with **bold key facts**. Cite facts inline with `([Source](URL))`. Paragraphs should flow naturally — no bullet points or numbered lists} + +## {Second signpost header} +{Continue with dense paragraphs in the same format as above} + + + +## Conclusion +{1-2 paragraphs of novel takeaways and implications only — no repetition of earlier content} +``` + +## Detailed Specs + +### Title +- ~6 words, concise, interesting, draws the reader in +- Use active verbs when possible + +## BLUF paragraph +- Lead with the most important finding, then expand into significance, impact and context - the first sentences should directly answer the user's primary question +- One paragraph - prioritize density over sentence counting +- The reader should be able to stop here and walk away informed + +### Section headers +- Sentence case, not title case +- Act as **signposts** - a reader skimming only headers should understand the report +- Use specific numbers or narrative framing +- Each header should feel fresh and distinct from others + +Good -> Bad examples: +| Good | Bad | +|------|-----| +| "Migratory geese forage 370 extra hours annually" | "Effects of climate on geese" | +| "Nocturnal feeding extends the foraging day" | "Feeding behavior changes" | +| "Arctic summer eliminates circadian constraints" | "Environmental factors" | + +If helpful for readability, you can include sub-headers within a section. These sub-headers should follow the same rules outlined above. + +### Body paragraphs +- Write in dense, narrative prose that balances scholarly rigor and clarity with narrative form +- Don't use bullets or numbered lists +- **Bold** the most critical facts and figures to enable quick scanning +- Cite facts inline with `([Source](URL))`. You must include citations so the user can review sources. +- Each section should advance the argument, not just catalog information +- Use tables or rich markdown formatting when these elements significantly help. + +### Conclusion +- Be concise. Use 1-2 paragraphs maximum +- Include key takeaways and novel insights only +- Show how understanding has changed or what it implies going forward +- Don't summarize or repeat earlier content + +## Writing guidelines + +### Length + +You should tailor the report's style and length to match the user's request and what is most appropriate for the topic. An in-depth scientific question likely warrants a long, rigorous, detailed report, while an engineering question likely warrants decision-ready, precise technical answers. In general, a focused report that clearly communicates key insights is far more valuable than a longer report that includes everything. However, if the user's question requires depth and breadth, then make the report as long as necessary to cover everything important. + +A good rule of thumb is to include 3-5 focused sections with clear headers. + +### Writing style + +The report should have a clear narrative arc guiding readers. The opening (following the BLUF) establishes stakes and poses questions. The body develops analysis with strong supporting evidence. The conclusion doesn't just summarize, but shows how understanding has changed or offers novel insights. + +In your writing you should: +- Maximize information density while maintaining narrative form and clarity +- Use clear, direct, concrete language in active voice, eliminating unnecessary words +- Avoid hedging ("may", "possibly") +- Vary sentence rhythm, front-load key points, create elegant transitions, and maintain concision +- Maintain a tone of approachable intelligence - informative but not stiff, demonstrating expertise through facts and analysis while using conversational language + +### Analytical rigor + +Be sure to maintain epistemic honesty: +- Explicitly state what is uncertain or unknown +- Consider different angles or steelman alternative positions +- For scientific or analytical questions, develop and explore hypotheses +- Take positions based on research - come to conclusions about what is true, don't just give overviews. + +### Dealing with data + +You should interpret what data *means* rather than just stating facts without context: +- Prioritize synthesis showing how the information connects, rather than listing pure facts +- Weave quantitative and qualitative insights together +- Balance stories with numbers and facts where relevant. + +### Citations + +You must include inline citations using the `([Source](URL))` format following key claims, so that the user can verify sources. Focus on: +- Major findings that readers would want to verify +- Quantitative claims and statistics +- References to specific published media +- Not every sentence - cite strategically + +If a research note contains a specific major finding, quantitative claim, or reference to published media without an inline source URL, do not include it in the final report. A report with 5 well-sourced claims is better than one with 20 claims where half are unsourced. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/references/researcher.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/references/researcher.md new file mode 100644 index 0000000..e60fe80 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/deep-research/references/researcher.md @@ -0,0 +1,76 @@ +# Executing Research + +You've been assigned to a specific research task as part of a larger effort. Other researchers are covering adjacent areas in parallel, so focus exclusively on your assigned scope. + +## Your Goal + +Meet the objective and answer the key questions you were given. + +Your notes will be read by a report writer who will synthesize findings from all researchers into a final report. The writer won't do any additional research, so your notes must be comprehensive, self-contained, and well-sourced. + +## How to Research + +Core loop: +1. Reflect on what knowledge gaps exist to meet your objective and answer key questions +2. Use search tools (WebSearch or internal knowledge base searches) to collect information to close those gaps - generally shorter queries (<5 words) provide better results +3. Use fetch tools (WebFetch or internal document retrieval) to get the full content of promising pages from your search results - snippets from search are easy to take out of context +4. Repeat steps 1-3 until you hit one of the stop conditions below + +Guidelines: +- If search results are sparse, try broader queries +- Never repeat the exact same query — always vary phrasing or you will get the same results +- Parallelize search and fetch tool calls as much as possible +- Roughly 10 tool calls is typical; avoid exceeding 15 in order to provide timely responses and prevent timeouts + +## When to stop + +- You've met your objective and answered your key questions with well-sourced findings +- You're no longer finding any new, relevant information +- You're approaching or have hit 15 tool calls + +## Evaluating Sources + +After receiving results from web searches or other tools, pay attention to the details of tool results, and do not just take them at face value. Maintain epistemic honesty and practice good reasoning by ensuring sources are high-quality and only reporting accurate information in your notes. + +Before including a finding, consider: +- Is this speculation or confirmed fact (mentioning predictions, using verbs like "could" or "may", using narrative driven speculation with future tense, etc.)? +- Is this the original source or an aggregator? Prefer primary sources. +- Is this source generally problematic (using false authority, pairing passive voice with nameless sources, using general qualifiers without specifics, parroting unconfirmed reports, etc.)? + +For recent topics, defer to search results over your training data as facts may have changed. + +When sources conflict or there are potential issues with results, note that explicitly. Don't silently pick a conflicting source or report a suspect fact. When information is sparse, unavailable, or unreliable, say so clearly. Noting "I found no reliable sources on X" communicates valuable signal for the report-writer. + +## Output + +Save your findings to the path specified in your assignment, using the Write tool (the path may contain spaces, so quote it if you use a shell command instead). The parent directory has already been created by the coordinator. + +Structure your notes using exactly these three sections. If helpful, sub-headings within sections are fine for organization, but do not add other top-level sections: + +```markdown +# {Your Assigned Topic} + +## {Key Question 1} + +### Takeaway +{1-2 sentence summary with the key takeaway from the research} + +### Cited Findings +- {Fact, stat, claim, or opinion} — [Source](URL) +- {Fact, stat, claim, or opinion} — [Source](URL) +- {Fact, stat, claim, or opinion} — [Source A](URL); contradicted by [Source B](URL) + +### Inferences +- {Inference made based on cited findings} + +### Gaps +- {Question that couldn't be answered and why} + +{repeated for each key question} +``` + +Never fabricate information. If you can't find a specific statistic, fact, opinion, etc. do not invent one. Instead you should note the missing information as a gap. This is essential to making sure that the report is accurate. + +To that end, every fact or claim listed in your report must have an inline [Source](URL). If you cannot find a source move it to the Gaps section. + +Focus on specific, citable facts that answer your key questions. The report-writer needs concrete material to synthesize, not vague summaries. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docs/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docs/SKILL.md new file mode 100644 index 0000000..a7de5af --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docs/SKILL.md @@ -0,0 +1,6 @@ +--- +name: docs +description: 'docs (editable docs people share and comment on; the default for any document, named as a doc or not: a document, report, proposal, resume, cover letter, letter, contract, policy, form, template, worksheet, essay, handbook, guide, how-to, cheat sheet, SOP or other writing to keep, share, collaborate on, send, submit, print or sign; a doc exports to Word, PDF, Markdown or Google Docs, so needing a file to send, attach, upload, submit or print is no reason to pick Word, and a file nobody asked for is a doc, not Word; a plan, comparison, summary or notes asked in chat stays in chat; a pasted claude.ai artifact link may be a doc: check with docs tools first; Word or another file format named, tracked changes wanted, or a .docx to change or use as a template → that format''s skill): making one → if no docs-connector instructions are in context, call its `guide` (topic.instructions) first; then create the doc (headings only, no body) before any search, file read or plan, even with files attached.' +--- + +Everything about docs is served by the docs connector — follow its instructions (they say what to call first). If no docs tools are present (they may be listed as pages tools on some accounts), say so. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/LICENSE.txt b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/LICENSE.txt new file mode 100644 index 0000000..c55ab42 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/LICENSE.txt @@ -0,0 +1,30 @@ +© 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/SKILL.md new file mode 100644 index 0000000..f76181f --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/SKILL.md @@ -0,0 +1,93 @@ +--- +name: docx +description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx) or Word templates (.dotx). Triggers include: any mention of Microsoft Word Documents, such as 'Word doc', 'word document', '.docx', '.dotx', 'microsoft doc'. Also use when extracting or reorganizing content from .docx or .dotx files, inserting or replacing images in documents, find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a deliverable as a Word or .docx file (to download, email or print), use this skill. However, if they ask for a document, page, report, memo, or notes WITHOUT naming a file format and the session offers Claude's own dedicated document or page skill or connector, use that instead, even if they will email or print it. Do NOT use for PDFs, spreadsheets, Google Docs, or coding unrelated to document generation." +license: Proprietary. LICENSE.txt has complete terms +--- + +# DOCX creation, editing, and analysis + +If you are about to make a new document and this session offers Claude's own docs (a Docs connector, docs skill or Docs artifact type), and the user has neither named Word/.docx/.dotx, asked for tracked changes, nor supplied a .docx/.dotx file to change or use as a template, make the document there instead. It exports to Word, PDF, Markdown or Google Docs, so needing a file to send, attach, upload, submit or print is no reason to make a Word file. A .docx/.dotx given only as source material does not count as supplied. This skill remains the right tool for producing, reading, editing, templating or converting Word files. + +A `.docx` is a ZIP archive of XML files. Choose your approach by task: + +| Task | Approach | +|---|---| +| **Create** a new document | Write a `docx` (npm) script — see gotchas below | +| **Edit** an existing document | `unzip` → edit `word/document.xml` → `zip` (docx-js cannot open existing files) | +| **Read** content | `pandoc -t markdown file.docx` | + +> Script paths below are relative to this skill's directory. + +## Creating with docx-js — gotchas + +`docx` is preinstalled — do not run `npm install` first; write the script and `require('docx')` directly. Only if that require fails: `npm install docx`. The model knows the API; these are the footguns: + +- **Page size defaults to A4.** For US Letter set `page: { size: { width: 12240, height: 15840 } }` (DXA; 1440 = 1″). +- **Landscape:** pass portrait dimensions and `orientation: PageOrientation.LANDSCAPE` — docx-js swaps width/height internally. +- **Tables need dual widths:** set `columnWidths` on the table AND `width` on every cell, both in `WidthType.DXA` (PERCENTAGE breaks in Google Docs). Column widths must sum to the table width. +- **Table shading:** use `ShadingType.CLEAR`, never `SOLID` (renders black). +- **Lists:** never insert `•` literally; use a `numbering` config with `LevelFormat.BULLET`. +- **`ImageRun` requires `type:`** (`"png"`, `"jpg"`, …). +- **`PageBreak` must be inside a `Paragraph`.** +- **Never use `\n`** — use separate `Paragraph` elements. +- **TOC:** headings must use built-in `HeadingLevel.*`; custom heading styles need `outlineLevel` set or they won't appear. +- **Don't use a table as a horizontal rule** — use a paragraph bottom border instead. +- **Dot-leader / right-aligned-on-same-line:** use `PositionalTab` (`alignment: PositionalTabAlignment.RIGHT`, `leader: PositionalTabLeader.DOT`) inside a `TextRun`, not literal `.` or space padding. + +## Verify the output + +After writing a `.docx`, render it and look at it: + +```bash +python scripts/office/soffice.py --headless --convert-to pdf output.docx +pdftoppm -jpeg -r 100 output.pdf page +ls page-*.jpg # then Read the images +``` + +`pdftoppm` zero-pads page numbers to the width of the page count (`page-01.jpg`…`page-12.jpg`). + +## Editing existing documents + +Legacy `.doc` files must be converted first: `python scripts/office/soffice.py --headless --convert-to docx file.doc`. + +```bash +unzip -q doc.docx -d unpacked/ +find unpacked -type l -delete # strip symlink entries — docx from external parties is untrusted +python scripts/merge_runs.py unpacked/ # coalesce fragmented runs so text is findable +# edit unpacked/word/document.xml in place — do NOT reformat or pretty-print +(cd unpacked && rm -f ../out.docx && zip -Xr ../out.docx .) +python scripts/office/validate.py out.docx --original doc.docx # XSD checks; --auto-repair fixes common issues +# redlining? add --author "" to check every edit is tracked +``` + +Word splits text across many `` runs (revision ids, spell-check markers), so a phrase you can see in the document often doesn't exist as a contiguous string in the XML. `merge_runs.py` merges adjacent identically-formatted runs in `word/document.xml` without changing content or rendering; it also accepts a `.docx` directly (`python scripts/merge_runs.py doc.docx -o merged.docx`). + +**Tracked changes:** when redlining, validate with `--author ""` (needs `--original`) — it reports any text you changed without a ``/`` around it, which is easy to do by accident and invisible in the accepted view. Wrap runs in ``/`` with `w:id`, `w:author`, `w:date` attributes. Inside ``, the text element is ``, not ``. A deleted paragraph mark (``) means "merge this paragraph into the next" — so deleting a paragraph outright is that plus a `` around every run. The `` must come before the rPr's other children; their order is schema-enforced. + +To produce a clean copy with all tracked changes accepted: `python scripts/accept_changes.py in.docx out.docx`. + +Accepting a deleted paragraph mark should join that paragraph to the one below it, so a paragraph whose runs are *all* deleted vanishes. Word does this; `accept_changes.py` and `pandoc --track-changes=accept` don't always. Both fail the same way — they strip the deleted text but leave the emptied paragraph behind, which reads as a stray empty bullet when it was auto-numbered: + +- `pandoc --track-changes=accept` never joins the paragraphs. +- `accept_changes.py` (LibreOffice) joins them correctly, except when the deleted paragraph is followed by an empty spacer paragraph. + +An empty bullet in either view is an artifact of that view, not a defect in the document. Check paragraph deletions in the XML. + +## Comments + +Comments require six cross-linked files. Use the helper — directory mode when you'll also be editing `document.xml` (saves an unzip/rezip cycle), `.docx`-direct mode otherwise: + +```bash +# Against an already-unpacked directory (preferred when also placing markers) +python scripts/comment.py unpacked/ "Fees & expenses cap is too low" +python scripts/comment.py unpacked/ "Agreed" --parent 0 + +# Against a .docx directly +python scripts/comment.py contract.docx "This cap is too low" -o annotated.docx +``` + +The script writes `comments.xml`, `commentsExtended.xml`, `commentsIds.xml`, `commentsExtensible.xml`, the relationships, and the content-type overrides. Comment IDs are auto-assigned. It then prints the ``/``/`` snippet to add to `word/document.xml` so the comment anchors to specific text — until you place those markers, the comment exists but is not visible. + +## Dependencies + +`docx` (npm, preinstalled — install only if `require('docx')` fails) · `pandoc` · LibreOffice (`soffice`) · `pdftoppm` (Poppler) diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/__init__.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/__init__.py new file mode 100755 index 0000000..8b13789 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/__init__.py @@ -0,0 +1 @@ + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/accept_changes.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/accept_changes.py new file mode 100755 index 0000000..8e36316 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/accept_changes.py @@ -0,0 +1,135 @@ +"""Accept all tracked changes in a DOCX file using LibreOffice. + +Requires LibreOffice (soffice) to be installed. +""" + +import argparse +import logging +import shutil +import subprocess +from pathlib import Path + +from office.soffice import get_soffice_env + +logger = logging.getLogger(__name__) + +LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile" +MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard" + +ACCEPT_CHANGES_MACRO = """ + + + Sub AcceptAllTrackedChanges() + Dim document As Object + Dim dispatcher As Object + + document = ThisComponent.CurrentController.Frame + dispatcher = createUnoService("com.sun.star.frame.DispatchHelper") + + dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array()) + ThisComponent.store() + ThisComponent.close(True) + End Sub +""" + + +def accept_changes( + input_file: str, + output_file: str, +) -> tuple[None, str]: + input_path = Path(input_file) + output_path = Path(output_file) + + if not input_path.exists(): + return None, f"Error: Input file not found: {input_file}" + + if not input_path.suffix.lower() == ".docx": + return None, f"Error: Input file is not a DOCX file: {input_file}" + + try: + output_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(input_path, output_path) + except Exception as e: + return None, f"Error: Failed to copy input file to output location: {e}" + + if not _setup_libreoffice_macro(): + return None, "Error: Failed to setup LibreOffice macro" + + cmd = [ + "soffice", + "--headless", + f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}", + "--norestore", + "vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application", + str(output_path.absolute()), + ] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + check=False, + env=get_soffice_env(), + ) + except subprocess.TimeoutExpired: + return ( + None, + f"Successfully accepted all tracked changes: {input_file} -> {output_file}", + ) + + if result.returncode != 0: + return None, f"Error: LibreOffice failed: {result.stderr}" + + return ( + None, + f"Successfully accepted all tracked changes: {input_file} -> {output_file}", + ) + + +def _setup_libreoffice_macro() -> bool: + macro_dir = Path(MACRO_DIR) + macro_file = macro_dir / "Module1.xba" + + if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text(): + return True + + if not macro_dir.exists(): + subprocess.run( + [ + "soffice", + "--headless", + f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}", + "--terminate_after_init", + ], + capture_output=True, + timeout=10, + check=False, + env=get_soffice_env(), + ) + macro_dir.mkdir(parents=True, exist_ok=True) + + try: + macro_file.write_text(ACCEPT_CHANGES_MACRO) + return True + except Exception as e: + logger.warning(f"Failed to setup LibreOffice macro: {e}") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Accept all tracked changes in a DOCX file" + ) + parser.add_argument("input_file", help="Input DOCX file with tracked changes") + parser.add_argument( + "output_file", help="Output DOCX file (clean, no tracked changes)" + ) + args = parser.parse_args() + + _, message = accept_changes(args.input_file, args.output_file) + print(message) + + if "Error" in message: + raise SystemExit(1) diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/comment.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/comment.py new file mode 100755 index 0000000..46ed5f5 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/comment.py @@ -0,0 +1,368 @@ +"""Add comments to a DOCX document. + +Accepts either an unpacked directory OR a .docx/.dotx file directly. + +Usage: + # Against an unpacked directory (writes satellite files in place) + python comment.py unpacked/ "Comment text" + python comment.py unpacked/ "Reply text" --parent 0 + + # Against a .docx directly (extracts, writes satellite files, rezips) + python comment.py contract.docx "This cap is too low" -o annotated.docx + python comment.py contract.docx "Comment" --id 5 # explicit ID + +The comment ID is auto-assigned (max existing + 1) unless --id is given. +Plain text is XML-escaped automatically; if you pass already-escaped text +(e.g. &, ’) use --raw to skip escaping. + +After running, add markers to word/document.xml so the comment is visible: + + ... commented content ... + + +""" + +import argparse +import random +import shutil +import sys +import tempfile +import zipfile +from datetime import datetime, timezone +from pathlib import Path + +import defusedxml.minidom +from xml.parsers.expat import ExpatError +from xml.sax.saxutils import escape as xml_escape + +from office.helpers import opc_target, rezip as _rezip, safe_extract as _safe_extract + +TEMPLATE_DIR = Path(__file__).parent / "templates" +NS = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "w15": "http://schemas.microsoft.com/office/word/2012/wordml", + "w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid", + "w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex", +} + +COMMENT_XML = """\ + + + + + + + + + + + + + {text} + + +""" + +COMMENT_MARKER_TEMPLATE = """ +Add to word/document.xml (markers must be direct children of w:p, never inside w:r): + + ... + + """ + +REPLY_MARKER_TEMPLATE = """ +Nest markers inside parent {pid}'s markers (direct children of w:p, never inside w:r): + + ... + + + """ + +SMART_QUOTE_ENTITIES = { + "“": "“", + "”": "”", + "‘": "‘", + "’": "’", +} + + +def _generate_hex_id() -> str: + return f"{random.randint(0, 0x7FFFFFFE):08X}" + + +def _encode_smart_quotes(text: str) -> str: + for char, entity in SMART_QUOTE_ENTITIES.items(): + text = text.replace(char, entity) + return text + + +def _append_xml(xml_path: Path, root_tag: str, content: str) -> None: + dom = defusedxml.minidom.parseString(xml_path.read_text(encoding="utf-8")) + root = dom.getElementsByTagName(root_tag)[0] + ns_attrs = " ".join(f'xmlns:{k}="{v}"' for k, v in NS.items()) + wrapper_dom = defusedxml.minidom.parseString(f"{content}") + for child in wrapper_dom.documentElement.childNodes: + if child.nodeType == child.ELEMENT_NODE: + root.appendChild(dom.importNode(child, True)) + output = _encode_smart_quotes(dom.toxml(encoding="UTF-8").decode("utf-8")) + xml_path.write_text(output, encoding="utf-8") + + +def _find_para_id(comments_path: Path, comment_id: int) -> str | None: + dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8")) + for c in dom.getElementsByTagName("w:comment"): + if c.getAttribute("w:id") == str(comment_id): + for p in c.getElementsByTagName("w:p"): + if pid := p.getAttribute("w14:paraId"): + return pid + return None + + +def _next_comment_id(comments_path: Path) -> int: + if not comments_path.exists(): + return 0 + dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8")) + ids = [] + for c in dom.getElementsByTagName("w:comment"): + try: + ids.append(int(c.getAttribute("w:id"))) + except ValueError: + pass + return (max(ids) + 1) if ids else 0 + + +def _get_next_rid(rels_path: Path) -> int: + dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8")) + max_rid = 0 + for rel in dom.getElementsByTagName("Relationship"): + rid = rel.getAttribute("Id") + if rid and rid.startswith("rId"): + try: + max_rid = max(max_rid, int(rid[3:])) + except ValueError: + pass + return max_rid + 1 + + +def _has_relationship(rels_path: Path, target: str) -> bool: + dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8")) + return any( + rel.getAttribute("Target") == target + for rel in dom.getElementsByTagName("Relationship") + ) + + +def _has_content_type(ct_path: Path, part_name: str) -> bool: + dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8")) + return any( + o.getAttribute("PartName") == part_name + for o in dom.getElementsByTagName("Override") + ) + + +_COMMENT_RELS = [ + ("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", "comments.xml"), + ("http://schemas.microsoft.com/office/2011/relationships/commentsExtended", "commentsExtended.xml"), + ("http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", "commentsIds.xml"), + ("http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", "commentsExtensible.xml"), +] +_COMMENT_OVERRIDES = [ + ("/word/comments.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"), + ("/word/commentsExtended.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml"), + ("/word/commentsIds.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml"), + ("/word/commentsExtensible.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml"), +] + + +def _ensure_comment_relationships(unpacked_dir: Path) -> None: + rels_path = unpacked_dir / "word" / "_rels" / "document.xml.rels" + if not rels_path.exists(): + return + dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8")) + root = dom.documentElement + comment_types = {rel_type for rel_type, _ in _COMMENT_RELS} + existing = set() + for rel in dom.getElementsByTagName("Relationship"): + if rel.getAttribute("Type") not in comment_types: + continue + part = opc_target( + rel.getAttribute("Target"), + "word/document.xml", + rel.getAttribute("TargetMode"), + ) + if part is not None: + existing.add(part) + next_rid = _get_next_rid(rels_path) + changed = False + for rel_type, target in _COMMENT_RELS: + if opc_target(target, "word/document.xml") in existing: + continue + rel = dom.createElement("Relationship") + rel.setAttribute("Id", f"rId{next_rid}") + rel.setAttribute("Type", rel_type) + rel.setAttribute("Target", target) + root.appendChild(rel) + next_rid += 1 + changed = True + if changed: + rels_path.write_bytes(dom.toxml(encoding="UTF-8")) + + +def _ensure_comment_content_types(unpacked_dir: Path) -> None: + ct_path = unpacked_dir / "[Content_Types].xml" + if not ct_path.exists(): + return + dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8")) + root = dom.documentElement + existing = { + o.getAttribute("PartName") + for o in dom.getElementsByTagName("Override") + } + changed = False + for part_name, content_type in _COMMENT_OVERRIDES: + if part_name in existing: + continue + override = dom.createElement("Override") + override.setAttribute("PartName", part_name) + override.setAttribute("ContentType", content_type) + root.appendChild(override) + changed = True + if changed: + ct_path.write_bytes(dom.toxml(encoding="UTF-8")) + + +def add_comment( + unpacked_dir: Path | str, + text: str, + comment_id: int | None = None, + author: str = "Claude", + initials: str = "C", + parent_id: int | None = None, + raw: bool = False, +) -> tuple[int, str, str]: + unpacked_dir = Path(unpacked_dir) + if not raw: + text = xml_escape(text) + author = xml_escape(author, {'"': """}) + initials = xml_escape(initials, {'"': """}) + word = unpacked_dir / "word" + if not word.exists(): + raise FileNotFoundError(f"{word} not found (not an unpacked .docx?)") + + comments = word / "comments.xml" + if comment_id is None: + comment_id = _next_comment_id(comments) + + parent_para = None + if parent_id is not None: + parent_para = _find_para_id(comments, parent_id) if comments.exists() else None + if not parent_para: + raise ValueError(f"parent comment {parent_id} not found") + + para_id, durable_id = _generate_hex_id(), _generate_hex_id() + ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + if not comments.exists(): + shutil.copy(TEMPLATE_DIR / "comments.xml", comments) + _ensure_comment_relationships(unpacked_dir) + _ensure_comment_content_types(unpacked_dir) + _append_xml( + comments, + "w:comments", + COMMENT_XML.format( + id=comment_id, author=author, date=ts, initials=initials, + para_id=para_id, text=text, + ), + ) + + ext = word / "commentsExtended.xml" + if not ext.exists(): + shutil.copy(TEMPLATE_DIR / "commentsExtended.xml", ext) + if parent_para is not None: + _append_xml( + ext, "w15:commentsEx", + f'', + ) + else: + _append_xml( + ext, "w15:commentsEx", + f'', + ) + + ids = word / "commentsIds.xml" + if not ids.exists(): + shutil.copy(TEMPLATE_DIR / "commentsIds.xml", ids) + _append_xml( + ids, "w16cid:commentsIds", + f'', + ) + + extensible = word / "commentsExtensible.xml" + if not extensible.exists(): + shutil.copy(TEMPLATE_DIR / "commentsExtensible.xml", extensible) + _append_xml( + extensible, "w16cex:commentsExtensible", + f'', + ) + + action = "reply" if parent_id is not None else "comment" + return comment_id, para_id, f"Added {action} id={comment_id} (paraId={para_id})" + + +def main() -> None: + p = argparse.ArgumentParser(description="Add a comment to a DOCX (directory or .docx file).") + p.add_argument("input", help="Unpacked DOCX directory OR a .docx/.dotx file") + p.add_argument("text", help="Comment text (plain text; XML-escaped automatically)") + p.add_argument("--raw", action="store_true", + help="Treat text as pre-escaped XML (skip automatic escaping)") + p.add_argument("--id", type=int, dest="comment_id", + help="Comment ID (default: auto-assign as max existing + 1)") + p.add_argument("--author", default="Claude", help="Author name") + p.add_argument("--initials", default="C", help="Author initials") + p.add_argument("--parent", type=int, help="Parent comment ID (makes this a reply)") + p.add_argument("-o", "--output", + help="Output .docx path (only used when input is a .docx; default: overwrite input)") + args = p.parse_args() + + src = Path(args.input) + + try: + if src.is_dir(): + if args.output: + print("Warning: --output ignored for directory input", file=sys.stderr) + cid, _, msg = add_comment( + src, args.text, comment_id=args.comment_id, + author=args.author, initials=args.initials, + parent_id=args.parent, raw=args.raw, + ) + print(msg) + elif src.is_file() and src.suffix.lower() in (".docx", ".dotx"): + out = Path(args.output) if args.output else src + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + with zipfile.ZipFile(src) as zf: + _safe_extract(zf, tmp_path) + cid, _, msg = add_comment( + tmp_path, args.text, comment_id=args.comment_id, + author=args.author, initials=args.initials, + parent_id=args.parent, raw=args.raw, + ) + _rezip(tmp_path, out) + print(msg) + print(f"Wrote {out} (comment defined; add markers to word/document.xml to make it visible)") + else: + print(f"Error: {src} is neither a directory nor a .docx/.dotx file", file=sys.stderr) + sys.exit(1) + except (FileNotFoundError, ValueError, zipfile.BadZipFile, ExpatError) as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + if args.parent is not None: + print(REPLY_MARKER_TEMPLATE.format(pid=args.parent, cid=cid)) + else: + print(COMMENT_MARKER_TEMPLATE.format(cid=cid)) + + +if __name__ == "__main__": + main() diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/merge_runs.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/merge_runs.py new file mode 100755 index 0000000..4c7c1bf --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/merge_runs.py @@ -0,0 +1,310 @@ +"""Merge adjacent identically-formatted runs in a DOCX. + +Word fragments paragraph text across many elements (revision ids, +spell-check markers, editing history), which makes find-and-replace on +word/document.xml unreliable — the string you're looking for is split +across runs. This coalesces adjacent runs whose formatting () is +identical, strips rsid attributes and proofErr markers, and consolidates the +text elements — , and for text inside a tracked deletion. + +Rendering is unchanged. The text you search is what Word draws, which is not +always the bytes in the file: an element without xml:space="preserve" has its +edge whitespace trimmed before it reaches the page, so `Hello ` +followed by `world` reads "Helloworld" and merges to exactly that. + +Runs in two different / wrappers are never merged: that would +rewrite tracked-change structure, collapsing separate revisions into one. + +Only word/document.xml is processed (not headers, footers, or footnotes). + +Usage: + python merge_runs.py unpacked/ # after unzip, before editing + python merge_runs.py document.docx # rewrite in place + python merge_runs.py document.docx -o out.docx +""" + + +import argparse +import sys +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from office.helpers import XML_SPACE, rendered_text, rezip, safe_extract + +WORDML_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +def merge_runs(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + run_names = _run_tag_names(root) + + _remove_elements(root, "proofErr") + + runs = _find_runs(root, run_names) + _strip_rsid_attrs(runs) + + merge_count = 0 + for container in {run.parentNode for run in runs}: + merge_count += _merge_runs_in(container, run_names) + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Merged {merge_count} runs" + + except Exception as e: + return 0, f"Error: {e}" + + + + +def _is_element(node, tag: str) -> bool: + name = node.localName or node.tagName + return name == tag or name.endswith(f":{tag}") + + +def _run_tag_names(root) -> set[str]: + names = set() + for attr in root.attributes.values(): + if attr.value == WORDML_NS: + if attr.name == "xmlns": + names.add("r") + elif attr.name.startswith("xmlns:"): + names.add(attr.name.split(":", 1)[1] + ":r") + return names or {"w:r", "r"} + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + if _is_element(node, tag): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def _find_runs(root, run_names: set[str]) -> list: + return [e for e in _find_elements(root, "r") if _is_run(e, run_names)] + + +def _get_child(parent, tag: str): + return next(iter(_get_children(parent, tag)), None) + + +def _get_children(parent, tag: str) -> list: + return [ + child + for child in parent.childNodes + if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag) + ] + + +def _is_adjacent(elem1, elem2) -> bool: + node = elem1.nextSibling + while node: + if node == elem2: + return True + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(XML_SPACE): + return False + node = node.nextSibling + return False + + + + +def _remove_elements(root, tag: str): + for elem in _find_elements(root, tag): + if elem.parentNode: + elem.parentNode.removeChild(elem) + + +def _strip_rsid_attrs(runs: list): + for run in runs: + for attr in list(run.attributes.values()): + if "rsid" in attr.name.lower(): + run.removeAttribute(attr.name) + + + + +def _merge_runs_in(container, run_names: set[str]) -> int: + merge_count = 0 + run = _first_child_run(container, run_names) + + while run: + while True: + next_elem = _next_element_sibling(run) + if next_elem and _is_run(next_elem, run_names) and _can_merge(run, next_elem): + _merge_run_content(run, next_elem) + container.removeChild(next_elem) + merge_count += 1 + else: + break + + _consolidate_text(run) + run = _next_sibling_run(run, run_names) + + return merge_count + + +def _first_child_run(container, run_names: set[str]): + for child in container.childNodes: + if child.nodeType == child.ELEMENT_NODE and _is_run(child, run_names): + return child + return None + + +def _next_element_sibling(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + return sibling + sibling = sibling.nextSibling + return None + + +def _next_sibling_run(node, run_names: set[str]): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + if _is_run(sibling, run_names): + return sibling + sibling = sibling.nextSibling + return None + + +def _is_run(node, run_names: set[str]) -> bool: + return node.tagName in run_names + + +def _can_merge(run1, run2) -> bool: + rpr1 = _get_child(run1, "rPr") + rpr2 = _get_child(run2, "rPr") + + if (rpr1 is None) != (rpr2 is None): + return False + if rpr1 is None: + return True + return rpr1.toxml() == rpr2.toxml() + + +def _merge_run_content(target, source): + for child in list(source.childNodes): + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name != "rPr" and not name.endswith(":rPr"): + target.appendChild(child) + + +def _element_text(elem) -> str: + return "".join( + child.data + for child in elem.childNodes + if child.nodeType in (child.TEXT_NODE, child.CDATA_SECTION_NODE) + ) + + +def _has_preserve(elem) -> bool: + return elem.getAttribute("xml:space") == "preserve" + + +def _rendered_text(elem) -> str: + return rendered_text(_element_text(elem), _has_preserve(elem)) + + +def _consolidate_text(run): + for tag in ("t", "delText"): + _consolidate_text_elements(run, tag) + + +def _consolidate_text_elements(run, tag: str): + t_elements = _get_children(run, tag) + + for i in range(len(t_elements) - 1, 0, -1): + curr, prev = t_elements[i], t_elements[i - 1] + + if _is_adjacent(prev, curr): + merged = _rendered_text(prev) + _rendered_text(curr) + had_preserve = _has_preserve(prev) or _has_preserve(curr) + + new_text = run.ownerDocument.createTextNode(merged) + for node in list(prev.childNodes): + if node.nodeType in (node.TEXT_NODE, node.CDATA_SECTION_NODE): + prev.removeChild(node) + else: + run.insertBefore(node, curr) + prev.appendChild(new_text) + for node in list(curr.childNodes): + if node.nodeType not in (node.TEXT_NODE, node.CDATA_SECTION_NODE): + run.insertBefore(node, curr) + + if merged != merged.strip(XML_SPACE) or had_preserve: + prev.setAttribute("xml:space", "preserve") + elif prev.hasAttribute("xml:space"): + prev.removeAttribute("xml:space") + + run.removeChild(curr) + + + + +def _merge_or_die(path: Path) -> str: + _, msg = merge_runs(str(path)) + if msg.startswith("Error"): + print(msg, file=sys.stderr) + sys.exit(1) + return msg + + +def main() -> None: + p = argparse.ArgumentParser( + description="Merge adjacent identically-formatted runs in a DOCX (directory or .docx file)." + ) + p.add_argument("input", help="Unpacked DOCX directory OR a .docx/.dotx file") + p.add_argument( + "-o", "--output", + help="Output .docx path (only valid when input is a .docx; default: overwrite input)", + ) + args = p.parse_args() + + src = Path(args.input) + + try: + if src.is_dir(): + if args.output: + p.error("--output is only valid for .docx input; directory input is modified in place") + print(_merge_or_die(src)) + elif src.is_file() and src.suffix.lower() in (".docx", ".dotx"): + out = Path(args.output) if args.output else src + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + with zipfile.ZipFile(src) as zf: + safe_extract(zf, tmp_path) + msg = _merge_or_die(tmp_path) + rezip(tmp_path, out) + print(f"{msg}; wrote {out}") + else: + print(f"Error: {src} is neither a directory nor a .docx/.dotx file", file=sys.stderr) + sys.exit(1) + except (OSError, ValueError, zipfile.BadZipFile) as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/__init__.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/__init__.py new file mode 100644 index 0000000..2390e6e --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/__init__.py @@ -0,0 +1,112 @@ +import os +import posixpath +import re +import stat +import tempfile +import urllib.parse +import zipfile +from pathlib import Path + +OOXML_FAMILY = { + ".docx": "docx", + ".dotx": "docx", + ".pptx": "pptx", + ".potx": "pptx", + ".xlsx": "xlsx", + ".xltx": "xlsx", +} + +_SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:") + +SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" + + +def opc_target(target: str, source_part: str, target_mode: str = "") -> str | None: + if not target: + return None + if target_mode.lower() == "external": + return None + target = urllib.parse.unquote(target) + + if _SCHEME_RE.match(target): + return None + + if "\\" in target: + raise ValueError(f"relationship target is not a POSIX part name: {target!r}") + + if target.startswith("/"): + joined = target.lstrip("/") + else: + joined = posixpath.join(posixpath.dirname(source_part), target) + + parts: list[str] = [] + for segment in posixpath.normpath(joined).split("/"): + if segment in ("", "."): + continue + if segment == "..": + if not parts: + raise ValueError(f"relationship target escapes the package: {target!r}") + parts.pop() + else: + parts.append(segment) + + if not parts: + raise ValueError(f"relationship target resolves to nothing: {target!r}") + return "/".join(parts) + + +def rels_source_part(rels_file: Path, unpacked_dir: Path) -> str: + owner_dir = rels_file.parent.parent.relative_to(unpacked_dir) + part = posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]) + return part.removeprefix("./") + + +def part_text(data: bytes) -> str: + return data.decode("utf-8", "surrogateescape") + + +XML_SPACE = " \t\r\n" + + +def rendered_text(text: str, preserve: bool) -> str: + return text if preserve else text.strip(XML_SPACE) + + +def safe_extract(zf: zipfile.ZipFile, dest: Path) -> None: + dest = dest.resolve() + for m in zf.infolist(): + if stat.S_ISLNK(m.external_attr >> 16): + raise ValueError(f"symlink archive entry not allowed: {m.filename!r}") + target = (dest / m.filename).resolve() + if not target.is_relative_to(dest): + raise ValueError(f"unsafe archive entry: {m.filename!r}") + zf.extract(m, dest) + + +def rezip(src_dir: Path, out_path: Path) -> None: + files = sorted(p for p in src_dir.rglob("*") if p.is_file()) + ct = src_dir / "[Content_Types].xml" + fd, tmp_name = tempfile.mkstemp( + prefix=out_path.name + ".", suffix=".tmp", dir=out_path.parent + ) + tmp_out = Path(tmp_name) + try: + with os.fdopen(fd, "wb") as fh: + with zipfile.ZipFile(fh, "w", zipfile.ZIP_DEFLATED) as zf: + if ct.exists(): + zf.write(ct, ct.relative_to(src_dir), compress_type=zipfile.ZIP_STORED) + for f in files: + if f == ct: + continue + zf.write(f, f.relative_to(src_dir)) + if out_path.exists(): + mode = out_path.stat().st_mode & 0o777 + else: + umask = os.umask(0) + os.umask(umask) + mode = 0o666 & ~umask + os.chmod(tmp_out, mode) + os.replace(tmp_out, out_path) + finally: + if tmp_out.exists(): + tmp_out.unlink() diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_chart.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_chart.py new file mode 100644 index 0000000..209cb7c --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_chart.py @@ -0,0 +1,170 @@ +"""Find chart XML that PowerPoint refuses but the schema accepts. + +Detection only: for either fault more than one repair is valid, and only the +author knows which was meant. +""" + + +from __future__ import annotations + +import re +from typing import Mapping + +from . import part_text + + +_CHART_PART_RE = re.compile(r"ppt/charts/chart\d+\.xml") + +_GROUPING_RE = re.compile(r"""]*?\bval=["'](\w+)["']""") +_DLBL_POS_RE = re.compile(r"""]*?\bval=["'](\w+)["']""") + +def _strip_ext_lst(text: str) -> str: + out, cursor = [], 0 + for lo, hi in _ext_lst_spans(text): + out.append(text[cursor:lo]) + cursor = hi + out.append(text[cursor:]) + return "".join(out) + +_BAR_GROUP_RE = re.compile(r"]*(?.*?", re.DOTALL) + +STACKED_GROUPINGS = frozenset({"stacked", "percentStacked"}) +ILLEGAL_ON_STACKED = frozenset({"outEnd"}) +LEGAL_ON_STACKED = ("ctr", "inEnd", "inBase") + + +def _check_stacked_label_positions(part: str, xml: str) -> list[str]: + problems: list[str] = [] + for match in _BAR_GROUP_RE.finditer(xml): + block = _strip_ext_lst(match.group(0)) + group = match.group(1) + + grouping = _GROUPING_RE.search(block) + if grouping is None or grouping.group(1) not in STACKED_GROUPINGS: + continue + + bad = [p for p in _DLBL_POS_RE.findall(block) if p in ILLEGAL_ON_STACKED] + for pos in sorted(set(bad)): + problems.append( + f'{part}: {bad.count(pos)} data label(s) use dLblPos="{pos}" on a ' + f"{grouping.group(1)} {group}; PowerPoint allows only " + f"{', '.join(LEGAL_ON_STACKED)} there" + ) + return problems + + + +_ANY_CHART_GROUP_RE = re.compile(r"]*(?.*?", re.DOTALL) + +_AXID_RE = re.compile( + r"""\s*]*?\bval=["'](-?\d+)["']\s*(?:/>|>\s*)""" +) + +_AXIS_DECL_RE = re.compile( + r"""]*(?\s*]*?\bval=["'](-?\d+)["']""" +) + +AXID_LIMIT = { + "barChart": 2, "lineChart": 2, "areaChart": 2, "scatterChart": 2, + "bubbleChart": 2, "radarChart": 2, "stockChart": 2, + "bar3DChart": 3, "line3DChart": 3, "area3DChart": 3, + "surfaceChart": 3, "surface3DChart": 3, +} + +AXID_MINIMUM = { + "barChart": 2, "lineChart": 2, "areaChart": 2, "scatterChart": 2, + "bubbleChart": 2, "radarChart": 2, "stockChart": 2, + "bar3DChart": 2, "area3DChart": 2, "surfaceChart": 2, + "line3DChart": 3, "surface3DChart": 3, +} + + +def _declared_axes(xml: str) -> dict[str, list[str]]: + axes: dict[str, list[str]] = {} + for kind, axid in _AXIS_DECL_RE.findall(xml): + axes.setdefault(kind, []).append(axid) + return axes + + +def _canonical_ids(axes: dict[str, list[str]], limit: int) -> list[str] | None: + category = axes.get("catAx", []) + axes.get("dateAx", []) + value = axes.get("valAx", []) + series = axes.get("serAx", []) + if len(category) != 1 or len(value) != 1 or len(series) > 1: + return None + ids = [category[0], value[0]] + if limit >= 3 and series: + ids.append(series[0]) + return ids + + +def _undeclared_axes(kind: str, block: str, axes: dict[str, list[str]]) -> list[str] | None: + if kind not in AXID_LIMIT: + return None + ids = _AXID_RE.findall(block) + declared = {i for group in axes.values() for i in group} + if len([i for i in ids if i in declared]) >= 2: + return None + return ids + + +def _check_chart_axis_references(part: str, xml: str) -> list[str]: + axes = _declared_axes(xml) + problems: list[str] = [] + declared = {i for group in axes.values() for i in group} + for match in _ANY_CHART_GROUP_RE.finditer(xml): + kind, block = match.group(1), match.group(0) + ids = _undeclared_axes(kind, block, axes) + if ids is None: + continue + if not ids: + problems.append( + f"{part}: declares no this part can resolve; a chart " + f"group needs {AXID_MINIMUM[kind]}, and PowerPoint discards one with fewer" + ) + continue + dead = [i for i in ids if i not in declared] + canonical = _canonical_ids(axes, AXID_LIMIT[kind]) + if canonical is not None and len(canonical) >= AXID_MINIMUM[kind]: + hint = f"Fix: point them at the axes this part declares ({', '.join(canonical)})" + else: + hint = ("Fix: the part declares several axes of a kind -- declare the " + "secondary axes the series expects, or drop them") + detail = (f"of which {', '.join(dead)} name no declared axis" + if dead else f"only {len(ids)} of which this part declares") + problems.append( + f"{part}: references axId {', '.join(ids)}, {detail}, " + f"leaving fewer than two live axes; PowerPoint discards the chart. {hint}" + ) + return problems + + +def _ext_lst_spans(text: str) -> list[tuple[int, int]]: + spans: list[tuple[int, int]] = [] + depth = 0 + start = 0 + for match in re.finditer(r"<(/?)c:extLst\b[^>]*?(/?)>", text): + closing, self_closing = match.group(1), match.group(2) + if self_closing: + continue + if closing: + depth -= 1 + if depth == 0: + spans.append((start, match.end())) + else: + if depth == 0: + start = match.start() + depth += 1 + return spans + + +CHART_CHECKS = (_check_stacked_label_positions, _check_chart_axis_references) + + +def find_chart_problems(files: Mapping[str, bytes]) -> list[str]: + problems: list[str] = [] + for part in sorted(n for n in files if _CHART_PART_RE.fullmatch(n)): + xml = part_text(files[part]) + for check in CHART_CHECKS: + problems.extend(check(part, xml)) + return problems diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_slide.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_slide.py new file mode 100644 index 0000000..22f9aee --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_slide.py @@ -0,0 +1,60 @@ +"""Pick the slide-XML schema errors PowerPoint refuses the file over. + +A denylist over lxml's messages, so an unrecognised error class is a miss rather +than a false alarm. +""" + + +from __future__ import annotations + +import re + +SLIDE_PART_RE = re.compile( + r"ppt/(slides|slideLayouts|slideMasters|notesSlides|notesMasters|handoutMasters)" + r"/[^/]+\.xml" +) + +FATAL_SLIDE_ERRORS: tuple[tuple[re.Pattern[str], str], ...] = ( + ( + re.compile(r"\}tableStyleId': This element is not expected"), + "two in one (the schema allows one)", + ), + ( + re.compile(r"\}srgbClr', attribute 'val'"), + "a colour that is not six hex digits", + ), + ( + re.compile(r"\}txBody': Missing child element"), + "a with no children", + ), + ( + re.compile(r"\}miter', attribute 'lim'"), + 'a line join with lim="NaN"', + ), + ( + re.compile(r"\}uLnTx': This element is not expected"), + " in a position the schema forbids", + ), + ( + re.compile(r"\}overrideClrMapping': This element is not expected"), + " in a position the schema forbids", + ), + ( + re.compile(r"\}nvGrpSpPr': Missing child element"), + "a with no children", + ), +) + + +def is_schema_verdict(error: str) -> bool: + return error.startswith("Element ") + + +def fatal_slide_errors(errors: set[str]) -> list[str]: + out = [] + for error in sorted(errors): + for pattern, meaning in FATAL_SLIDE_ERRORS: + if pattern.search(error): + out.append(f"{meaning}: {error}") + break + return out diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_theme.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_theme.py new file mode 100644 index 0000000..8446620 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/helpers/pptx_theme.py @@ -0,0 +1,114 @@ +"""Find masters sharing a theme part in the way PowerPoint refuses to open. + +Reports only; the fix is to move back to directly after + in ppt/presentation.xml. +""" + + +from __future__ import annotations + +import posixpath +import re +from typing import Mapping + +from . import part_text + +THEME_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" + +_MASTER_RE = re.compile( + r"^ppt/(?PslideMasters|notesMasters|handoutMasters)/" + r"(?:slide|notes|handout)Master(?P\d+)\.xml$" +) +_GROUP_ORDER = {"slideMasters": 0, "notesMasters": 1, "handoutMasters": 2} + +_RELATIONSHIP_RE = re.compile( + r"]*?(?:/>|>.*?)", re.DOTALL +) + + +def _sort_key(name: str) -> tuple[int, int]: + m = _MASTER_RE.match(name) + assert m is not None + return (_GROUP_ORDER[m.group("group")], int(m.group("num"))) + + +def _rels_path(part: str) -> str: + directory, base = posixpath.split(part) + return f"{directory}/_rels/{base}.rels" + + +def _resolve(rels_path: str, target: str) -> str: + if target.startswith("/"): + return target.lstrip("/") + part_dir = posixpath.dirname(posixpath.dirname(rels_path)) + return posixpath.normpath(posixpath.join(part_dir, target)) + + +def _theme_rel(files: Mapping[str, bytes], master: str): + rels_path = _rels_path(master) + rels = files.get(rels_path) + if rels is None: + return None + for element in _RELATIONSHIP_RE.findall(part_text(rels)): + if f'Type="{THEME_REL_TYPE}"' not in element: + continue + target = re.search(r'\bTarget="([^"]+)"', element) + if target is None: + continue + return rels_path, element, _resolve(rels_path, target.group(1)) + return None + + +def _masters(files: Mapping[str, bytes]) -> list[str]: + return sorted((n for n in files if _MASTER_RE.match(n)), key=_sort_key) + + +_PRESENTATION = "ppt/presentation.xml" +_NOTES_MASTERS = "ppt/notesMasters/" +_IGNORABLE_RE = re.compile(r"|<\?.*?\?>", re.DOTALL) +_AFTER_SLDIDLST_RE = re.compile( + r"]*/>|[^>]*>.*?)\s*(<[^>\s/]+)", re.DOTALL +) + + +def _notes_master_share_is_inert(files: Mapping[str, bytes]) -> bool: + data = files.get(_PRESENTATION) + if data is None: + return False + match = _AFTER_SLDIDLST_RE.search(_IGNORABLE_RE.sub("", part_text(data))) + return match is not None and match.group(1) == " bool: + return inert_notes and master.startswith(_NOTES_MASTERS) + + +def find_shared_master_themes(files: Mapping[str, bytes]) -> list[str]: + return [ + f"{master} shares {theme} with {first}" + for master, _, _, theme, first in _shares(files) + ] + + +def live_shared_master_themes(files: Mapping[str, bytes]) -> list[str]: + inert_notes = _notes_master_share_is_inert(files) + return [ + f"{master} shares {theme} with {first}" + for master, _, _, theme, first in _shares(files) + if not _is_inert(master, inert_notes) + ] diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 0000000..6454ef9 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 0000000..afa4f46 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 0000000..64e66b8 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 0000000..687eea8 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 0000000..6ac81b0 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 0000000..1dbf051 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 0000000..f1af17d --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 0000000..0a185ab --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 0000000..14ef488 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 0000000..c20f3bf --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 0000000..ac60252 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 0000000..424b8ba --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 0000000..2bddce2 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 0000000..8a8c18b --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 0000000..5c42706 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 0000000..853c341 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 0000000..da835ee --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 0000000..87ad265 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 0000000..9e86f1b --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 0000000..d0be42e --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 0000000..8821dd1 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 0000000..ca2575c --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 0000000..dd079e6 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 0000000..3dd6cf6 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 0000000..f1041e3 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 0000000..9c5b7a6 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 0000000..0f13678 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 0000000..a6de9d2 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 0000000..10e978b --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 0000000..4248bf7 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 0000000..5649746 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/mce/mc.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/mce/mc.xsd new file mode 100644 index 0000000..ef72545 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2010.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2010.xsd new file mode 100644 index 0000000..f65f777 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2012.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2012.xsd new file mode 100644 index 0000000..6b00755 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2018.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2018.xsd new file mode 100644 index 0000000..f321d33 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 0000000..364c6a9 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 0000000..fed9d15 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 0000000..680cf15 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 0000000..89ada90 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/soffice.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/soffice.py new file mode 100644 index 0000000..eec1687 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/soffice.py @@ -0,0 +1,192 @@ +""" +Helper for running LibreOffice (soffice) in environments where AF_UNIX +sockets may be blocked (e.g., sandboxed VMs). Detects the restriction +at runtime and applies an LD_PRELOAD shim if needed. + +Usage: + from office.soffice import run_soffice + + result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"]) + +Call soffice through run_soffice, not through subprocess with get_soffice_env(): +the env dict carries the shim but names no user profile, and a non-root sandbox +cannot bootstrap the default one -- soffice aborts with "User installation could +not be completed" and converts nothing. get_soffice_env() stays public for the +callers that build their own argv (they must pass -env:UserInstallation too). +""" + +import contextlib +import os +import socket +import subprocess +import tempfile +from collections.abc import Iterable +from pathlib import Path + + +def get_soffice_env() -> dict: + env = os.environ.copy() + env["SAL_USE_VCLPLUGIN"] = "svp" + + if _needs_shim(): + shim = _ensure_shim() + env["LD_PRELOAD"] = str(shim) + + return env + + +def run_soffice(args: Iterable[str], **kwargs) -> subprocess.CompletedProcess: + args = list(args) + with contextlib.ExitStack() as stack: + if not any(str(a).startswith("-env:UserInstallation") for a in args): + profile = stack.enter_context( + tempfile.TemporaryDirectory(prefix="lo_profile_", ignore_cleanup_errors=True) + ) + args = [f"-env:UserInstallation={Path(profile).as_uri()}"] + args + return subprocess.run(["soffice"] + args, env=get_soffice_env(), **kwargs) + + + +_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so" + + +def _needs_shim() -> bool: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.close() + return False + except OSError: + return True + + +def _ensure_shim() -> Path: + if _SHIM_SO.exists(): + return _SHIM_SO + + src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" + src.write_text(_SHIM_SOURCE, encoding="utf-8") + subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], + check=True, + capture_output=True, + ) + src.unlink() + return _SHIM_SO + + + +_SHIM_SOURCE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +static int (*real_socket)(int, int, int); +static int (*real_socketpair)(int, int, int, int[2]); +static int (*real_listen)(int, int); +static int (*real_accept)(int, struct sockaddr *, socklen_t *); +static int (*real_close)(int); +static int (*real_read)(int, void *, size_t); + +/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */ +static int is_shimmed[1024]; +static int peer_of[1024]; +static int wake_r[1024]; /* accept() blocks reading this */ +static int wake_w[1024]; /* close() writes to this */ +static int listener_fd = -1; /* FD that received listen() */ + +__attribute__((constructor)) +static void init(void) { + real_socket = dlsym(RTLD_NEXT, "socket"); + real_socketpair = dlsym(RTLD_NEXT, "socketpair"); + real_listen = dlsym(RTLD_NEXT, "listen"); + real_accept = dlsym(RTLD_NEXT, "accept"); + real_close = dlsym(RTLD_NEXT, "close"); + real_read = dlsym(RTLD_NEXT, "read"); + for (int i = 0; i < 1024; i++) { + peer_of[i] = -1; + wake_r[i] = -1; + wake_w[i] = -1; + } +} + +/* ---- socket ---------------------------------------------------------- */ +int socket(int domain, int type, int protocol) { + if (domain == AF_UNIX) { + int fd = real_socket(domain, type, protocol); + if (fd >= 0) return fd; + /* socket(AF_UNIX) blocked – fall back to socketpair(). */ + int sv[2]; + if (real_socketpair(domain, type, protocol, sv) == 0) { + if (sv[0] >= 0 && sv[0] < 1024) { + is_shimmed[sv[0]] = 1; + peer_of[sv[0]] = sv[1]; + int wp[2]; + if (pipe(wp) == 0) { + wake_r[sv[0]] = wp[0]; + wake_w[sv[0]] = wp[1]; + } + } + return sv[0]; + } + errno = EPERM; + return -1; + } + return real_socket(domain, type, protocol); +} + +/* ---- listen ---------------------------------------------------------- */ +int listen(int sockfd, int backlog) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + listener_fd = sockfd; + return 0; + } + return real_listen(sockfd, backlog); +} + +/* ---- accept ---------------------------------------------------------- */ +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + /* Block until close() writes to the wake pipe. */ + if (wake_r[sockfd] >= 0) { + char buf; + real_read(wake_r[sockfd], &buf, 1); + } + errno = ECONNABORTED; + return -1; + } + return real_accept(sockfd, addr, addrlen); +} + +/* ---- close ----------------------------------------------------------- */ +int close(int fd) { + if (fd >= 0 && fd < 1024 && is_shimmed[fd]) { + int was_listener = (fd == listener_fd); + is_shimmed[fd] = 0; + + if (wake_w[fd] >= 0) { /* unblock accept() */ + char c = 0; + write(wake_w[fd], &c, 1); + real_close(wake_w[fd]); + wake_w[fd] = -1; + } + if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; } + if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; } + + if (was_listener) + _exit(0); /* conversion done – exit */ + } + return real_close(fd); +} +""" + + + +if __name__ == "__main__": + import sys + result = run_soffice(sys.argv[1:]) + sys.exit(result.returncode) diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validate.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validate.py new file mode 100755 index 0000000..29ca186 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validate.py @@ -0,0 +1,173 @@ +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py [--original ] [--auto-repair] [--author NAME] + +The first argument can be either: +- An unpacked directory containing the Office document XML files +- A packed Office file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx template) which will be unpacked to a temp directory + +Auto-repair fixes: +- paraId/durableId values that exceed OOXML limits +- Missing xml:space="preserve" on w:t elements with whitespace +""" + +import argparse +import sys +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.ElementTree as ET +from defusedxml.common import DefusedXmlException + +from helpers import OOXML_FAMILY, rezip, safe_extract +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + +WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +def _fail(message: str): + print(f"Error: {message}", file=sys.stderr) + sys.exit(2) + + +def _has_tracked_changes(unpacked_dir: Path) -> bool: + document = unpacked_dir / "word" / "document.xml" + if not document.is_file(): + return False + try: + root = ET.parse(document).getroot() + except (ET.ParseError, DefusedXmlException): + return False + tracked = {f"{{{WORD_NS}}}ins", f"{{{WORD_NS}}}del"} + return any(elem.tag in tracked for elem in root.iter()) + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "path", + help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx)", + ) + parser.add_argument( + "--original", + required=False, + default=None, + help="Path to original file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx). If omitted, all XSD errors are reported and redlining validation is skipped.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--auto-repair", + action="store_true", + help="Automatically repair common issues (hex IDs, whitespace preservation). " + "Modifies the input in place: repairs to a packed file are written back to it.", + ) + parser.add_argument( + "--author", + default=None, + help="The name you are redlining under. Passing it turns on the " + "tracked-change check: any text differing from --original without a " + "/ recording it is reported. Untracked edits carry no " + "author, so the check covers them whoever made them — the name marks " + "the run as redlining work and is not used to filter. Requires " + "--original; docx only.", + ) + args = parser.parse_args() + + if args.author is not None and not args.original: + _fail("--author requires --original") + + path = Path(args.path) + if not path.exists(): + _fail(f"{path} does not exist") + + original_file = None + if args.original: + original_file = Path(args.original) + if not original_file.is_file(): + _fail(f"{original_file} is not a file") + if original_file.suffix.lower() not in OOXML_FAMILY: + _fail(f"{original_file} must be one of: {', '.join(sorted(OOXML_FAMILY))}") + + family = OOXML_FAMILY.get((original_file or path).suffix.lower()) + if family is None: + _fail( + f"Cannot determine file type from {path}. Use --original or provide one of: {', '.join(sorted(OOXML_FAMILY))}." + ) + + if args.author is not None and family != "docx": + _fail(f"--author only applies to docx files, not {family}") + + packed_file = None + temp_dir_ctx = None + if path.is_file() and path.suffix.lower() in OOXML_FAMILY: + packed_file = path + temp_dir_ctx = tempfile.TemporaryDirectory() + unpacked_dir = Path(temp_dir_ctx.name) + try: + with zipfile.ZipFile(path, "r") as zf: + safe_extract(zf, unpacked_dir) + except (zipfile.BadZipFile, ValueError, OSError) as e: + _fail(f"cannot unpack {path}: {e}") + else: + if not path.is_dir(): + _fail(f"{path} is not a directory or Office file") + unpacked_dir = path + + match family: + case "docx": + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + if args.author is not None: + validators.append( + RedliningValidator(unpacked_dir, original_file, verbose=args.verbose) + ) + elif original_file and _has_tracked_changes(unpacked_dir): + print( + "Note: this document has tracked changes; they were not " + "checked against the original (pass --author to check)." + ) + case "pptx": + validators = [ + PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + case "xlsx": + exts = ", ".join(k for k, v in sorted(OOXML_FAMILY.items()) if v == "xlsx") + print( + f"No XSD schema validation is performed for xlsx-family files ({exts}). " + "For formula-error checking, use scripts/recalc.py instead." + ) + sys.exit(0) + case _: + print(f"Error: Validation not supported for file type {family}") + sys.exit(1) + + if args.auto_repair: + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + print(f"Auto-repaired {total_repairs} issue(s)") + if packed_file is not None: + rezip(unpacked_dir, packed_file) + print(f"Wrote repaired file to {packed_file}") + + success = all([v.validate() for v in validators]) + + if temp_dir_ctx is not None: + temp_dir_ctx.cleanup() + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/__init__.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/__init__.py new file mode 100644 index 0000000..db092ec --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/base.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/base.py new file mode 100644 index 0000000..e27ba58 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/base.py @@ -0,0 +1,874 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import defusedxml.minidom +from functools import lru_cache + +import lxml.etree + +from helpers import safe_extract + + +@lru_cache(maxsize=None) +def _load_schema(schema_path: str): + with open(schema_path, "rb") as xsd_file: + xsd_doc = lxml.etree.parse( + xsd_file, parser=lxml.etree.XMLParser(), base_url=schema_path + ) + return lxml.etree.XMLSchema(xsd_doc) + +class BaseSchemaValidator: + + IGNORED_VALIDATION_ERRORS = [ + "hyphenationZone", + "purl.org/dc/terms", + ] + + UNIQUE_ID_REQUIREMENTS = { + "comment": ("id", "file"), + "commentrangestart": ("id", "file"), + "commentrangeend": ("id", "file"), + "bookmarkstart": ("id", "file"), + "bookmarkend": ("id", "file"), + "sldid": ("id", "file"), + "sldmasterid": ("id", "global"), + "sldlayoutid": ("id", "global"), + "cm": ("authorid", "file"), + "sheet": ("sheetid", "file"), + "definedname": ("id", "file"), + "cxnsp": ("id", "file"), + "sp": ("id", "file"), + "pic": ("id", "file"), + "grpsp": ("id", "file"), + } + + EXCLUDED_ID_CONTAINERS = { + "sectionlst", + } + + ELEMENT_RELATIONSHIP_TYPES = {} + + SCHEMA_MAPPINGS = { + "word": "ISO-IEC29500-4_2016/wml.xsd", + "ppt": "ISO-IEC29500-4_2016/pml.xsd", + "xl": "ISO-IEC29500-4_2016/sml.xsd", + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file=None, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) if original_file else None + self.verbose = verbose + + self.schemas_dir = Path(__file__).parent.parent / "schemas" + + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + raise NotImplementedError("Subclasses must implement the validate method") + + def repair(self) -> int: + return self.repair_whitespace_preservation() + + def repair_whitespace_preservation(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + pending = [] + + for elem in dom.getElementsByTagName("*"): + local_name = elem.tagName.rsplit(":", 1)[-1] + if local_name in ("t", "delText", "instrText", "delInstrText"): + text = "".join( + child.data + for child in elem.childNodes + if child.nodeType in (child.TEXT_NODE, child.CDATA_SECTION_NODE) + ) + ws = (" ", "\t", "\n", "\r") + if text and (text.startswith(ws) or text.endswith(ws)): + if elem.getAttribute("xml:space") != "preserve": + elem.setAttribute("xml:space", "preserve") + text_preview = repr(text[:30]) + "..." if len(text) > 30 else repr(text) + pending.append(f" Repaired: {xml_file.name}: Added xml:space='preserve' to {elem.tagName}: {text_preview}") + + if pending: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + for message in pending: + print(message) + repairs += len(pending) + + except Exception: + pass + + return repairs + + def validate_xml(self): + errors = [] + + for xml_file in self.xml_files: + try: + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + errors = [] + global_ids = {} + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} + + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + for elem in root.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + if tag in self.UNIQUE_ID_REQUIREMENTS: + in_excluded_container = any( + ancestor.tag.split("}")[-1].lower() in self.EXCLUDED_ID_CONTAINERS + for ancestor in elem.iterancestors() + ) + if in_excluded_container: + continue + + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + errors = [] + + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): + all_files.append(file_path.resolve()) + + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + for rels_file in rels_files: + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + rels_dir = rels_file.parent + + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if rel.get("TargetMode") == "External": + continue + if target and not target.startswith( + ("http", "mailto:") + ): + if target.startswith("/"): + target_path = self.unpacked_dir / target.lstrip("/") + elif rels_file.name == ".rels": + target_path = self.unpacked_dir / target + else: + base_dir = rels_dir.parent + target_path = base_dir / target + + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + import lxml.etree + + errors = [] + + for xml_file in self.xml_files: + if xml_file.suffix == ".rels": + continue + + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + if not rels_file.exists(): + continue + + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + r_ns = self.OFFICE_RELATIONSHIPS_NAMESPACE + rid_attrs_to_check = ["id", "embed", "link"] + for elem in xml_root.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + for attr_name in rid_attrs_to_check: + rid_attr = elem.get(f"{{{r_ns}}}{attr_name}") + if not rid_attr: + continue + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> r:{attr_name} references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + elif attr_name == "id" and self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + elem_lower = element_name.lower() + + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + if elem_lower.endswith("id") and len(elem_lower) > 2: + prefix = elem_lower[:-2] + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + if prefix == "sld": + return "slide" + return prefix.lower() + + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] + return prefix.lower() + + return None + + def validate_content_types(self): + errors = [] + + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", + "document", + "workbook", + "worksheet", + "theme", + } + + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue + + for file_path in all_files: + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() + elif is_valid: + return True, set() + + original_errors = self._get_original_file_errors(xml_file) + + assert current_errors is not None + new_errors = current_errors - original_errors + + new_errors = { + e for e in new_errors + if not any(pattern in e for pattern in self.IGNORED_VALIDATION_ERRORS) + } + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + original_error_count += 1 + valid_count += 1 + continue + + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + for attr in attrs_to_remove: + del elem.attrib[attr] + + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + elements_to_remove = [] + + for elem in list(root): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + self._remove_ignorable_elements(elem) + + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + root = xml_doc.getroot() + + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _preprocess_for_schema(self, xml_doc, relative_path): + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path, schema_path=None): + schema_path = schema_path or self._get_schema_path(xml_file) + if not schema_path: + return None, None + + try: + schema = _load_schema(str(schema_path)) + + xml_doc = lxml.etree.parse(str(xml_file)) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + xml_doc = self._preprocess_for_schema(xml_doc, relative_path) + + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file, schema_path=None): + if self.original_file is None: + return set() + + import tempfile + import zipfile + + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + try: + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + safe_extract(zip_ref, temp_path) + except (zipfile.BadZipFile, ValueError, OSError): + return set() + + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + return set() + + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path, schema_path=schema_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + for elem in xml_copy.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/docx.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/docx.py new file mode 100644 index 0000000..b181499 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/docx.py @@ -0,0 +1,466 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import random +import re +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom +import lxml.etree + +from helpers import safe_extract + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml" + W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid" + + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_whitespace_preservation(): + all_valid = False + + if not self.validate_deletions(): + all_valid = False + + if not self.validate_insertions(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_id_constraints(): + all_valid = False + + if not self.validate_comment_markers(): + all_valid = False + + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + if re.search(r"^[ \t\n\r]", text) or re.search( + r"[ \t\n\r]$", text + ): + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + for t_elem in root.xpath(".//w:del//w:t", namespaces=namespaces): + if t_elem.text: + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + for instr_elem in root.xpath( + ".//w:del//w:instrText", namespaces=namespaces + ): + text_preview = ( + repr(instr_elem.text or "")[:50] + "..." + if len(repr(instr_elem.text or "")) > 50 + else repr(instr_elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {instr_elem.sourceline}: found within (use ): {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + count = 0 + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + original = self.original_file + if original is None: + return 0 + + count = 0 + + try: + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(original, "r") as zip_ref: + safe_extract(zip_ref, Path(temp_dir)) + + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + new_count = self.count_paragraphs_in_unpacked() + if self.original_file is None: + print(f"\nParagraphs: {new_count}") + return + + original_count = self.count_paragraphs_in_original() + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})") + + def _parse_id_value(self, val: str, base: int = 16) -> int: + return int(val, base) + + def validate_id_constraints(self): + errors = [] + para_id_attr = f"{{{self.W14_NAMESPACE}}}paraId" + durable_id_attr = f"{{{self.W16CID_NAMESPACE}}}durableId" + + for xml_file in self.xml_files: + try: + for elem in lxml.etree.parse(str(xml_file)).iter(): + if val := elem.get(para_id_attr): + try: + if self._parse_id_value(val, base=16) >= 0x80000000: + errors.append( + f" {xml_file.name}:{elem.sourceline}: paraId={val} >= 0x80000000" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"paraId={val} is not valid hex" + ) + + if val := elem.get(durable_id_attr): + if xml_file.name == "numbering.xml": + try: + if self._parse_id_value(val, base=10) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} must be decimal in numbering.xml" + ) + else: + try: + if self._parse_id_value(val, base=16) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} is not valid hex" + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} ID constraint violations:") + for e in errors: + print(e) + elif self.verbose: + print("PASSED - All paraId/durableId values within constraints") + return not errors + + def validate_comment_markers(self): + errors = [] + + document_xml = None + comments_xml = None + for xml_file in self.xml_files: + if xml_file.name == "document.xml" and "word" in str(xml_file): + document_xml = xml_file + elif xml_file.name == "comments.xml": + comments_xml = xml_file + + if not document_xml: + if self.verbose: + print("PASSED - No document.xml found (skipping comment validation)") + return True + + try: + doc_root = lxml.etree.parse(str(document_xml)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + range_starts = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeStart", namespaces=namespaces + ) + } + range_ends = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeEnd", namespaces=namespaces + ) + } + references = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentReference", namespaces=namespaces + ) + } + + orphaned_ends = range_ends - range_starts + for comment_id in sorted( + orphaned_ends, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeEnd id="{comment_id}" has no matching commentRangeStart' + ) + + orphaned_starts = range_starts - range_ends + for comment_id in sorted( + orphaned_starts, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeStart id="{comment_id}" has no matching commentRangeEnd' + ) + + comment_ids = set() + if comments_xml and comments_xml.exists(): + comments_root = lxml.etree.parse(str(comments_xml)).getroot() + comment_ids = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in comments_root.xpath( + ".//w:comment", namespaces=namespaces + ) + } + + marker_ids = range_starts | range_ends | references + invalid_refs = marker_ids - comment_ids + for comment_id in sorted( + invalid_refs, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + if comment_id: + errors.append( + f' document.xml: marker id="{comment_id}" references non-existent comment' + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append(f" Error parsing XML: {e}") + + if errors: + print(f"FAILED - {len(errors)} comment marker violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All comment markers properly paired") + return True + + def repair(self) -> int: + repairs = super().repair() + repairs += self.repair_durableId() + return repairs + + def repair_durableId(self) -> int: + DURABLE_ID_ATTRS = ("w16cid:durableId", "w16cex:durableId") + repairs = 0 + renames: dict = {} + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + is_numbering = xml_file.name == "numbering.xml" + base = 10 if is_numbering else 16 + pending = [] + seen_in_file = set() + modified = False + + for elem in dom.getElementsByTagName("*"): + for attr_name in DURABLE_ID_ATTRS: + if not elem.hasAttribute(attr_name): + continue + + durable_id = elem.getAttribute(attr_name) + try: + key = self._parse_id_value(durable_id, base=base) + needs_repair = key >= 0x7FFFFFFF + except ValueError: + key = durable_id + needs_repair = True + + if needs_repair: + if key in seen_in_file: + value = random.randint(1, 0x7FFFFFFE) + else: + seen_in_file.add(key) + if key not in renames: + renames[key] = random.randint(1, 0x7FFFFFFE) + value = renames[key] + new_id = str(value) if is_numbering else f"{value:08X}" + + elem.setAttribute(attr_name, new_id) + pending.append( + f" Repaired: {xml_file.name}: durableId {durable_id} → {new_id}" + ) + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + for message in pending: + print(message) + repairs += len(pending) + + except Exception: + pass + + return repairs + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/pptx.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/pptx.py new file mode 100644 index 0000000..318f0e6 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/pptx.py @@ -0,0 +1,441 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re +from pathlib import Path + +from helpers import opc_target, rels_source_part, safe_extract + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_uuid_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_slide_layout_ids(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_notes_slide_references(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + if not self.validate_master_theme_uniqueness(): + all_valid = False + + if not self.validate_charts(): + all_valid = False + + if not self.validate_slides(): + all_valid = False + + return all_valid + + def _package_map(self) -> dict: + wanted = [] + wanted += list(self.unpacked_dir.glob("[[]Content_Types[]].xml")) + wanted += list(self.unpacked_dir.glob("ppt/presentation.xml")) + wanted += list(self.unpacked_dir.glob("ppt/theme/*.xml")) + wanted += list(self.unpacked_dir.glob("ppt/theme/_rels/*.rels")) + wanted += list(self.unpacked_dir.glob("ppt/charts/chart*.xml")) + for group in ("slideMasters", "notesMasters", "handoutMasters"): + wanted += list(self.unpacked_dir.glob(f"ppt/{group}/*.xml")) + wanted += list(self.unpacked_dir.glob(f"ppt/{group}/_rels/*.rels")) + return { + p.relative_to(self.unpacked_dir).as_posix(): p.read_bytes() + for p in wanted + if p.is_file() + } + + def validate_master_theme_uniqueness(self): + from helpers.pptx_theme import _NOTES_MASTERS, live_shared_master_themes + + shared = live_shared_master_themes(self._package_map()) + if shared: + print(f"FAILED - Found {len(shared)} master(s) sharing a theme part:") + for message in shared: + print(f" {message}") + if any(m.startswith(_NOTES_MASTERS) for m in shared): + print(" Fix: in ppt/presentation.xml, move back to " + "directly after . PowerPoint reads that happily.") + else: + print(" Fix: give each master its own theme part.") + return False + + if self.verbose: + print("PASSED - No master shares a theme part in a way PowerPoint refuses") + return True + + def validate_charts(self): + from helpers.pptx_chart import find_chart_problems + + problems = find_chart_problems(self._package_map()) + if problems: + print(f"FAILED - Found {len(problems)} chart problem(s) PowerPoint rejects:") + for message in problems: + print(f" {message}") + return False + + if self.verbose: + print("PASSED - Charts satisfy the constraints PowerPoint enforces") + return True + + def _original_slide_defects(self, schema) -> set[str]: + import tempfile + import zipfile + + from helpers.pptx_slide import SLIDE_PART_RE, fatal_slide_errors + + if self.original_file is None: + return set() + + found: set[str] = set() + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + try: + with zipfile.ZipFile(self.original_file, "r") as zf: + safe_extract(zf, temp_path) + except (zipfile.BadZipFile, ValueError, OSError): + return set() + + for part in sorted(temp_path.rglob("*.xml")): + relative = part.relative_to(temp_path).as_posix() + if not SLIDE_PART_RE.fullmatch(relative): + continue + ok, errors = self._validate_single_file_xsd( + part.resolve(), temp_path.resolve(), schema_path=schema + ) + if ok is None or ok or not errors: + continue + found |= set(fatal_slide_errors(set(errors))) + return found + + def validate_slides(self): + from helpers.pptx_slide import ( + SLIDE_PART_RE, + fatal_slide_errors, + is_schema_verdict, + ) + + schema = self.schemas_dir / self.SCHEMA_MAPPINGS["ppt"] + inherited = self._original_slide_defects(schema) + problems: list[str] = [] + broken: list[str] = [] + + for xml_file in self.xml_files: + relative = xml_file.relative_to(self.unpacked_dir).as_posix() + if not SLIDE_PART_RE.fullmatch(relative): + continue + ok, errors = self._validate_single_file_xsd( + xml_file.resolve(), self.unpacked_dir.resolve(), schema_path=schema + ) + if ok is None or not errors: + continue + + unreadable = [f"{relative}: {e}" for e in errors if not is_schema_verdict(e)] + if unreadable: + broken.extend(unreadable) + continue + if ok: + continue + + for message in fatal_slide_errors(set(errors)): + if message in inherited: + continue + problems.append(f"{relative}: {message}") + + if broken: + print(f"FAILED - Could not check {len(broken)} slide part(s):") + for message in sorted(broken): + print(f" {message[:240]}") + + if problems: + print(f"FAILED - Found {len(problems)} slide problem(s) PowerPoint rejects:") + for message in sorted(problems): + print(f" {message[:240]}") + + if broken or problems: + return False + + if self.verbose: + print("PASSED - Slide XML has none of the defects PowerPoint refuses") + return True + + def _get_schema_path(self, xml_file): + if xml_file.parent.name == "charts" and xml_file.name.startswith("chart"): + return None + return super()._get_schema_path(xml_file) + + def _preprocess_for_schema(self, xml_doc, relative_path): + if relative_path.as_posix() != "ppt/presentation.xml": + return xml_doc + + root = xml_doc.getroot() + ns = f"{{{self.PRESENTATIONML_NAMESPACE}}}" + notes = root.find(f"{ns}notesMasterIdLst") + slides = root.find(f"{ns}sldIdLst") + if notes is None or slides is None: + return xml_doc + + children = list(root) + if children.index(notes) < children.index(slides): + return xml_doc + + root.remove(notes) + root.insert(list(root).index(slides), notes) + return xml_doc + + def validate_uuid_ids(self): + import lxml.etree + + errors = [] + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(): + for attr, value in elem.attrib.items(): + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + if self._looks_like_uuid(value): + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + clean_value = value.strip("{}()").replace("-", "") + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + import lxml.etree + + errors = [] + + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + root = lxml.etree.parse(str(slide_master)).getroot() + + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + import lxml.etree + + errors = [] + notes_slide_references = {} + + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + part = opc_target( + rel.get("Target", ""), + rels_source_part(rels_file, self.unpacked_dir), + rel.get("TargetMode", ""), + ) + if part: + slide_name = rels_file.stem.replace( + ".xml", "" + ) + + notes_slide_references.setdefault(part, []).append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/redlining.py b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/redlining.py new file mode 100644 index 0000000..50df615 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/office/validators/redlining.py @@ -0,0 +1,461 @@ +""" +Validator for tracked changes in Word documents. + +Detects untracked edits in a Word document: text that differs from the +original without a / wrapper recording it. The tracked changes +that are new relative to the original are undone, and the result is compared +against the original; whatever text still differs was edited without being +tracked. + +The body (word/document.xml) is compared, and so is each header, footer, +footnotes and endnotes part, against the original's part of the same name. +Other parts, comments among them, are not checked. A page-number field +whose cached result is a plain number counts as its name in braces +("{PAGE}"), not as that number, which any application that repaginates the +document rewrites. +""" + +import re +import subprocess +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.ElementTree as ET +from defusedxml.common import DefusedXmlException + +from helpers import rendered_text, safe_extract + + +XML_ERRORS = (ET.ParseError, DefusedXmlException, LookupError, ValueError) + + +class RedliningValidator: + + def __init__(self, unpacked_dir, original_docx, verbose=False): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def repair(self) -> int: + return 0 + + def validate(self): + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + safe_extract(zip_ref, temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + sides = (("original", temp_path), ("modified", self.unpacked_dir)) + change_count, failures, differences = 0, [], [] + for part in self._parts_to_compare(temp_path, self.unpacked_dir): + roots = [] + for side, unpacked in sides: + try: + roots.append(self._parse_part(unpacked / part)) + except XML_ERRORS as e: + failures.append( + f"FAILED - Error parsing {part} of the {side} document: {e}" + ) + if len(roots) < len(sides): + continue + new_changes, original_text, modified_text = self._compare_part(*roots) + change_count += len(new_changes) + if modified_text != original_text: + differences.append((part, original_text, modified_text)) + + if differences: + failures.append(self._generate_detailed_diff(differences)) + if failures: + print("\n".join(failures)) + return False + + if self.verbose: + print( + f"PASSED - All {change_count} change(s) against the original " + "are properly tracked" + ) + return True + + OTHER_PARTS = ( + "word/header*.xml", + "word/footer*.xml", + "word/footnotes.xml", + "word/endnotes.xml", + ) + + def _parts_to_compare(self, original_dir, modified_dir): + others = set() + for unpacked in (Path(original_dir), Path(modified_dir)): + for pattern in self.OTHER_PARTS: + others.update( + path.relative_to(unpacked).as_posix() + for path in unpacked.glob(pattern) + ) + return ["word/document.xml", *sorted(others)] + + def _compare_part(self, original_root, modified_root): + for root in (original_root, modified_root): + self._flatten_text_elements(root) + new_changes = self._new_tracked_changes(original_root, modified_root) + self._remove_tracked_changes(modified_root, new_changes) + return ( + new_changes, + self._extract_text_content(original_root), + self._extract_text_content(modified_root), + ) + + def _parse_part(self, path): + if not path.is_file(): + return ET.fromstring("") + return ET.parse(path).getroot() + + def _flatten_text_elements(self, root): + for elem in self._text_elements(root): + if len(elem): + elem.text = "".join(elem.itertext()) + for child in list(elem): + elem.remove(child) + + def _tracked_change_elements(self, root): + w = self.namespaces["w"] + tags = (f"{{{w}}}ins", f"{{{w}}}del", f"{{{w}}}moveFrom", f"{{{w}}}moveTo") + return [elem for elem in root.iter() if elem.tag in tags] + + def _rendered_text(self, elem): + preserve = elem.get("{http://www.w3.org/XML/1998/namespace}space") == "preserve" + return rendered_text(elem.text or "", preserve) + + def _text_elements(self, elem): + w = self.namespaces["w"] + return [ + node + for node in elem.iter() + if node.tag in (f"{{{w}}}t", f"{{{w}}}delText") + ] + + def _tracked_change_key(self, elem): + w = self.namespaces["w"] + text = "".join(self._rendered_text(node) for node in self._text_elements(elem)) + return (elem.tag, elem.get(f"{{{w}}}author"), elem.get(f"{{{w}}}date"), text) + + def _new_tracked_changes(self, original_root, modified_root): + original = self._tracked_change_elements(original_root) + modified = self._tracked_change_elements(modified_root) + + pool = {} + for elem in original: + pool.setdefault(self._tracked_change_key(elem), []).append(elem) + + matched, leftover = set(), [] + for elem in modified: + bucket = pool.get(self._tracked_change_key(elem)) + if bucket: + matched.add(bucket.pop()) + else: + leftover.append(elem) + + def group(elem): + return self._tracked_change_key(elem)[:3] + + def text_of(elems): + return "".join(self._tracked_change_key(e)[3] for e in elems) + + unmatched_original = {} + for elem in original: + if elem not in matched: + unmatched_original.setdefault(group(elem), []).append(elem) + + by_group = {} + for elem in leftover: + by_group.setdefault(group(elem), []).append(elem) + + new = set() + for key, elems in by_group.items(): + rebuilt = text_of(elems) + if rebuilt and rebuilt == text_of(unmatched_original.get(key, [])): + continue + new.update(elems) + return new + + def _generate_detailed_diff(self, differences): + error_parts = [ + "FAILED - Document text doesn't match after removing the tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + " 4. Rewrote another author's / and changed its text on", + " the way. A tracked change from the original is recognised by its", + " author, date and text; anything that doesn't reproduce one exactly", + " reads as new, and the text it carried is reported missing.", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To reject PART of one: nest around only the runs you reject.", + " Their may be split around it, so long as the pieces keep", + " their author and date and still spell out the same text.", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + explain_page_fields = False + for part, original_text, modified_text in differences: + body = part == "word/document.xml" + heading = "Differences:" if body else f"Differences in {part}:" + if error_parts[-1]: + error_parts.append("") + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend([heading, "=" * len(heading), git_diff]) + if re.search(r"\{(%s)\}" % "|".join(self.PAGE_FIELDS), git_diff): + explain_page_fields = True + else: + error_parts.extend( + [heading, "Unable to generate word diff (git not available)"] + ) + if explain_page_fields: + error_parts.extend(["", self.PAGE_FIELD_NOTE]) + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + encoding="utf-8", + errors="replace", + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + encoding="utf-8", + errors="replace", + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + pass + + return None + + def _remove_tracked_changes(self, root, targets): + w = self.namespaces["w"] + ins_tag = f"{{{w}}}ins" + del_tag = f"{{{w}}}del" + move_from_tag = f"{{{w}}}moveFrom" + move_to_tag = f"{{{w}}}moveTo" + + self._merge_split_paragraphs(root, targets) + + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag in (ins_tag, move_to_tag) and child in targets: + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + renamed = { + f"{{{w}}}delText": f"{{{w}}}t", + f"{{{w}}}delInstrText": f"{{{w}}}instrText", + } + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag in (del_tag, move_from_tag) and child in targets: + to_process.append((child, list(parent).index(child))) + + for del_elem, del_index in reversed(to_process): + for elem in del_elem.iter(): + elem.tag = renamed.get(elem.tag, elem.tag) + + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + range_tags = ( + f"{{{w}}}moveFromRangeStart", + f"{{{w}}}moveFromRangeEnd", + f"{{{w}}}moveToRangeStart", + f"{{{w}}}moveToRangeEnd", + ) + for parent in root.iter(): + to_remove = [ + c + for c in parent + if c.tag in range_tags and len(c) == 0 and not (c.text or "").strip() + ] + for elem in to_remove: + parent.remove(elem) + + def _merge_split_paragraphs(self, root, targets): + w = self.namespaces["w"] + p_tag = f"{{{w}}}p" + ppr_tag = f"{{{w}}}pPr" + marker_path = f"{ppr_tag}/{{{w}}}rPr/{{{w}}}ins" + + for parent in list(root.iter()): + for para in reversed([child for child in parent if child.tag == p_tag]): + if not any(m in targets for m in para.findall(marker_path)): + continue + siblings = list(parent) + index = siblings.index(para) + following = siblings[index + 1] if index + 1 < len(siblings) else None + if following is None or following.tag != p_tag: + continue + if any(self._text_elements(c) for c in para if c.tag == ppr_tag): + continue + insert_at = 1 if len(following) and following[0].tag == ppr_tag else 0 + for child in reversed([c for c in para if c.tag != ppr_tag]): + following.insert(insert_at, child) + parent.remove(para) + + def _extract_text_content(self, root): + p_tag = f"{{{self.namespaces['w']}}}p" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + paragraph_text = self._paragraph_text(p_elem) + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + PAGE_FIELDS = {"PAGE", "NUMPAGES", "SECTIONPAGES"} + PAGE_NUMBER = re.compile(r"-? ?[0-9]{1,6} ?-?|[ivxlcdm]{1,8}|[a-z]{1,2}|", re.I) + PAGE_FIELD_NOTE = ( + "Note: {PAGE}, {NUMPAGES} or {SECTIONPAGES} above stands for a page-number\n" + "field. Word recomputes the number it displays, so the number stored in\n" + "the file is not compared, but adding or removing the field still counts." + ) + + def _paragraph_text(self, p_elem): + w = self.namespaces["w"] + t_tag, instr_tag = f"{{{w}}}t", f"{{{w}}}instrText" + fld_char, fld_simple = f"{{{w}}}fldChar", f"{{{w}}}fldSimple" + + def page_field(instruction, field): + words = instruction.split() + locked = field.get(f"{{{w}}}fldLock") in ("1", "true", "on") + if words and words[0].upper() in self.PAGE_FIELDS and not locked: + return words[0].upper() + + def shown(name, result): + return "{%s}" % name if self.PAGE_NUMBER.fullmatch(result) else result + + parts, fields = [], [] + skipped = set() + + def sink(): + for *_, result in reversed(fields): + if result is not None: + return result + return parts + + for elem in p_elem.iter(): + if elem.tag == fld_simple: + name = page_field(elem.get(f"{{{w}}}instr", ""), elem) + if name: + texts = [self._rendered_text(t) for t in elem.iter(t_tag)] + sink().append(shown(name, "".join(texts).strip())) + skipped.update(elem.iter(t_tag)) + elif elem.tag == fld_char: + kind = elem.get(f"{{{w}}}fldCharType") + if kind == "begin": + fields.append([elem, "", False, None]) + elif kind == "separate" and fields and not fields[-1][2]: + fields[-1][2] = True + if page_field(fields[-1][1], fields[-1][0]): + fields[-1][3] = [] + elif kind == "end" and fields: + begin, instruction, _, result = fields.pop() + name = page_field(instruction, begin) + if name: + sink().append(shown(name, "".join(result or []).strip())) + elif elem.tag == instr_tag and fields and not fields[-1][2]: + fields[-1][1] += elem.text or "" + elif elem.tag == t_tag and elem not in skipped: + sink().append(self._rendered_text(elem)) + for *_, result in fields: + parts.extend(result or []) + return "".join(parts) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/comments.xml b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/comments.xml new file mode 100644 index 0000000..cd01a7d --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/comments.xml @@ -0,0 +1,3 @@ + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsExtended.xml b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsExtended.xml new file mode 100644 index 0000000..411003c --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsExtended.xml @@ -0,0 +1,3 @@ + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsExtensible.xml b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsExtensible.xml new file mode 100644 index 0000000..f5572d7 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsExtensible.xml @@ -0,0 +1,3 @@ + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsIds.xml b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsIds.xml new file mode 100644 index 0000000..32f1629 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/commentsIds.xml @@ -0,0 +1,3 @@ + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/people.xml b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/people.xml new file mode 100644 index 0000000..3803d2d --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/docx/scripts/templates/people.xml @@ -0,0 +1,3 @@ + + + diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/SKILL.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/SKILL.md new file mode 100644 index 0000000..f42d210 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/SKILL.md @@ -0,0 +1,97 @@ +--- +name: google-workspace +description: "Read this before the first Google Drive, Docs, Sheets or Slides connector call whenever the task creates or changes a Google file. Use this skill whenever the user wants to create or change a Google Doc, Sheet or Slides file in their Google Drive. Triggers include: a request that names Google Docs, Sheets, Slides or Drive and asks to make, edit, format, copy or rename a file; a docs.google.com link with a request to change that file, even a one-line fix or suggested edits; and any follow-up change to a Google file from earlier in the chat, even \"change it\" or \"add a tab\". Includes helper scripts for document positions, cell ranges and slide layout. However, if the user asks for a doc, deck or spreadsheet without naming Google, or gives a Google file only as source material for something new, use Claude's own output type instead. Do NOT use for read-only questions about a Google file, or for Word, Excel, PowerPoint or PDF files." +--- + +# Google Docs, Sheets, and Slides + +The Google connectors are thin wrappers over Google's raw APIs, with almost no guidance of their own. This skill supplies that guidance: which connector does what, the rules that keep edits in the user's file, and, in one reference file per app, how each API really behaves. Most of the app-specific rules in the references were tested against Google's Docs, Sheets and Slides APIs; the rest were seen through the connectors, and a few have not been checked yet. + +## Read the reference before you touch the file + +| Working on | Read first | Why it matters | +|---|---|---| +| A Google Doc | `references/docs.md` | Docs edits address UTF-16 positions that shift after every insert and go stale after every write. Tabs and pending suggestions change the positions too. | +| A Google Sheet | `references/sheets.md` | Both write tools parse input like the Sheets UI, so text can silently become numbers or dates. Formatting needs numeric sheet IDs, 0-based ranges, and field masks without parentheses. | +| A Google Slides deck | `references/slides.md` | Positions are in EMU, an element's real size is its size times its scale, and an unmasked read can exceed 150 KB for three slides. Slides never shrinks text to fit. | + +Read the reference for every app the task touches before the first edit. Embedding a Sheets chart in a deck means reading both. The references are short, and skipping one is how edits land in the wrong place. + +## 1. Check the connectors before you start + +| Connector | What it can do | +|---|---| +| Google Drive | Create files, upload and convert content, rename, read a file as text, export (PDF and other formats), search, trash | +| Google Docs | Read a doc's full structure and edit it in place, directly or as suggestions | +| Google Sheets | Read values and structure, write values and formulas, format, add tabs and charts | +| Google Slides | Read a deck, add and edit slides, shapes, text, tables, and linked charts | + +Drive can create all three file types. It can't edit a file after that. Without the matching editor connector, every change means a new file and a new link, and the user loses the link they already have. + +1. Check which Google tools are available in this conversation. On surfaces where tools are deferred, search for and load the tools you need first, such as "google sheets update". Tool names differ by surface, so use the names your surface lists. If a search returns nothing, list all available tools before deciding the connector is missing, because the tool may exist under a different name. +2. If the editor tools are missing, tell the user. When you can list the conversation's connectors, say which case it is: a connector that is set up but turned off in this chat (ask them to turn it on in the chat's connector settings, then continue), or one that isn't connected at all (tell them which connector to add and what it enables). +3. With an editor connector missing, a request to create a new file continues with Drive. A change to an existing file stops and asks. See the missing-connector rule in section 2. +4. If the user asks only for a new file, create it with Drive. If the editor connector is off, add one line saying edits will need it. + +Example: "I can create the sheet now. If you want changes later, turn on the Google Sheets connector in this chat first. Then I can edit this file, and the link will stay the same." + +## 2. Rules for every file + +- **A change goes in the same file.** "Change", "update", "fix", "add", and "switch it to" all mean the user wants the same file and link. Do not recreate the file to skip an edit. +- **A missing editor connector is a choice for the user, not a workaround for you.** When the user asks for a change to an existing file and the editor connector is missing, your whole reply is a short question, not a deliverable. Building the next-best thing feels helpful, but the user's file is still untouched and now there are two artifacts, which is the exact failure this skill exists to prevent. Lead with the fix: name the connector and say that with it on, the edit lands in their existing file and the link stays the same. You may offer an alternative, such as drafted text to paste or a file to import by hand, but only as a named option, and build it only after the user picks it. Example reply, in full: "To add the slide to your deck directly, turn on the Google Slides connector in this chat and I'll do it. Same deck, same link. Or I can draft the slide as a file you'd import by hand. Which do you prefer?" +- **Never trash a file the user didn't ask you to delete.** Drive can trash a file, but it can't restore one. The Docs, Sheets, and Slides connectors can't open a trashed file. +- **Read before you edit, and guard the write.** Send every Docs and Slides batch with `writeControl.requiredRevisionId` set to the `revisionId` from a read taken after your last write, including batches built on IDs or positions from your own earlier writes. The only exception is a batch made entirely of `replaceAllText` requests, even one limited to some tabs or slides. If the file changed since that read, whether from your own write or someone else's, Google rejects the whole batch instead of applying it at stale positions or IDs. Read again before each guarded batch, and don't reuse a revision ID from a write reply. On a rejection, read again and rebuild; never drop the guard. `read_doc` and `read_presentation` (the mask in `references/slides.md` includes it) return the ID. `read_slide_page` returns it only if its mask includes `revisionId`; `read_slide_page_thumbnail` and Drive `read_file_content` never do. For a saved read, the first line of `docs_index.py outline` or `slides_helper.py outline` shows it; if that line says `None`, read again with `read_doc` or `read_presentation`. `get_spreadsheet` returns a `revisionId` even with a field mask, but don't send it: send Sheets writes without `writeControl`, and read again right before a write that depends on positions. +- **Put everything for one step in one batch.** Batch requests run in order and atomically: if one is invalid, none apply. One call per logical step is faster and leaves no half-finished state. +- **Sheets field masks.** Write each path in full, with no parentheses: one path per item in `get_spreadsheet`'s `fields` list (`["spreadsheetId", "sheets.properties"]`), and comma-separated in a batch update request's `fields` string (`"userEnteredFormat.numberFormat,userEnteredFormat.textFormat.bold"`). +- **Verify the result.** Read the file again after each edit and check the result before you report it. A tool accepting the call does not mean the content is correct. Each reference has a Verify section. +- **Rename with Drive `update_file`.** A rename keeps the same link. +- **Link every file you name.** When you name a Google file you created, copied, found, or edited, make its title a link, inside the sentence that says what you did: "I added the row to [Team roster](its link)." Don't set the file apart after a colon or on a line of its own ("I've created a file for you: Team roster"). If a Drive result says the user can already see or open a file from the chat, leave that file's link out unless they ask. Always give the link when the user asks for it, wants to send it to someone, or says they can't see or open the file, even if a result said they could. Don't tell the user to use a card or preview unless a result said one is shown. After an edit, say that the link stays the same. + +## 3. Drive, for all three apps + +- **Find a file:** use Drive search when the user names a file without a link. Confirm the match with the user if more than one file fits. +- **Create:** `create_file` with `contentMimeType` set to `application/vnd.google-apps.document`, `.spreadsheet`, or `.presentation` creates an empty file. Uploading content with a source type (HTML, CSV, .xlsx, .pptx) converts it into the matching Google type. Each reference says which route fits that app. +- **Read as text:** `read_file_content` returns a compact text rendering: Markdown for a Doc. It is a few KB where the editor connector's full read can be over 100 KB, so use it to understand content, then use the editor read when you need positions. +- **Export:** `download_file_content` with `exportMimeType: "application/pdf"` returns the rendered file as base64. It is large (about 65 KB of text for a four-slide deck), so export only for a visual check, and only where you can run code. + +## 4. Helper scripts + +The `scripts/` folder next to this file holds tested helpers. They turn the APIs' raw JSON into short readable summaries, and turn simple specs into correct request batches, so the error-prone arithmetic never happens by hand. Use them wherever you can run Python. Run them by their full path inside this skill's folder; the references write that folder as ``. + +| Script | Commands | Used for | +|---|---|---| +| `docs_index.py` | `outline`, `find`, `new-table`, `fill-table` | Docs positions, text search with bold and suggestion state, adding a filled table in one call, filling an existing table | +| `sheets_helper.py` | `range`, `format`, `cells` | A1 ranges to grid ranges, formatting batches from a short spec, reading formulas and error cells | +| `slides_helper.py` | `outline`, `build` | Real slide geometry with overflow and overlap warnings, building slides from an inch-based spec | +| `render_export.py` | one command | Decoding a PDF export into page images to look at | + +**Getting JSON to a script.** Large tool results are often saved to a file by the app, and the result tells you the path. Point the script at that path; the scripts read the file as the app saved it. When a result comes back in context and is small (a few KB, such as a masked read or sheet metadata), write it to a file and run the script on it. Don't re-type a large result into a file: that doubles the cost and invites copying errors. If a large result was cut off and not saved, read a smaller slice instead (a field mask, a range, or one tab) rather than guessing. + +Every script prints `--help` with its full usage. Each reference shows the commands in context. + +## 5. Common failures across apps + +| Symptom | Cause | Fix | +|---|---|---| +| "No such tool available" | The tool is deferred, or the name is different on this surface | Search for and load the tool. Use the exact name your surface lists. | +| Permission denied on a file you created | The file is in the trash | Ask the user to restore it from Drive's trash. You can't restore it with the connectors. | +| 400: required revision ID does not match | The file changed after your read: your own previous write, or someone else editing | Read again, rebuild the requests from the new read, and send them with the new revision. For Sheets, send them without `writeControl`. | +| A whole batch failed on one bad request | Batches are atomic | Fix the named request and resend the full batch. Nothing from the failed call was applied. | +| A read is too large or cut off | Unmasked editor reads include everything | Use Drive `read_file_content`, a field mask, a range, or a single tab. Run the helper on a saved result. | +| Two files where the user expected one | An edit was done by creating a new file | Make changes in place with the editor connector. Tell the user about the extra file; don't trash it without asking. | + +Each reference ends with the failures specific to that app. + +## 6. What the connectors can't do + +When a request runs into one of these, say so plainly and do the alternative. *Reported*: seen through the connectors or in their tool schemas. *Untested*: not yet checked. + +- **Claude can't see the user's screen.** No connector shows what the user has selected, or which file, tab or slide they have open. If "this" or "here" isn't clear from the chat, ask which file, heading, slide number or cell range they mean. *Reported.* +- **Comments:** `read_doc` has returned no comments even with `commentsIncluded: true`. To read comments, use Drive `read_file_content` with `includeComments: true`, or ask the user to paste them. `update_doc` has accepted `insertComment` in one report, although its description is cut before listing it; if that request fails, say you can't add comments and offer to list your notes in the reply. *Reported.* +- **Suggestion mode can be refused** with "Unsupported WriteControl mode". Never make direct edits in its place: offer to list the proposed changes or to edit directly (see `references/docs.md`). *Reported.* +- **Request lists are cut short.** The `update_doc`, `update_spreadsheet` and `update_presentation` descriptions stop partway through their request lists. Requests missing from the description, such as `addDocumentTab` and `addChart`, have still worked, so try one before deciding it isn't supported. *Reported.* +- **Uploads travel inside the call.** `create_file` takes the whole file as text or base64, so an .xlsx or .pptx upload is slow and often fails. Build in the file with the editor connectors where you can, and keep uploads small. *Reported.* +- **Images need a public URL.** Slides `createImage` and Docs `insertInlineImage` fetch the image from a URL Google can reach; a file from the chat can't be inserted this way. For a deck, offer the .pptx route; otherwise ask the user to insert the image. *Untested.* +- **A Slides chart must already exist in a Sheet.** Create it there with `addChart`, then embed it with `createSheetsChart`. *Reported.* +- **Drive search uses `title`, not `name`.** `name contains '…'` fails with "Unsupported query field: name"; write `title contains '…'`, and put the file type in a `mimeType` clause. *Reported.* +- **Slides edits may not show in an open deck right away.** If the user doesn't see a change, ask them to reload the deck before you change anything again. *Reported; may be fixed.* diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/references/charts.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/references/charts.md new file mode 100644 index 0000000..38e1487 --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/references/charts.md @@ -0,0 +1,87 @@ +# Charts + +Read the slide design rules in `references/slides.md` first; this file covers charts on slides. Assume each chart will be projected, printed or forwarded without you there to explain it, so everything the reader needs is on the chart. + +## Choose the display + +| The reader needs to... | Use | +| :---- | :---- | +| See one or two numbers | No chart: plain words in a bullet, or a big number, the figure set large ("15%") with a line of smaller text under it saying what it is ("revenue growth last quarter"). Clearer than a one-bar chart or a two-slice pie | +| Compare magnitudes across categories | Bar chart: vertical columns by default, horizontal bars when the category names are long | +| See a trend, or anything else where the points are connected | Line: for time series and other connected data, never for categories; add error bars or a shaded range when the spread matters alongside an average | +| See how a few items moved between two points | Slope graph with no gridlines or value axis (say, education rate by region, 2015 against 2025), not a multi-series bar chart. Few lines, the one that moved most in the accent color; the start value alone on the left, the end value and the name once on the right | +| See one series against context | That series in the accent color, everything else gray | +| See parts of a whole | Stacked column: absolute to compare totals and their components, 100% to compare mix. Stacks get overwhelming fast; they show the gist, not precise comparisons of middle segments | +| Compare an ordered scale across categories (Likert responses) | 100% stacked horizontal bar: consistent baselines at the far left and far right | +| Follow a starting point, the changes, and the result | Waterfall: breaks a complicated calculation into pieces | +| See share on two dimensions at once (segment size and the mix within it) | Mekko | +| See a schedule | Gantt | +| See the relationship between two quantities | Scatterplot: mostly for samples and studies; fine in business when there is a real relationship to show | +| See data that normal charts don't accommodate | Shapes as illustration: a funnel, a grid of squares, an illustrative bar, when the data doesn't fit a chart and exact units don't matter, as long as it is obviously not to scale | + +- Avoid unless the user explicitly asks: pie and donut charts (people judge angles badly; use a sorted bar chart instead); area charts (hard to read; the one exception is the square-area graph, where each square's area is a quantity, useful for very different magnitudes such as TAM, SAM and SOM); 3D; and secondary axes (the alignment of two scales is arbitrary and invents a correlation; label the units on each line, stack two charts on the same horizontal axis, or index both series to a common base). +- One to three colored series read clearly; at four or more, label each series directly even if the deck's charts use a legend; five or six is the most one chart can hold. Past that, combine the smallest into "Other", or use small multiples (a grid of small identical charts on shared scales). Never fix "too many series" by adding colors. + +Combinations are sometimes right, such as a stacked bar with one segment broken out beside it (in a chart, a small table or a text box) when the audience will want that detail. + +## Which standard the chart follows + +If the deck already contains charts and they share a consistent style (axis treatment, gridlines, legend or direct labels, label placement, whether the title sits inside the chart or above it, fonts, colors), that is the standard, and every new chart follows it, even where it differs from the master's theme. Charts that disagree with each other are not a standard. Otherwise follow the house standard below. + +Under a deck standard, the deck's charts decide everything the house standard decides, including where the title sits and how it is worded. Where you cannot reproduce their style directly, copy one of the deck's charts and replace its data and labels, which is the surest way to get an identical look. + +## The house standard + +- **No legend**, except where Series names (Finishing touches) allows one. +- **Bar and column charts:** no gridlines and no value axis; every bar carries its value, and the category axis names the bars. Keep a thin gray category axis line (about #BFBFBF), so the bars don't float. +- **Line charts:** faint horizontal gridlines at four or five round values, so the reader can read values across (faint means close to the background: light gray on a white slide, a dark gray just off black on a dark one); value-axis labels in a muted color that carry the unit ("$0B", "$25B"), with no axis line; no vertical gridlines. A small marker on each labeled point and none on the others, since markers on every point clutter a long line. Lines run straight from point to point, never smoothed. +- **Scatter charts:** no gridlines; both value axes, four or five ticks at round numbers, bare numbers in a muted color. +- **One accent color** for the point that is the message, gray for everything else. Use the deck's accent: the theme's first accent color, the accent the deck's finished slides use, or the accent you chose when setting up a new deck. Use this fallback palette only when the deck has no accent of its own (default theme, and no finished slide uses one), so decks for different companies don't share a look: accent #1A6BB8, bars #BFBFBF, lines #A6A6A6, text #1A1A1A, secondary text #595959. +- **Transparent background.** The chart area and the plot area have no fill and no border, so the chart sits on the slide background. +- **Units in the number format.** Every data label on a bar or column carries its unit ("$42M", "38%", "12K"); only a plain count such as 355 goes bare. On stacked columns only the column total carries the unit ("$40.5M"); the segments are bare numbers (18.2, 12.5, 9.8), and the total sits above each stack. +- **Bar proportions.** The gap between bars is 50% of a bar's width, so bars are twice as wide as the gaps. On stacked bars, set the series overlap to 100% so the segments stack on one bar. +- **Headroom.** Set the value axis maximum about 15% above the tallest bar, so labels above the bars fit; about 25% when totals or growth arrows sit above the bars. +- **Sorting.** Horizontal bars are sorted with the largest at the top. +- **Likert and other ordered scales** are tints of the one accent, darkest at the top of the scale (such as "Strongly agree") and lightest at the bottom, never a red-to-green ramp. This overrides the two-hue rule for values above and below a baseline in Finishing touches. +- **Waterfalls.** The start and end totals are dark gray, increases are the accent, and decreases are one contrasting warm color, such as orange #E07B39. Each step is labeled with its signed change, and the totals carry the unit. +- **Text.** Axis and data labels are 14pt in the deck's body font; the year row and callouts are smaller (Finishing touches). Text set in a series color (line names, callouts) uses the secondary text color when that series is a light gray. + +## The chart label + +The story is in the slide title (or in the slide's commentary when the deck uses label titles), never in the chart. A story title states the takeaway as a full sentence with the number ("Sales grew every quarter for two years and finished 2025 at $12.4M"). The chart carries a descriptive label only, with these parts in this order, separated by commas: what is plotted, the scope if there is one, the unit, the timeframe. + +- The unit is a short token, never spelled out. Currency scale letters follow the deck's own convention when it has one, otherwise the deck's language: $K, $M and $B for US English (also the default when the language is unclear), £k, £m and £bn for UK English, and so on. Do not mix conventions in one deck. +- Parentheses hold only a second unit or a data basis, never the primary unit. +- What is plotted always stays. +- Drop the timeframe when the axis shows the year, in the tick labels ("FY22") or on a year row. Keep it when the axis shows only bare periods ("Q1", "Jan"), as on a single-year chart, so the year appears somewhere on the slide. +- The unit is on every data label of a bar or column chart and on the value-axis labels of a line chart, so it drops out of the chart label for both. On stacked columns the totals carry it. On scatter charts the axes show bare numbers, so the unit stays in the label. +- Decide at the first chart which parts this deck's labels carry (scope, unit, timeframe), and apply that and the drop rules the same way on every chart, checking the earlier labels before each new one. Half the charts labeled "$M, FY25" and half not looks careless. +- Never in the label: the takeaway, a figure (a growth rate, total or change goes in the slide title, a data label, a growth arrow or a callout), a colon or a dash between parts, a spelled-out unit. + +Examples: + +- Column chart, bars labeled "$42M", x-axis "Q1 … Q4 | Q1 … Q4" with "2024" and "2025" on a year row → **"Quarterly revenue"**, not "Quarterly revenue, $M, Q1 2024–Q4 2025". +- Column chart, bars labeled "$42M", x-axis "Q1 … Q4" (one year, no year row) → **"Quarterly revenue, FY25"**: the timeframe stays because the axis has none. +- Line chart, axis labels "0K … 250K", x-axis "Jan … Dec | Jan … Jun" with "2025" and "2026" on a year row → **"Monthly active users"**: the axis carries the unit and the year row the timeframe. +- Column chart with growth arrows, bars labeled "$42M", growth labels "+21%" → **"Quarterly revenue, FY25 (% growth QoQ)"**: a second unit goes in parentheses at the end. +- Bar chart of segment shares, bars labeled "58%" → **"Regional share of revenue, FY25 (based on reported segments)"**: a data basis goes in parentheses. + +The label is a text box in the slide's caption style directly above the chart, so its typography matches the slide; the chart's own built-in title stays off. Line the label's first character up with the slide title's: the same left position and the same left text inset (internal padding). A title placeholder usually has a nonzero inset from the master and a new text box has an inset of 0.1 in (7.2pt), so copy the title's inset rather than setting zero. Keep the label to one line when the subject is short. When the subject runs to about six words or more ("Quarterly sales by department, Product X"), put the unit and timeframe on a second line about two points smaller and muted ("FY25", or "$M, FY25" where the label keeps the unit) rather than letting the label wrap or run into the chart. A slide with two or more charts gives each its own descriptive label, and the slide title carries the message they make together. + +## Placing the chart + +- A chart on its own takes the whole content area: the full content width (the title's left edge to its right edge), from just under the title to just above the footnotes. The common mistake is a chart that stops halfway down the slide, not one that is too big. +- Small multiples: no panel shorter than about 120pt, or the labels have nowhere to go. Share one scale across the panels only when the comparison is about magnitude; when a shared scale flattens the smaller panels into a line, give each panel its own scale and say so in the caption. + +## Finishing touches + +Once the chart exists, adjust it so the message is visible: + +- **Color does one job per chart.** A second accent is a second message; if two things matter, make two charts. Use distinct hues only when telling the series apart is the point, and then the same hue for the same entity on every chart in the deck. Use a single hue from light to dark for magnitude, and two opposed hues around gray for above and below a baseline. Never a rainbow, and never color as the only carrier of a distinction (red and green least of all): a label or position always backs it up. +- **Source line** below the chart, at footnote size (10–12pt, muted), following "Footnotes and sources" in `references/slides.md`, including citing only sources you have. +- **Period axes.** Tick labels show only the short period (Q1 … Q4, Jan … Dec). The year sits on a second row under each year's first period, at 11pt in the secondary text color, and is not repeated on the other ticks; "Q1 FY25, Q2 FY25, …" on every tick is the defect to avoid. A linked Sheets chart has no tested way to draw it, so put small text boxes under the axis. A single-year chart has no year row and carries the year in its label. +- **Series names.** Give each line a short name, one or two words ("Data center"), in its series color, right of its last point on the same line as the end value ("$89.0B Data center"), with room left on the right of the plot. On a chart of three lines or fewer other than a slope graph, there are two exceptions: when the deck standard uses a legend, use the deck's legend; otherwise a name longer than about 14 characters gets a small legend instead of a wrapped end label. With four or more lines, or on a slope graph, shorten long names instead. A chart with two or more lines is not done until every line is named on the chart, by an end label or a legend. +- **Point labels on a line:** the start, the end, and the peak, trough or break the title or bullets refer to. Three labels on a ten-point line is right; more than four is crowding, and the first and last alone are too few for a long line. Each label except the end label sits above or below its point, off the line. +- **Annotate the conclusion:** a shaded band over the downturn, or a callout when Callouts below calls for one. Where actuals turn into a projection, mark the boundary so nobody reads the forecast as history: on a line, the forecast portion is dashed; on columns, forecast bars are hatched or a visibly lighter fill, and their category labels carry an E suffix ("2026E", "Q3E"). A short label at the boundary ("Actual" | "Forecast") helps when the deck will be read on its own. +- **Growth arrows** show growth or decline where it is significant, not between every pair of bars. The default is an elbow: up from one bar, across at a height that clears the bars between and their labels, and down onto the other. A straight line from one bar top to the next works only between adjacent bars with room above them. The third shape is a difference marker (a short horizontal mark at each bar top and a vertical line between the two levels with the change on it). Whichever shape, the change number sits centered on the arrow (on an elbow, on its horizontal part), in front of it, on a fill matching the slide background. Growth arrows and CAGR brackets are shapes drawn over the chart, never chart elements; position them from the chart's geometry. +- **Callouts:** when the slide title or commentary claims a change in trajectory (an acceleration, a break, a launch, a one-off), put a callout naming the cause at the point where it happens, on the chart rather than only suggested in your reply. If nothing you were given names the cause, ask, or leave the callout out; never guess one. Otherwise most charts need none. At most one, occasionally two; more than that is clutter, and the explanation belongs in a text block beside the chart instead. The callout is a short phrase ("Supply chain outage", "Product X launch"), never a full sentence, at 11–12pt in the color of the series it annotates, with no box, border or fill, and a thin leader line in the same color from the text to the point. Place the text in the nearest open area of the plot, never over a bar, the line, or a data label. diff --git a/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/references/docs.md b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/references/docs.md new file mode 100644 index 0000000..2cef4ad --- /dev/null +++ b/claude/.claude/skills/synced/fb829d43-7db9-4fa9-87ae-5d8d251b25c3_7b05094f-7fab-413c-9d26-d7d2da3dce57/google-workspace/references/docs.md @@ -0,0 +1,293 @@ +# Google Docs reference + +As a first step, before you create a Google Doc or change one, you must read the design rules below in full: how a document should look and read, and how to edit one. After them, "Applying the design rules" says how to carry out the rules with these tools, and the rest of the file covers the Docs connector's `read_doc` and `update_doc` and the Drive tools that help with Docs. + +# Document design + +When the user asks for something specific, such as a font, a color, a length or a structure, do that; these rules decide what the user left open. The rules for the text itself are under Writing, and the rules for changing a document that already exists are under Editing an existing document. + +## Structure + +- **Plan the sections before you write, and merge any two that answer the same question in different words.** If the user asked for a set number of sections, replace the merged one with a section that covers something new. +- **Each section appears once.** Before you finish, read the headings in order and check that none is repeated. +- **An executive summary opens with the conclusion.** Its first paragraph states what the reader should believe or do. Numbers belong in the body sections, where they support the conclusion; they are not the conclusion. "$1.2B total debt across 7 instruments, weighted-average coupon 4.8%" is data. "Refinancing risk is concentrated in 2027. We recommend an opportunistic tender for the 2027 notes given the current cash position" is a conclusion. A summary that is only a list of numbers has not stated a conclusion. +- **A length the user gave is a limit.** "Three pages", "one page" or "500 words" is a hard constraint: check it before you finish and cut if you are over. Five pages for a three-page request is a defect, not thoroughness. Count the pages in the rendered document; without a render, estimate about 3,000 characters per page. +- **Limits still apply after the first request.** A length or a set of sources the user gave at the start still holds when a later request doesn't repeat it. +- **Numbers and quotes taken from a source match the source.** Take every figure, quote and page reference from a part of the source you have actually read, not from memory. If you can't find a value, say so instead of estimating it. + +## Styles + +The look of a document comes from its styles: the Normal text style for body text and the built-in heading styles for headings. Formatting set directly on individual paragraphs carries over into the text added after them and spreads across a long document. + +- **Set the body font once, for the whole body**, not on each paragraph or run. A font set paragraph by paragraph misses the paragraphs added later, and the document ends up in two fonts. +- **Headings use the heading styles.** Never make a heading out of bold, larger body text: it is missing from the table of contents and the outline, and its formatting carries over into the next paragraph. +- **Don't resize a single heading.** Each heading level already has its own size, and an override on one paragraph can make a subsection look like a section. If a heading is at the wrong level, change its level. If every heading at one level should be smaller, change that heading style. Don't move a correctly placed heading to a lower level only to make it smaller. +- **New content of the same kind takes the style around it; new content of a different kind does not.** A clause added next to other clauses gets their style. A table or a body paragraph added right after a list item or a heading gets the Normal text style, without the heading's bold. Otherwise the table cells get list markers and the body text comes out bold or as a heading. +- **Color goes on a phrase, not on a section.** One sentence in red is emphasis. Three paragraphs in red is too much, and the reader stops reading the color as a signal. If you want to color more than one paragraph, you need a heading or a callout box instead. Color the words themselves, not the whole paragraph, so the color doesn't carry over into the next paragraph. + +## Fonts + +- **New legal documents use Times New Roman.** When you draft a contract, brief, motion, legal memo or legal letter from scratch, with no template, set the body in Times New Roman. It is the professional default in legal practice, and other fonts read as informal. This doesn't apply when the document already has content (use its body font), when a template sets the font, or when the user names a font. + +## Lists and numbering + +- **Never type list markers or section numbers** in a new document, or in a list or heading the document numbers automatically. In those, don't write "•", "-", "*", "1.", "3.2" or "(a)" as text; use the document's list and heading numbering. Typed markers look right until someone inserts an item or a section above them, and then the numbers are wrong. +- **List items directly next to each other with the same list style join one list.** Numbering can also continue across a paragraph between two lists, so when a second numbered list should start again at 1, check that it does. + +## Tables + +- **Match the existing tables.** When the document already has tables, give a new one the same borders, fills and header row. One table with a colored header among three plain ones looks like a mistake. + +## Page layout + +- **Leave the page setup as it is unless the user asks**, and in a pageless document skip the rules below about pages and columns. "Applying the design rules" says how to recognize one. +- **Keep each heading on the same page as the paragraph after it.** A heading alone at the bottom of a page reads as broken even when the content is right. In a document you create, set "keep with next" on the heading styles, so it applies to every heading; in an existing document, do it when you find a heading separated from its paragraph. +- **To keep a section on one page, such as a financial statement, start it on a new page with one page break before its heading.** Check for an existing break first: a second one only adds a blank page. If the section still splits with a break in front of it, it is longer than a page; say so instead of adding another break. +- **When a table is longer than a page, stop its rows from breaking across pages**, so that each row is whole on one page. +- **Page numbers are fields, not typed text.** "Page 1" typed into a footer reads 1 on every page. +- **Headers and footers can have variants.** If the document has a different first-page header, or different headers on odd and even pages, each one is separate: change every one that applies. +- **Columns belong to a section, not to a paragraph style.** To set part of a document in two columns, give that part its own section. +- **Use real footnotes, not [1] markers typed into the text.** + +## Check the result + +Look at the rendered pages, not only the text. Assume there are formatting problems and look for them: + +- **Fonts:** a paragraph in a different font from the rest, or a size change in the middle of a section. +- **Styles:** added text that looks like plain body text where the paragraphs around it are headings or styled body text. +- **Numbering:** a list item that lost its marker, or (a) (b) that restarted as (1) (2). +- **Tables:** columns that changed width, or cells that wrap where they didn't before. +- **Formatting that runs on:** bold or italic that continues past where it should stop. +- **Spacing:** double blank lines, a paragraph pressed against the one above it, uneven indents. +- **Suggestions:** insertions and deletions that render garbled or overlap the normal text. +- **Page breaks:** a heading alone at the bottom of a page, a blank page, a section that used to fit on one page and now splits. + +After you fix one problem, check the paragraphs and pages around it. A fix to one paragraph often changes the next one, and a change in length moves the page breaks. + +## Writing + +Text you write in a document should read as though a person wrote it. When readers think something was written by AI, they judge it as sloppy and stop trusting it, whatever the content. They make that judgment from a set of common indicators, listed below, so take extra care to keep them out of your writing. These rules are for text you write, and a style the user or their style guide asks for takes priority. Do not rewrite the user's existing text to follow them unless the user asks you to. + +- Say what is true without first denying something else. "Revenue grew 12%, three times the US rate," not "This isn't a growth story, it's a market-share story." Do not open a paragraph or bullet with "Here's the thing" or "The real story is". +- Match the number of bullets, examples, and adjectives to the content, not to a default of three. Two drivers get two bullets; five get five. A list of exactly three ("fast, reliable, and scalable") usually means the third item was added for rhythm, not because there were three things to say, and readers read it as filler. +- Do not use a metaphor where a literal word will do. If a plain description exists ("the same construction," "the same pattern," "slowed," "fell"), use it. Metaphors are for when the literal version would be longer or less precise, which is rare in analytical writing. Test: if the metaphor can be replaced by a plain word without losing meaning, replace it. A metaphor makes the reader translate it back into the plain claim and carries meaning you did not choose. The ones that appear most are "north star", "move the needle", "double-click" (meaning look closer), "unpack", "journey", and "landscape" (meaning a market). Common ones in business writing: "moat", "headwind", "drag" (meaning a cost on results), "safety net", "clears the bar", and "land" meaning finish or total ("lands $4.4k under budget"). Examples: + - Bad: "The ones it has are the same species." Good: "The ones it has follow the same pattern." + - Bad: "A coordinated digestion pause would be visible immediately." Good: "If several large customers cut capex in the same quarter, it would show up in the next guide." + - Bad: "Architecture transitions compressed margin on the way in and expanded it on the way out." Good: "Gross margin fell during the Hopper-to-Blackwell ramp and recovered once Blackwell shipped at volume." + - Bad: "The lever that unlocks growth." Good: "The pricing change is what makes the target reachable." +- Cut words that claim importance without giving evidence: "genuinely", "truly", "actually", "clearly", "significantly", "robust", "leverage", "delve", "actionable insights", "learnings". Where one of them stood in for a fact, put the fact there ("margins fell 4 points", not "margins fell significantly"); otherwise delete it. When "leverage" or "significant" carries its financial or statistical meaning ("net leverage", "statistically significant"), it is a literal term; keep it. +- Use full stops and commas, and a colon before a list. No emoji in the document. +- Use em dashes sparingly: at most one in a paragraph, and none in a heading, a title, or between a bold label and the text after it. Several em dashes in one paragraph is one of the first things readers use to spot machine writing. In place of one, use a comma, a colon, parentheses, or a new sentence, not an en dash or a spaced hyphen. +- Headings in the document are about the content, not about your process: "Europe missed plan by $1.9M" or "Pricing", not "What I changed" or "The hard part". Standard labels such as "Executive Summary" or "Next steps" are fine. When the document already has headings, phrase new ones the same way, for example as short labels or as full sentences. +- Keep the conversation out of the document. When the user asks for a shorter or more formal version, make it shorter or more formal; do not title it "Executive Summary (Condensed)" or open it with "Updated per your feedback". The document's readers did not see the request, so those lines mean nothing to them. Say what changed in your reply, not in the document. This covers the document's text, not replies in comment threads. + +## Editing an existing document + +These rules apply whenever you change a document that already exists. Everything you add also follows the rules above. + +### Change only what was asked + +- **Match the scope of the edit to the request.** "Fill in this section" means add text. It doesn't mean also changing the alignment, adding underlining, reformatting tables or restyling nearby paragraphs. If a check shows a formatting change you didn't intend, undo it. +- **Change the smallest range that covers the change.** Replace the words that change, not the paragraph or section around them. Never delete and rebuild a paragraph, a section or the document to change part of it: that loses comments, bookmarks, images, charts and other embedded objects. +- **Keep the original wording wherever you aren't deliberately changing it.** Rewording the user didn't ask for is one more change they have to find and review. In a contract it is a substantive change: "aggregate" becoming "total", or "shall not" becoming "will not", changes what the text says. +- **Reformat by role.** "Make the body text 11pt" means the body text, not the headings; "indent the section headers" means the headings. Pick out the paragraphs by their style before you change them. +- **Take target values from the document, not from a guess.** If one table's header row is wrong and three others are right, copy the values from a correct one. When the user points at a reference ("make it look like section 3"), read that section's exact style, font, size, color, alignment and line spacing, and apply those. A guessed 18pt in a document built at 11pt is worse than the original problem. +- **Keep similar tables consistent, but only when the user asked you to reformat tables.** When the user asks you to reformat one of several similar tables, make the same change to the others or tell the user you changed only the one. +- **A table that changed as a side effect of another edit is a mistake to undo**, not a change to copy to the other tables. +- **Never replace a bare number across the whole document.** Closing a gap in section numbers by replacing every "7" with "6" also changes a "7.1x" interest coverage, a "7.5%" coupon and "FY2027". Renumber the headings one at a time. + +### Match what is there + +- **Text you add uses the document's body font and style**, not the editor's default. After you insert text, check its font and size against the paragraph before it. +- **Use the document's spelling variety**, for example British or American English. Keep new text in the variety the document already uses, never mix varieties in one document, and don't change existing text to another variety unless the user asks. If the document has no text yet, follow the variety of the user's messages. +- **Set explicit colors from the document.** If you must set a text color, copy it from an existing body paragraph instead of assuming black: many document styles use a dark gray or a theme color. +- **New numbered headings get their number from the document.** When the document numbers its headings automatically, write the heading text without a number and give the heading the same style or list level as the headings next to it, so it takes the next number. If the document numbers its headings by hand, follow that convention instead. +- **New list items continue the list.** Add an item as part of the list it belongs to, and check that it took the next marker: (b) after (a), not a second (a) or a skipped letter. +- **Making headings consistent.** When the user asks for all the headings to match, first give any hand-formatted heading (bold, larger body text) the right heading style. Then set each level to the font of its own heading style, one level at a time; applying the first level's font to every level removes the difference between levels. Applying a heading style doesn't remove a size or color set directly on the text, so clear those as well. + +### Things inside the text + +Some elements sit inside a paragraph's text without looking like separate objects: footnote marks, chips (such as a person, date or file chip), bookmark boundaries, comment anchors, and inline images and charts. Replacing or deleting a range that contains one removes it: the footnote is gone, the chip is deleted, the bookmark moves, the chart disappears. + +- **Before you edit a sentence, check what is inside it**, and change the text on either side of such an element rather than through it. +- **A paragraph with no visible text can still hold an image or a chart**, and a floating shape is anchored to a paragraph. Deleting the paragraph deletes them, so check before you delete an empty-looking paragraph. +- **In a section set in columns, edit the text inside the section.** Don't remove the section break that holds the column setting. + +### Templates + +- **Fill the placeholders and keep the structure.** Never delete an image placeholder or a signature line while filling a template; removing one breaks the template for the next person who uses it. +- **Find every placeholder before you fill any.** Templates often use typed markers such as [CLIENT NAME]. If a template has none, find each section by its heading and write after it. + +### Comments + +- **Reply in the thread.** When you respond to a comment, reply to it; don't start a new comment. If your tool can't reply to comments, give the user the reply text to paste into each thread. +- **One thread per topic.** Before you add a comment, look for an existing thread on the same text or topic, including one you left earlier, and reply there. Several comments stacked on one paragraph make it unclear which note is current. +- **Leave comments where they are.** Don't delete or resolve a comment unless the user asks. Reply once per comment; don't add a second reply that says nothing new. +- **When you edit commented text, change words inside the commented range, not all of it.** A comment is attached to its text, and replacing all of that text deletes the comment and its replies. Make the edit first and reply after it, so that no reply describes an edit that didn't happen. +- **"Address the comments" means every comment.** Handle each one: make the edit it asks for, if any, reply with a one-line note, and leave the thread open unless the user asks you to resolve it. + +### Suggestions + +- **Use real suggestions**, never strikethrough and colored text made to look like them. The reviewer needs to accept or reject each change. +- **Don't accept or reject changes, or delete comments, to clean up.** In a review, the suggestions and comment threads are the work product: accepting them erases the record of what changed, and deleting comments erases the reviewers' notes. "Clean up the document" means fix the formatting. Accept, reject or delete only the ones the user names. +- **Mark only the words that change.** A paragraph replaced as a whole shows as the whole paragraph deleted and inserted again, and the reviewer can't see what changed. Replace only the words that change, and make several small changes in one paragraph as separate edits. To change a cap from twelve months to six, the change is "twelve (12)" to "six (6)", not the sentence it sits in. + +### After structural changes + +- **Keep the table of contents current.** After you add, remove or rename a heading in a document that has a table of contents, update it in the same step: a table of contents that lists "Section 4: Risk Factors" when section 4 is now "Liquidity" is a visible defect. Update an existing one; don't add one the document doesn't have unless the user asks for it. If your tool can't update it, tell the user it needs updating. +- **Remove everything in a section you delete.** Deleting a section's paragraphs can leave its table or images behind. Count the tables before and after, and check the count dropped by the number you removed. +- **Before removing a duplicate section, read both copies.** Keep the one whose formatting matches the rest of the document, and tell the user which one you kept and why. +- **Clear what deletions leave behind:** empty tables, two or more blank paragraphs in a row (collapse them to one), list items cut off from their list, and blank paragraphs at the end that push an empty last page. + +### Check your edits + +After each edit, read back what you changed: the text, its style, its font and any list marker. After an edit that can move the layout, such as added or removed content, a table or formatting change, or a fix to something that looked wrong, also check the rendered pages, including the pages around the change, against the list under Check the result above. + +# Docs and Drive tools + +## Applying the design rules + +Some of the design rules above act on a named style (Normal text, Heading 1 to 6) or on a page-number field, and the Docs API can apply a named style but not change one, and can't insert a page number. Others depend on the page setup. Do this instead: + +- **Body font, such as Times New Roman for a new legal draft:** set it once over the whole body with `updateTextStyle` (`weightedFontFamily`, `fields: "weightedFontFamily"`) after the text is in, and give text you add later the same font. +- **Keep each heading with the next paragraph:** set `keepWithNext: true` on each heading paragraph with `updateParagraphStyle` (`fields: "keepWithNext"`). +- **Make every heading at one level smaller or larger:** tell the user they can resize one heading at that level and then choose Format > Paragraph styles > Heading N > Update 'Heading N' to match, which changes the style itself. If they want you to do it instead, set the same size on every heading at that level in one batch. +- **Page numbers:** tell the user to add them with Insert > Page numbers. +- **Whether a tab is pageless:** in the `read_doc` result, check the tab you are editing. It is pageless when `documentFormat.documentMode` is `PAGELESS` in its `documentTab.documentStyle`, or in the top-level `documentStyle` when there is no `tabs` key. In a pageless tab, don't add page numbers, headers, footers or columns; tell the user to switch it to pages first, with Format > Switch to Pages format. +- **Page setup, when the user asks for a change:** send `updateDocumentStyle` with `tabId` on the request itself (it has no location or range), the new values in `documentStyle`, and only those field names in `fields`, without the `documentStyle.` prefix: `{"updateDocumentStyle": {"tabId": "t.0", "documentStyle": {"marginTop": {"magnitude": 54, "unit": "PT"}}, "fields": "marginTop"}}`. A field named in `fields` but missing from `documentStyle` is cleared, so never use `*`, which names every field. + +These are based on the Docs API reference and have not been tested through the connector. + +## Create + +Call Drive `create_file` with `contentMimeType: "text/html"` and the document as `textContent`. Drive converts `

`, `

`, `

`, `