diff --git a/agent-lib.lock.json b/agent-lib.lock.json index ca4f151..924525e 100644 --- a/agent-lib.lock.json +++ b/agent-lib.lock.json @@ -1,60 +1,6 @@ { "version": 2, "sources": { - "anthropic": { - "url": "https://github.com/anthropics/skills.git", - "ref": "main", - "rev": "3b3fad96af16a10759d930941b4520ba0c40edae", - "selection": { - "mode": "include", - "include": [ - "docx", - "pdf", - "pptx", - "skill-creator", - "xlsx" - ] - }, - "inventory": { - "skills": [ - "docx", - "pdf", - "pptx", - "skill-creator", - "xlsx" - ] - }, - "warnings": [ - "skills/academy-guide: malformed frontmatter line (missing colon): Stop and check this skill before finishing any reply to a question about how", - "skills/academy-guide: malformed frontmatter line (missing colon): about artifacts, projects, skills, plugins, connectors, MCP; requests about", - "skills/academy-guide: malformed frontmatter line (missing colon): after consulting product documentation to answer how a Claude feature works,", - "skills/academy-guide: malformed frontmatter line (missing colon): also check here for a matching course or tutorial — a docs-grounded answer", - "skills/academy-guide: malformed frontmatter line (missing colon): and an Academy recommendation belong together. Only recommend on a strong", - "skills/academy-guide: malformed frontmatter line (missing colon): match; never invent Academy content.", - "skills/academy-guide: malformed frontmatter line (missing colon): rolling Claude out to a team, class, or organization; and any ask for", - "skills/academy-guide: malformed frontmatter line (missing colon): started with\", \"what can Claude do\", \"teach me\", \"learn to use\"; questions", - "skills/academy-guide: malformed frontmatter line (missing colon): the user is learning how to use a feature or product — not when they are", - "skills/academy-guide: malformed frontmatter line (missing colon): to use Claude or a Claude product — it recommends matching courses,", - "skills/academy-guide: malformed frontmatter line (missing colon): training materials, onboarding content, or learning resources. Use it when", - "skills/academy-guide: malformed frontmatter line (missing colon): tutorials, and use cases from Claude Academy (academy.claude.com),", - "skills/claude-api: malformed frontmatter line (missing colon): Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.", - "skills/discernment-nudge: malformed frontmatter line (missing colon): After you give a substantive answer or draft that the user may act on", - "skills/discernment-nudge: malformed frontmatter line (missing colon): and then, if it applies, append 2-3 short follow-up questions, each", - "skills/discernment-nudge: malformed frontmatter line (missing colon): assemble a file from content they provided, is writing code they will", - "skills/discernment-nudge: malformed frontmatter line (missing colon): boundaries and the exact output format.", - "skills/discernment-nudge: malformed frontmatter line (missing colon): educational explanation, asked you only to format, convert, or", - "skills/discernment-nudge: malformed frontmatter line (missing colon): interpretation of data, factual claims they may rely on, or a", - "skills/discernment-nudge: malformed frontmatter line (missing colon): missing context. Do this at most once per conversation. Skip it when", - "skills/discernment-nudge: malformed frontmatter line (missing colon): multi-step argument — invoke this skill BEFORE finalizing your reply", - "skills/discernment-nudge: malformed frontmatter line (missing colon): pitches, proposals, or emails, estimates or projections, analysis or", - "skills/discernment-nudge: malformed frontmatter line (missing colon): run, is doing creative writing or casual chat, or already asked you", - "skills/discernment-nudge: malformed frontmatter line (missing colon): the user asked a trivial how-to or simple lookup, wants a purely", - "skills/discernment-nudge: malformed frontmatter line (missing colon): tied to something specific in what you just produced, that help the", - "skills/discernment-nudge: malformed frontmatter line (missing colon): to double-check, cite, or review — the skill file explains these", - "skills/discernment-nudge: malformed frontmatter line (missing colon): user check key facts, probe the reasoning or assumptions, and notice", - "skills/discernment-nudge: malformed frontmatter line (missing colon): — advice or recommendations, drafted artifacts such as goals, plans," - ] - }, "mattpocock": { "url": "https://github.com/mattpocock/skills.git", "ref": "main", diff --git a/external/anthropic/skills/docx/LICENSE.txt b/external/anthropic/skills/docx/LICENSE.txt deleted file mode 100644 index c55ab42..0000000 --- a/external/anthropic/skills/docx/LICENSE.txt +++ /dev/null @@ -1,30 +0,0 @@ -© 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/external/anthropic/skills/docx/SKILL.md b/external/anthropic/skills/docx/SKILL.md deleted file mode 100644 index fb954a4..0000000 --- a/external/anthropic/skills/docx/SKILL.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -name: docx -description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files) or Word templates (.dotx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', '.dotx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx or .dotx files, inserting or replacing images in documents, performing 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 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation." -license: Proprietary. LICENSE.txt has complete terms ---- - -# DOCX creation, editing, and analysis - -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/external/anthropic/skills/docx/scripts/__init__.py b/external/anthropic/skills/docx/scripts/__init__.py deleted file mode 100755 index 8b13789..0000000 --- a/external/anthropic/skills/docx/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/external/anthropic/skills/docx/scripts/accept_changes.py b/external/anthropic/skills/docx/scripts/accept_changes.py deleted file mode 100755 index 8e36316..0000000 --- a/external/anthropic/skills/docx/scripts/accept_changes.py +++ /dev/null @@ -1,135 +0,0 @@ -"""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/external/anthropic/skills/docx/scripts/comment.py b/external/anthropic/skills/docx/scripts/comment.py deleted file mode 100755 index 46ed5f5..0000000 --- a/external/anthropic/skills/docx/scripts/comment.py +++ /dev/null @@ -1,368 +0,0 @@ -"""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/external/anthropic/skills/docx/scripts/merge_runs.py b/external/anthropic/skills/docx/scripts/merge_runs.py deleted file mode 100755 index 4c7c1bf..0000000 --- a/external/anthropic/skills/docx/scripts/merge_runs.py +++ /dev/null @@ -1,310 +0,0 @@ -"""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/external/anthropic/skills/docx/scripts/office/helpers/__init__.py b/external/anthropic/skills/docx/scripts/office/helpers/__init__.py deleted file mode 100644 index d3c5817..0000000 --- a/external/anthropic/skills/docx/scripts/office/helpers/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -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 - if _SCHEME_RE.match(target): - return None - - target = urllib.parse.unquote(target) - - 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) - return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]).lstrip("./") - - -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/external/anthropic/skills/docx/scripts/office/helpers/pptx_chart.py b/external/anthropic/skills/docx/scripts/office/helpers/pptx_chart.py deleted file mode 100644 index 209cb7c..0000000 --- a/external/anthropic/skills/docx/scripts/office/helpers/pptx_chart.py +++ /dev/null @@ -1,170 +0,0 @@ -"""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/external/anthropic/skills/docx/scripts/office/helpers/pptx_slide.py b/external/anthropic/skills/docx/scripts/office/helpers/pptx_slide.py deleted file mode 100644 index 22f9aee..0000000 --- a/external/anthropic/skills/docx/scripts/office/helpers/pptx_slide.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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/external/anthropic/skills/docx/scripts/office/helpers/pptx_theme.py b/external/anthropic/skills/docx/scripts/office/helpers/pptx_theme.py deleted file mode 100644 index 8446620..0000000 --- a/external/anthropic/skills/docx/scripts/office/helpers/pptx_theme.py +++ /dev/null @@ -1,114 +0,0 @@ -"""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/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd deleted file mode 100644 index 6454ef9..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +++ /dev/null @@ -1,1499 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd deleted file mode 100644 index afa4f46..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd deleted file mode 100644 index 64e66b8..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +++ /dev/null @@ -1,1085 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd deleted file mode 100644 index 687eea8..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd deleted file mode 100644 index 6ac81b0..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +++ /dev/null @@ -1,3081 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd deleted file mode 100644 index 1dbf051..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd deleted file mode 100644 index f1af17d..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd deleted file mode 100644 index 0a185ab..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd deleted file mode 100644 index 14ef488..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +++ /dev/null @@ -1,1676 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd deleted file mode 100644 index c20f3bf..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd deleted file mode 100644 index ac60252..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd deleted file mode 100644 index 424b8ba..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd deleted file mode 100644 index 2bddce2..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd deleted file mode 100644 index 8a8c18b..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd deleted file mode 100644 index 5c42706..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd deleted file mode 100644 index 853c341..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd deleted file mode 100644 index da835ee..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd deleted file mode 100644 index 87ad265..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +++ /dev/null @@ -1,582 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd deleted file mode 100644 index 9e86f1b..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd deleted file mode 100644 index d0be42e..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +++ /dev/null @@ -1,4439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd deleted file mode 100644 index 8821dd1..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +++ /dev/null @@ -1,570 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd deleted file mode 100644 index ca2575c..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +++ /dev/null @@ -1,509 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd deleted file mode 100644 index dd079e6..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd deleted file mode 100644 index 3dd6cf6..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd deleted file mode 100644 index f1041e3..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd deleted file mode 100644 index 9c5b7a6..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +++ /dev/null @@ -1,3646 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd deleted file mode 100644 index 0f13678..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - 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/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd deleted file mode 100644 index a6de9d2..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd deleted file mode 100644 index 10e978b..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd deleted file mode 100644 index 4248bf7..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd deleted file mode 100644 index 5649746..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/mce/mc.xsd b/external/anthropic/skills/docx/scripts/office/schemas/mce/mc.xsd deleted file mode 100644 index ef72545..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/mce/mc.xsd +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd deleted file mode 100644 index f65f777..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd deleted file mode 100644 index 6b00755..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd deleted file mode 100644 index f321d33..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd deleted file mode 100644 index 364c6a9..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd deleted file mode 100644 index fed9d15..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd deleted file mode 100644 index 680cf15..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd deleted file mode 100644 index 89ada90..0000000 --- a/external/anthropic/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/external/anthropic/skills/docx/scripts/office/soffice.py b/external/anthropic/skills/docx/scripts/office/soffice.py deleted file mode 100644 index 0b4c99d..0000000 --- a/external/anthropic/skills/docx/scripts/office/soffice.py +++ /dev/null @@ -1,192 +0,0 @@ -""" -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) - 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/external/anthropic/skills/docx/scripts/office/validate.py b/external/anthropic/skills/docx/scripts/office/validate.py deleted file mode 100755 index 29ca186..0000000 --- a/external/anthropic/skills/docx/scripts/office/validate.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -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/external/anthropic/skills/docx/scripts/office/validators/__init__.py b/external/anthropic/skills/docx/scripts/office/validators/__init__.py deleted file mode 100644 index db092ec..0000000 --- a/external/anthropic/skills/docx/scripts/office/validators/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -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/external/anthropic/skills/docx/scripts/office/validators/base.py b/external/anthropic/skills/docx/scripts/office/validators/base.py deleted file mode 100644 index 33fc97b..0000000 --- a/external/anthropic/skills/docx/scripts/office/validators/base.py +++ /dev/null @@ -1,875 +0,0 @@ -""" -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)) - - with open(xml_file, "r") as f: - xml_doc = lxml.etree.parse(f) - - 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/external/anthropic/skills/docx/scripts/office/validators/docx.py b/external/anthropic/skills/docx/scripts/office/validators/docx.py deleted file mode 100644 index b181499..0000000 --- a/external/anthropic/skills/docx/scripts/office/validators/docx.py +++ /dev/null @@ -1,466 +0,0 @@ -""" -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/external/anthropic/skills/docx/scripts/office/validators/pptx.py b/external/anthropic/skills/docx/scripts/office/validators/pptx.py deleted file mode 100644 index 318f0e6..0000000 --- a/external/anthropic/skills/docx/scripts/office/validators/pptx.py +++ /dev/null @@ -1,441 +0,0 @@ -""" -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/external/anthropic/skills/docx/scripts/office/validators/redlining.py b/external/anthropic/skills/docx/scripts/office/validators/redlining.py deleted file mode 100644 index 4185c51..0000000 --- a/external/anthropic/skills/docx/scripts/office/validators/redlining.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -Validator for tracked changes in Word documents. - -Detects untracked edits in word/document.xml: 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. - -Only the document body is compared. Headers, footers, footnotes and endnotes -are separate parts and are not checked. -""" - -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 - - -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 - - try: - modified_tree = ET.parse(modified_file) - modified_root = modified_tree.getroot() - original_tree = ET.parse(original_file) - original_root = original_tree.getroot() - except (ET.ParseError, DefusedXmlException) as e: - print(f"FAILED - Error parsing XML files: {e}") - return False - - new_changes = self._new_tracked_changes(original_root, modified_root) - self._remove_tracked_changes(modified_root, new_changes) - - modified_text = self._extract_text_content(modified_root) - original_text = self._extract_text_content(original_root) - - if modified_text != original_text: - error_message = self._generate_detailed_diff( - original_text, modified_text - ) - print(error_message) - return False - - if self.verbose: - print( - f"PASSED - All {len(new_changes)} change(s) against the original " - "are properly tracked" - ) - return True - - def _tracked_change_elements(self, root): - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - return [elem for elem in root.iter() if elem.tag in (ins_tag, del_tag)] - - 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, original_text, modified_text): - 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 ", - "", - ] - - git_diff = self._get_git_word_diff(original_text, modified_text) - if git_diff: - error_parts.extend(["Differences:", "============", git_diff]) - else: - error_parts.append("Unable to generate word diff (git not available)") - - 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, - text=True, - ) - - 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, - text=True, - ) - - 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): - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - - for parent in root.iter(): - to_remove = [] - for child in parent: - if child.tag == ins_tag and child in targets: - to_remove.append(child) - for elem in to_remove: - parent.remove(elem) - - deltext_tag = f"{{{self.namespaces['w']}}}delText" - t_tag = f"{{{self.namespaces['w']}}}t" - - for parent in root.iter(): - to_process = [] - for child in parent: - if child.tag == del_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(): - if elem.tag == deltext_tag: - elem.tag = t_tag - - for child in reversed(list(del_elem)): - parent.insert(del_index, child) - parent.remove(del_elem) - - def _extract_text_content(self, root): - p_tag = f"{{{self.namespaces['w']}}}p" - t_tag = f"{{{self.namespaces['w']}}}t" - - paragraphs = [] - for p_elem in root.findall(f".//{p_tag}"): - text_parts = [] - for t_elem in p_elem.findall(f".//{t_tag}"): - text_parts.append(self._rendered_text(t_elem)) - paragraph_text = "".join(text_parts) - if paragraph_text: - paragraphs.append(paragraph_text) - - return "\n".join(paragraphs) - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/external/anthropic/skills/docx/scripts/templates/comments.xml b/external/anthropic/skills/docx/scripts/templates/comments.xml deleted file mode 100644 index cd01a7d..0000000 --- a/external/anthropic/skills/docx/scripts/templates/comments.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/external/anthropic/skills/docx/scripts/templates/commentsExtended.xml b/external/anthropic/skills/docx/scripts/templates/commentsExtended.xml deleted file mode 100644 index 411003c..0000000 --- a/external/anthropic/skills/docx/scripts/templates/commentsExtended.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/external/anthropic/skills/docx/scripts/templates/commentsExtensible.xml b/external/anthropic/skills/docx/scripts/templates/commentsExtensible.xml deleted file mode 100644 index f5572d7..0000000 --- a/external/anthropic/skills/docx/scripts/templates/commentsExtensible.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/external/anthropic/skills/docx/scripts/templates/commentsIds.xml b/external/anthropic/skills/docx/scripts/templates/commentsIds.xml deleted file mode 100644 index 32f1629..0000000 --- a/external/anthropic/skills/docx/scripts/templates/commentsIds.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/external/anthropic/skills/docx/scripts/templates/people.xml b/external/anthropic/skills/docx/scripts/templates/people.xml deleted file mode 100644 index 3803d2d..0000000 --- a/external/anthropic/skills/docx/scripts/templates/people.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/external/anthropic/skills/pdf/LICENSE.txt b/external/anthropic/skills/pdf/LICENSE.txt deleted file mode 100644 index c55ab42..0000000 --- a/external/anthropic/skills/pdf/LICENSE.txt +++ /dev/null @@ -1,30 +0,0 @@ -© 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/external/anthropic/skills/pdf/SKILL.md b/external/anthropic/skills/pdf/SKILL.md deleted file mode 100644 index d3e046a..0000000 --- a/external/anthropic/skills/pdf/SKILL.md +++ /dev/null @@ -1,314 +0,0 @@ ---- -name: pdf -description: Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill. -license: Proprietary. LICENSE.txt has complete terms ---- - -# PDF Processing Guide - -## Overview - -This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see REFERENCE.md. If you need to fill out a PDF form, read FORMS.md and follow its instructions. - -## Quick Start - -```python -from pypdf import PdfReader, PdfWriter - -# Read a PDF -reader = PdfReader("document.pdf") -print(f"Pages: {len(reader.pages)}") - -# Extract text -text = "" -for page in reader.pages: - text += page.extract_text() -``` - -## Python Libraries - -### pypdf - Basic Operations - -#### Merge PDFs -```python -from pypdf import PdfWriter, PdfReader - -writer = PdfWriter() -for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]: - reader = PdfReader(pdf_file) - for page in reader.pages: - writer.add_page(page) - -with open("merged.pdf", "wb") as output: - writer.write(output) -``` - -#### Split PDF -```python -reader = PdfReader("input.pdf") -for i, page in enumerate(reader.pages): - writer = PdfWriter() - writer.add_page(page) - with open(f"page_{i+1}.pdf", "wb") as output: - writer.write(output) -``` - -#### Extract Metadata -```python -reader = PdfReader("document.pdf") -meta = reader.metadata -print(f"Title: {meta.title}") -print(f"Author: {meta.author}") -print(f"Subject: {meta.subject}") -print(f"Creator: {meta.creator}") -``` - -#### Rotate Pages -```python -reader = PdfReader("input.pdf") -writer = PdfWriter() - -page = reader.pages[0] -page.rotate(90) # Rotate 90 degrees clockwise -writer.add_page(page) - -with open("rotated.pdf", "wb") as output: - writer.write(output) -``` - -### pdfplumber - Text and Table Extraction - -#### Extract Text with Layout -```python -import pdfplumber - -with pdfplumber.open("document.pdf") as pdf: - for page in pdf.pages: - text = page.extract_text() - print(text) -``` - -#### Extract Tables -```python -with pdfplumber.open("document.pdf") as pdf: - for i, page in enumerate(pdf.pages): - tables = page.extract_tables() - for j, table in enumerate(tables): - print(f"Table {j+1} on page {i+1}:") - for row in table: - print(row) -``` - -#### Advanced Table Extraction -```python -import pandas as pd - -with pdfplumber.open("document.pdf") as pdf: - all_tables = [] - for page in pdf.pages: - tables = page.extract_tables() - for table in tables: - if table: # Check if table is not empty - df = pd.DataFrame(table[1:], columns=table[0]) - all_tables.append(df) - -# Combine all tables -if all_tables: - combined_df = pd.concat(all_tables, ignore_index=True) - combined_df.to_excel("extracted_tables.xlsx", index=False) -``` - -### reportlab - Create PDFs - -#### Basic PDF Creation -```python -from reportlab.lib.pagesizes import letter -from reportlab.pdfgen import canvas - -c = canvas.Canvas("hello.pdf", pagesize=letter) -width, height = letter - -# Add text -c.drawString(100, height - 100, "Hello World!") -c.drawString(100, height - 120, "This is a PDF created with reportlab") - -# Add a line -c.line(100, height - 140, 400, height - 140) - -# Save -c.save() -``` - -#### Create PDF with Multiple Pages -```python -from reportlab.lib.pagesizes import letter -from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak -from reportlab.lib.styles import getSampleStyleSheet - -doc = SimpleDocTemplate("report.pdf", pagesize=letter) -styles = getSampleStyleSheet() -story = [] - -# Add content -title = Paragraph("Report Title", styles['Title']) -story.append(title) -story.append(Spacer(1, 12)) - -body = Paragraph("This is the body of the report. " * 20, styles['Normal']) -story.append(body) -story.append(PageBreak()) - -# Page 2 -story.append(Paragraph("Page 2", styles['Heading1'])) -story.append(Paragraph("Content for page 2", styles['Normal'])) - -# Build PDF -doc.build(story) -``` - -#### Subscripts and Superscripts - -**IMPORTANT**: Never use Unicode subscript/superscript characters (₀₁₂₃₄₅₆₇₈₉, ⁰¹²³⁴⁵⁶⁷⁸⁹) in ReportLab PDFs. The built-in fonts do not include these glyphs, causing them to render as solid black boxes. - -Instead, use ReportLab's XML markup tags in Paragraph objects: -```python -from reportlab.platypus import Paragraph -from reportlab.lib.styles import getSampleStyleSheet - -styles = getSampleStyleSheet() - -# Subscripts: use tag -chemical = Paragraph("H2O", styles['Normal']) - -# Superscripts: use tag -squared = Paragraph("x2 + y2", styles['Normal']) -``` - -For canvas-drawn text (not Paragraph objects), manually adjust font the size and position rather than using Unicode subscripts/superscripts. - -## Command-Line Tools - -### pdftotext (poppler-utils) -```bash -# Extract text -pdftotext input.pdf output.txt - -# Extract text preserving layout -pdftotext -layout input.pdf output.txt - -# Extract specific pages -pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5 -``` - -### qpdf -```bash -# Merge PDFs -qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf - -# Split pages -qpdf input.pdf --pages . 1-5 -- pages1-5.pdf -qpdf input.pdf --pages . 6-10 -- pages6-10.pdf - -# Rotate pages -qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees - -# Remove password -qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf -``` - -### pdftk (if available) -```bash -# Merge -pdftk file1.pdf file2.pdf cat output merged.pdf - -# Split -pdftk input.pdf burst - -# Rotate -pdftk input.pdf rotate 1east output rotated.pdf -``` - -## Common Tasks - -### Extract Text from Scanned PDFs -```python -# Requires: pip install pytesseract pdf2image -import pytesseract -from pdf2image import convert_from_path - -# Convert PDF to images -images = convert_from_path('scanned.pdf') - -# OCR each page -text = "" -for i, image in enumerate(images): - text += f"Page {i+1}:\n" - text += pytesseract.image_to_string(image) - text += "\n\n" - -print(text) -``` - -### Add Watermark -```python -from pypdf import PdfReader, PdfWriter - -# Create watermark (or load existing) -watermark = PdfReader("watermark.pdf").pages[0] - -# Apply to all pages -reader = PdfReader("document.pdf") -writer = PdfWriter() - -for page in reader.pages: - page.merge_page(watermark) - writer.add_page(page) - -with open("watermarked.pdf", "wb") as output: - writer.write(output) -``` - -### Extract Images -```bash -# Using pdfimages (poppler-utils) -pdfimages -j input.pdf output_prefix - -# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc. -``` - -### Password Protection -```python -from pypdf import PdfReader, PdfWriter - -reader = PdfReader("input.pdf") -writer = PdfWriter() - -for page in reader.pages: - writer.add_page(page) - -# Add password -writer.encrypt("userpassword", "ownerpassword") - -with open("encrypted.pdf", "wb") as output: - writer.write(output) -``` - -## Quick Reference - -| Task | Best Tool | Command/Code | -|------|-----------|--------------| -| Merge PDFs | pypdf | `writer.add_page(page)` | -| Split PDFs | pypdf | One page per file | -| Extract text | pdfplumber | `page.extract_text()` | -| Extract tables | pdfplumber | `page.extract_tables()` | -| Create PDFs | reportlab | Canvas or Platypus | -| Command line merge | qpdf | `qpdf --empty --pages ...` | -| OCR scanned PDFs | pytesseract | Convert to image first | -| Fill PDF forms | pdf-lib or pypdf (see FORMS.md) | See FORMS.md | - -## Next Steps - -- For advanced pypdfium2 usage, see REFERENCE.md -- For JavaScript libraries (pdf-lib), see REFERENCE.md -- If you need to fill out a PDF form, follow the instructions in FORMS.md -- For troubleshooting guides, see REFERENCE.md diff --git a/external/anthropic/skills/pdf/forms.md b/external/anthropic/skills/pdf/forms.md deleted file mode 100644 index 6e7e1e0..0000000 --- a/external/anthropic/skills/pdf/forms.md +++ /dev/null @@ -1,294 +0,0 @@ -**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.** - -If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory: - `python scripts/check_fillable_fields `, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions. - -# Fillable fields -If the PDF has fillable form fields: -- Run this script from this file's directory: `python scripts/extract_form_field_info.py `. It will create a JSON file with a list of fields in this format: -``` -[ - { - "field_id": (unique ID for the field), - "page": (page number, 1-based), - "rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page), - "type": ("text", "checkbox", "radio_group", or "choice"), - }, - // Checkboxes have "checked_value" and "unchecked_value" properties: - { - "field_id": (unique ID for the field), - "page": (page number, 1-based), - "type": "checkbox", - "checked_value": (Set the field to this value to check the checkbox), - "unchecked_value": (Set the field to this value to uncheck the checkbox), - }, - // Radio groups have a "radio_options" list with the possible choices. - { - "field_id": (unique ID for the field), - "page": (page number, 1-based), - "type": "radio_group", - "radio_options": [ - { - "value": (set the field to this value to select this radio option), - "rect": (bounding box for the radio button for this option) - }, - // Other radio options - ] - }, - // Multiple choice fields have a "choice_options" list with the possible choices: - { - "field_id": (unique ID for the field), - "page": (page number, 1-based), - "type": "choice", - "choice_options": [ - { - "value": (set the field to this value to select this option), - "text": (display text of the option) - }, - // Other choice options - ], - } -] -``` -- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory): -`python scripts/convert_pdf_to_images.py ` -Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates). -- Create a `field_values.json` file in this format with the values to be entered for each field: -``` -[ - { - "field_id": "last_name", // Must match the field_id from `extract_form_field_info.py` - "description": "The user's last name", - "page": 1, // Must match the "page" value in field_info.json - "value": "Simpson" - }, - { - "field_id": "Checkbox12", - "description": "Checkbox to be checked if the user is 18 or over", - "page": 1, - "value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options". - }, - // more fields -] -``` -- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF: -`python scripts/fill_fillable_fields.py ` -This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again. - -# Non-fillable fields -If the PDF doesn't have fillable form fields, you'll add text annotations. First try to extract coordinates from the PDF structure (more accurate), then fall back to visual estimation if needed. - -## Step 1: Try Structure Extraction First - -Run this script to extract text labels, lines, and checkboxes with their exact PDF coordinates: -`python scripts/extract_form_structure.py form_structure.json` - -This creates a JSON file containing: -- **labels**: Every text element with exact coordinates (x0, top, x1, bottom in PDF points) -- **lines**: Horizontal lines that define row boundaries -- **checkboxes**: Small square rectangles that are checkboxes (with center coordinates) -- **row_boundaries**: Row top/bottom positions calculated from horizontal lines - -**Check the results**: If `form_structure.json` has meaningful labels (text elements that correspond to form fields), use **Approach A: Structure-Based Coordinates**. If the PDF is scanned/image-based and has few or no labels, use **Approach B: Visual Estimation**. - ---- - -## Approach A: Structure-Based Coordinates (Preferred) - -Use this when `extract_form_structure.py` found text labels in the PDF. - -### A.1: Analyze the Structure - -Read form_structure.json and identify: - -1. **Label groups**: Adjacent text elements that form a single label (e.g., "Last" + "Name") -2. **Row structure**: Labels with similar `top` values are in the same row -3. **Field columns**: Entry areas start after label ends (x0 = label.x1 + gap) -4. **Checkboxes**: Use the checkbox coordinates directly from the structure - -**Coordinate system**: PDF coordinates where y=0 is at TOP of page, y increases downward. - -### A.2: Check for Missing Elements - -The structure extraction may not detect all form elements. Common cases: -- **Circular checkboxes**: Only square rectangles are detected as checkboxes -- **Complex graphics**: Decorative elements or non-standard form controls -- **Faded or light-colored elements**: May not be extracted - -If you see form fields in the PDF images that aren't in form_structure.json, you'll need to use **visual analysis** for those specific fields (see "Hybrid Approach" below). - -### A.3: Create fields.json with PDF Coordinates - -For each field, calculate entry coordinates from the extracted structure: - -**Text fields:** -- entry x0 = label x1 + 5 (small gap after label) -- entry x1 = next label's x0, or row boundary -- entry top = same as label top -- entry bottom = row boundary line below, or label bottom + row_height - -**Checkboxes:** -- Use the checkbox rectangle coordinates directly from form_structure.json -- entry_bounding_box = [checkbox.x0, checkbox.top, checkbox.x1, checkbox.bottom] - -Create fields.json using `pdf_width` and `pdf_height` (signals PDF coordinates): -```json -{ - "pages": [ - {"page_number": 1, "pdf_width": 612, "pdf_height": 792} - ], - "form_fields": [ - { - "page_number": 1, - "description": "Last name entry field", - "field_label": "Last Name", - "label_bounding_box": [43, 63, 87, 73], - "entry_bounding_box": [92, 63, 260, 79], - "entry_text": {"text": "Smith", "font_size": 10} - }, - { - "page_number": 1, - "description": "US Citizen Yes checkbox", - "field_label": "Yes", - "label_bounding_box": [260, 200, 280, 210], - "entry_bounding_box": [285, 197, 292, 205], - "entry_text": {"text": "X"} - } - ] -} -``` - -**Important**: Use `pdf_width`/`pdf_height` and coordinates directly from form_structure.json. - -### A.4: Validate Bounding Boxes - -Before filling, check your bounding boxes for errors: -`python scripts/check_bounding_boxes.py fields.json` - -This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling. - ---- - -## Approach B: Visual Estimation (Fallback) - -Use this when the PDF is scanned/image-based and structure extraction found no usable text labels (e.g., all text shows as "(cid:X)" patterns). - -### B.1: Convert PDF to Images - -`python scripts/convert_pdf_to_images.py ` - -### B.2: Initial Field Identification - -Examine each page image to identify form sections and get **rough estimates** of field locations: -- Form field labels and their approximate positions -- Entry areas (lines, boxes, or blank spaces for text input) -- Checkboxes and their approximate locations - -For each field, note approximate pixel coordinates (they don't need to be precise yet). - -### B.3: Zoom Refinement (CRITICAL for accuracy) - -For each field, crop a region around the estimated position to refine coordinates precisely. - -**Create a zoomed crop using ImageMagick:** -```bash -magick -crop x++ +repage -``` - -Where: -- `, ` = top-left corner of crop region (use your rough estimate minus padding) -- `, ` = size of crop region (field area plus ~50px padding on each side) - -**Example:** To refine a "Name" field estimated around (100, 150): -```bash -magick images_dir/page_1.png -crop 300x80+50+120 +repage crops/name_field.png -``` - -(Note: if the `magick` command isn't available, try `convert` with the same arguments). - -**Examine the cropped image** to determine precise coordinates: -1. Identify the exact pixel where the entry area begins (after the label) -2. Identify where the entry area ends (before next field or edge) -3. Identify the top and bottom of the entry line/box - -**Convert crop coordinates back to full image coordinates:** -- full_x = crop_x + crop_offset_x -- full_y = crop_y + crop_offset_y - -Example: If the crop started at (50, 120) and the entry box starts at (52, 18) within the crop: -- entry_x0 = 52 + 50 = 102 -- entry_top = 18 + 120 = 138 - -**Repeat for each field**, grouping nearby fields into single crops when possible. - -### B.4: Create fields.json with Refined Coordinates - -Create fields.json using `image_width` and `image_height` (signals image coordinates): -```json -{ - "pages": [ - {"page_number": 1, "image_width": 1700, "image_height": 2200} - ], - "form_fields": [ - { - "page_number": 1, - "description": "Last name entry field", - "field_label": "Last Name", - "label_bounding_box": [120, 175, 242, 198], - "entry_bounding_box": [255, 175, 720, 218], - "entry_text": {"text": "Smith", "font_size": 10} - } - ] -} -``` - -**Important**: Use `image_width`/`image_height` and the refined pixel coordinates from the zoom analysis. - -### B.5: Validate Bounding Boxes - -Before filling, check your bounding boxes for errors: -`python scripts/check_bounding_boxes.py fields.json` - -This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling. - ---- - -## Hybrid Approach: Structure + Visual - -Use this when structure extraction works for most fields but misses some elements (e.g., circular checkboxes, unusual form controls). - -1. **Use Approach A** for fields that were detected in form_structure.json -2. **Convert PDF to images** for visual analysis of missing fields -3. **Use zoom refinement** (from Approach B) for the missing fields -4. **Combine coordinates**: For fields from structure extraction, use `pdf_width`/`pdf_height`. For visually-estimated fields, you must convert image coordinates to PDF coordinates: - - pdf_x = image_x * (pdf_width / image_width) - - pdf_y = image_y * (pdf_height / image_height) -5. **Use a single coordinate system** in fields.json - convert all to PDF coordinates with `pdf_width`/`pdf_height` - ---- - -## Step 2: Validate Before Filling - -**Always validate bounding boxes before filling:** -`python scripts/check_bounding_boxes.py fields.json` - -This checks for: -- Intersecting bounding boxes (which would cause overlapping text) -- Entry boxes that are too small for the specified font size - -Fix any reported errors in fields.json before proceeding. - -## Step 3: Fill the Form - -The fill script auto-detects the coordinate system and handles conversion: -`python scripts/fill_pdf_form_with_annotations.py fields.json ` - -## Step 4: Verify Output - -Convert the filled PDF to images and verify text placement: -`python scripts/convert_pdf_to_images.py ` - -If text is mispositioned: -- **Approach A**: Check that you're using PDF coordinates from form_structure.json with `pdf_width`/`pdf_height` -- **Approach B**: Check that image dimensions match and coordinates are accurate pixels -- **Hybrid**: Ensure coordinate conversions are correct for visually-estimated fields diff --git a/external/anthropic/skills/pdf/reference.md b/external/anthropic/skills/pdf/reference.md deleted file mode 100644 index 41400bf..0000000 --- a/external/anthropic/skills/pdf/reference.md +++ /dev/null @@ -1,612 +0,0 @@ -# PDF Processing Advanced Reference - -This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions. - -## pypdfium2 Library (Apache/BSD License) - -### Overview -pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement. - -### Render PDF to Images -```python -import pypdfium2 as pdfium -from PIL import Image - -# Load PDF -pdf = pdfium.PdfDocument("document.pdf") - -# Render page to image -page = pdf[0] # First page -bitmap = page.render( - scale=2.0, # Higher resolution - rotation=0 # No rotation -) - -# Convert to PIL Image -img = bitmap.to_pil() -img.save("page_1.png", "PNG") - -# Process multiple pages -for i, page in enumerate(pdf): - bitmap = page.render(scale=1.5) - img = bitmap.to_pil() - img.save(f"page_{i+1}.jpg", "JPEG", quality=90) -``` - -### Extract Text with pypdfium2 -```python -import pypdfium2 as pdfium - -pdf = pdfium.PdfDocument("document.pdf") -for i, page in enumerate(pdf): - text = page.get_text() - print(f"Page {i+1} text length: {len(text)} chars") -``` - -## JavaScript Libraries - -### pdf-lib (MIT License) - -pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment. - -#### Load and Manipulate Existing PDF -```javascript -import { PDFDocument } from 'pdf-lib'; -import fs from 'fs'; - -async function manipulatePDF() { - // Load existing PDF - const existingPdfBytes = fs.readFileSync('input.pdf'); - const pdfDoc = await PDFDocument.load(existingPdfBytes); - - // Get page count - const pageCount = pdfDoc.getPageCount(); - console.log(`Document has ${pageCount} pages`); - - // Add new page - const newPage = pdfDoc.addPage([600, 400]); - newPage.drawText('Added by pdf-lib', { - x: 100, - y: 300, - size: 16 - }); - - // Save modified PDF - const pdfBytes = await pdfDoc.save(); - fs.writeFileSync('modified.pdf', pdfBytes); -} -``` - -#### Create Complex PDFs from Scratch -```javascript -import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; -import fs from 'fs'; - -async function createPDF() { - const pdfDoc = await PDFDocument.create(); - - // Add fonts - const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); - const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); - - // Add page - const page = pdfDoc.addPage([595, 842]); // A4 size - const { width, height } = page.getSize(); - - // Add text with styling - page.drawText('Invoice #12345', { - x: 50, - y: height - 50, - size: 18, - font: helveticaBold, - color: rgb(0.2, 0.2, 0.8) - }); - - // Add rectangle (header background) - page.drawRectangle({ - x: 40, - y: height - 100, - width: width - 80, - height: 30, - color: rgb(0.9, 0.9, 0.9) - }); - - // Add table-like content - const items = [ - ['Item', 'Qty', 'Price', 'Total'], - ['Widget', '2', '$50', '$100'], - ['Gadget', '1', '$75', '$75'] - ]; - - let yPos = height - 150; - items.forEach(row => { - let xPos = 50; - row.forEach(cell => { - page.drawText(cell, { - x: xPos, - y: yPos, - size: 12, - font: helveticaFont - }); - xPos += 120; - }); - yPos -= 25; - }); - - const pdfBytes = await pdfDoc.save(); - fs.writeFileSync('created.pdf', pdfBytes); -} -``` - -#### Advanced Merge and Split Operations -```javascript -import { PDFDocument } from 'pdf-lib'; -import fs from 'fs'; - -async function mergePDFs() { - // Create new document - const mergedPdf = await PDFDocument.create(); - - // Load source PDFs - const pdf1Bytes = fs.readFileSync('doc1.pdf'); - const pdf2Bytes = fs.readFileSync('doc2.pdf'); - - const pdf1 = await PDFDocument.load(pdf1Bytes); - const pdf2 = await PDFDocument.load(pdf2Bytes); - - // Copy pages from first PDF - const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices()); - pdf1Pages.forEach(page => mergedPdf.addPage(page)); - - // Copy specific pages from second PDF (pages 0, 2, 4) - const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]); - pdf2Pages.forEach(page => mergedPdf.addPage(page)); - - const mergedPdfBytes = await mergedPdf.save(); - fs.writeFileSync('merged.pdf', mergedPdfBytes); -} -``` - -### pdfjs-dist (Apache License) - -PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser. - -#### Basic PDF Loading and Rendering -```javascript -import * as pdfjsLib from 'pdfjs-dist'; - -// Configure worker (important for performance) -pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js'; - -async function renderPDF() { - // Load PDF - const loadingTask = pdfjsLib.getDocument('document.pdf'); - const pdf = await loadingTask.promise; - - console.log(`Loaded PDF with ${pdf.numPages} pages`); - - // Get first page - const page = await pdf.getPage(1); - const viewport = page.getViewport({ scale: 1.5 }); - - // Render to canvas - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - - const renderContext = { - canvasContext: context, - viewport: viewport - }; - - await page.render(renderContext).promise; - document.body.appendChild(canvas); -} -``` - -#### Extract Text with Coordinates -```javascript -import * as pdfjsLib from 'pdfjs-dist'; - -async function extractText() { - const loadingTask = pdfjsLib.getDocument('document.pdf'); - const pdf = await loadingTask.promise; - - let fullText = ''; - - // Extract text from all pages - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const textContent = await page.getTextContent(); - - const pageText = textContent.items - .map(item => item.str) - .join(' '); - - fullText += `\n--- Page ${i} ---\n${pageText}`; - - // Get text with coordinates for advanced processing - const textWithCoords = textContent.items.map(item => ({ - text: item.str, - x: item.transform[4], - y: item.transform[5], - width: item.width, - height: item.height - })); - } - - console.log(fullText); - return fullText; -} -``` - -#### Extract Annotations and Forms -```javascript -import * as pdfjsLib from 'pdfjs-dist'; - -async function extractAnnotations() { - const loadingTask = pdfjsLib.getDocument('annotated.pdf'); - const pdf = await loadingTask.promise; - - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const annotations = await page.getAnnotations(); - - annotations.forEach(annotation => { - console.log(`Annotation type: ${annotation.subtype}`); - console.log(`Content: ${annotation.contents}`); - console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`); - }); - } -} -``` - -## Advanced Command-Line Operations - -### poppler-utils Advanced Features - -#### Extract Text with Bounding Box Coordinates -```bash -# Extract text with bounding box coordinates (essential for structured data) -pdftotext -bbox-layout document.pdf output.xml - -# The XML output contains precise coordinates for each text element -``` - -#### Advanced Image Conversion -```bash -# Convert to PNG images with specific resolution -pdftoppm -png -r 300 document.pdf output_prefix - -# Convert specific page range with high resolution -pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages - -# Convert to JPEG with quality setting -pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output -``` - -#### Extract Embedded Images -```bash -# Extract all embedded images with metadata -pdfimages -j -p document.pdf page_images - -# List image info without extracting -pdfimages -list document.pdf - -# Extract images in their original format -pdfimages -all document.pdf images/img -``` - -### qpdf Advanced Features - -#### Complex Page Manipulation -```bash -# Split PDF into groups of pages -qpdf --split-pages=3 input.pdf output_group_%02d.pdf - -# Extract specific pages with complex ranges -qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf - -# Merge specific pages from multiple PDFs -qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf -``` - -#### PDF Optimization and Repair -```bash -# Optimize PDF for web (linearize for streaming) -qpdf --linearize input.pdf optimized.pdf - -# Remove unused objects and compress -qpdf --optimize-level=all input.pdf compressed.pdf - -# Attempt to repair corrupted PDF structure -qpdf --check input.pdf -qpdf --fix-qdf damaged.pdf repaired.pdf - -# Show detailed PDF structure for debugging -qpdf --show-all-pages input.pdf > structure.txt -``` - -#### Advanced Encryption -```bash -# Add password protection with specific permissions -qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf - -# Check encryption status -qpdf --show-encryption encrypted.pdf - -# Remove password protection (requires password) -qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf -``` - -## Advanced Python Techniques - -### pdfplumber Advanced Features - -#### Extract Text with Precise Coordinates -```python -import pdfplumber - -with pdfplumber.open("document.pdf") as pdf: - page = pdf.pages[0] - - # Extract all text with coordinates - chars = page.chars - for char in chars[:10]: # First 10 characters - print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}") - - # Extract text by bounding box (left, top, right, bottom) - bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text() -``` - -#### Advanced Table Extraction with Custom Settings -```python -import pdfplumber -import pandas as pd - -with pdfplumber.open("complex_table.pdf") as pdf: - page = pdf.pages[0] - - # Extract tables with custom settings for complex layouts - table_settings = { - "vertical_strategy": "lines", - "horizontal_strategy": "lines", - "snap_tolerance": 3, - "intersection_tolerance": 15 - } - tables = page.extract_tables(table_settings) - - # Visual debugging for table extraction - img = page.to_image(resolution=150) - img.save("debug_layout.png") -``` - -### reportlab Advanced Features - -#### Create Professional Reports with Tables -```python -from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib import colors - -# Sample data -data = [ - ['Product', 'Q1', 'Q2', 'Q3', 'Q4'], - ['Widgets', '120', '135', '142', '158'], - ['Gadgets', '85', '92', '98', '105'] -] - -# Create PDF with table -doc = SimpleDocTemplate("report.pdf") -elements = [] - -# Add title -styles = getSampleStyleSheet() -title = Paragraph("Quarterly Sales Report", styles['Title']) -elements.append(title) - -# Add table with advanced styling -table = Table(data) -table.setStyle(TableStyle([ - ('BACKGROUND', (0, 0), (-1, 0), colors.grey), - ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), - ('ALIGN', (0, 0), (-1, -1), 'CENTER'), - ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), - ('FONTSIZE', (0, 0), (-1, 0), 14), - ('BOTTOMPADDING', (0, 0), (-1, 0), 12), - ('BACKGROUND', (0, 1), (-1, -1), colors.beige), - ('GRID', (0, 0), (-1, -1), 1, colors.black) -])) -elements.append(table) - -doc.build(elements) -``` - -## Complex Workflows - -### Extract Figures/Images from PDF - -#### Method 1: Using pdfimages (fastest) -```bash -# Extract all images with original quality -pdfimages -all document.pdf images/img -``` - -#### Method 2: Using pypdfium2 + Image Processing -```python -import pypdfium2 as pdfium -from PIL import Image -import numpy as np - -def extract_figures(pdf_path, output_dir): - pdf = pdfium.PdfDocument(pdf_path) - - for page_num, page in enumerate(pdf): - # Render high-resolution page - bitmap = page.render(scale=3.0) - img = bitmap.to_pil() - - # Convert to numpy for processing - img_array = np.array(img) - - # Simple figure detection (non-white regions) - mask = np.any(img_array != [255, 255, 255], axis=2) - - # Find contours and extract bounding boxes - # (This is simplified - real implementation would need more sophisticated detection) - - # Save detected figures - # ... implementation depends on specific needs -``` - -### Batch PDF Processing with Error Handling -```python -import os -import glob -from pypdf import PdfReader, PdfWriter -import logging - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def batch_process_pdfs(input_dir, operation='merge'): - pdf_files = glob.glob(os.path.join(input_dir, "*.pdf")) - - if operation == 'merge': - writer = PdfWriter() - for pdf_file in pdf_files: - try: - reader = PdfReader(pdf_file) - for page in reader.pages: - writer.add_page(page) - logger.info(f"Processed: {pdf_file}") - except Exception as e: - logger.error(f"Failed to process {pdf_file}: {e}") - continue - - with open("batch_merged.pdf", "wb") as output: - writer.write(output) - - elif operation == 'extract_text': - for pdf_file in pdf_files: - try: - reader = PdfReader(pdf_file) - text = "" - for page in reader.pages: - text += page.extract_text() - - output_file = pdf_file.replace('.pdf', '.txt') - with open(output_file, 'w', encoding='utf-8') as f: - f.write(text) - logger.info(f"Extracted text from: {pdf_file}") - - except Exception as e: - logger.error(f"Failed to extract text from {pdf_file}: {e}") - continue -``` - -### Advanced PDF Cropping -```python -from pypdf import PdfWriter, PdfReader - -reader = PdfReader("input.pdf") -writer = PdfWriter() - -# Crop page (left, bottom, right, top in points) -page = reader.pages[0] -page.mediabox.left = 50 -page.mediabox.bottom = 50 -page.mediabox.right = 550 -page.mediabox.top = 750 - -writer.add_page(page) -with open("cropped.pdf", "wb") as output: - writer.write(output) -``` - -## Performance Optimization Tips - -### 1. For Large PDFs -- Use streaming approaches instead of loading entire PDF in memory -- Use `qpdf --split-pages` for splitting large files -- Process pages individually with pypdfium2 - -### 2. For Text Extraction -- `pdftotext -bbox-layout` is fastest for plain text extraction -- Use pdfplumber for structured data and tables -- Avoid `pypdf.extract_text()` for very large documents - -### 3. For Image Extraction -- `pdfimages` is much faster than rendering pages -- Use low resolution for previews, high resolution for final output - -### 4. For Form Filling -- pdf-lib maintains form structure better than most alternatives -- Pre-validate form fields before processing - -### 5. Memory Management -```python -# Process PDFs in chunks -def process_large_pdf(pdf_path, chunk_size=10): - reader = PdfReader(pdf_path) - total_pages = len(reader.pages) - - for start_idx in range(0, total_pages, chunk_size): - end_idx = min(start_idx + chunk_size, total_pages) - writer = PdfWriter() - - for i in range(start_idx, end_idx): - writer.add_page(reader.pages[i]) - - # Process chunk - with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output: - writer.write(output) -``` - -## Troubleshooting Common Issues - -### Encrypted PDFs -```python -# Handle password-protected PDFs -from pypdf import PdfReader - -try: - reader = PdfReader("encrypted.pdf") - if reader.is_encrypted: - reader.decrypt("password") -except Exception as e: - print(f"Failed to decrypt: {e}") -``` - -### Corrupted PDFs -```bash -# Use qpdf to repair -qpdf --check corrupted.pdf -qpdf --replace-input corrupted.pdf -``` - -### Text Extraction Issues -```python -# Fallback to OCR for scanned PDFs -import pytesseract -from pdf2image import convert_from_path - -def extract_text_with_ocr(pdf_path): - images = convert_from_path(pdf_path) - text = "" - for i, image in enumerate(images): - text += pytesseract.image_to_string(image) - return text -``` - -## License Information - -- **pypdf**: BSD License -- **pdfplumber**: MIT License -- **pypdfium2**: Apache/BSD License -- **reportlab**: BSD License -- **poppler-utils**: GPL-2 License -- **qpdf**: Apache License -- **pdf-lib**: MIT License -- **pdfjs-dist**: Apache License \ No newline at end of file diff --git a/external/anthropic/skills/pdf/scripts/check_bounding_boxes.py b/external/anthropic/skills/pdf/scripts/check_bounding_boxes.py deleted file mode 100644 index 2cc5e34..0000000 --- a/external/anthropic/skills/pdf/scripts/check_bounding_boxes.py +++ /dev/null @@ -1,65 +0,0 @@ -from dataclasses import dataclass -import json -import sys - - - - -@dataclass -class RectAndField: - rect: list[float] - rect_type: str - field: dict - - -def get_bounding_box_messages(fields_json_stream) -> list[str]: - messages = [] - fields = json.load(fields_json_stream) - messages.append(f"Read {len(fields['form_fields'])} fields") - - def rects_intersect(r1, r2): - disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0] - disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1] - return not (disjoint_horizontal or disjoint_vertical) - - rects_and_fields = [] - for f in fields["form_fields"]: - rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f)) - rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f)) - - has_error = False - for i, ri in enumerate(rects_and_fields): - for j in range(i + 1, len(rects_and_fields)): - rj = rects_and_fields[j] - if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect): - has_error = True - if ri.field is rj.field: - messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})") - else: - messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})") - if len(messages) >= 20: - messages.append("Aborting further checks; fix bounding boxes and try again") - return messages - if ri.rect_type == "entry": - if "entry_text" in ri.field: - font_size = ri.field["entry_text"].get("font_size", 14) - entry_height = ri.rect[3] - ri.rect[1] - if entry_height < font_size: - has_error = True - messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.") - if len(messages) >= 20: - messages.append("Aborting further checks; fix bounding boxes and try again") - return messages - - if not has_error: - messages.append("SUCCESS: All bounding boxes are valid") - return messages - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: check_bounding_boxes.py [fields.json]") - sys.exit(1) - with open(sys.argv[1]) as f: - messages = get_bounding_box_messages(f) - for msg in messages: - print(msg) diff --git a/external/anthropic/skills/pdf/scripts/check_fillable_fields.py b/external/anthropic/skills/pdf/scripts/check_fillable_fields.py deleted file mode 100644 index 36dfb95..0000000 --- a/external/anthropic/skills/pdf/scripts/check_fillable_fields.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -from pypdf import PdfReader - - - - -reader = PdfReader(sys.argv[1]) -if (reader.get_fields()): - print("This PDF has fillable form fields") -else: - print("This PDF does not have fillable form fields; you will need to visually determine where to enter data") diff --git a/external/anthropic/skills/pdf/scripts/convert_pdf_to_images.py b/external/anthropic/skills/pdf/scripts/convert_pdf_to_images.py deleted file mode 100644 index 7939cef..0000000 --- a/external/anthropic/skills/pdf/scripts/convert_pdf_to_images.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -import sys - -from pdf2image import convert_from_path - - - - -def convert(pdf_path, output_dir, max_dim=1000): - images = convert_from_path(pdf_path, dpi=200) - - for i, image in enumerate(images): - width, height = image.size - if width > max_dim or height > max_dim: - scale_factor = min(max_dim / width, max_dim / height) - new_width = int(width * scale_factor) - new_height = int(height * scale_factor) - image = image.resize((new_width, new_height)) - - image_path = os.path.join(output_dir, f"page_{i+1}.png") - image.save(image_path) - print(f"Saved page {i+1} as {image_path} (size: {image.size})") - - print(f"Converted {len(images)} pages to PNG images") - - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("Usage: convert_pdf_to_images.py [input pdf] [output directory]") - sys.exit(1) - pdf_path = sys.argv[1] - output_directory = sys.argv[2] - convert(pdf_path, output_directory) diff --git a/external/anthropic/skills/pdf/scripts/create_validation_image.py b/external/anthropic/skills/pdf/scripts/create_validation_image.py deleted file mode 100644 index 10eadd8..0000000 --- a/external/anthropic/skills/pdf/scripts/create_validation_image.py +++ /dev/null @@ -1,37 +0,0 @@ -import json -import sys - -from PIL import Image, ImageDraw - - - - -def create_validation_image(page_number, fields_json_path, input_path, output_path): - with open(fields_json_path, 'r') as f: - data = json.load(f) - - img = Image.open(input_path) - draw = ImageDraw.Draw(img) - num_boxes = 0 - - for field in data["form_fields"]: - if field["page_number"] == page_number: - entry_box = field['entry_bounding_box'] - label_box = field['label_bounding_box'] - draw.rectangle(entry_box, outline='red', width=2) - draw.rectangle(label_box, outline='blue', width=2) - num_boxes += 2 - - img.save(output_path) - print(f"Created validation image at {output_path} with {num_boxes} bounding boxes") - - -if __name__ == "__main__": - if len(sys.argv) != 5: - print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") - sys.exit(1) - page_number = int(sys.argv[1]) - fields_json_path = sys.argv[2] - input_image_path = sys.argv[3] - output_image_path = sys.argv[4] - create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/external/anthropic/skills/pdf/scripts/extract_form_field_info.py b/external/anthropic/skills/pdf/scripts/extract_form_field_info.py deleted file mode 100644 index 64cd470..0000000 --- a/external/anthropic/skills/pdf/scripts/extract_form_field_info.py +++ /dev/null @@ -1,122 +0,0 @@ -import json -import sys - -from pypdf import PdfReader - - - - -def get_full_annotation_field_id(annotation): - components = [] - while annotation: - field_name = annotation.get('/T') - if field_name: - components.append(field_name) - annotation = annotation.get('/Parent') - return ".".join(reversed(components)) if components else None - - -def make_field_dict(field, field_id): - field_dict = {"field_id": field_id} - ft = field.get('/FT') - if ft == "/Tx": - field_dict["type"] = "text" - elif ft == "/Btn": - field_dict["type"] = "checkbox" - states = field.get("/_States_", []) - if len(states) == 2: - if "/Off" in states: - field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1] - field_dict["unchecked_value"] = "/Off" - else: - print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.") - field_dict["checked_value"] = states[0] - field_dict["unchecked_value"] = states[1] - elif ft == "/Ch": - field_dict["type"] = "choice" - states = field.get("/_States_", []) - field_dict["choice_options"] = [{ - "value": state[0], - "text": state[1], - } for state in states] - else: - field_dict["type"] = f"unknown ({ft})" - return field_dict - - -def get_field_info(reader: PdfReader): - fields = reader.get_fields() - - field_info_by_id = {} - possible_radio_names = set() - - for field_id, field in fields.items(): - if field.get("/Kids"): - if field.get("/FT") == "/Btn": - possible_radio_names.add(field_id) - continue - field_info_by_id[field_id] = make_field_dict(field, field_id) - - - radio_fields_by_id = {} - - for page_index, page in enumerate(reader.pages): - annotations = page.get('/Annots', []) - for ann in annotations: - field_id = get_full_annotation_field_id(ann) - if field_id in field_info_by_id: - field_info_by_id[field_id]["page"] = page_index + 1 - field_info_by_id[field_id]["rect"] = ann.get('/Rect') - elif field_id in possible_radio_names: - try: - on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"] - except KeyError: - continue - if len(on_values) == 1: - rect = ann.get("/Rect") - if field_id not in radio_fields_by_id: - radio_fields_by_id[field_id] = { - "field_id": field_id, - "type": "radio_group", - "page": page_index + 1, - "radio_options": [], - } - radio_fields_by_id[field_id]["radio_options"].append({ - "value": on_values[0], - "rect": rect, - }) - - fields_with_location = [] - for field_info in field_info_by_id.values(): - if "page" in field_info: - fields_with_location.append(field_info) - else: - print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring") - - def sort_key(f): - if "radio_options" in f: - rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0] - else: - rect = f.get("rect") or [0, 0, 0, 0] - adjusted_position = [-rect[1], rect[0]] - return [f.get("page"), adjusted_position] - - sorted_fields = fields_with_location + list(radio_fields_by_id.values()) - sorted_fields.sort(key=sort_key) - - return sorted_fields - - -def write_field_info(pdf_path: str, json_output_path: str): - reader = PdfReader(pdf_path) - field_info = get_field_info(reader) - with open(json_output_path, "w") as f: - json.dump(field_info, f, indent=2) - print(f"Wrote {len(field_info)} fields to {json_output_path}") - - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("Usage: extract_form_field_info.py [input pdf] [output json]") - sys.exit(1) - write_field_info(sys.argv[1], sys.argv[2]) diff --git a/external/anthropic/skills/pdf/scripts/extract_form_structure.py b/external/anthropic/skills/pdf/scripts/extract_form_structure.py deleted file mode 100755 index f219e7d..0000000 --- a/external/anthropic/skills/pdf/scripts/extract_form_structure.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Extract form structure from a non-fillable PDF. - -This script analyzes the PDF to find: -- Text labels with their exact coordinates -- Horizontal lines (row boundaries) -- Checkboxes (small rectangles) - -Output: A JSON file with the form structure that can be used to generate -accurate field coordinates for filling. - -Usage: python extract_form_structure.py -""" - -import json -import sys -import pdfplumber - - -def extract_form_structure(pdf_path): - structure = { - "pages": [], - "labels": [], - "lines": [], - "checkboxes": [], - "row_boundaries": [] - } - - with pdfplumber.open(pdf_path) as pdf: - for page_num, page in enumerate(pdf.pages, 1): - structure["pages"].append({ - "page_number": page_num, - "width": float(page.width), - "height": float(page.height) - }) - - words = page.extract_words() - for word in words: - structure["labels"].append({ - "page": page_num, - "text": word["text"], - "x0": round(float(word["x0"]), 1), - "top": round(float(word["top"]), 1), - "x1": round(float(word["x1"]), 1), - "bottom": round(float(word["bottom"]), 1) - }) - - for line in page.lines: - if abs(float(line["x1"]) - float(line["x0"])) > page.width * 0.5: - structure["lines"].append({ - "page": page_num, - "y": round(float(line["top"]), 1), - "x0": round(float(line["x0"]), 1), - "x1": round(float(line["x1"]), 1) - }) - - for rect in page.rects: - width = float(rect["x1"]) - float(rect["x0"]) - height = float(rect["bottom"]) - float(rect["top"]) - if 5 <= width <= 15 and 5 <= height <= 15 and abs(width - height) < 2: - structure["checkboxes"].append({ - "page": page_num, - "x0": round(float(rect["x0"]), 1), - "top": round(float(rect["top"]), 1), - "x1": round(float(rect["x1"]), 1), - "bottom": round(float(rect["bottom"]), 1), - "center_x": round((float(rect["x0"]) + float(rect["x1"])) / 2, 1), - "center_y": round((float(rect["top"]) + float(rect["bottom"])) / 2, 1) - }) - - lines_by_page = {} - for line in structure["lines"]: - page = line["page"] - if page not in lines_by_page: - lines_by_page[page] = [] - lines_by_page[page].append(line["y"]) - - for page, y_coords in lines_by_page.items(): - y_coords = sorted(set(y_coords)) - for i in range(len(y_coords) - 1): - structure["row_boundaries"].append({ - "page": page, - "row_top": y_coords[i], - "row_bottom": y_coords[i + 1], - "row_height": round(y_coords[i + 1] - y_coords[i], 1) - }) - - return structure - - -def main(): - if len(sys.argv) != 3: - print("Usage: extract_form_structure.py ") - sys.exit(1) - - pdf_path = sys.argv[1] - output_path = sys.argv[2] - - print(f"Extracting structure from {pdf_path}...") - structure = extract_form_structure(pdf_path) - - with open(output_path, "w") as f: - json.dump(structure, f, indent=2) - - print(f"Found:") - print(f" - {len(structure['pages'])} pages") - print(f" - {len(structure['labels'])} text labels") - print(f" - {len(structure['lines'])} horizontal lines") - print(f" - {len(structure['checkboxes'])} checkboxes") - print(f" - {len(structure['row_boundaries'])} row boundaries") - print(f"Saved to {output_path}") - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/pdf/scripts/fill_fillable_fields.py b/external/anthropic/skills/pdf/scripts/fill_fillable_fields.py deleted file mode 100644 index 51c2600..0000000 --- a/external/anthropic/skills/pdf/scripts/fill_fillable_fields.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import sys - -from pypdf import PdfReader, PdfWriter - -from extract_form_field_info import get_field_info - - - - -def fill_pdf_fields(input_pdf_path: str, fields_json_path: str, output_pdf_path: str): - with open(fields_json_path) as f: - fields = json.load(f) - fields_by_page = {} - for field in fields: - if "value" in field: - field_id = field["field_id"] - page = field["page"] - if page not in fields_by_page: - fields_by_page[page] = {} - fields_by_page[page][field_id] = field["value"] - - reader = PdfReader(input_pdf_path) - - has_error = False - field_info = get_field_info(reader) - fields_by_ids = {f["field_id"]: f for f in field_info} - for field in fields: - existing_field = fields_by_ids.get(field["field_id"]) - if not existing_field: - has_error = True - print(f"ERROR: `{field['field_id']}` is not a valid field ID") - elif field["page"] != existing_field["page"]: - has_error = True - print(f"ERROR: Incorrect page number for `{field['field_id']}` (got {field['page']}, expected {existing_field['page']})") - else: - if "value" in field: - err = validation_error_for_field_value(existing_field, field["value"]) - if err: - print(err) - has_error = True - if has_error: - sys.exit(1) - - writer = PdfWriter(clone_from=reader) - for page, field_values in fields_by_page.items(): - writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerate=False) - - writer.set_need_appearances_writer(True) - - with open(output_pdf_path, "wb") as f: - writer.write(f) - - -def validation_error_for_field_value(field_info, field_value): - field_type = field_info["type"] - field_id = field_info["field_id"] - if field_type == "checkbox": - checked_val = field_info["checked_value"] - unchecked_val = field_info["unchecked_value"] - if field_value != checked_val and field_value != unchecked_val: - return f'ERROR: Invalid value "{field_value}" for checkbox field "{field_id}". The checked value is "{checked_val}" and the unchecked value is "{unchecked_val}"' - elif field_type == "radio_group": - option_values = [opt["value"] for opt in field_info["radio_options"]] - if field_value not in option_values: - return f'ERROR: Invalid value "{field_value}" for radio group field "{field_id}". Valid values are: {option_values}' - elif field_type == "choice": - choice_values = [opt["value"] for opt in field_info["choice_options"]] - if field_value not in choice_values: - return f'ERROR: Invalid value "{field_value}" for choice field "{field_id}". Valid values are: {choice_values}' - return None - - -def monkeypatch_pydpf_method(): - from pypdf.generic import DictionaryObject - from pypdf.constants import FieldDictionaryAttributes - - original_get_inherited = DictionaryObject.get_inherited - - def patched_get_inherited(self, key: str, default = None): - result = original_get_inherited(self, key, default) - if key == FieldDictionaryAttributes.Opt: - if isinstance(result, list) and all(isinstance(v, list) and len(v) == 2 for v in result): - result = [r[0] for r in result] - return result - - DictionaryObject.get_inherited = patched_get_inherited - - -if __name__ == "__main__": - if len(sys.argv) != 4: - print("Usage: fill_fillable_fields.py [input pdf] [field_values.json] [output pdf]") - sys.exit(1) - monkeypatch_pydpf_method() - input_pdf = sys.argv[1] - fields_json = sys.argv[2] - output_pdf = sys.argv[3] - fill_pdf_fields(input_pdf, fields_json, output_pdf) diff --git a/external/anthropic/skills/pdf/scripts/fill_pdf_form_with_annotations.py b/external/anthropic/skills/pdf/scripts/fill_pdf_form_with_annotations.py deleted file mode 100644 index b430069..0000000 --- a/external/anthropic/skills/pdf/scripts/fill_pdf_form_with_annotations.py +++ /dev/null @@ -1,107 +0,0 @@ -import json -import sys - -from pypdf import PdfReader, PdfWriter -from pypdf.annotations import FreeText - - - - -def transform_from_image_coords(bbox, image_width, image_height, pdf_width, pdf_height): - x_scale = pdf_width / image_width - y_scale = pdf_height / image_height - - left = bbox[0] * x_scale - right = bbox[2] * x_scale - - top = pdf_height - (bbox[1] * y_scale) - bottom = pdf_height - (bbox[3] * y_scale) - - return left, bottom, right, top - - -def transform_from_pdf_coords(bbox, pdf_height): - left = bbox[0] - right = bbox[2] - - pypdf_top = pdf_height - bbox[1] - pypdf_bottom = pdf_height - bbox[3] - - return left, pypdf_bottom, right, pypdf_top - - -def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path): - - with open(fields_json_path, "r") as f: - fields_data = json.load(f) - - reader = PdfReader(input_pdf_path) - writer = PdfWriter() - - writer.append(reader) - - pdf_dimensions = {} - for i, page in enumerate(reader.pages): - mediabox = page.mediabox - pdf_dimensions[i + 1] = [mediabox.width, mediabox.height] - - annotations = [] - for field in fields_data["form_fields"]: - page_num = field["page_number"] - - page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num) - pdf_width, pdf_height = pdf_dimensions[page_num] - - if "pdf_width" in page_info: - transformed_entry_box = transform_from_pdf_coords( - field["entry_bounding_box"], - float(pdf_height) - ) - else: - image_width = page_info["image_width"] - image_height = page_info["image_height"] - transformed_entry_box = transform_from_image_coords( - field["entry_bounding_box"], - image_width, image_height, - float(pdf_width), float(pdf_height) - ) - - if "entry_text" not in field or "text" not in field["entry_text"]: - continue - entry_text = field["entry_text"] - text = entry_text["text"] - if not text: - continue - - font_name = entry_text.get("font", "Arial") - font_size = str(entry_text.get("font_size", 14)) + "pt" - font_color = entry_text.get("font_color", "000000") - - annotation = FreeText( - text=text, - rect=transformed_entry_box, - font=font_name, - font_size=font_size, - font_color=font_color, - border_color=None, - background_color=None, - ) - annotations.append(annotation) - writer.add_annotation(page_number=page_num - 1, annotation=annotation) - - with open(output_pdf_path, "wb") as output: - writer.write(output) - - print(f"Successfully filled PDF form and saved to {output_pdf_path}") - print(f"Added {len(annotations)} text annotations") - - -if __name__ == "__main__": - if len(sys.argv) != 4: - print("Usage: fill_pdf_form_with_annotations.py [input pdf] [fields.json] [output pdf]") - sys.exit(1) - input_pdf = sys.argv[1] - fields_json = sys.argv[2] - output_pdf = sys.argv[3] - - fill_pdf_form(input_pdf, fields_json, output_pdf) diff --git a/external/anthropic/skills/pptx/LICENSE.txt b/external/anthropic/skills/pptx/LICENSE.txt deleted file mode 100644 index c55ab42..0000000 --- a/external/anthropic/skills/pptx/LICENSE.txt +++ /dev/null @@ -1,30 +0,0 @@ -© 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/external/anthropic/skills/pptx/SKILL.md b/external/anthropic/skills/pptx/SKILL.md deleted file mode 100644 index 41cd230..0000000 --- a/external/anthropic/skills/pptx/SKILL.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -name: pptx -description: "Use this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates (.potx), layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx or .potx filename, regardless of what they plan to do with the content afterward. If a .pptx or .potx file needs to be opened, created, or touched, use this skill." -license: Proprietary. LICENSE.txt has complete terms ---- - -# PPTX creation, editing, and analysis - -A `.pptx` is a ZIP archive of XML files. Choose your approach by task: - -| Task | Approach | -|---|---| -| **Create** a new deck | Write a `pptxgenjs` script — see gotchas below | -| **Edit** an existing deck, or build from a template | unzip → edit `ppt/slides/slideN.xml` → zip | -| **Read** content | `markitdown deck.pptx` (one block per slide under `` markers); visual grid: `python scripts/thumbnail.py deck.pptx` | - -## Scripts - -Paths are relative to this skill's directory. Everything else is plain Python, `node`, or shell. - -| Script | What it does | -|---|---| -| `scripts/thumbnail.py deck.pptx [prefix]` | Labeled grid of every slide, for picking template layouts. `.pptx` only. Pass `prefix` — it defaults to `thumbnails`, which overwrites the grids of any other deck done in the same directory | -| `scripts/add_slide.py unpacked/ slide2.xml [--after slideN.xml]` | Duplicate a slide (or a `slideLayoutN.xml`) with all the package bookkeeping. Also takes a `.pptx` directly with `-o out.pptx` | -| `scripts/clean.py unpacked/` | Delete slides, media, and rels no longer referenced. Run **after** `` is final | -| `scripts/office/validate.py deck.pptx [--original src.pptx]` | Schema, relationship, content-type, chart and slide checks; each failure names its fix. Pass `--original` for any template-derived deck — it baselines the schema checks against the template, so the template's own XSD errors don't read as yours | -| `scripts/office/soffice.py --headless --convert-to pdf deck.pptx` | LibreOffice wrapper — bare `soffice` hangs in this sandbox | - -## Creating with pptxgenjs — gotchas - -`pptxgenjs` is preinstalled — do not run `npm install` first; write the script and `require('pptxgenjs')` directly. Only if that require fails: `npm install pptxgenjs`. The model knows the API; these are the footguns: - -- **Set `pres.layout` before adding slides.** The default canvas is `LAYOUT_16x9` = **10" × 5.625"**, not 13.3" wide. Coordinates past the edge are written, not clamped — the shape just isn't on the slide. (`LAYOUT_WIDE` is 13.3" × 7.5".) -- **Hex colors: never `#`, never 8 digits.** `color: "FF0000"`. Both `"#FF0000"` and alpha baked into the hex (`"00000020"`) **corrupt the file**. For translucency: `transparency: 0-100` on fills and images, `opacity: 0.0-1.0` on shadows — each is silently ignored on the other. -- **pptxgenjs mutates option objects in place** (converts values to EMU on first use). Never share one `shadow`/options object across two `add*` calls — build a fresh object each time. -- **Shadow `offset` must be ≥ 0** — a negative offset corrupts the file. To cast a shadow upward, use `angle: 270` with a positive offset. -- **`letterSpacing` is silently ignored** — the real option is `charSpacing`. -- **Lists:** `bullet: true` on each item, never a literal `•` (renders double bullets). Set `breakLine: true` on every array item except the last. Space bulleted paragraphs with `paraSpaceAfter`, not `lineSpacing` (huge gaps). -- **One `new pptxgen()` per output file** — never reuse an instance. -- **`rectRadius` only works on `ROUNDED_RECTANGLE`**, not `RECTANGLE`. -- **Gradient fills aren't supported** — use a gradient image as the background instead. -- **Text boxes have built-in internal padding** — set `margin: 0` whenever text must align with a shape, line, or icon at the same x. -- **Speaker notes go in `slide.addNotes("...")`** (plain text, once per slide), never in a text box on the slide. -- **Keep charts native.** Use `addChart()` for everything PowerPoint can chart (pass an array of `{type, data, options}` for combos). For PowerPoint-native features the library doesn't expose (trendlines, error bars), compute the extra series yourself or post-process the generated OOXML — do not fall back to a rendered image. Only chart types PowerPoint has no native form for (Sankey, network, chord) go in as images. -- **Default charts render bare** — no title, no data labels, dated palette. Set `showTitle` + `title`, `showValue: true` + `dataLabelPosition`, `chartColors: [...]` from your palette, and quiet the frame (`catAxisLabelColor`/`valAxisLabelColor`, `valGridLine: { color, size }`, `catGridLine: { style: "none" }`, `showLegend: false` for a single series). -- **On a stacked bar or column chart, `dataLabelPosition` must be `ctr`, `inEnd`, or `inBase`.** `outEnd` **corrupts the file**. -- **A combo series using `secondaryValAxis`/`secondaryCatAxis` needs both `valAxes` and `catAxes` on the chart options, two entries each.** Without them pptxgenjs writes axis *ids* it never declares, and PowerPoint **discards that chart** and reports the file as corrupt. Supplying only `valAxes` is not enough. -- **After `writeFile()`, run `python scripts/office/validate.py deck.pptx`.** It reports the two chart faults above and the slide-XML defects PowerPoint refuses, and names the fix for each. Fix them in your generator, not by hand-editing the packed XML. -- **Never reorder the children of ``.** pptxgenjs writes `` right after `` and points both masters at one theme part. PowerPoint reads that happily — move the element and the same deck becomes unopenable. -- **Icons:** render `react-icons` to SVG (`ReactDOMServer.renderToStaticMarkup`), rasterize with `sharp` at ≥256px, and insert via `addImage({ data: "image/png;base64," + buf.toString("base64") })` — the `image/png;base64,` prefix is required (`react-icons`, `react`, `react-dom`, and `sharp` are preinstalled — `npm install react-icons react react-dom sharp` only if a require fails). - -## Editing existing decks and templates - -Pick layouts first: `python scripts/thumbnail.py template.pptx template-thumbs` writes a labeled grid of every slide and prints the file(s) it created — `template-thumbs.jpg`, split into `template-thumbs-N.jpg` past 12 slides. **Always pass that second argument, named after the deck.** It defaults to `thumbnails`, so two decks thumbnailed in one directory silently overwrite each other's grids — the first deck's are simply gone (template analysis only — visual QA needs the full-resolution renders from [Converting to Images](#converting-to-images); it only accepts `.pptx`, so copy a `.potx` to a `.pptx` name first). Use it with `markitdown` to map each content section onto a template slide, and vary the layouts — don't put every section on the same title-and-bullets slide. - -```bash -python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall('unpacked')" deck.pptx -python scripts/add_slide.py unpacked/ slide2.xml --after slide2.xml # duplicate a slide (or slideLayoutN.xml); prints the new slide's path -# reorder / delete slides = edit in ppt/presentation.xml -python scripts/clean.py unpacked/ # after deletions: removes orphaned slides, media, rels -# edit slide content in ppt/slides/slideN.xml -(cd unpacked && rm -f ../out.pptx && zip -Xr ../out.pptx .) # zip from INSIDE the dir; rm first or deleted parts survive -python scripts/office/validate.py out.pptx --original deck.pptx -``` - -- **Do all structural work — add, delete, reorder — before editing any slide's content.** `add_slide.py` copies a slide file verbatim, so duplicating after you edit clones the edited content; and `clean.py` deletes any slide missing from ``, including one you just wrote. -- **Never copy a slide file by hand** — `add_slide.py` does every registration a new slide needs and reports what it made (`Created ppt/slides/slide17.xml from slide2.xml`). It also works directly on a file: `add_slide.py deck.pptx slide2.xml -o out.pptx` — **pass `-o`, or it rewrites the input deck in place.** A duplicated slide still *references* its source's chart/SmartArt/embedded-object parts rather than cloning them, so editing one slide's chart changes the other's. -- **If you use `python-pptx`**, three things it won't do: duplicate a slide (its only entry point is `add_slide(layout)`), preserve formatting through `text_frame.text = "..."` (that collapses the paragraph to a single unstyled run — assign `run.text` instead), or read the SVG/EMF most template art uses (`add_picture` raises `UnidentifiedImageError`). -- Legacy `.ppt` must be converted first: `python scripts/office/soffice.py --headless --convert-to pptx file.ppt`. `.potx` templates unpack and pack identically — keep the `.potx` extension on the output. -- To reuse a template icon or image, duplicate a slide or layout that already contains it. - -When filling in a template: - -- If you script an XML transform, parse with `defusedxml.minidom` — round-tripping OOXML through `xml.etree.ElementTree` rewrites namespace prefixes and corrupts the deck. -- **Template slots ≠ source items.** If the template shows 4 team members and you have 3, delete the 4th member's entire group (image + text boxes), not just its text — then check for orphaned visuals in QA. -- One `` per list item — never concatenate items into a single paragraph. Copy the sibling `` to preserve spacing, and put `b="1"` on the `` of titles, section headers, and inline labels (`Status:`, `Owner:`). -- Let bullets inherit from the layout; only add ``, `` (numbered), or `` to override — never a literal `•` in the text. -- Text with leading or trailing spaces needs `xml:space="preserve"` on its ``. - -## Design Ideas - -**Don't create boring slides.** Plain bullets on a white background won't impress anyone. Consider ideas from this list for each slide. - -### Before Starting - -- **Pick a bold, content-informed color palette**: The palette should feel designed for THIS topic. If swapping your colors into a completely different presentation would still "work," you haven't made specific enough choices. -- **Dominance over equality**: One color should dominate (60-70% visual weight), with 1-2 supporting tones and one sharp accent. Never give all colors equal weight. -- **Dark/light contrast**: Dark backgrounds for title + conclusion slides, light for content ("sandwich" structure). Or commit to dark throughout for a premium feel. -- **Commit to a visual motif**: Pick ONE distinctive element and repeat it — rounded image frames, icons in colored circles. Carry it across every slide. **Do not use a color bar or accent stripe as your motif** (see Avoid list). - -### Color Palettes - -Choose colors that match your topic — don't default to generic blue. Use these palettes as inspiration: - -| Theme | Primary | Secondary | Accent | -|-------|---------|-----------|--------| -| **Midnight Executive** | `1E2761` (navy) | `CADCFC` (ice blue) | `FFFFFF` (white) | -| **Forest & Moss** | `2C5F2D` (forest) | `97BC62` (moss) | `F5F5F5` (cream) | -| **Coral Energy** | `F96167` (coral) | `F9E795` (gold) | `2F3C7E` (navy) | -| **Warm Terracotta** | `B85042` (terracotta) | `E7E8D1` (sand) | `A7BEAE` (sage) | -| **Ocean Gradient** | `065A82` (deep blue) | `1C7293` (teal) | `21295C` (midnight) | -| **Charcoal Minimal** | `36454F` (charcoal) | `F2F2F2` (off-white) | `212121` (black) | -| **Teal Trust** | `028090` (teal) | `00A896` (seafoam) | `02C39A` (mint) | -| **Berry & Cream** | `6D2E46` (berry) | `A26769` (dusty rose) | `ECE2D0` (cream) | -| **Sage Calm** | `84B59F` (sage) | `69A297` (eucalyptus) | `50808E` (slate) | -| **Cherry Bold** | `990011` (cherry) | `FCF6F5` (off-white) | `2F3C7E` (navy) | - -### For Each Slide - -**Every slide needs a visual element** — image, chart, icon, or shape. Text-only slides are forgettable. - -**Layout options:** -- Two-column (text left, illustration on right) -- Icon + text rows (icon in colored circle, bold header, description below) -- 2x2 or 2x3 grid (image on one side, grid of content blocks on other) -- Half-bleed image (full left or right side) with content overlay - -**Data display:** -- Large stat callouts (big numbers 60-72pt with small labels below) -- Comparison columns (before/after, pros/cons, side-by-side options) -- Timeline or process flow (numbered steps, arrows) - -**Visual polish:** -- Icons in small colored circles next to section headers -- Italic accent text for key stats or taglines - -### Typography - -**Font names you write into the .pptx are rendered by the user's PowerPoint, not by this environment.** Your visual QA renders via LibreOffice, which substitutes fonts it doesn't have — and for some fonts the substitute has different widths, so your QA preview can show text overflow (or fit) that the real deck won't have. To keep your QA trustworthy: - -- **Safe fonts** (render true-to-width in QA *and* ship with Office): **Arial, Calibri, Cambria, Times New Roman, Courier New, Bookman Old Style, Century Schoolbook**. Use these for body text and anything where fit matters. -- **Headers with personality at zero QA risk**: pair a safe-list serif header (Cambria, Bookman Old Style, Century Schoolbook) with a safe-list sans body (Calibri or Arial). You get visual contrast without giving up reliable overflow checks. -- **If the user asks for a font outside the safe list** (e.g. Georgia or Trebuchet MS): use it where the user asked, but size those containers with extra slack (~10%) and don't trust QA text-fit on those elements — the preview of that font is approximate. If the user hasn't specified, prefer safe-list fonts for body text. -- **QA-unreliable fonts** (substitute has different widths — overflow checks can be wrong): Georgia, Trebuchet MS, Impact, Arial Black, Garamond, Consolas, Palatino Linotype. Calibri Light substitution varies by environment; treat as QA-unreliable. Fine for titles/accents with slack; don't trust QA text-fit on these. -- **Never default to Aptos** — Office's post-2023 default has no metric-compatible substitute here *and* is missing from older Office installs, so it's unreliable on both ends. - -| Element | Size | -|---------|------| -| Slide title | 36-44pt bold | -| Section header | 20-24pt bold | -| Body text | 14-16pt | -| Captions | 10-12pt muted | - -### Spacing - -- 0.5" minimum margins -- 0.3-0.5" between content blocks -- Leave breathing room—don't fill every inch - -### Avoid (Common Mistakes) - -- **Don't repeat the same layout** — vary columns, cards, and callouts across slides -- **Don't center body text** — left-align paragraphs and lists; center only titles -- **Don't skimp on size contrast** — titles need 36pt+ to stand out from 14-16pt body -- **Don't default to blue** — pick colors that reflect the specific topic -- **Don't mix spacing randomly** — choose 0.3" or 0.5" gaps and use consistently -- **Don't style one slide and leave the rest plain** — commit fully or keep it simple throughout -- **Don't create text-only slides** — add images, icons, charts, or visual elements; avoid plain title + bullets -- **Don't forget text box padding** — when aligning lines or shapes with text edges, set `margin: 0` on the text box or offset the shape to account for padding -- **Don't use low-contrast elements** — icons AND text need strong contrast against the background; avoid light text on light backgrounds or dark text on dark backgrounds -- **NEVER use accent lines under titles** — these are a hallmark of AI-generated slides; use whitespace or background color instead -- **NEVER add decorative color bars or accent stripes** — this includes: header/footer bars spanning the slide width, vertical sidebar stripes down one edge of the slide, thin accent stripes along one edge of a card or content block, and "single-side borders" on rectangles. These read as AI-generated filler. If you want to set a card apart, use a subtle background tint, a drop shadow, or an icon — not an edge stripe. -- **Don't default to cream/beige backgrounds** — when no background is specified, use white (`FFFFFF`) or the user's brand palette; avoid warm-neutral defaults like `F5F5DC`, `FAF0E6`, `FAEBD7`, `FFF8E1` -- **Don't ship text that overflows its shape** — if text doesn't fit, reduce font size, split across slides, or enlarge the container; never leave content cut off or spilling past bounds - -## QA (Required) - -Your first render usually has a few real issues — overlaps, overflow, misalignment. Find and fix those, re-render only the slides you changed, and stop. - -### Content QA - -```bash -markitdown output.pptx -``` - -Check for missing content, typos, wrong order. - -**When using templates, check for leftover placeholder text:** - -```bash -markitdown output.pptx | grep -iE "\bx{3,}\b|lorem|ipsum|\bTODO|\[insert|this.*(page|slide).*layout" -``` - -If grep returns results, fix them before declaring success. - -### File QA (required) - -```bash -python scripts/office/validate.py output.pptx # built from scratch -python scripts/office/validate.py output.pptx --original src.pptx # built from a template -``` - -**If the deck came from a template, always pass `--original`.** A template may itself -contain parts the XSD rejects, so a bare run can report failures you never caused — and -a genuine regression can hide among them. `--original` baselines -the schema and slide checks against the template, suppressing errors it already had. -The structural checks — relationships, content types, charts — ignore `--original` and -report template-inherited problems either way, so read those on their own merits. - -pptxgenjs emits chart XML PowerPoint refuses to open, and every other tool -accepts: python-pptx opens those decks, LibreOffice renders them, the XSD -passes them. Every failure names its fix. Fix it in the generator and rebuild. - -### Visual QA - -Convert the slides to images (see [Converting to Images](#converting-to-images)) and inspect every one. After staring at the generating code you tend to see what you expect rather than what rendered, so look at the images fresh (a subagent works well for this if you have one). User-visible defects to look for: - -- **Text overflow or text cut off at a box or slide boundary — check this first.** It is the most common defect and always user-visible. (For a font the previewer renders unreliably per Typography, the preview is approximate: trust the ~10% slack you left, not its apparent fit.) -- Overlapping elements (text through shapes, lines through words, stacked elements) -- Source citations or footers colliding with content above -- Elements too close (< 0.3" gaps) or cards/sections nearly touching -- Uneven gaps (large empty area in one place, cramped in another) -- Insufficient margin from slide edges (< 0.5") -- Columns or similar elements not aligned consistently -- Low-contrast text (e.g., light gray text on cream-colored background) -- Template decoration mispositioned after text replacement — e.g., a title underline positioned for one line, but the replaced title wrapped to two -- Low-contrast icons (e.g., dark icons on dark backgrounds without a contrasting circle) -- Text boxes too narrow causing excessive wrapping -- Leftover placeholder content - -## Converting to Images - -Convert presentations to individual slide images for visual inspection: - -```bash -python scripts/office/soffice.py --headless --convert-to pdf output.pptx -rm -f slide-*.jpg -pdftoppm -jpeg -r 150 output.pdf slide -ls -1 "$PWD"/slide-*.jpg -``` - -**Pass the absolute paths printed above directly to the view tool.** The `rm` clears stale images from prior runs. `pdftoppm` zero-pads based on page count: `slide-1.jpg` for decks under 10 pages, `slide-01.jpg` for 10-99, `slide-001.jpg` for 100+. - -**After fixes, rerun all four commands above** — the PDF must be regenerated from the edited `.pptx` before `pdftoppm` can reflect your changes. - -## Dependencies - -`pptxgenjs` (npm, preinstalled — install only if `require('pptxgenjs')` fails) · `markitdown[pptx]`, `Pillow`, `defusedxml`, `lxml` (pip — text dump, thumbnail, clean, validate) · LibreOffice (`soffice`, auto-configured for sandboxed environments via `scripts/office/soffice.py`) · `pdftoppm` (Poppler) diff --git a/external/anthropic/skills/pptx/scripts/__init__.py b/external/anthropic/skills/pptx/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/external/anthropic/skills/pptx/scripts/add_slide.py b/external/anthropic/skills/pptx/scripts/add_slide.py deleted file mode 100755 index f013ea9..0000000 --- a/external/anthropic/skills/pptx/scripts/add_slide.py +++ /dev/null @@ -1,367 +0,0 @@ -"""Add a slide to a PPTX: duplicate an existing slide or instantiate a layout. - -Does all of the package bookkeeping, so the deck stays valid: - - writes the new ppt/slides/slideN.xml (and its .rels, minus any - notesSlide reference, so the source's speaker notes aren't shared) - - registers it in [Content_Types].xml - - adds a slide relationship with a fresh rId to presentation.xml.rels - - inserts with a fresh id into - — at the end, or after --after SLIDE - -Works on an unpacked directory (during an editing session) or directly on a -.pptx/.potx file (extracted to a temp dir, then rezipped atomically; the -temp dir is discarded, so unpack the output if you still need to edit the -new slide's content). - -Usage: - python add_slide.py unpacked/ slide2.xml # duplicate slide2 - python add_slide.py unpacked/ slideLayout3.xml # new slide from a layout - python add_slide.py unpacked/ slide2.xml --after slide2.xml - python add_slide.py deck.pptx slide2.xml # rewrite deck.pptx in place - python add_slide.py deck.pptx slide2.xml -o out.pptx - -A duplicated slide still holds the source's content: edit ppt/slides/slideN.xml -(printed on success) to change it. To list layouts: ls /ppt/slideLayouts/ -""" - -import argparse -import re -import shutil -import sys -from typing import NoReturn -import tempfile -import zipfile -from pathlib import Path - -from office.helpers import rezip, safe_extract - -MINIMAL_SLIDE_XML = ''' - - - - - - - - - - - - - - - - - - - - - -''' - -SHARED_PART_TYPES = ("chart", "diagramData", "oleObject", "package") - -NOTES_SLIDE_TYPE_RE = re.compile(r"""Type=["'][^"']*/relationships/notesSlide["']""") -RELATIONSHIP_RE = re.compile(r"]*?(?:/>|>.*?)", re.DOTALL) - -SLIDE_ID_MIN = 256 -SLIDE_ID_MAX = 2147483647 - - -def _die(msg: str) -> NoReturn: - print(f"Error: {msg}", file=sys.stderr) - sys.exit(1) - - -def get_next_slide_number(slides_dir: Path) -> int: - existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml") - if (m := re.match(r"slide(\d+)\.xml", f.name))] - return max(existing) + 1 if existing else 1 - - -def parse_source(source: str) -> tuple[str, str | None]: - if source.startswith("slideLayout") and source.endswith(".xml"): - return ("layout", source) - - return ("slide", None) - - -def create_slide_from_layout(unpacked_dir: Path, layout_file: str, after: str | None = None) -> str: - slides_dir = unpacked_dir / "ppt" / "slides" - rels_dir = slides_dir / "_rels" - layout_path = unpacked_dir / "ppt" / "slideLayouts" / layout_file - - if not layout_path.exists(): - _die(f"{layout_path} not found") - - next_num = get_next_slide_number(slides_dir) - dest = f"slide{next_num}.xml" - after_rid = _precheck_registration(unpacked_dir, after, dest) - slides_dir.mkdir(parents=True, exist_ok=True) - - (slides_dir / dest).write_text(MINIMAL_SLIDE_XML, encoding="utf-8") - - rels_dir.mkdir(exist_ok=True) - rels_xml = f''' - - -''' - (rels_dir / f"{dest}.rels").write_text(rels_xml, encoding="utf-8") - - _register_slide(unpacked_dir, dest, layout_file, after_rid) - return dest - - -def duplicate_slide(unpacked_dir: Path, source: str, after: str | None = None) -> str: - slides_dir = unpacked_dir / "ppt" / "slides" - rels_dir = slides_dir / "_rels" - source_slide = slides_dir / source - - if not source_slide.exists(): - _die(f"{source_slide} not found") - - next_num = get_next_slide_number(slides_dir) - dest = f"slide{next_num}.xml" - after_rid = _precheck_registration(unpacked_dir, after, dest) - - shutil.copy2(source_slide, slides_dir / dest) - - source_rels = rels_dir / f"{source}.rels" - shared_parts: list[str] = [] - if source_rels.exists(): - dest_rels = rels_dir / f"{dest}.rels" - shutil.copy2(source_rels, dest_rels) - rels_content = dest_rels.read_text(encoding="utf-8") - rels_content = RELATIONSHIP_RE.sub( - lambda m: "" if NOTES_SLIDE_TYPE_RE.search(m.group(0)) else m.group(0), - rels_content, - ) - dest_rels.write_text(rels_content, encoding="utf-8") - shared_parts = sorted({ - t for t in re.findall(r'Type="[^"]*/relationships/(\w+)"', rels_content) - if t in SHARED_PART_TYPES - }) - - _register_slide(unpacked_dir, dest, source, after_rid) - if shared_parts: - print( - f"Note: {dest} shares its {', '.join(shared_parts)} part(s) with {source} " - f"(they are referenced, not copied) — editing those parts changes both slides" - ) - return dest - - -def _precheck_registration(unpacked_dir: Path, after: str | None, dest: str) -> str | None: - pres_path = unpacked_dir / "ppt" / "presentation.xml" - if not pres_path.exists(): - _die(f"{pres_path} not found — is this an unpacked PPTX?") - xml = pres_path.read_text(encoding="utf-8") - - has_slot = ( - "" in xml - or re.search(r"", xml) - or "" in xml - ) - if not has_slot: - _die("presentation.xml has no (or to anchor a new one)") - - stale = [] - content_types = unpacked_dir / "[Content_Types].xml" - if content_types.exists() and f'PartName="/ppt/slides/{dest}"' in content_types.read_text(encoding="utf-8"): - stale.append("[Content_Types].xml") - pres_rels = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" - if pres_rels.exists() and _find_slide_relationship( - pres_rels.read_text(encoding="utf-8"), dest - ): - stale.append("presentation.xml.rels") - if stale: - _die( - f"{dest} is still registered in {' and '.join(stale)} but absent from ppt/slides/ — " - f"run clean.py first" - ) - - if not after: - return None - after_rid = _rid_for_slide(unpacked_dir, after) - if not re.search(rf']*r:id="{re.escape(after_rid)}"[^>]*>', xml): - _die(f"{after} ({after_rid}) is not listed in ") - return after_rid - - -def _register_slide(unpacked_dir: Path, dest: str, source_desc: str, after_rid: str | None) -> None: - _add_to_content_types(unpacked_dir, dest) - rid = _add_to_presentation_rels(unpacked_dir, dest) - slide_id = _get_next_slide_id(unpacked_dir) - pos, total = _insert_into_sld_id_lst(unpacked_dir, slide_id, rid, after_rid) - - print(f"Created ppt/slides/{dest} from {source_desc}") - print( - f'Inserted into ' - f"at position {pos} of {total}" - ) - - -def _add_to_content_types(unpacked_dir: Path, dest: str) -> None: - content_types_path = unpacked_dir / "[Content_Types].xml" - content_types = content_types_path.read_text(encoding="utf-8") - - new_override = f'' - - if f'PartName="/ppt/slides/{dest}"' not in content_types: - content_types = content_types.replace("", f" {new_override}\n") - content_types_path.write_text(content_types, encoding="utf-8") - - -def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str: - pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" - pres_rels = pres_rels_path.read_text(encoding="utf-8") - - existing = _find_slide_relationship(pres_rels, dest) - if existing: - return existing - - pres_xml = (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8") - used = {int(n) for n in re.findall(r'\bId="rId(\d+)"', pres_rels)} - used |= {int(n) for n in re.findall(r'\br:id="rId(\d+)"', pres_xml)} - rid = f"rId{max(used) + 1 if used else 1}" - - new_rel = f'' - pres_rels = pres_rels.replace("", f" {new_rel}\n") - pres_rels_path.write_text(pres_rels, encoding="utf-8") - - return rid - - -def _find_slide_relationship(pres_rels: str, slide_name: str) -> str | None: - for m in re.finditer(r"]*>", pres_rels): - element = m.group(0) - if re.search(rf'Target="(?:/ppt/)?slides/{re.escape(slide_name)}"', element): - id_match = re.search(r'\bId="([^"]+)"', element) - if id_match: - return id_match.group(1) - return None - - -def _get_next_slide_id(unpacked_dir: Path) -> int: - pres_content = (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8") - used = {int(m) for m in re.findall(r']*\bid="(\d+)"', pres_content)} - - candidate = max((i for i in used if i >= SLIDE_ID_MIN), default=SLIDE_ID_MIN - 1) + 1 - if candidate <= SLIDE_ID_MAX and candidate not in used: - return candidate - for i in range(SLIDE_ID_MIN, SLIDE_ID_MAX + 1): - if i not in used: - return i - _die("no slide id available in [256, 2147483647] — the deck is full") - - -def _insert_into_sld_id_lst( - unpacked_dir: Path, slide_id: int, rid: str, after_rid: str | None = None -) -> tuple[int, int]: - pres_path = unpacked_dir / "ppt" / "presentation.xml" - xml = pres_path.read_text(encoding="utf-8") - entry = f'' - - if f'r:id="{rid}"' in xml: - _die(f"presentation.xml already references {rid}; refusing to add a duplicate") - - if after_rid: - open_tag = re.search(rf']*r:id="{re.escape(after_rid)}"[^>]*>', xml) - if not open_tag: - _die(f"{after_rid} is not listed in ") - end = open_tag.end() - if not open_tag.group(0).endswith("/>"): - close = xml.find("", end) - if close == -1: - _die(f"unclosed for {after_rid} in presentation.xml") - end = close + len("") - xml = xml[:end] + entry + xml[end:] - elif "" in xml: - xml = xml.replace("", f"{entry}", 1) - elif re.search(r"", xml): - xml = re.sub(r"", f"{entry}", xml, count=1) - elif "" in xml: - xml = xml.replace( - "", f"{entry}", 1 - ) - else: - _die("presentation.xml has no (or to anchor a new one)") - - pres_path.write_text(xml, encoding="utf-8") - - lst = re.search(r"(.*)", xml, re.DOTALL) - entries = re.findall(r"]*>", lst.group(1)) if lst else [] - position = next( - (i for i, e in enumerate(entries, 1) if f'r:id="{rid}"' in e), len(entries) - ) - return position, len(entries) - - -def _rid_for_slide(unpacked_dir: Path, slide_name: str) -> str: - pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" - rid = _find_slide_relationship(pres_rels_path.read_text(encoding="utf-8"), slide_name) - if not rid: - _die(f"{slide_name} has no relationship in presentation.xml.rels") - return rid - - -def add_slide(unpacked_dir: Path, source: str, after: str | None = None) -> str: - source_type, layout_file = parse_source(source) - if source_type == "layout" and layout_file is not None: - return create_slide_from_layout(unpacked_dir, layout_file, after) - return duplicate_slide(unpacked_dir, source, after) - - -def add_slide_to_package( - package: Path, source: str, after: str | None = None, output: Path | None = None -) -> str: - out = output or package - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - with zipfile.ZipFile(package) as zf: - safe_extract(zf, tmp_path) - dest = add_slide(tmp_path, source, after) - rezip(tmp_path, out) - print(f"Wrote {out} — the new slide is ppt/slides/{dest} inside it (unpack to edit its content)") - return dest - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Add a slide to a PPTX: duplicate a slide or instantiate a layout. " - "Registers content types, relationships, and ." - ) - parser.add_argument("target", help="Unpacked PPTX directory OR a .pptx/.potx file") - parser.add_argument( - "source", - help="slideN.xml to duplicate, or slideLayoutN.xml to create from a layout " - "(list layouts with: ls /ppt/slideLayouts/)", - ) - parser.add_argument( - "--after", - metavar="SLIDE", - help="insert after this slide, e.g. slide2.xml (default: append at the end)", - ) - parser.add_argument( - "-o", - "--output", - help="output file (only with a .pptx/.potx target; default: rewrite the input in place)", - ) - args = parser.parse_args() - - target = Path(args.target) - if target.is_dir(): - if args.output: - parser.error("--output is only valid for .pptx/.potx input; a directory is modified in place") - add_slide(target, args.source, args.after) - elif target.is_file() and target.suffix.lower() in (".pptx", ".potx"): - try: - add_slide_to_package(target, args.source, args.after, Path(args.output) if args.output else None) - except (OSError, ValueError, zipfile.BadZipFile) as e: - _die(str(e)) - else: - _die(f"{target} is neither a directory nor a .pptx/.potx file") - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/pptx/scripts/clean.py b/external/anthropic/skills/pptx/scripts/clean.py deleted file mode 100755 index 551dd23..0000000 --- a/external/anthropic/skills/pptx/scripts/clean.py +++ /dev/null @@ -1,309 +0,0 @@ -"""Remove unreferenced files from an unpacked PPTX directory. - -Usage: python clean.py - -Example: - python clean.py unpacked/ - -This script removes: -- Orphaned slides (not in sldIdLst) and their relationships -- [trash] directory (unreferenced files) -- Orphaned .rels files for deleted resources -- Unreferenced media, embeddings, charts, diagrams, drawings, ink files -- Unreferenced theme files -- Unreferenced notes slides -- Content-Type overrides for deleted files -""" - -import posixpath -import re -import sys -from pathlib import Path - -import defusedxml.minidom - -from office.helpers import SLIDE_REL_TYPE, opc_target, rels_source_part - - -def _slide_rids(pres_rels_path: Path, unpacked_dir: Path) -> dict[str, str]: - source_part = rels_source_part(pres_rels_path, unpacked_dir) - rels_dom = defusedxml.minidom.parse(str(pres_rels_path)) - - rids: dict[str, str] = {} - for rel in rels_dom.getElementsByTagName("Relationship"): - if rel.getAttribute("Type") != SLIDE_REL_TYPE: - continue - part = opc_target( - rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode") - ) - if part is not None: - rids[rel.getAttribute("Id")] = part - return rids - - -def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]: - pres_path = unpacked_dir / "ppt" / "presentation.xml" - pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" - - if not pres_path.exists() or not pres_rels_path.exists(): - return set() - - rid_to_slide = _slide_rids(pres_rels_path, unpacked_dir) - - pres_content = pres_path.read_text(encoding="utf-8") - referenced_rids = set(re.findall(r']*r:id="([^"]+)"', pres_content)) - - return { - posixpath.basename(rid_to_slide[rid]) - for rid in referenced_rids - if rid in rid_to_slide - } - - -class RefusedToClean(Exception): - """The package does not look the way a readable package should.""" - - -def remove_orphaned_slides(unpacked_dir: Path) -> list[str]: - slides_dir = unpacked_dir / "ppt" / "slides" - slides_rels_dir = slides_dir / "_rels" - pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" - - if not slides_dir.exists(): - return [] - - referenced_slides = get_slides_in_sldidlst(unpacked_dir) - on_disk = sorted(slides_dir.glob("slide*.xml")) - - if on_disk and not any(s.name in referenced_slides for s in on_disk): - listed = re.findall( - r']*r:id="([^"]+)"', - (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8") - if (unpacked_dir / "ppt" / "presentation.xml").exists() - else "", - ) - if listed: - raise RefusedToClean( - f" lists {len(listed)} slide(s) and none of the " - f"{len(on_disk)} slide(s) on disk match any of them. Refusing to " - f"delete them all — this is a parse failure, not an empty deck." - ) - - removed = [] - - for slide_file in on_disk: - if slide_file.name not in referenced_slides: - rel_path = slide_file.relative_to(unpacked_dir) - slide_file.unlink() - removed.append(str(rel_path)) - - rels_file = slides_rels_dir / f"{slide_file.name}.rels" - if rels_file.exists(): - rels_file.unlink() - removed.append(str(rels_file.relative_to(unpacked_dir))) - - if removed and pres_rels_path.exists(): - rels_dom = defusedxml.minidom.parse(str(pres_rels_path)) - source_part = rels_source_part(pres_rels_path, unpacked_dir) - changed = False - - for rel in list(rels_dom.getElementsByTagName("Relationship")): - if rel.getAttribute("Type") != SLIDE_REL_TYPE: - continue - part = opc_target( - rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode") - ) - if part is None: - continue - if posixpath.basename(part) not in referenced_slides: - if rel.parentNode: - rel.parentNode.removeChild(rel) - changed = True - - if changed: - with open(pres_rels_path, "wb") as f: - f.write(rels_dom.toxml(encoding="utf-8")) - - return removed - - -def remove_trash_directory(unpacked_dir: Path) -> list[str]: - trash_dir = unpacked_dir / "[trash]" - removed = [] - - if trash_dir.exists() and trash_dir.is_dir(): - for file_path in trash_dir.iterdir(): - if file_path.is_file(): - rel_path = file_path.relative_to(unpacked_dir) - removed.append(str(rel_path)) - file_path.unlink() - trash_dir.rmdir() - - return removed - - -def _referenced_by(rels_files, unpacked_dir: Path) -> set: - referenced = set() - - for rels_file in rels_files: - source_part = rels_source_part(rels_file, unpacked_dir) - dom = defusedxml.minidom.parse(str(rels_file)) - for rel in dom.getElementsByTagName("Relationship"): - part = opc_target( - rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode") - ) - if part is not None: - referenced.add(Path(part)) - - return referenced - - -def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]: - resource_dirs = ["charts", "diagrams", "drawings"] - removed = [] - - for dir_name in resource_dirs: - rels_dir = unpacked_dir / "ppt" / dir_name / "_rels" - if not rels_dir.exists(): - continue - - for rels_file in rels_dir.glob("*.rels"): - resource_file = rels_dir.parent / rels_file.name.replace(".rels", "") - if not resource_file.exists(): - rels_file.unlink() - removed.append(str(rels_file.relative_to(unpacked_dir))) - - return removed - - -def get_referenced_files(unpacked_dir: Path) -> set: - return _referenced_by(sorted(unpacked_dir.rglob("*.rels")), unpacked_dir) - - -def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]: - resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"] - removed = [] - - for dir_name in resource_dirs: - dir_path = unpacked_dir / "ppt" / dir_name - if not dir_path.exists(): - continue - - for file_path in dir_path.glob("*"): - if not file_path.is_file(): - continue - rel_path = file_path.relative_to(unpacked_dir) - if rel_path not in referenced: - file_path.unlink() - removed.append(str(rel_path)) - - theme_dir = unpacked_dir / "ppt" / "theme" - if theme_dir.exists(): - for file_path in theme_dir.glob("theme*.xml"): - rel_path = file_path.relative_to(unpacked_dir) - if rel_path not in referenced: - file_path.unlink() - removed.append(str(rel_path)) - theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels" - if theme_rels.exists(): - theme_rels.unlink() - removed.append(str(theme_rels.relative_to(unpacked_dir))) - - notes_dir = unpacked_dir / "ppt" / "notesSlides" - if notes_dir.exists(): - for file_path in notes_dir.glob("*.xml"): - if not file_path.is_file(): - continue - rel_path = file_path.relative_to(unpacked_dir) - if rel_path not in referenced: - file_path.unlink() - removed.append(str(rel_path)) - - notes_rels_dir = notes_dir / "_rels" - if notes_rels_dir.exists(): - for file_path in notes_rels_dir.glob("*.rels"): - notes_file = notes_dir / file_path.name.replace(".rels", "") - if not notes_file.exists(): - file_path.unlink() - removed.append(str(file_path.relative_to(unpacked_dir))) - - return removed - - -def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None: - ct_path = unpacked_dir / "[Content_Types].xml" - if not ct_path.exists(): - return - - dom = defusedxml.minidom.parse(str(ct_path)) - changed = False - - for override in list(dom.getElementsByTagName("Override")): - part_name = override.getAttribute("PartName").lstrip("/") - if part_name in removed_files: - if override.parentNode: - override.parentNode.removeChild(override) - changed = True - - if changed: - with open(ct_path, "wb") as f: - f.write(dom.toxml(encoding="utf-8")) - - -def clean_unused_files(unpacked_dir: Path) -> list[str]: - all_removed = [] - - if list(unpacked_dir.rglob("*.rels")) and not get_referenced_files(unpacked_dir): - raise RefusedToClean( - "no relationship in this package names a part we can resolve. " - "Refusing to treat every file as unreferenced." - ) - - slides_removed = remove_orphaned_slides(unpacked_dir) - all_removed.extend(slides_removed) - - trash_removed = remove_trash_directory(unpacked_dir) - all_removed.extend(trash_removed) - - while True: - removed_rels = remove_orphaned_rels_files(unpacked_dir) - referenced = get_referenced_files(unpacked_dir) - removed_files = remove_orphaned_files(unpacked_dir, referenced) - - total_removed = removed_rels + removed_files - if not total_removed: - break - - all_removed.extend(total_removed) - - if all_removed: - update_content_types(unpacked_dir, all_removed) - - return all_removed - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python clean.py ", file=sys.stderr) - print("Example: python clean.py unpacked/", file=sys.stderr) - sys.exit(1) - - unpacked_dir = Path(sys.argv[1]) - - if not unpacked_dir.exists(): - print(f"Error: {unpacked_dir} not found", file=sys.stderr) - sys.exit(1) - - try: - removed = clean_unused_files(unpacked_dir) - except (RefusedToClean, ValueError) as e: - print(f"Error: {e}", file=sys.stderr) - print("Nothing was deleted.", file=sys.stderr) - sys.exit(1) - - if removed: - print(f"Removed {len(removed)} unreferenced files:") - for f in removed: - print(f" {f}") - else: - print("No unreferenced files found") diff --git a/external/anthropic/skills/pptx/scripts/office/helpers/__init__.py b/external/anthropic/skills/pptx/scripts/office/helpers/__init__.py deleted file mode 100644 index d3c5817..0000000 --- a/external/anthropic/skills/pptx/scripts/office/helpers/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -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 - if _SCHEME_RE.match(target): - return None - - target = urllib.parse.unquote(target) - - 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) - return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]).lstrip("./") - - -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/external/anthropic/skills/pptx/scripts/office/helpers/pptx_chart.py b/external/anthropic/skills/pptx/scripts/office/helpers/pptx_chart.py deleted file mode 100644 index 209cb7c..0000000 --- a/external/anthropic/skills/pptx/scripts/office/helpers/pptx_chart.py +++ /dev/null @@ -1,170 +0,0 @@ -"""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/external/anthropic/skills/pptx/scripts/office/helpers/pptx_slide.py b/external/anthropic/skills/pptx/scripts/office/helpers/pptx_slide.py deleted file mode 100644 index 22f9aee..0000000 --- a/external/anthropic/skills/pptx/scripts/office/helpers/pptx_slide.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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/external/anthropic/skills/pptx/scripts/office/helpers/pptx_theme.py b/external/anthropic/skills/pptx/scripts/office/helpers/pptx_theme.py deleted file mode 100644 index 8446620..0000000 --- a/external/anthropic/skills/pptx/scripts/office/helpers/pptx_theme.py +++ /dev/null @@ -1,114 +0,0 @@ -"""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/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd deleted file mode 100644 index 6454ef9..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +++ /dev/null @@ -1,1499 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd deleted file mode 100644 index afa4f46..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd deleted file mode 100644 index 64e66b8..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +++ /dev/null @@ -1,1085 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd deleted file mode 100644 index 687eea8..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd deleted file mode 100644 index 6ac81b0..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +++ /dev/null @@ -1,3081 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd deleted file mode 100644 index 1dbf051..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd deleted file mode 100644 index f1af17d..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd deleted file mode 100644 index 0a185ab..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd deleted file mode 100644 index 14ef488..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +++ /dev/null @@ -1,1676 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd deleted file mode 100644 index c20f3bf..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd deleted file mode 100644 index ac60252..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd deleted file mode 100644 index 424b8ba..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd deleted file mode 100644 index 2bddce2..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd deleted file mode 100644 index 8a8c18b..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd deleted file mode 100644 index 5c42706..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd deleted file mode 100644 index 853c341..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd deleted file mode 100644 index da835ee..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd deleted file mode 100644 index 87ad265..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +++ /dev/null @@ -1,582 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd deleted file mode 100644 index 9e86f1b..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd deleted file mode 100644 index d0be42e..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +++ /dev/null @@ -1,4439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd deleted file mode 100644 index 8821dd1..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +++ /dev/null @@ -1,570 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd deleted file mode 100644 index ca2575c..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +++ /dev/null @@ -1,509 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd deleted file mode 100644 index dd079e6..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd deleted file mode 100644 index 3dd6cf6..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd deleted file mode 100644 index f1041e3..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd deleted file mode 100644 index 9c5b7a6..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +++ /dev/null @@ -1,3646 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd deleted file mode 100644 index 0f13678..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - 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/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd deleted file mode 100644 index a6de9d2..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd deleted file mode 100644 index 10e978b..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd deleted file mode 100644 index 4248bf7..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd deleted file mode 100644 index 5649746..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/mce/mc.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/mce/mc.xsd deleted file mode 100644 index ef72545..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/mce/mc.xsd +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd deleted file mode 100644 index f65f777..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd deleted file mode 100644 index 6b00755..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd deleted file mode 100644 index f321d33..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd deleted file mode 100644 index 364c6a9..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd deleted file mode 100644 index fed9d15..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd deleted file mode 100644 index 680cf15..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd deleted file mode 100644 index 89ada90..0000000 --- a/external/anthropic/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/external/anthropic/skills/pptx/scripts/office/soffice.py b/external/anthropic/skills/pptx/scripts/office/soffice.py deleted file mode 100644 index 0b4c99d..0000000 --- a/external/anthropic/skills/pptx/scripts/office/soffice.py +++ /dev/null @@ -1,192 +0,0 @@ -""" -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) - 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/external/anthropic/skills/pptx/scripts/office/validate.py b/external/anthropic/skills/pptx/scripts/office/validate.py deleted file mode 100755 index 29ca186..0000000 --- a/external/anthropic/skills/pptx/scripts/office/validate.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -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/external/anthropic/skills/pptx/scripts/office/validators/__init__.py b/external/anthropic/skills/pptx/scripts/office/validators/__init__.py deleted file mode 100644 index db092ec..0000000 --- a/external/anthropic/skills/pptx/scripts/office/validators/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -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/external/anthropic/skills/pptx/scripts/office/validators/base.py b/external/anthropic/skills/pptx/scripts/office/validators/base.py deleted file mode 100644 index 33fc97b..0000000 --- a/external/anthropic/skills/pptx/scripts/office/validators/base.py +++ /dev/null @@ -1,875 +0,0 @@ -""" -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)) - - with open(xml_file, "r") as f: - xml_doc = lxml.etree.parse(f) - - 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/external/anthropic/skills/pptx/scripts/office/validators/docx.py b/external/anthropic/skills/pptx/scripts/office/validators/docx.py deleted file mode 100644 index b181499..0000000 --- a/external/anthropic/skills/pptx/scripts/office/validators/docx.py +++ /dev/null @@ -1,466 +0,0 @@ -""" -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/external/anthropic/skills/pptx/scripts/office/validators/pptx.py b/external/anthropic/skills/pptx/scripts/office/validators/pptx.py deleted file mode 100644 index 318f0e6..0000000 --- a/external/anthropic/skills/pptx/scripts/office/validators/pptx.py +++ /dev/null @@ -1,441 +0,0 @@ -""" -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/external/anthropic/skills/pptx/scripts/office/validators/redlining.py b/external/anthropic/skills/pptx/scripts/office/validators/redlining.py deleted file mode 100644 index 4185c51..0000000 --- a/external/anthropic/skills/pptx/scripts/office/validators/redlining.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -Validator for tracked changes in Word documents. - -Detects untracked edits in word/document.xml: 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. - -Only the document body is compared. Headers, footers, footnotes and endnotes -are separate parts and are not checked. -""" - -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 - - -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 - - try: - modified_tree = ET.parse(modified_file) - modified_root = modified_tree.getroot() - original_tree = ET.parse(original_file) - original_root = original_tree.getroot() - except (ET.ParseError, DefusedXmlException) as e: - print(f"FAILED - Error parsing XML files: {e}") - return False - - new_changes = self._new_tracked_changes(original_root, modified_root) - self._remove_tracked_changes(modified_root, new_changes) - - modified_text = self._extract_text_content(modified_root) - original_text = self._extract_text_content(original_root) - - if modified_text != original_text: - error_message = self._generate_detailed_diff( - original_text, modified_text - ) - print(error_message) - return False - - if self.verbose: - print( - f"PASSED - All {len(new_changes)} change(s) against the original " - "are properly tracked" - ) - return True - - def _tracked_change_elements(self, root): - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - return [elem for elem in root.iter() if elem.tag in (ins_tag, del_tag)] - - 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, original_text, modified_text): - 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 ", - "", - ] - - git_diff = self._get_git_word_diff(original_text, modified_text) - if git_diff: - error_parts.extend(["Differences:", "============", git_diff]) - else: - error_parts.append("Unable to generate word diff (git not available)") - - 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, - text=True, - ) - - 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, - text=True, - ) - - 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): - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - - for parent in root.iter(): - to_remove = [] - for child in parent: - if child.tag == ins_tag and child in targets: - to_remove.append(child) - for elem in to_remove: - parent.remove(elem) - - deltext_tag = f"{{{self.namespaces['w']}}}delText" - t_tag = f"{{{self.namespaces['w']}}}t" - - for parent in root.iter(): - to_process = [] - for child in parent: - if child.tag == del_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(): - if elem.tag == deltext_tag: - elem.tag = t_tag - - for child in reversed(list(del_elem)): - parent.insert(del_index, child) - parent.remove(del_elem) - - def _extract_text_content(self, root): - p_tag = f"{{{self.namespaces['w']}}}p" - t_tag = f"{{{self.namespaces['w']}}}t" - - paragraphs = [] - for p_elem in root.findall(f".//{p_tag}"): - text_parts = [] - for t_elem in p_elem.findall(f".//{t_tag}"): - text_parts.append(self._rendered_text(t_elem)) - paragraph_text = "".join(text_parts) - if paragraph_text: - paragraphs.append(paragraph_text) - - return "\n".join(paragraphs) - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/external/anthropic/skills/pptx/scripts/thumbnail.py b/external/anthropic/skills/pptx/scripts/thumbnail.py deleted file mode 100755 index d49cac0..0000000 --- a/external/anthropic/skills/pptx/scripts/thumbnail.py +++ /dev/null @@ -1,311 +0,0 @@ -"""Create thumbnail grids from PowerPoint presentation slides. - -Creates a grid layout of slide thumbnails for quick visual analysis. -Labels each thumbnail with its XML filename (e.g., slide1.xml). -Hidden slides are shown with a placeholder pattern. - -Usage: - python thumbnail.py input.pptx [output_prefix] [--cols N] - -Examples: - python thumbnail.py presentation.pptx - # Creates: thumbnails.jpg - - python thumbnail.py template.pptx grid --cols 4 - # Creates: grid.jpg (or grid-1.jpg, grid-2.jpg for large decks) -""" - -import argparse -import posixpath -import subprocess -import sys -import tempfile -import zipfile -from pathlib import Path - -import defusedxml.minidom -from defusedxml import ElementTree -from office.helpers import SLIDE_REL_TYPE, opc_target -from office.soffice import run_soffice -from PIL import Image, ImageDraw, ImageFont - - -THUMBNAIL_WIDTH = 300 -CONVERSION_DPI = 100 -MAX_COLS = 6 -DEFAULT_COLS = 3 -JPEG_QUALITY = 95 -GRID_PADDING = 20 -BORDER_WIDTH = 2 -FONT_SIZE_RATIO = 0.10 -LABEL_PADDING_RATIO = 0.4 - - -def main(): - parser = argparse.ArgumentParser( - description="Create thumbnail grids from PowerPoint slides." - ) - parser.add_argument("input", help="Input PowerPoint file (.pptx)") - parser.add_argument( - "output_prefix", - nargs="?", - default="thumbnails", - help="Output prefix for image files (default: thumbnails)", - ) - parser.add_argument( - "--cols", - type=int, - default=DEFAULT_COLS, - help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})", - ) - - args = parser.parse_args() - - cols = min(args.cols, MAX_COLS) - if args.cols > MAX_COLS: - print(f"Warning: Columns limited to {MAX_COLS}") - - input_path = Path(args.input) - if not input_path.exists() or input_path.suffix.lower() != ".pptx": - print(f"Error: Invalid PowerPoint file: {args.input}", file=sys.stderr) - sys.exit(1) - - output_path = Path(f"{args.output_prefix}.jpg") - - try: - slide_info = get_slide_info(input_path) - - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - visible_images = convert_to_images(input_path, temp_path) - - if not visible_images and not any(s["hidden"] for s in slide_info): - print("Error: No slides found", file=sys.stderr) - sys.exit(1) - - slides = build_slide_list(slide_info, visible_images, temp_path) - - grid_files = create_grids(slides, cols, THUMBNAIL_WIDTH, output_path) - - print(f"Created {len(grid_files)} grid(s):") - for grid_file in grid_files: - print(f" {grid_file}") - - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - - -def _is_hidden(zf: zipfile.ZipFile, part: str) -> bool: - try: - with zf.open(part) as f: - for _, root in ElementTree.iterparse(f, events=("start",)): - return root.get("show") in ("0", "false") - except (KeyError, ElementTree.ParseError): - return False - return False - - -def get_slide_info(pptx_path: Path) -> list[dict]: - with zipfile.ZipFile(pptx_path, "r") as zf: - rels_content = zf.read("ppt/_rels/presentation.xml.rels").decode("utf-8") - rels_dom = defusedxml.minidom.parseString(rels_content) - - rid_to_part = {} - for rel in rels_dom.getElementsByTagName("Relationship"): - if rel.getAttribute("Type") != SLIDE_REL_TYPE: - continue - part = opc_target( - rel.getAttribute("Target"), - "ppt/presentation.xml", - rel.getAttribute("TargetMode"), - ) - if part is not None: - rid_to_part[rel.getAttribute("Id")] = part - - pres_content = zf.read("ppt/presentation.xml").decode("utf-8") - pres_dom = defusedxml.minidom.parseString(pres_content) - - present = set(zf.namelist()) - - slides = [] - for sld_id in pres_dom.getElementsByTagName("p:sldId"): - part = rid_to_part.get(sld_id.getAttribute("r:id")) - if part is not None and part in present: - slides.append( - {"name": posixpath.basename(part), "hidden": _is_hidden(zf, part)} - ) - - return slides - - -def build_slide_list( - slide_info: list[dict], - visible_images: list[Path], - temp_dir: Path, -) -> list[tuple[Path, str]]: - visible_count = sum(1 for info in slide_info if not info["hidden"]) - rendered_hidden = len(visible_images) == len(slide_info) != visible_count - - if not rendered_hidden and visible_count != len(visible_images): - raise ValueError( - f"LibreOffice rendered {len(visible_images)} page(s) for {visible_count} " - f"visible slide(s) of {len(slide_info)}; thumbnails would be mislabeled" - ) - - if visible_images: - with Image.open(visible_images[0]) as img: - placeholder_size = img.size - else: - placeholder_size = (1920, 1080) - - slides = [] - visible_idx = 0 - - for info in slide_info: - if info["hidden"] and not rendered_hidden: - placeholder_path = temp_dir / f"hidden-{info['name']}.jpg" - placeholder_img = create_hidden_placeholder(placeholder_size) - placeholder_img.save(placeholder_path, "JPEG") - slides.append((placeholder_path, f"{info['name']} (hidden)")) - else: - label = f"{info['name']} (hidden)" if info["hidden"] else info["name"] - slides.append((visible_images[visible_idx], label)) - visible_idx += 1 - - return slides - - -def create_hidden_placeholder(size: tuple[int, int]) -> Image.Image: - img = Image.new("RGB", size, color="#F0F0F0") - draw = ImageDraw.Draw(img) - line_width = max(5, min(size) // 100) - draw.line([(0, 0), size], fill="#CCCCCC", width=line_width) - draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width) - return img - - -def convert_to_images(pptx_path: Path, temp_dir: Path) -> list[Path]: - pdf_path = temp_dir / f"{pptx_path.stem}.pdf" - - result = run_soffice( - ["--headless", "--convert-to", "pdf", "--outdir", str(temp_dir), str(pptx_path)], - capture_output=True, - text=True, - ) - if result.returncode != 0 or not pdf_path.exists(): - detail = (result.stderr or result.stdout or "").strip() - raise RuntimeError(f"PDF conversion failed: {detail}" if detail else "PDF conversion failed") - - result = subprocess.run( - [ - "pdftoppm", - "-jpeg", - "-r", - str(CONVERSION_DPI), - str(pdf_path), - str(temp_dir / "slide"), - ], - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise RuntimeError("Image conversion failed") - - return sorted(temp_dir.glob("slide-*.jpg")) - - -def create_grids( - slides: list[tuple[Path, str]], - cols: int, - width: int, - output_path: Path, -) -> list[str]: - max_per_grid = cols * (cols + 1) - grid_files = [] - - for chunk_idx, start_idx in enumerate(range(0, len(slides), max_per_grid)): - end_idx = min(start_idx + max_per_grid, len(slides)) - chunk_slides = slides[start_idx:end_idx] - - grid = create_grid(chunk_slides, cols, width) - - if len(slides) <= max_per_grid: - grid_filename = output_path - else: - stem = output_path.stem - suffix = output_path.suffix - grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}" - - grid_filename.parent.mkdir(parents=True, exist_ok=True) - grid.save(str(grid_filename), quality=JPEG_QUALITY) - grid_files.append(str(grid_filename)) - - return grid_files - - -def create_grid( - slides: list[tuple[Path, str]], - cols: int, - width: int, -) -> Image.Image: - font_size = int(width * FONT_SIZE_RATIO) - label_padding = int(font_size * LABEL_PADDING_RATIO) - - with Image.open(slides[0][0]) as img: - aspect = img.height / img.width - height = int(width * aspect) - - rows = (len(slides) + cols - 1) // cols - grid_w = cols * width + (cols + 1) * GRID_PADDING - grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING - - grid = Image.new("RGB", (grid_w, grid_h), "white") - draw = ImageDraw.Draw(grid) - - try: - font = ImageFont.load_default(size=font_size) - except Exception: - font = ImageFont.load_default() - - for i, (img_path, slide_name) in enumerate(slides): - row, col = i // cols, i % cols - x = col * width + (col + 1) * GRID_PADDING - y_base = ( - row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING - ) - - label = slide_name - bbox = draw.textbbox((0, 0), label, font=font) - text_w = bbox[2] - bbox[0] - draw.text( - (x + (width - text_w) // 2, y_base + label_padding), - label, - fill="black", - font=font, - ) - - y_thumbnail = y_base + label_padding + font_size + label_padding - - with Image.open(img_path) as img: - img.thumbnail((width, height), Image.Resampling.LANCZOS) - w, h = img.size - tx = x + (width - w) // 2 - ty = y_thumbnail + (height - h) // 2 - grid.paste(img, (tx, ty)) - - if BORDER_WIDTH > 0: - draw.rectangle( - [ - (tx - BORDER_WIDTH, ty - BORDER_WIDTH), - (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1), - ], - outline="gray", - width=BORDER_WIDTH, - ) - - return grid - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/LICENSE.txt b/external/anthropic/skills/skill-creator/LICENSE.txt deleted file mode 100644 index 4f881c5..0000000 --- a/external/anthropic/skills/skill-creator/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2026 Anthropic, PBC. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/external/anthropic/skills/skill-creator/SKILL.md b/external/anthropic/skills/skill-creator/SKILL.md deleted file mode 100644 index 65b3a40..0000000 --- a/external/anthropic/skills/skill-creator/SKILL.md +++ /dev/null @@ -1,485 +0,0 @@ ---- -name: skill-creator -description: Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy. ---- - -# Skill Creator - -A skill for creating new skills and iteratively improving them. - -At a high level, the process of creating a skill goes like this: - -- Decide what you want the skill to do and roughly how it should do it -- Write a draft of the skill -- Create a few test prompts and run claude-with-access-to-the-skill on them -- Help the user evaluate the results both qualitatively and quantitatively - - While the runs happen in the background, draft some quantitative evals if there aren't any (if there are some, you can either use as is or modify if you feel something needs to change about them). Then explain them to the user (or if they already existed, explain the ones that already exist) - - Use the `eval-viewer/generate_review.py` script to show the user the results for them to look at, and also let them look at the quantitative metrics -- Rewrite the skill based on feedback from the user's evaluation of the results (and also if there are any glaring flaws that become apparent from the quantitative benchmarks) -- Repeat until you're satisfied -- Expand the test set and try again at larger scale - -Your job when using this skill is to figure out where the user is in this process and then jump in and help them progress through these stages. So for instance, maybe they're like "I want to make a skill for X". You can help narrow down what they mean, write a draft, write the test cases, figure out how they want to evaluate, run all the prompts, and repeat. - -On the other hand, maybe they already have a draft of the skill. In this case you can go straight to the eval/iterate part of the loop. - -Of course, you should always be flexible and if the user is like "I don't need to run a bunch of evaluations, just vibe with me", you can do that instead. - -Then after the skill is done (but again, the order is flexible), you can also run the skill description improver, which we have a whole separate script for, to optimize the triggering of the skill. - -Cool? Cool. - -## Communicating with the user - -The skill creator is liable to be used by people across a wide range of familiarity with coding jargon. If you haven't heard (and how could you, it's only very recently that it started), there's a trend now where the power of Claude is inspiring plumbers to open up their terminals, parents and grandparents to google "how to install npm". On the other hand, the bulk of users are probably fairly computer-literate. - -So please pay attention to context cues to understand how to phrase your communication! In the default case, just to give you some idea: - -- "evaluation" and "benchmark" are borderline, but OK -- for "JSON" and "assertion" you want to see serious cues from the user that they know what those things are before using them without explaining them - -It's OK to briefly explain terms if you're in doubt, and feel free to clarify terms with a short definition if you're unsure if the user will get it. - ---- - -## Creating a skill - -### Capture Intent - -Start by understanding the user's intent. The current conversation might already contain a workflow the user wants to capture (e.g., they say "turn this into a skill"). If so, extract answers from the conversation history first — the tools used, the sequence of steps, corrections the user made, input/output formats observed. The user may need to fill the gaps, and should confirm before proceeding to the next step. - -1. What should this skill enable Claude to do? -2. When should this skill trigger? (what user phrases/contexts) -3. What's the expected output format? -4. Should we set up test cases to verify the skill works? Skills with objectively verifiable outputs (file transforms, data extraction, code generation, fixed workflow steps) benefit from test cases. Skills with subjective outputs (writing style, art) often don't need them. Suggest the appropriate default based on the skill type, but let the user decide. - -### Interview and Research - -Proactively ask questions about edge cases, input/output formats, example files, success criteria, and dependencies. Wait to write test prompts until you've got this part ironed out. - -Check available MCPs - if useful for research (searching docs, finding similar skills, looking up best practices), research in parallel via subagents if available, otherwise inline. Come prepared with context to reduce burden on the user. - -### Write the SKILL.md - -Based on the user interview, fill in these components: - -- **name**: Skill identifier -- **description**: When to trigger, what it does. This is the primary triggering mechanism - include both what the skill does AND specific contexts for when to use it. All "when to use" info goes here, not in the body. Note: currently Claude has a tendency to "undertrigger" skills -- to not use them when they'd be useful. To combat this, please make the skill descriptions a little bit "pushy". So for instance, instead of "How to build a simple fast dashboard to display internal Anthropic data.", you might write "How to build a simple fast dashboard to display internal Anthropic data. Make sure to use this skill whenever the user mentions dashboards, data visualization, internal metrics, or wants to display any kind of company data, even if they don't explicitly ask for a 'dashboard.'" -- **compatibility**: Required tools, dependencies (optional, rarely needed) -- **the rest of the skill :)** - -### Skill Writing Guide - -#### Anatomy of a Skill - -``` -skill-name/ -├── SKILL.md (required) -│ ├── YAML frontmatter (name, description required) -│ └── Markdown instructions -└── Bundled Resources (optional) - ├── scripts/ - Executable code for deterministic/repetitive tasks - ├── references/ - Docs loaded into context as needed - └── assets/ - Files used in output (templates, icons, fonts) -``` - -#### Progressive Disclosure - -Skills use a three-level loading system: -1. **Metadata** (name + description) - Always in context (~100 words) -2. **SKILL.md body** - In context whenever skill triggers (<500 lines ideal) -3. **Bundled resources** - As needed (unlimited, scripts can execute without loading) - -These word counts are approximate and you can feel free to go longer if needed. - -**Key patterns:** -- Keep SKILL.md under 500 lines; if you're approaching this limit, add an additional layer of hierarchy along with clear pointers about where the model using the skill should go next to follow up. -- Reference files clearly from SKILL.md with guidance on when to read them -- For large reference files (>300 lines), include a table of contents - -**Domain organization**: When a skill supports multiple domains/frameworks, organize by variant: -``` -cloud-deploy/ -├── SKILL.md (workflow + selection) -└── references/ - ├── aws.md - ├── gcp.md - └── azure.md -``` -Claude reads only the relevant reference file. - -#### Principle of Lack of Surprise - -This goes without saying, but skills must not contain malware, exploit code, or any content that could compromise system security. A skill's contents should not surprise the user in their intent if described. Don't go along with requests to create misleading skills or skills designed to facilitate unauthorized access, data exfiltration, or other malicious activities. Things like a "roleplay as an XYZ" are OK though. - -#### Writing Patterns - -Prefer using the imperative form in instructions. - -**Defining output formats** - You can do it like this: -```markdown -## Report structure -ALWAYS use this exact template: -# [Title] -## Executive summary -## Key findings -## Recommendations -``` - -**Examples pattern** - It's useful to include examples. You can format them like this (but if "Input" and "Output" are in the examples you might want to deviate a little): -```markdown -## Commit message format -**Example 1:** -Input: Added user authentication with JWT tokens -Output: feat(auth): implement JWT-based authentication -``` - -### Writing Style - -Try to explain to the model why things are important in lieu of heavy-handed musty MUSTs. Use theory of mind and try to make the skill general and not super-narrow to specific examples. Start by writing a draft and then look at it with fresh eyes and improve it. - -### Test Cases - -After writing the skill draft, come up with 2-3 realistic test prompts — the kind of thing a real user would actually say. Share them with the user: [you don't have to use this exact language] "Here are a few test cases I'd like to try. Do these look right, or do you want to add more?" Then run them. - -Save test cases to `evals/evals.json`. Don't write assertions yet — just the prompts. You'll draft assertions in the next step while the runs are in progress. - -```json -{ - "skill_name": "example-skill", - "evals": [ - { - "id": 1, - "prompt": "User's task prompt", - "expected_output": "Description of expected result", - "files": [] - } - ] -} -``` - -See `references/schemas.md` for the full schema (including the `assertions` field, which you'll add later). - -## Running and evaluating test cases - -This section is one continuous sequence — don't stop partway through. Do NOT use `/skill-test` or any other testing skill. - -Put results in `-workspace/` as a sibling to the skill directory. Within the workspace, organize results by iteration (`iteration-1/`, `iteration-2/`, etc.) and within that, each test case gets a directory (`eval-0/`, `eval-1/`, etc.). Don't create all of this upfront — just create directories as you go. - -### Step 1: Spawn all runs (with-skill AND baseline) in the same turn - -For each test case, spawn two subagents in the same turn — one with the skill, one without. This is important: don't spawn the with-skill runs first and then come back for baselines later. Launch everything at once so it all finishes around the same time. - -**With-skill run:** - -``` -Execute this task: -- Skill path: -- Task: -- Input files: -- Save outputs to: /iteration-/eval-/with_skill/outputs/ -- Outputs to save: -``` - -**Baseline run** (same prompt, but the baseline depends on context): -- **Creating a new skill**: no skill at all. Same prompt, no skill path, save to `without_skill/outputs/`. -- **Improving an existing skill**: the old version. Before editing, snapshot the skill (`cp -r /skill-snapshot/`), then point the baseline subagent at the snapshot. Save to `old_skill/outputs/`. - -Write an `eval_metadata.json` for each test case (assertions can be empty for now). Give each eval a descriptive name based on what it's testing — not just "eval-0". Use this name for the directory too. If this iteration uses new or modified eval prompts, create these files for each new eval directory — don't assume they carry over from previous iterations. - -```json -{ - "eval_id": 0, - "eval_name": "descriptive-name-here", - "prompt": "The user's task prompt", - "assertions": [] -} -``` - -### Step 2: While runs are in progress, draft assertions - -Don't just wait for the runs to finish — you can use this time productively. Draft quantitative assertions for each test case and explain them to the user. If assertions already exist in `evals/evals.json`, review them and explain what they check. - -Good assertions are objectively verifiable and have descriptive names — they should read clearly in the benchmark viewer so someone glancing at the results immediately understands what each one checks. Subjective skills (writing style, design quality) are better evaluated qualitatively — don't force assertions onto things that need human judgment. - -Update the `eval_metadata.json` files and `evals/evals.json` with the assertions once drafted. Also explain to the user what they'll see in the viewer — both the qualitative outputs and the quantitative benchmark. - -### Step 3: As runs complete, capture timing data - -When each subagent task completes, you receive a notification containing `total_tokens` and `duration_ms`. Save this data immediately to `timing.json` in the run directory: - -```json -{ - "total_tokens": 84852, - "duration_ms": 23332, - "total_duration_seconds": 23.3 -} -``` - -This is the only opportunity to capture this data — it comes through the task notification and isn't persisted elsewhere. Process each notification as it arrives rather than trying to batch them. - -### Step 4: Grade, aggregate, and launch the viewer - -Once all runs are done: - -1. **Grade each run** — spawn a grader subagent (or grade inline) that reads `agents/grader.md` and evaluates each assertion against the outputs. Save results to `grading.json` in each run directory. The grading.json expectations array must use the fields `text`, `passed`, and `evidence` (not `name`/`met`/`details` or other variants) — the viewer depends on these exact field names. For assertions that can be checked programmatically, write and run a script rather than eyeballing it — scripts are faster, more reliable, and can be reused across iterations. - -2. **Aggregate into benchmark** — run the aggregation script from the skill-creator directory: - ```bash - python -m scripts.aggregate_benchmark /iteration-N --skill-name - ``` - This produces `benchmark.json` and `benchmark.md` with pass_rate, time, and tokens for each configuration, with mean ± stddev and the delta. If generating benchmark.json manually, see `references/schemas.md` for the exact schema the viewer expects. -Put each with_skill version before its baseline counterpart. - -3. **Do an analyst pass** — read the benchmark data and surface patterns the aggregate stats might hide. See `agents/analyzer.md` (the "Analyzing Benchmark Results" section) for what to look for — things like assertions that always pass regardless of skill (non-discriminating), high-variance evals (possibly flaky), and time/token tradeoffs. - -4. **Launch the viewer** with both qualitative outputs and quantitative data: - ```bash - nohup python /eval-viewer/generate_review.py \ - /iteration-N \ - --skill-name "my-skill" \ - --benchmark /iteration-N/benchmark.json \ - > /dev/null 2>&1 & - VIEWER_PID=$! - ``` - For iteration 2+, also pass `--previous-workspace /iteration-`. - - **Cowork / headless environments:** If `webbrowser.open()` is not available or the environment has no display, use `--static ` to write a standalone HTML file instead of starting a server. Feedback will be downloaded as a `feedback.json` file when the user clicks "Submit All Reviews". After download, copy `feedback.json` into the workspace directory for the next iteration to pick up. - -Note: please use generate_review.py to create the viewer; there's no need to write custom HTML. - -5. **Tell the user** something like: "I've opened the results in your browser. There are two tabs — 'Outputs' lets you click through each test case and leave feedback, 'Benchmark' shows the quantitative comparison. When you're done, come back here and let me know." - -### What the user sees in the viewer - -The "Outputs" tab shows one test case at a time: -- **Prompt**: the task that was given -- **Output**: the files the skill produced, rendered inline where possible -- **Previous Output** (iteration 2+): collapsed section showing last iteration's output -- **Formal Grades** (if grading was run): collapsed section showing assertion pass/fail -- **Feedback**: a textbox that auto-saves as they type -- **Previous Feedback** (iteration 2+): their comments from last time, shown below the textbox - -The "Benchmark" tab shows the stats summary: pass rates, timing, and token usage for each configuration, with per-eval breakdowns and analyst observations. - -Navigation is via prev/next buttons or arrow keys. When done, they click "Submit All Reviews" which saves all feedback to `feedback.json`. - -### Step 5: Read the feedback - -When the user tells you they're done, read `feedback.json`: - -```json -{ - "reviews": [ - {"run_id": "eval-0-with_skill", "feedback": "the chart is missing axis labels", "timestamp": "..."}, - {"run_id": "eval-1-with_skill", "feedback": "", "timestamp": "..."}, - {"run_id": "eval-2-with_skill", "feedback": "perfect, love this", "timestamp": "..."} - ], - "status": "complete" -} -``` - -Empty feedback means the user thought it was fine. Focus your improvements on the test cases where the user had specific complaints. - -Kill the viewer server when you're done with it: - -```bash -kill $VIEWER_PID 2>/dev/null -``` - ---- - -## Improving the skill - -This is the heart of the loop. You've run the test cases, the user has reviewed the results, and now you need to make the skill better based on their feedback. - -### How to think about improvements - -1. **Generalize from the feedback.** The big picture thing that's happening here is that we're trying to create skills that can be used a million times (maybe literally, maybe even more who knows) across many different prompts. Here you and the user are iterating on only a few examples over and over again because it helps move faster. The user knows these examples in and out and it's quick for them to assess new outputs. But if the skill you and the user are codeveloping works only for those examples, it's useless. Rather than put in fiddly overfitty changes, or oppressively constrictive MUSTs, if there's some stubborn issue, you might try branching out and using different metaphors, or recommending different patterns of working. It's relatively cheap to try and maybe you'll land on something great. - -2. **Keep the prompt lean.** Remove things that aren't pulling their weight. Make sure to read the transcripts, not just the final outputs — if it looks like the skill is making the model waste a bunch of time doing things that are unproductive, you can try getting rid of the parts of the skill that are making it do that and seeing what happens. - -3. **Explain the why.** Try hard to explain the **why** behind everything you're asking the model to do. Today's LLMs are *smart*. They have good theory of mind and when given a good harness can go beyond rote instructions and really make things happen. Even if the feedback from the user is terse or frustrated, try to actually understand the task and why the user is writing what they wrote, and what they actually wrote, and then transmit this understanding into the instructions. If you find yourself writing ALWAYS or NEVER in all caps, or using super rigid structures, that's a yellow flag — if possible, reframe and explain the reasoning so that the model understands why the thing you're asking for is important. That's a more humane, powerful, and effective approach. - -4. **Look for repeated work across test cases.** Read the transcripts from the test runs and notice if the subagents all independently wrote similar helper scripts or took the same multi-step approach to something. If all 3 test cases resulted in the subagent writing a `create_docx.py` or a `build_chart.py`, that's a strong signal the skill should bundle that script. Write it once, put it in `scripts/`, and tell the skill to use it. This saves every future invocation from reinventing the wheel. - -This task is pretty important (we are trying to create billions a year in economic value here!) and your thinking time is not the blocker; take your time and really mull things over. I'd suggest writing a draft revision and then looking at it anew and making improvements. Really do your best to get into the head of the user and understand what they want and need. - -### The iteration loop - -After improving the skill: - -1. Apply your improvements to the skill -2. Rerun all test cases into a new `iteration-/` directory, including baseline runs. If you're creating a new skill, the baseline is always `without_skill` (no skill) — that stays the same across iterations. If you're improving an existing skill, use your judgment on what makes sense as the baseline: the original version the user came in with, or the previous iteration. -3. Launch the reviewer with `--previous-workspace` pointing at the previous iteration -4. Wait for the user to review and tell you they're done -5. Read the new feedback, improve again, repeat - -Keep going until: -- The user says they're happy -- The feedback is all empty (everything looks good) -- You're not making meaningful progress - ---- - -## Advanced: Blind comparison - -For situations where you want a more rigorous comparison between two versions of a skill (e.g., the user asks "is the new version actually better?"), there's a blind comparison system. Read `agents/comparator.md` and `agents/analyzer.md` for the details. The basic idea is: give two outputs to an independent agent without telling it which is which, and let it judge quality. Then analyze why the winner won. - -This is optional, requires subagents, and most users won't need it. The human review loop is usually sufficient. - ---- - -## Description Optimization - -The description field in SKILL.md frontmatter is the primary mechanism that determines whether Claude invokes a skill. After creating or improving a skill, offer to optimize the description for better triggering accuracy. - -### Step 1: Generate trigger eval queries - -Create 20 eval queries — a mix of should-trigger and should-not-trigger. Save as JSON: - -```json -[ - {"query": "the user prompt", "should_trigger": true}, - {"query": "another prompt", "should_trigger": false} -] -``` - -The queries must be realistic and something a Claude Code or Claude.ai user would actually type. Not abstract requests, but requests that are concrete and specific and have a good amount of detail. For instance, file paths, personal context about the user's job or situation, column names and values, company names, URLs. A little bit of backstory. Some might be in lowercase or contain abbreviations or typos or casual speech. Use a mix of different lengths, and focus on edge cases rather than making them clear-cut (the user will get a chance to sign off on them). - -Bad: `"Format this data"`, `"Extract text from PDF"`, `"Create a chart"` - -Good: `"ok so my boss just sent me this xlsx file (its in my downloads, called something like 'Q4 sales final FINAL v2.xlsx') and she wants me to add a column that shows the profit margin as a percentage. The revenue is in column C and costs are in column D i think"` - -For the **should-trigger** queries (8-10), think about coverage. You want different phrasings of the same intent — some formal, some casual. Include cases where the user doesn't explicitly name the skill or file type but clearly needs it. Throw in some uncommon use cases and cases where this skill competes with another but should win. - -For the **should-not-trigger** queries (8-10), the most valuable ones are the near-misses — queries that share keywords or concepts with the skill but actually need something different. Think adjacent domains, ambiguous phrasing where a naive keyword match would trigger but shouldn't, and cases where the query touches on something the skill does but in a context where another tool is more appropriate. - -The key thing to avoid: don't make should-not-trigger queries obviously irrelevant. "Write a fibonacci function" as a negative test for a PDF skill is too easy — it doesn't test anything. The negative cases should be genuinely tricky. - -### Step 2: Review with user - -Present the eval set to the user for review using the HTML template: - -1. Read the template from `assets/eval_review.html` -2. Replace the placeholders: - - `__EVAL_DATA_PLACEHOLDER__` → the JSON array of eval items (no quotes around it — it's a JS variable assignment) - - `__SKILL_NAME_PLACEHOLDER__` → the skill's name - - `__SKILL_DESCRIPTION_PLACEHOLDER__` → the skill's current description -3. Write to a temp file (e.g., `/tmp/eval_review_.html`) and open it: `open /tmp/eval_review_.html` -4. The user can edit queries, toggle should-trigger, add/remove entries, then click "Export Eval Set" -5. The file downloads to `~/Downloads/eval_set.json` — check the Downloads folder for the most recent version in case there are multiple (e.g., `eval_set (1).json`) - -This step matters — bad eval queries lead to bad descriptions. - -### Step 3: Run the optimization loop - -Tell the user: "This will take some time — I'll run the optimization loop in the background and check on it periodically." - -Save the eval set to the workspace, then run in the background: - -```bash -python -m scripts.run_loop \ - --eval-set \ - --skill-path \ - --model \ - --max-iterations 5 \ - --verbose -``` - -Use the model ID from your system prompt (the one powering the current session) so the triggering test matches what the user actually experiences. - -While it runs, periodically tail the output to give the user updates on which iteration it's on and what the scores look like. - -This handles the full optimization loop automatically. It splits the eval set into 60% train and 40% held-out test, evaluates the current description (running each query 3 times to get a reliable trigger rate), then calls Claude to propose improvements based on what failed. It re-evaluates each new description on both train and test, iterating up to 5 times. When it's done, it opens an HTML report in the browser showing the results per iteration and returns JSON with `best_description` — selected by test score rather than train score to avoid overfitting. - -### How skill triggering works - -Understanding the triggering mechanism helps design better eval queries. Skills appear in Claude's `available_skills` list with their name + description, and Claude decides whether to consult a skill based on that description. The important thing to know is that Claude only consults skills for tasks it can't easily handle on its own — simple, one-step queries like "read this PDF" may not trigger a skill even if the description matches perfectly, because Claude can handle them directly with basic tools. Complex, multi-step, or specialized queries reliably trigger skills when the description matches. - -This means your eval queries should be substantive enough that Claude would actually benefit from consulting a skill. Simple queries like "read file X" are poor test cases — they won't trigger skills regardless of description quality. - -### Step 4: Apply the result - -Take `best_description` from the JSON output and update the skill's SKILL.md frontmatter. Show the user before/after and report the scores. - ---- - -### Package and Present (only if `present_files` tool is available) - -Check whether you have access to the `present_files` tool. If you don't, skip this step. If you do, package the skill and present the .skill file to the user: - -```bash -python -m scripts.package_skill -``` - -After packaging, direct the user to the resulting `.skill` file path so they can install it. - ---- - -## Claude.ai-specific instructions - -In Claude.ai, the core workflow is the same (draft → test → review → improve → repeat), but because Claude.ai doesn't have subagents, some mechanics change. Here's what to adapt: - -**Running test cases**: No subagents means no parallel execution. For each test case, read the skill's SKILL.md, then follow its instructions to accomplish the test prompt yourself. Do them one at a time. This is less rigorous than independent subagents (you wrote the skill and you're also running it, so you have full context), but it's a useful sanity check — and the human review step compensates. Skip the baseline runs — just use the skill to complete the task as requested. - -**Reviewing results**: If you can't open a browser (e.g., Claude.ai's VM has no display, or you're on a remote server), skip the browser reviewer entirely. Instead, present results directly in the conversation. For each test case, show the prompt and the output. If the output is a file the user needs to see (like a .docx or .xlsx), save it to the filesystem and tell them where it is so they can download and inspect it. Ask for feedback inline: "How does this look? Anything you'd change?" - -**Benchmarking**: Skip the quantitative benchmarking — it relies on baseline comparisons which aren't meaningful without subagents. Focus on qualitative feedback from the user. - -**The iteration loop**: Same as before — improve the skill, rerun the test cases, ask for feedback — just without the browser reviewer in the middle. You can still organize results into iteration directories on the filesystem if you have one. - -**Description optimization**: This section requires the `claude` CLI tool (specifically `claude -p`) which is only available in Claude Code. Skip it if you're on Claude.ai. - -**Blind comparison**: Requires subagents. Skip it. - -**Packaging**: The `package_skill.py` script works anywhere with Python and a filesystem. On Claude.ai, you can run it and the user can download the resulting `.skill` file. - -**Updating an existing skill**: The user might be asking you to update an existing skill, not create a new one. In this case: -- **Preserve the original name.** Note the skill's directory name and `name` frontmatter field -- use them unchanged. E.g., if the installed skill is `research-helper`, output `research-helper.skill` (not `research-helper-v2`). -- **Copy to a writeable location before editing.** The installed skill path may be read-only. Copy to `/tmp/skill-name/`, edit there, and package from the copy. -- **If packaging manually, stage in `/tmp/` first**, then copy to the output directory -- direct writes may fail due to permissions. - ---- - -## Cowork-Specific Instructions - -If you're in Cowork, the main things to know are: - -- You have subagents, so the main workflow (spawn test cases in parallel, run baselines, grade, etc.) all works. (However, if you run into severe problems with timeouts, it's OK to run the test prompts in series rather than parallel.) -- You don't have a browser or display, so when generating the eval viewer, use `--static ` to write a standalone HTML file instead of starting a server. Then proffer a link that the user can click to open the HTML in their browser. -- For whatever reason, the Cowork setup seems to disincline Claude from generating the eval viewer after running the tests, so just to reiterate: whether you're in Cowork or in Claude Code, after running tests, you should always generate the eval viewer for the human to look at examples before revising the skill yourself and trying to make corrections, using `generate_review.py` (not writing your own boutique html code). Sorry in advance but I'm gonna go all caps here: GENERATE THE EVAL VIEWER *BEFORE* evaluating inputs yourself. You want to get them in front of the human ASAP! -- Feedback works differently: since there's no running server, the viewer's "Submit All Reviews" button will download `feedback.json` as a file. You can then read it from there (you may have to request access first). -- Packaging works — `package_skill.py` just needs Python and a filesystem. -- Description optimization (`run_loop.py` / `run_eval.py`) should work in Cowork just fine since it uses `claude -p` via subprocess, not a browser, but please save it until you've fully finished making the skill and the user agrees it's in good shape. -- **Updating an existing skill**: The user might be asking you to update an existing skill, not create a new one. Follow the update guidance in the claude.ai section above. - ---- - -## Reference files - -The agents/ directory contains instructions for specialized subagents. Read them when you need to spawn the relevant subagent. - -- `agents/grader.md` — How to evaluate assertions against outputs -- `agents/comparator.md` — How to do blind A/B comparison between two outputs -- `agents/analyzer.md` — How to analyze why one version beat another - -The references/ directory has additional documentation: -- `references/schemas.md` — JSON structures for evals.json, grading.json, etc. - ---- - -Repeating one more time the core loop here for emphasis: - -- Figure out what the skill is about -- Draft or edit the skill -- Run claude-with-access-to-the-skill on test prompts -- With the user, evaluate the outputs: - - Create benchmark.json and run `eval-viewer/generate_review.py` to help the user review them - - Run quantitative evals -- Repeat until you and the user are satisfied -- Package the final skill and return it to the user. - -Please add steps to your TodoList, if you have such a thing, to make sure you don't forget. If you're in Cowork, please specifically put "Create evals JSON and run `eval-viewer/generate_review.py` so human can review test cases" in your TodoList to make sure it happens. - -Good luck! diff --git a/external/anthropic/skills/skill-creator/agents/analyzer.md b/external/anthropic/skills/skill-creator/agents/analyzer.md deleted file mode 100644 index 14e41d6..0000000 --- a/external/anthropic/skills/skill-creator/agents/analyzer.md +++ /dev/null @@ -1,274 +0,0 @@ -# Post-hoc Analyzer Agent - -Analyze blind comparison results to understand WHY the winner won and generate improvement suggestions. - -## Role - -After the blind comparator determines a winner, the Post-hoc Analyzer "unblids" the results by examining the skills and transcripts. The goal is to extract actionable insights: what made the winner better, and how can the loser be improved? - -## Inputs - -You receive these parameters in your prompt: - -- **winner**: "A" or "B" (from blind comparison) -- **winner_skill_path**: Path to the skill that produced the winning output -- **winner_transcript_path**: Path to the execution transcript for the winner -- **loser_skill_path**: Path to the skill that produced the losing output -- **loser_transcript_path**: Path to the execution transcript for the loser -- **comparison_result_path**: Path to the blind comparator's output JSON -- **output_path**: Where to save the analysis results - -## Process - -### Step 1: Read Comparison Result - -1. Read the blind comparator's output at comparison_result_path -2. Note the winning side (A or B), the reasoning, and any scores -3. Understand what the comparator valued in the winning output - -### Step 2: Read Both Skills - -1. Read the winner skill's SKILL.md and key referenced files -2. Read the loser skill's SKILL.md and key referenced files -3. Identify structural differences: - - Instructions clarity and specificity - - Script/tool usage patterns - - Example coverage - - Edge case handling - -### Step 3: Read Both Transcripts - -1. Read the winner's transcript -2. Read the loser's transcript -3. Compare execution patterns: - - How closely did each follow their skill's instructions? - - What tools were used differently? - - Where did the loser diverge from optimal behavior? - - Did either encounter errors or make recovery attempts? - -### Step 4: Analyze Instruction Following - -For each transcript, evaluate: -- Did the agent follow the skill's explicit instructions? -- Did the agent use the skill's provided tools/scripts? -- Were there missed opportunities to leverage skill content? -- Did the agent add unnecessary steps not in the skill? - -Score instruction following 1-10 and note specific issues. - -### Step 5: Identify Winner Strengths - -Determine what made the winner better: -- Clearer instructions that led to better behavior? -- Better scripts/tools that produced better output? -- More comprehensive examples that guided edge cases? -- Better error handling guidance? - -Be specific. Quote from skills/transcripts where relevant. - -### Step 6: Identify Loser Weaknesses - -Determine what held the loser back: -- Ambiguous instructions that led to suboptimal choices? -- Missing tools/scripts that forced workarounds? -- Gaps in edge case coverage? -- Poor error handling that caused failures? - -### Step 7: Generate Improvement Suggestions - -Based on the analysis, produce actionable suggestions for improving the loser skill: -- Specific instruction changes to make -- Tools/scripts to add or modify -- Examples to include -- Edge cases to address - -Prioritize by impact. Focus on changes that would have changed the outcome. - -### Step 8: Write Analysis Results - -Save structured analysis to `{output_path}`. - -## Output Format - -Write a JSON file with this structure: - -```json -{ - "comparison_summary": { - "winner": "A", - "winner_skill": "path/to/winner/skill", - "loser_skill": "path/to/loser/skill", - "comparator_reasoning": "Brief summary of why comparator chose winner" - }, - "winner_strengths": [ - "Clear step-by-step instructions for handling multi-page documents", - "Included validation script that caught formatting errors", - "Explicit guidance on fallback behavior when OCR fails" - ], - "loser_weaknesses": [ - "Vague instruction 'process the document appropriately' led to inconsistent behavior", - "No script for validation, agent had to improvise and made errors", - "No guidance on OCR failure, agent gave up instead of trying alternatives" - ], - "instruction_following": { - "winner": { - "score": 9, - "issues": [ - "Minor: skipped optional logging step" - ] - }, - "loser": { - "score": 6, - "issues": [ - "Did not use the skill's formatting template", - "Invented own approach instead of following step 3", - "Missed the 'always validate output' instruction" - ] - } - }, - "improvement_suggestions": [ - { - "priority": "high", - "category": "instructions", - "suggestion": "Replace 'process the document appropriately' with explicit steps: 1) Extract text, 2) Identify sections, 3) Format per template", - "expected_impact": "Would eliminate ambiguity that caused inconsistent behavior" - }, - { - "priority": "high", - "category": "tools", - "suggestion": "Add validate_output.py script similar to winner skill's validation approach", - "expected_impact": "Would catch formatting errors before final output" - }, - { - "priority": "medium", - "category": "error_handling", - "suggestion": "Add fallback instructions: 'If OCR fails, try: 1) different resolution, 2) image preprocessing, 3) manual extraction'", - "expected_impact": "Would prevent early failure on difficult documents" - } - ], - "transcript_insights": { - "winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script -> Fixed 2 issues -> Produced output", - "loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods -> No validation -> Output had errors" - } -} -``` - -## Guidelines - -- **Be specific**: Quote from skills and transcripts, don't just say "instructions were unclear" -- **Be actionable**: Suggestions should be concrete changes, not vague advice -- **Focus on skill improvements**: The goal is to improve the losing skill, not critique the agent -- **Prioritize by impact**: Which changes would most likely have changed the outcome? -- **Consider causation**: Did the skill weakness actually cause the worse output, or is it incidental? -- **Stay objective**: Analyze what happened, don't editorialize -- **Think about generalization**: Would this improvement help on other evals too? - -## Categories for Suggestions - -Use these categories to organize improvement suggestions: - -| Category | Description | -|----------|-------------| -| `instructions` | Changes to the skill's prose instructions | -| `tools` | Scripts, templates, or utilities to add/modify | -| `examples` | Example inputs/outputs to include | -| `error_handling` | Guidance for handling failures | -| `structure` | Reorganization of skill content | -| `references` | External docs or resources to add | - -## Priority Levels - -- **high**: Would likely change the outcome of this comparison -- **medium**: Would improve quality but may not change win/loss -- **low**: Nice to have, marginal improvement - ---- - -# Analyzing Benchmark Results - -When analyzing benchmark results, the analyzer's purpose is to **surface patterns and anomalies** across multiple runs, not suggest skill improvements. - -## Role - -Review all benchmark run results and generate freeform notes that help the user understand skill performance. Focus on patterns that wouldn't be visible from aggregate metrics alone. - -## Inputs - -You receive these parameters in your prompt: - -- **benchmark_data_path**: Path to the in-progress benchmark.json with all run results -- **skill_path**: Path to the skill being benchmarked -- **output_path**: Where to save the notes (as JSON array of strings) - -## Process - -### Step 1: Read Benchmark Data - -1. Read the benchmark.json containing all run results -2. Note the configurations tested (with_skill, without_skill) -3. Understand the run_summary aggregates already calculated - -### Step 2: Analyze Per-Assertion Patterns - -For each expectation across all runs: -- Does it **always pass** in both configurations? (may not differentiate skill value) -- Does it **always fail** in both configurations? (may be broken or beyond capability) -- Does it **always pass with skill but fail without**? (skill clearly adds value here) -- Does it **always fail with skill but pass without**? (skill may be hurting) -- Is it **highly variable**? (flaky expectation or non-deterministic behavior) - -### Step 3: Analyze Cross-Eval Patterns - -Look for patterns across evals: -- Are certain eval types consistently harder/easier? -- Do some evals show high variance while others are stable? -- Are there surprising results that contradict expectations? - -### Step 4: Analyze Metrics Patterns - -Look at time_seconds, tokens, tool_calls: -- Does the skill significantly increase execution time? -- Is there high variance in resource usage? -- Are there outlier runs that skew the aggregates? - -### Step 5: Generate Notes - -Write freeform observations as a list of strings. Each note should: -- State a specific observation -- Be grounded in the data (not speculation) -- Help the user understand something the aggregate metrics don't show - -Examples: -- "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value" -- "Eval 3 shows high variance (50% ± 40%) - run 2 had an unusual failure that may be flaky" -- "Without-skill runs consistently fail on table extraction expectations (0% pass rate)" -- "Skill adds 13s average execution time but improves pass rate by 50%" -- "Token usage is 80% higher with skill, primarily due to script output parsing" -- "All 3 without-skill runs for eval 1 produced empty output" - -### Step 6: Write Notes - -Save notes to `{output_path}` as a JSON array of strings: - -```json -[ - "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value", - "Eval 3 shows high variance (50% ± 40%) - run 2 had an unusual failure", - "Without-skill runs consistently fail on table extraction expectations", - "Skill adds 13s average execution time but improves pass rate by 50%" -] -``` - -## Guidelines - -**DO:** -- Report what you observe in the data -- Be specific about which evals, expectations, or runs you're referring to -- Note patterns that aggregate metrics would hide -- Provide context that helps interpret the numbers - -**DO NOT:** -- Suggest improvements to the skill (that's for the improvement step, not benchmarking) -- Make subjective quality judgments ("the output was good/bad") -- Speculate about causes without evidence -- Repeat information already in the run_summary aggregates diff --git a/external/anthropic/skills/skill-creator/agents/comparator.md b/external/anthropic/skills/skill-creator/agents/comparator.md deleted file mode 100644 index 80e00eb..0000000 --- a/external/anthropic/skills/skill-creator/agents/comparator.md +++ /dev/null @@ -1,202 +0,0 @@ -# Blind Comparator Agent - -Compare two outputs WITHOUT knowing which skill produced them. - -## Role - -The Blind Comparator judges which output better accomplishes the eval task. You receive two outputs labeled A and B, but you do NOT know which skill produced which. This prevents bias toward a particular skill or approach. - -Your judgment is based purely on output quality and task completion. - -## Inputs - -You receive these parameters in your prompt: - -- **output_a_path**: Path to the first output file or directory -- **output_b_path**: Path to the second output file or directory -- **eval_prompt**: The original task/prompt that was executed -- **expectations**: List of expectations to check (optional - may be empty) - -## Process - -### Step 1: Read Both Outputs - -1. Examine output A (file or directory) -2. Examine output B (file or directory) -3. Note the type, structure, and content of each -4. If outputs are directories, examine all relevant files inside - -### Step 2: Understand the Task - -1. Read the eval_prompt carefully -2. Identify what the task requires: - - What should be produced? - - What qualities matter (accuracy, completeness, format)? - - What would distinguish a good output from a poor one? - -### Step 3: Generate Evaluation Rubric - -Based on the task, generate a rubric with two dimensions: - -**Content Rubric** (what the output contains): -| Criterion | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) | -|-----------|----------|----------------|---------------| -| Correctness | Major errors | Minor errors | Fully correct | -| Completeness | Missing key elements | Mostly complete | All elements present | -| Accuracy | Significant inaccuracies | Minor inaccuracies | Accurate throughout | - -**Structure Rubric** (how the output is organized): -| Criterion | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) | -|-----------|----------|----------------|---------------| -| Organization | Disorganized | Reasonably organized | Clear, logical structure | -| Formatting | Inconsistent/broken | Mostly consistent | Professional, polished | -| Usability | Difficult to use | Usable with effort | Easy to use | - -Adapt criteria to the specific task. For example: -- PDF form → "Field alignment", "Text readability", "Data placement" -- Document → "Section structure", "Heading hierarchy", "Paragraph flow" -- Data output → "Schema correctness", "Data types", "Completeness" - -### Step 4: Evaluate Each Output Against the Rubric - -For each output (A and B): - -1. **Score each criterion** on the rubric (1-5 scale) -2. **Calculate dimension totals**: Content score, Structure score -3. **Calculate overall score**: Average of dimension scores, scaled to 1-10 - -### Step 5: Check Assertions (if provided) - -If expectations are provided: - -1. Check each expectation against output A -2. Check each expectation against output B -3. Count pass rates for each output -4. Use expectation scores as secondary evidence (not the primary decision factor) - -### Step 6: Determine the Winner - -Compare A and B based on (in priority order): - -1. **Primary**: Overall rubric score (content + structure) -2. **Secondary**: Assertion pass rates (if applicable) -3. **Tiebreaker**: If truly equal, declare a TIE - -Be decisive - ties should be rare. One output is usually better, even if marginally. - -### Step 7: Write Comparison Results - -Save results to a JSON file at the path specified (or `comparison.json` if not specified). - -## Output Format - -Write a JSON file with this structure: - -```json -{ - "winner": "A", - "reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.", - "rubric": { - "A": { - "content": { - "correctness": 5, - "completeness": 5, - "accuracy": 4 - }, - "structure": { - "organization": 4, - "formatting": 5, - "usability": 4 - }, - "content_score": 4.7, - "structure_score": 4.3, - "overall_score": 9.0 - }, - "B": { - "content": { - "correctness": 3, - "completeness": 2, - "accuracy": 3 - }, - "structure": { - "organization": 3, - "formatting": 2, - "usability": 3 - }, - "content_score": 2.7, - "structure_score": 2.7, - "overall_score": 5.4 - } - }, - "output_quality": { - "A": { - "score": 9, - "strengths": ["Complete solution", "Well-formatted", "All fields present"], - "weaknesses": ["Minor style inconsistency in header"] - }, - "B": { - "score": 5, - "strengths": ["Readable output", "Correct basic structure"], - "weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"] - } - }, - "expectation_results": { - "A": { - "passed": 4, - "total": 5, - "pass_rate": 0.80, - "details": [ - {"text": "Output includes name", "passed": true}, - {"text": "Output includes date", "passed": true}, - {"text": "Format is PDF", "passed": true}, - {"text": "Contains signature", "passed": false}, - {"text": "Readable text", "passed": true} - ] - }, - "B": { - "passed": 3, - "total": 5, - "pass_rate": 0.60, - "details": [ - {"text": "Output includes name", "passed": true}, - {"text": "Output includes date", "passed": false}, - {"text": "Format is PDF", "passed": true}, - {"text": "Contains signature", "passed": false}, - {"text": "Readable text", "passed": true} - ] - } - } -} -``` - -If no expectations were provided, omit the `expectation_results` field entirely. - -## Field Descriptions - -- **winner**: "A", "B", or "TIE" -- **reasoning**: Clear explanation of why the winner was chosen (or why it's a tie) -- **rubric**: Structured rubric evaluation for each output - - **content**: Scores for content criteria (correctness, completeness, accuracy) - - **structure**: Scores for structure criteria (organization, formatting, usability) - - **content_score**: Average of content criteria (1-5) - - **structure_score**: Average of structure criteria (1-5) - - **overall_score**: Combined score scaled to 1-10 -- **output_quality**: Summary quality assessment - - **score**: 1-10 rating (should match rubric overall_score) - - **strengths**: List of positive aspects - - **weaknesses**: List of issues or shortcomings -- **expectation_results**: (Only if expectations provided) - - **passed**: Number of expectations that passed - - **total**: Total number of expectations - - **pass_rate**: Fraction passed (0.0 to 1.0) - - **details**: Individual expectation results - -## Guidelines - -- **Stay blind**: DO NOT try to infer which skill produced which output. Judge purely on output quality. -- **Be specific**: Cite specific examples when explaining strengths and weaknesses. -- **Be decisive**: Choose a winner unless outputs are genuinely equivalent. -- **Output quality first**: Assertion scores are secondary to overall task completion. -- **Be objective**: Don't favor outputs based on style preferences; focus on correctness and completeness. -- **Explain your reasoning**: The reasoning field should make it clear why you chose the winner. -- **Handle edge cases**: If both outputs fail, pick the one that fails less badly. If both are excellent, pick the one that's marginally better. diff --git a/external/anthropic/skills/skill-creator/agents/grader.md b/external/anthropic/skills/skill-creator/agents/grader.md deleted file mode 100644 index 558ab05..0000000 --- a/external/anthropic/skills/skill-creator/agents/grader.md +++ /dev/null @@ -1,223 +0,0 @@ -# Grader Agent - -Evaluate expectations against an execution transcript and outputs. - -## Role - -The Grader reviews a transcript and output files, then determines whether each expectation passes or fails. Provide clear evidence for each judgment. - -You have two jobs: grade the outputs, and critique the evals themselves. A passing grade on a weak assertion is worse than useless — it creates false confidence. When you notice an assertion that's trivially satisfied, or an important outcome that no assertion checks, say so. - -## Inputs - -You receive these parameters in your prompt: - -- **expectations**: List of expectations to evaluate (strings) -- **transcript_path**: Path to the execution transcript (markdown file) -- **outputs_dir**: Directory containing output files from execution - -## Process - -### Step 1: Read the Transcript - -1. Read the transcript file completely -2. Note the eval prompt, execution steps, and final result -3. Identify any issues or errors documented - -### Step 2: Examine Output Files - -1. List files in outputs_dir -2. Read/examine each file relevant to the expectations. If outputs aren't plain text, use the inspection tools provided in your prompt — don't rely solely on what the transcript says the executor produced. -3. Note contents, structure, and quality - -### Step 3: Evaluate Each Assertion - -For each expectation: - -1. **Search for evidence** in the transcript and outputs -2. **Determine verdict**: - - **PASS**: Clear evidence the expectation is true AND the evidence reflects genuine task completion, not just surface-level compliance - - **FAIL**: No evidence, or evidence contradicts the expectation, or the evidence is superficial (e.g., correct filename but empty/wrong content) -3. **Cite the evidence**: Quote the specific text or describe what you found - -### Step 4: Extract and Verify Claims - -Beyond the predefined expectations, extract implicit claims from the outputs and verify them: - -1. **Extract claims** from the transcript and outputs: - - Factual statements ("The form has 12 fields") - - Process claims ("Used pypdf to fill the form") - - Quality claims ("All fields were filled correctly") - -2. **Verify each claim**: - - **Factual claims**: Can be checked against the outputs or external sources - - **Process claims**: Can be verified from the transcript - - **Quality claims**: Evaluate whether the claim is justified - -3. **Flag unverifiable claims**: Note claims that cannot be verified with available information - -This catches issues that predefined expectations might miss. - -### Step 5: Read User Notes - -If `{outputs_dir}/user_notes.md` exists: -1. Read it and note any uncertainties or issues flagged by the executor -2. Include relevant concerns in the grading output -3. These may reveal problems even when expectations pass - -### Step 6: Critique the Evals - -After grading, consider whether the evals themselves could be improved. Only surface suggestions when there's a clear gap. - -Good suggestions test meaningful outcomes — assertions that are hard to satisfy without actually doing the work correctly. Think about what makes an assertion *discriminating*: it passes when the skill genuinely succeeds and fails when it doesn't. - -Suggestions worth raising: -- An assertion that passed but would also pass for a clearly wrong output (e.g., checking filename existence but not file content) -- An important outcome you observed — good or bad — that no assertion covers at all -- An assertion that can't actually be verified from the available outputs - -Keep the bar high. The goal is to flag things the eval author would say "good catch" about, not to nitpick every assertion. - -### Step 7: Write Grading Results - -Save results to `{outputs_dir}/../grading.json` (sibling to outputs_dir). - -## Grading Criteria - -**PASS when**: -- The transcript or outputs clearly demonstrate the expectation is true -- Specific evidence can be cited -- The evidence reflects genuine substance, not just surface compliance (e.g., a file exists AND contains correct content, not just the right filename) - -**FAIL when**: -- No evidence found for the expectation -- Evidence contradicts the expectation -- The expectation cannot be verified from available information -- The evidence is superficial — the assertion is technically satisfied but the underlying task outcome is wrong or incomplete -- The output appears to meet the assertion by coincidence rather than by actually doing the work - -**When uncertain**: The burden of proof to pass is on the expectation. - -### Step 8: Read Executor Metrics and Timing - -1. If `{outputs_dir}/metrics.json` exists, read it and include in grading output -2. If `{outputs_dir}/../timing.json` exists, read it and include timing data - -## Output Format - -Write a JSON file with this structure: - -```json -{ - "expectations": [ - { - "text": "The output includes the name 'John Smith'", - "passed": true, - "evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'" - }, - { - "text": "The spreadsheet has a SUM formula in cell B10", - "passed": false, - "evidence": "No spreadsheet was created. The output was a text file." - }, - { - "text": "The assistant used the skill's OCR script", - "passed": true, - "evidence": "Transcript Step 2 shows: 'Tool: Bash - python ocr_script.py image.png'" - } - ], - "summary": { - "passed": 2, - "failed": 1, - "total": 3, - "pass_rate": 0.67 - }, - "execution_metrics": { - "tool_calls": { - "Read": 5, - "Write": 2, - "Bash": 8 - }, - "total_tool_calls": 15, - "total_steps": 6, - "errors_encountered": 0, - "output_chars": 12450, - "transcript_chars": 3200 - }, - "timing": { - "executor_duration_seconds": 165.0, - "grader_duration_seconds": 26.0, - "total_duration_seconds": 191.0 - }, - "claims": [ - { - "claim": "The form has 12 fillable fields", - "type": "factual", - "verified": true, - "evidence": "Counted 12 fields in field_info.json" - }, - { - "claim": "All required fields were populated", - "type": "quality", - "verified": false, - "evidence": "Reference section was left blank despite data being available" - } - ], - "user_notes_summary": { - "uncertainties": ["Used 2023 data, may be stale"], - "needs_review": [], - "workarounds": ["Fell back to text overlay for non-fillable fields"] - }, - "eval_feedback": { - "suggestions": [ - { - "assertion": "The output includes the name 'John Smith'", - "reason": "A hallucinated document that mentions the name would also pass — consider checking it appears as the primary contact with matching phone and email from the input" - }, - { - "reason": "No assertion checks whether the extracted phone numbers match the input — I observed incorrect numbers in the output that went uncaught" - } - ], - "overall": "Assertions check presence but not correctness. Consider adding content verification." - } -} -``` - -## Field Descriptions - -- **expectations**: Array of graded expectations - - **text**: The original expectation text - - **passed**: Boolean - true if expectation passes - - **evidence**: Specific quote or description supporting the verdict -- **summary**: Aggregate statistics - - **passed**: Count of passed expectations - - **failed**: Count of failed expectations - - **total**: Total expectations evaluated - - **pass_rate**: Fraction passed (0.0 to 1.0) -- **execution_metrics**: Copied from executor's metrics.json (if available) - - **output_chars**: Total character count of output files (proxy for tokens) - - **transcript_chars**: Character count of transcript -- **timing**: Wall clock timing from timing.json (if available) - - **executor_duration_seconds**: Time spent in executor subagent - - **total_duration_seconds**: Total elapsed time for the run -- **claims**: Extracted and verified claims from the output - - **claim**: The statement being verified - - **type**: "factual", "process", or "quality" - - **verified**: Boolean - whether the claim holds - - **evidence**: Supporting or contradicting evidence -- **user_notes_summary**: Issues flagged by the executor - - **uncertainties**: Things the executor wasn't sure about - - **needs_review**: Items requiring human attention - - **workarounds**: Places where the skill didn't work as expected -- **eval_feedback**: Improvement suggestions for the evals (only when warranted) - - **suggestions**: List of concrete suggestions, each with a `reason` and optionally an `assertion` it relates to - - **overall**: Brief assessment — can be "No suggestions, evals look solid" if nothing to flag - -## Guidelines - -- **Be objective**: Base verdicts on evidence, not assumptions -- **Be specific**: Quote the exact text that supports your verdict -- **Be thorough**: Check both transcript and output files -- **Be consistent**: Apply the same standard to each expectation -- **Explain failures**: Make it clear why evidence was insufficient -- **No partial credit**: Each expectation is pass or fail, not partial diff --git a/external/anthropic/skills/skill-creator/assets/eval_review.html b/external/anthropic/skills/skill-creator/assets/eval_review.html deleted file mode 100644 index 938ff32..0000000 --- a/external/anthropic/skills/skill-creator/assets/eval_review.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - Eval Set Review - __SKILL_NAME_PLACEHOLDER__ - - - - - - -

