From d462d085367716d94cbe084b252216efee7bbc25 Mon Sep 17 00:00:00 2001 From: Daoji Wang <627665797@qq.com> Date: Tue, 1 Sep 2026 17:55:40 +0800 Subject: [PATCH 001/177] feat(skills): add pdf skill --- App/memmy-agent/src/skills/pdf/LICENSE.txt | 201 +++++++++++++++++++++ App/memmy-agent/src/skills/pdf/SKILL.md | 67 +++++++ 2 files changed, 268 insertions(+) create mode 100644 App/memmy-agent/src/skills/pdf/LICENSE.txt create mode 100644 App/memmy-agent/src/skills/pdf/SKILL.md diff --git a/App/memmy-agent/src/skills/pdf/LICENSE.txt b/App/memmy-agent/src/skills/pdf/LICENSE.txt new file mode 100644 index 000000000..13e25df86 --- /dev/null +++ b/App/memmy-agent/src/skills/pdf/LICENSE.txt @@ -0,0 +1,201 @@ +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 [yyyy] [name of copyright owner] + +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. diff --git a/App/memmy-agent/src/skills/pdf/SKILL.md b/App/memmy-agent/src/skills/pdf/SKILL.md new file mode 100644 index 000000000..fd5d56058 --- /dev/null +++ b/App/memmy-agent/src/skills/pdf/SKILL.md @@ -0,0 +1,67 @@ +--- +name: "pdf" +description: "Use when tasks involve reading, creating, or reviewing PDF files where rendering and layout matter; prefer visual checks by rendering pages (Poppler) and use Python tools such as `reportlab`, `pdfplumber`, and `pypdf` for generation and extraction." +--- + + +# PDF Skill + +## When to use +- Read or review PDF content where layout and visuals matter. +- Create PDFs programmatically with reliable formatting. +- Validate final rendering before delivery. + +## Workflow +1. Prefer visual review: render PDF pages to PNGs and inspect them. + - Use `pdftoppm` if available. + - If unavailable, install Poppler or ask the user to review the output locally. +2. Use `reportlab` to generate PDFs when creating new documents. +3. Use `pdfplumber` (or `pypdf`) for text extraction and quick checks; do not rely on it for layout fidelity. +4. After each meaningful update, re-render pages and verify alignment, spacing, and legibility. + +## Temp and output conventions +- Use `tmp/pdfs/` for intermediate files; delete when done. +- Write final artifacts under `output/pdf/` when working in this repo. +- Keep filenames stable and descriptive. + +## Dependencies (install if missing) +Prefer `uv` for dependency management. + +Python packages: +``` +uv pip install reportlab pdfplumber pypdf +``` +If `uv` is unavailable: +``` +python3 -m pip install reportlab pdfplumber pypdf +``` +System tools (for rendering): +``` +# macOS (Homebrew) +brew install poppler + +# Ubuntu/Debian +sudo apt-get install -y poppler-utils +``` + +If installation isn't possible in this environment, tell the user which dependency is missing and how to install it locally. + +## Environment +No required environment variables. + +## Rendering command +``` +pdftoppm -png $INPUT_PDF $OUTPUT_PREFIX +``` + +## Quality expectations +- Maintain polished visual design: consistent typography, spacing, margins, and section hierarchy. +- Avoid rendering issues: clipped text, overlapping elements, broken tables, black squares, or unreadable glyphs. +- Charts, tables, and images must be sharp, aligned, and clearly labeled. +- Use ASCII hyphens only. Avoid U+2011 (non-breaking hyphen) and other Unicode dashes. +- Citations and references must be human-readable; never leave tool tokens or placeholder strings. + +## Final checks +- Do not deliver until the latest PNG inspection shows zero visual or formatting defects. +- Confirm headers/footers, page numbering, and section transitions look polished. +- Keep intermediate files organized or remove them after final approval. From b587bf6226d110e65076ac2b3ad1140c0f734a22 Mon Sep 17 00:00:00 2001 From: Daoji Wang <627665797@qq.com> Date: Wed, 2 Sep 2026 16:41:25 +0800 Subject: [PATCH 002/177] chore: freeze Python DOCX skill baseline --- App/memmy-agent/src/skills/docx/SKILL.md | 132 ++++ .../skills/docx/references/design_presets.md | 248 +++++++ .../examples/end_to_end_smoke_test.md | 32 + .../docx/references/header_templates.md | 373 ++++++++++ .../src/skills/docx/references/manifest.txt | 75 ++ .../skills/docx/references/ooxml/comments.md | 37 + .../references/ooxml/hyperlinks_and_fields.md | 69 ++ .../ooxml/rels_and_content_types.md | 37 + .../docx/references/ooxml/tracked_changes.md | 41 ++ .../references/tasks/accessibility_a11y.md | 55 ++ .../references/tasks/captions_crossrefs.md | 97 +++ .../references/tasks/clean_tracked_changes.md | 44 ++ .../docx/references/tasks/comments_manage.md | 71 ++ .../docx/references/tasks/compare_diff.md | 32 + .../docx/references/tasks/create_edit.md | 51 ++ .../docx/references/tasks/fields_update.md | 66 ++ .../references/tasks/fixtures_edge_cases.md | 42 ++ .../references/tasks/footnotes_endnotes.md | 52 ++ .../tasks/forms_content_controls.md | 49 ++ .../references/tasks/headings_numbering.md | 44 ++ .../docx/references/tasks/images_figures.md | 35 + .../docx/references/tasks/multi_doc_merge.md | 31 + .../tasks/navigation_internal_links.md | 62 ++ .../tasks/privacy_scrub_metadata.md | 24 + .../tasks/protection_restrict_editing.md | 36 + .../docx/references/tasks/read_review.md | 60 ++ .../tasks/redaction_anonymization.md | 57 ++ .../docx/references/tasks/sections_layout.md | 62 ++ .../references/tasks/style_lint_normalize.md | 70 ++ .../references/tasks/tables_spreadsheets.md | 36 + .../references/tasks/templates_style_packs.md | 34 + .../docx/references/tasks/toc_workflow.md | 61 ++ .../docx/references/tasks/verify_render.md | 56 ++ .../references/tasks/watermarks_background.md | 35 + .../skills/docx/references/template-create.md | 93 +++ .../docx/references/template-distill.md | 89 +++ .../troubleshooting/libreoffice_headless.md | 44 ++ .../troubleshooting/run_splitting.md | 15 + .../src/skills/docx/scripts/a11y_audit.py | 372 ++++++++++ .../docx/scripts/accept_tracked_changes.py | 180 +++++ .../docx/scripts/add_tracked_replacements.py | 186 +++++ .../docx/scripts/apply_template_styles.py | 131 ++++ .../docx/scripts/captions_and_crossrefs.py | 305 +++++++++ .../src/skills/docx/scripts/comments_add.py | 256 +++++++ .../docx/scripts/comments_apply_patch.py | 156 +++++ .../skills/docx/scripts/comments_extract.py | 168 +++++ .../src/skills/docx/scripts/comments_strip.py | 151 ++++ .../skills/docx/scripts/content_controls.py | 358 ++++++++++ .../skills/docx/scripts/docx_ooxml_patch.py | 643 ++++++++++++++++++ .../skills/docx/scripts/docx_table_to_csv.py | 72 ++ .../skills/docx/scripts/fields_materialize.py | 310 +++++++++ .../src/skills/docx/scripts/fields_report.py | 182 +++++ .../skills/docx/scripts/flatten_ref_fields.py | 154 +++++ .../skills/docx/scripts/footnotes_report.py | 99 +++ .../src/skills/docx/scripts/heading_audit.py | 103 +++ .../src/skills/docx/scripts/images_audit.py | 171 +++++ .../src/skills/docx/scripts/insert_note.py | 244 +++++++ .../skills/docx/scripts/insert_ref_fields.py | 239 +++++++ .../src/skills/docx/scripts/insert_toc.py | 154 +++++ .../src/skills/docx/scripts/internal_nav.py | 365 ++++++++++ .../src/skills/docx/scripts/make_fixtures.py | 256 +++++++ .../mark_artifact_operation_started.mjs | 22 + .../skills/docx/scripts/merge_docx_append.py | 110 +++ .../src/skills/docx/scripts/privacy_scrub.py | 176 +++++ .../src/skills/docx/scripts/redact_docx.py | 315 +++++++++ .../skills/docx/scripts/render_and_diff.py | 165 +++++ .../src/skills/docx/scripts/render_docx.py | 448 ++++++++++++ .../src/skills/docx/scripts/section_audit.py | 86 +++ .../src/skills/docx/scripts/set_protection.py | 149 ++++ .../src/skills/docx/scripts/style_lint.py | 170 +++++ .../skills/docx/scripts/style_normalize.py | 187 +++++ .../src/skills/docx/scripts/table_geometry.py | 270 ++++++++ .../src/skills/docx/scripts/watermark_add.py | 170 +++++ .../docx/scripts/watermark_audit_remove.py | 145 ++++ .../skills/docx/scripts/xlsx_to_docx_table.py | 179 +++++ 75 files changed, 10394 insertions(+) create mode 100644 App/memmy-agent/src/skills/docx/SKILL.md create mode 100644 App/memmy-agent/src/skills/docx/references/design_presets.md create mode 100644 App/memmy-agent/src/skills/docx/references/examples/end_to_end_smoke_test.md create mode 100644 App/memmy-agent/src/skills/docx/references/header_templates.md create mode 100644 App/memmy-agent/src/skills/docx/references/manifest.txt create mode 100644 App/memmy-agent/src/skills/docx/references/ooxml/comments.md create mode 100644 App/memmy-agent/src/skills/docx/references/ooxml/hyperlinks_and_fields.md create mode 100644 App/memmy-agent/src/skills/docx/references/ooxml/rels_and_content_types.md create mode 100644 App/memmy-agent/src/skills/docx/references/ooxml/tracked_changes.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/accessibility_a11y.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/captions_crossrefs.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/clean_tracked_changes.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/comments_manage.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/compare_diff.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/create_edit.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/fields_update.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/fixtures_edge_cases.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/footnotes_endnotes.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/forms_content_controls.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/headings_numbering.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/images_figures.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/multi_doc_merge.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/navigation_internal_links.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/privacy_scrub_metadata.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/protection_restrict_editing.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/read_review.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/redaction_anonymization.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/sections_layout.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/style_lint_normalize.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/tables_spreadsheets.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/templates_style_packs.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/toc_workflow.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/verify_render.md create mode 100644 App/memmy-agent/src/skills/docx/references/tasks/watermarks_background.md create mode 100644 App/memmy-agent/src/skills/docx/references/template-create.md create mode 100644 App/memmy-agent/src/skills/docx/references/template-distill.md create mode 100644 App/memmy-agent/src/skills/docx/references/troubleshooting/libreoffice_headless.md create mode 100644 App/memmy-agent/src/skills/docx/references/troubleshooting/run_splitting.md create mode 100644 App/memmy-agent/src/skills/docx/scripts/a11y_audit.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/accept_tracked_changes.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/add_tracked_replacements.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/apply_template_styles.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/captions_and_crossrefs.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/comments_add.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/comments_apply_patch.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/comments_extract.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/comments_strip.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/content_controls.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/docx_ooxml_patch.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/docx_table_to_csv.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/fields_materialize.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/fields_report.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/flatten_ref_fields.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/footnotes_report.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/heading_audit.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/images_audit.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/insert_note.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/insert_ref_fields.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/insert_toc.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/internal_nav.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/make_fixtures.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/mark_artifact_operation_started.mjs create mode 100644 App/memmy-agent/src/skills/docx/scripts/merge_docx_append.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/privacy_scrub.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/redact_docx.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/render_and_diff.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/render_docx.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/section_audit.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/set_protection.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/style_lint.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/style_normalize.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/table_geometry.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/watermark_add.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/watermark_audit_remove.py create mode 100644 App/memmy-agent/src/skills/docx/scripts/xlsx_to_docx_table.py diff --git a/App/memmy-agent/src/skills/docx/SKILL.md b/App/memmy-agent/src/skills/docx/SKILL.md new file mode 100644 index 000000000..5b2fe3414 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/SKILL.md @@ -0,0 +1,132 @@ +--- +name: docx +description: Create, read, edit, review, redline, comment on, merge, audit, render, and verify .docx Word documents, including layout-sensitive and OOXML-level work. +--- + +# DOCX Skill + +Use this skill when a task involves a Word `.docx` document and correctness +depends on document structure, formatting, tracked changes, comments, fields, +tables, forms, or rendered layout. + +## Core workflow + +1. Identify the operation: read/review, create/edit, template following, + redlining/comments, conversion, or final verification. +2. Keep the source document unchanged for review and template-distillation + work. Write outputs and intermediate files to a task-specific writable + directory. +3. Use `python-docx` for ordinary paragraphs, runs, styles, tables, + headers/footers, and page setup. Use the bundled helpers under `scripts/` + for deterministic audits and OOXML operations. +4. After every meaningful create or edit batch, render the document with + `render_docx.py`, inspect every generated page image, and iterate until the + layout is clean. +5. Deliver only the requested final document. Keep rendered PNGs, temporary + PDFs, audit JSON, and diff images as internal QA artifacts unless requested. + +## Route to the relevant guide + +Read only the supporting guide needed for the current operation: + +- Existing-document reading or review: [references/tasks/read_review.md](references/tasks/read_review.md) +- Creating or editing a document: [references/tasks/create_edit.md](references/tasks/create_edit.md) +- Render and visual verification: [references/tasks/verify_render.md](references/tasks/verify_render.md) +- Accessibility and document structure: [references/tasks/accessibility_a11y.md](references/tasks/accessibility_a11y.md) +- Styles and formatting cleanup: [references/tasks/style_lint_normalize.md](references/tasks/style_lint_normalize.md) +- Templates and style packs: [references/tasks/templates_style_packs.md](references/tasks/templates_style_packs.md) +- Template distillation and creation: [references/template-distill.md](references/template-distill.md) and + [references/template-create.md](references/template-create.md) +- Tables and spreadsheet-to-document conversion: [references/tasks/tables_spreadsheets.md](references/tasks/tables_spreadsheets.md) +- Forms/content controls: [references/tasks/forms_content_controls.md](references/tasks/forms_content_controls.md) +- Captions and cross-references: [references/tasks/captions_crossrefs.md](references/tasks/captions_crossrefs.md) +- Fields and field display text: [references/tasks/fields_update.md](references/tasks/fields_update.md) +- Navigation and table of contents: [references/tasks/navigation_internal_links.md](references/tasks/navigation_internal_links.md) and + [references/tasks/toc_workflow.md](references/tasks/toc_workflow.md) +- Tracked changes: [references/tasks/clean_tracked_changes.md](references/tasks/clean_tracked_changes.md) and + [references/ooxml/tracked_changes.md](references/ooxml/tracked_changes.md) +- Comments: [references/tasks/comments_manage.md](references/tasks/comments_manage.md) and + [references/ooxml/comments.md](references/ooxml/comments.md) +- Hyperlinks, fields, headers, and page numbers: [references/ooxml/hyperlinks_and_fields.md](references/ooxml/hyperlinks_and_fields.md) +- Package relationships and content types: [references/ooxml/rels_and_content_types.md](references/ooxml/rels_and_content_types.md) +- Redaction, privacy, protection, watermarks, footnotes, merging, or sections: + use the matching file under `references/tasks/`. + +## Rendering and visual QA + +The canonical renderer converts DOCX to PDF internally and rasterizes each page +to `page-.png`: + +```bash +python3 scripts/render_docx.py input.docx --output_dir /path/to/qa +``` + +Use `--emit_pdf` only when an intermediate PDF is useful for diagnosis. Use +`--verbose` when LibreOffice conversion needs debugging. Inspect every page at +full resolution for clipped or overlapping text, missing glyphs, broken tables, +unexpected page breaks, and header/footer drift. Text extraction or XML checks +alone cannot prove visual correctness. + +If LibreOffice is unavailable, complete structural checks with the relevant +audits and state that visual QA could not be performed. If conversion fails for +another reason, diagnose the renderer/profile problem before judging the DOCX. + +For repeated comparisons, use `scripts/render_and_diff.py`. For a quick +structural pass, use the audits for sections, headings, images, fields, +footnotes, comments, tables, styles, watermarks, accessibility, or content +controls as applicable. + +## New documents and major rewrites + +When no supplied template controls the design, choose exactly one preset from +[references/design_presets.md](references/design_presets.md): + +- `standard_business_brief` for formal memos and executive briefs +- `compact_reference_guide` for checklists, launch guides, and dense references +- `narrative_proposal` for proposals and longer persuasive documents +- an archetype alias defined by the reference when it is a closer fit + +Resolve the preset into explicit page, margin, typography, spacing, list, +table, color, header, and footer tokens before drafting. Use real Word styles, +numbering definitions, and fixed DXA table geometry; do not depend on inherited +defaults or visual approximations. Read [references/header_templates.md](references/header_templates.md) +when a new first-page header, cover, or title block is needed. + +Use the lightest form factor that matches the content: prose for explanation, +bullets for unordered considerations, numbered steps for procedures, checklists +for acceptance criteria, callouts for warnings, and tables only for genuinely +tabular data. Avoid using tables as layout containers or turning cells into +long prose blocks. + +## Editing, redlining, and OOXML + +For an existing document, preserve the original structure and make the smallest +local change that satisfies the request. Do not rewrite unrelated paragraphs or +styles. Real tracked changes and Word comments require OOXML patching; use the +corresponding helpers and perform another render plus structural check after +any package-level change. + +Use `scripts/privacy_scrub.py` before publication when personal metadata or +`rsid` values should be removed. Use `scripts/redact_docx.py` for explicit +redaction/anonymization requests and verify the result structurally and +visually. Use `scripts/set_protection.py` only when the requested output needs +editing restrictions. + +## File and command conventions + +- Refer to inputs and outputs by absolute paths when reporting results. +- Keep temporary work in a unique writable directory; never modify a retained + reference document during inspection. +- Prefer the bundled Python helpers and the runtime's available `python3`, + `soffice`, and Poppler commands. Check command availability before relying on + an optional operation. +- Do not claim a render or audit passed unless its output was actually checked. +- Keep citation text human-readable; do not place internal tool tokens in the + document. + +## Bundled resources + +The package includes the canonical renderer, OOXML notes, task playbooks, +design references, examples, and reusable scripts. `references/manifest.txt` lists the +bundled paths. Scripts are intended to be run from a writable working +directory, while this skill directory remains the source of reusable helpers. diff --git a/App/memmy-agent/src/skills/docx/references/design_presets.md b/App/memmy-agent/src/skills/docx/references/design_presets.md new file mode 100644 index 000000000..3137e0293 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/design_presets.md @@ -0,0 +1,248 @@ +# Design Presets + +Use this reference for new DOCX creation and major rewrites that do not have a selected template or supplied design reference. Existing-document edits should preserve the source document's style unless the user asks for a redesign. + +## Required workflow + +1. Pick exactly one preset or archetype alias before drafting, based on the document's audience and content. +2. Resolve it into a concrete token map with exact values for every preset-controlled property: page geometry, margins, header/footer distance, body spacing, heading spacing, line spacing, list marker alignment, list text indent, hanging indent, table widths, table indents, cell margins, colors, and fills. +3. Apply the tokens through Word styles, real numbering definitions, explicit table geometry, callout styles, headers, and footers. +4. Treat any deviation as a named override and reuse that override consistently. +5. Before rendering, audit the DOCX against the selected token map, including direct inspection of styles, numbering definitions, section properties, and table XML when needed. + +Do not combine presets in one document unless the user explicitly asks for a mixed style system. Do not rely on Word defaults, inherited built-in style values, or approximate visual matches. If a value appears in the selected preset, encode that exact value in the DOCX. + +## Exactness requirement + +Preset compliance means the generated DOCX carries the selected preset's actual numbers: + +- Paragraph styles must encode the preset's font, size, color, `before`, `after`, and line spacing values. For OOXML, this means values such as `w:before`, `w:after`, and `w:line` are present where the preset controls them. +- Lists must use numbering definitions whose marker alignment, text indent, hanging indent, tab stop, paragraph spacing, and line spacing match the preset. Built-in styles such as `List Bullet` or `List Number` are acceptable only after their numbering definitions have been set or patched to the preset values. +- Tables must use fixed DXA geometry. `tblW`, `tblGrid/gridCol`, and every `tcW` must agree with the preset or a named table-pattern override. `tblInd` must match the start cell margin token so the visible outer border aligns with surrounding paragraph text. +- Table-adjacent citation text must use the selected preset's `table_citation_text` component token instead of inheriting body or caption defaults. +- Page setup must encode the preset's page size, margins, usable width, and header/footer distances in section properties. +- Named overrides are allowed only when the document needs a specific exception. Record the override and apply it consistently; do not let ad-hoc direct formatting drift across similar elements. + +## Shared base tokens + +All presets inherit these values unless they override them. + +| Token | Value | +|---|---| +| Page size | US Letter, 8.5 x 11 in, portrait | +| Margins | 1.0 in top/right/bottom/left | +| Header/footer distance | 0.492 in | +| Usable width | 6.5 in / 9360 DXA | +| Base body style | `Normal` | +| Default base font | Calibri | +| Default base size | 11 pt | +| Heading 1 | 16 pt, `#2E74B5` | +| Heading 2 | 13 pt, `#2E74B5` | +| Heading 3 | 12 pt, `#1F4D78` | +| Table width | 6.5 in / 9360 DXA | +| Table indent | 120 DXA / 0.083 in, matching default start cell margin | +| Table geometry | fixed DXA `tblW`, `tblInd`, `tblGrid`, and matching `tcW` | +| Table default visual | thin single grid, white body cells, restrained optional header/callout fill | +| Header/footer style | quiet running label/header rule and muted right-aligned page number for multi-page polished docs | + +## OOXML conversion cheatsheet + +| Design value | OOXML value | +|---|---| +| 1.0 in | 1440 DXA | +| 6.5 in content/table width | 9360 DXA | +| 0.083 in table indent / cell start margin | 120 DXA | +| 0.5 in list text indent | 720 DXA | +| 0.38 in list text indent | about 540 DXA | +| 0.25 in marker alignment | 360 DXA | +| 0.18 in marker alignment | about 260 DXA | +| 0.19 in hanging indent | about 270 DXA | +| 10 pt before | `w:before="200"` | +| 8 pt after | `w:after="160"` | +| 7 pt after | `w:after="140"` | +| 6 pt after | `w:after="120"` | +| 5 pt after | `w:after="100"` | +| 4 pt before | `w:before="80"` | +| 4 pt after | `w:after="80"` | +| 3 pt after | `w:after="60"` | +| 1.333 line spacing | `w:line="320" w:lineRule="auto"` | +| 1.25 line spacing | `w:line="300" w:lineRule="auto"` | +| 1.208 line spacing | `w:line="290" w:lineRule="auto"` | +| 1.167 line spacing | `w:line="280" w:lineRule="auto"` | + +## Preset token schema + +Use this shape mentally or in builder code. Fill every value before writing content. + +```yaml +document_style_preset: + preset_name: "" + page: + size: Letter + orientation: portrait + margins: {top: 1.0in, right: 1.0in, bottom: 1.0in, left: 1.0in} + header: 0.492in + footer: 0.492in + content_width: {in: 6.5, dxa: 9360} + typography: + base_font: "" + base_size: 11pt + body: {alignment: left, before: 0pt, after: "", line_spacing: ""} + title: + size: "" + color: "" + before: "" + after: "" + headings: + h1: {size: 16pt, color: "#2E74B5", before: "", after: ""} + h2: {size: 13pt, color: "#2E74B5", before: "", after: ""} + h3: {size: 12pt, color: "#1F4D78", before: "", after: ""} + lists: + bullet_level_0: {marker: "•", marker_aligned_at: "", text_indent_at: "", hanging: "", after: "", line_spacing: ""} + decimal_level_0: {marker: "%1.", marker_aligned_at: "", text_indent_at: "", hanging: "", after: "", line_spacing: ""} + tables: + width_dxa: 9360 + indent_dxa: 120 + border_style: single_grid + header_fill: "" + cell_margins_dxa: {top: 80, bottom: 80, start: 120, end: 120} + table_citation_text: + use: "source/citation text immediately above or below a table" + paragraph: {before: 4pt, after: 4pt} + colors: + heading_blue: "#2E74B5" + heading_dark_blue: "#1F4D78" + ink_blue: "#0B2545" + table_fill_blue_gray: "#E8EEF5" + table_fill_light_gray: "#F2F4F7" + callout_fill: "#F4F6F9" + positive_dark_blue: "#1F3A5F" + caution_gold: "#7A5A00" + risk_red: "#9B1C1C" +``` + +## Base presets + +### `standard_business_brief` + +Use for formal memos, RFI responses, decision memos, board memos, and executive briefs. + +```yaml +preset_name: standard_business_brief +typography: + base_font: Calibri + body: {size: 11pt, alignment: left, before: 0pt, after: 6pt, line_spacing: 1.10} +headings: + h1: {size: 16pt, color: "#2E74B5", before: 16pt, after: 8pt} + h2: {size: 13pt, color: "#2E74B5", before: 12pt, after: 6pt} + h3: {size: 12pt, color: "#1F4D78", before: 8pt, after: 4pt} +lists: + bullet_level_0: {marker_aligned_at: 0.25in, text_indent_at: 0.5in, hanging: 0.25in, after: 8pt, line_spacing: 1.167} + decimal_level_0: {marker_aligned_at: 0.25in, text_indent_at: 0.5in, hanging: 0.25in, after: 8pt, line_spacing: 1.167} +tables: + width_dxa: 9360 + indent_dxa: 120 + cell_margins_dxa: {top: 80, bottom: 80, start: 120, end: 120} + border_style: single_grid + header_fill: "#F2F4F7" +table_citation_text: + use: "source/citation text immediately above or below a table" + paragraph: {before: 4pt, after: 4pt} +``` + +### `compact_reference_guide` + +Use for launch guides, negotiation briefs, checklists, and dense operator references. + +```yaml +preset_name: compact_reference_guide +typography: + base_font: Calibri + body: {size: 11pt, alignment: left, before: 0pt, after: 6pt, line_spacing: 1.25} +headings: + h1: {size: 16pt, color: "#2E74B5", before: 18pt, after: 10pt} + h2: {size: 13pt, color: "#2E74B5", before: 14pt, after: 7pt} + h3: {size: 12pt, color: "#1F4D78", before: 10pt, after: 5pt} +lists: + bullet_level_0: {marker_aligned_at: 0.187in, text_indent_at: 0.375in, hanging: 0.188in, after: 4pt, line_spacing: 1.25} + decimal_level_0: {marker_aligned_at: 0.187in, text_indent_at: 0.375in, hanging: 0.188in, after: 4pt, line_spacing: 1.25} +tables: + width_dxa: 9360 + indent_dxa: 120 + cell_margins_dxa: {top: 80, bottom: 80, start: 120, end: 120} + border_style: single_grid + header_fill: "#E8EEF5" + compact_label_detail_widths: [1.181in, 5.319in] + standard_label_detail_widths: [1.875in, 4.625in] +table_citation_text: + use: "source/citation text immediately above or below a table" + paragraph: {before: 4pt, after: 4pt} +``` + +### `narrative_proposal` + +Use for grant proposals, business proposals, and persuasive documents with longer prose. + +```yaml +preset_name: narrative_proposal +typography: + base_font: Calibri + body: {size: 11pt, alignment: justified, before: 0pt, after: 8pt, line_spacing: 1.333} +headings: + h1: {size: 16pt, color: "#2E74B5", before: 18pt, after: 10pt} + h2: {size: 13pt, color: "#2E74B5", before: 12pt, after: 6pt} + h3: {size: 12pt, color: "#1F4D78", before: 8pt, after: 4pt} +lists: + bullet_level_0: {marker_aligned_at: 0.181in, text_indent_at: 0.375in, hanging: 0.194in, after: 4pt, line_spacing: 1.208} + decimal_level_0: {marker_aligned_at: 0.181in, text_indent_at: 0.375in, hanging: 0.194in, after: 4pt, line_spacing: 1.208} +tables: + width_dxa: 9360 + indent_dxa: 120 + cell_margins_dxa: {top: 80, bottom: 80, start: 120, end: 120} + border_style: single_grid + header_fill: "#F4F6F9" +table_citation_text: + use: "source/citation text immediately above or below a table" + paragraph: {before: 4pt, after: 4pt} +``` + +## Archetype aliases + +Aliases inherit a base preset and override only the listed values. + +| Alias | Base preset | Overrides | +|---|---|---| +| `rfi_response` | `standard_business_brief` | Body after 6pt; H1 before 16pt/after 8pt; H2 before 12pt/after 6pt; list marker 0.25in, text 0.5in; use 3-4 column full-width compliance matrices. | +| `decision_memo` | `standard_business_brief` | Base font Arial; body after 6pt; H1 before 12pt/after 6pt; H2 before 10pt/after 5pt; list marker 0.25in, text 0.5in. | +| `launch_messaging_guide` | `compact_reference_guide` | Body after 6pt, line 1.25; H1 before 18pt/after 10pt; H2 before 14pt/after 7pt; H3 before 10pt/after 5pt; table use can be heavy. | +| `contract_negotiation_brief` | `compact_reference_guide` | Body after 6pt; H1 before 14pt/after 8pt; H2 before 11pt/after 6pt; H3 before 8pt/after 4pt; prefer 1.181in/5.319in label-detail grids. | +| `neighborhood_business_proposal` | `narrative_proposal` | Body justified, after 8pt, line 1.333; H1 before 18pt/after 10pt; H2 before 12pt/after 6pt; H3 before 8pt/after 4pt; decimal lists may lead action sequences. | +| `grant_proposal` | `narrative_proposal` | Body left or justified by section, after 6pt, line 1.25 for compact prose; H1 before 16pt/after 8pt; H2 before 12pt/after 6pt; reserve tables for budget and evaluation. | + +## Table patterns + +Use full-width tables by default. Pick column widths by content and keep the total at 9360 DXA. Use `tblInd=120` DXA unless a named override intentionally changes table placement; this aligns the visible outer border with surrounding paragraph text instead of aligning only the first cell's text. + +| Pattern | Widths | Use | +|---|---|---| +| One-column callout | 6.5 in | Message blocks, grouped examples, callouts. | +| Compact label-detail | 1.181 in, 5.319 in | Term/value, clause/position, compact reference rows. | +| Standard label-detail | 1.875 in, 4.625 in | Brief metadata, description tables, playbooks. | +| Two-up comparison | 3.25 in, 3.25 in | Option A/B, do/don't, before/after. | +| Three-column matrix | 1.5 in, 2.5 in, 2.5 in | Decision criteria, stakeholder impact, roadmap. | +| Four-column matrix | content-specific, sum 6.5 in | RFI compliance, budget, status, risk tables. | + +Always run the table geometry helper or an equivalent audit after table generation. + +## Preset audit + +Before final render review, verify: + +- Page size, margins, header/footer distance, and content width match the token map. +- Body and heading styles carry the selected font, size, color, spacing, and line spacing. +- Lists use real numbering definitions with the selected marker alignment, text indent, hanging indent, spacing, and line spacing. +- Tables use 9360 DXA unless intentionally compact, `tblInd` equals the start cell margin token, and `tblW`, `tblGrid`, and each `tcW` agree. +- Table-adjacent citation text above or below tables carries the selected preset's `table_citation_text` spacing. +- Callout/header/table fills use only the preset colors or a named override. +- Headers and footers are consistent across pages. +- There are no fake headings, fake bullets, manual numbering, percentage-width tables, fixed row heights that clip, or unexplained direct formatting drift. diff --git a/App/memmy-agent/src/skills/docx/references/examples/end_to_end_smoke_test.md b/App/memmy-agent/src/skills/docx/references/examples/end_to_end_smoke_test.md new file mode 100644 index 000000000..919909ff6 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/examples/end_to_end_smoke_test.md @@ -0,0 +1,32 @@ +# End-to-end smoke test (optional) + +This is a quick checklist to validate the environment and the helper scripts. + +## 1) Render check +```bash +python scripts/render_docx.py /mnt/data/some.docx --output_dir /mnt/data/out +``` + +## 2) Add header date, page numbers, hyperlink +```bash +python scripts/docx_ooxml_patch.py /mnt/data/some.docx \ + --header-date "Date: 01/05/2026" \ + --add-page-numbers \ + --hyperlink-first "https://example.com" +``` + +## 3) Add comment (structural) +```bash +python scripts/docx_ooxml_patch.py /mnt/data/some.docx \ + --add-comment --comment-text "Hello comment" # optionally add --contains "..." to anchor elsewhere +``` + +## 4) Tracked replace +If you already have a `` in the doc: +```bash +python scripts/docx_ooxml_patch.py /mnt/data/some.docx \ + --enable-track --tracked-replace-ins-id 102 --new-text " HELLO" +``` + +## 5) Verify visually +Use `references/tasks/verify_render.md` (DOCX → PNG) and inspect. diff --git a/App/memmy-agent/src/skills/docx/references/header_templates.md b/App/memmy-agent/src/skills/docx/references/header_templates.md new file mode 100644 index 000000000..5c098dc0a --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/header_templates.md @@ -0,0 +1,373 @@ +# Header Templates + +Use this reference when creating a new DOCX or a major repackage that needs first-page title/header furniture. Pick one pattern before drafting the document body. + +## Opinionated Method + +1. Choose the document job first: decision/reference, persuasive ask, report/guide, customer or partner packet, session plan, or proof story. +2. Use exactly one first-page header pattern. Do not mix centered-cover, memo metadata, metric-strip, and quote treatments in the same opening block. +3. Set the running section header/footer first, then build the visible first-page title block. +4. Replace sample text, colors, and metadata with the document's actual content. Keep the structure, hierarchy, and spacing intent. +5. Treat these snippets as inspiration, not a dependency. If helper names differ, implement the same Word-native effects with `python-docx`: real paragraphs, paragraph borders for rules, explicit table geometry for metadata grids, and standard section headers/footers. +6. No border bottoms for header + +## Pattern Picker + +| Pattern | Use for | First-page signal | +|---|---|---| +| `memo_masthead` | decision memos, exec briefs, board recommendations, strategy notes, PRDs, RFCs, specs, policy memos, status updates, incident reports, postmortems, risk reviews, audit/compliance notes, technical findings, research summaries | title, subtitle, dense metadata rows, strong bottom rule | +| `proposal_centerpiece` | grants, RFP/RFI responses, sales proposals, project proposals, SOWs, funding asks, business cases, sponsorship pitches, partnership proposals, product pitches, procurement responses, formal applications | centered title stack plus balanced two-column metadata | +| `editorial_cover` | reports, white papers, market/trend research, field guides, playbooks, handbooks, manuals, SOPs, reference guides, newsletters, annual/quarterly reviews, thought leadership, narrative briefs | generous vertical whitespace and a cover-like centered title | +| `customer_pack` | onboarding packs, kickoff packets, implementation plans, customer success plans, QBR/EBR packets, account plans, partner enablement, rollout plans, change-management docs, stakeholder packets, leave-behinds, training packets | left-aligned commercial title with compact metadata grid | +| `workshop_agenda` | agendas, offsites, workshops, design sprints, trainings, classes, webinars, run-of-show docs, meeting briefs, interview guides, discovery sessions, retrospectives, planning sessions, checklists with timed steps | title stack anchored by a time/objective metric strip | +| `customer_story` | case studies, testimonials, success stories, proof narratives, launch announcements, impact reports, before/after writeups, user research readouts, customer profiles, press one-pagers, advocacy stories | title stack plus centered pull quote | + +Default to `memo_masthead` for serious internal or technical documents, `proposal_centerpiece` for persuasive asks, `editorial_cover` for polished long-form reading, and `customer_pack` for operational customer/partner docs. Use `workshop_agenda` when sequence or participation is the point. Use `customer_story` when a quote, outcome, or proof point is central. + +## Shared Helper Contract + +The examples assume local helpers like `set_section_header`, `set_section_footer`, `add_title`, `add_subtitle`, `add_kicker`, `add_para`, `add_spacer`, `add_metadata_rows`, `add_inline_metadata_grid`, `add_metric_strip`, and `paragraph_border_bottom`. + +If those helpers are unavailable, recreate the effects directly: + +- Running header/footer: use `section.header` and `section.footer`. +- Title stack: use normal Word paragraphs with explicit size, bold/italic, color, alignment, and spacing. +- Metadata grid: use a small fixed-width table with explicit DXA geometry and cell margins. +- Bottom rule: use a paragraph border, not a fake table or repeated characters. +- Metric strip: use a fixed-width table with clear fill, compact labels, and enough cell padding. + +## `memo_masthead` + +```python +def set_run_font(run, name="Arial", size=None, color=None, bold=None, italic=None): + run.font.name = name + run._element.rPr.rFonts.set(qn("w:ascii"), name) + run._element.rPr.rFonts.set(qn("w:hAnsi"), name) + if size is not None: + run.font.size = Pt(size) + if color is not None: + run.font.color.rgb = color + if bold is not None: + run.bold = bold + if italic is not None: + run.italic = italic + +## don't change me +def add_metadata_rows(doc): + rows = [ + ("To", "Executive Team"), + ("From", "Launch Program Lead"), + ("Date", "May 4, 2026"), + ("Re", "Decision: delay launch by two weeks vs. ship with onboarding gap"), + ("Status", "Decision required this week"), + ] + + for label, value in rows: + p = doc.add_paragraph() + p.paragraph_format.space_before = Pt(0) + p.paragraph_format.space_after = Pt(2) + p.paragraph_format.line_spacing = MASTHEAD_METADATA_LINE_SPACING + label_run = p.add_run(f"{label}: ") + set_run_font(label_run, size=11, color=BLACK, bold=True) + value_run = p.add_run(value) + set_run_font(value_run, size=11, color=BLACK) + +def page_memo_masthead(doc, section): + set_section_header( + section, + "Decision Memo", + "Project Lighthouse - Confidential", + color=MUTED, + rule=False, + ) + set_section_footer(section, f"Page 1 of {TOTAL_PAGES}") + + add_spacer(doc, 16) + add_title(doc, "DECISION MEMO", size=23, color=RGBColor(0, 0, 0), after=4) + add_subtitle( + doc, + "Project Lighthouse - Launch Date vs. Onboarding Completeness", + size=14, + color=RGBColor(55, 55, 55), + after=16, + ) + add_metadata_rows( + doc, + [ + ("To:", "Project Lighthouse Cross-Functional Launch Team"), + ("From:", "Launch Program Lead"), + ("Date:", "April 29, 2026"), + ("Re:", "Decision: delay launch by two weeks vs. ship with onboarding gap"), + ("Status:", "Decision required by EOD Friday, May 1, 2026"), + ], + label_width=1.0, + ) + + add_spacer(doc, 14) + rule = doc.add_paragraph() +``` + +## `proposal_centerpiece` + +```python +def page_proposal_centerpiece(doc, section): + set_section_header( + section, + "Riverbend Arts Collective | Walls That Speak", + "City of Riverbend Small Community Grant", + color=MUTED, + rule=False, + ) + set_section_footer(section, f"Page 2 of {TOTAL_PAGES}") + + add_para( + doc, + "Riverbend Arts Collective", + size=12, + bold=True, + color=GRAY, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=8, + ) + add_title( + doc, + "Walls That Speak", + size=24, + color=RGBColor(0, 0, 0), + align=WD_ALIGN_PARAGRAPH.CENTER, + after=4, + ) + add_subtitle( + doc, + "A Weekend Public Mural Festival", + size=14, + color=GRAY, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=8, + ) + add_para( + doc, + "Proposal to the City of Riverbend Cultural Affairs Office | Small Community Grant Program", + size=10.5, + color=GRAY, + bold=True, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=26, + ) + + rule = doc.add_paragraph() + add_spacer(doc, 10) + add_inline_metadata_grid( + doc, + section, + [ + ("Applicant:", "Riverbend Arts Collective (501(c)(3))"), + ("Contact:", "Maya Ortiz, Executive Director"), + ("Email/Phone:", "maya@riverbendarts.org | (555) 412-9087"), + ("Site:", "Mill District, between 4th and 7th Streets"), + ], + [ + ("Proposed Dates:", "September 19-20, 2026"), + ("Amount Requested:", "$8,500"), + ("Total Budget:", "$23,400"), + ("Project Lead:", "Devon Pierce"), + ], + ) +``` + +## `editorial_cover` + +```python +def page_editorial_cover(doc, section): + set_section_header( + section, + "Pour With Intention", + "Trend Report - April 2026", + color=MUTED, + rule=True, + ) + set_section_footer(section, f"Page 3 of {TOTAL_PAGES}") + + add_spacer(doc, 132) + add_kicker( + doc, + "Trend Report", + color=GOLD, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=18, + ) + add_title( + doc, + "Pour With Intention", + size=30, + color=RGBColor(32, 55, 72), + align=WD_ALIGN_PARAGRAPH.CENTER, + after=8, + ) + add_subtitle( + doc, + "How Neighborhood Coffee Shops Can Use AI", + size=15, + color=RGBColor(43, 81, 99), + align=WD_ALIGN_PARAGRAPH.CENTER, + after=2, + ) + add_subtitle( + doc, + "Without Losing Their Local Voice", + size=15, + color=RGBColor(43, 81, 99), + align=WD_ALIGN_PARAGRAPH.CENTER, + after=28, + ) + add_para( + doc, + "- An Independent Operator's Field Guide -", + size=10.5, + color=GOLD, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=88, + ) + add_para( + doc, + "April 2026", + size=12, + bold=True, + color=RGBColor(32, 55, 72), + align=WD_ALIGN_PARAGRAPH.CENTER, + after=4, + ) + add_para( + doc, + "Prepared for independent cafe owners and small-chain operators", + size=9.5, + italic=True, + color=RGBColor(80, 80, 80), + align=WD_ALIGN_PARAGRAPH.CENTER, + after=22, + ) +``` + +## `customer_pack` + +```python +def page_customer_pack(doc, section): + set_section_header( + section, + "Partner Pack", + "Acme Onboarding Enablement", + color=MUTED, + rule=True, + rule_color="D7DBE2", + ) + set_section_footer(section, f"Page 4 of {TOTAL_PAGES}") + + add_kicker(doc, "Customer Enablement Pack", color=GOLD, after=0) + add_title(doc, "Acme Onboarding Sprint", size=31, color=NAVY, after=8) + add_subtitle( + doc, + "A partner-ready packet for kickoff, enablement, and success planning.", + size=13.5, + color=GRAY, + after=22, + ) + add_inline_metadata_grid( + doc, + section, + [ + ("Prepared for:", "Acme Revenue Operations"), + ("Prepared by:", "Customer Success and Solutions"), + ], + [ + ("Engagement:", "Four-week onboarding sprint"), + ("Start:", "May 18, 2026"), + ], + left_weight=1.05, + right_weight=1.0, + ) +``` + +## `workshop_agenda` + +```python +def page_workshop_agenda(doc, section): + set_section_header( + section, + "Workshop Agenda", + "Design Sprint - Day 1", + color=MUTED, + rule=False, + ) + set_section_footer(section, f"Page 5 of {TOTAL_PAGES}") + + add_kicker(doc, "Workshop Agenda", color=BLUE, after=0) + add_title(doc, "AI Support Design Sprint", size=29, color=NAVY, after=8) + add_subtitle( + doc, + "A session-first intro for agendas, offsites, working sessions, and trainings.", + size=13.2, + color=GRAY, + after=18, + ) + add_metric_strip( + doc, + section, + [ + ("9:00", "Context and goals"), + ("10:15", "Customer journey map"), + ("1:00", "Concept sketches"), + ("3:30", "Decision readout"), + ], + fill="FFF8E8", + accent=GOLD, + ) +``` + +## `customer_story` + +```python +def page_customer_story(doc, section): + set_section_header( + section, + "Customer Story", + "Northstar Retail - Draft", + color=MUTED, + rule=True, + rule_color="D8D3C7", + ) + set_section_footer(section, f"Page 6 of {TOTAL_PAGES}") + + add_kicker(doc, "Customer Story", color=GOLD, after=0) + add_title( + doc, + "How Northstar Retail Cut Support Escalations by 31%", + size=28, + color=NAVY, + after=12, + ) + add_subtitle( + doc, + "A narrative intro for case studies, success stories, and customer-facing proof.", + size=13.2, + color=GRAY, + after=22, + ) + add_para( + doc, + '"We needed a support experience that helped store teams move faster without losing the human tone our customers expect."', + size=14, + color=RGBColor(70, 70, 70), + italic=True, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=18, + ) + add_para( + doc, + "- VP Customer Experience, Northstar Retail", + size=9.5, + color=GOLD, + bold=True, + align=WD_ALIGN_PARAGRAPH.CENTER, + after=24, + ) +``` diff --git a/App/memmy-agent/src/skills/docx/references/manifest.txt b/App/memmy-agent/src/skills/docx/references/manifest.txt new file mode 100644 index 000000000..043c52630 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/manifest.txt @@ -0,0 +1,75 @@ +SKILL.md +scripts/render_docx.py +references/manifest.txt +references/design_presets.md +references/header_templates.md +references/template-create.md +references/template-distill.md +references/examples/end_to_end_smoke_test.md +references/ooxml/comments.md +references/ooxml/hyperlinks_and_fields.md +references/ooxml/rels_and_content_types.md +references/ooxml/tracked_changes.md +references/tasks/accessibility_a11y.md +references/tasks/captions_crossrefs.md +references/tasks/clean_tracked_changes.md +references/tasks/comments_manage.md +references/tasks/compare_diff.md +references/tasks/create_edit.md +references/tasks/fields_update.md +references/tasks/fixtures_edge_cases.md +references/tasks/footnotes_endnotes.md +references/tasks/forms_content_controls.md +references/tasks/headings_numbering.md +references/tasks/images_figures.md +references/tasks/multi_doc_merge.md +references/tasks/navigation_internal_links.md +references/tasks/privacy_scrub_metadata.md +references/tasks/protection_restrict_editing.md +references/tasks/read_review.md +references/tasks/redaction_anonymization.md +references/tasks/sections_layout.md +references/tasks/style_lint_normalize.md +references/tasks/tables_spreadsheets.md +references/tasks/templates_style_packs.md +references/tasks/toc_workflow.md +references/tasks/verify_render.md +references/tasks/watermarks_background.md +references/troubleshooting/libreoffice_headless.md +references/troubleshooting/run_splitting.md +scripts/a11y_audit.py +scripts/accept_tracked_changes.py +scripts/add_tracked_replacements.py +scripts/apply_template_styles.py +scripts/captions_and_crossrefs.py +scripts/comments_add.py +scripts/comments_apply_patch.py +scripts/comments_extract.py +scripts/comments_strip.py +scripts/content_controls.py +scripts/docx_ooxml_patch.py +scripts/docx_table_to_csv.py +scripts/fields_materialize.py +scripts/fields_report.py +scripts/flatten_ref_fields.py +scripts/footnotes_report.py +scripts/heading_audit.py +scripts/images_audit.py +scripts/insert_note.py +scripts/insert_ref_fields.py +scripts/insert_toc.py +scripts/internal_nav.py +scripts/make_fixtures.py +scripts/mark_artifact_operation_started.mjs +scripts/merge_docx_append.py +scripts/privacy_scrub.py +scripts/redact_docx.py +scripts/render_and_diff.py +scripts/section_audit.py +scripts/set_protection.py +scripts/style_lint.py +scripts/style_normalize.py +scripts/table_geometry.py +scripts/watermark_add.py +scripts/watermark_audit_remove.py +scripts/xlsx_to_docx_table.py diff --git a/App/memmy-agent/src/skills/docx/references/ooxml/comments.md b/App/memmy-agent/src/skills/docx/references/ooxml/comments.md new file mode 100644 index 000000000..997d0f1e7 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/ooxml/comments.md @@ -0,0 +1,37 @@ +# OOXML: Word comments (true comments) + +## Important reality +**PDF/image rendering is not a reliable way to verify comments.** Comments often do not render at all in headless LibreOffice. + +If the task requires verifying comments, do a structural check. + +## Minimum wiring for a comment +A comment requires three cooperating pieces: + +1) `word/comments.xml` exists and contains a `` with the comment text. +2) `word/document.xml` contains anchors: + - `` + - `` + - a `` in a run after the range +3) Relationships + content-types: + - `word/_rels/document.xml.rels`: add a relationship of Type `http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments` targeting `comments.xml` + - `[Content_Types].xml`: add an Override for `/word/comments.xml` with ContentType `application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml` + +## Typical insertion strategy +- Identify the target paragraph or run range in `document.xml`. +- Insert `commentRangeStart` before the first run you want covered. +- Insert `commentRangeEnd` after the last run you want covered. +- Append a run containing `commentReference`. +- Create or append the comment body in `comments.xml`. + +## Recommended: use the helper script +See `scripts/docx_ooxml_patch.py` (`--add-comment`). It: +- auto-picks a non-colliding comment id by scanning the DOCX +- **appends** to `word/comments.xml` if it already exists (does not overwrite existing comments) +- reuses an existing comments relationship if present (avoids duplicate rels) + +## Structural verification checklist +- `comments.xml` present in the ZIP +- `document.xml.rels` has a comments relationship +- `[Content_Types].xml` includes the Override +- For each comment id, `document.xml` has start/end/reference anchors diff --git a/App/memmy-agent/src/skills/docx/references/ooxml/hyperlinks_and_fields.md b/App/memmy-agent/src/skills/docx/references/ooxml/hyperlinks_and_fields.md new file mode 100644 index 000000000..c3d32c209 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/ooxml/hyperlinks_and_fields.md @@ -0,0 +1,69 @@ +# OOXML: Hyperlinks, headers/footers, and fields (page numbers) + +This file covers the common "small but annoying" features that often require OOXML or low-level python-docx work. + +## Hyperlinks +### Reality +`python-docx` can create external hyperlink relationships but does not provide a high-level hyperlink API. The easiest path is to build the `` element manually. + +### Pattern (external hyperlink) +1) Create a relationship to the URL (Type = `RT.HYPERLINK`) +2) Insert a `` containing a run with a `` + +Minimal python-docx snippet: +```python +from docx.opc.constants import RELATIONSHIP_TYPE as RT +from docx.oxml import OxmlElement +from docx.oxml.ns import qn + +p = doc.paragraphs[0] +url = "https://example.com" +r_id = p.part.relate_to(url, RT.HYPERLINK, is_external=True) + +hyperlink = OxmlElement("w:hyperlink") +hyperlink.set(qn("r:id"), r_id) + +r = OxmlElement("w:r") +rPr = OxmlElement("w:rPr") +# optional styling (blue + underline) +color = OxmlElement("w:color"); color.set(qn("w:val"), "0000FF"); rPr.append(color) +u = OxmlElement("w:u"); u.set(qn("w:val"), "single"); rPr.append(u) +r.append(rPr) + +t = OxmlElement("w:t"); t.text = "link text"; r.append(t) +hyperlink.append(r) +p._p.append(hyperlink) +``` + +## Headers and footers +### Right-aligned date header +Most of the time python-docx is enough: +```python +from docx.enum.text import WD_ALIGN_PARAGRAPH +section = doc.sections[0] +hp = section.header.paragraphs[0] +hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT +hp.text = "Date: 01/05/2026" +``` + +### Footer left/center/right zones +A common reliable trick is a 1x3 table (remember: width required in headers/footers): +```python +from docx.shared import Inches +from docx.enum.text import WD_ALIGN_PARAGRAPH +footer = doc.sections[0].footer +table = footer.add_table(rows=1, cols=3, width=Inches(6.5)) +# set paragraph alignment per cell +``` + +## Page number field +### Reality +A PAGE field is a Word field code. Some renderers may show placeholder values in PDF unless fields are updated. + +### Pattern +Insert a field with `w:fldChar` begin/separate/end and an `w:instrText` of `PAGE`. + +See `scripts/docx_ooxml_patch.py` for helpers that add a centered page number field to the footer and add an external hyperlink. + +### Helper limitations (intentional) +The `--hyperlink-first` helper is pragmatic: it replaces the first paragraph with a single linked run. It does not preserve per-run formatting. It does preserve leading/trailing spaces via `xml:space="preserve"` when needed. diff --git a/App/memmy-agent/src/skills/docx/references/ooxml/rels_and_content_types.md b/App/memmy-agent/src/skills/docx/references/ooxml/rels_and_content_types.md new file mode 100644 index 000000000..c1481871a --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/ooxml/rels_and_content_types.md @@ -0,0 +1,37 @@ +# OOXML: Relationships and content types (the plumbing) + +These files are the most common reason a "patched" DOCX opens but features don't work. + +## Key files +- `word/_rels/document.xml.rels` (relationships for the main document) +- `[Content_Types].xml` (MIME types for parts) + +## Relationships: `word/_rels/document.xml.rels` +- Namespace: `http://schemas.openxmlformats.org/package/2006/relationships` +- Each `Relationship` has: + - `Id` (e.g., `rIdComments1`) + - `Type` (e.g., comments, hyperlinks, footer) + - `Target` (e.g., `comments.xml`) + +Example relationship for comments: +```xml + +``` + +## Content types: `[Content_Types].xml` +- Namespace: `http://schemas.openxmlformats.org/package/2006/content-types` +- For new parts (like `comments.xml`), add an `Override`: + +```xml + +``` + +## Troubleshooting checklist +If Word says the doc is corrupted or features don't appear: +- Check that the part exists in the ZIP at the expected path +- Check `document.xml.rels` has the correct `Type` and `Target` +- Check `[Content_Types].xml` contains the `Override` +- Check namespace prefixes are correct (Word is picky) diff --git a/App/memmy-agent/src/skills/docx/references/ooxml/tracked_changes.md b/App/memmy-agent/src/skills/docx/references/ooxml/tracked_changes.md new file mode 100644 index 000000000..1a38a1061 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/ooxml/tracked_changes.md @@ -0,0 +1,41 @@ +# OOXML: Tracked changes (true redlines) + +## When to use +Use OOXML patching when the user needs *real* Word tracked changes, i.e. redlines that appear as insertions/deletions in Word. + +`python-docx` does **not** provide a first-class API for tracked changes. + +## Minimum wiring +Tracked changes typically involve: +- `word/settings.xml`: add `` to enable tracking mode +- `word/document.xml`: wrap inserted runs with `` and deletions with `` + +## Key rules (to avoid broken docs) +- IDs: `w:id` should be an integer string and **must not collide** with existing ids in the document +- This skill records `w:date` for chronology and intentionally omits reviewer identity metadata. +- Deletions must use `` (not ``) inside `` +- Word can split text into many runs; operate at run granularity + +## Example pattern: replace a word via tracked delete + tracked insert +Pseudo-structure: + +```xml + + old text + + + new text + +``` + +## Recommended: use the helper script +See `scripts/docx_ooxml_patch.py` for a runnable patcher that: +- enables `` +- converts an existing `` to `` and inserts a new `` + +The CLI defaults to auto-generated `w:id` values (`--del-id auto --ins-id auto`) by scanning existing ids and choosing new ones. + +## Verification +- Render to PDF/PNG for layout sanity (`references/tasks/verify_render.md`) +- Confirm Word shows the change as tracked +- Be aware: renders usually show redlines, but always verify the OOXML is correct too diff --git a/App/memmy-agent/src/skills/docx/references/tasks/accessibility_a11y.md b/App/memmy-agent/src/skills/docx/references/tasks/accessibility_a11y.md new file mode 100644 index 000000000..e6341c129 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/accessibility_a11y.md @@ -0,0 +1,55 @@ +# Accessibility (A11y) Audit + Quick Fixes + +## Goal +Given a `.docx`, produce an **accessibility audit report** and (optionally) apply **safe, mechanical fixes** that reduce common A11y failures. + +This is **not** a full WCAG compliance engine. It targets the highest-ROI checks you can do reliably in OOXML: +- Heading hierarchy (no skipping levels) +- Images missing alt text (`descr`) +- Tables missing a header row flag +- Hyperlink text that is non-descriptive ("click here", raw URLs) + +## Audit +```bash +python scripts/a11y_audit.py input.docx +``` + +This prints a JSON-ish report to stdout and exits non-zero if **high severity** issues exist. + +To write the report to a file instead: +```bash +python scripts/a11y_audit.py input.docx --out_json a11y_report.json +``` + +## Apply quick fixes (optional) +### 1) Fill missing image alt text using filenames +This is a pragmatic baseline that is better than empty alt text. +```bash +python scripts/a11y_audit.py input.docx --fix_image_alt from_filename --out a11y_fixed.docx +``` + +### 2) Mark first row as a table header +Only do this when the first row *is actually* a header. +```bash +python scripts/a11y_audit.py input.docx --fix_table_headers first_row --out a11y_fixed.docx +``` + +You can combine fixes: +```bash +python scripts/a11y_audit.py input.docx \ + --fix_image_alt from_filename \ + --fix_table_headers first_row \ + --out a11y_fixed.docx +``` + +## Verification loop +1) Apply fixes (if any) +2) **Render → inspect PNGs** to confirm nothing drifted visually: +```bash +python scripts/render_docx.py a11y_fixed.docx --output_dir out_a11y +``` + +## Pitfalls +- "Fixing" headings is rarely mechanical; it usually requires editorial judgement. This tool **reports** heading issues but does not rewrite styles. +- Setting table header flags can change repeated header rendering across page breaks. Always re-render and review. +- Alt text generated from filenames is a baseline; replace it with meaningful descriptions for real accessibility. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/captions_crossrefs.md b/App/memmy-agent/src/skills/docx/references/tasks/captions_crossrefs.md new file mode 100644 index 000000000..b5d04ff3c --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/captions_crossrefs.md @@ -0,0 +1,97 @@ +# Task: Captions + cross-references (SEQ + REF) + +## When to use +Use this when the user wants: +- **Figure/Table captions** ("Figure 1", "Table 2"…) +- **Cross-references** ("see Figure 3") +- **Stable numbering** for headless rendering/QA + +Word implements captions/cross-references using fields: +- `SEQ` for numbering (e.g., `SEQ Table` / `SEQ Figure`) +- `REF` to reference a bookmark (cross-reference target) + +`python-docx` does not provide a high-level API for these fields, so this bundle uses OOXML-level helpers: +- `scripts/captions_and_crossrefs.py` — insert caption paragraphs + optional bookmarks around the caption number +- `scripts/insert_ref_fields.py` — replace `[[REF:bookmark]]` markers with real `REF` fields +- `scripts/fields_materialize.py` — materialize `SEQ/REF` *display text* so headless renders show the correct numbers + +## The practical gotcha +Fields **do not reliably update** in headless environments. If you only insert field codes (`SEQ`/`REF`), the rendered number may be blank or stale. + +For deterministic automation / QA, the reliable pattern is: +1) insert field codes, then +2) **materialize** the field display text + +A human can still open the document later and update fields, but for automation you want deterministic visuals. + +--- + +## Workflow + +### 1) Add captions (and bookmarks) +This adds captions for tables and/or figures that don't already have a `Caption` paragraph immediately after them. + +```bash +python scripts/captions_and_crossrefs.py \ + /mnt/data/in.docx \ + /mnt/data/with_captions.docx \ + --tables --figures \ + --caption_text "Caption" \ + --bookmarks +``` + +What it does: +- Inserts a `Caption`-styled paragraph after each table / figure paragraph. +- Uses a `SEQ Table` / `SEQ Figure` field for numbering. +- If `--bookmarks` is set, wraps the *caption number* in a bookmark: + - tables: `tbl1`, `tbl2`, … + - figures: `fig1`, `fig2`, … + +### 2) Insert cross-references (REF) +**Authoring trick:** put explicit markers into the doc where you want a cross-ref, e.g. +- `See [[REF:tbl1]] for details.` +- `As shown in [[REF:fig1]] …` + +Then replace markers with real `REF` fields: + +```bash +python scripts/insert_ref_fields.py \ + /mnt/data/with_captions.docx \ + /mnt/data/with_refs.docx +``` + +Notes: +- This script replaces markers in `document.xml` and headers/footers. +- Multiple `[[REF:...]]` markers inside a single text run are supported. +- **Limitation:** the marker must be fully contained in a single text run (``). If Word split the marker across runs, retype it as a single contiguous token. + +### 3) Materialize (freeze) SEQ/REF results for deterministic renders +```bash +python scripts/fields_materialize.py \ + /mnt/data/with_refs.docx \ + --out /mnt/data/with_refs_materialized.docx +``` + +Implementation note: `fields_materialize.py` materializes `SEQ` values before `REF` values so cross-references see the updated caption numbers. + +If you only want to materialize one type: +```bash +python scripts/fields_materialize.py /mnt/data/with_refs.docx --out /mnt/data/out.docx --only REF +``` + +### 4) Render and visually QA +```bash +python scripts/render_docx.py /mnt/data/with_refs_materialized.docx --output_dir /mnt/data/out_caps +``` +Inspect the PNGs. + +--- + +## Pitfalls / tips +- **Caption style availability:** if the document doesn’t define a `Caption` style, captions may appear as Normal text. If the user cares, apply a template/style pack first. +- **Figures detection:** this script treats paragraphs containing a ``/`` as a "figure paragraph". +- **Edits after materializing:** if you insert/remove figures/tables later, re-run `fields_materialize.py` to recompute numbering. + +## Deliverables +- Deliver **only the final DOCX** requested by the user. +- PNGs / optional PDFs are internal QA only unless explicitly requested. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/clean_tracked_changes.md b/App/memmy-agent/src/skills/docx/references/tasks/clean_tracked_changes.md new file mode 100644 index 000000000..b1a02b6a5 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/clean_tracked_changes.md @@ -0,0 +1,44 @@ +# Accept / Reject Tracked Changes (produce a clean DOCX) + +## Goal +Given a `.docx` with tracked changes (redlines), produce a **final clean** copy with changes accepted (or rejected), and verify visually. + +## What tracked changes are (OOXML) +Tracked changes are usually wrappers in `word/document.xml`: +- `w:ins` — inserted content +- `w:del` — deleted content +- sometimes: `w:moveTo` / `w:moveFrom` for moved text + +Many “looks wrong” reports come from: +- leaving revisions in place (Word shows redlines; LO export may hide/show inconsistently) +- stale renders (you didn’t re-render after patching) + +## Steps +1. **Inspect** how many revisions exist: + ```bash + python scripts/accept_tracked_changes.py input.docx --mode report + ``` +2. **Accept** all tracked changes into a clean copy: + ```bash + python scripts/accept_tracked_changes.py input.docx --mode accept --out accepted.docx + ``` + (Or reject): + ```bash + python scripts/accept_tracked_changes.py input.docx --mode reject --out rejected.docx + ``` +3. **Render → PNG review** (required): + ```bash + python scripts/render_docx.py accepted.docx --output_dir out_accept + ``` + +## Render → PNG review checklist +Open all `out_accept/page-*.png` at 100% zoom: +- No redlines/strikethrough remain +- No missing words (especially around the edited region) +- No spacing drift caused by removed wrappers +- Headers/footers still correct + +## Pitfalls +- This is a pragmatic helper, not a perfect Word revision engine. +- Always re-run `--mode report` on the output; it should be zero. +- If Word-specific revision constructs remain, fall back to “Open in Word → Accept All → Save As” and re-render. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/comments_manage.md b/App/memmy-agent/src/skills/docx/references/tasks/comments_manage.md new file mode 100644 index 000000000..b9166910a --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/comments_manage.md @@ -0,0 +1,71 @@ +# Comments: Extract, Remove, or Preserve for Review + +## Goal +Handle reviewer comments in a `.docx` without confusing intermediate artifacts. + +Common situations: +- **Review mode**: keep comments and deliver a commented `.docx`. +- **Final mode**: remove comments (and optionally accept tracked changes) and deliver a clean `.docx`. +- **Triage mode**: extract comments into a machine-readable report (JSON/Markdown) for summarization. + +> Word "resolved" state is not reliably round-trippable with `python-docx` alone. This skill focuses on **reliable** operations. + +## Adding comments (true Word comments) +If the task is to *insert* new comments (not just extract/strip), use the OOXML-level guide: `references/ooxml/comments.md` (via `scripts/docx_ooxml_patch.py`). + +## Add comments at scale (review mode) +For programmatic review injection (multiple comments across the document), use: +```bash +python scripts/comments_add.py input.docx --out reviewed.docx --add "Payment Terms=Please confirm Net 45 is acceptable." --add "Governing Law=Prefer Delaware; any constraints?" --ignore_case +``` +Notes: +- Matching looks across normal text **and** deleted text (`w:delText`), so it can still find anchors in docs with tracked changes. +- The script warns on patterns with no matches; add `--require_all` to fail fast. + +## Patch / resolve existing comments +For updating or marking comments as resolved: +```bash +python scripts/comments_extract.py reviewed.docx --out comments.json + +# Create a separate patch file (JSON). Example: +# { +# "ops": [ +# {"id": 0, "append": "Follow-up note"}, +# {"id": 0, "replace": "Full replacement text"}, +# {"id": 0, "resolved": true} +# ] +# } +# (Set "resolved": false to clear the resolved state.) + +python scripts/comments_apply_patch.py reviewed.docx patch.json --out reviewed_v2.docx +``` + + + +## Extract comments (triage) +Produces JSON with comment text, author, date (if present), and the anchored snippet. +```bash +python scripts/comments_extract.py input.docx --out comments.json +``` + +## Remove all comments (final mode) +This removes: +- comment ranges and references in story parts (main doc + headers/footers) +- `word/comments.xml` and any comment-related relationships / content type overrides + +```bash +python scripts/comments_strip.py input.docx --out no_comments.docx +``` + +## Recommended finalize workflow +If the requested deliverable is a **clean final DOCX**: +```bash +python scripts/accept_tracked_changes.py input.docx --mode accept --out accepted.docx +python scripts/comments_strip.py accepted.docx --out final_clean.docx +python scripts/render_docx.py final_clean.docx --output_dir out_final_clean +``` + +## Pitfalls +- Comments can be anchored in headers/footers too; always strip across all story parts. +- Some docs include `commentsExtended.xml` (newer Word). This script removes it if present. +- After stripping, render PNGs and verify nothing disappeared around comment anchors. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/compare_diff.md b/App/memmy-agent/src/skills/docx/references/tasks/compare_diff.md new file mode 100644 index 000000000..aa50e1864 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/compare_diff.md @@ -0,0 +1,32 @@ +# Compare / Diff Two DOCXs (visual + structural) + +## Goal +Given two `.docx` files, produce an easy QA bundle: +- renders of both docs (`page-*.png`) +- per-page diff images for changed pages +- a text-level unified diff + +This is high-ROI for regression testing and reviewer confidence. + +## Steps +1. Run the helper: + ```bash + python scripts/render_and_diff.py a.docx b.docx --outdir diff_out + ``` + +2. Inspect: + - `diff_out/a_render/page-*.png` + - `diff_out/b_render/page-*.png` + - `diff_out/diff_pages/diff-page-*.png` + - `diff_out/text_diff.txt` + +## Render → PNG review checklist +- Confirm page counts match expectations +- Open **each changed page** in both A and B at 100% zoom +- Verify the visual diff highlights only intended changes +- Spot-check unchanged pages if the edit is layout-sensitive (tables/images/sections) + +## Pitfalls +- LO headless rendering can differ from Word; this tool catches visual diffs in *your* render loop, which is what you ship. +- If pagination differs, many pages may show as changed. Use the text diff to confirm content-level changes. +- If you see changes that are *only* anti-aliasing noise, increase render DPI and re-run. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/create_edit.md b/App/memmy-agent/src/skills/docx/references/tasks/create_edit.md new file mode 100644 index 000000000..f119ac88b --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/create_edit.md @@ -0,0 +1,51 @@ +# Task: Create / edit a DOCX + +## Default tool: python-docx +Use `python-docx` for: +- paragraphs/runs +- built-in heading styles (Heading 1 / Heading 2) +- tables (structure + cell text + basic formatting) +- simple headers/footers and margins + +For a new document, choose and resolve a design preset before drafting. Set +title, heading, body, list, table, header, and footer styles explicitly rather +than relying on renderer defaults. See `references/design_presets.md`. + +## Practical python-docx gotchas + +### 1) Header/footer tables require a width +When adding tables to headers/footers, `add_table` requires an explicit width: + +```python +from docx.shared import Inches +from docx.enum.text import WD_ALIGN_PARAGRAPH + +section = doc.sections[0] +footer = section.footer +table = footer.add_table(rows=1, cols=3, width=Inches(6.5)) +# Align text inside each cell +table.rows[0].cells[0].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT +``` + +### 2) Fonts can require setting both `run.font.name` and `w:rFonts` +Some renderers/Word builds don’t respect only `run.font.name`: + +```python +from docx.oxml.ns import qn + +run.font.name = "Gill Sans" +run._element.rPr.rFonts.set(qn("w:ascii"), "Gill Sans") +run._element.rPr.rFonts.set(qn("w:hAnsi"), "Gill Sans") +``` + +### 3) “Clear header paragraph” isn’t always one call +If you need to replace an existing header paragraph, remove runs (or replace the paragraph XML). Avoid assuming a `clear()` method exists. + +### 4) Tracked changes and comments are not first-class +If the user requests *real* tracked changes or *real* Word comments, plan for OOXML patching (see `references/ooxml/`). + +## After every meaningful batch of edits: render and review +Use the loop from `references/tasks/verify_render.md` (DOCX → PNG) to avoid shipping layout defects. (Internally the renderer uses a PDF step; `--emit_pdf` can persist it if needed.) + +## Output hygiene +Keep `/mnt/data` clean: deliverables only unless the user asks for intermediate render artifacts. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/fields_update.md b/App/memmy-agent/src/skills/docx/references/tasks/fields_update.md new file mode 100644 index 000000000..aa84c5e54 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/fields_update.md @@ -0,0 +1,66 @@ +# Task: Fields + update behavior (TOC / page # / refs) + +## Goal +Avoid "looks wrong" renders that are actually **stale Word fields**. + +Common fields: +- `PAGE` — current page number +- `NUMPAGES` — total page count +- `TOC` — table of contents +- `REF` / `PAGEREF` — cross references (often "see page X") + +## When this matters +- PDF/PNG render shows placeholders (e.g., TOC looks empty, refs show wrong page, page numbers all “1”). +- The doc was modified programmatically (python-docx / OOXML patch) and then exported without a field refresh. +- LibreOffice vs Word disagree. + +## What to do +### 1) Scan for fields +Run a quick field inventory: + +```bash +python scripts/fields_report.py /mnt/data/input.docx +``` + +If you see `TOC`, `REF`, `PAGEREF`, `NUMPAGES`, or `PAGE`, plan for a field refresh step. + +### 2) Render and inspect + +```bash +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out +``` + +Inspect all `page-*.png` at 100% zoom. + +### 3) If anything is wrong: update fields in a GUI editor +**Fast checklist (Word):** +1. Open the DOCX in **Microsoft Word** +2. `Ctrl+A` (select all) +3. `F9` (Update Fields) +4. Save +5. Re-render with `render_docx.py` + +LibreOffice (GUI) can also update fields, but Word is the reference implementation. + +## Deterministic rendering workaround (when you can't update fields) +If your goal is **stable PNG regression testing** (not perfect Word semantics), you can +*materialize* some field results into literal text so headless renders won't omit them: + +```bash +# Replace REF/PAGEREF blocks with their currently cached visible text +python scripts/flatten_ref_fields.py input.docx --out ref_flattened.docx + +# Materialize SEQ/REF results (e.g., caption numbers / cross-refs) +python scripts/fields_materialize.py ref_flattened.docx --out fields_materialized.docx +``` + +Notes: +- This does **not** refresh TOC/PAGE/NUMPAGES; those still typically require Word/LO GUI. +- Always render and visually verify after materialization. + +## Render → PNG review checklist (fields) +- Page numbers increment correctly (footer/header) +- Total page count (`NUMPAGES`) matches the rendered page count +- TOC entries exist, have correct indentation, and page numbers match headings +- Cross references (`REF`/`PAGEREF`) resolve (no "Error! Reference source not found.") +- No placeholder text like “(TOC will populate...)” remains diff --git a/App/memmy-agent/src/skills/docx/references/tasks/fixtures_edge_cases.md b/App/memmy-agent/src/skills/docx/references/tasks/fixtures_edge_cases.md new file mode 100644 index 000000000..cfc88e488 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/fixtures_edge_cases.md @@ -0,0 +1,42 @@ +# Repro Fixtures for Edge Cases (Tracked Changes, Watermarks) + +## Goal +Generate small deterministic `.docx` fixtures that exercise known tricky OOXML patterns so you can test helper scripts without hand-editing XML. + +## Why this is useful +- Tracked changes and watermarks are frequently the source of "looks wrong" issues. +- Reproducing them manually is slow and error-prone. +- A standard fixture makes smoke tests and future debugging much faster. + +## Generate fixtures +```bash +python scripts/make_fixtures.py --outdir fixtures +``` +Produces: +- `fixtures/tracked_changes_fixture.docx` +- `fixtures/watermark_fixture.docx` + +Or generate a single fixture: +```bash +python scripts/make_fixtures.py --outdir fixtures --only tracked +python scripts/make_fixtures.py --outdir fixtures --only watermark +``` + +## How to use fixtures +### Tracked changes +```bash +python scripts/accept_tracked_changes.py fixtures/tracked_changes_fixture.docx --mode report +python scripts/accept_tracked_changes.py fixtures/tracked_changes_fixture.docx --mode accept --out accepted.docx +python scripts/render_docx.py accepted.docx --output_dir out_accepted +``` + +### Watermarks +```bash +python scripts/watermark_audit_remove.py fixtures/watermark_fixture.docx --mode report +python scripts/watermark_audit_remove.py fixtures/watermark_fixture.docx --mode remove --contains DRAFT --out no_watermark.docx +python scripts/render_and_diff.py fixtures/watermark_fixture.docx no_watermark.docx --outdir diff_watermark +``` + +## Render → PNG review checklist +- Tracked-changes fixture: redlines appear in the original, and are gone in the accepted output +- Watermark fixture: `report` finds watermark-like VML; removal yields zero hits; headers remain intact diff --git a/App/memmy-agent/src/skills/docx/references/tasks/footnotes_endnotes.md b/App/memmy-agent/src/skills/docx/references/tasks/footnotes_endnotes.md new file mode 100644 index 000000000..efe3f15fc --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/footnotes_endnotes.md @@ -0,0 +1,52 @@ +# True Footnotes / Endnotes (OOXML parts, numbering, refs) + +## Goal +Add or audit **true** footnotes/endnotes in a `.docx` and verify they render correctly. + +## What footnotes/endnotes are (OOXML) +Footnotes and endnotes are **not** "text in the footer." They live in separate parts: +- `word/footnotes.xml` +- `word/endnotes.xml` + +And the body refers to them using references: +- `w:footnoteReference w:id="N"` +- `w:endnoteReference w:id="N"` + +The note parts also contain required separators (`w:id=-1` and `w:id=0`). + +## Audit +Use the reporter to see what a doc contains: +```bash +python scripts/footnotes_report.py input.docx +``` + +## Insert a note (minimal helper) +This repo includes `insert_note.py` which patches OOXML to insert a note. + +1. Add a marker into the document where you want the reference: +- `[[FN]]` for a footnote +- `[[EN]]` for an endnote + +2. Insert the note: +```bash +python scripts/insert_note.py input.docx --kind footnote --marker "[[FN]]" --text "Footnote text" --out with_fn.docx +python scripts/insert_note.py input.docx --kind endnote --marker "[[EN]]" --text "Endnote text" --out with_en.docx +``` + +3. Render → PNG review: +```bash +python scripts/render_docx.py with_fn.docx --output_dir out_fn +``` + +## Render → PNG review checklist +- Footnote/endnote marker appears in the body where expected +- Footnote text appears at page bottom (footnotes) or note section (endnotes) +- Numbering is correct (no duplicates, starts at 1) +- Long notes wrap nicely (no overlap/clipping) + +## Pitfalls +- Some consumers are strict about separator entries in footnotes.xml/endnotes.xml. +- If the marker appears but note text doesn't, run `footnotes_report.py` to confirm: + - reference IDs exist in `document.xml` + - note IDs exist in `footnotes.xml`/`endnotes.xml` +- For high-stakes deliverables, verify in Microsoft Word in addition to LO rendering. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/forms_content_controls.md b/App/memmy-agent/src/skills/docx/references/tasks/forms_content_controls.md new file mode 100644 index 000000000..4e91807b4 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/forms_content_controls.md @@ -0,0 +1,49 @@ +# Task: Forms / content controls (SDTs) + +## When to use +Use this when the user wants a **fillable DOCX template** (fields, dropdowns, checkboxes) or when you need to **populate** an existing template that contains Word content controls. + +`python-docx` does not support SDTs. Use the helper script: +- `scripts/content_controls.py` + +This task doc focuses on **plain-text SDTs** (the most common case for templates). + +## Golden path +1. **Make placeholders visible (authoring step)** + - Write placeholders like `{{NAME}}`, `{{DATE}}`, `{{EMAIL}}` in the DOCX where values should go. + - If you control authoring, keep each placeholder contiguous (a single token). + +2. **Wrap placeholders into SDTs** +```bash +python scripts/content_controls.py /mnt/data/template.docx wrap_placeholders \ + --output /mnt/data/template_sdt.docx +``` + +3. **Populate SDTs by tag** +```bash +python scripts/content_controls.py /mnt/data/template_sdt.docx fill \ + --set NAME="Ada Lovelace" \ + --set EMAIL="ada@example.com" \ + --output /mnt/data/filled.docx +``` + +4. **Render for QA** +```bash +python scripts/render_docx.py /mnt/data/filled.docx --output_dir /mnt/data/out_forms +``` +Inspect `page-.png` at 100% zoom. + +## Listing / debugging +List all SDTs (tag, alias, visible text, part location): +```bash +python scripts/content_controls.py /mnt/data/template_sdt.docx list --json +``` + +## Pitfalls / lessons learned +- **Markers split across runs:** if Word splits `{{NAME}}` into multiple runs (common when styling is applied mid-token), the wrapper may miss it. Fix by retyping the placeholder so it is one contiguous token. +- **SDTs in footnotes/comments:** this helper patches document.xml + headers/footers. If a template uses SDTs in other parts, you may need a custom patch. +- **Rich content controls** (dropdown, checkbox, date picker): those require additional SDT properties/parts. This bundle does not attempt full fidelity. + +## Deliverables +- Deliver **only the final DOCX** requested by the user. +- PNGs / optional PDFs are for internal QA only unless explicitly requested. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/headings_numbering.md b/App/memmy-agent/src/skills/docx/references/tasks/headings_numbering.md new file mode 100644 index 000000000..760fd036b --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/headings_numbering.md @@ -0,0 +1,44 @@ +# Task: Heading hierarchy + multilevel numbering (H1/H2/H3) + +## Goal +Produce structured documents that are consistent, readable, and TOC-friendly. + +## Rules of thumb +1. **Use paragraph styles**, not direct formatting. + - Good: `p.style = doc.styles["Heading 1"]` + - Bad: make text 16pt bold in a Normal paragraph and hope it behaves like a heading. +2. Keep heading hierarchy consistent: don’t jump from Heading 1 → Heading 3 unless the document truly skips a level. +3. Numbered headings are *not* the same thing as bullet lists. If you need Word’s multilevel numbering, use a template where the numbering definitions already exist (DOTX), or accept that it’s brittle to generate from scratch. + +## Minimal python-docx patterns + +### Set heading styles +```python +from docx import Document + +doc = Document() +doc.add_paragraph("Executive Summary", style="Heading 1") +doc.add_paragraph("Background", style="Heading 2") +doc.add_paragraph("Prior Work", style="Heading 3") +doc.save("out.docx") +``` + +### Avoid direct formatting +If you must adjust typography, do it by editing the style definitions (template) rather than changing every paragraph. + +## Validate structure quickly +```bash +python scripts/heading_audit.py /mnt/data/input.docx +``` + +## Render → PNG review checklist (headings) +- Heading sizes/weights are consistent across the document +- Spacing before/after headings is consistent +- Indentation is consistent (especially for numbered headings) +- No "fake headings" (big bold Normal text) are used for actual sections +- TOC (if present) reflects heading hierarchy correctly + +## Common pitfalls +- Mixing manual numbering (“1. ” typed in text) with TOC-generated numbering +- Using Normal paragraphs with bold/size changes instead of Heading styles +- Having different documents disagree on what Heading 1/2/3 look like (solve with templates) diff --git a/App/memmy-agent/src/skills/docx/references/tasks/images_figures.md b/App/memmy-agent/src/skills/docx/references/tasks/images_figures.md new file mode 100644 index 000000000..52dd15342 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/images_figures.md @@ -0,0 +1,35 @@ +# Task: Images/figures placement + anchoring pitfalls + +## Goal +Keep images and captions where you expect across Word/LibreOffice/PDF exports. + +## Key reality +Image placement is the #1 LO-vs-Word mismatch. + +## Inline vs floating +- **Inline** (`wp:inline`): behaves like a big character in the text flow. Most reliable for automation. +- **Floating/anchored** (`wp:anchor`): supports text wrapping, precise positioning, and "keep with paragraph" effects — also most likely to render differently between apps. + +## Recommendations +1. Prefer **inline** images for automation unless you truly need wrap-around. +2. Use high-resolution sources and let Word scale down (avoid scaling up low-DPI images). +3. Keep a caption in a separate paragraph immediately after the image. + +## Audit +```bash +python scripts/images_audit.py /mnt/data/input.docx +``` + +If you see `anchor` rows, treat as high-risk and inspect renders closely. + +## Render → PNG review checklist (images) +- Images appear on the intended page(s) +- No overlap with text, tables, or margins +- Captions remain adjacent to their figures +- Images aren’t blurry/pixelated (zoom to 200% to check) +- No unexpected cropping/stretching + +## Common pitfalls +- Floating images shifting pages after small text edits +- Wrap modes causing overlap in LibreOffice exports +- Copy/pasted images with huge DPI metadata leading to surprising sizes \ No newline at end of file diff --git a/App/memmy-agent/src/skills/docx/references/tasks/multi_doc_merge.md b/App/memmy-agent/src/skills/docx/references/tasks/multi_doc_merge.md new file mode 100644 index 000000000..ed3ab726f --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/multi_doc_merge.md @@ -0,0 +1,31 @@ +# Merge DOCXs (Append Body Content) + +## Goal +Append the body content of one `.docx` to another **while preserving OOXML fidelity** better than text-only copying. + +This helper is intentionally scoped: +- **Preserves** paragraphs, runs, tables, numbering, and most formatting *inside the body*. +- **Does not** merge headers/footers/section settings across documents (it keeps the base document's section settings). +- **Does not** copy relationships for images/objects by default (safe). You can enable a looser mode if you know both docs are text-only. + +## Append doc B to doc A +```bash +python scripts/merge_docx_append.py base.docx append.docx --out merged.docx +``` + +## Allow drawings/images (optional, less safe) +If you know both documents have compatible relationships and you are okay with best-effort behavior: +```bash +python scripts/merge_docx_append.py base.docx append.docx --out merged.docx --allow_drawings +``` + +## Verify +Always render and inspect: +```bash +python scripts/render_docx.py merged.docx --output_dir out_merged +``` + +## Pitfalls +- If `append.docx` contains images or embedded objects, merging body XML alone is **not sufficient** unless you also merge relationships and binary parts. This script defaults to **refusing** drawings unless `--allow_drawings` is set. +- If styles/numbers in `append.docx` rely on definitions absent from `base.docx`, Word may substitute defaults. +- If either document contains tracked changes or comments, merge first *then* run the tracked-changes / comments tasks. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/navigation_internal_links.md b/App/memmy-agent/src/skills/docx/references/tasks/navigation_internal_links.md new file mode 100644 index 000000000..08c776ad4 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/navigation_internal_links.md @@ -0,0 +1,62 @@ +# Task: Internal navigation links (Top/Bottom/TOC + jump links) + +## Goal +Create deterministic **internal hyperlinks** inside a DOCX so readers can quickly jump: +- TOC entry → section +- section header → back to TOC +- quick links → Top / Bottom +- quick links → Figure/Table caption numbers (when bookmarks exist) + +This is especially useful for long reports, specs, or demo/QA artifacts. + +## Key idea +Word internal links are `w:hyperlink w:anchor=""`. + +This bundle provides `scripts/internal_nav.py` to add: +- `TOC` bookmark + a **static** TOC section (no Word field required) +- `Top` / `Bottom` bookmarks +- bookmarks on headings (either `Heading 1/2/3` styles **or** `w:outlineLvl`) +- "Back to TOC" links on each heading +- a quick-links bar (Top/Bottom/TOC + figN/tblN if present) + +## Workflow + +### Option A: Deterministic (headless-safe) static TOC + links + +1) Ensure your document has "headings". + +Prefer real heading styles (`Heading 1/2/3`). If the document doesn't use heading styles, a practical alternative is setting `w:outlineLvl` on the heading paragraphs (outline level 0 == top-level). This can be done via OOXML patching. + +2) (Optional) Add figure/table caption bookmarks first + +If you want jump links for figures/tables, run: + +```bash +python scripts/captions_and_crossrefs.py /mnt/data/in.docx /mnt/data/with_caps.docx --figures --tables --bookmarks +``` + +3) Add navigation + +```bash +python scripts/internal_nav.py /mnt/data/with_caps.docx --out /mnt/data/with_nav.docx +``` + +4) Render and verify + +```bash +python scripts/render_docx.py /mnt/data/with_nav.docx --output_dir /mnt/data/out_nav +``` + +Verify: +- TOC entries jump to the intended headings +- each heading has a working "Back to TOC" link +- Top/Bottom work +- figN/tblN links appear if bookmarks exist + +### Option B: Word-native TOC field (requires a field update) + +If you need a true Word TOC with page numbers, use `references/tasks/toc_workflow.md`. + +## Deliverables +- Internal navigation is part of the final DOCX. +- Rendered PNGs (and optional PDFs) are **internal QA only** unless the user explicitly asks for them. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/privacy_scrub_metadata.md b/App/memmy-agent/src/skills/docx/references/tasks/privacy_scrub_metadata.md new file mode 100644 index 000000000..ab47f6c34 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/privacy_scrub_metadata.md @@ -0,0 +1,24 @@ +# Privacy Scrub (Remove Personal Metadata) + +## Goal +Produce a `.docx` suitable for external sharing by removing common personal / +machine metadata: +- Core properties: creator, lastModifiedBy +- Custom properties: docProps/custom.xml (if present) +- Word revision session IDs (rsid* attributes) in story parts + +This does **not** remove semantic content (text/images) and does not redact PII in the content. For content redaction, use `redact_docx.py`. + +## Scrub a doc +```bash +python scripts/privacy_scrub.py input.docx --out scrubbed.docx +``` + +## Verify +```bash +python scripts/render_docx.py scrubbed.docx --output_dir out_scrubbed +``` + +## Pitfalls +- Some viewers may cache author info outside the file; always check the resulting `docProps/core.xml` if this is high-stakes. +- If you need to keep custom properties (e.g., templates), do not run this. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/protection_restrict_editing.md b/App/memmy-agent/src/skills/docx/references/tasks/protection_restrict_editing.md new file mode 100644 index 000000000..501138214 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/protection_restrict_editing.md @@ -0,0 +1,36 @@ +# Restrict Editing / Make Read-Only (Document Protection) + +## Goal +Set Word's **document protection** flags in `settings.xml` so a `.docx` opens as: +- read-only, or +- comments-only, or +- tracked-changes-only, or +- forms-only + +This is useful for: +- shipping a template that should not be casually modified +- forcing reviewers to comment instead of edit + +## Set protection mode +```bash +python scripts/set_protection.py input.docx --mode readOnly --out protected.docx +python scripts/set_protection.py input.docx --mode comments --out comments_only.docx +python scripts/set_protection.py input.docx --mode trackedChanges --out tc_only.docx +python scripts/set_protection.py input.docx --mode forms --out forms_only.docx +``` + +## Remove protection +```bash +python scripts/set_protection.py input.docx --mode off --out unprotected.docx +``` + +## Verification +Render to PNGs (layout should be unchanged): +```bash +python scripts/render_docx.py protected.docx --output_dir out_protected +``` + +## Pitfalls +- Protection is enforced by Word; some viewers may ignore it. +- Password protection is intentionally not implemented (high complexity, low ROI). +- Some docs may not have `word/settings.xml`; this helper creates it. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/read_review.md b/App/memmy-agent/src/skills/docx/references/tasks/read_review.md new file mode 100644 index 000000000..c9b6ae41c --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/read_review.md @@ -0,0 +1,60 @@ +# Task: Read / review an existing DOCX + +## What to review +- Layout: page breaks, margins, clipping/overlap +- Typography: heading hierarchy, font consistency, line spacing +- Tables/figures: alignment, legibility, truncation +- Redlines: do tracked insertions/deletions show up? +- Comments: do they exist (structurally), even if they don’t render? + +## Primary method: DOCX → PNG(s) (internally via PDF) + +### Preferred: use the packaged renderer +This is the recommended path because it creates an isolated LibreOffice profile and normalizes output names to `page-.png`. + +```bash +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out +# If debugging LibreOffice: +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out --verbose +# Optional: also write .pdf to --output_dir (for debugging/archival): +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out --emit_pdf +``` + +### Manual method (only if debugging) +Use a unique LibreOffice profile and writable HOME when debugging profile or permission issues: + +```bash +OUTDIR=/mnt/data/out +INPUT=/mnt/data/input.docx +BASENAME=$(basename "$INPUT" .docx) +LO_PROFILE=/mnt/data/.lo_profile_${BASENAME}_$$ +mkdir -p "$OUTDIR" "$LO_PROFILE" + +HOME="$LO_PROFILE" soffice --headless -env:UserInstallation=file://"$LO_PROFILE" \ + --convert-to pdf --outdir "$OUTDIR" "$INPUT" + +# Manual naming: produces "$OUTDIR/$BASENAME-1.png", "$OUTDIR/$BASENAME-2.png", ... +pdftoppm -png "$OUTDIR/$BASENAME.pdf" "$OUTDIR/$BASENAME" +``` + +### Success criteria +- Page images exist for each page +- Spot-check page count and representative pages + +**Note:** LibreOffice sometimes prints scary-looking stderr (e.g., `error : Unknown IO error`) even when output is correct. Prefer file existence + visual inspection over stderr content. + +### Visually inspect every page +Focus on: +- clipped/overlapping text +- tables that wrap unexpectedly +- inconsistent fonts/sizes +- misplaced headers/footers + +## Notes on redlines vs comments +- **Tracked changes** (insertions/deletions) often show up in PDF renders. +- **Comments frequently do NOT show up in PDF/image renders** (especially via headless LibreOffice). + - Rendering is not proof of comments. + - To verify comments, do a structural check (see `references/ooxml/comments.md`) or use `pandoc --track-changes=all` to confirm comment markup is present. + +## If the doc is huge +Render and inspect key pages first (title, TOC, sections with tables, appendices), then spot-check. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/redaction_anonymization.md b/App/memmy-agent/src/skills/docx/references/tasks/redaction_anonymization.md new file mode 100644 index 000000000..f946303c7 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/redaction_anonymization.md @@ -0,0 +1,57 @@ +# Task: Redaction / anonymization (layout-preserving) + +## When to use +Use this when the user wants to remove or anonymize sensitive information while keeping the document usable and visually stable: +- remove emails, names, IDs +- anonymize customer/company names +- produce a shareable version of a report + +This bundle provides `scripts/redact_docx.py`, which redacts by editing OOXML text nodes while attempting to preserve layout: +- default mode replaces matches with a fixed-length mask (`█` repeated), so line breaks and pagination drift less + +## Golden path +1. Create a copy of the input DOCX (don’t mutate the only copy). +2. Run `redact_docx.py` with carefully scoped patterns. +3. Render to PNGs and inspect the redacted areas. +4. Ensure no sensitive info remains (spot-check and use text search). + +## Run it +### Mask common patterns (examples) +```bash +python scripts/redact_docx.py /mnt/data/input.docx \ + --output /mnt/data/redacted.docx \ + --pattern "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}" \ + --pattern "\b\d{3}[-. ]\d{3}[-. ]\d{4}\b" +``` + +### Replace with a stable token (length-preserving) +If you want a visible label but still preserve length, repeat/truncate the label to the match length: +```bash +python scripts/redact_docx.py /mnt/data/input.docx \ + --output /mnt/data/redacted.docx \ + --pattern "Acme Corp" \ + --replacement "[REDACTED]" \ + --preserve_length +``` + +### Include comments (optional) +```bash +python scripts/redact_docx.py /mnt/data/input.docx \ + --output /mnt/data/redacted.docx \ + --pattern "secret" \ + --include_comments +``` + +## Pitfalls (learned the hard way) +- **Regex too broad:** you can accidentally redact normal prose. Prefer specific patterns. +- **Matches spanning paragraphs:** this tool only redacts *within a single paragraph* (`w:p`). Keep patterns local. +- **Non-text content:** images of text, embedded objects, charts, and tracked changes deletions may contain sensitive info. Masking text nodes won’t remove those. + +## QA checklist +- Render and visually inspect pages that contain redactions. +- Use a text-search pass to confirm strings are gone. +- Confirm headers/footers and footnotes/endnotes were redacted (this script patches them by default). + +## Deliverables +- Deliver **only the final DOCX** requested by the user. +- PNGs / optional PDFs are internal QA only unless explicitly requested. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/sections_layout.md b/App/memmy-agent/src/skills/docx/references/tasks/sections_layout.md new file mode 100644 index 000000000..1f1c9d7bd --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/sections_layout.md @@ -0,0 +1,62 @@ +# Task: Section breaks + mixed page layout (portrait/landscape, margins, page size) + +## Goal +Safely handle documents with mixed layouts without breaking headers/footers. + +## Key concept: sections +In DOCX, page layout is controlled by **sections**. A section defines: +- page size +- orientation (portrait/landscape) +- margins +- header/footer settings and linkage + +If anything looks wrong after an edit (suddenly landscape pages, header disappears, etc.), suspect sections. + +## How to audit +```bash +python scripts/section_audit.py /mnt/data/input.docx +``` + +Look for: +- multiple sections +- orientation changes +- headers/footers linked to previous when you expected them not to be + +## Creating a landscape section with python-docx (pattern) +```python +from docx import Document +from docx.enum.section import WD_SECTION +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.section import WD_ORIENT + +doc = Document() +doc.add_paragraph("Portrait page") + +sec2 = doc.add_section(WD_SECTION.NEW_PAGE) +sec2.orientation = WD_ORIENT.LANDSCAPE +sec2.page_width, sec2.page_height = sec2.page_height, sec2.page_width +doc.add_paragraph("Landscape page") + +doc.save("out.docx") +``` + +## Header/footer linkage gotcha +Each new section can inherit header/footer via **Link to Previous**. +If you need a different header/footer, you must break the linkage. +In Word UI: Header/Footer tools → toggle "Link to Previous". + +python-docx exposes `section.header.is_linked_to_previous` and `section.footer.is_linked_to_previous`. + +## Render → PNG review checklist (sections) +- Landscape pages are actually landscape (and only the intended ones) +- Margins look consistent with expectations +- Header/footer appears on all pages where expected +- "Different first page" behaves as intended +- Odd/even headers are correct (if enabled) + +## Common pitfalls +- Forgetting to swap width/height after setting landscape +- Editing only the first section’s header/footer and assuming it applies to later sections +- A continuous section break changing margins unexpectedly + +**Renderer note:** when a document mixes page sizes/orientations, `scripts/render_docx.py` computes DPI from the first section. If you care about exact pixel sizes, pass an explicit `--dpi`. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/style_lint_normalize.md b/App/memmy-agent/src/skills/docx/references/tasks/style_lint_normalize.md new file mode 100644 index 000000000..8185c0af9 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/style_lint_normalize.md @@ -0,0 +1,70 @@ +# Task: Style lint + normalize (format consistency) + +## When to use +Use this when the user asks for: +- "Make formatting consistent" +- "Apply our style guide" +- "Remove random bold/Calibri/spacing changes" +- "Why do headings look inconsistent?" + +This bundle provides: +- `scripts/style_lint.py` — report likely inconsistencies +- `scripts/style_normalize.py` — conservative cleanup (optional) + +## The reliable workflow +1. **Render to PNGs** (baseline) and inspect a few problem areas. +2. Run the **lint** to see what is causing drift. +3. Apply **normalization** only if it matches the user’s intent. +4. **Re-render and inspect all pages**. + +## 1) Lint +```bash +python scripts/style_lint.py input.docx --json /mnt/data/style_report.json +``` +What to look for: +- Lots of `run_direct_formatting`: common cause of “why is this one different”. +- Multiple fonts/sizes in `Normal` body text. +- “Heading-like” paragraphs that are not actually Heading styles. + +## 2) Normalize (conservative) +`style_normalize.py` always clears **run-level** direct formatting overrides (bold/italic/underline/font/size/color) so styles drive appearance. + +### A) Default normalization (recommended starting point) +```bash +python scripts/style_normalize.py input.docx out_normalized.docx +``` + + +> Tip: `style_normalize.py` also accepts `--out` as an alias: +> ```bash +> python scripts/style_normalize.py input.docx --out out_normalized.docx +> ``` + +### B) Also clear paragraph-level overrides (use sparingly) +This can change layout. Use only when the user wants style-driven spacing/indents: +```bash +python scripts/style_normalize.py input.docx out_normalized.docx --clear_paragraph_format +``` + +### C) Enforce a simple heading spacing rule +Useful when headings are visually inconsistent (space-after drift): +```bash +python scripts/style_normalize.py input.docx out_normalized.docx --enforce_heading_spacing +``` + +## Visual QA gate +```bash +python scripts/render_docx.py out_normalized.docx --output_dir /mnt/data/out_norm +``` +Success criteria: +- No clipped/overlapping text +- Headings and body text are consistent +- Tables remain aligned + +## Pitfalls / gotchas +- **Clearing run overrides can remove intentional emphasis.** If the user wants to keep bold/italic emphasis, don’t normalize globally; instead normalize only certain styles/sections. +- Some docs intentionally mix fonts (e.g., code blocks). Consider whitelisting styles rather than global clearing. + +## Deliverables +- Deliver **only the final DOCX** requested by the user. +- PNGs / optional PDFs are internal QA only unless explicitly requested. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/tables_spreadsheets.md b/App/memmy-agent/src/skills/docx/references/tasks/tables_spreadsheets.md new file mode 100644 index 000000000..82854f19a --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/tables_spreadsheets.md @@ -0,0 +1,36 @@ +# Task: Tables ↔ spreadsheets (import/export) + +## Goal +Move tabular data between Excel and Word reliably, without hand-copying. + +## Import XLSX → DOCX table (simple) +Use the helper to convert a sheet into a Word table: + +```bash +python scripts/xlsx_to_docx_table.py /mnt/data/input.xlsx /mnt/data/table.docx --title "Table: Results" +``` + +What it preserves (best-effort) +- cell values (data_only) +- basic alignment (left/center/right) +- header rows as bold +- column widths (heuristic) + +What it does **not** preserve +- merged cells, formulas, charts, conditional formatting, complex number formats + +## Export DOCX table → CSV + +```bash +python scripts/docx_table_to_csv.py /mnt/data/input.docx --table_index 0 --out /mnt/data/table0.csv +``` + +## Render → PNG review checklist (tables) +- Table fits within margins (no clipped columns) +- Header row is visually distinct +- Numbers align consistently (esp. decimals) +- No unexpected wrapping that hurts readability + +## Common pitfalls +- Word tables do not auto-match Excel column widths; you must verify visually. +- Multi-line cells and merged cells round-trip poorly. \ No newline at end of file diff --git a/App/memmy-agent/src/skills/docx/references/tasks/templates_style_packs.md b/App/memmy-agent/src/skills/docx/references/tasks/templates_style_packs.md new file mode 100644 index 000000000..738cd7838 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/templates_style_packs.md @@ -0,0 +1,34 @@ +# Templates / Style Packs (DOTX) — apply consistent styles + +## Goal +Apply a `.dotx` (or template `.docx`) style pack onto an existing report to improve professionalism and reduce bespoke styling. + +## Key idea +A Word template is mostly: +- `word/styles.xml` (style definitions) +- `word/theme/theme1.xml` (colors/fonts) +- optional: `word/fontTable.xml`, `word/numbering.xml` + +Direct formatting (manual bold/size/etc. on runs) can override style packs and cause inconsistent results. + +## Steps +1. Apply the template parts to your doc: + ```bash + python scripts/apply_template_styles.py --template template.dotx --target report.docx --out report_styled.docx + ``` + +2. Render and review: + ```bash + python scripts/render_docx.py report_styled.docx --output_dir out_styled + ``` + +## Render → PNG review checklist +- Typography looks consistent (headings/body) +- Spacing and margins still acceptable +- Tables didn’t reflow in a way that breaks readability +- Page count changes are acceptable + +## Pitfalls +- Style packs can change pagination. That’s expected. +- If the report contains heavy direct formatting, consider normalizing by re-applying paragraph styles (Heading 1/2/3, Normal) before applying the template. +- Custom style IDs in the target may be overwritten. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/toc_workflow.md b/App/memmy-agent/src/skills/docx/references/tasks/toc_workflow.md new file mode 100644 index 000000000..1cbb2fd94 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/toc_workflow.md @@ -0,0 +1,61 @@ +# Task: Insert + update a Table of Contents (TOC) + +## Goal +Create a TOC that **actually populates and stays correct** after edits. + +## Key reality +A TOC is a **field**. It will not update unless fields are refreshed. + +## Headless-safe alternative (no Word field update) +If you need a deterministic TOC in a fully automated / headless flow, prefer the **static TOC** workflow: + +```bash +python scripts/internal_nav.py /mnt/data/input.docx --out /mnt/data/with_static_toc.docx +``` + +This builds a static TOC + internal links (TOC -> headings, headings -> Back to TOC) without relying on Word field updates. +See: `references/tasks/navigation_internal_links.md`. + +## Requirements for a working TOC +1. **Use Heading styles** (`Heading 1/2/3`) for headings. Do not fake headings with bold + bigger font. +2. Keep heading text in the paragraph (avoid leading manual numbers as plain text). +3. After edits, **update fields** before final export. + +## Insert a TOC at a placeholder +1) Add a single paragraph containing the placeholder token: + +``` +[[TOC]] +``` + +2) Run the inserter: + +```bash +python scripts/insert_toc.py /mnt/data/input.docx --out /mnt/data/with_toc.docx +``` + +Defaults: include Heading 1–3. + +3) Open in Word and update fields: +- `Ctrl+A` → `F9` (Update Fields) +- Save + +4) Render and visually verify: + +```bash +python scripts/render_docx.py /mnt/data/with_toc.docx --output_dir /mnt/data/out +``` + +## Render → PNG review checklist (TOC) +- TOC is present (not blank) +- Indentation reflects heading levels +- Page numbers in TOC match the actual headings’ pages +- Headings that should appear do appear (and vice versa) +- No placeholder text remains (e.g., “TOC will populate…”) + +## Common pitfalls +- **Headings not styled** → TOC is empty. +- **Manual numbering/direct formatting** → TOC levels/indentation drift. +- **Fields not updated** → TOC and page numbers stale after edits. + +Tip: run `scripts/heading_audit.py` if you suspect heading-style issues. diff --git a/App/memmy-agent/src/skills/docx/references/tasks/verify_render.md b/App/memmy-agent/src/skills/docx/references/tasks/verify_render.md new file mode 100644 index 000000000..41d90a0a3 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/verify_render.md @@ -0,0 +1,56 @@ +# Task: Verify / render a DOCX (DOCX → PNG) + +## Why this exists +DOCX editing tools can "succeed" while the visual output is broken. Always verify by rendering. + +## Preferred: use the packaged renderer +This uses a dedicated LibreOffice profile + writable HOME and produces `page-.png` images: + +```bash +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out +# macOS: start Python with a stable temp dir if LibreOffice aborts +env TMPDIR=/private/tmp python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out +# For debugging LibreOffice failures: +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out --verbose +# Optional: also write .pdf to --output_dir (for debugging/archival): +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out --emit_pdf +``` + +## Manual render command (if you need it) +Use a unique LibreOffice profile when permission or locking issues occur: + +```bash +OUTDIR=/mnt/data/out +INPUT=/mnt/data/input.docx +BASENAME=$(basename "$INPUT" .docx) +LO_PROFILE=/mnt/data/.lo_profile_${BASENAME}_$$ +mkdir -p "$OUTDIR" "$LO_PROFILE" + +HOME="$LO_PROFILE" soffice --headless -env:UserInstallation=file://"$LO_PROFILE" \ + --convert-to pdf --outdir "$OUTDIR" "$INPUT" + +pdftoppm -png "$OUTDIR/$BASENAME.pdf" "$OUTDIR/$BASENAME" +``` + +## Success criteria +- PNGs exist for each page +- Spot-check page count and representative pages + +**Note:** LibreOffice sometimes prints scary-looking stderr (e.g., `error : Unknown IO error`) even when output is correct. Treat the conversion as successful if the PNGs exist and look correct (and if you used `--emit_pdf`, the PDF exists and is non-empty). + +## What to check in the PNGs +- clipped text (especially headings and table cells) +- overlapping objects +- broken tables (wrapping, misalignment, missing borders) +- unexpected font substitution +- header/footer alignment and page breaks + +## Caveats +- **Comments often don’t render** in headless LibreOffice PDFs. Use structural checks for comments. +- **Field codes (page numbers, TOC)** may show placeholder values in some PDF renders. If the user needs proof, re-check in Word or update fields before final render. +- **Multi-section docs** can have different page sizes/orientations; DPI is computed from the first section by default. If some pages look scaled oddly, use `--dpi` to override. + +## Delivery checklist +- Final DOCX is clean (no internal citation tokens, no placeholder text) +- Final render looks correct on all pages +- `/mnt/data` contains only final outputs (unless user asked for intermediates) diff --git a/App/memmy-agent/src/skills/docx/references/tasks/watermarks_background.md b/App/memmy-agent/src/skills/docx/references/tasks/watermarks_background.md new file mode 100644 index 000000000..40c21995a --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/tasks/watermarks_background.md @@ -0,0 +1,35 @@ +# Watermarks + Background Elements + +## Goal +Detect and (carefully) remove watermark-like background elements from a DOCX, then verify the output via **render + diff**. + +## What watermarks are in Word +Watermarks are often implemented as **VML shapes in headers**: +`w:hdr → w:p → w:r → w:pict → v:shape → v:textpath string="DRAFT"` + +Other documents may use DrawingML shapes or background images. + +## Steps +1. Audit the document: + ```bash + python scripts/watermark_audit_remove.py input.docx --mode report + ``` + +2. Remove (heuristic) by matching a substring inside the watermark text: + ```bash + python scripts/watermark_audit_remove.py input.docx --mode remove --contains DRAFT --out cleaned.docx + ``` + +3. Render and diff (recommended for QA/regressions): + ```bash + python scripts/render_and_diff.py input.docx cleaned.docx --outdir diff_watermark + ``` + +## Render → PNG review checklist +- If the watermark is visible in your renderer, confirm it is gone in the cleaned version. +- Confirm headers/footers still render correctly (no missing logos/lines). +- Confirm page count and layout remain acceptable. + +## Pitfalls +- Removal is heuristic: it can delete legitimate header graphics if they match the substring. +- Some VML watermarks won’t show in LibreOffice headless. When in doubt, validate in Word. diff --git a/App/memmy-agent/src/skills/docx/references/template-create.md b/App/memmy-agent/src/skills/docx/references/template-create.md new file mode 100644 index 000000000..40d8c3365 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/template-create.md @@ -0,0 +1,93 @@ +# Create a document from a distilled template + +Use this workflow with the retained DOCX, `$TMP_DIR/artifact.md`, and the user's +content. If `artifact.md` is missing, unresolved, or describes another +reference, run `template-distill.md` first. + +Before running commands, set `SKILL_DIR`, `TMP_DIR`, and `REFERENCE_DOCX` as in +`template-distill.md`, and set `FINAL_DOCX` to an absolute output path different +from `REFERENCE_DOCX`. + +Verify the retained DOCX against the path and SHA-256 recorded in +`artifact.md` before editing. A mismatch requires fresh distillation. + +Explicit user changes take precedence. Otherwise the retained DOCX controls +layout and formatting, and `artifact.md` explains how to use it. Generic +document presets do not replace the template's visual system. + +## Build from the reference + +1. Make a working copy of the retained DOCX. Do not start from a blank + document, apply a generic style pack, or alter the retained file. +2. Map each supported piece of user content to an editable slot in + `artifact.md`. Leave unsupported optional slots empty or remove them only + when the slot contract permits it; never invent facts to fill space. +3. Edit the copied source elements in place. Preserve untouched sections, + styles, numbering, relationships, headers, footers, images, tables, and + page furniture. +4. Reuse the source's real styles and components. When content exceeds a slot, + shorten it, use another documented source pattern, or add a cloned pattern + that `artifact.md` permits. Do not silently shrink text or overlay a second + design system. +5. For an existing text or relationship-backed slot, prefer a task-local + package patch built on `scripts/docx_ooxml_patch.py`; this preserves untouched + package parts byte-for-byte. Use `python-docx` only when the planned edit + needs its object model and the preserve-only package comparison still passes. + Do not rebuild unaffected package parts. +6. Use `scripts/content_controls.py list` to locate content controls, but do not + use its `fill` command in template-following mode because it reconstructs the + control content. For a verified plain-text control, use a task-local package + patch that changes only the intended text nodes while preserving the + existing control, paragraph, run properties, bookmarks, and every untouched + package part byte-for-byte. Preserve rich-text, repeating-section, image, + and table controls; if the intended edit requires changing their structure + or a plain-text control cannot be patched without rebuilding it, stop and + report the fidelity blocker. If the field inventory contains `TOC`, `REF`, + `PAGEREF`, `PAGE`, or `NUMPAGES`, follow `references/tasks/fields_update.md`. Refresh + fields in Word when available. Do not use a headless LibreOffice save as the + refresh step in template-following mode because it can rewrite or remove + unrelated package parts. If Word refresh is unavailable, set + `w:updateFields` to `true` through a settings-only package patch and record + that cached field text will refresh when the document opens in Word. + +## Verify fidelity + +Set `QA_RUN_DIR` to a new path that has never been used by a prior iteration, +such as `$TMP_DIR/template-fidelity-diff-$ITERATION`. Produce a reference/final +diff, then inspect every final page under its `b_render` directory at 100% zoom: + +```bash +"$PYTHON_BIN" "$SKILL_DIR/scripts/render_and_diff.py" \ + "$REFERENCE_DOCX" "$FINAL_DOCX" \ + --outdir "$QA_RUN_DIR" +``` + +Content changes will produce expected pixel differences. Treat the diff as a +scope check: unexplained movement outside intended slots, changed page +geometry, altered recurring chrome, or unexpected pagination is a failure. + +Before delivery, confirm: + +- rerun the section/style audits and every feature-specific audit used during + distillation, then compare the preserve-only structures recorded in + `artifact.md`; +- compare the final package-part inventory with the baseline and fail if any + preserve-only part or relationship changed or disappeared; +- section count and page geometry still match the contract unless explicitly + changed; +- typography, paragraph rhythm, lists, tables, headers, footers, page numbers, + images, and recurring components remain recognizably source-derived; +- every intended slot is filled, intentionally blank, or intentionally + removed; +- no text clips, overlaps, wraps unexpectedly, or leaves a broken page/table; +- refreshed TOC, reference, page, and page-count fields agree with the final + document when Word refresh is available; otherwise `w:updateFields` is set and + the deferred refresh is recorded; +- every deviation from `artifact.md` follows an explicit user request; +- the retained DOCX still matches the SHA-256 recorded before authoring. + +The image diff is not sufficient by itself: fields, relationships, bookmarks, +comments, content controls, numbering, and drawing anchors can regress without +changing a rendered page. Any unexplained structural loss is a failure. + +Revise and rerender until both document correctness and visual fidelity pass. diff --git a/App/memmy-agent/src/skills/docx/references/template-distill.md b/App/memmy-agent/src/skills/docx/references/template-distill.md new file mode 100644 index 000000000..44ac935d0 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/template-distill.md @@ -0,0 +1,89 @@ +# Distill a DOCX template + +Use this workflow only when a retained reference DOCX or an attached DOCX is +intended to control a new document's structure and appearance. Do not use it +for document review, content extraction, or a narrow edit to the reference. + +The retained DOCX stays unchanged and authoritative. Write one task-local +`$TMP_DIR/artifact.md` as the execution contract used alongside the reference. + +## Inputs + +Set `PYTHON_BIN` to the Python interpreter available in the Memmy runtime. +Prepend that interpreter's directory to `PATH` because packaged helpers may +spawn `python`. Set `SKILL_DIR` to this skill directory, `TMP_DIR` to a writable +task-specific temporary directory, and `REFERENCE_DOCX` to the absolute +retained reference path. Create `TMP_DIR` if needed. + +## Inspect the reference + +1. Render every page and inspect it at 100% zoom: + + ```bash + "$PYTHON_BIN" "$SKILL_DIR/scripts/render_docx.py" "$REFERENCE_DOCX" \ + --output_dir "$TMP_DIR/template-reference-render" + ``` + +2. Capture section and style evidence without modifying the reference: + + ```bash + "$PYTHON_BIN" "$SKILL_DIR/scripts/section_audit.py" "$REFERENCE_DOCX" + "$PYTHON_BIN" "$SKILL_DIR/scripts/style_lint.py" "$REFERENCE_DOCX" \ + --json "$TMP_DIR/template-style-evidence.json" + ``` + + Run the packaged heading, image, field, footnote, and content-control audits + too when the reference contains those features. Inventory content controls + by tag with + `"$PYTHON_BIN" "$SKILL_DIR/scripts/content_controls.py" "$REFERENCE_DOCX" list --json`. + +3. Inspect the DOCX package read-only when rendered or high-level evidence is + insufficient. Check the relevant styles, theme, numbering, section, + header/footer, relationship, drawing, and table XML. Do not rewrite the + package during distillation. + +4. Review every distinct section and page pattern. A first-page sample is not + enough when later pages, landscape sections, tables, headers, or footers use + different rules. + +## Write `artifact.md` + +Record only evidence needed to recreate the document: + +- **Reference:** absolute retained DOCX path, SHA-256, page count, section count, + and the render/evidence paths used. +- **Page system:** exact page sizes, orientation, margins, columns, + header/footer distances, first/odd/even-page behavior, and section breaks. +- **Typography:** named paragraph roles and exact font family, size, color, + weight, capitalization, alignment, spacing, line spacing, keep behavior, + indents, tabs, borders, and rules. +- **Lists and tables:** numbering definitions, nesting, marker and hanging + indents, table widths, column grids, cell margins, fills, borders, row rules, + alignment, and repeating headers. +- **Components:** title blocks, metadata, callouts, figures, captions, quotes, + headers, footers, page numbers, recurring rules, and image treatment. +- **Content flow:** ordered sections and the purpose and density of each. +- **Slot map:** each editable source location, its semantic purpose, allowed + content, capacity, and whether it must be rewritten, preserved, or removed. +- **Text coverage:** inspect body paragraphs, table cells, headers, footers, + text boxes, fields, and content controls. `Document.paragraphs` alone is not a + complete DOCX slot inventory. +- **Stable locators:** identify slots by package part plus structural path, + style, bookmark, content-control tag, or relationship ID; do not rely on + copied prose alone. +- **Package preservation:** a path, size, and SHA-256 inventory of package parts + and relationships, classifying `customXml`, styles, numbering, headers, + footers, drawings, comments, controls, and other opaque parts as editable or + preserve-only. +- **Fidelity gates:** source features that must remain unchanged and the visual + comparisons required before delivery. + +Do not paste the full source text or broad labels such as “professional.” Use +exact measurements and roles. If an important value cannot be established, +mark it unresolved rather than inventing it. + +## Distillation gate + +Do not continue until `artifact.md` accounts for every distinct page/section +pattern, every recurring element, and every intended edit slot. The retained +DOCX must still exist at the recorded path and remain byte-for-byte unchanged. diff --git a/App/memmy-agent/src/skills/docx/references/troubleshooting/libreoffice_headless.md b/App/memmy-agent/src/skills/docx/references/troubleshooting/libreoffice_headless.md new file mode 100644 index 000000000..ac5f064cb --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/troubleshooting/libreoffice_headless.md @@ -0,0 +1,44 @@ +# Troubleshooting: LibreOffice headless rendering + +## Symptom: `soffice` hangs, times out, or errors +This is commonly caused by LibreOffice failing to create/lock its user profile, or attempting to write config/cache under a non-writable `HOME`. + +## Fix (recommended): use the packaged renderer script +Use the canonical helper (`render_docx.py`). It: +- creates a unique per-run LibreOffice profile +- forces a writable `HOME` / XDG dirs under that profile +- captures stdout/stderr so failures are diagnosable + +```bash +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out +# macOS: set TMPDIR before Python starts when the default temp directory is unstable +env TMPDIR=/private/tmp python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out +# If you're debugging a conversion failure: +python scripts/render_docx.py /mnt/data/input.docx --output_dir /mnt/data/out --verbose +``` + +## Fix (manual): profile + writable HOME +If you must run `soffice` directly, do this: + +```bash +OUTDIR=/mnt/data/out +INPUT=/mnt/data/input.docx +BASENAME=$(basename "$INPUT" .docx) +LO_PROFILE=/mnt/data/.lo_profile_${BASENAME}_$$ +mkdir -p "$OUTDIR" "$LO_PROFILE" + +HOME="$LO_PROFILE" soffice --headless -env:UserInstallation=file://"$LO_PROFILE" \ + --convert-to pdf --outdir "$OUTDIR" "$INPUT" +``` + +## About scary stderr on "successful" conversions +LibreOffice sometimes prints scary-looking messages (notably `error : Unknown IO error`) even when the output PDF is correct. + +Prefer these success criteria over stderr: +- command completes +- downstream PNGs exist and look correct + +## If you still get weird behavior +- Ensure the profile directory is unique per process (use `$$` or a uuid) +- Delete stale profiles between runs +- Prefer `/mnt/data` over `/tmp` if you suspect permission sandboxing diff --git a/App/memmy-agent/src/skills/docx/references/troubleshooting/run_splitting.md b/App/memmy-agent/src/skills/docx/references/troubleshooting/run_splitting.md new file mode 100644 index 000000000..8e8cf37a4 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/references/troubleshooting/run_splitting.md @@ -0,0 +1,15 @@ +# Troubleshooting: run splitting ("why isn't my replace working?") + +## Reality +Word splits text into runs unpredictably (style changes, proofing boundaries, fields, etc.). +So searching for a substring and replacing it "as text" often fails. + +## Practical strategies +- Work at the `` / `` level, not the paragraph text level. +- When you must replace a token, consider inserting a hidden marker run first (during `python-docx` authoring) so you can reliably locate the target later when patching OOXML. +- For tracked changes replacements, wrap **exact runs** you want deleted as ``, then insert new `` adjacent. + +## Helper script +`scripts/docx_ooxml_patch.py` contains utilities that: +- find paragraphs by simple predicates (e.g., indentation) +- replace the Nth tracked insertion inside a paragraph diff --git a/App/memmy-agent/src/skills/docx/scripts/a11y_audit.py b/App/memmy-agent/src/skills/docx/scripts/a11y_audit.py new file mode 100644 index 000000000..282d82b0b --- /dev/null +++ b/App/memmy-agent/src/skills/docx/scripts/a11y_audit.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +"""Accessibility (A11y) audit for DOCX with optional safe fixes. + +What it checks (high ROI) +------------------------- +- Heading hierarchy: flags skipped heading levels (e.g., Heading 1 -> Heading 3) +- Images missing alt text: checks on inline/anchor drawings +- Tables missing header flag: checks first row for +- Non-descriptive hyperlinks: "click here", "here", "link", or raw URLs as visible text + +Optional fixes +-------------- +- --fix_image_alt from_filename: fill missing alt text using the relationship target filename +- --fix_table_headers first_row: set first row as header row (w:tblHeader) + +Notes +----- +This is not a full WCAG checker. It aims for consistent, mechanical checks/fixes +that are stable in headless pipelines. + +Usage +----- +python scripts/a11y_audit.py input.docx +python scripts/a11y_audit.py input.docx --fix_image_alt from_filename --out fixed.docx +python scripts/a11y_audit.py input.docx --fix_table_headers first_row --out fixed.docx +""" + +from __future__ import annotations + +import argparse +import json +import re +import zipfile +from dataclasses import dataclass +from typing import Any + +from lxml import etree + +W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships" +WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" +A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +PIC_NS = "http://schemas.openxmlformats.org/drawingml/2006/picture" + +NS = {"w": W_NS, "r": R_NS, "rel": REL_NS, "wp": WP_NS, "a": A_NS, "pic": PIC_NS} + + +@dataclass +class Finding: + severity: str # high|medium|low + kind: str + message: str + context: dict[str, Any] + + +NONDESCRIPTIVE = {"click here", "here", "link", "this link"} +URL_RE = re.compile(r"https?://\S+", re.IGNORECASE) + + +def _read_xml(z: zipfile.ZipFile, name: str) -> etree._Element: + return etree.fromstring(z.read(name)) + + +def _xml_bytes(root: etree._Element) -> bytes: + return etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone="yes") + + +def _load_document_rels(z: zipfile.ZipFile) -> dict[str, str]: + """Map rId -> Target for word/document.xml relationships.""" + rels_path = "word/_rels/document.xml.rels" + if rels_path not in z.namelist(): + return {} + rels = _read_xml(z, rels_path) + out: dict[str, str] = {} + for rel in rels.findall(f"{{{REL_NS}}}Relationship"): + rid = rel.get("Id") + tgt = rel.get("Target") + if rid and tgt: + out[rid] = tgt + return out + + +def _iter_story_parts(z: zipfile.ZipFile) -> list[str]: + """Return doc parts where content lives (main + headers/footers). + + For A11y, headers/footers matter (images, links). + """ + parts = ["word/document.xml"] + for name in z.namelist(): + if re.match(r"word/header\d+\.xml$", name) or re.match(r"word/footer\d+\.xml$", name): + parts.append(name) + return parts + + +def _heading_level_from_style(style_val: str | None) -> int | None: + if not style_val: + return None + m = re.match(r"Heading\s*(\d+)$", style_val) + if m: + try: + return int(m.group(1)) + except ValueError: + return None + return None + + +def audit_headings(root: etree._Element, part: str) -> list[Finding]: + findings: list[Finding] = [] + last: int | None = None + for p in root.xpath(".//w:p", namespaces=NS): + ppr = p.find("w:pPr", namespaces=NS) + if ppr is None: + continue + pstyle = ppr.find("w:pStyle", namespaces=NS) + lvl = _heading_level_from_style( + pstyle.get(f"{{{W_NS}}}val") if pstyle is not None else None + ) + if lvl is None: + continue + if last is not None and lvl > last + 1: + text = "".join([t.text or "" for t in p.xpath(".//w:t", namespaces=NS)]) + findings.append( + Finding( + severity="medium", + kind="heading_skip", + message=f"Heading level jumped from {last} to {lvl}", + context={"part": part, "text": text[:120]}, + ) + ) + last = lvl + return findings + + +def audit_images_alt(root: etree._Element, part: str) -> list[Finding]: + findings: list[Finding] = [] + # Look for wp:docPr under drawings + for docpr in root.xpath(".//wp:docPr", namespaces=NS): + descr = docpr.get("descr") or "" + title = docpr.get("title") or "" + if (descr.strip() == "") and (title.strip() == ""): + findings.append( + Finding( + severity="high", + kind="image_missing_alt", + message="Image missing alt text (descr/title empty)", + context={ + "part": part, + "id": docpr.get("id"), + "name": docpr.get("name"), + }, + ) + ) + return findings + + +def audit_tables(root: etree._Element, part: str) -> list[Finding]: + findings: list[Finding] = [] + for tbl in root.xpath(".//w:tbl", namespaces=NS): + rows = tbl.xpath("./w:tr", namespaces=NS) + if not rows: + continue + first = rows[0] + trpr = first.find("w:trPr", namespaces=NS) + has_header = False + if trpr is not None and trpr.find("w:tblHeader", namespaces=NS) is not None: + has_header = True + if not has_header: + findings.append( + Finding( + severity="medium", + kind="table_no_header_row", + message="Table first row is not marked as header (w:tblHeader missing)", + context={"part": part}, + ) + ) + return findings + + +def _visible_text_for_hyperlink(h: etree._Element) -> str: + return "".join([t.text or "" for t in h.xpath(".//w:t", namespaces=NS)]).strip() + + +def audit_hyperlinks(root: etree._Element, part: str) -> list[Finding]: + findings: list[Finding] = [] + for h in root.xpath(".//w:hyperlink", namespaces=NS): + txt = _visible_text_for_hyperlink(h) + if not txt: + continue + low = txt.strip().lower() + if low in NONDESCRIPTIVE: + findings.append( + Finding( + severity="medium", + kind="hyperlink_nondescriptive", + message=f"Non-descriptive hyperlink text: '{txt}'", + context={"part": part}, + ) + ) + if URL_RE.fullmatch(txt.strip()): + findings.append( + Finding( + severity="low", + kind="hyperlink_raw_url", + message="Hyperlink display text is a raw URL (often less accessible)", + context={"part": part, "text": txt[:120]}, + ) + ) + return findings + + +def _fix_image_alt_from_filename(root: etree._Element, part: str, rels_map: dict[str, str]) -> int: + """Fill missing docPr descr with image filename when possible.""" + changed = 0 + # Map docPr to embed relationship if possible by walking up to a:blip + # Pattern: wp:docPr is sibling to a:graphic; inside it, a:blip r:embed="rId.." + for drawing in root.xpath(".//w:drawing", namespaces=NS): + docpr = drawing.xpath(".//wp:docPr", namespaces=NS) + if not docpr: + continue + docpr = docpr[0] + descr = (docpr.get("descr") or "").strip() + title = (docpr.get("title") or "").strip() + if descr or title: + continue + blips = drawing.xpath(".//a:blip", namespaces=NS) + rid = None + if blips: + rid = blips[0].get(f"{{{R_NS}}}embed") + filename = None + if rid and rid in rels_map: + # Target like media/image1.png + filename = rels_map[rid].split("/")[-1] + if filename: + docpr.set("descr", f"Image: {filename}") + changed += 1 + else: + # fallback + docpr.set("descr", "Image") + changed += 1 + return changed + + +def _fix_table_headers_first_row(root: etree._Element) -> int: + changed = 0 + for tbl in root.xpath(".//w:tbl", namespaces=NS): + rows = tbl.xpath("./w:tr", namespaces=NS) + if not rows: + continue + first = rows[0] + trpr = first.find("w:trPr", namespaces=NS) + if trpr is None: + trpr = etree.SubElement(first, f"{{{W_NS}}}trPr") + if trpr.find("w:tblHeader", namespaces=NS) is None: + etree.SubElement(trpr, f"{{{W_NS}}}tblHeader") + changed += 1 + return changed + + +def audit_docx(path: str) -> dict[str, Any]: + with zipfile.ZipFile(path, "r") as z: + rels_map = _load_document_rels(z) + parts = _iter_story_parts(z) + findings: list[Finding] = [] + for part in parts: + root = _read_xml(z, part) + findings += audit_headings(root, part) + findings += audit_images_alt(root, part) + findings += audit_tables(root, part) + findings += audit_hyperlinks(root, part) + out = { + "file": path, + "counts": { + "high": sum(1 for f in findings if f.severity == "high"), + "medium": sum(1 for f in findings if f.severity == "medium"), + "low": sum(1 for f in findings if f.severity == "low"), + }, + "findings": [f.__dict__ for f in findings], + } + return out + + +def apply_fixes( + in_docx: str, + out_docx: str, + fix_image_alt: str | None, + fix_table_headers: str | None, +) -> dict[str, int]: + stats = {"image_alt_filled": 0, "table_headers_set": 0} + with zipfile.ZipFile(in_docx, "r") as zin: + rels_map = _load_document_rels(zin) + parts = _iter_story_parts(zin) + overrides: dict[str, bytes] = {} + + for part in parts: + root = _read_xml(zin, part) + changed = False + if fix_image_alt == "from_filename": + n = _fix_image_alt_from_filename(root, part, rels_map) + if n: + stats["image_alt_filled"] += n + changed = True + if fix_table_headers == "first_row": + n = _fix_table_headers_first_row(root) + if n: + stats["table_headers_set"] += n + changed = True + if changed: + overrides[part] = _xml_bytes(root) + + with zipfile.ZipFile(out_docx, "w", zipfile.ZIP_DEFLATED) as zout: + for info in zin.infolist(): + name = info.filename + if name in overrides: + zout.writestr(name, overrides[name]) + else: + zout.writestr(name, zin.read(name)) + return stats + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("in_docx") + ap.add_argument("--fix_image_alt", choices=["from_filename"], help="Apply safe image alt fix") + ap.add_argument("--fix_table_headers", choices=["first_row"], help="Mark first row as header") + ap.add_argument("--out", help="Write fixed DOCX") + ap.add_argument( + "--out_json", + help=( + "Optional path to write the audit report JSON. " + "When provided, stdout only prints a short summary." + ), + ) + args = ap.parse_args() + + if (args.fix_image_alt or args.fix_table_headers) and not args.out: + raise SystemExit("--out is required when applying fixes") + + if args.fix_image_alt or args.fix_table_headers: + stats = apply_fixes(args.in_docx, args.out, args.fix_image_alt, args.fix_table_headers) + print(f"[OK] wrote {args.out} | {stats}") + + report = audit_docx( + args.out if (args.out and (args.fix_image_alt or args.fix_table_headers)) else args.in_docx + ) + + if args.out_json: + with open(args.out_json, "w", encoding="utf-8") as f: + f.write(json.dumps(report, indent=2, ensure_ascii=False)) + f.write("\n") + print( + "[a11y] wrote report -> %s | high=%s medium=%s low=%s" + % ( + args.out_json, + report["counts"]["high"], + report["counts"]["medium"], + report["counts"]["low"], + ) + ) + else: + print(json.dumps(report, indent=2, ensure_ascii=False)) + + # Exit codes: + # - 0: no high-severity findings + # - 1: high-severity findings present + # - 2: reserved for argparse/usage errors + if report["counts"]["high"] > 0: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/App/memmy-agent/src/skills/docx/scripts/accept_tracked_changes.py b/App/memmy-agent/src/skills/docx/scripts/accept_tracked_changes.py new file mode 100644 index 000000000..940de0070 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/scripts/accept_tracked_changes.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Accept/reject tracked changes in a DOCX by patching OOXML. + +This is a pragmatic helper for the common requirement: + "Give me the final version with tracked changes accepted." + +It rewrites revision wrapper elements in word/document.xml: + - w:ins / w:del + - w:moveTo / w:moveFrom + +Modes +----- +- report: print counts (no output file) +- accept: keep insertions/moveTo, drop deletions/moveFrom +- reject: drop insertions/moveTo, keep deletions/moveFrom + +Caveats +------- +- This is not a full fidelity Word revision engine. It aims for common cases. +- After running, always render and visually review. + +Usage +----- +python scripts/accept_tracked_changes.py in.docx --mode report +python scripts/accept_tracked_changes.py in.docx --mode accept --out out.docx +python scripts/accept_tracked_changes.py in.docx --mode reject --out out.docx +""" + +from __future__ import annotations + +import argparse +import zipfile +from dataclasses import dataclass + +from lxml import etree + +W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +NS = {"w": W_NS} + + +@dataclass +class Counts: + ins: int = 0 + del_: int = 0 + moveto: int = 0 + movefrom: int = 0 + + +def _read_xml(z: zipfile.ZipFile, name: str) -> etree._Element: + return etree.fromstring(z.read(name)) + + +def _xml_bytes(root: etree._Element) -> bytes: + return etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone="yes") + + +def _unwrap(el: etree._Element) -> None: + """Replace element with its children (preserving order).""" + parent = el.getparent() + if parent is None: + return + idx = parent.index(el) + children = list(el) + parent.remove(el) + for i, c in enumerate(children): + parent.insert(idx + i, c) + + +def count_revisions(doc_root: etree._Element) -> Counts: + c = Counts( + ins=len(doc_root.xpath(".//w:ins", namespaces=NS)), + del_=len(doc_root.xpath(".//w:del", namespaces=NS)), + moveto=len(doc_root.xpath(".//w:moveTo", namespaces=NS)), + movefrom=len(doc_root.xpath(".//w:moveFrom", namespaces=NS)), + ) + return c + + +def apply_mode(doc_root: etree._Element, mode: str) -> None: + """Mutate doc_root in-place.""" + # We must process deepest-first to avoid invalidating iterators. + for tag, action in [ + ("moveTo", "ins"), + ("moveFrom", "del"), + ("ins", "ins"), + ("del", "del"), + ]: + els = doc_root.xpath(f".//w:{tag}", namespaces=NS) + # reverse document order + for el in reversed(els): + if action == "ins": + if mode == "accept": + _unwrap(el) + elif mode == "reject": + # drop entirely + parent = el.getparent() + if parent is not None: + parent.remove(el) + else: # del + if mode == "accept": + parent = el.getparent() + if parent is not None: + parent.remove(el) + elif mode == "reject": + _unwrap(el) + + +def disable_track_revisions(settings_root: etree._Element) -> bool: + changed = False + for el in settings_root.xpath(".//w:trackRevisions", namespaces=NS): + el.getparent().remove(el) + changed = True + return changed + + +def write_out( + src_docx: str, out_docx: str, doc_xml_bytes: bytes, settings_xml_bytes: bytes | None +) -> None: + with ( + zipfile.ZipFile(src_docx, "r") as zin, + zipfile.ZipFile(out_docx, "w", zipfile.ZIP_DEFLATED) as zout, + ): + for info in zin.infolist(): + name = info.filename + if name == "word/document.xml": + zout.writestr(name, doc_xml_bytes) + elif settings_xml_bytes is not None and name == "word/settings.xml": + zout.writestr(name, settings_xml_bytes) + else: + zout.writestr(name, zin.read(name)) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("in_docx") + ap.add_argument("--mode", choices=["report", "accept", "reject"], required=True) + ap.add_argument("--out", help="Output DOCX (required for accept/reject)") + ap.add_argument( + "--keep_tracking_on", + action="store_true", + help="Do not remove trackRevisions from settings.xml", + ) + args = ap.parse_args() + + with zipfile.ZipFile(args.in_docx, "r") as z: + doc_root = _read_xml(z, "word/document.xml") + counts_before = count_revisions(doc_root) + settings_root = None + if "word/settings.xml" in z.namelist(): + settings_root = _read_xml(z, "word/settings.xml") + + print( + f"[report] ins={counts_before.ins} del={counts_before.del_} moveTo={counts_before.moveto} moveFrom={counts_before.movefrom}" + ) + + if args.mode == "report": + return + + if not args.out: + raise SystemExit("--out is required for accept/reject") + + apply_mode(doc_root, args.mode) + + settings_bytes = None + if settings_root is not None and not args.keep_tracking_on: + if disable_track_revisions(settings_root): + settings_bytes = _xml_bytes(settings_root) + + # Re-count + counts_after = count_revisions(doc_root) + print( + f"[after] ins={counts_after.ins} del={counts_after.del_} moveTo={counts_after.moveto} moveFrom={counts_after.movefrom}" + ) + + write_out(args.in_docx, args.out, _xml_bytes(doc_root), settings_bytes) + print(f"[OK] wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/App/memmy-agent/src/skills/docx/scripts/add_tracked_replacements.py b/App/memmy-agent/src/skills/docx/scripts/add_tracked_replacements.py new file mode 100644 index 000000000..f8012d1fb --- /dev/null +++ b/App/memmy-agent/src/skills/docx/scripts/add_tracked_replacements.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Create tracked-change *replacements* in a DOCX by OOXML patching. + +The v6 skill includes tools to enable tracking and to accept tracked changes, +but it doesn't provide a direct way to *generate* tracked insertions/deletions. +This helper adds simple replacements (old -> new) as `` + ``. + +Scope +----- +- Best-effort: only replaces occurrences within a single `w:t` text node. +- Enables `w:trackRevisions` in settings.xml. + +Usage +----- +python scripts/add_tracked_replacements.py in.docx --out out.docx \ + --replace "foo=bar" --replace "old phrase=new phrase" +""" + +from __future__ import annotations + +import argparse +import datetime as _dt +import zipfile + +from lxml import etree + +W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +NS = {"w": W_NS} + + +def w(tag: str) -> str: + return f"{{{W_NS}}}{tag}" + + +def _xml_bytes(root: etree._Element) -> bytes: + return etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone="yes") + + +def _enable_track(settings_root: etree._Element) -> bool: + if settings_root.find("w:trackRevisions", namespaces=NS) is not None: + return False + settings_root.insert(0, etree.Element(w("trackRevisions"))) + return True + + +def _next_change_id(doc_root: etree._Element) -> int: + ids = [] + for el in doc_root.xpath(".//*[@w:id]", namespaces=NS): + try: + ids.append(int(el.get(w("id")))) + except Exception: + pass + return (max(ids) + 1) if ids else 1 + + +def _make_del(text: str, cid: int, when: str) -> etree._Element: + d = etree.Element(w("del")) + d.set(w("id"), str(cid)) + d.set(w("date"), when) + r = etree.SubElement(d, w("r")) + dt = etree.SubElement(r, w("delText")) + dt.text = text + return d + + +def _make_ins(text: str, cid: int, when: str) -> etree._Element: + ins = etree.Element(w("ins")) + ins.set(w("id"), str(cid)) + ins.set(w("date"), when) + r = etree.SubElement(ins, w("r")) + t = etree.SubElement(r, w("t")) + t.text = text + return ins + + +def _replace_in_text_node( + t_node: etree._Element, old: str, new: str, cid_start: int, when: str +) -> tuple[int, bool]: + txt = t_node.text or "" + if old not in txt: + return cid_start, False + # Only handle a single occurrence per node to keep ids simple/deterministic. + before, after = txt.split(old, 1) + parent_r = t_node.getparent() # w:r + if parent_r is None: + return cid_start, False + run_parent = parent_r.getparent() + if run_parent is None: + return cid_start, False + + idx = run_parent.index(parent_r) + + # Replace the run with: [before run] old new [after run] + rpr = parent_r.find("w:rPr", namespaces=NS) + + def make_run(s: str) -> etree._Element: + r = etree.Element(w("r")) + if rpr is not None: + r.append(etree.fromstring(etree.tostring(rpr))) + t = etree.SubElement(r, w("t")) + t.text = s + return r + + inserts = [] + if before: + inserts.append(make_run(before)) + d = _make_del(old, cid_start, when) + cid_start += 1 + ins = _make_ins(new, cid_start, when) + cid_start += 1 + inserts.extend([d, ins]) + if after: + inserts.append(make_run(after)) + + for node in inserts[::-1]: + run_parent.insert(idx, node) + run_parent.remove(parent_r) + return cid_start, True + + +def add_tracked_replacements( + in_docx: str, out_docx: str, replaces: list[tuple[str, str]] +) -> None: + when = _dt.datetime.utcnow().replace(microsecond=0).isoformat() + "Z" + with zipfile.ZipFile(in_docx, "r") as zin: + overrides: dict[str, bytes] = {} + + doc_root = etree.fromstring(zin.read("word/document.xml")) + settings_name = "word/settings.xml" + settings_root = ( + etree.fromstring(zin.read(settings_name)) + if settings_name in zin.namelist() + else etree.Element(w("settings")) + ) + + _enable_track(settings_root) + cid = _next_change_id(doc_root) + total = 0 + + for old, new in replaces: + for t in doc_root.xpath(".//w:t", namespaces=NS): + cid, changed = _replace_in_text_node(t, old, new, cid, when=when) + if changed: + total += 1 + break + + overrides["word/document.xml"] = _xml_bytes(doc_root) + overrides[settings_name] = _xml_bytes(settings_root) + + with zipfile.ZipFile(out_docx, "w", zipfile.ZIP_DEFLATED) as zout: + for info in zin.infolist(): + name = info.filename + if name in overrides: + zout.writestr(name, overrides[name]) + else: + zout.writestr(name, zin.read(name)) + + print(f"[OK] wrote {out_docx} (replacements={total})") + + +def main() -> None: + ap = argparse.ArgumentParser(description="Add tracked replacement edits (best-effort)") + ap.add_argument("in_docx") + ap.add_argument("--out", required=True) + ap.add_argument( + "--replace", + action="append", + default=[], + help="Replacement formatted as OLD=NEW (repeatable)", + ) + args = ap.parse_args() + + replaces: list[tuple[str, str]] = [] + for rpl in args.replace: + if "=" not in rpl: + raise SystemExit("--replace must be formatted as OLD=NEW") + old, new = rpl.split("=", 1) + replaces.append((old, new)) + if not replaces: + raise SystemExit("Provide at least one --replace") + + add_tracked_replacements(args.in_docx, args.out, replaces) + + +if __name__ == "__main__": + main() diff --git a/App/memmy-agent/src/skills/docx/scripts/apply_template_styles.py b/App/memmy-agent/src/skills/docx/scripts/apply_template_styles.py new file mode 100644 index 000000000..0cc77d825 --- /dev/null +++ b/App/memmy-agent/src/skills/docx/scripts/apply_template_styles.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Apply a template/style pack (DOTX or DOCX) onto a target DOCX. + +Goal: make it easy to "start from template" or retrofit a style pack without +manual re-styling. + +What it does (minimal, high ROI) +-------------------------------- +- Copies key parts from the template into the target: + - word/styles.xml + - word/theme/theme1.xml + - word/fontTable.xml (if present) + - word/numbering.xml (if present) + +It also ensures [Content_Types].xml has the required Overrides for any newly +added parts. + +Usage +----- +python scripts/apply_template_styles.py --template template.dotx --target report.docx --out styled.docx + +Caveats +------- +- This can change pagination/layout. Always render and inspect PNGs. +- If the target uses custom styles with the same IDs, they will be overwritten. +""" + +from __future__ import annotations + +import argparse +import zipfile + +from lxml import etree + +CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types" + + +def _read(z: zipfile.ZipFile, name: str) -> bytes: + return z.read(name) + + +def _has(z: zipfile.ZipFile, name: str) -> bool: + return name in z.namelist() + + +def _ensure_override(ct_root: etree._Element, part_name: str, content_type: str) -> bool: + changed = False + # Normalize PartName to start with '/' + if not part_name.startswith("/"): + part_name = "/" + part_name + + # If override exists, update contentType if needed + for ov in ct_root.findall(f"{{{CT_NS}}}Override"): + if ov.get("PartName") == part_name: + if ov.get("ContentType") != content_type: + ov.set("ContentType", content_type) + changed = True + return changed + + ov = etree.SubElement(ct_root, f"{{{CT_NS}}}Override") + ov.set("PartName", part_name) + ov.set("ContentType", content_type) + return True + + +def apply(template_path: str, target_path: str, out_path: str) -> None: + parts = [ + ("word/styles.xml", None), + ( + "word/theme/theme1.xml", + "application/vnd.openxmlformats-officedocument.theme+xml", + ), + ( + "word/fontTable.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml", + ), + ( + "word/numbering.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", + ), + ] + + with ( + zipfile.ZipFile(template_path, "r") as zt, + zipfile.ZipFile(target_path, "r") as zg, + ): + overrides = {} + for name, _ct in parts: + if _has(zt, name): + overrides[name] = _read(zt, name) + + # Update content types in target if we add/override optional parts + ct_bytes = _read(zg, "[Content_Types].xml") + ct_root = etree.fromstring(ct_bytes) + ct_changed = False + + for name, ctype in parts: + if name in overrides and ctype: + ct_changed |= _ensure_override(ct_root, name, ctype) + + if ct_changed: + overrides["[Content_Types].xml"] = etree.tostring( + ct_root, xml_declaration=True, encoding="UTF-8", standalone="yes" + ) + + with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zout: + for info in zg.infolist(): + name = info.filename + if name in overrides: + zout.writestr(name, overrides[name]) + else: + zout.writestr(name, zg.read(name)) + # Add new parts not present in target + for name, data in overrides.items(): + if name not in {i.filename for i in zg.infolist()}: + zout.writestr(name, data) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--template", required=True) + ap.add_argument("--target", required=True) + ap.add_argument("--out", required=True) + args = ap.parse_args() + + apply(args.template, args.target, args.out) + print(f"[OK] wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/App/memmy-agent/src/skills/docx/scripts/captions_and_crossrefs.py b/App/memmy-agent/src/skills/docx/scripts/captions_and_crossrefs.py new file mode 100644 index 000000000..6a693d41a --- /dev/null +++ b/App/memmy-agent/src/skills/docx/scripts/captions_and_crossrefs.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Insert simple captions (Figure/Table) and optional cross-references. + +This is a pragmatic OOXML-level helper for: +- Adding Figure/Table captions using SEQ fields +- (Optional) adding bookmarks around the caption number for later REF fields +- (Optional) materializing SEQ/REF fields so headless renders show correct numbers + +It targets common automation needs, not the full Word caption feature set. +""" + +from __future__ import annotations + +import argparse +import tempfile +from pathlib import Path + +from lxml import etree + +try: + from docx_ooxml_patch import unzip_docx, zip_docx +except Exception: + import os + import shutil + import zipfile + + def unzip_docx(docx_path: Path, out_dir: Path) -> None: + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(docx_path, "r") as z: + z.extractall(out_dir) + + def zip_docx(in_dir: Path, out_docx_path: Path) -> None: + if out_docx_path.exists(): + out_docx_path.unlink() + with zipfile.ZipFile(out_docx_path, "w", compression=zipfile.ZIP_DEFLATED) as z: + for root, _dirs, files in os.walk(in_dir): + for f in files: + abs_path = Path(root) / f + rel_path = abs_path.relative_to(in_dir) + z.write(abs_path, rel_path.as_posix()) + + +W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +NS = { + "w": W_NS, + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", +} + + +def qn(local: str) -> str: + return f"{{{W_NS}}}{local}" + + +def w_attr(local: str) -> str: + return f"{{{W_NS}}}{local}" + + +def _xml_space_preserve(t_el: etree._Element) -> None: + if t_el.text and (t_el.text.startswith(" ") or t_el.text.endswith(" ")): + t_el.set("{http://www.w3.org/XML/1998/namespace}space", "preserve") + + +def _load_tree(path: Path) -> etree._ElementTree: + parser = etree.XMLParser(remove_blank_text=False) + return etree.parse(str(path), parser) + + +def _save_tree(tree: etree._ElementTree, path: Path) -> None: + tree.write(str(path), xml_declaration=True, encoding="UTF-8", standalone="yes") + + +def _iter_word_parts(unzipped: Path) -> list[Path]: + parts = [unzipped / "word" / "document.xml"] + for pat in ("header*.xml", "footer*.xml"): + parts.extend(sorted((unzipped / "word").glob(pat))) + return [p for p in parts if p.exists()] + + +def _max_bookmark_id(root: etree._Element) -> int: + mx = 0 + for b in root.findall(".//w:bookmarkStart", namespaces=NS): + v = b.get(w_attr("id")) + if v and v.isdigit(): + mx = max(mx, int(v)) + for b in root.findall(".//w:bookmarkEnd", namespaces=NS): + v = b.get(w_attr("id")) + if v and v.isdigit(): + mx = max(mx, int(v)) + return mx + + +def _caption_paragraph(label: str, caption_text: str, seq_name: str) -> etree._Element: + """Create a containing: '