Eval Set Review: __SKILL_NAME_PLACEHOLDER__

-

Current description: __SKILL_DESCRIPTION_PLACEHOLDER__

- -
- - -
- - - - - - - - - - -
QueryShould TriggerActions
- -

- - - - diff --git a/external/anthropic/skills/skill-creator/eval-viewer/generate_review.py b/external/anthropic/skills/skill-creator/eval-viewer/generate_review.py deleted file mode 100644 index 7fa5978..0000000 --- a/external/anthropic/skills/skill-creator/eval-viewer/generate_review.py +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/env python3 -"""Generate and serve a review page for eval results. - -Reads the workspace directory, discovers runs (directories with outputs/), -embeds all output data into a self-contained HTML page, and serves it via -a tiny HTTP server. Feedback auto-saves to feedback.json in the workspace. - -Usage: - python generate_review.py [--port PORT] [--skill-name NAME] - python generate_review.py --previous-feedback /path/to/old/feedback.json - -No dependencies beyond the Python stdlib are required. -""" - -import argparse -import base64 -import json -import mimetypes -import os -import re -import signal -import subprocess -import sys -import time -import webbrowser -from functools import partial -from http.server import HTTPServer, BaseHTTPRequestHandler -from pathlib import Path - -# Files to exclude from output listings -METADATA_FILES = {"transcript.md", "user_notes.md", "metrics.json"} - -# Extensions we render as inline text -TEXT_EXTENSIONS = { - ".txt", ".md", ".json", ".csv", ".py", ".js", ".ts", ".tsx", ".jsx", - ".yaml", ".yml", ".xml", ".html", ".css", ".sh", ".rb", ".go", ".rs", - ".java", ".c", ".cpp", ".h", ".hpp", ".sql", ".r", ".toml", -} - -# Extensions we render as inline images -IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"} - -# MIME type overrides for common types -MIME_OVERRIDES = { - ".svg": "image/svg+xml", - ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", -} - - -def get_mime_type(path: Path) -> str: - ext = path.suffix.lower() - if ext in MIME_OVERRIDES: - return MIME_OVERRIDES[ext] - mime, _ = mimetypes.guess_type(str(path)) - return mime or "application/octet-stream" - - -def find_runs(workspace: Path) -> list[dict]: - """Recursively find directories that contain an outputs/ subdirectory.""" - runs: list[dict] = [] - _find_runs_recursive(workspace, workspace, runs) - runs.sort(key=lambda r: (r.get("eval_id", float("inf")), r["id"])) - return runs - - -def _find_runs_recursive(root: Path, current: Path, runs: list[dict]) -> None: - if not current.is_dir(): - return - - outputs_dir = current / "outputs" - if outputs_dir.is_dir(): - run = build_run(root, current) - if run: - runs.append(run) - return - - skip = {"node_modules", ".git", "__pycache__", "skill", "inputs"} - for child in sorted(current.iterdir()): - if child.is_dir() and child.name not in skip: - _find_runs_recursive(root, child, runs) - - -def build_run(root: Path, run_dir: Path) -> dict | None: - """Build a run dict with prompt, outputs, and grading data.""" - prompt = "" - eval_id = None - - # Try eval_metadata.json - for candidate in [run_dir / "eval_metadata.json", run_dir.parent / "eval_metadata.json"]: - if candidate.exists(): - try: - metadata = json.loads(candidate.read_text()) - prompt = metadata.get("prompt", "") - eval_id = metadata.get("eval_id") - except (json.JSONDecodeError, OSError): - pass - if prompt: - break - - # Fall back to transcript.md - if not prompt: - for candidate in [run_dir / "transcript.md", run_dir / "outputs" / "transcript.md"]: - if candidate.exists(): - try: - text = candidate.read_text() - match = re.search(r"## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)", text) - if match: - prompt = match.group(1).strip() - except OSError: - pass - if prompt: - break - - if not prompt: - prompt = "(No prompt found)" - - run_id = str(run_dir.relative_to(root)).replace("/", "-").replace("\\", "-") - - # Collect output files - outputs_dir = run_dir / "outputs" - output_files: list[dict] = [] - if outputs_dir.is_dir(): - for f in sorted(outputs_dir.iterdir()): - if f.is_file() and f.name not in METADATA_FILES: - output_files.append(embed_file(f)) - - # Load grading if present - grading = None - for candidate in [run_dir / "grading.json", run_dir.parent / "grading.json"]: - if candidate.exists(): - try: - grading = json.loads(candidate.read_text()) - except (json.JSONDecodeError, OSError): - pass - if grading: - break - - return { - "id": run_id, - "prompt": prompt, - "eval_id": eval_id, - "outputs": output_files, - "grading": grading, - } - - -def embed_file(path: Path) -> dict: - """Read a file and return an embedded representation.""" - ext = path.suffix.lower() - mime = get_mime_type(path) - - if ext in TEXT_EXTENSIONS: - try: - content = path.read_text(errors="replace") - except OSError: - content = "(Error reading file)" - return { - "name": path.name, - "type": "text", - "content": content, - } - elif ext in IMAGE_EXTENSIONS: - try: - raw = path.read_bytes() - b64 = base64.b64encode(raw).decode("ascii") - except OSError: - return {"name": path.name, "type": "error", "content": "(Error reading file)"} - return { - "name": path.name, - "type": "image", - "mime": mime, - "data_uri": f"data:{mime};base64,{b64}", - } - elif ext == ".pdf": - try: - raw = path.read_bytes() - b64 = base64.b64encode(raw).decode("ascii") - except OSError: - return {"name": path.name, "type": "error", "content": "(Error reading file)"} - return { - "name": path.name, - "type": "pdf", - "data_uri": f"data:{mime};base64,{b64}", - } - elif ext == ".xlsx": - try: - raw = path.read_bytes() - b64 = base64.b64encode(raw).decode("ascii") - except OSError: - return {"name": path.name, "type": "error", "content": "(Error reading file)"} - return { - "name": path.name, - "type": "xlsx", - "data_b64": b64, - } - else: - # Binary / unknown — base64 download link - try: - raw = path.read_bytes() - b64 = base64.b64encode(raw).decode("ascii") - except OSError: - return {"name": path.name, "type": "error", "content": "(Error reading file)"} - return { - "name": path.name, - "type": "binary", - "mime": mime, - "data_uri": f"data:{mime};base64,{b64}", - } - - -def load_previous_iteration(workspace: Path) -> dict[str, dict]: - """Load previous iteration's feedback and outputs. - - Returns a map of run_id -> {"feedback": str, "outputs": list[dict]}. - """ - result: dict[str, dict] = {} - - # Load feedback - feedback_map: dict[str, str] = {} - feedback_path = workspace / "feedback.json" - if feedback_path.exists(): - try: - data = json.loads(feedback_path.read_text()) - feedback_map = { - r["run_id"]: r["feedback"] - for r in data.get("reviews", []) - if r.get("feedback", "").strip() - } - except (json.JSONDecodeError, OSError, KeyError): - pass - - # Load runs (to get outputs) - prev_runs = find_runs(workspace) - for run in prev_runs: - result[run["id"]] = { - "feedback": feedback_map.get(run["id"], ""), - "outputs": run.get("outputs", []), - } - - # Also add feedback for run_ids that had feedback but no matching run - for run_id, fb in feedback_map.items(): - if run_id not in result: - result[run_id] = {"feedback": fb, "outputs": []} - - return result - - -def generate_html( - runs: list[dict], - skill_name: str, - previous: dict[str, dict] | None = None, - benchmark: dict | None = None, -) -> str: - """Generate the complete standalone HTML page with embedded data.""" - template_path = Path(__file__).parent / "viewer.html" - template = template_path.read_text() - - # Build previous_feedback and previous_outputs maps for the template - previous_feedback: dict[str, str] = {} - previous_outputs: dict[str, list[dict]] = {} - if previous: - for run_id, data in previous.items(): - if data.get("feedback"): - previous_feedback[run_id] = data["feedback"] - if data.get("outputs"): - previous_outputs[run_id] = data["outputs"] - - embedded = { - "skill_name": skill_name, - "runs": runs, - "previous_feedback": previous_feedback, - "previous_outputs": previous_outputs, - } - if benchmark: - embedded["benchmark"] = benchmark - - data_json = json.dumps(embedded) - - return template.replace("/*__EMBEDDED_DATA__*/", f"const EMBEDDED_DATA = {data_json};") - - -# --------------------------------------------------------------------------- -# HTTP server (stdlib only, zero dependencies) -# --------------------------------------------------------------------------- - -def _kill_port(port: int) -> None: - """Kill any process listening on the given port.""" - try: - result = subprocess.run( - ["lsof", "-ti", f":{port}"], - capture_output=True, text=True, timeout=5, - ) - for pid_str in result.stdout.strip().split("\n"): - if pid_str.strip(): - try: - os.kill(int(pid_str.strip()), signal.SIGTERM) - except (ProcessLookupError, ValueError): - pass - if result.stdout.strip(): - time.sleep(0.5) - except subprocess.TimeoutExpired: - pass - except FileNotFoundError: - print("Note: lsof not found, cannot check if port is in use", file=sys.stderr) - -class ReviewHandler(BaseHTTPRequestHandler): - """Serves the review HTML and handles feedback saves. - - Regenerates the HTML on each page load so that refreshing the browser - picks up new eval outputs without restarting the server. - """ - - def __init__( - self, - workspace: Path, - skill_name: str, - feedback_path: Path, - previous: dict[str, dict], - benchmark_path: Path | None, - *args, - **kwargs, - ): - self.workspace = workspace - self.skill_name = skill_name - self.feedback_path = feedback_path - self.previous = previous - self.benchmark_path = benchmark_path - super().__init__(*args, **kwargs) - - def do_GET(self) -> None: - if self.path == "/" or self.path == "/index.html": - # Regenerate HTML on each request (re-scans workspace for new outputs) - runs = find_runs(self.workspace) - benchmark = None - if self.benchmark_path and self.benchmark_path.exists(): - try: - benchmark = json.loads(self.benchmark_path.read_text()) - except (json.JSONDecodeError, OSError): - pass - html = generate_html(runs, self.skill_name, self.previous, benchmark) - content = html.encode("utf-8") - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.send_header("Content-Length", str(len(content))) - self.end_headers() - self.wfile.write(content) - elif self.path == "/api/feedback": - data = b"{}" - if self.feedback_path.exists(): - data = self.feedback_path.read_bytes() - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(data))) - self.end_headers() - self.wfile.write(data) - else: - self.send_error(404) - - def do_POST(self) -> None: - if self.path == "/api/feedback": - length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(length) - try: - data = json.loads(body) - if not isinstance(data, dict) or "reviews" not in data: - raise ValueError("Expected JSON object with 'reviews' key") - self.feedback_path.write_text(json.dumps(data, indent=2) + "\n") - resp = b'{"ok":true}' - self.send_response(200) - except (json.JSONDecodeError, OSError, ValueError) as e: - resp = json.dumps({"error": str(e)}).encode() - self.send_response(500) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(resp))) - self.end_headers() - self.wfile.write(resp) - else: - self.send_error(404) - - def log_message(self, format: str, *args: object) -> None: - # Suppress request logging to keep terminal clean - pass - - -def main() -> None: - parser = argparse.ArgumentParser(description="Generate and serve eval review") - parser.add_argument("workspace", type=Path, help="Path to workspace directory") - parser.add_argument("--port", "-p", type=int, default=3117, help="Server port (default: 3117)") - parser.add_argument("--skill-name", "-n", type=str, default=None, help="Skill name for header") - parser.add_argument( - "--previous-workspace", type=Path, default=None, - help="Path to previous iteration's workspace (shows old outputs and feedback as context)", - ) - parser.add_argument( - "--benchmark", type=Path, default=None, - help="Path to benchmark.json to show in the Benchmark tab", - ) - parser.add_argument( - "--static", "-s", type=Path, default=None, - help="Write standalone HTML to this path instead of starting a server", - ) - args = parser.parse_args() - - workspace = args.workspace.resolve() - if not workspace.is_dir(): - print(f"Error: {workspace} is not a directory", file=sys.stderr) - sys.exit(1) - - runs = find_runs(workspace) - if not runs: - print(f"No runs found in {workspace}", file=sys.stderr) - sys.exit(1) - - skill_name = args.skill_name or workspace.name.replace("-workspace", "") - feedback_path = workspace / "feedback.json" - - previous: dict[str, dict] = {} - if args.previous_workspace: - previous = load_previous_iteration(args.previous_workspace.resolve()) - - benchmark_path = args.benchmark.resolve() if args.benchmark else None - benchmark = None - if benchmark_path and benchmark_path.exists(): - try: - benchmark = json.loads(benchmark_path.read_text()) - except (json.JSONDecodeError, OSError): - pass - - if args.static: - html = generate_html(runs, skill_name, previous, benchmark) - args.static.parent.mkdir(parents=True, exist_ok=True) - args.static.write_text(html) - print(f"\n Static viewer written to: {args.static}\n") - sys.exit(0) - - # Kill any existing process on the target port - port = args.port - _kill_port(port) - handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path) - try: - server = HTTPServer(("127.0.0.1", port), handler) - except OSError: - # Port still in use after kill attempt — find a free one - server = HTTPServer(("127.0.0.1", 0), handler) - port = server.server_address[1] - - url = f"http://localhost:{port}" - print(f"\n Eval Viewer") - print(f" ─────────────────────────────────") - print(f" URL: {url}") - print(f" Workspace: {workspace}") - print(f" Feedback: {feedback_path}") - if previous: - print(f" Previous: {args.previous_workspace} ({len(previous)} runs)") - if benchmark_path: - print(f" Benchmark: {benchmark_path}") - print(f"\n Press Ctrl+C to stop.\n") - - webbrowser.open(url) - - try: - server.serve_forever() - except KeyboardInterrupt: - print("\nStopped.") - server.server_close() - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/eval-viewer/viewer.html b/external/anthropic/skills/skill-creator/eval-viewer/viewer.html deleted file mode 100644 index 6d8e963..0000000 --- a/external/anthropic/skills/skill-creator/eval-viewer/viewer.html +++ /dev/null @@ -1,1325 +0,0 @@ - - - - - - Eval Review - - - - - - - -
-
-
-

Eval Review:

-
Review each output and leave feedback below. Navigate with arrow keys or buttons. When done, copy feedback and paste into Claude Code.
-
-
-
- - - - - -
-
- -
-
Prompt
-
-
-
-
- - -
-
Output
-
-
No output files found
-
-
- - - - - - - - -
-
Your Feedback
-
- - - -
-
-
- - -
- - -
-
-
No benchmark data available. Run a benchmark to see quantitative results here.
-
-
-
- - -
-
-

Review Complete

-

Your feedback has been saved. Go back to your Claude Code session and tell Claude you're done reviewing.

-
- -
-
-
- - -
- - - - diff --git a/external/anthropic/skills/skill-creator/references/schemas.md b/external/anthropic/skills/skill-creator/references/schemas.md deleted file mode 100644 index b6eeaa2..0000000 --- a/external/anthropic/skills/skill-creator/references/schemas.md +++ /dev/null @@ -1,430 +0,0 @@ -# JSON Schemas - -This document defines the JSON schemas used by skill-creator. - ---- - -## evals.json - -Defines the evals for a skill. Located at `evals/evals.json` within the skill directory. - -```json -{ - "skill_name": "example-skill", - "evals": [ - { - "id": 1, - "prompt": "User's example prompt", - "expected_output": "Description of expected result", - "files": ["evals/files/sample1.pdf"], - "expectations": [ - "The output includes X", - "The skill used script Y" - ] - } - ] -} -``` - -**Fields:** -- `skill_name`: Name matching the skill's frontmatter -- `evals[].id`: Unique integer identifier -- `evals[].prompt`: The task to execute -- `evals[].expected_output`: Human-readable description of success -- `evals[].files`: Optional list of input file paths (relative to skill root) -- `evals[].expectations`: List of verifiable statements - ---- - -## history.json - -Tracks version progression in Improve mode. Located at workspace root. - -```json -{ - "started_at": "2026-01-15T10:30:00Z", - "skill_name": "pdf", - "current_best": "v2", - "iterations": [ - { - "version": "v0", - "parent": null, - "expectation_pass_rate": 0.65, - "grading_result": "baseline", - "is_current_best": false - }, - { - "version": "v1", - "parent": "v0", - "expectation_pass_rate": 0.75, - "grading_result": "won", - "is_current_best": false - }, - { - "version": "v2", - "parent": "v1", - "expectation_pass_rate": 0.85, - "grading_result": "won", - "is_current_best": true - } - ] -} -``` - -**Fields:** -- `started_at`: ISO timestamp of when improvement started -- `skill_name`: Name of the skill being improved -- `current_best`: Version identifier of the best performer -- `iterations[].version`: Version identifier (v0, v1, ...) -- `iterations[].parent`: Parent version this was derived from -- `iterations[].expectation_pass_rate`: Pass rate from grading -- `iterations[].grading_result`: "baseline", "won", "lost", or "tie" -- `iterations[].is_current_best`: Whether this is the current best version - ---- - -## grading.json - -Output from the grader agent. Located at `/grading.json`. - -```json -{ - "expectations": [ - { - "text": "The output includes the name 'John Smith'", - "passed": true, - "evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'" - }, - { - "text": "The spreadsheet has a SUM formula in cell B10", - "passed": false, - "evidence": "No spreadsheet was created. The output was a text file." - } - ], - "summary": { - "passed": 2, - "failed": 1, - "total": 3, - "pass_rate": 0.67 - }, - "execution_metrics": { - "tool_calls": { - "Read": 5, - "Write": 2, - "Bash": 8 - }, - "total_tool_calls": 15, - "total_steps": 6, - "errors_encountered": 0, - "output_chars": 12450, - "transcript_chars": 3200 - }, - "timing": { - "executor_duration_seconds": 165.0, - "grader_duration_seconds": 26.0, - "total_duration_seconds": 191.0 - }, - "claims": [ - { - "claim": "The form has 12 fillable fields", - "type": "factual", - "verified": true, - "evidence": "Counted 12 fields in field_info.json" - } - ], - "user_notes_summary": { - "uncertainties": ["Used 2023 data, may be stale"], - "needs_review": [], - "workarounds": ["Fell back to text overlay for non-fillable fields"] - }, - "eval_feedback": { - "suggestions": [ - { - "assertion": "The output includes the name 'John Smith'", - "reason": "A hallucinated document that mentions the name would also pass" - } - ], - "overall": "Assertions check presence but not correctness." - } -} -``` - -**Fields:** -- `expectations[]`: Graded expectations with evidence -- `summary`: Aggregate pass/fail counts -- `execution_metrics`: Tool usage and output size (from executor's metrics.json) -- `timing`: Wall clock timing (from timing.json) -- `claims`: Extracted and verified claims from the output -- `user_notes_summary`: Issues flagged by the executor -- `eval_feedback`: (optional) Improvement suggestions for the evals, only present when the grader identifies issues worth raising - ---- - -## metrics.json - -Output from the executor agent. Located at `/outputs/metrics.json`. - -```json -{ - "tool_calls": { - "Read": 5, - "Write": 2, - "Bash": 8, - "Edit": 1, - "Glob": 2, - "Grep": 0 - }, - "total_tool_calls": 18, - "total_steps": 6, - "files_created": ["filled_form.pdf", "field_values.json"], - "errors_encountered": 0, - "output_chars": 12450, - "transcript_chars": 3200 -} -``` - -**Fields:** -- `tool_calls`: Count per tool type -- `total_tool_calls`: Sum of all tool calls -- `total_steps`: Number of major execution steps -- `files_created`: List of output files created -- `errors_encountered`: Number of errors during execution -- `output_chars`: Total character count of output files -- `transcript_chars`: Character count of transcript - ---- - -## timing.json - -Wall clock timing for a run. Located at `/timing.json`. - -**How to capture:** When a subagent task completes, the task notification includes `total_tokens` and `duration_ms`. Save these immediately — they are not persisted anywhere else and cannot be recovered after the fact. - -```json -{ - "total_tokens": 84852, - "duration_ms": 23332, - "total_duration_seconds": 23.3, - "executor_start": "2026-01-15T10:30:00Z", - "executor_end": "2026-01-15T10:32:45Z", - "executor_duration_seconds": 165.0, - "grader_start": "2026-01-15T10:32:46Z", - "grader_end": "2026-01-15T10:33:12Z", - "grader_duration_seconds": 26.0 -} -``` - ---- - -## benchmark.json - -Output from Benchmark mode. Located at `benchmarks//benchmark.json`. - -```json -{ - "metadata": { - "skill_name": "pdf", - "skill_path": "/path/to/pdf", - "executor_model": "claude-sonnet-4-20250514", - "analyzer_model": "most-capable-model", - "timestamp": "2026-01-15T10:30:00Z", - "evals_run": [1, 2, 3], - "runs_per_configuration": 3 - }, - - "runs": [ - { - "eval_id": 1, - "eval_name": "Ocean", - "configuration": "with_skill", - "run_number": 1, - "result": { - "pass_rate": 0.85, - "passed": 6, - "failed": 1, - "total": 7, - "time_seconds": 42.5, - "tokens": 3800, - "tool_calls": 18, - "errors": 0 - }, - "expectations": [ - {"text": "...", "passed": true, "evidence": "..."} - ], - "notes": [ - "Used 2023 data, may be stale", - "Fell back to text overlay for non-fillable fields" - ] - } - ], - - "run_summary": { - "with_skill": { - "pass_rate": {"mean": 0.85, "stddev": 0.05, "min": 0.80, "max": 0.90}, - "time_seconds": {"mean": 45.0, "stddev": 12.0, "min": 32.0, "max": 58.0}, - "tokens": {"mean": 3800, "stddev": 400, "min": 3200, "max": 4100} - }, - "without_skill": { - "pass_rate": {"mean": 0.35, "stddev": 0.08, "min": 0.28, "max": 0.45}, - "time_seconds": {"mean": 32.0, "stddev": 8.0, "min": 24.0, "max": 42.0}, - "tokens": {"mean": 2100, "stddev": 300, "min": 1800, "max": 2500} - }, - "delta": { - "pass_rate": "+0.50", - "time_seconds": "+13.0", - "tokens": "+1700" - } - }, - - "notes": [ - "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value", - "Eval 3 shows high variance (50% ± 40%) - may be flaky or model-dependent", - "Without-skill runs consistently fail on table extraction expectations", - "Skill adds 13s average execution time but improves pass rate by 50%" - ] -} -``` - -**Fields:** -- `metadata`: Information about the benchmark run - - `skill_name`: Name of the skill - - `timestamp`: When the benchmark was run - - `evals_run`: List of eval names or IDs - - `runs_per_configuration`: Number of runs per config (e.g. 3) -- `runs[]`: Individual run results - - `eval_id`: Numeric eval identifier - - `eval_name`: Human-readable eval name (used as section header in the viewer) - - `configuration`: Must be `"with_skill"` or `"without_skill"` (the viewer uses this exact string for grouping and color coding) - - `run_number`: Integer run number (1, 2, 3...) - - `result`: Nested object with `pass_rate`, `passed`, `total`, `time_seconds`, `tokens`, `errors` -- `run_summary`: Statistical aggregates per configuration - - `with_skill` / `without_skill`: Each contains `pass_rate`, `time_seconds`, `tokens` objects with `mean` and `stddev` fields - - `delta`: Difference strings like `"+0.50"`, `"+13.0"`, `"+1700"` -- `notes`: Freeform observations from the analyzer - -**Important:** The viewer reads these field names exactly. Using `config` instead of `configuration`, or putting `pass_rate` at the top level of a run instead of nested under `result`, will cause the viewer to show empty/zero values. Always reference this schema when generating benchmark.json manually. - ---- - -## comparison.json - -Output from blind comparator. Located at `/comparison-N.json`. - -```json -{ - "winner": "A", - "reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.", - "rubric": { - "A": { - "content": { - "correctness": 5, - "completeness": 5, - "accuracy": 4 - }, - "structure": { - "organization": 4, - "formatting": 5, - "usability": 4 - }, - "content_score": 4.7, - "structure_score": 4.3, - "overall_score": 9.0 - }, - "B": { - "content": { - "correctness": 3, - "completeness": 2, - "accuracy": 3 - }, - "structure": { - "organization": 3, - "formatting": 2, - "usability": 3 - }, - "content_score": 2.7, - "structure_score": 2.7, - "overall_score": 5.4 - } - }, - "output_quality": { - "A": { - "score": 9, - "strengths": ["Complete solution", "Well-formatted", "All fields present"], - "weaknesses": ["Minor style inconsistency in header"] - }, - "B": { - "score": 5, - "strengths": ["Readable output", "Correct basic structure"], - "weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"] - } - }, - "expectation_results": { - "A": { - "passed": 4, - "total": 5, - "pass_rate": 0.80, - "details": [ - {"text": "Output includes name", "passed": true} - ] - }, - "B": { - "passed": 3, - "total": 5, - "pass_rate": 0.60, - "details": [ - {"text": "Output includes name", "passed": true} - ] - } - } -} -``` - ---- - -## analysis.json - -Output from post-hoc analyzer. Located at `/analysis.json`. - -```json -{ - "comparison_summary": { - "winner": "A", - "winner_skill": "path/to/winner/skill", - "loser_skill": "path/to/loser/skill", - "comparator_reasoning": "Brief summary of why comparator chose winner" - }, - "winner_strengths": [ - "Clear step-by-step instructions for handling multi-page documents", - "Included validation script that caught formatting errors" - ], - "loser_weaknesses": [ - "Vague instruction 'process the document appropriately' led to inconsistent behavior", - "No script for validation, agent had to improvise" - ], - "instruction_following": { - "winner": { - "score": 9, - "issues": ["Minor: skipped optional logging step"] - }, - "loser": { - "score": 6, - "issues": [ - "Did not use the skill's formatting template", - "Invented own approach instead of following step 3" - ] - } - }, - "improvement_suggestions": [ - { - "priority": "high", - "category": "instructions", - "suggestion": "Replace 'process the document appropriately' with explicit steps", - "expected_impact": "Would eliminate ambiguity that caused inconsistent behavior" - } - ], - "transcript_insights": { - "winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script", - "loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods" - } -} -``` diff --git a/external/anthropic/skills/skill-creator/scripts/__init__.py b/external/anthropic/skills/skill-creator/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/external/anthropic/skills/skill-creator/scripts/aggregate_benchmark.py b/external/anthropic/skills/skill-creator/scripts/aggregate_benchmark.py deleted file mode 100755 index 3e66e8c..0000000 --- a/external/anthropic/skills/skill-creator/scripts/aggregate_benchmark.py +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/env python3 -""" -Aggregate individual run results into benchmark summary statistics. - -Reads grading.json files from run directories and produces: -- run_summary with mean, stddev, min, max for each metric -- delta between with_skill and without_skill configurations - -Usage: - python aggregate_benchmark.py - -Example: - python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/ - -The script supports two directory layouts: - - Workspace layout (from skill-creator iterations): - / - └── eval-N/ - ├── with_skill/ - │ ├── run-1/grading.json - │ └── run-2/grading.json - └── without_skill/ - ├── run-1/grading.json - └── run-2/grading.json - - Legacy layout (with runs/ subdirectory): - / - └── runs/ - └── eval-N/ - ├── with_skill/ - │ └── run-1/grading.json - └── without_skill/ - └── run-1/grading.json -""" - -import argparse -import json -import math -import sys -from datetime import datetime, timezone -from pathlib import Path - - -def calculate_stats(values: list[float]) -> dict: - """Calculate mean, stddev, min, max for a list of values.""" - if not values: - return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0} - - n = len(values) - mean = sum(values) / n - - if n > 1: - variance = sum((x - mean) ** 2 for x in values) / (n - 1) - stddev = math.sqrt(variance) - else: - stddev = 0.0 - - return { - "mean": round(mean, 4), - "stddev": round(stddev, 4), - "min": round(min(values), 4), - "max": round(max(values), 4) - } - - -def load_run_results(benchmark_dir: Path) -> dict: - """ - Load all run results from a benchmark directory. - - Returns dict keyed by config name (e.g. "with_skill"/"without_skill", - or "new_skill"/"old_skill"), each containing a list of run results. - """ - # Support both layouts: eval dirs directly under benchmark_dir, or under runs/ - runs_dir = benchmark_dir / "runs" - if runs_dir.exists(): - search_dir = runs_dir - elif list(benchmark_dir.glob("eval-*")): - search_dir = benchmark_dir - else: - print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}") - return {} - - results: dict[str, list] = {} - - for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))): - metadata_path = eval_dir / "eval_metadata.json" - if metadata_path.exists(): - try: - with open(metadata_path) as mf: - eval_id = json.load(mf).get("eval_id", eval_idx) - except (json.JSONDecodeError, OSError): - eval_id = eval_idx - else: - try: - eval_id = int(eval_dir.name.split("-")[1]) - except ValueError: - eval_id = eval_idx - - # Discover config directories dynamically rather than hardcoding names - for config_dir in sorted(eval_dir.iterdir()): - if not config_dir.is_dir(): - continue - # Skip non-config directories (inputs, outputs, etc.) - if not list(config_dir.glob("run-*")): - continue - config = config_dir.name - if config not in results: - results[config] = [] - - for run_dir in sorted(config_dir.glob("run-*")): - run_number = int(run_dir.name.split("-")[1]) - grading_file = run_dir / "grading.json" - - if not grading_file.exists(): - print(f"Warning: grading.json not found in {run_dir}") - continue - - try: - with open(grading_file) as f: - grading = json.load(f) - except json.JSONDecodeError as e: - print(f"Warning: Invalid JSON in {grading_file}: {e}") - continue - - # Extract metrics - result = { - "eval_id": eval_id, - "run_number": run_number, - "pass_rate": grading.get("summary", {}).get("pass_rate", 0.0), - "passed": grading.get("summary", {}).get("passed", 0), - "failed": grading.get("summary", {}).get("failed", 0), - "total": grading.get("summary", {}).get("total", 0), - } - - # Extract timing — check grading.json first, then sibling timing.json - timing = grading.get("timing", {}) - result["time_seconds"] = timing.get("total_duration_seconds", 0.0) - timing_file = run_dir / "timing.json" - if result["time_seconds"] == 0.0 and timing_file.exists(): - try: - with open(timing_file) as tf: - timing_data = json.load(tf) - result["time_seconds"] = timing_data.get("total_duration_seconds", 0.0) - result["tokens"] = timing_data.get("total_tokens", 0) - except json.JSONDecodeError: - pass - - # Extract metrics if available - metrics = grading.get("execution_metrics", {}) - result["tool_calls"] = metrics.get("total_tool_calls", 0) - if not result.get("tokens"): - result["tokens"] = metrics.get("output_chars", 0) - result["errors"] = metrics.get("errors_encountered", 0) - - # Extract expectations — viewer requires fields: text, passed, evidence - raw_expectations = grading.get("expectations", []) - for exp in raw_expectations: - if "text" not in exp or "passed" not in exp: - print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}") - result["expectations"] = raw_expectations - - # Extract notes from user_notes_summary - notes_summary = grading.get("user_notes_summary", {}) - notes = [] - notes.extend(notes_summary.get("uncertainties", [])) - notes.extend(notes_summary.get("needs_review", [])) - notes.extend(notes_summary.get("workarounds", [])) - result["notes"] = notes - - results[config].append(result) - - return results - - -def aggregate_results(results: dict) -> dict: - """ - Aggregate run results into summary statistics. - - Returns run_summary with stats for each configuration and delta. - """ - run_summary = {} - configs = list(results.keys()) - - for config in configs: - runs = results.get(config, []) - - if not runs: - run_summary[config] = { - "pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}, - "time_seconds": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}, - "tokens": {"mean": 0, "stddev": 0, "min": 0, "max": 0} - } - continue - - pass_rates = [r["pass_rate"] for r in runs] - times = [r["time_seconds"] for r in runs] - tokens = [r.get("tokens", 0) for r in runs] - - run_summary[config] = { - "pass_rate": calculate_stats(pass_rates), - "time_seconds": calculate_stats(times), - "tokens": calculate_stats(tokens) - } - - # Calculate delta between the first two configs (if two exist) - if len(configs) >= 2: - primary = run_summary.get(configs[0], {}) - baseline = run_summary.get(configs[1], {}) - else: - primary = run_summary.get(configs[0], {}) if configs else {} - baseline = {} - - delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0) - delta_time = primary.get("time_seconds", {}).get("mean", 0) - baseline.get("time_seconds", {}).get("mean", 0) - delta_tokens = primary.get("tokens", {}).get("mean", 0) - baseline.get("tokens", {}).get("mean", 0) - - run_summary["delta"] = { - "pass_rate": f"{delta_pass_rate:+.2f}", - "time_seconds": f"{delta_time:+.1f}", - "tokens": f"{delta_tokens:+.0f}" - } - - return run_summary - - -def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict: - """ - Generate complete benchmark.json from run results. - """ - results = load_run_results(benchmark_dir) - run_summary = aggregate_results(results) - - # Build runs array for benchmark.json - runs = [] - for config in results: - for result in results[config]: - runs.append({ - "eval_id": result["eval_id"], - "configuration": config, - "run_number": result["run_number"], - "result": { - "pass_rate": result["pass_rate"], - "passed": result["passed"], - "failed": result["failed"], - "total": result["total"], - "time_seconds": result["time_seconds"], - "tokens": result.get("tokens", 0), - "tool_calls": result.get("tool_calls", 0), - "errors": result.get("errors", 0) - }, - "expectations": result["expectations"], - "notes": result["notes"] - }) - - # Determine eval IDs from results - eval_ids = sorted(set( - r["eval_id"] - for config in results.values() - for r in config - )) - - benchmark = { - "metadata": { - "skill_name": skill_name or "", - "skill_path": skill_path or "", - "executor_model": "", - "analyzer_model": "", - "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), - "evals_run": eval_ids, - "runs_per_configuration": 3 - }, - "runs": runs, - "run_summary": run_summary, - "notes": [] # To be filled by analyzer - } - - return benchmark - - -def generate_markdown(benchmark: dict) -> str: - """Generate human-readable benchmark.md from benchmark data.""" - metadata = benchmark["metadata"] - run_summary = benchmark["run_summary"] - - # Determine config names (excluding "delta") - configs = [k for k in run_summary if k != "delta"] - config_a = configs[0] if len(configs) >= 1 else "config_a" - config_b = configs[1] if len(configs) >= 2 else "config_b" - label_a = config_a.replace("_", " ").title() - label_b = config_b.replace("_", " ").title() - - lines = [ - f"# Skill Benchmark: {metadata['skill_name']}", - "", - f"**Model**: {metadata['executor_model']}", - f"**Date**: {metadata['timestamp']}", - f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({metadata['runs_per_configuration']} runs each per configuration)", - "", - "## Summary", - "", - f"| Metric | {label_a} | {label_b} | Delta |", - "|--------|------------|---------------|-------|", - ] - - a_summary = run_summary.get(config_a, {}) - b_summary = run_summary.get(config_b, {}) - delta = run_summary.get("delta", {}) - - # Format pass rate - a_pr = a_summary.get("pass_rate", {}) - b_pr = b_summary.get("pass_rate", {}) - lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |") - - # Format time - a_time = a_summary.get("time_seconds", {}) - b_time = b_summary.get("time_seconds", {}) - lines.append(f"| Time | {a_time.get('mean', 0):.1f}s ± {a_time.get('stddev', 0):.1f}s | {b_time.get('mean', 0):.1f}s ± {b_time.get('stddev', 0):.1f}s | {delta.get('time_seconds', '—')}s |") - - # Format tokens - a_tokens = a_summary.get("tokens", {}) - b_tokens = b_summary.get("tokens", {}) - lines.append(f"| Tokens | {a_tokens.get('mean', 0):.0f} ± {a_tokens.get('stddev', 0):.0f} | {b_tokens.get('mean', 0):.0f} ± {b_tokens.get('stddev', 0):.0f} | {delta.get('tokens', '—')} |") - - # Notes section - if benchmark.get("notes"): - lines.extend([ - "", - "## Notes", - "" - ]) - for note in benchmark["notes"]: - lines.append(f"- {note}") - - return "\n".join(lines) - - -def main(): - parser = argparse.ArgumentParser( - description="Aggregate benchmark run results into summary statistics" - ) - parser.add_argument( - "benchmark_dir", - type=Path, - help="Path to the benchmark directory" - ) - parser.add_argument( - "--skill-name", - default="", - help="Name of the skill being benchmarked" - ) - parser.add_argument( - "--skill-path", - default="", - help="Path to the skill being benchmarked" - ) - parser.add_argument( - "--output", "-o", - type=Path, - help="Output path for benchmark.json (default: /benchmark.json)" - ) - - args = parser.parse_args() - - if not args.benchmark_dir.exists(): - print(f"Directory not found: {args.benchmark_dir}") - sys.exit(1) - - # Generate benchmark - benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path) - - # Determine output paths - output_json = args.output or (args.benchmark_dir / "benchmark.json") - output_md = output_json.with_suffix(".md") - - # Write benchmark.json - with open(output_json, "w") as f: - json.dump(benchmark, f, indent=2) - print(f"Generated: {output_json}") - - # Write benchmark.md - markdown = generate_markdown(benchmark) - with open(output_md, "w") as f: - f.write(markdown) - print(f"Generated: {output_md}") - - # Print summary - run_summary = benchmark["run_summary"] - configs = [k for k in run_summary if k != "delta"] - delta = run_summary.get("delta", {}) - - print(f"\nSummary:") - for config in configs: - pr = run_summary[config]["pass_rate"]["mean"] - label = config.replace("_", " ").title() - print(f" {label}: {pr*100:.1f}% pass rate") - print(f" Delta: {delta.get('pass_rate', '—')}") - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/scripts/generate_report.py b/external/anthropic/skills/skill-creator/scripts/generate_report.py deleted file mode 100755 index 959e30a..0000000 --- a/external/anthropic/skills/skill-creator/scripts/generate_report.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 -"""Generate an HTML report from run_loop.py output. - -Takes the JSON output from run_loop.py and generates a visual HTML report -showing each description attempt with check/x for each test case. -Distinguishes between train and test queries. -""" - -import argparse -import html -import json -import sys -from pathlib import Path - - -def generate_html(data: dict, auto_refresh: bool = False, skill_name: str = "") -> str: - """Generate HTML report from loop output data. If auto_refresh is True, adds a meta refresh tag.""" - history = data.get("history", []) - holdout = data.get("holdout", 0) - title_prefix = html.escape(skill_name + " \u2014 ") if skill_name else "" - - # Get all unique queries from train and test sets, with should_trigger info - train_queries: list[dict] = [] - test_queries: list[dict] = [] - if history: - for r in history[0].get("train_results", history[0].get("results", [])): - train_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)}) - if history[0].get("test_results"): - for r in history[0].get("test_results", []): - test_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)}) - - refresh_tag = ' \n' if auto_refresh else "" - - html_parts = [""" - - - -""" + refresh_tag + """ """ + title_prefix + """Skill Description Optimization - - - - - - -

""" + title_prefix + """Skill Description Optimization

-
- Optimizing your skill's description. This page updates automatically as Claude tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong. The "Train" score shows performance on queries used to improve the description; the "Test" score shows performance on held-out queries the optimizer hasn't seen. When it's done, Claude will apply the best-performing description to your skill. -
-"""] - - # Summary section - best_test_score = data.get('best_test_score') - best_train_score = data.get('best_train_score') - html_parts.append(f""" -
-

Original: {html.escape(data.get('original_description', 'N/A'))}

-

Best: {html.escape(data.get('best_description', 'N/A'))}

-

Best Score: {data.get('best_score', 'N/A')} {'(test)' if best_test_score else '(train)'}

-

Iterations: {data.get('iterations_run', 0)} | Train: {data.get('train_size', '?')} | Test: {data.get('test_size', '?')}

-
-""") - - # Legend - html_parts.append(""" -
- Query columns: - Should trigger - Should NOT trigger - Train - Test -
-""") - - # Table header - html_parts.append(""" -
- - - - - - - -""") - - # Add column headers for train queries - for qinfo in train_queries: - polarity = "positive-col" if qinfo["should_trigger"] else "negative-col" - html_parts.append(f' \n') - - # Add column headers for test queries (different color) - for qinfo in test_queries: - polarity = "positive-col" if qinfo["should_trigger"] else "negative-col" - html_parts.append(f' \n') - - html_parts.append(""" - - -""") - - # Find best iteration for highlighting - if test_queries: - best_iter = max(history, key=lambda h: h.get("test_passed") or 0).get("iteration") - else: - best_iter = max(history, key=lambda h: h.get("train_passed", h.get("passed", 0))).get("iteration") - - # Add rows for each iteration - for h in history: - iteration = h.get("iteration", "?") - train_passed = h.get("train_passed", h.get("passed", 0)) - train_total = h.get("train_total", h.get("total", 0)) - test_passed = h.get("test_passed") - test_total = h.get("test_total") - description = h.get("description", "") - train_results = h.get("train_results", h.get("results", [])) - test_results = h.get("test_results", []) - - # Create lookups for results by query - train_by_query = {r["query"]: r for r in train_results} - test_by_query = {r["query"]: r for r in test_results} if test_results else {} - - # Compute aggregate correct/total runs across all retries - def aggregate_runs(results: list[dict]) -> tuple[int, int]: - correct = 0 - total = 0 - for r in results: - runs = r.get("runs", 0) - triggers = r.get("triggers", 0) - total += runs - if r.get("should_trigger", True): - correct += triggers - else: - correct += runs - triggers - return correct, total - - train_correct, train_runs = aggregate_runs(train_results) - test_correct, test_runs = aggregate_runs(test_results) - - # Determine score classes - def score_class(correct: int, total: int) -> str: - if total > 0: - ratio = correct / total - if ratio >= 0.8: - return "score-good" - elif ratio >= 0.5: - return "score-ok" - return "score-bad" - - train_class = score_class(train_correct, train_runs) - test_class = score_class(test_correct, test_runs) - - row_class = "best-row" if iteration == best_iter else "" - - html_parts.append(f""" - - - - -""") - - # Add result for each train query - for qinfo in train_queries: - r = train_by_query.get(qinfo["query"], {}) - did_pass = r.get("pass", False) - triggers = r.get("triggers", 0) - runs = r.get("runs", 0) - - icon = "✓" if did_pass else "✗" - css_class = "pass" if did_pass else "fail" - - html_parts.append(f' \n') - - # Add result for each test query (with different background) - for qinfo in test_queries: - r = test_by_query.get(qinfo["query"], {}) - did_pass = r.get("pass", False) - triggers = r.get("triggers", 0) - runs = r.get("runs", 0) - - icon = "✓" if did_pass else "✗" - css_class = "pass" if did_pass else "fail" - - html_parts.append(f' \n') - - html_parts.append(" \n") - - html_parts.append(""" -
IterTrainTestDescription{html.escape(qinfo["query"])}{html.escape(qinfo["query"])}
{iteration}{train_correct}/{train_runs}{test_correct}/{test_runs}{html.escape(description)}{icon}{triggers}/{runs}{icon}{triggers}/{runs}
-
-""") - - html_parts.append(""" - - -""") - - return "".join(html_parts) - - -def main(): - parser = argparse.ArgumentParser(description="Generate HTML report from run_loop output") - parser.add_argument("input", help="Path to JSON output from run_loop.py (or - for stdin)") - parser.add_argument("-o", "--output", default=None, help="Output HTML file (default: stdout)") - parser.add_argument("--skill-name", default="", help="Skill name to include in the report title") - args = parser.parse_args() - - if args.input == "-": - data = json.load(sys.stdin) - else: - data = json.loads(Path(args.input).read_text()) - - html_output = generate_html(data, skill_name=args.skill_name) - - if args.output: - Path(args.output).write_text(html_output) - print(f"Report written to {args.output}", file=sys.stderr) - else: - print(html_output) - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/scripts/improve_description.py b/external/anthropic/skills/skill-creator/scripts/improve_description.py deleted file mode 100755 index 06bcec7..0000000 --- a/external/anthropic/skills/skill-creator/scripts/improve_description.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python3 -"""Improve a skill description based on eval results. - -Takes eval results (from run_eval.py) and generates an improved description -by calling `claude -p` as a subprocess (same auth pattern as run_eval.py — -uses the session's Claude Code auth, no separate ANTHROPIC_API_KEY needed). -""" - -import argparse -import json -import os -import re -import subprocess -import sys -from pathlib import Path - -from scripts.utils import parse_skill_md - - -def _call_claude(prompt: str, model: str | None, timeout: int = 300) -> str: - """Run `claude -p` with the prompt on stdin and return the text response. - - Prompt goes over stdin (not argv) because it embeds the full SKILL.md - body and can easily exceed comfortable argv length. - """ - cmd = ["claude", "-p", "--output-format", "text"] - if model: - cmd.extend(["--model", model]) - - # Remove CLAUDECODE env var to allow nesting claude -p inside a - # Claude Code session. The guard is for interactive terminal conflicts; - # programmatic subprocess usage is safe. Same pattern as run_eval.py. - env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"} - - result = subprocess.run( - cmd, - input=prompt, - capture_output=True, - text=True, - env=env, - timeout=timeout, - ) - if result.returncode != 0: - raise RuntimeError( - f"claude -p exited {result.returncode}\nstderr: {result.stderr}" - ) - return result.stdout - - -def improve_description( - skill_name: str, - skill_content: str, - current_description: str, - eval_results: dict, - history: list[dict], - model: str, - test_results: dict | None = None, - log_dir: Path | None = None, - iteration: int | None = None, -) -> str: - """Call Claude to improve the description based on eval results.""" - failed_triggers = [ - r for r in eval_results["results"] - if r["should_trigger"] and not r["pass"] - ] - false_triggers = [ - r for r in eval_results["results"] - if not r["should_trigger"] and not r["pass"] - ] - - # Build scores summary - train_score = f"{eval_results['summary']['passed']}/{eval_results['summary']['total']}" - if test_results: - test_score = f"{test_results['summary']['passed']}/{test_results['summary']['total']}" - scores_summary = f"Train: {train_score}, Test: {test_score}" - else: - scores_summary = f"Train: {train_score}" - - prompt = f"""You are optimizing a skill description for a Claude Code skill called "{skill_name}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples. - -The description appears in Claude's "available_skills" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones. - -Here's the current description: - -"{current_description}" - - -Current scores ({scores_summary}): - -""" - if failed_triggers: - prompt += "FAILED TO TRIGGER (should have triggered but didn't):\n" - for r in failed_triggers: - prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n' - prompt += "\n" - - if false_triggers: - prompt += "FALSE TRIGGERS (triggered but shouldn't have):\n" - for r in false_triggers: - prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n' - prompt += "\n" - - if history: - prompt += "PREVIOUS ATTEMPTS (do NOT repeat these — try something structurally different):\n\n" - for h in history: - train_s = f"{h.get('train_passed', h.get('passed', 0))}/{h.get('train_total', h.get('total', 0))}" - test_s = f"{h.get('test_passed', '?')}/{h.get('test_total', '?')}" if h.get('test_passed') is not None else None - score_str = f"train={train_s}" + (f", test={test_s}" if test_s else "") - prompt += f'\n' - prompt += f'Description: "{h["description"]}"\n' - if "results" in h: - prompt += "Train results:\n" - for r in h["results"]: - status = "PASS" if r["pass"] else "FAIL" - prompt += f' [{status}] "{r["query"][:80]}" (triggered {r["triggers"]}/{r["runs"]})\n' - if h.get("note"): - prompt += f'Note: {h["note"]}\n' - prompt += "\n\n" - - prompt += f""" - -Skill content (for context on what the skill does): - -{skill_content} - - -Based on the failures, write a new and improved description that is more likely to trigger correctly. When I say "based on the failures", it's a bit of a tricky line to walk because we don't want to overfit to the specific cases you're seeing. So what I DON'T want you to do is produce an ever-expanding list of specific queries that this skill should or shouldn't trigger for. Instead, try to generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The reason for this is twofold: - -1. Avoid overfitting -2. The list might get loooong and it's injected into ALL queries and there might be a lot of skills, so we don't want to blow too much space on any given description. - -Concretely, your description should not be more than about 100-200 words, even if that comes at the cost of accuracy. There is a hard limit of 1024 characters — descriptions over that will be truncated, so stay comfortably under it. - -Here are some tips that we've found to work well in writing these descriptions: -- The skill should be phrased in the imperative -- "Use this skill for" rather than "this skill does" -- The skill description should focus on the user's intent, what they are trying to achieve, vs. the implementation details of how the skill works. -- The description competes with other skills for Claude's attention — make it distinctive and immediately recognizable. -- If you're getting lots of failures after repeated attempts, change things up. Try different sentence structures or wordings. - -I'd encourage you to be creative and mix up the style in different iterations since you'll have multiple opportunities to try different approaches and we'll just grab the highest-scoring one at the end. - -Please respond with only the new description text in tags, nothing else.""" - - text = _call_claude(prompt, model) - - match = re.search(r"(.*?)", text, re.DOTALL) - description = match.group(1).strip().strip('"') if match else text.strip().strip('"') - - transcript: dict = { - "iteration": iteration, - "prompt": prompt, - "response": text, - "parsed_description": description, - "char_count": len(description), - "over_limit": len(description) > 1024, - } - - # Safety net: the prompt already states the 1024-char hard limit, but if - # the model blew past it anyway, make one fresh single-turn call that - # quotes the too-long version and asks for a shorter rewrite. (The old - # SDK path did this as a true multi-turn; `claude -p` is one-shot, so we - # inline the prior output into the new prompt instead.) - if len(description) > 1024: - shorten_prompt = ( - f"{prompt}\n\n" - f"---\n\n" - f"A previous attempt produced this description, which at " - f"{len(description)} characters is over the 1024-character hard limit:\n\n" - f'"{description}"\n\n' - f"Rewrite it to be under 1024 characters while keeping the most " - f"important trigger words and intent coverage. Respond with only " - f"the new description in tags." - ) - shorten_text = _call_claude(shorten_prompt, model) - match = re.search(r"(.*?)", shorten_text, re.DOTALL) - shortened = match.group(1).strip().strip('"') if match else shorten_text.strip().strip('"') - - transcript["rewrite_prompt"] = shorten_prompt - transcript["rewrite_response"] = shorten_text - transcript["rewrite_description"] = shortened - transcript["rewrite_char_count"] = len(shortened) - description = shortened - - transcript["final_description"] = description - - if log_dir: - log_dir.mkdir(parents=True, exist_ok=True) - log_file = log_dir / f"improve_iter_{iteration or 'unknown'}.json" - log_file.write_text(json.dumps(transcript, indent=2)) - - return description - - -def main(): - parser = argparse.ArgumentParser(description="Improve a skill description based on eval results") - parser.add_argument("--eval-results", required=True, help="Path to eval results JSON (from run_eval.py)") - parser.add_argument("--skill-path", required=True, help="Path to skill directory") - parser.add_argument("--history", default=None, help="Path to history JSON (previous attempts)") - parser.add_argument("--model", required=True, help="Model for improvement") - parser.add_argument("--verbose", action="store_true", help="Print thinking to stderr") - args = parser.parse_args() - - skill_path = Path(args.skill_path) - if not (skill_path / "SKILL.md").exists(): - print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) - sys.exit(1) - - eval_results = json.loads(Path(args.eval_results).read_text()) - history = [] - if args.history: - history = json.loads(Path(args.history).read_text()) - - name, _, content = parse_skill_md(skill_path) - current_description = eval_results["description"] - - if args.verbose: - print(f"Current: {current_description}", file=sys.stderr) - print(f"Score: {eval_results['summary']['passed']}/{eval_results['summary']['total']}", file=sys.stderr) - - new_description = improve_description( - skill_name=name, - skill_content=content, - current_description=current_description, - eval_results=eval_results, - history=history, - model=args.model, - ) - - if args.verbose: - print(f"Improved: {new_description}", file=sys.stderr) - - # Output as JSON with both the new description and updated history - output = { - "description": new_description, - "history": history + [{ - "description": current_description, - "passed": eval_results["summary"]["passed"], - "failed": eval_results["summary"]["failed"], - "total": eval_results["summary"]["total"], - "results": eval_results["results"], - }], - } - print(json.dumps(output, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/scripts/package_skill.py b/external/anthropic/skills/skill-creator/scripts/package_skill.py deleted file mode 100755 index f48eac4..0000000 --- a/external/anthropic/skills/skill-creator/scripts/package_skill.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -""" -Skill Packager - Creates a distributable .skill file of a skill folder - -Usage: - python utils/package_skill.py [output-directory] - -Example: - python utils/package_skill.py skills/public/my-skill - python utils/package_skill.py skills/public/my-skill ./dist -""" - -import fnmatch -import sys -import zipfile -from pathlib import Path -from scripts.quick_validate import validate_skill - -# Patterns to exclude when packaging skills. -EXCLUDE_DIRS = {"__pycache__", "node_modules"} -EXCLUDE_GLOBS = {"*.pyc"} -EXCLUDE_FILES = {".DS_Store"} -# Directories excluded only at the skill root (not when nested deeper). -ROOT_EXCLUDE_DIRS = {"evals"} - - -def should_exclude(rel_path: Path) -> bool: - """Check if a path should be excluded from packaging.""" - parts = rel_path.parts - if any(part in EXCLUDE_DIRS for part in parts): - return True - # rel_path is relative to skill_path.parent, so parts[0] is the skill - # folder name and parts[1] (if present) is the first subdir. - if len(parts) > 1 and parts[1] in ROOT_EXCLUDE_DIRS: - return True - name = rel_path.name - if name in EXCLUDE_FILES: - return True - return any(fnmatch.fnmatch(name, pat) for pat in EXCLUDE_GLOBS) - - -def package_skill(skill_path, output_dir=None): - """ - Package a skill folder into a .skill file. - - Args: - skill_path: Path to the skill folder - output_dir: Optional output directory for the .skill file (defaults to current directory) - - Returns: - Path to the created .skill file, or None if error - """ - skill_path = Path(skill_path).resolve() - - # Validate skill folder exists - if not skill_path.exists(): - print(f"❌ Error: Skill folder not found: {skill_path}") - return None - - if not skill_path.is_dir(): - print(f"❌ Error: Path is not a directory: {skill_path}") - return None - - # Validate SKILL.md exists - skill_md = skill_path / "SKILL.md" - if not skill_md.exists(): - print(f"❌ Error: SKILL.md not found in {skill_path}") - return None - - # Run validation before packaging - print("🔍 Validating skill...") - valid, message = validate_skill(skill_path) - if not valid: - print(f"❌ Validation failed: {message}") - print(" Please fix the validation errors before packaging.") - return None - print(f"✅ {message}\n") - - # Determine output location - skill_name = skill_path.name - if output_dir: - output_path = Path(output_dir).resolve() - output_path.mkdir(parents=True, exist_ok=True) - else: - output_path = Path.cwd() - - skill_filename = output_path / f"{skill_name}.skill" - - # Create the .skill file (zip format) - try: - with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: - # Walk through the skill directory, excluding build artifacts - for file_path in skill_path.rglob('*'): - if not file_path.is_file(): - continue - arcname = file_path.relative_to(skill_path.parent) - if should_exclude(arcname): - print(f" Skipped: {arcname}") - continue - zipf.write(file_path, arcname) - print(f" Added: {arcname}") - - print(f"\n✅ Successfully packaged skill to: {skill_filename}") - return skill_filename - - except Exception as e: - print(f"❌ Error creating .skill file: {e}") - return None - - -def main(): - if len(sys.argv) < 2: - print("Usage: python utils/package_skill.py [output-directory]") - print("\nExample:") - print(" python utils/package_skill.py skills/public/my-skill") - print(" python utils/package_skill.py skills/public/my-skill ./dist") - sys.exit(1) - - skill_path = sys.argv[1] - output_dir = sys.argv[2] if len(sys.argv) > 2 else None - - print(f"📦 Packaging skill: {skill_path}") - if output_dir: - print(f" Output directory: {output_dir}") - print() - - result = package_skill(skill_path, output_dir) - - if result: - sys.exit(0) - else: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/scripts/quick_validate.py b/external/anthropic/skills/skill-creator/scripts/quick_validate.py deleted file mode 100755 index ed8e1dd..0000000 --- a/external/anthropic/skills/skill-creator/scripts/quick_validate.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -""" -Quick validation script for skills - minimal version -""" - -import sys -import os -import re -import yaml -from pathlib import Path - -def validate_skill(skill_path): - """Basic validation of a skill""" - skill_path = Path(skill_path) - - # Check SKILL.md exists - skill_md = skill_path / 'SKILL.md' - if not skill_md.exists(): - return False, "SKILL.md not found" - - # Read and validate frontmatter - content = skill_md.read_text() - if not content.startswith('---'): - return False, "No YAML frontmatter found" - - # Extract frontmatter - match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) - if not match: - return False, "Invalid frontmatter format" - - frontmatter_text = match.group(1) - - # Parse YAML frontmatter - try: - frontmatter = yaml.safe_load(frontmatter_text) - if not isinstance(frontmatter, dict): - return False, "Frontmatter must be a YAML dictionary" - except yaml.YAMLError as e: - return False, f"Invalid YAML in frontmatter: {e}" - - # Define allowed properties - ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'} - - # Check for unexpected properties (excluding nested keys under metadata) - unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES - if unexpected_keys: - return False, ( - f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. " - f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}" - ) - - # Check required fields - if 'name' not in frontmatter: - return False, "Missing 'name' in frontmatter" - if 'description' not in frontmatter: - return False, "Missing 'description' in frontmatter" - - # Extract name for validation - name = frontmatter.get('name', '') - if not isinstance(name, str): - return False, f"Name must be a string, got {type(name).__name__}" - name = name.strip() - if name: - # Check naming convention (kebab-case: lowercase with hyphens) - if not re.match(r'^[a-z0-9-]+$', name): - return False, f"Name '{name}' should be kebab-case (lowercase letters, digits, and hyphens only)" - if name.startswith('-') or name.endswith('-') or '--' in name: - return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" - # Check name length (max 64 characters per spec) - if len(name) > 64: - return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters." - - # Extract and validate description - description = frontmatter.get('description', '') - if not isinstance(description, str): - return False, f"Description must be a string, got {type(description).__name__}" - description = description.strip() - if description: - # Check for angle brackets - if '<' in description or '>' in description: - return False, "Description cannot contain angle brackets (< or >)" - # Check description length (max 1024 characters per spec) - if len(description) > 1024: - return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters." - - # Validate compatibility field if present (optional) - compatibility = frontmatter.get('compatibility', '') - if compatibility: - if not isinstance(compatibility, str): - return False, f"Compatibility must be a string, got {type(compatibility).__name__}" - if len(compatibility) > 500: - return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters." - - return True, "Skill is valid!" - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python quick_validate.py ") - sys.exit(1) - - valid, message = validate_skill(sys.argv[1]) - print(message) - sys.exit(0 if valid else 1) \ No newline at end of file diff --git a/external/anthropic/skills/skill-creator/scripts/run_eval.py b/external/anthropic/skills/skill-creator/scripts/run_eval.py deleted file mode 100755 index e58c70b..0000000 --- a/external/anthropic/skills/skill-creator/scripts/run_eval.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -"""Run trigger evaluation for a skill description. - -Tests whether a skill's description causes Claude to trigger (read the skill) -for a set of queries. Outputs results as JSON. -""" - -import argparse -import json -import os -import select -import subprocess -import sys -import time -import uuid -from concurrent.futures import ProcessPoolExecutor, as_completed -from pathlib import Path - -from scripts.utils import parse_skill_md - - -def find_project_root() -> Path: - """Find the project root by walking up from cwd looking for .claude/. - - Mimics how Claude Code discovers its project root, so the command file - we create ends up where claude -p will look for it. - """ - current = Path.cwd() - for parent in [current, *current.parents]: - if (parent / ".claude").is_dir(): - return parent - return current - - -def run_single_query( - query: str, - skill_name: str, - skill_description: str, - timeout: int, - project_root: str, - model: str | None = None, -) -> bool: - """Run a single query and return whether the skill was triggered. - - Creates a command file in .claude/commands/ so it appears in Claude's - available_skills list, then runs `claude -p` with the raw query. - Uses --include-partial-messages to detect triggering early from - stream events (content_block_start) rather than waiting for the - full assistant message, which only arrives after tool execution. - """ - unique_id = uuid.uuid4().hex[:8] - clean_name = f"{skill_name}-skill-{unique_id}" - project_commands_dir = Path(project_root) / ".claude" / "commands" - command_file = project_commands_dir / f"{clean_name}.md" - - try: - project_commands_dir.mkdir(parents=True, exist_ok=True) - # Use YAML block scalar to avoid breaking on quotes in description - indented_desc = "\n ".join(skill_description.split("\n")) - command_content = ( - f"---\n" - f"description: |\n" - f" {indented_desc}\n" - f"---\n\n" - f"# {skill_name}\n\n" - f"This skill handles: {skill_description}\n" - ) - command_file.write_text(command_content) - - cmd = [ - "claude", - "-p", query, - "--output-format", "stream-json", - "--verbose", - "--include-partial-messages", - ] - if model: - cmd.extend(["--model", model]) - - # Remove CLAUDECODE env var to allow nesting claude -p inside a - # Claude Code session. The guard is for interactive terminal conflicts; - # programmatic subprocess usage is safe. - env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"} - - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - cwd=project_root, - env=env, - ) - - triggered = False - start_time = time.time() - buffer = "" - # Track state for stream event detection - pending_tool_name = None - accumulated_json = "" - - try: - while time.time() - start_time < timeout: - if process.poll() is not None: - remaining = process.stdout.read() - if remaining: - buffer += remaining.decode("utf-8", errors="replace") - break - - ready, _, _ = select.select([process.stdout], [], [], 1.0) - if not ready: - continue - - chunk = os.read(process.stdout.fileno(), 8192) - if not chunk: - break - buffer += chunk.decode("utf-8", errors="replace") - - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.strip() - if not line: - continue - - try: - event = json.loads(line) - except json.JSONDecodeError: - continue - - # Early detection via stream events - if event.get("type") == "stream_event": - se = event.get("event", {}) - se_type = se.get("type", "") - - if se_type == "content_block_start": - cb = se.get("content_block", {}) - if cb.get("type") == "tool_use": - tool_name = cb.get("name", "") - if tool_name in ("Skill", "Read"): - pending_tool_name = tool_name - accumulated_json = "" - else: - return False - - elif se_type == "content_block_delta" and pending_tool_name: - delta = se.get("delta", {}) - if delta.get("type") == "input_json_delta": - accumulated_json += delta.get("partial_json", "") - if clean_name in accumulated_json: - return True - - elif se_type in ("content_block_stop", "message_stop"): - if pending_tool_name: - return clean_name in accumulated_json - if se_type == "message_stop": - return False - - # Fallback: full assistant message - elif event.get("type") == "assistant": - message = event.get("message", {}) - for content_item in message.get("content", []): - if content_item.get("type") != "tool_use": - continue - tool_name = content_item.get("name", "") - tool_input = content_item.get("input", {}) - if tool_name == "Skill" and clean_name in tool_input.get("skill", ""): - triggered = True - elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""): - triggered = True - return triggered - - elif event.get("type") == "result": - return triggered - finally: - # Clean up process on any exit path (return, exception, timeout) - if process.poll() is None: - process.kill() - process.wait() - - return triggered - finally: - if command_file.exists(): - command_file.unlink() - - -def run_eval( - eval_set: list[dict], - skill_name: str, - description: str, - num_workers: int, - timeout: int, - project_root: Path, - runs_per_query: int = 1, - trigger_threshold: float = 0.5, - model: str | None = None, -) -> dict: - """Run the full eval set and return results.""" - results = [] - - with ProcessPoolExecutor(max_workers=num_workers) as executor: - future_to_info = {} - for item in eval_set: - for run_idx in range(runs_per_query): - future = executor.submit( - run_single_query, - item["query"], - skill_name, - description, - timeout, - str(project_root), - model, - ) - future_to_info[future] = (item, run_idx) - - query_triggers: dict[str, list[bool]] = {} - query_items: dict[str, dict] = {} - for future in as_completed(future_to_info): - item, _ = future_to_info[future] - query = item["query"] - query_items[query] = item - if query not in query_triggers: - query_triggers[query] = [] - try: - query_triggers[query].append(future.result()) - except Exception as e: - print(f"Warning: query failed: {e}", file=sys.stderr) - query_triggers[query].append(False) - - for query, triggers in query_triggers.items(): - item = query_items[query] - trigger_rate = sum(triggers) / len(triggers) - should_trigger = item["should_trigger"] - if should_trigger: - did_pass = trigger_rate >= trigger_threshold - else: - did_pass = trigger_rate < trigger_threshold - results.append({ - "query": query, - "should_trigger": should_trigger, - "trigger_rate": trigger_rate, - "triggers": sum(triggers), - "runs": len(triggers), - "pass": did_pass, - }) - - passed = sum(1 for r in results if r["pass"]) - total = len(results) - - return { - "skill_name": skill_name, - "description": description, - "results": results, - "summary": { - "total": total, - "passed": passed, - "failed": total - passed, - }, - } - - -def main(): - parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description") - parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file") - parser.add_argument("--skill-path", required=True, help="Path to skill directory") - parser.add_argument("--description", default=None, help="Override description to test") - parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers") - parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds") - parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query") - parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold") - parser.add_argument("--model", default=None, help="Model to use for claude -p (default: user's configured model)") - parser.add_argument("--verbose", action="store_true", help="Print progress to stderr") - args = parser.parse_args() - - eval_set = json.loads(Path(args.eval_set).read_text()) - skill_path = Path(args.skill_path) - - if not (skill_path / "SKILL.md").exists(): - print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) - sys.exit(1) - - name, original_description, content = parse_skill_md(skill_path) - description = args.description or original_description - project_root = find_project_root() - - if args.verbose: - print(f"Evaluating: {description}", file=sys.stderr) - - output = run_eval( - eval_set=eval_set, - skill_name=name, - description=description, - num_workers=args.num_workers, - timeout=args.timeout, - project_root=project_root, - runs_per_query=args.runs_per_query, - trigger_threshold=args.trigger_threshold, - model=args.model, - ) - - if args.verbose: - summary = output["summary"] - print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr) - for r in output["results"]: - status = "PASS" if r["pass"] else "FAIL" - rate_str = f"{r['triggers']}/{r['runs']}" - print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr) - - print(json.dumps(output, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/scripts/run_loop.py b/external/anthropic/skills/skill-creator/scripts/run_loop.py deleted file mode 100755 index 30a263d..0000000 --- a/external/anthropic/skills/skill-creator/scripts/run_loop.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 -"""Run the eval + improve loop until all pass or max iterations reached. - -Combines run_eval.py and improve_description.py in a loop, tracking history -and returning the best description found. Supports train/test split to prevent -overfitting. -""" - -import argparse -import json -import random -import sys -import tempfile -import time -import webbrowser -from pathlib import Path - -from scripts.generate_report import generate_html -from scripts.improve_description import improve_description -from scripts.run_eval import find_project_root, run_eval -from scripts.utils import parse_skill_md - - -def split_eval_set(eval_set: list[dict], holdout: float, seed: int = 42) -> tuple[list[dict], list[dict]]: - """Split eval set into train and test sets, stratified by should_trigger.""" - random.seed(seed) - - # Separate by should_trigger - trigger = [e for e in eval_set if e["should_trigger"]] - no_trigger = [e for e in eval_set if not e["should_trigger"]] - - # Shuffle each group - random.shuffle(trigger) - random.shuffle(no_trigger) - - # Calculate split points - n_trigger_test = max(1, int(len(trigger) * holdout)) - n_no_trigger_test = max(1, int(len(no_trigger) * holdout)) - - # Split - test_set = trigger[:n_trigger_test] + no_trigger[:n_no_trigger_test] - train_set = trigger[n_trigger_test:] + no_trigger[n_no_trigger_test:] - - return train_set, test_set - - -def run_loop( - eval_set: list[dict], - skill_path: Path, - description_override: str | None, - num_workers: int, - timeout: int, - max_iterations: int, - runs_per_query: int, - trigger_threshold: float, - holdout: float, - model: str, - verbose: bool, - live_report_path: Path | None = None, - log_dir: Path | None = None, -) -> dict: - """Run the eval + improvement loop.""" - project_root = find_project_root() - name, original_description, content = parse_skill_md(skill_path) - current_description = description_override or original_description - - # Split into train/test if holdout > 0 - if holdout > 0: - train_set, test_set = split_eval_set(eval_set, holdout) - if verbose: - print(f"Split: {len(train_set)} train, {len(test_set)} test (holdout={holdout})", file=sys.stderr) - else: - train_set = eval_set - test_set = [] - - history = [] - exit_reason = "unknown" - - for iteration in range(1, max_iterations + 1): - if verbose: - print(f"\n{'='*60}", file=sys.stderr) - print(f"Iteration {iteration}/{max_iterations}", file=sys.stderr) - print(f"Description: {current_description}", file=sys.stderr) - print(f"{'='*60}", file=sys.stderr) - - # Evaluate train + test together in one batch for parallelism - all_queries = train_set + test_set - t0 = time.time() - all_results = run_eval( - eval_set=all_queries, - skill_name=name, - description=current_description, - num_workers=num_workers, - timeout=timeout, - project_root=project_root, - runs_per_query=runs_per_query, - trigger_threshold=trigger_threshold, - model=model, - ) - eval_elapsed = time.time() - t0 - - # Split results back into train/test by matching queries - train_queries_set = {q["query"] for q in train_set} - train_result_list = [r for r in all_results["results"] if r["query"] in train_queries_set] - test_result_list = [r for r in all_results["results"] if r["query"] not in train_queries_set] - - train_passed = sum(1 for r in train_result_list if r["pass"]) - train_total = len(train_result_list) - train_summary = {"passed": train_passed, "failed": train_total - train_passed, "total": train_total} - train_results = {"results": train_result_list, "summary": train_summary} - - if test_set: - test_passed = sum(1 for r in test_result_list if r["pass"]) - test_total = len(test_result_list) - test_summary = {"passed": test_passed, "failed": test_total - test_passed, "total": test_total} - test_results = {"results": test_result_list, "summary": test_summary} - else: - test_results = None - test_summary = None - - history.append({ - "iteration": iteration, - "description": current_description, - "train_passed": train_summary["passed"], - "train_failed": train_summary["failed"], - "train_total": train_summary["total"], - "train_results": train_results["results"], - "test_passed": test_summary["passed"] if test_summary else None, - "test_failed": test_summary["failed"] if test_summary else None, - "test_total": test_summary["total"] if test_summary else None, - "test_results": test_results["results"] if test_results else None, - # For backward compat with report generator - "passed": train_summary["passed"], - "failed": train_summary["failed"], - "total": train_summary["total"], - "results": train_results["results"], - }) - - # Write live report if path provided - if live_report_path: - partial_output = { - "original_description": original_description, - "best_description": current_description, - "best_score": "in progress", - "iterations_run": len(history), - "holdout": holdout, - "train_size": len(train_set), - "test_size": len(test_set), - "history": history, - } - live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name)) - - if verbose: - def print_eval_stats(label, results, elapsed): - pos = [r for r in results if r["should_trigger"]] - neg = [r for r in results if not r["should_trigger"]] - tp = sum(r["triggers"] for r in pos) - pos_runs = sum(r["runs"] for r in pos) - fn = pos_runs - tp - fp = sum(r["triggers"] for r in neg) - neg_runs = sum(r["runs"] for r in neg) - tn = neg_runs - fp - total = tp + tn + fp + fn - precision = tp / (tp + fp) if (tp + fp) > 0 else 1.0 - recall = tp / (tp + fn) if (tp + fn) > 0 else 1.0 - accuracy = (tp + tn) / total if total > 0 else 0.0 - print(f"{label}: {tp+tn}/{total} correct, precision={precision:.0%} recall={recall:.0%} accuracy={accuracy:.0%} ({elapsed:.1f}s)", file=sys.stderr) - for r in results: - status = "PASS" if r["pass"] else "FAIL" - rate_str = f"{r['triggers']}/{r['runs']}" - print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:60]}", file=sys.stderr) - - print_eval_stats("Train", train_results["results"], eval_elapsed) - if test_summary: - print_eval_stats("Test ", test_results["results"], 0) - - if train_summary["failed"] == 0: - exit_reason = f"all_passed (iteration {iteration})" - if verbose: - print(f"\nAll train queries passed on iteration {iteration}!", file=sys.stderr) - break - - if iteration == max_iterations: - exit_reason = f"max_iterations ({max_iterations})" - if verbose: - print(f"\nMax iterations reached ({max_iterations}).", file=sys.stderr) - break - - # Improve the description based on train results - if verbose: - print(f"\nImproving description...", file=sys.stderr) - - t0 = time.time() - # Strip test scores from history so improvement model can't see them - blinded_history = [ - {k: v for k, v in h.items() if not k.startswith("test_")} - for h in history - ] - new_description = improve_description( - skill_name=name, - skill_content=content, - current_description=current_description, - eval_results=train_results, - history=blinded_history, - model=model, - log_dir=log_dir, - iteration=iteration, - ) - improve_elapsed = time.time() - t0 - - if verbose: - print(f"Proposed ({improve_elapsed:.1f}s): {new_description}", file=sys.stderr) - - current_description = new_description - - # Find the best iteration by TEST score (or train if no test set) - if test_set: - best = max(history, key=lambda h: h["test_passed"] or 0) - best_score = f"{best['test_passed']}/{best['test_total']}" - else: - best = max(history, key=lambda h: h["train_passed"]) - best_score = f"{best['train_passed']}/{best['train_total']}" - - if verbose: - print(f"\nExit reason: {exit_reason}", file=sys.stderr) - print(f"Best score: {best_score} (iteration {best['iteration']})", file=sys.stderr) - - return { - "exit_reason": exit_reason, - "original_description": original_description, - "best_description": best["description"], - "best_score": best_score, - "best_train_score": f"{best['train_passed']}/{best['train_total']}", - "best_test_score": f"{best['test_passed']}/{best['test_total']}" if test_set else None, - "final_description": current_description, - "iterations_run": len(history), - "holdout": holdout, - "train_size": len(train_set), - "test_size": len(test_set), - "history": history, - } - - -def main(): - parser = argparse.ArgumentParser(description="Run eval + improve loop") - parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file") - parser.add_argument("--skill-path", required=True, help="Path to skill directory") - parser.add_argument("--description", default=None, help="Override starting description") - parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers") - parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds") - parser.add_argument("--max-iterations", type=int, default=5, help="Max improvement iterations") - parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query") - parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold") - parser.add_argument("--holdout", type=float, default=0.4, help="Fraction of eval set to hold out for testing (0 to disable)") - parser.add_argument("--model", required=True, help="Model for improvement") - parser.add_argument("--verbose", action="store_true", help="Print progress to stderr") - parser.add_argument("--report", default="auto", help="Generate HTML report at this path (default: 'auto' for temp file, 'none' to disable)") - parser.add_argument("--results-dir", default=None, help="Save all outputs (results.json, report.html, log.txt) to a timestamped subdirectory here") - args = parser.parse_args() - - eval_set = json.loads(Path(args.eval_set).read_text()) - skill_path = Path(args.skill_path) - - if not (skill_path / "SKILL.md").exists(): - print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) - sys.exit(1) - - name, _, _ = parse_skill_md(skill_path) - - # Set up live report path - if args.report != "none": - if args.report == "auto": - timestamp = time.strftime("%Y%m%d_%H%M%S") - live_report_path = Path(tempfile.gettempdir()) / f"skill_description_report_{skill_path.name}_{timestamp}.html" - else: - live_report_path = Path(args.report) - # Open the report immediately so the user can watch - live_report_path.write_text("

Starting optimization loop...

") - webbrowser.open(str(live_report_path)) - else: - live_report_path = None - - # Determine output directory (create before run_loop so logs can be written) - if args.results_dir: - timestamp = time.strftime("%Y-%m-%d_%H%M%S") - results_dir = Path(args.results_dir) / timestamp - results_dir.mkdir(parents=True, exist_ok=True) - else: - results_dir = None - - log_dir = results_dir / "logs" if results_dir else None - - output = run_loop( - eval_set=eval_set, - skill_path=skill_path, - description_override=args.description, - num_workers=args.num_workers, - timeout=args.timeout, - max_iterations=args.max_iterations, - runs_per_query=args.runs_per_query, - trigger_threshold=args.trigger_threshold, - holdout=args.holdout, - model=args.model, - verbose=args.verbose, - live_report_path=live_report_path, - log_dir=log_dir, - ) - - # Save JSON output - json_output = json.dumps(output, indent=2) - print(json_output) - if results_dir: - (results_dir / "results.json").write_text(json_output) - - # Write final HTML report (without auto-refresh) - if live_report_path: - live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name)) - print(f"\nReport: {live_report_path}", file=sys.stderr) - - if results_dir and live_report_path: - (results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name)) - - if results_dir: - print(f"Results saved to: {results_dir}", file=sys.stderr) - - -if __name__ == "__main__": - main() diff --git a/external/anthropic/skills/skill-creator/scripts/utils.py b/external/anthropic/skills/skill-creator/scripts/utils.py deleted file mode 100644 index 51b6a07..0000000 --- a/external/anthropic/skills/skill-creator/scripts/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Shared utilities for skill-creator scripts.""" - -from pathlib import Path - - - -def parse_skill_md(skill_path: Path) -> tuple[str, str, str]: - """Parse a SKILL.md file, returning (name, description, full_content).""" - content = (skill_path / "SKILL.md").read_text() - lines = content.split("\n") - - if lines[0].strip() != "---": - raise ValueError("SKILL.md missing frontmatter (no opening ---)") - - end_idx = None - for i, line in enumerate(lines[1:], start=1): - if line.strip() == "---": - end_idx = i - break - - if end_idx is None: - raise ValueError("SKILL.md missing frontmatter (no closing ---)") - - name = "" - description = "" - frontmatter_lines = lines[1:end_idx] - i = 0 - while i < len(frontmatter_lines): - line = frontmatter_lines[i] - if line.startswith("name:"): - name = line[len("name:"):].strip().strip('"').strip("'") - elif line.startswith("description:"): - value = line[len("description:"):].strip() - # Handle YAML multiline indicators (>, |, >-, |-) - if value in (">", "|", ">-", "|-"): - continuation_lines: list[str] = [] - i += 1 - while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")): - continuation_lines.append(frontmatter_lines[i].strip()) - i += 1 - description = " ".join(continuation_lines) - continue - else: - description = value.strip('"').strip("'") - i += 1 - - return name, description, content diff --git a/external/anthropic/skills/xlsx/LICENSE.txt b/external/anthropic/skills/xlsx/LICENSE.txt deleted file mode 100644 index c55ab42..0000000 --- a/external/anthropic/skills/xlsx/LICENSE.txt +++ /dev/null @@ -1,30 +0,0 @@ -© 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/external/anthropic/skills/xlsx/SKILL.md b/external/anthropic/skills/xlsx/SKILL.md deleted file mode 100644 index 9da5480..0000000 --- a/external/anthropic/skills/xlsx/SKILL.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: xlsx -description: "Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .xltx, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved." -license: Proprietary. LICENSE.txt has complete terms ---- - -# XLSX creation, editing, and analysis - -| Task | Approach | -|---|---| -| **Create** or **edit** with formulas/formatting | `openpyxl` — see gotchas below | -| **Bulk data** in or out | `pandas` (`read_excel`, `to_excel`) | -| **Quick look** at a sheet | `markitdown file.xlsx` — `## SheetName` per sheet; reads `.xlsm` too. No cell coordinates, so don't plan edits from it | -| **Read** a model (formulas *and* values) | two `load_workbook` passes — see gotchas | - -> `openpyxl`, `pandas`, and `markitdown` are preinstalled — do not run `pip install` first; write the script and import directly. Only if an import fails (or the `markitdown` command is missing): `pip install` the missing package. - -> Script paths below are relative to this skill's directory. - -## Requirements for every output - -- **Professional font** (Arial, Times New Roman) throughout, unless the user says otherwise. -- **Zero formula errors.** Never ship while `recalc.py` reports `errors_found`. If you think an error predates you, prove it: load the *original* with `data_only=True` and look at that cell. An error you introduced looks exactly like one you inherited. -- **Use formulas, never hardcoded results.** Write `sheet['B10'] = '=SUM(B2:B9)'`, not the Python-computed total. The sheet must recalculate when its inputs change. -- **Follow the user's spec literally.** Exact tab names, exact column headers, and the formula they spelled out. A redesign that computes something else fails, however elegant. -- **Document every assumption and hardcoded number** where the reader will see it — a cell comment, or an adjacent cell at a table's end. Cite a real source when one exists (`Source: Company 10-K, FY2024, Page 45, Revenue Note, [SEC EDGAR URL]`); when the number came from the user, say so plainly. -- **A workbook *you create* for someone to fill in** needs a short legend naming which cells to edit, and one example row of realistic values showing the expected format. Never add such a row to a file you were asked to edit. -- **Editing an existing file: match its conventions exactly.** They override every guideline here. Find its designated input cells first — a distinct font color, fill, or shading marks them — write only there, and leave every existing formula untouched. - -## Recalculate (mandatory whenever the file contains formulas) - -openpyxl writes formulas as strings with **no cached values**. Until you recalculate, every -formula cell reads back as `None` to anything reading cached values — `pandas`, -`load_workbook(data_only=True)`, and most previewers. - -```bash -python scripts/recalc.py output.xlsx [timeout_seconds] # default 30 -``` - -LibreOffice computes every formula, the file is **rewritten in place**, and you get JSON: -`status` (`success` | `errors_found`), `total_formulas`, `total_errors`, and an -`error_summary` naming up to 100 cells per error type (`locations_truncated` says how many it -withheld — trust `total_errors`, not the length of the list). Fix what it names and run it -again. **JSON with an `error` key instead of a `status` means nothing was recalculated**, and -only that case exits non-zero — `errors_found` exits 0, so never treat a clean exit as a clean -workbook. - -**A green recalc proves your formulas *evaluate*, not that they are *right*.** An off-by-one -range or a reference to the wrong row yields a clean, error-free file with wrong numbers. -Write 2–3 formulas first and check they pull the values you expect, before building out a grid. - -**A workbook that links to another file loses those links** if you re-save it with openpyxl and -then recalculate. Such a formula reads `='[1]Returns Analysis'!$B$2` — the `[1]` is an index -into the workbook's external-reference list, naming a *separate file on disk*, not a sheet. -That file is rarely present here, so the cell's cached value is the only thing holding its -data. openpyxl strips that value on save; LibreOffice then has to resolve the reference for -real, fails, writes `#NAME?`, and deletes every link. `recalc.py` refuses to run in that state -— copy those cells' values out of the original before you save over them (`--force` overrides, -and accepts the loss). - -## Choosing formulas that survive verification - -LibreOffice implements fewer functions than Excel, and one it cannot evaluate becomes a -literal `#NAME?` baked into the file you deliver. - -- **Prefer Excel-2007-era functions** — `SUMIFS`, `INDEX`, `MATCH`, `IFERROR`, `SUMPRODUCT` — which need no prefix. -- **Six post-2007 functions work, but only with an `_xlfn.` prefix**, because openpyxl writes your formula into the XML verbatim and Excel stores post-2007 names prefixed (its UI hides the prefix): `_xlfn.TEXTJOIN`, `_xlfn.CONCAT`, `_xlfn.IFS`, `_xlfn.SWITCH`, `_xlfn.MAXIFS`, `_xlfn.MINIFS`. Written bare, each yields `#NAME?`. -- **Never use `XLOOKUP`, `XMATCH`, `SORT`, `FILTER`, `UNIQUE`, or `SEQUENCE`.** The runtime's LibreOffice cannot evaluate them under *any* prefix. Newer builds do evaluate them, but they are spilling array functions and an openpyxl-written file has no spill metadata, so only the top-left cell of the range gets a value — and `recalc.py` reports `total_errors: 0` on the truncated result. Use `INDEX`/`MATCH` for lookups, and sort, filter, and de-duplicate in Python before writing the cells. -- A formula LibreOffice could not parse is written back **lowercased** — a quick tell beside a `#NAME?`. - -## openpyxl gotchas - -- **Reading a model takes two loads.** `data_only=True` yields cached values with the formulas gone; the default yields formula strings with no values. One pass cannot give you both. -- **`data_only=True` is destructive if you save.** That workbook has no formulas left, so saving replaces every one with a literal — permanently. -- **`data_only=True` on a file openpyxl just wrote returns `None` everywhere** — run `recalc.py` first. (A formula whose result is `""` also reads back as `None`.) -- **Merged cells: write the top-left anchor only.** Every other cell in the range is a `MergedCell` whose `.value` is read-only. -- **`.xlsm` loses its macros unless you pass `keep_vba=True`** to `load_workbook`. -- **A sheet name containing a space must be quoted** in a cross-sheet reference: `='Assumptions Inputs'!$B$5`. Unquoted, it evaluates to `#VALUE!`. - -## Financial models - -Unless the user says otherwise, or the existing file already does something else. - -**Color:** blue text (`0,0,255`) for hardcoded inputs and scenario levers · black for formulas · -green (`0,128,0`) for links to another sheet · red (`255,0,0`) for links to another file · -yellow fill (`255,255,0`) for key assumptions and cells the user should fill in. - -**Numbers:** currency `$#,##0`, with the unit named in the header (`Revenue ($mm)`) · zeros -render as `-`, including in percentages (`$#,##0;($#,##0);-`) · negatives in parentheses · -percentages `0.0%`, **stored as fractions** (`0.15` renders `15.0%`; storing `15` renders -`1500.0%`) · valuation multiples `0.0x` · years as text (`"2024"`, never `2,024`). - -**Structure:** every assumption in its own labeled cell, referenced by the formulas that use it -(`=B5*(1+$B$6)`, never `=B5*1.05`) · formulas consistent across every projection period, since a -lone edited cell mid-row is the commonest silent error · guard denominators that can be zero. - -## Dependencies - -`openpyxl`, `pandas`, `markitdown` (pip, preinstalled — install only if an import fails or the command is missing) · LibreOffice (`soffice`, auto-configured for sandboxed environments via `scripts/office/soffice.py`) diff --git a/external/anthropic/skills/xlsx/scripts/office/helpers/__init__.py b/external/anthropic/skills/xlsx/scripts/office/helpers/__init__.py deleted file mode 100644 index d3c5817..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/helpers/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -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 - if _SCHEME_RE.match(target): - return None - - target = urllib.parse.unquote(target) - - 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) - return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]).lstrip("./") - - -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/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_chart.py b/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_chart.py deleted file mode 100644 index 209cb7c..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_chart.py +++ /dev/null @@ -1,170 +0,0 @@ -"""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/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_slide.py b/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_slide.py deleted file mode 100644 index 22f9aee..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_slide.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_theme.py b/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_theme.py deleted file mode 100644 index 8446620..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/helpers/pptx_theme.py +++ /dev/null @@ -1,114 +0,0 @@ -"""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/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd deleted file mode 100644 index 6454ef9..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +++ /dev/null @@ -1,1499 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd deleted file mode 100644 index afa4f46..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd deleted file mode 100644 index 64e66b8..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +++ /dev/null @@ -1,1085 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd deleted file mode 100644 index 687eea8..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd deleted file mode 100644 index 6ac81b0..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +++ /dev/null @@ -1,3081 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd deleted file mode 100644 index 1dbf051..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd deleted file mode 100644 index f1af17d..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd deleted file mode 100644 index 0a185ab..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd deleted file mode 100644 index 14ef488..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +++ /dev/null @@ -1,1676 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd deleted file mode 100644 index c20f3bf..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd deleted file mode 100644 index ac60252..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd deleted file mode 100644 index 424b8ba..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd deleted file mode 100644 index 2bddce2..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd deleted file mode 100644 index 8a8c18b..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd deleted file mode 100644 index 5c42706..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd deleted file mode 100644 index 853c341..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd deleted file mode 100644 index da835ee..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd deleted file mode 100644 index 87ad265..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +++ /dev/null @@ -1,582 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd deleted file mode 100644 index 9e86f1b..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd deleted file mode 100644 index d0be42e..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +++ /dev/null @@ -1,4439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd deleted file mode 100644 index 8821dd1..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +++ /dev/null @@ -1,570 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd deleted file mode 100644 index ca2575c..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +++ /dev/null @@ -1,509 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd deleted file mode 100644 index dd079e6..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd deleted file mode 100644 index 3dd6cf6..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd deleted file mode 100644 index f1041e3..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd deleted file mode 100644 index 9c5b7a6..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +++ /dev/null @@ -1,3646 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd deleted file mode 100644 index 0f13678..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - 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/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd deleted file mode 100644 index a6de9d2..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd deleted file mode 100644 index 10e978b..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd deleted file mode 100644 index 4248bf7..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd deleted file mode 100644 index 5649746..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/mce/mc.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/mce/mc.xsd deleted file mode 100644 index ef72545..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/mce/mc.xsd +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd deleted file mode 100644 index f65f777..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd deleted file mode 100644 index 6b00755..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd deleted file mode 100644 index f321d33..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd deleted file mode 100644 index 364c6a9..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd deleted file mode 100644 index fed9d15..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd deleted file mode 100644 index 680cf15..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd deleted file mode 100644 index 89ada90..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/external/anthropic/skills/xlsx/scripts/office/soffice.py b/external/anthropic/skills/xlsx/scripts/office/soffice.py deleted file mode 100644 index 0b4c99d..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/soffice.py +++ /dev/null @@ -1,192 +0,0 @@ -""" -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) - 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/external/anthropic/skills/xlsx/scripts/office/validate.py b/external/anthropic/skills/xlsx/scripts/office/validate.py deleted file mode 100755 index 29ca186..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/validate.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -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/external/anthropic/skills/xlsx/scripts/office/validators/__init__.py b/external/anthropic/skills/xlsx/scripts/office/validators/__init__.py deleted file mode 100644 index db092ec..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/validators/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -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/external/anthropic/skills/xlsx/scripts/office/validators/base.py b/external/anthropic/skills/xlsx/scripts/office/validators/base.py deleted file mode 100644 index 33fc97b..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/validators/base.py +++ /dev/null @@ -1,875 +0,0 @@ -""" -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)) - - with open(xml_file, "r") as f: - xml_doc = lxml.etree.parse(f) - - 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/external/anthropic/skills/xlsx/scripts/office/validators/docx.py b/external/anthropic/skills/xlsx/scripts/office/validators/docx.py deleted file mode 100644 index b181499..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/validators/docx.py +++ /dev/null @@ -1,466 +0,0 @@ -""" -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/external/anthropic/skills/xlsx/scripts/office/validators/pptx.py b/external/anthropic/skills/xlsx/scripts/office/validators/pptx.py deleted file mode 100644 index 318f0e6..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/validators/pptx.py +++ /dev/null @@ -1,441 +0,0 @@ -""" -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/external/anthropic/skills/xlsx/scripts/office/validators/redlining.py b/external/anthropic/skills/xlsx/scripts/office/validators/redlining.py deleted file mode 100644 index 4185c51..0000000 --- a/external/anthropic/skills/xlsx/scripts/office/validators/redlining.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -Validator for tracked changes in Word documents. - -Detects untracked edits in word/document.xml: 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. - -Only the document body is compared. Headers, footers, footnotes and endnotes -are separate parts and are not checked. -""" - -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 - - -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 - - try: - modified_tree = ET.parse(modified_file) - modified_root = modified_tree.getroot() - original_tree = ET.parse(original_file) - original_root = original_tree.getroot() - except (ET.ParseError, DefusedXmlException) as e: - print(f"FAILED - Error parsing XML files: {e}") - return False - - new_changes = self._new_tracked_changes(original_root, modified_root) - self._remove_tracked_changes(modified_root, new_changes) - - modified_text = self._extract_text_content(modified_root) - original_text = self._extract_text_content(original_root) - - if modified_text != original_text: - error_message = self._generate_detailed_diff( - original_text, modified_text - ) - print(error_message) - return False - - if self.verbose: - print( - f"PASSED - All {len(new_changes)} change(s) against the original " - "are properly tracked" - ) - return True - - def _tracked_change_elements(self, root): - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - return [elem for elem in root.iter() if elem.tag in (ins_tag, del_tag)] - - 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, original_text, modified_text): - 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 ", - "", - ] - - git_diff = self._get_git_word_diff(original_text, modified_text) - if git_diff: - error_parts.extend(["Differences:", "============", git_diff]) - else: - error_parts.append("Unable to generate word diff (git not available)") - - 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, - text=True, - ) - - 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, - text=True, - ) - - 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): - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - - for parent in root.iter(): - to_remove = [] - for child in parent: - if child.tag == ins_tag and child in targets: - to_remove.append(child) - for elem in to_remove: - parent.remove(elem) - - deltext_tag = f"{{{self.namespaces['w']}}}delText" - t_tag = f"{{{self.namespaces['w']}}}t" - - for parent in root.iter(): - to_process = [] - for child in parent: - if child.tag == del_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(): - if elem.tag == deltext_tag: - elem.tag = t_tag - - for child in reversed(list(del_elem)): - parent.insert(del_index, child) - parent.remove(del_elem) - - def _extract_text_content(self, root): - p_tag = f"{{{self.namespaces['w']}}}p" - t_tag = f"{{{self.namespaces['w']}}}t" - - paragraphs = [] - for p_elem in root.findall(f".//{p_tag}"): - text_parts = [] - for t_elem in p_elem.findall(f".//{t_tag}"): - text_parts.append(self._rendered_text(t_elem)) - paragraph_text = "".join(text_parts) - if paragraph_text: - paragraphs.append(paragraph_text) - - return "\n".join(paragraphs) - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/external/anthropic/skills/xlsx/scripts/recalc.py b/external/anthropic/skills/xlsx/scripts/recalc.py deleted file mode 100755 index 6232be2..0000000 --- a/external/anthropic/skills/xlsx/scripts/recalc.py +++ /dev/null @@ -1,308 +0,0 @@ -""" -Excel Formula Recalculation Script -Recalculates all formulas in an Excel file using LibreOffice -""" - -import contextlib -import json -import os -import platform -import re -import shutil -import subprocess -import sys -import tempfile -import time -import zipfile -from pathlib import Path - -from office.soffice import get_soffice_env, run_soffice - -from openpyxl import load_workbook - -MACRO_FILENAME = "Module1.xba" -SOFFICE_MISSING = "soffice not found on PATH; LibreOffice is required to recalculate" - -MAX_LOCATIONS = 100 - -EXTERNAL_REF_RE = re.compile(r"""(? - - - Sub RecalculateAndSave() - ThisComponent.calculateAll() - ThisComponent.store() - ThisComponent.close(True) - End Sub -""" - - -def has_gtimeout(): - try: - subprocess.run( - ["gtimeout", "--version"], capture_output=True, timeout=1, check=False - ) - return True - except (FileNotFoundError, subprocess.TimeoutExpired): - return False - - -def _stamp(path): - st = os.stat(path) - return st.st_mtime_ns, st.st_size - - -def setup_libreoffice_macro(profile_dir: Path, timeout=30): - url = profile_dir.as_uri() - try: - run_soffice( - ["--headless", "--terminate_after_init", f"-env:UserInstallation={url}"], - capture_output=True, - timeout=timeout, - ) - except FileNotFoundError: - return None, SOFFICE_MISSING - except subprocess.TimeoutExpired: - return None, "LibreOffice timed out creating its profile; formulas were NOT recalculated" - - macro_dir = profile_dir / "user" / "basic" / "Standard" - if not macro_dir.exists(): - return None, "LibreOffice did not create a usable profile; formulas were NOT recalculated" - - try: - (macro_dir / MACRO_FILENAME).write_text(RECALCULATE_MACRO) - except OSError as e: - return None, f"Could not install the recalculation macro: {e}" - - return url, None - - -def external_links_at_risk(filename): - try: - with zipfile.ZipFile(filename) as archive: - names = archive.namelist() - except (zipfile.BadZipFile, OSError): - return [] - if not any(n.startswith("xl/externalLinks/") for n in names): - return [] - - with contextlib.ExitStack() as stack: - formulas = load_workbook(filename, data_only=False) - stack.callback(formulas.close) - values = load_workbook(filename, data_only=True) - stack.callback(values.close) - - external_names = [ - name - for name, dn in formulas.defined_names.items() - if isinstance(getattr(dn, "value", None), str) and EXTERNAL_REF_RE.search(dn.value) - ] - name_re = ( - re.compile(r"\b(" + "|".join(re.escape(n) for n in external_names) + r")\b") - if external_names - else None - ) - - at_risk = [] - for sheet in formulas.sheetnames: - ws = formulas[sheet] - if not hasattr(ws, "iter_rows"): - continue - cached = values[sheet] - for row in ws.iter_rows(): - for cell in row: - v = cell.value - if not (isinstance(v, str) and v.startswith("=")): - continue - reaches_out = EXTERNAL_REF_RE.search(v) or (name_re and name_re.search(v)) - if reaches_out and cached[cell.coordinate].value is None: - at_risk.append(f"{sheet}!{cell.coordinate}") - return at_risk - - -def recalc(filename, timeout=30, force=False): - if not Path(filename).exists(): - return {"error": f"File {filename} does not exist"} - - abs_path = str(Path(filename).absolute()) - - if not os.access(abs_path, os.W_OK): - return {"error": f"{filename} is not writable; recalculation rewrites the file in place"} - - try: - get_soffice_env() - except Exception as e: - return {"error": f"Could not prepare the LibreOffice environment: {e}"} - - if not force: - try: - at_risk = external_links_at_risk(filename) - except Exception as e: - return {"error": f"Could not inspect {filename} for external links: {e}"} - if at_risk: - shown = at_risk[:MAX_LOCATIONS] - return { - "error": ( - "Refusing to recalculate: this workbook links to another workbook, and " - f"{len(at_risk)} linked cell(s) have lost their cached value (openpyxl strips " - "these on save). Recalculating would resolve them to #NAME? and delete the " - "external links for good. Copy those cells' values from the original file " - "before saving, or pass --force to accept the loss. Charts and conditional " - "formats can hold external references too, so this list may not be exhaustive." - ), - "external_link_cells": shown, - "external_link_cells_truncated": max(0, len(at_risk) - len(shown)), - } - - with tempfile.TemporaryDirectory( - prefix="recalc-lo-profile-", ignore_cleanup_errors=True - ) as profile_dir: - return _recalc_with_profile(filename, abs_path, timeout, Path(profile_dir)) - - -def _recalc_with_profile(filename, abs_path, timeout, profile_dir: Path): - started = time.monotonic() - profile_url, err = setup_libreoffice_macro(profile_dir, timeout=timeout) - if err: - return {"error": err} - - timeout = max(5, int(timeout - (time.monotonic() - started))) - - before = _stamp(abs_path) - - cmd = [ - "soffice", - "--headless", - "--norestore", - f"-env:UserInstallation={profile_url}", - "vnd.sun.star.script:Standard.Module1.RecalculateAndSave?language=Basic&location=application", - abs_path, - ] - - if platform.system() == "Linux" and shutil.which("timeout"): - cmd = ["timeout", str(timeout)] + cmd - elif platform.system() == "Darwin" and has_gtimeout(): - cmd = ["gtimeout", str(timeout)] + cmd - - timed_out = f"LibreOffice timed out after {timeout}s; formulas were NOT recalculated. Re-run with a longer timeout." - - try: - result = subprocess.run( - cmd, capture_output=True, text=True, env=get_soffice_env(), timeout=timeout + 15 - ) - except subprocess.TimeoutExpired: - return {"error": timed_out} - except FileNotFoundError: - return {"error": SOFFICE_MISSING} - - if result.returncode == 124: - return {"error": timed_out} - - if result.returncode != 0: - detail = (result.stderr or "").strip() or f"soffice exited {result.returncode}" - return {"error": f"LibreOffice failed to recalculate: {detail}"} - - if _stamp(abs_path) == before: - return { - "error": ( - "LibreOffice exited cleanly but never rewrote the file, so nothing was " - "recalculated. Check that no other LibreOffice instance is running, then retry." - ) - } - - try: - wb = load_workbook(filename, data_only=True) - - excel_errors = [ - "#VALUE!", - "#DIV/0!", - "#REF!", - "#NAME?", - "#NULL!", - "#NUM!", - "#N/A", - ] - error_details = {err: [] for err in excel_errors} - total_errors = 0 - - for sheet_name in wb.sheetnames: - ws = wb[sheet_name] - if not hasattr(ws, "iter_rows"): - continue - for row in ws.iter_rows(): - for cell in row: - if cell.value is not None and isinstance(cell.value, str): - for err in excel_errors: - if err in cell.value: - location = f"{sheet_name}!{cell.coordinate}" - error_details[err].append(location) - total_errors += 1 - break - - result = { - "status": "success" if total_errors == 0 else "errors_found", - "total_errors": total_errors, - "error_summary": {}, - } - - for err_type, locations in error_details.items(): - if locations: - entry = {"count": len(locations), "locations": locations[:MAX_LOCATIONS]} - if len(locations) > MAX_LOCATIONS: - entry["locations_truncated"] = len(locations) - MAX_LOCATIONS - result["error_summary"][err_type] = entry - - wb.close() - - wb_formulas = load_workbook(filename, data_only=False) - formula_count = 0 - for sheet_name in wb_formulas.sheetnames: - ws = wb_formulas[sheet_name] - if not hasattr(ws, "iter_rows"): - continue - for row in ws.iter_rows(): - for cell in row: - if ( - cell.value - and isinstance(cell.value, str) - and cell.value.startswith("=") - ): - formula_count += 1 - wb_formulas.close() - - result["total_formulas"] = formula_count - - return result - - except Exception as e: - return {"error": str(e)} - - -def main(): - args = [a for a in sys.argv[1:] if a != "--force"] - force = "--force" in sys.argv[1:] - - if not args: - print("Usage: python recalc.py [timeout_seconds] [--force]") - print("\nRecalculates all formulas in an Excel file using LibreOffice") - print("\nReturns JSON with error details:") - print(" - status: 'success' or 'errors_found'") - print(" - total_errors: Total number of Excel errors found") - print(" - total_formulas: Number of formulas in the file") - print(" - error_summary: Breakdown by error type with locations") - print(" - #VALUE!, #DIV/0!, #REF!, #NAME?, #NULL!, #NUM!, #N/A") - print("\nOn any failure the JSON has an 'error' key and no 'status'.") - print("--force recalculates even when it would destroy external links.") - sys.exit(1) - - filename = args[0] - timeout = int(args[1]) if len(args) > 1 else 30 - - result = recalc(filename, timeout, force=force) - print(json.dumps(result, indent=2)) - sys.exit(1 if "error" in result else 0) - - -if __name__ == "__main__": - main()