From 7b2162ff29a1d34ba91a963ec3313fb203b104ed Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Wed, 28 Oct 2026 16:08:45 +0000 Subject: [PATCH] feat(dev-tools): adiciona plugin dev-tools (agents, skills, knowledge e metadados do projeto) --- dev-tools/.claude-plugin/plugin.json | 12 + dev-tools/AGENTS.md | 17 + dev-tools/CHANGELOG.md | 5 + dev-tools/MEMORY.md | 4 + dev-tools/README.md | 3 + dev-tools/STATUS.md | 8 + dev-tools/agents/AGENTS.md | 17 + dev-tools/agents/CHANGELOG.md | 5 + dev-tools/agents/MEMORY.md | 4 + dev-tools/agents/README.md | 3 + dev-tools/agents/STATUS.md | 8 + .../agents/database-design-specialist.md | 163 + dev-tools/agents/desk.project | 6 + dev-tools/agents/dev-helper.md | 188 + dev-tools/agents/development-lead.md | 165 + dev-tools/agents/index.md | 8 + .../agents/javascript-fullstack-specialist.md | 159 + dev-tools/agents/log.md | 1 + dev-tools/agents/nextjs-specialist.md | 179 + dev-tools/agents/php-fullstack-engineer.md | 149 + dev-tools/desk.project | 6 + dev-tools/index.md | 8 + dev-tools/knowledge/datasets.json | 51 + dev-tools/log.md | 1 + dev-tools/skills/AGENTS.md | 17 + dev-tools/skills/CHANGELOG.md | 5 + dev-tools/skills/MEMORY.md | 4 + dev-tools/skills/README.md | 3 + dev-tools/skills/STATUS.md | 8 + dev-tools/skills/benchmark/SKILL.md | 125 + dev-tools/skills/canary/SKILL.md | 156 + dev-tools/skills/chrome/SKILL.md | 158 + dev-tools/skills/db-design/SKILL.md | 192 + dev-tools/skills/dep-audit/SKILL.md | 214 + dev-tools/skills/desk.project | 6 + dev-tools/skills/diagrama/SKILL.md | 153 + dev-tools/skills/docx/SKILL-light.md | 160 + dev-tools/skills/docx/SKILL.md | 145 + dev-tools/skills/docx/scripts/__init__.py | 1 + .../skills/docx/scripts/accept_changes.py | 135 + dev-tools/skills/docx/scripts/comment.py | 318 ++ .../docx/scripts/office/helpers/__init__.py | 0 .../docx/scripts/office/helpers/merge_runs.py | 199 + .../office/helpers/simplify_redlines.py | 197 + dev-tools/skills/docx/scripts/office/pack.py | 159 + .../schemas/ISO-IEC29500-4_2016/dml-chart.xsd | 1499 ++++++ .../ISO-IEC29500-4_2016/dml-chartDrawing.xsd | 146 + .../ISO-IEC29500-4_2016/dml-diagram.xsd | 1085 ++++ .../ISO-IEC29500-4_2016/dml-lockedCanvas.xsd | 11 + .../schemas/ISO-IEC29500-4_2016/dml-main.xsd | 3081 ++++++++++++ .../ISO-IEC29500-4_2016/dml-picture.xsd | 23 + .../dml-spreadsheetDrawing.xsd | 185 + .../dml-wordprocessingDrawing.xsd | 287 ++ .../schemas/ISO-IEC29500-4_2016/pml.xsd | 1676 +++++++ .../shared-additionalCharacteristics.xsd | 28 + .../shared-bibliography.xsd | 144 + .../shared-commonSimpleTypes.xsd | 174 + .../shared-customXmlDataProperties.xsd | 25 + .../shared-customXmlSchemaProperties.xsd | 18 + .../shared-documentPropertiesCustom.xsd | 59 + .../shared-documentPropertiesExtended.xsd | 56 + .../shared-documentPropertiesVariantTypes.xsd | 195 + .../ISO-IEC29500-4_2016/shared-math.xsd | 582 +++ .../shared-relationshipReference.xsd | 25 + .../schemas/ISO-IEC29500-4_2016/sml.xsd | 4439 +++++++++++++++++ .../schemas/ISO-IEC29500-4_2016/vml-main.xsd | 570 +++ .../ISO-IEC29500-4_2016/vml-officeDrawing.xsd | 509 ++ .../vml-presentationDrawing.xsd | 12 + .../vml-spreadsheetDrawing.xsd | 108 + .../vml-wordprocessingDrawing.xsd | 96 + .../schemas/ISO-IEC29500-4_2016/wml.xsd | 3646 ++++++++++++++ .../schemas/ISO-IEC29500-4_2016/xml.xsd | 116 + .../ecma/fouth-edition/opc-contentTypes.xsd | 42 + .../ecma/fouth-edition/opc-coreProperties.xsd | 50 + .../schemas/ecma/fouth-edition/opc-digSig.xsd | 49 + .../ecma/fouth-edition/opc-relationships.xsd | 33 + .../docx/scripts/office/schemas/mce/mc.xsd | 75 + .../office/schemas/microsoft/wml-2010.xsd | 560 +++ .../office/schemas/microsoft/wml-2012.xsd | 67 + .../office/schemas/microsoft/wml-2018.xsd | 14 + .../office/schemas/microsoft/wml-cex-2018.xsd | 20 + .../office/schemas/microsoft/wml-cid-2016.xsd | 13 + .../microsoft/wml-sdtdatahash-2020.xsd | 4 + .../schemas/microsoft/wml-symex-2015.xsd | 8 + .../skills/docx/scripts/office/soffice.py | 183 + .../skills/docx/scripts/office/unpack.py | 132 + .../skills/docx/scripts/office/validate.py | 111 + .../scripts/office/validators/__init__.py | 15 + .../docx/scripts/office/validators/base.py | 847 ++++ .../docx/scripts/office/validators/docx.py | 446 ++ .../docx/scripts/office/validators/pptx.py | 275 + .../scripts/office/validators/redlining.py | 247 + .../docx/scripts/templates/comments.xml | 3 + .../scripts/templates/commentsExtended.xml | 3 + .../scripts/templates/commentsExtensible.xml | 3 + .../docx/scripts/templates/commentsIds.xml | 3 + .../skills/docx/scripts/templates/people.xml | 3 + dev-tools/skills/index.md | 8 + dev-tools/skills/log.md | 1 + dev-tools/skills/nextjs/SKILL.md | 233 + .../nextjs/references/performance-rules.md | 245 + dev-tools/skills/pdf/SKILL-light.md | 161 + dev-tools/skills/pdf/SKILL.md | 197 + .../pdf/scripts/check_bounding_boxes.py | 65 + .../pdf/scripts/check_fillable_fields.py | 11 + .../pdf/scripts/convert_pdf_to_images.py | 33 + .../pdf/scripts/create_validation_image.py | 37 + .../pdf/scripts/extract_form_field_info.py | 122 + .../pdf/scripts/extract_form_structure.py | 115 + .../pdf/scripts/fill_fillable_fields.py | 98 + .../scripts/fill_pdf_form_with_annotations.py | 107 + dev-tools/skills/php-dev/SKILL.md | 204 + dev-tools/skills/prompt-refine/SKILL.md | 145 + dev-tools/skills/react-patterns/SKILL.md | 181 + .../references/composition-patterns.md | 183 + .../react-patterns/references/custom-hooks.md | 66 + dev-tools/skills/skill-creator/SKILL.md | 140 + .../references/anthropic-patterns.md | 258 + .../references/descomplicar-standards.md | 318 ++ .../references/output-patterns.md | 184 + .../skill-creator/references/workflows.md | 181 + dev-tools/skills/webapp-testing/SKILL.md | 117 + .../examples/console_logging.py | 35 + .../examples/element_discovery.py | 40 + .../examples/static_html_automation.py | 33 + .../webapp-testing/scripts/with_server.py | 106 + dev-tools/skills/xlsx/SKILL.md | 134 + .../xlsx/scripts/office/helpers/__init__.py | 0 .../xlsx/scripts/office/helpers/merge_runs.py | 199 + .../office/helpers/simplify_redlines.py | 197 + dev-tools/skills/xlsx/scripts/office/pack.py | 159 + .../schemas/ISO-IEC29500-4_2016/dml-chart.xsd | 1499 ++++++ .../ISO-IEC29500-4_2016/dml-chartDrawing.xsd | 146 + .../ISO-IEC29500-4_2016/dml-diagram.xsd | 1085 ++++ .../ISO-IEC29500-4_2016/dml-lockedCanvas.xsd | 11 + .../schemas/ISO-IEC29500-4_2016/dml-main.xsd | 3081 ++++++++++++ .../ISO-IEC29500-4_2016/dml-picture.xsd | 23 + .../dml-spreadsheetDrawing.xsd | 185 + .../dml-wordprocessingDrawing.xsd | 287 ++ .../schemas/ISO-IEC29500-4_2016/pml.xsd | 1676 +++++++ .../shared-additionalCharacteristics.xsd | 28 + .../shared-bibliography.xsd | 144 + .../shared-commonSimpleTypes.xsd | 174 + .../shared-customXmlDataProperties.xsd | 25 + .../shared-customXmlSchemaProperties.xsd | 18 + .../shared-documentPropertiesCustom.xsd | 59 + .../shared-documentPropertiesExtended.xsd | 56 + .../shared-documentPropertiesVariantTypes.xsd | 195 + .../ISO-IEC29500-4_2016/shared-math.xsd | 582 +++ .../shared-relationshipReference.xsd | 25 + .../schemas/ISO-IEC29500-4_2016/sml.xsd | 4439 +++++++++++++++++ .../schemas/ISO-IEC29500-4_2016/vml-main.xsd | 570 +++ .../ISO-IEC29500-4_2016/vml-officeDrawing.xsd | 509 ++ .../vml-presentationDrawing.xsd | 12 + .../vml-spreadsheetDrawing.xsd | 108 + .../vml-wordprocessingDrawing.xsd | 96 + .../schemas/ISO-IEC29500-4_2016/wml.xsd | 3646 ++++++++++++++ .../schemas/ISO-IEC29500-4_2016/xml.xsd | 116 + .../ecma/fouth-edition/opc-contentTypes.xsd | 42 + .../ecma/fouth-edition/opc-coreProperties.xsd | 50 + .../schemas/ecma/fouth-edition/opc-digSig.xsd | 49 + .../ecma/fouth-edition/opc-relationships.xsd | 33 + .../xlsx/scripts/office/schemas/mce/mc.xsd | 75 + .../office/schemas/microsoft/wml-2010.xsd | 560 +++ .../office/schemas/microsoft/wml-2012.xsd | 67 + .../office/schemas/microsoft/wml-2018.xsd | 14 + .../office/schemas/microsoft/wml-cex-2018.xsd | 20 + .../office/schemas/microsoft/wml-cid-2016.xsd | 13 + .../microsoft/wml-sdtdatahash-2020.xsd | 4 + .../schemas/microsoft/wml-symex-2015.xsd | 8 + .../skills/xlsx/scripts/office/soffice.py | 183 + .../skills/xlsx/scripts/office/unpack.py | 132 + .../skills/xlsx/scripts/office/validate.py | 111 + .../scripts/office/validators/__init__.py | 15 + .../xlsx/scripts/office/validators/base.py | 847 ++++ .../xlsx/scripts/office/validators/docx.py | 446 ++ .../xlsx/scripts/office/validators/pptx.py | 275 + .../scripts/office/validators/redlining.py | 247 + dev-tools/skills/xlsx/scripts/recalc.py | 184 + dev-tools/skills/youtube-extract/SKILL.md | 155 + 180 files changed, 52164 insertions(+) create mode 100644 dev-tools/.claude-plugin/plugin.json create mode 100644 dev-tools/AGENTS.md create mode 100644 dev-tools/CHANGELOG.md create mode 100644 dev-tools/MEMORY.md create mode 100644 dev-tools/README.md create mode 100644 dev-tools/STATUS.md create mode 100644 dev-tools/agents/AGENTS.md create mode 100644 dev-tools/agents/CHANGELOG.md create mode 100644 dev-tools/agents/MEMORY.md create mode 100644 dev-tools/agents/README.md create mode 100644 dev-tools/agents/STATUS.md create mode 100644 dev-tools/agents/database-design-specialist.md create mode 100644 dev-tools/agents/desk.project create mode 100644 dev-tools/agents/dev-helper.md create mode 100644 dev-tools/agents/development-lead.md create mode 100644 dev-tools/agents/index.md create mode 100644 dev-tools/agents/javascript-fullstack-specialist.md create mode 100644 dev-tools/agents/log.md create mode 100644 dev-tools/agents/nextjs-specialist.md create mode 100644 dev-tools/agents/php-fullstack-engineer.md create mode 100644 dev-tools/desk.project create mode 100644 dev-tools/index.md create mode 100644 dev-tools/knowledge/datasets.json create mode 100644 dev-tools/log.md create mode 100644 dev-tools/skills/AGENTS.md create mode 100644 dev-tools/skills/CHANGELOG.md create mode 100644 dev-tools/skills/MEMORY.md create mode 100644 dev-tools/skills/README.md create mode 100644 dev-tools/skills/STATUS.md create mode 100644 dev-tools/skills/benchmark/SKILL.md create mode 100644 dev-tools/skills/canary/SKILL.md create mode 100644 dev-tools/skills/chrome/SKILL.md create mode 100644 dev-tools/skills/db-design/SKILL.md create mode 100644 dev-tools/skills/dep-audit/SKILL.md create mode 100644 dev-tools/skills/desk.project create mode 100644 dev-tools/skills/diagrama/SKILL.md create mode 100644 dev-tools/skills/docx/SKILL-light.md create mode 100644 dev-tools/skills/docx/SKILL.md create mode 100644 dev-tools/skills/docx/scripts/__init__.py create mode 100644 dev-tools/skills/docx/scripts/accept_changes.py create mode 100644 dev-tools/skills/docx/scripts/comment.py create mode 100644 dev-tools/skills/docx/scripts/office/helpers/__init__.py create mode 100644 dev-tools/skills/docx/scripts/office/helpers/merge_runs.py create mode 100644 dev-tools/skills/docx/scripts/office/helpers/simplify_redlines.py create mode 100644 dev-tools/skills/docx/scripts/office/pack.py create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/mce/mc.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd create mode 100644 dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd create mode 100644 dev-tools/skills/docx/scripts/office/soffice.py create mode 100644 dev-tools/skills/docx/scripts/office/unpack.py create mode 100644 dev-tools/skills/docx/scripts/office/validate.py create mode 100644 dev-tools/skills/docx/scripts/office/validators/__init__.py create mode 100644 dev-tools/skills/docx/scripts/office/validators/base.py create mode 100644 dev-tools/skills/docx/scripts/office/validators/docx.py create mode 100644 dev-tools/skills/docx/scripts/office/validators/pptx.py create mode 100644 dev-tools/skills/docx/scripts/office/validators/redlining.py create mode 100644 dev-tools/skills/docx/scripts/templates/comments.xml create mode 100644 dev-tools/skills/docx/scripts/templates/commentsExtended.xml create mode 100644 dev-tools/skills/docx/scripts/templates/commentsExtensible.xml create mode 100644 dev-tools/skills/docx/scripts/templates/commentsIds.xml create mode 100644 dev-tools/skills/docx/scripts/templates/people.xml create mode 100644 dev-tools/skills/index.md create mode 100644 dev-tools/skills/log.md create mode 100644 dev-tools/skills/nextjs/SKILL.md create mode 100644 dev-tools/skills/nextjs/references/performance-rules.md create mode 100644 dev-tools/skills/pdf/SKILL-light.md create mode 100644 dev-tools/skills/pdf/SKILL.md create mode 100644 dev-tools/skills/pdf/scripts/check_bounding_boxes.py create mode 100644 dev-tools/skills/pdf/scripts/check_fillable_fields.py create mode 100644 dev-tools/skills/pdf/scripts/convert_pdf_to_images.py create mode 100644 dev-tools/skills/pdf/scripts/create_validation_image.py create mode 100644 dev-tools/skills/pdf/scripts/extract_form_field_info.py create mode 100644 dev-tools/skills/pdf/scripts/extract_form_structure.py create mode 100644 dev-tools/skills/pdf/scripts/fill_fillable_fields.py create mode 100644 dev-tools/skills/pdf/scripts/fill_pdf_form_with_annotations.py create mode 100644 dev-tools/skills/php-dev/SKILL.md create mode 100644 dev-tools/skills/prompt-refine/SKILL.md create mode 100644 dev-tools/skills/react-patterns/SKILL.md create mode 100644 dev-tools/skills/react-patterns/references/composition-patterns.md create mode 100644 dev-tools/skills/react-patterns/references/custom-hooks.md create mode 100644 dev-tools/skills/skill-creator/SKILL.md create mode 100644 dev-tools/skills/skill-creator/references/anthropic-patterns.md create mode 100644 dev-tools/skills/skill-creator/references/descomplicar-standards.md create mode 100644 dev-tools/skills/skill-creator/references/output-patterns.md create mode 100644 dev-tools/skills/skill-creator/references/workflows.md create mode 100644 dev-tools/skills/webapp-testing/SKILL.md create mode 100644 dev-tools/skills/webapp-testing/examples/console_logging.py create mode 100644 dev-tools/skills/webapp-testing/examples/element_discovery.py create mode 100644 dev-tools/skills/webapp-testing/examples/static_html_automation.py create mode 100644 dev-tools/skills/webapp-testing/scripts/with_server.py create mode 100644 dev-tools/skills/xlsx/SKILL.md create mode 100644 dev-tools/skills/xlsx/scripts/office/helpers/__init__.py create mode 100644 dev-tools/skills/xlsx/scripts/office/helpers/merge_runs.py create mode 100644 dev-tools/skills/xlsx/scripts/office/helpers/simplify_redlines.py create mode 100644 dev-tools/skills/xlsx/scripts/office/pack.py create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/mce/mc.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd create mode 100644 dev-tools/skills/xlsx/scripts/office/soffice.py create mode 100644 dev-tools/skills/xlsx/scripts/office/unpack.py create mode 100644 dev-tools/skills/xlsx/scripts/office/validate.py create mode 100644 dev-tools/skills/xlsx/scripts/office/validators/__init__.py create mode 100644 dev-tools/skills/xlsx/scripts/office/validators/base.py create mode 100644 dev-tools/skills/xlsx/scripts/office/validators/docx.py create mode 100644 dev-tools/skills/xlsx/scripts/office/validators/pptx.py create mode 100644 dev-tools/skills/xlsx/scripts/office/validators/redlining.py create mode 100644 dev-tools/skills/xlsx/scripts/recalc.py create mode 100644 dev-tools/skills/youtube-extract/SKILL.md diff --git a/dev-tools/.claude-plugin/plugin.json b/dev-tools/.claude-plugin/plugin.json new file mode 100644 index 0000000..df37d71 --- /dev/null +++ b/dev-tools/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "dev-tools", + "description": "PHP, React, Next.js development, database design and frontend patterns. Backed by NotebookLM notebooks.", + "version": "1.0.0", + "author": { + "name": "Descomplicar - Crescimento Digital", + "url": "https://descomplicar.pt" + }, + "homepage": "https://git.descomplicar.pt/ealmeida/descomplicar-plugins", + "license": "MIT", + "keywords": ["php", "react", "nextjs", "database", "frontend", "desenvolvimento"] +} diff --git a/dev-tools/AGENTS.md b/dev-tools/AGENTS.md new file mode 100644 index 0000000..6bd84df --- /dev/null +++ b/dev-tools/AGENTS.md @@ -0,0 +1,17 @@ +# AGENTS.md — dev-tools + +Directrizes específicas deste projecto para agentes IA. + +## Referências obrigatórias + +- `ECOSYSTEM.md` — Hub/06-Operacoes/Documentacao/ECOSYSTEM.md +- `PROC-DEV-STANDARD` — D7-SIS-006 +- `PROC-AUDIT-STANDARD` — D7-SIS-007 + +## Protocolo de sessão + +Início: ler MEMORY.md + STATUS.md. Fim: actualizar ambos. + +## Convenções específicas + +- (a definir) diff --git a/dev-tools/CHANGELOG.md b/dev-tools/CHANGELOG.md new file mode 100644 index 0000000..a2466db --- /dev/null +++ b/dev-tools/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG — dev-tools + +## 0.1.0 — 2026-06-30 +### Adicionado +- Estrutura padrão (PROC-DEV-STANDARD) diff --git a/dev-tools/MEMORY.md b/dev-tools/MEMORY.md new file mode 100644 index 0000000..7eab5b0 --- /dev/null +++ b/dev-tools/MEMORY.md @@ -0,0 +1,4 @@ +# MEMORY.md — dev-tools + +## 2026-06-30 +- Estrutura padrão aplicada via propagate-structure.sh diff --git a/dev-tools/README.md b/dev-tools/README.md new file mode 100644 index 0000000..91e9e6a --- /dev/null +++ b/dev-tools/README.md @@ -0,0 +1,3 @@ +# dev-tools + +Subprojecto dev-tools do ecossistema Descomplicar. diff --git a/dev-tools/STATUS.md b/dev-tools/STATUS.md new file mode 100644 index 0000000..7eedfec --- /dev/null +++ b/dev-tools/STATUS.md @@ -0,0 +1,8 @@ +# STATUS.md — dev-tools + +**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** — + +## Estado actual +- Em curso: — +- Bloqueios: — +- Próximos passos: — diff --git a/dev-tools/agents/AGENTS.md b/dev-tools/agents/AGENTS.md new file mode 100644 index 0000000..e6608c3 --- /dev/null +++ b/dev-tools/agents/AGENTS.md @@ -0,0 +1,17 @@ +# AGENTS.md — agents + +Directrizes específicas deste projecto para agentes IA. + +## Referências obrigatórias + +- `ECOSYSTEM.md` — Hub/06-Operacoes/Documentacao/ECOSYSTEM.md +- `PROC-DEV-STANDARD` — D7-SIS-006 +- `PROC-AUDIT-STANDARD` — D7-SIS-007 + +## Protocolo de sessão + +Início: ler MEMORY.md + STATUS.md. Fim: actualizar ambos. + +## Convenções específicas + +- (a definir) diff --git a/dev-tools/agents/CHANGELOG.md b/dev-tools/agents/CHANGELOG.md new file mode 100644 index 0000000..c7fefdf --- /dev/null +++ b/dev-tools/agents/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG — agents + +## 0.1.0 — 2026-06-30 +### Adicionado +- Estrutura padrão (PROC-DEV-STANDARD) diff --git a/dev-tools/agents/MEMORY.md b/dev-tools/agents/MEMORY.md new file mode 100644 index 0000000..c42153f --- /dev/null +++ b/dev-tools/agents/MEMORY.md @@ -0,0 +1,4 @@ +# MEMORY.md — agents + +## 2026-06-30 +- Estrutura padrão aplicada via propagate-structure.sh diff --git a/dev-tools/agents/README.md b/dev-tools/agents/README.md new file mode 100644 index 0000000..d3e531d --- /dev/null +++ b/dev-tools/agents/README.md @@ -0,0 +1,3 @@ +# agents + +Subprojecto agents do ecossistema Descomplicar. diff --git a/dev-tools/agents/STATUS.md b/dev-tools/agents/STATUS.md new file mode 100644 index 0000000..05ba326 --- /dev/null +++ b/dev-tools/agents/STATUS.md @@ -0,0 +1,8 @@ +# STATUS.md — agents + +**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** — + +## Estado actual +- Em curso: — +- Bloqueios: — +- Próximos passos: — diff --git a/dev-tools/agents/database-design-specialist.md b/dev-tools/agents/database-design-specialist.md new file mode 100644 index 0000000..93ae386 --- /dev/null +++ b/dev-tools/agents/database-design-specialist.md @@ -0,0 +1,163 @@ +--- +name: database-design-specialist +description: "Especialista em design, optimizacao e gestao de bases de dados relacionais e nao-relacionais com foco em performance e escalabilidade" +role: Especialista em arquitectura de bases de dados +domain: Dev +model: sonnet +tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch + +# Dependencies +primary_mcps: + - desk-crm-v3 + - ssh-unified +recommended_mcps: + - gitea + - context7 + - filesystem +skills: + - _core +desk_task: 1498 +desk_project: 65 +milestone: 274 +tags: + - agent + - stackworkflow + - claude-code + - database + - sql + - optimization +version: "2.0" +status: active +quality_score: 70 +compliance: + sacred_rules: true + excellence_standards: true + data_sources: true + knowledge_first: true +reports_to: System Development Agent +collaborates_with: + - PHP Fullstack Engineer + - JavaScript Fullstack Specialist + - WordPress Plugin Developer + - Performance Optimization Engineer +created: "2025-01-13" +updated: "2026-02-04" +author: "Descomplicar®" +layer: wiki +--- + +# Database Design Specialist Descomplicar + +Especialista em arquitectura de bases de dados, entregando solucoes eficientes, escalaveis e seguras atraves de design optimizado e implementacao de boas praticas. + +## Responsabilidades + +- Design de schemas com normalizacao e consideracoes de performance +- Estrategias de indexacao e optimizacao de queries +- Implementacao de seguranca: access control, encriptacao, audit trails +- Planeamento de escalabilidade: partitioning, sharding +- Backup e disaster recovery com testes automatizados +- Migracao de dados e integracao multi-sistema + +## Knowledge Sources (Consultar SEMPRE) + +### NotebookLM (Primario - usar PRIMEIRO) + +``` +mcp__notebooklm__notebook_query notebook_id:"f9a79b5a-649f-4443-afaf-7ff562b6c2e7" query:"database SQL optimizacao" +mcp__notebooklm__notebook_query notebook_id:"24947ffa-0019-448a-a340-2f4a275d2eb1" query:"arquitectura dados" +``` + +## System Prompt + +### Papel +Especialista em arquitectura de bases de dados, entregando solucoes eficientes, escalaveis e seguras atraves de design optimizado e implementacao de boas praticas. + +### Regras Obrigatórias +1. SEMPRE normalizar schemas (3NF mínimo, desnormalizar só com justificação) +2. Indexes em TODAS as foreign keys +3. Backup strategy antes de migrations +4. NUNCA hardcode credentials (usar .env) +5. Documentar schema changes em CHANGELOG.md +6. Testes de performance antes e depois de optimizações + +### Output Format +- Diagramas ER (Mermaid ou drawSQL) +- Scripts SQL comentados +- Documentação de índices e razões +- Métricas de performance (antes/depois) +- Migration plan com rollback steps + +## Workflows + +### Workflow 1: Novo Schema Design +1. Análise de requisitos (entidades, relações, queries esperadas) +2. Normalização e modelação ER +3. Definição de índices estratégicos +4. Planeamento de partitioning (se grande volume) +5. Review de segurança (access control) +6. Documentação completa + +### Workflow 2: Optimização de Queries +1. Identificar slow queries (logs, APM) +2. EXPLAIN ANALYZE para cada query +3. Propor índices ou rewrites +4. Testar em staging +5. Medir impacto (antes/depois) +6. Deploy com monitoring + +### Workflow 3: Migration Segura +1. Backup completo +2. Testes em ambiente staging +3. Migration script com rollback +4. Execution em janela de manutenção +5. Validação pós-migration +6. Monitoring 24h + +## MCPs Relevantes +- `desk-crm-v3`: Acesso directo à BD Perfex para análise +- `ssh-unified`: Acesso MySQL em servidores +- `notebooklm`: KB primaria (Gemini 2.5 RAG) | ``: KB TI (SQL, optimização), AWS (RDS) + +## Colaboracao + +- Reports to: System Development Agent +- Colabora com: PHP Fullstack Engineer, JavaScript Fullstack Specialist, WordPress Plugin Developer, Performance Optimization Engineer + +## Your Available MCPs + +### Primary MCPs (Your Domain) +✓ **desk-crm-v3** (business) + - Clientes, projectos, facturas, time tracking + - Usage: `mcp__desk-crm-v3__*` + +✓ **ssh-unified** (infra) + - SSH, SFTP, servidor management + - Usage: `mcp__ssh-unified__*` + +✓ **** (knowledge) + - Knowledge base AI + - Usage: `mcp____*` + +### Recommended for dev +- **gitea** - Repositórios Git, issues, PRs +- **context7** - Context documentation +- **n8n** - Workflows automation +- **filesystem** - Ficheiros locais +- **magic** - AI-powered UI component generation (tipo v0.dev) +- **mem0** - Memória longo prazo +- **cwp** - CentOS Web Panel +- **puppeteer** - Browser automation + +### All Available (32 total) +moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, wikijs, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate + +**Discovery:** Use ToolSearch to find specific tools. +**Example:** `ToolSearch("ssh upload")` finds SSH upload tools. +## Your Available Skills + +### Primary Skills (Your Domain) +✓ **/wp-dev** - Desenvolvimento WordPress especializado - plugins, temas, WooCommerce. Usar para + - Invoke: `/wp-dev` + +✓ **/php-dev** - \ No newline at end of file diff --git a/dev-tools/agents/desk.project b/dev-tools/agents/desk.project new file mode 100644 index 0000000..5678920 --- /dev/null +++ b/dev-tools/agents/desk.project @@ -0,0 +1,6 @@ +{ + "task_id": null, + "project_id": null, + "customer_id": null, + "gitea_repo": null +} diff --git a/dev-tools/agents/dev-helper.md b/dev-tools/agents/dev-helper.md new file mode 100644 index 0000000..8232724 --- /dev/null +++ b/dev-tools/agents/dev-helper.md @@ -0,0 +1,188 @@ +--- +name: dev-helper +description: "USAR PROATIVAMENTE para code review, debugging, arquitectura software, boas práticas, refactoring, documentação técnica, troubleshooting. Assistente desenvolvimento generalista." +role: Assistente de desenvolvimento generalista +domain: Dev +model: sonnet +tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch + +# Dependencies +primary_mcps: + - desk-crm-v3 + - gitea + - filesystem +recommended_mcps: + - context7 + - ssh-unified + - mem0 +skills: + - _core +desk_task: 1500 +desk_project: 65 +milestone: 274 +tags: + - agent + - stackworkflow + - claude-code + - dev-helper + - code-review +version: "2.1" +status: active +quality_score: 75 +compliance: + sacred_rules: true + excellence_standards: true + data_sources: true + knowledge_first: true + security_audit: true +reports_to: Development Lead +collaborates_with: + - All development specialists + - System Architects + - Database Design Specialist + - Security Compliance Specialist +escalates_to: + - Development Lead (decisões arquitectura complexas) + - Security Compliance Specialist (vulnerabilidades críticas) +created: "2025-01-13" +updated: "2026-02-14" +author: "Descomplicar®" +layer: wiki +--- + +# Dev Helper Descomplicar + +Assistente de desenvolvimento generalista que ajuda com code review, debugging, decisões de arquitectura e orientação de boas práticas. Fornece assistência clara, concisa e prática. + +## System Prompt + +Você é um desenvolvedor experiente generalista responsável por: +- Ajudar com code review e identificação de bugs +- Orientar em decisões de arquitectura +- Fornecer guidance de boas práticas de desenvolvimento +- Criar documentação técnica clara +- Debugging e resolução de problemas +- Suporte a questões técnicas gerais + +### Regras OBRIGATÓRIAS (checklist antes de agir) + +- [ ] **Entender Contexto**: Ler código existente ANTES de sugerir mudanças +- [ ] **Boas Práticas**: Seguir SOLID, DRY, KISS +- [ ] **Segurança Primeiro**: Identificar vulnerabilidades (SQL injection, XSS) +- [ ] **Security Audit**: Se commit pendente, executar `pnpm audit` - 0 vulnerabilidades ou documentar +- [ ] **Performance**: Considerar impacto em escala +- [ ] **Manutenibilidade**: Código legível > código "clever" +- [ ] **Testar**: Validar sugestões quando possível +- [ ] **Documentar**: Explicar "porquê", não só "como" + +### Output Format Esperado + +```markdown +## Code Review: [Ficheiro/Componente] + +### Problemas Identificados +1. **[Tipo]**: [Descrição] + - Localização: [linha/função] + - Risco: Alto/Médio/Baixo + - Fix sugerido: [código ou explicação] + +### Melhorias Sugeridas +- [Melhoria 1] (performance/legibilidade/manutenção) +- [Melhoria 2] + +### Boas Práticas Violadas +- [ ] SOLID: [princípio] +- [ ] Security: [vulnerabilidade] +- [ ] Performance: [bottleneck] + +### Código Melhorado +```language +[código refactorado com comentários] +``` + +### Próximos Passos +1. [Acção prioritária] +2. [Acção secundária] +``` + +### MCPs a Usar + +| MCP | Quando Usar | +|-----|-------------| +| `` | Best practices, padrões arquitectura | +| `mem0` | Soluções anteriores similares | +| `gitea` | Consultar histórico código | + +## Responsabilidades + +- Code review e identificação de bugs +- Orientação em decisões de arquitectura +- Guidance de boas práticas de desenvolvimento +- Documentação técnica clara e útil +- Debugging e resolução de problemas +- Suporte a questões técnicas gerais + +## Workflows + +### 1. Code Review +``` +1. Ler código completo (contexto) +2. Verificar segurança (SQL injection, XSS, auth) +3. Avaliar performance (queries N+1, loops, caching) +4. Analisar legibilidade (nomes, estrutura) +5. Identificar code smells (duplicação, complexidade) +6. Sugerir melhorias com exemplos +7. Priorizar: crítico → importante → nice-to-have +``` + +### 2. Debugging +``` +1. Reproduzir problema (steps claros) +2. Isolar causa (logs, breakpoints) +3. Verificar casos edge +4. Propor fix com testes +5. Validar que não quebra nada +6. Documentar para evitar recorrência +``` + +### 3. Decisão Arquitectural +``` +1. Entender requisitos (funcionais + não-funcionais) +2. Listar opções viáveis (2-3 máximo) +3. Avaliar trade-offs: + - Performance vs. Complexidade + - Custo vs. Escalabilidade + - Time-to-market vs. Qualidade +4. Recomendar com justificação +5. Documentar decisão (ADR se relevante) +``` + +### 4. Criar Documentação +``` +1. Identificar audiência (dev júnior, ops, cliente) +2. Estrutura: Propósito → Como usar → Exemplos +3. Incluir diagramas se ajudar +4. Manter actualizada (parte do PR) +``` + +### 5. Pre-Commit Security Check +``` +OBRIGATÓRIO antes de git commit: + +1. Security Audit + pnpm audit # Node.js + composer audit # PHP + +2. Verificar dependências desactualizadas + pnpm outdated # Priorizar AI SDKs (langchain, together-ai) + +3. Environment Safety + git diff .env.local # Garantir placeholders, NUNCA keys reais + +4. Type Safety (se TypeScript) + pnpm type + +5. Decisão + - 0 vulnerabilidades → ✅ Commit safe + - Vulnerabilidades transitivas → Usar pnpm.overrides + - Vulnerabilidades críticas → Corrigir o \ No newline at end of file diff --git a/dev-tools/agents/development-lead.md b/dev-tools/agents/development-lead.md new file mode 100644 index 0000000..b707be9 --- /dev/null +++ b/dev-tools/agents/development-lead.md @@ -0,0 +1,165 @@ +--- +name: development-lead +description: "Lider tecnico de desenvolvimento especializado em coordenacao de equipas, arquitectura de solucoes e entrega de projectos com qualidade superior" +role: Líder de desenvolvimento e arquitectura +domain: Dev +model: sonnet +tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch + +# Dependencies +primary_mcps: + - desk-crm-v3 + - gitea + - ssh-unified +recommended_mcps: + - context7 + - google-workspace + - mem0 +skills: + - _core +desk_task: 1499 +desk_project: 65 +milestone: 274 +tags: + - agent + - stackworkflow + - claude-code + - leadership + - architecture +version: "2.0" +status: active +quality_score: 70 +compliance: + sacred_rules: true + excellence_standards: true + data_sources: true + knowledge_first: true +reports_to: Master Orchestrator +collaborates_with: + - PHP Fullstack Engineer + - JavaScript Fullstack Specialist + - WordPress Plugin Developer + - Infrastructure Lead + - Automation Lead +created: "2025-01-13" +updated: "2026-02-04" +author: "Descomplicar®" +layer: wiki +--- + +# Development Lead Descomplicar + +Lider de desenvolvimento responsavel por liderar equipas, garantir qualidade de codigo, decisoes arquitecturais e coordenar entrega de solucoes de software de alta qualidade. + +## Responsabilidades + +- Lideranca tecnica de equipas de desenvolvimento +- Arquitectura de codigo e standards de qualidade +- Planeamento de projectos e coordenacao de entrega +- Mentoring e desenvolvimento de skills da equipa +- Decisoes tecnicas e definicao de standards +- Integracao e colaboracao cross-team + +## Knowledge Sources (Consultar SEMPRE) + +### NotebookLM (Primario - usar PRIMEIRO) + +``` +mcp__notebooklm__notebook_query notebook_id:"24947ffa-0019-448a-a340-2f4a275d2eb1" query:"lideranca tecnica" +mcp__notebooklm__notebook_query notebook_id:"0c9c079c-a426-486c-99eb-1564d42d37ad" query:"coordenacao equipas" +mcp__notebooklm__notebook_query notebook_id:"79d43410-0e29-4be1-881d-84db6bdc239a" query:"arquitectura solucoes" +``` + +## System Prompt + +### Papel +Lider de desenvolvimento responsavel por liderar equipas, garantir qualidade de codigo, decisoes arquitecturais e coordenar entrega de solucoes de software de alta qualidade. + +### Regras Obrigatórias +1. SEMPRE code review antes de merge (Regra #9: Dev) +2. CHANGELOG.md actualizado em cada release +3. Git commits descritivos com referência a tarefas (#ID) +4. NUNCA skip de testes ou linting +5. Documentação técnica obrigatória para novas features +6. Sync Gitea após commits (Regra #22) + +### Output Format +- Arquitectura em diagramas (Mermaid/C4) +- Code review reports estruturados +- Tech debt tracking (priorizado) +- Roadmap técnico trimestral +- Post-mortems de incidents + +## Workflows + +### Workflow 1: Code Review +1. Verificar testes passam +2. Review lógica de negócio +3. Review segurança (SQL injection, XSS, auth) +4. Review performance (N+1, memory leaks) +5. Feedback construtivo ao autor +6. Approve ou request changes + +### Workflow 2: Decisão Arquitectural +1. Entender problema e contexto +2. Pesquisar soluções existentes (memory) +3. Propor 2-3 opções com tradeoffs +4. Discussão com stakeholders +5. Documentar decisão (ADR - Architecture Decision Record) +6. Comunicar à equipa + +### Workflow 3: Sprint Planning +1. Review backlog com Product Owner +2. Estimativa técnica (t-shirt sizing) +3. Identificar dependências e blockers +4. Alocação de tarefas por skills +5. Definir Definition of Done +6. Kickoff com equipa + +## MCPs Relevantes +- `desk-crm-v3`: Gestão de tarefas e projectos +- `gitea`: Gestão de repositórios e PRs +- `mem0`: Histórico de decisões técnicas +- `notebooklm`: KB primaria (Gemini 2.5 RAG) | ``: KB Desenvolvimento, Gestão Projectos + +## Colaboracao + +- Reports to: Master Orchestrator +- Colabora com: PHP Fullstack Engineer, JavaScript Fullstack Specialist, WordPress Plugin Developer, Infrastructure Lead, Automation Lead + +## Your Available MCPs + +### Primary MCPs (Your Domain) +✓ **desk-crm-v3** (business) + - Clientes, projectos, facturas, time tracking + - Usage: `mcp__desk-crm-v3__*` + +✓ **gitea** (dev) + - Repositórios Git, issues, PRs + - Usage: `mcp__gitea__*` + +✓ **** (knowledge) + - Knowledge base AI + - Usage: `mcp____*` + +✓ **mem0** (system) + - Memória longo prazo + - Usage: `mcp__mem_*` + +### Recommended for dev +- **ssh-unified** - SSH, SFTP, servidor management +- **context7** - Context documentation +- **n8n** - Workflows automation +- **filesystem** - Ficheiros locais +- **magic** - AI-powered UI component generation (tipo v0.dev) +- **cwp** - CentOS Web Panel +- **puppeteer** - Browser automation + +### All Available (32 total) +moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, wikijs, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate + +**Discovery:** Use ToolSearch to find specific tools. +**Example:** `ToolSearch("ssh upload")` finds SSH upload tools. +## Your Available Skills + +### Primary Ski \ No newline at end of file diff --git a/dev-tools/agents/index.md b/dev-tools/agents/index.md new file mode 100644 index 0000000..00c027f --- /dev/null +++ b/dev-tools/agents/index.md @@ -0,0 +1,8 @@ +# agents + +## Documentação + +- [README](README.md) +- [AGENTS](AGENTS.md) +- [STATUS](STATUS.md) +- [MEMORY](MEMORY.md) diff --git a/dev-tools/agents/javascript-fullstack-specialist.md b/dev-tools/agents/javascript-fullstack-specialist.md new file mode 100644 index 0000000..15250b5 --- /dev/null +++ b/dev-tools/agents/javascript-fullstack-specialist.md @@ -0,0 +1,159 @@ +--- +name: javascript-fullstack-specialist +description: Especialista em desenvolvimento JavaScript fullstack com expertise em + frameworks modernos, arquitecturas escalaveis e aplicacoes web modernas +role: Especialista em desenvolvimento JavaScript fullstack com expertise em frameworks + modernos, arquitecturas escalaveis e aplicacoes web modernas +domain: Dev +model: sonnet +tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch + +# Dependencies +primary_mcps: + - desk-crm-v3 + - gitea +recommended_mcps: + - context7 + - filesystem + - ssh-unified + - n8n +skills: +- _core +desk_task: 1504 +desk_project: 65 +milestone: 274 +tags: +- agent +- stackworkflow +- claude-code +- javascript +version: '2.0' +status: active +quality_score: 70 +compliance: + sacred_rules: true + excellence_standards: true + data_sources: true + knowledge_first: true +created: '2025-01-13' +updated: '2026-02-04' +author: Descomplicar® +layer: wiki +--- +# JavaScript Fullstack Specialist Descomplicar + +Especialista em desenvolvimento JavaScript fullstack, coordenando frontend e backend para entregar aplicacoes web modernas, escalaveis e performantes. + +## Responsabilidades +- Desenvolvimento frontend com React, Vue.js, Angular e TypeScript +- Desenvolvimento backend com Node.js, Express, Fastify, Nest.js +- Criacao de APIs RESTful e GraphQL com WebSockets +- Implementacao de testes automatizados (Jest, Cypress, Playwright) +- Optimizacao de performance e Core Web Vitals + +## Knowledge Sources (Consultar SEMPRE) + +### NotebookLM (Primario - usar PRIMEIRO) + +``` +mcp__notebooklm__notebook_query notebook_id:"24947ffa-0019-448a-a340-2f4a275d2eb1" query:"javascript typescript react node" +mcp__notebooklm__notebook_query notebook_id:"f9a79b5a-649f-4443-afaf-7ff562b6c2e7" query:"fullstack web development modern frameworks" +``` + +## System Prompt + +### Papel +Especialista em desenvolvimento JavaScript fullstack, coordenando frontend e backend para entregar aplicacoes web modernas, escalaveis e performantes. + +### Regras Obrigatórias +1. SEMPRE TypeScript em novos projectos (type safety) +2. Testes unitários obrigatórios (Jest/Vitest) +3. ESLint + Prettier configurados +4. NUNCA commit sem linting a passar +5. CHANGELOG.md actualizado (Regra #9) +6. Git commits descritivos + push Gitea (Regra #22) + +### Output Format +- Código TypeScript bem tipado +- Testes com >80% coverage +- Documentação JSDoc/TSDoc +- README com setup e deployment +- Performance metrics (Core Web Vitals) + +## Workflows + +### Workflow 1: Nova Feature Frontend +1. Design de componentes (estrutura, props, state) +2. Implementação com TypeScript +3. Styling (CSS Modules/Tailwind/Styled Components) +4. Unit tests (Jest + Testing Library) +5. Integration tests (Cypress/Playwright) +6. Code review + merge + +### Workflow 2: API REST/GraphQL +1. Design de endpoints (OpenAPI/GraphQL schema) +2. Implementação com validação (Zod/Joi) +3. Middleware (auth, logging, error handling) +4. Testes de integração +5. Documentação (Swagger/GraphQL Playground) +6. Deploy + monitoring + +### Workflow 3: Optimização Performance +1. Audit (Lighthouse, Core Web Vitals) +2. Identificar bottlenecks (Network, JS, Render) +3. Implementar optimizações (code splitting, lazy loading, caching) +4. Measure impacto (antes/depois) +5. Deploy gradual (canary/A-B) +6. Monitoring contínuo + +## MCPs Relevantes +- `gitea`: Gestão de repositórios e PRs +- `desk-crm-v3`: Tracking de tarefas +- `notebooklm`: KB primaria (Gemini 2.5 RAG) | ``: KB Desenvolvimento Software, TI +- `mem0`: Padrões e decisões técnicas + +## Colaboracao +- Reports to: Development Lead +- Colabora com: PHP Fullstack Engineer, Database Specialist, UI/UX designers + +## Your Available MCPs + +### Primary MCPs (Your Domain) +✓ **desk-crm-v3** (business) + - Clientes, projectos, facturas, time tracking + - Usage: `mcp__desk-crm-v3__*` + +✓ **gitea** (dev) + - Repositórios Git, issues, PRs + - Usage: `mcp__gitea__*` + +✓ **notebooklm** (knowledge primaria) +✓ **** (knowledge fallback) + - Knowledge base AI + - Usage: `mcp____*` + +✓ **mem0** (system) + - Memória longo prazo + - Usage: `mcp__mem_*` + +### Recommended for dev +- **ssh-unified** - SSH, SFTP, servidor management +- **context7** - Context documentation +- **n8n** - Workflows automation +- **filesystem** - Ficheiros locais +- **magic** - AI-powered UI component generation (tipo v0.dev) +- **cwp** - CentOS Web Panel +- **puppeteer** - Browser automation + +### All Available (32 total) +moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, wikijs, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate + +**Discovery:** Use ToolSearch to find specific tools. +**Example:** `ToolSearch("ssh upload")` finds SSH upload tools. +## Your Available Skills + +### Primary Skills (Your Domain) +✓ **/wp-dev** - Desenvolvimento WordPress especializado - plugins, temas, WooCommerce. Usar para + - Invoke: `/wp-dev` + +✓ **/php-dev** - Desenvolvimento PHP fullstack - L \ No newline at end of file diff --git a/dev-tools/agents/log.md b/dev-tools/agents/log.md new file mode 100644 index 0000000..66e9c4d --- /dev/null +++ b/dev-tools/agents/log.md @@ -0,0 +1 @@ +## 2026-06-30 init | Estrutura padrão diff --git a/dev-tools/agents/nextjs-specialist.md b/dev-tools/agents/nextjs-specialist.md new file mode 100644 index 0000000..23f6934 --- /dev/null +++ b/dev-tools/agents/nextjs-specialist.md @@ -0,0 +1,179 @@ +--- +name: nextjs-specialist +description: "USAR PROATIVAMENTE para Next.js, App Router, Server Components, Server Actions, Edge Runtime, Vercel, SSR, SSG, ISR. Especialista em Next.js moderno com React Server Components." +role: Especialista em desenvolvimento Next.js moderno +domain: Dev +model: sonnet +tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch + +# Dependencies +primary_mcps: + - desk-crm-v3 + - gitea +recommended_mcps: + - context7 + - filesystem + - stitch +skills: + - _core + - nextjs +desk_task: null +desk_project: 65 +milestone: 274 +tags: + - agent + - stackworkflow + - claude-code + - nextjs + - react + - fullstack +version: "2.0" +status: active +quality_score: 70 +compliance: + sacred_rules: true + excellence_standards: true + data_sources: true + knowledge_first: true +reports_to: Development Lead +collaborates_with: + - JavaScript Fullstack Specialist + - UI Designer + - Database Design Specialist +created: "2025-01-13" +updated: "2026-02-04" +author: "Descomplicar®" +layer: wiki +--- + +# Next.js Specialist + +## Propósito +Especialista em desenvolvimento Next.js - App Router, Server Components, e arquitecturas modernas. + +## Domínio +- Next.js 14+ App Router +- React Server Components +- Server Actions +- Edge Runtime +- Vercel deployment +- Full-stack Next.js + +## Triggers +- "Next.js", "Next", "Nextjs" +- "App Router", "Server Components" +- "Vercel", "Edge" +- "SSR", "SSG", "ISR" + +## Stack Técnica +- Next.js 14+ +- React 18+ +- TypeScript +- Tailwind CSS +- Prisma / Drizzle +- Vercel / Docker + +## Capacidades +1. Arquitectura App Router +2. Server Components vs Client Components +3. Data fetching patterns +4. Authentication (NextAuth/Clerk) +5. API Routes e Server Actions +6. Optimização de performance +7. Deploy em Vercel ou self-hosted + +## Patterns +- Streaming e Suspense +- Parallel Routes +- Intercepting Routes +- Route Handlers +- Middleware + +## Knowledge Sources (Consultar SEMPRE) + +### NotebookLM (Primario - usar PRIMEIRO) + +- Cloud e Infraestrutura TI (`f9a79b5a`) + +``` +mcp__notebooklm__notebook_query notebook_id:"f9a79b5a-649f-4443-afaf-7ff562b6c2e7" query:"" +``` +- Desenvolvimento Software +- TI +- React + +## Skill Associada +- `/nextjs` + +## Integração +Complementa javascript-fullstack-specialist para projectos Next.js específicos. + +## System Prompt + +### Papel +Especialista em desenvolvimento Next.js moderno com App Router, React Server Components e arquitecturas de alta performance. + +### Regras Obrigatórias +1. SEMPRE usar App Router (não Pages Router em projectos novos) +2. TypeScript obrigatório +3. Server Components por defeito, Client Components quando necessário +4. Server Actions para mutations (não API routes) +5. Metadata API para SEO +6. Testing (Vitest + Playwright) +7. SEMPRE aplicar Vercel Performance Rules (57 regras em `/nextjs` skill) +8. Waterfalls são o killer #1 - Promise.all() para operações independentes +9. Autenticar Server Actions como API routes (são endpoints públicos) +10. Minimizar serialização RSC - só campos necessários no cliente + +### Output Format +- Código Next.js 14+ bem estruturado +- README com dev setup e deploy +- Testes E2E críticos +- Performance metrics (Lighthouse) +- Documentação de decisões arquitecturais + +## Workflows + +### Workflow 1: Setup Projecto Next.js +1. `npx create-next-app@latest` (App Router, TypeScript, Tailwind) +2. Estrutura pastas: `/app`, `/components`, `/lib` +3. Configurar ESLint + Prettier +4. Setup testing (Vitest, Playwright) +5. Configurar ORM (Prisma/Drizzle) +6. Git init + primeiro commit + +### Workflow 2: Implementar Feature (Server-First) +1. Criar route em `/app/feature/page.tsx` +2. Server Component para fetching data +3. Client Components só para interactividade +4. Server Action para mutations +5. Loading/Error states +6. Metadata dinâmico + +### Workflow 3: Optimização Performance (Vercel Engineering Rules) +1. **Waterfalls (CRITICAL):** Audit awaits sequenciais → Promise.all(), Suspense boundaries +2. **Bundle Size (CRITICAL):** Eliminar barrel imports, dynamic imports >50KB, preload on hover +3. **Server-Side (HIGH):** React.cache() dedup, after() non-blocking, minimizar RSC serialização +4. **Client (MEDIUM-HIGH):** SWR dedup, passive listeners, localStorage versionado +5. **Re-renders (MEDIUM):** Derived state (não useEffect), functional setState, lazy init +6. **Rendering (MEDIUM):** content-visibility listas, hoist static JSX, useTransition +7. Audit Lighthouse (mobile + desktop) +8. Image Optimization (next/image) + Font (next/font) +9. Caching strategy (fetch cache, ISR, LRU cross-request) + +## MCPs Relevantes +- `gitea`: Gestão de repositórios Next.js +- `desk-crm-v3`: Tracking de desenvolvimento +- `notebooklm`: KB primaria (Gemini 2.5 RAG) | ``: KB Desenvolvimento Software, React, TI +- `ssh-unified`: Deploy (Docker ou Node.js) + +## Colaboracao + +- Reports to: Development Lead +- Colabora com: JavaScript Fullstack Specialist, UI Designer, Database Design Specialist + +## Your Available MCPs + +### Recommended for dev +- **ssh-unified** - SSH, SFTP, servidor management +- **gitea** - Repositórios Git, issues, P \ No newline at end of file diff --git a/dev-tools/agents/php-fullstack-engineer.md b/dev-tools/agents/php-fullstack-engineer.md new file mode 100644 index 0000000..758314e --- /dev/null +++ b/dev-tools/agents/php-fullstack-engineer.md @@ -0,0 +1,149 @@ +--- +# Core Identification +name: php-fullstack-engineer +description: > + Senior PHP fullstack engineer expert in Laravel, Symfony, and modern PHP 8+ development with + RESTful APIs, databases, and enterprise architecture. Use when user needs "PHP development", + "Laravel", "Symfony", "API development", "backend development", "MySQL optimization", + "GraphQL", "PHP 8+", "PSR standards", "Composer", "database design", "server-side programming". +author: Descomplicar® Crescimento Digital +version: 1.0.0 +desk_task: 1512 +category: dev +model: sonnet + +# Tools & Capabilities +tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch +user_invocable: true +tier: 1 + +# Dependencies +primary_mcps: + - desk-crm-v3 # Business operations + - gitea # Version control + - ssh-unified # Server deployment +recommended_mcps: + - context7 # Technical documentation + - n8n # Workflow automation + - filesystem # Local file operations + - magic # UI component generation + - mem0 # Long-term memory + - cwp # Server management + - puppeteer # Browser automation + +primary_skills: + - _core # Core patterns + - wp-dev # WordPress development + - php-dev # PHP development + - db-design # Database design + - mcp-dev # MCP development +recommended_skills: + - react-patterns # React patterns + - nextjs # Next.js development + - elementor # Elementor development + - woocommerce # WooCommerce optimization + - crm-admin # CRM administration + - video # Video creation + - doc-sync # Documentation sync + - delegate # Task delegation + - time # Time tracking + +primary_lsps: + - intelephense # PHP language server +recommended_lsps: + - typescript-language-server # TypeScript/JS + - sql-language-server # SQL queries + +# Team & Collaboration +sdks: + - Bases de Dados + - Claude Agents + - DeskDev + - MCP + - Segurança e Autenticação + +sdk_responsibilities: + claude_agents: "Sistema de agentes especializados para delegacao de tarefas via Task tool" + mcp: "Desenvolvimento de servidores MCP - criar, configurar, testar e publicar" + +collaborations: + - agent: wordpress-plugin-developer + type: technical + context: "WordPress and PHP integration" + - agent: database-specialist + type: technical + context: "Database architecture and optimization" + - agent: javascript-fullstack + type: cross-domain + context: "Full-stack integration and APIs" + +# Metadata +created: 2026-02-04 +last_updated: 2026-02-04 +quality_score: 85 +layer: wiki +--- + +# PHP Fullstack Engineer Descomplicar + +Especialista senior em desenvolvimento PHP fullstack com expertise em frameworks modernos (Laravel, Symfony), arquitectura de sistemas, bases de dados e integracao de APIs para solucoes empresariais. + +## Responsabilidades +- Desenvolver aplicacoes PHP 8+ com arquitectura MVC e padroes PSR +- Criar APIs RESTful e GraphQL com autenticacao e documentacao +- Optimizar queries MySQL/PostgreSQL e schemas de base de dados +- Implementar seguranca OWASP e validacao de dados +- Integrar sistemas WordPress, WooCommerce e PerfexCRM + +## Knowledge Sources (Consultar SEMPRE) + +### NotebookLM (Primario - usar PRIMEIRO) + +``` +mcp__notebooklm__notebook_query notebook_id:"24947ffa-0019-448a-a340-2f4a275d2eb1" query:"PHP Laravel Symfony APIs" +mcp__notebooklm__notebook_query notebook_id:"5be0d1a6-00f2-4cd9-b835-978cb7721601" query:"PHP WordPress WooCommerce" +mcp__notebooklm__notebook_query notebook_id:"f9a79b5a-649f-4443-afaf-7ff562b6c2e7" query:"backend MySQL PostgreSQL arquitectura" +``` + +## System Prompt + +### Papel +Especialista senior em desenvolvimento PHP fullstack com expertise em frameworks modernos (Laravel, Symfony), arquitectura de sistemas, bases de dados e integracao de APIs para solucoes empresariais. + +### Regras Obrigatórias +1. SEMPRE PHP 8+ com type hints +2. PSR-4 autoloading e PSR-12 coding standards +3. Composer para dependências +4. Testes unitários obrigatórios (PHPUnit) +5. NUNCA SQL directo (usar ORM ou Query Builder) +6. CHANGELOG.md + git workflow (Regra #9, #22) + +### Output Format +- Código PHP bem tipado e documentado (PHPDoc) +- Testes com >70% coverage +- API documentation (OpenAPI/Swagger) +- README com setup e deployment +- Database migrations versionadas + +## Workflows + +### Workflow 1: API RESTful (Laravel/Symfony) +1. Design endpoints (recursos, verbos HTTP) +2. Controllers com validation (Form Requests) +3. Services layer (business logic) +4. Repositories (data access) +5. Authentication (Sanctum/JWT) +6. Tests + documentation + +### Workflow 2: Integração WordPress/WooCommerce +1. Custom plugin structure +2. Hooks e filters adequados +3. OOP (classes, namespaces) +4. Database custom tables (se necessário) +5. AJAX endpoints seguros +6. Translation ready (i18n) + +### Workflow 3: Performance Optimization +1. Audit queries (Query Monitor, Debug Bar) +2. Implement caching (Redis/Memcached) +3. Lazy loading e eager lo \ No newline at end of file diff --git a/dev-tools/desk.project b/dev-tools/desk.project new file mode 100644 index 0000000..5678920 --- /dev/null +++ b/dev-tools/desk.project @@ -0,0 +1,6 @@ +{ + "task_id": null, + "project_id": null, + "customer_id": null, + "gitea_repo": null +} diff --git a/dev-tools/index.md b/dev-tools/index.md new file mode 100644 index 0000000..23e8ee8 --- /dev/null +++ b/dev-tools/index.md @@ -0,0 +1,8 @@ +# dev-tools + +## Documentação + +- [README](README.md) +- [AGENTS](AGENTS.md) +- [STATUS](STATUS.md) +- [MEMORY](MEMORY.md) diff --git a/dev-tools/knowledge/datasets.json b/dev-tools/knowledge/datasets.json new file mode 100644 index 0000000..d6edfb6 --- /dev/null +++ b/dev-tools/knowledge/datasets.json @@ -0,0 +1,51 @@ +{ + "description": "Knowledge sources for Development Tools domain", + "sources": { + "notebooklm": { + "description": "NotebookLM - conhecimento curado profundo via Gemini 2.5 RAG (PRIMARIO)", + "query_tool": "mcp__notebooklm__notebook_query", + "notebooks": [ + { + "id": "2876d1fe-5cea-4d98-8140-b0e1a81c6bc4", + "title": "Claude Code", + "topics": [ + "claude", + "code" + ] + }, + { + "id": "73102308-70ef-403e-9be9-eae0cfc62d55", + "title": "Desenvolvimento de MCPs", + "topics": [ + "mcp", + "servers" + ] + }, + { + "id": "57d9c6c9-48ba-4d83-8f71-cc890f348a53", + "title": "AI Code Editors", + "topics": [ + "code" + ] + }, + { + "id": "24947ffa-0019-448a-a340-2f4a275d2eb1", + "title": "Programacao", + "topics": [ + "desenvolvimento", + "software" + ] + }, + { + "id": "ebee9fe1-78fd-4f85-8938-f19f3ea32131", + "title": "Obsidian + Claude", + "topics": [ + "obsidian", + "claude", + "code" + ] + } + ] + } + } +} diff --git a/dev-tools/log.md b/dev-tools/log.md new file mode 100644 index 0000000..66e9c4d --- /dev/null +++ b/dev-tools/log.md @@ -0,0 +1 @@ +## 2026-06-30 init | Estrutura padrão diff --git a/dev-tools/skills/AGENTS.md b/dev-tools/skills/AGENTS.md new file mode 100644 index 0000000..37d90a2 --- /dev/null +++ b/dev-tools/skills/AGENTS.md @@ -0,0 +1,17 @@ +# AGENTS.md — skills + +Directrizes específicas deste projecto para agentes IA. + +## Referências obrigatórias + +- `ECOSYSTEM.md` — Hub/06-Operacoes/Documentacao/ECOSYSTEM.md +- `PROC-DEV-STANDARD` — D7-SIS-006 +- `PROC-AUDIT-STANDARD` — D7-SIS-007 + +## Protocolo de sessão + +Início: ler MEMORY.md + STATUS.md. Fim: actualizar ambos. + +## Convenções específicas + +- (a definir) diff --git a/dev-tools/skills/CHANGELOG.md b/dev-tools/skills/CHANGELOG.md new file mode 100644 index 0000000..5e14603 --- /dev/null +++ b/dev-tools/skills/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG — skills + +## 0.1.0 — 2026-06-30 +### Adicionado +- Estrutura padrão (PROC-DEV-STANDARD) diff --git a/dev-tools/skills/MEMORY.md b/dev-tools/skills/MEMORY.md new file mode 100644 index 0000000..4971060 --- /dev/null +++ b/dev-tools/skills/MEMORY.md @@ -0,0 +1,4 @@ +# MEMORY.md — skills + +## 2026-06-30 +- Estrutura padrão aplicada via propagate-structure.sh diff --git a/dev-tools/skills/README.md b/dev-tools/skills/README.md new file mode 100644 index 0000000..2ad232f --- /dev/null +++ b/dev-tools/skills/README.md @@ -0,0 +1,3 @@ +# skills + +Subprojecto skills do ecossistema Descomplicar. diff --git a/dev-tools/skills/STATUS.md b/dev-tools/skills/STATUS.md new file mode 100644 index 0000000..07c1590 --- /dev/null +++ b/dev-tools/skills/STATUS.md @@ -0,0 +1,8 @@ +# STATUS.md — skills + +**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** — + +## Estado actual +- Em curso: — +- Bloqueios: — +- Próximos passos: — diff --git a/dev-tools/skills/benchmark/SKILL.md b/dev-tools/skills/benchmark/SKILL.md new file mode 100644 index 0000000..0e6efa8 --- /dev/null +++ b/dev-tools/skills/benchmark/SKILL.md @@ -0,0 +1,125 @@ +--- +name: benchmark +description: > + Mede Core Web Vitals e performance antes/depois de alterações. Detecta regressões. + Usar antes e depois de qualquer alteração a WP, WooCommerce, Next.js ou infra. + Baseado no gstack /benchmark. Eixo 2B. +layer: wiki +--- + +# /benchmark — Performance Antes/Depois + +Medir o impacto real de alterações. Nunca fazer deploy sem benchmark. + +--- + +## Quando Usar + +- Antes de qualquer optimização de performance +- Antes de deploy de plugins/temas WordPress +- Antes de alterações ao servidor ou cache +- Antes de implementar nova feature em produção +- Como baseline mensal de cada site + +--- + +## Métricas Alvo (Core Web Vitals 2024) + +| Métrica | Bom | Precisa Melhorar | Mau | +|---------|-----|-----------------|-----| +| LCP (Largest Contentful Paint) | ≤2.5s | 2.5-4.0s | >4.0s | +| FID / INP (Interaction to Next Paint) | ≤200ms | 200-500ms | >500ms | +| CLS (Cumulative Layout Shift) | ≤0.1 | 0.1-0.25 | >0.25 | +| TTFB (Time to First Byte) | ≤800ms | 800-1800ms | >1800ms | +| FCP (First Contentful Paint) | ≤1.8s | 1.8-3.0s | >3.0s | + +--- + +## Protocolo + +### Passo 1 — Baseline (ANTES) + +```bash +# Via MCP Lighthouse (se disponível) +mcp__lighthouse__get_core_web_vitals({ url: "" }) +mcp__lighthouse__run_audit({ url: "", categories: ["performance"] }) + +# Ou via Lighthouse CLI no servidor +ssh server "lighthouse --output=json --output-path=/tmp/before.json --chrome-flags='--headless'" + +# Ou via PageSpeed Insights API +curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=&strategy=mobile" | jq '.lighthouseResult.categories.performance.score' +``` + +**Guardar baseline:** +```bash +BEFORE_LCP= +BEFORE_CLS= +BEFORE_FCP= +BEFORE_SCORE= +``` + +### Passo 2 — Executar Alteração + +Fazer a alteração (deploy, optimização, plugin, etc.) + +### Passo 3 — Depois + +```bash +# Repetir as mesmas medições +mcp__lighthouse__get_core_web_vitals({ url: "" }) + +AFTER_LCP= +AFTER_CLS= +AFTER_FCP= +AFTER_SCORE= +``` + +### Passo 4 — Comparação + +```markdown +## Benchmark — [Site] — [Data] + +| Métrica | Antes | Depois | Delta | Estado | +|---------|-------|--------|-------|--------| +| LCP | Xs | Ys | ±Zs | ✅/⚠️/❌ | +| CLS | X | Y | ±Z | ✅/⚠️/❌ | +| FCP | Xs | Ys | ±Zs | ✅/⚠️/❌ | +| Score | X% | Y% | ±Z% | ✅/⚠️/❌ | + +**Veredicto:** MELHOROU / NEUTRO / REGREDIU +**Acção:** Manter deploy / Reverter / Investigar +``` + +--- + +## Regras de Decisão + +``` +SE qualquer métrica piorou >10% → REVERTER imediatamente +SE Score global piorou >5 pontos → investigar antes de continuar +SE CLS aumentou >0.05 → bloquear — UX crítica +SE LCP piorou >500ms em mobile → reverter +``` + +--- + +## Sites Descomplicar a Monitorizar + +| Site | URL | Frequência | +|------|-----|-----------| +| Descomplicar main | descomplicar.pt | Mensal | +| Clip | clip.descomplicar.pt | Após cada deploy | +| Outros clientes | variável | Após alterações WP | + +--- + +## Healing Log + +```jsonl +{"date":"","issue":"","fix":"","source":"user|auto"} +``` + +--- + +*Skill /benchmark v1.0 | 06-04-2026 | Eixo 2B — gstack pattern* diff --git a/dev-tools/skills/canary/SKILL.md b/dev-tools/skills/canary/SKILL.md new file mode 100644 index 0000000..6a4ea08 --- /dev/null +++ b/dev-tools/skills/canary/SKILL.md @@ -0,0 +1,156 @@ +--- +name: canary +description: > + Monitorização pós-deploy — detecta regressões em produção. Verifica que o deploy + não quebrou nada crítico nos 15min seguintes. Baseado no gstack /canary. Eixo 2B. + Usar após qualquer deploy em produção. +layer: wiki +--- + +# /canary — Monitorização Pós-Deploy + +Os primeiros 15 minutos após um deploy são os mais críticos. Esta skill verifica que tudo está OK. + +--- + +## Quando Usar (SEMPRE após deploy produção) + +- Após deploy de código em produção +- Após actualização de WordPress (core, plugins, temas) +- Após alterações de servidor (PHP, Nginx, MySQL) +- Após mudanças de DNS ou SSL +- Após activação de nova funcionalidade + +--- + +## Protocolo (15 minutos) + +### Minuto 0-2 — Status Checks + +```bash +# 1. Site responde? +curl -s -o /dev/null -w "%{http_code}" / +# Esperado: 200 + +# 2. Admin WP responde? +curl -s -o /dev/null -w "%{http_code}" /wp-admin/ +# Esperado: 200 ou 302 + +# 3. SSL válido? +curl -vI 2>&1 | grep "SSL certificate verify" +# Esperado: SSL certificate verify ok + +# 4. Tempo de resposta aceitável? +curl -s -o /dev/null -w "%{time_total}\n" / +# Esperado: < 3.0 segundos +``` + +### Minuto 2-5 — Funcionalidades Críticas + +**Para WordPress:** +```bash +# Página principal carrega sem erros +curl -s / | grep -c "wp-content" +# Esperado: > 0 + +# Sem erro crítico PHP +curl -s / | grep -i "fatal error\|parse error\|warning" +# Esperado: sem output + +# WP-CLI status +wp --path=/var/www/html core verify-checksums 2>&1 | tail -1 +# Esperado: "WordPress installation verifies against checksums." +``` + +**Para aplicações:** +```bash +# Health endpoint +curl -s /api/health | jq '.status' +# Esperado: "ok" + +# Database conecta? +curl -s /api/health | jq '.database' +# Esperado: "connected" +``` + +### Minuto 5-10 — Métricas de Performance + +```javascript +// Via Lighthouse MCP +mcp__lighthouse__get_performance_score({ url: "" }) +// Esperado: >= baseline (ver /benchmark) + +mcp__lighthouse__get_core_web_vitals({ url: "" }) +// Comparar com baseline guardado +``` + +### Minuto 10-15 — Logs e Erros + +```bash +# Erros PHP nas últimas 15 min +ssh server "tail -100 /var/log/php/error.log | grep '$(date -d '15 minutes ago' +%H:%M)'" + +# Erros Nginx/Apache +ssh server "tail -50 /var/log/nginx/error.log" + +# WooCommerce (se aplicável) +wp --path=/var/www/html wc log list 2>&1 | head -20 +``` + +--- + +## Output — Relatório Canary + +```markdown +## Canary Check — [Site] — [Data] [Hora] + +**Deploy:** [O que foi alterado] + +| Check | Estado | Detalhe | +|-------|--------|---------| +| HTTP 200 | ✅/❌ | | +| SSL | ✅/❌ | | +| Tempo resposta | ✅/❌ | Xs | +| Sem erros PHP | ✅/❌ | | +| Performance score | ✅/❌ | X% (base: Y%) | +| Logs limpos | ✅/❌ | | + +**Resultado:** ✅ DEPLOY ESTÁVEL | ⚠️ INVESTIGAR | ❌ REVERTER +``` + +--- + +## Critérios de Reversão Imediata + +``` +❌ HTTP response != 200 → REVERTER +❌ SSL falha → REVERTER +❌ "Fatal error" em qualquer página → REVERTER +❌ Tempo resposta > 10s → INVESTIGAR +❌ Performance score caiu >15 pontos → INVESTIGAR +❌ Logs com erros críticos → INVESTIGAR +``` + +--- + +## Escalada Automática + +Se algum check falha: +``` +1. NOTIFICAR: criar issue no Desk CRM com urgência P1 +2. REVERTER: se erro crítico, reverter imediatamente +3. DOCUMENTAR: o que falhou e quando +4. ANALISAR: root cause antes de re-deploy +``` + +--- + +## Healing Log + +```jsonl +{"date":"","issue":"","fix":"","source":"user|auto"} +``` + +--- + +*Skill /canary v1.0 | 06-04-2026 | Eixo 2B — gstack pattern* diff --git a/dev-tools/skills/chrome/SKILL.md b/dev-tools/skills/chrome/SKILL.md new file mode 100644 index 0000000..58d9a76 --- /dev/null +++ b/dev-tools/skills/chrome/SKILL.md @@ -0,0 +1,158 @@ +--- +name: chrome +description: Integração com Chrome para automação browser — navegar páginas, preencher formulários, debug de consola, extracção de dados, testes e screenshots. +allowed-tools: Bash, Read, Write, Glob +layer: wiki +--- + +# /chrome - Chrome Browser Integration + +Integra Claude Code com o Chrome para automacao browser, testes, debug, performance e extraccao de dados. Suporta dois backends: **Claude in Chrome** (extensao) e **Chrome DevTools MCP** (npm). + +## Backends Disponiveis + +### Backend A: Claude in Chrome (Extensao) + +Extensao oficial Anthropic. Requer instalacao manual no browser. + +| Requisito | Minimo | Verificar | +|-----------|--------|-----------| +| Google Chrome | Instalado | `google-chrome --version` | +| Extensao Claude in Chrome | v1.0.36+ | `chrome://extensions` | +| Claude Code | v2.0.73+ | `claude --version` | +| Plano Claude | Pro/Max/Team/Enterprise | - | +| Native Host | Configurado | `~/.config/google-chrome/NativeMessagingHosts/com.anthropic.claude_code_browser_extension.json` | + +**Limitacoes:** Apenas Google Chrome (nao Brave/Arc). Nao suportado em WSL. + +**Activacao:** +```bash +# Por sessao +claude --chrome + +# Por defeito (alias .bashrc) +alias cc="claude --permission-mode bypassPermissions --chrome" + +# Dentro de sessao activa +/chrome -> "Enabled by default" + +# Ver ferramentas +/mcp -> seleccionar claude-in-chrome +``` + +### Backend B: Chrome DevTools MCP (npm) + +Pacote oficial Google (`chrome-devtools-mcp`). Controla Chrome via DevTools Protocol. Nao requer extensao. + +| Requisito | Minimo | Verificar | +|-----------|--------|-----------| +| Google Chrome | Instalado | `google-chrome --version` | +| Node.js | v18+ | `node --version` | +| chrome-devtools-mcp | latest | `claude mcp list` | + +**Instalacao:** +```bash +claude mcp add chrome-devtools --scope user -- npx -y chrome-devtools-mcp@latest +``` + +**Activacao:** +```bash +# Lancar Chrome com remote debugging +google-chrome --remote-debugging-port=9222 + +# Reiniciar sessao Claude Code para carregar ferramentas +``` + +**Ferramentas disponibilizadas:** +- `page_navigate`, `page_go_back`, `page_go_forward` - Navegacao +- `page_screenshot` - Captura de ecra +- `page_click`, `page_type`, `page_select_option` - Interaccao +- `page_execute_javascript` - Executar JS no contexto da pagina +- `page_wait_for_selector` - Esperar por elementos +- `console_get_logs` - Ler consola (logs, errors, warnings) +- `network_get_requests`, `network_get_response_body` - Inspeccionar rede +- `dom_get_element`, `dom_query_selector` - Inspeccionar DOM +- `css_get_computed_styles`, `css_get_matched_styles` - Inspeccionar CSS +- `performance_start_trace`, `performance_stop_trace` - Gravar traces +- `performance_analyze_insight` - Metricas automaticas (LCP, TBT, CLS) +- `accessibility_get_tree` - Arvore de acessibilidade +- `storage_get_cookies`, `storage_get_local_storage` - Inspecionar storage + +## Capacidades + +### 1. Navegacao e Interaccao +- Abrir URLs em novas tabs +- Clicar botoes, links, elementos +- Preencher formularios (input, select, textarea) +- Submeter forms +- Scroll e hover + +### 2. Debug e Diagnostico +- Ler console logs, errors, warnings +- Inspeccionar DOM state e CSS computado +- Verificar network requests e response bodies +- Ler JavaScript errors em tempo real +- Inspeccionar cookies, localStorage, sessionStorage + +### 3. Performance (DevTools MCP) +- Gravar performance traces completos +- Metricas automaticas: LCP, TBT, CLS, FCP, TTFB +- Identificar JavaScript nao utilizado +- Analise de recursos (tamanho, tempo de carga) +- Equivalente a Lighthouse audit on-demand + +### 4. Acessibilidade (DevTools MCP) +- Arvore de acessibilidade completa +- Verificar roles ARIA, labels, estados +- Identificar elementos sem texto alternativo + +### 5. Extraccao de Dados +- Extrair texto estruturado de paginas +- Scraping de tabelas, listas, precos +- Guardar dados como CSV/JSON local +- Ler conteudo de paginas autenticadas (Chrome extensao) +- Query DOM com selectores CSS/XPath + +### 6. Testes Web +- Testar forms com dados invalidos +- Verificar UI contra mocks +- Validar user flows completos +- Testar responsividade +- Monitorizar network requests durante testes + +### 7. Sites Autenticados (Chrome extensao) +- Google Docs, Gmail, Sheets +- Notion, Slack web +- CRMs, dashboards +- Qualquer site com sessao activa no Chrome + +### 8. Automacao +- Data entry repetitivo +- Multi-site workflows +- Gravar demos como GIF (Chrome extensao) +- Preencher CRMs a partir de CSV + +## Workflows Comuns + +### Testar aplicacao local +``` +Abre localhost:3000, tenta submeter o formulario com dados invalidos +e verifica se as mensagens de erro aparecem correctamente. +``` + +### Debug com console +``` +Abre a pagina do dashboard e verifica o console por erros +quando a pagina carrega. +``` + +### Preencher formularios em massa +``` +Tenho um ficheiro contacts.csv. Para cada linha, vai ao CRM em +crm.example.com, clica "Adicionar Contacto" e preenche nome, email e telefone. +``` + +### Escrever em Google Docs +``` +Escreve um resumo do projecto baseado nos commits recentes +no Google D \ No newline at end of file diff --git a/dev-tools/skills/db-design/SKILL.md b/dev-tools/skills/db-design/SKILL.md new file mode 100644 index 0000000..035982b --- /dev/null +++ b/dev-tools/skills/db-design/SKILL.md @@ -0,0 +1,192 @@ +--- +name: db-design +description: Design e optimização de schemas de base de dados — normalização, índices, planos de migração e particionamento. +layer: wiki +--- + +# Database Design Specialist + +Skill para design e optimização de bases de dados seguindo padrões Descomplicar®. + +## Quando Usar + +- Desenhar schema para novo sistema +- Optimizar queries lentas +- Criar migrations (Laravel, Doctrine, Raw SQL) +- Auditar e optimizar índices +- Planear partitioning/sharding + +## Protocolo Obrigatório + +### 1. Pesquisa Inicial +``` +mcp__mem_search_memories "database [projecto]" +mcp__wikijs__search_pages "database schema [sistema]" +``` + +### 2. Verificar Schema Existente +Antes de propor alterações, SEMPRE verificar estrutura actual. + +## Princípios de Design + +### Normalização +- **OLTP**: Mínimo 3NF (Third Normal Form) +- **OLAP/Reporting**: Desnormalização justificada +- **Híbrido**: Tabelas normalizadas + views materializadas + +### Naming Conventions +```sql +-- Tabelas: snake_case, plural +users, order_items, product_categories + +-- Colunas: snake_case, singular +created_at, user_id, is_active + +-- Primary keys +id (auto-increment ou UUID) + +-- Foreign keys: {tabela_singular}_id +user_id, order_id, category_id + +-- Índices: idx_{tabela}_{coluna(s)} +idx_orders_customer_id +idx_orders_created_at_status +``` + +## Performance + +### Índices Estratégicos +```sql +-- WHERE frequente +CREATE INDEX idx_users_email ON users(email); + +-- JOIN +CREATE INDEX idx_orders_customer_id ON orders(customer_id); + +-- ORDER BY +CREATE INDEX idx_products_created_at ON products(created_at DESC); + +-- Composto para queries específicas +CREATE INDEX idx_orders_status_date ON orders(status, created_at); +``` + +### Análise de Queries +```sql +-- SEMPRE usar EXPLAIN antes de optimizar +EXPLAIN ANALYZE +SELECT c.name, COUNT(o.id) as total +FROM customers c +LEFT JOIN orders o ON o.customer_id = c.id +WHERE o.created_at >= '2025-01-01' +GROUP BY c.id +ORDER BY total DESC +LIMIT 100; +``` + +### Evitar Anti-patterns +```sql +-- ❌ EVITAR +SELECT * FROM orders; -- Usar colunas específicas +SELECT ... WHERE YEAR(created_at) = 2025; -- Função impede uso de índice + +-- ✅ PREFERIR +SELECT id, customer_id, total FROM orders; +SELECT ... WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'; +``` + +## Template Migration Laravel + +```php +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('plan_id')->constrained(); + $table->enum('status', ['active', 'cancelled', 'expired', 'trial']); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('current_period_start'); + $table->timestamp('current_period_end'); + $table->timestamp('cancelled_at')->nullable(); + $table->timestamps(); + + // Índices + $table->index(['user_id', 'status']); + $table->index('current_period_end'); + }); + } + + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; +``` + +## Segurança + +- **Prepared statements**: SEMPRE para queries com input +- **Least privilege**: Utilizadores DB com permissões mínimas +- **Audit logging**: Para tabelas com dados sensíveis +- **Encriptação**: Colunas com PII devem ser encriptadas + +## Checklist Schema + +### Design +- [ ] Normalização adequada (sem redundância desnecessária) +- [ ] Foreign keys definidas +- [ ] Tipos de dados apropriados +- [ ] Constraints (NOT NULL, UNIQUE, CHECK) +- [ ] Soft deletes onde apropriado (deleted_at) + +### Performance +- [ ] Índices em colunas de WHERE +- [ ] Índices em colunas de JOIN +- [ ] Índices compostos onde necessário +- [ ] Evitados índices redundantes +- [ ] EXPLAIN em queries críticas + +### Segurança +- [ ] Dados sensíveis identificados +- [ ] Audit trail se necessário +- [ ] Prepared statements em todo código + +## Entregáveis Standard + +1. DDL completo (CREATE TABLE, INDEX) +2. Diagrama ER se schema complexo +3. Migrations se framework especificado +4. Queries exemplo para operações comuns +5. EXPLAIN de queries críticas + +--- + +## Datasets Dify (Consulta Obrigatória) + +Em caso de dúvidas ou para aprofundar conhecimento, consultar os seguintes datasets via MCP: + +| Dataset | ID | Prioridade | +|---------|----|-----------:| +| **TI (Tecnologia da Informação)** | `7f63ec0c-6321-488c-b107-980140199850` | 1 | +| **Desenvolvimento de Software** | `e7c7decc-0ded-4351-ab14-b110b3c38ec9` | 1 | +| **AWS (Amazon Web Services)** | `cc7f000a-ad86-49b6-b59b-179e65f8a229` | 2 | + +### Como Consultar + +```javascript +// Pesquisar optimiza \ No newline at end of file diff --git a/dev-tools/skills/dep-audit/SKILL.md b/dev-tools/skills/dep-audit/SKILL.md new file mode 100644 index 0000000..312fa16 --- /dev/null +++ b/dev-tools/skills/dep-audit/SKILL.md @@ -0,0 +1,214 @@ +--- +name: dep-audit +description: Auditoria de dependencias para projectos Node.js e PHP — vulnerabilidades, pacotes desactualizados, seguranca de ambiente e verificacao de tipos. Obrigatoria antes de commits. +disable-model-invocation: true +layer: wiki +--- + +# /dep-audit - Dependency Security Audit Pre-Commit + +> **Renomeado de /security-check (12-03-2026).** `/security-check` e agora a skill de auditoria de infraestrutura (plugin infraestrutura). + +Auditoria de segurança completa para projectos Node.js e PHP. **Obrigatória antes de commits** (Regra CLAUDE.md #47). + +## Contexto NotebookLM + +ANTES de executar, consultar notebooks para contexto especializado: + +| Notebook | ID | Consultar quando | +|----------|-----|-----------------| +| Programação | 24947ffa-0019-448a-a340-2f4a275d2eb1 | Para contexto de vulnerabilidades | + +``` +mcp__notebooklm__notebook_query({ + notebook_id: "24947ffa-0019-448a-a340-2f4a275d2eb1", + query: "" +}) +``` + +Integrar insights do NotebookLM nas recomendações e decisões. + +### Procedimentos relacionados + +- [PROC-DEV-SEC-001-Security-Audit.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/Seguranca/PROC-DEV-SEC-001-Security-Audit.md) +- [PROC-Security-Audit-API-Keys.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/Seguranca/PROC-Security-Audit-API-Keys.md) + +--- + +## Quando Usar + +### OBRIGATÓRIO: +- Antes de `git commit` +- Antes de `git push` +- Antes de criar Pull Request +- Antes de deploy para produção + +### RECOMENDADO: +- Após `pnpm add ` (especialmente AI SDKs) +- Semanalmente em projectos activos +- Após actualizar dependências major + +--- + +## Workflow Completo + +### 1. Detectar Tipo de Projecto + +```bash +# Verificar package managers +if [ -f "package.json" ]; then + # Node.js detected + MANAGER="pnpm" # ou npm/yarn +elif [ -f "composer.json" ]; then + # PHP detected + MANAGER="composer" +fi +``` + +### 2. Dependency Security Audit + +**Node.js:** +```bash +pnpm audit --json > ~/.claude-work/security-audit.json +``` + +**PHP:** +```bash +composer audit --format=json > ~/.claude-work/security-audit.json +``` + +**Parse resultados:** +- Contar: critical, high, moderate, low +- Identificar se são directas ou transitivas +- Listar pacotes afectados + +### 3. Outdated Dependencies + +**Node.js:** +```bash +pnpm outdated --json > ~/.claude-work/security-outdated.json +``` + +**Priorizar:** +- AI SDKs: langchain, together-ai, @tavily/core, @langchain/core +- Security packages: @auth/*, axios, form-data +- Framework core: next, react, prisma + +### 4. Environment Safety Check + +**Verificar ficheiros:** +```bash +# Lista de ficheiros a verificar +FILES=(.env .env.local .env.production .env.development) + +for file in "${FILES[@]}"; do + if [ -f "$file" ]; then + # Verificar se tem placeholders ou keys reais + grep -E "(API_KEY|SECRET|TOKEN|PASSWORD)" "$file" + fi +done +``` + +**Validação:** +- ✅ `API_KEY=placeholder` (safe) +- ❌ `API_KEY=sk-proj-abc123...` (UNSAFE - real key) + +### 5. Type Safety (se TypeScript) + +```bash +# Verificar se é TypeScript +if [ -f "tsconfig.json" ]; then + pnpm type # ou tsc --noEmit +fi +``` + +### 6. Análise de Risco AI SDKs + +**Padrões conhecidos:** + +| Pacote | Risco | Acção | +|--------|-------|-------| +| `langchain` | Alto | Verificar @langchain/core | +| `together-ai` | Alto | form-data transitiva | +| `@tavily/core` | Alto | axios transitiva | +| `@platejs/ai` | Médio | lodash transitiva | + +**Workflow AI SDKs:** +```bash +# 1. Verificar se @langchain/core está desactualizado +pnpm list @langchain/core + +# 2. Se versão < 0.3.80 (vulnerável) +pnpm add @langchain/core@latest + +# 3. Re-audit +pnpm audit +``` + +### 7. Gerar Relatório + +**Formato output:** + +```markdown +## Security Audit Report + +**Status:** [✅ SAFE | ⚠️ ISSUES FOUND | ❌ CRITICAL] +**Data:** YYYY-MM-DD HH:MM + +| Category | Status | +|----------|--------| +| Vulnerabilities | [N critical, M high, P moderate, Q low] | +| Outdated Packages | N total (M major updates) | +| Environment Safety | ✅ Placeholders only / ❌ Real keys found | +| Type Safety | ✅ 0 errors / ❌ N errors | + +### Vulnerabilities Detail + +#### Critical (N) +- package@version (via path) - description + - **Patched:** >=safe-version + - **Action:** [Override | Update | Replace] + +#### High (M) +[...] + +### Outdated Packages (Priority) + +**AI SDKs:** +- langchain: current → latest +- @langchain/core: current → latest (SECURITY UPDATE) + +**Framework:** +- next: current → latest + +### Action Required + +**Priority 1 (Critical):** +1. [Acção específica] + +**Priority 2 (High):** +1. [Acção específica] + +**Priority 3 (Maintenance):** +1. [Acção específica] + +### Safe to Commit? + +[✅ YES | ⚠️ YES (with documentation) | ❌ NO (fix critical first)] + +**If YES with documentation:** +``` +git commit -m "feat: feature X + +Known vulnerabilities (will be resolved): +- package@version (severity) - waiting for upstream update + +Mitigation: [explanation] +" +``` + +**If NO:** +Fix critical/high vulnerabilities first. See PROC-DEV-SEC-001 for guidance. +``` + +### 8. Criar F \ No newline at end of file diff --git a/dev-tools/skills/desk.project b/dev-tools/skills/desk.project new file mode 100644 index 0000000..5678920 --- /dev/null +++ b/dev-tools/skills/desk.project @@ -0,0 +1,6 @@ +{ + "task_id": null, + "project_id": null, + "customer_id": null, + "gitea_repo": null +} diff --git a/dev-tools/skills/diagrama/SKILL.md b/dev-tools/skills/diagrama/SKILL.md new file mode 100644 index 0000000..8597398 --- /dev/null +++ b/dev-tools/skills/diagrama/SKILL.md @@ -0,0 +1,153 @@ +--- +name: diagrama +category: dev-tools +description: "Gera diagramas de arquitectura, fluxo, ER e sequência em três formatos: draw.io (.drawio), Excalidraw e Mermaid. Usar quando 'diagrama', 'arquitectura', 'draw.io', 'fluxo', 'ER', 'sequência', 'infra', 'componentes'." +version: "1.0.0" +created: 2026-04-04 +tools: [mcp__claude_ai_Excalidraw__export_to_excalidraw, mcp__mcp-mermaid__generate, Write, Bash] +layer: wiki +--- + +# Skill: /diagrama + +Gera diagramas técnicos em três formatos consoante o caso de uso. Suporta arquitectura de sistemas, fluxogramas, diagramas ER, sequência, infra-estrutura e redes. + +--- + +## Quando usar cada formato + +| Formato | Quando usar | Output | +|---------|------------|--------| +| **draw.io** | Diagramas formais para documentação, propostas, clientes | `.drawio` (editável) | +| **Excalidraw** | Esboços rápidos, brainstorming visual, estilo whiteboard | `.excalidraw` / MCP | +| **Mermaid** | Diagramas como código, versionáveis em Git, embeds Markdown | `.svg` / `.png` | + +--- + +## Processo de execução + +### 1. Identificar pedido + +Quando o utilizador pede um diagrama, detectar: +- **Tipo:** arquitectura | fluxo | ER | sequência | rede | organograma | classe +- **Formato pretendido:** draw.io (default para doc. formal) | excalidraw | mermaid +- **Contexto:** sistemas envolvidos, entidades, actores + +Se não especificado, perguntar apenas: "draw.io (formal) ou Excalidraw (esboço)?" + +### 2. Gerar conteúdo + +--- + +## Geração draw.io (.drawio) + +O formato draw.io é mxGraph XML. Gerar o XML e guardar como `.drawio`. + +### Estrutura base + +```xml + + + + + + + + + + + +``` + +### Templates de elementos mxGraph + +**Rectângulo (componente/serviço):** +```xml + + + +``` + +**Seta/ligação:** +```xml + + + +``` + +**Cilindro (base de dados):** +```xml + + + +``` + +**Cloud (serviço externo):** +```xml + + + +``` + +**Actor (pessoa/utilizador):** +```xml + + + +``` + +**Swimlane (grupo/camada):** +```xml + + + +``` + +### Paleta de cores Descomplicar + +| Componente | fillColor | strokeColor | +|-----------|-----------|-------------| +| Serviço/App | `#dae8fc` | `#6c8ebf` | +| Base de dados | `#f8cecc` | `#b85450` | +| Fila/Queue | `#d5e8d4` | `#82b366` | +| Externo/Cloud | `#fff2cc` | `#d6b656` | +| Infra/Servidor | `#e1d5e7` | `#9673a6` | +| Claude/IA | `#f0f4ff` | `#4a6cf7` | + +### Guardar ficheiro + +Guardar em `/media/ealmeida/Dados/Hub/tmp/diagramas/` com nome descritivo: +``` +YYYY-MM-DD-nome-diagrama.drawio +``` + +Abrir com: +```bash +drawio "/media/ealmeida/Dados/Hub/tmp/diagramas/NOME.drawio" & +``` +(ou abrir manualmente com draw.io desktop / draw.io no browser) + +--- + +## Geração Excalidraw + +Usar o MCP `mcp__claude_ai_Excalidraw__export_to_excalidraw` para diagramas no estilo whiteboard. + +### Fluxo + +1. Chamar `mcp__claude_ai_Excalidraw__create_view` para iniciar sessão +2. Chamar `mcp__claude_ai_Excalidraw__export_to_excalidraw` com o conteúdo estruturado +3. Informar o utilizador do link/path gerado + +### Casos de uso preferidos + +- Brainstorming e exploração rápida +- Diagramas de fluxo com estilo manual +- Esboços para reuniões +- Arquitecturas de alto nível para comunicação interna + +--- + +## Geração Mermaid + +Usar o MCP `mcp__mcp-mermaid__generate` \ No newline at end of file diff --git a/dev-tools/skills/docx/SKILL-light.md b/dev-tools/skills/docx/SKILL-light.md new file mode 100644 index 0000000..8f5edf5 --- /dev/null +++ b/dev-tools/skills/docx/SKILL-light.md @@ -0,0 +1,160 @@ +--- +name: docx +description: "Criação, edição e análise de documentos Word (.docx). Versão light para heartbeats." +layer: wiki +--- + +# DOCX — Referência Rápida + +## Referência rápida + +| Tarefa | Abordagem | +|--------|-----------| +| Ler conteúdo | `pandoc` ou descompactar XML | +| Criar novo | `docx-js` (npm install -g docx) | +| Editar existente | Descompactar → editar XML → recompactar | +| Converter .doc | `python scripts/office/soffice.py --headless --convert-to docx doc.doc` | + +## Criar novos documentos (docx-js) + +### Setup básico + +```javascript +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, + Header, Footer, AlignmentType, PageOrientation, LevelFormat, HeadingLevel, + BorderStyle, WidthType, ShadingType, PageNumber, PageBreak } = require('docx'); + +const doc = new Document({ sections: [{ children: [/* conteúdo */] }] }); +Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); +``` + +### Tamanho de página (A4) + +```javascript +sections: [{ + properties: { + page: { + size: { width: 11906, height: 16838 }, // A4 em DXA + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } + } + }, + children: [/* conteúdo */] +}] +``` + +Largura de conteúdo A4 com margens 1": `11906 - 2880 = 9026 DXA` + +**Paisagem:** Passar dimensões retrato + `orientation: PageOrientation.LANDSCAPE` (docx-js troca internamente). + +### Listas + +```javascript +numbering: { config: [ + { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", + alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] } +] } +// Usar: new Paragraph({ numbering: { reference: "bullets", level: 0 }, children: [...] }) +``` + +### Tabelas + +```javascript +// Largura tabela = soma columnWidths. Usar SEMPRE WidthType.DXA (nunca PERCENTAGE). +new Table({ + width: { size: 9026, type: WidthType.DXA }, + columnWidths: [4513, 4513], + rows: [new TableRow({ children: [ + new TableCell({ + borders: { top: b, bottom: b, left: b, right: b }, + width: { size: 4513, type: WidthType.DXA }, + shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR, nunca SOLID + margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: [new TextRun("Célula")] })] + }) + ] })] +}) +``` + +### Imagens + +```javascript +new ImageRun({ + type: "png", // Obrigatório + data: fs.readFileSync("image.png"), + transformation: { width: 200, height: 150 }, + altText: { title: "T", description: "D", name: "N" } // Três obrigatórios +}) +``` + +### Cabeçalhos/rodapés + +```javascript +headers: { default: new Header({ children: [new Paragraph({ children: [new TextRun("Cabeçalho")] })] }) }, +footers: { default: new Footer({ children: [new Paragraph({ + children: [new TextRun("Página "), new TextRun({ children: [PageNumber.CURRENT] })] +})] }) } +``` + +### Regras críticas docx-js + +- Nunca usar `\n` — usar Paragraphs separados +- Nunca usar bullets unicode — usar `LevelFormat.BULLET` +- PageBreak dentro de Paragraph: `new Paragraph({ children: [new PageBreak()] })` +- ImageRun requer `type` +- Tabelas: `columnWidths` E `width` na célula, ambos devem corresponder +- Usar `ShadingType.CLEAR`, nunca SOLID +- TOC requer `HeadingLevel` apenas, sem estilos custom +- Override estilos: IDs exactos "Heading1", "Heading2" + `outlineLevel` + +## Editar documentos existentes + +### Passo 1: Descompactar + +```bash +python scripts/office/unpack.py document.docx unpacked/ +``` + +### Passo 2: Editar XML + +Editar ficheiros em `unpacked/word/`. Usar ferramenta Edit directamente (não scripts Python). + +**Tracked changes:** +```xml + + + texto inserido + + + + + texto eliminado + +``` + +**Comentários:** +```bash +python scripts/comment.py unpacked/ 0 "Texto do comentário" +python scripts/comment.py unpacked/ 1 "Resposta" --parent 0 +``` + +**Smart quotes:** `‘` `’` `“` `”` + +### Passo 3: Recompactar + +```bash +python scripts/office/pack.py unpacked/ output.docx --original document.docx +``` + +Validação: `python scripts/office/validate.py doc.docx` + +## Convenções Descomplicar + +- Data: DD-MM-YYYY +- Monetário: 1.234,56 EUR +- Fonte: Arial 12pt +- Página: A4 (11906 x 16838 DXA) +- Autor tracked changes: "Claude" +- Idioma: PT-PT + +--- +**Versão**: 1.0.0-light | **Base**: 1.0.0 diff --git a/dev-tools/skills/docx/SKILL.md b/dev-tools/skills/docx/SKILL.md new file mode 100644 index 0000000..1424f30 --- /dev/null +++ b/dev-tools/skills/docx/SKILL.md @@ -0,0 +1,145 @@ +--- +name: docx +description: "Criacao, edicao e analise de documentos Word (.docx). Usar quando o utilizador mencionar \"Word\", \".docx\", ou pedir documentos profissionais com formatacao (indices, cabecalhos, numeracao de paginas, letterheads). Inclui extracao de conteudo, insercao/substituicao de imagens, find-and-replace, tracked changes, comentarios, e conversao de conteudo para Word. Aplica-se a relatorios, memorandos, cartas, templates e propostas em formato .docx. NAO usar para PDFs, folhas de calculo, Google Docs, ou tarefas de codigo nao relacionadas." +layer: wiki +--- + +# Criacao, edicao e analise de documentos DOCX + +## Visao geral + +Um ficheiro .docx e um arquivo ZIP contendo ficheiros XML. + +## Referencia rapida + +| Tarefa | Abordagem | +|--------|-----------| +| Ler/analisar conteudo | `pandoc` ou descompactar para XML bruto | +| Criar novo documento | Usar `docx-js` - ver seccao Criar Novos Documentos | +| Editar documento existente | Descompactar -> editar XML -> recompactar - ver seccao Editar Documentos Existentes | + +### Converter .doc para .docx + +Ficheiros legacy `.doc` devem ser convertidos antes de editar: + +```bash +python scripts/office/soffice.py --headless --convert-to docx document.doc +``` + +### Ler conteudo + +```bash +# Extracao de texto com tracked changes +pandoc --track-changes=all document.docx -o output.md + +# Acesso ao XML bruto +python scripts/office/unpack.py document.docx unpacked/ +``` + +### Converter para imagens + +```bash +python scripts/office/soffice.py --headless --convert-to pdf document.docx +pdftoppm -jpeg -r 150 document.pdf page +``` + +### Aceitar tracked changes + +Para produzir um documento limpo com todas as alteracoes aceites (requer LibreOffice): + +```bash +python scripts/accept_changes.py input.docx output.docx +``` + +--- + +## Criar novos documentos + +Gerar ficheiros .docx com JavaScript, depois validar. Instalacao: `npm install -g docx` + +### Setup +```javascript +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, + Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, + TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType, + VerticalAlign, PageNumber, PageBreak } = require('docx'); + +const doc = new Document({ sections: [{ children: [/* conteudo */] }] }); +Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); +``` + +### Validacao +Apos criar o ficheiro, validar. Se a validacao falhar, descompactar, corrigir o XML e recompactar. +```bash +python scripts/office/validate.py doc.docx +``` + +### Tamanho da pagina + +```javascript +// CRITICO: docx-js assume A4 por defeito +// Definir sempre o tamanho da pagina explicitamente para resultados consistentes +// Para documentos PT/EU, A4 e o padrao correcto +sections: [{ + properties: { + page: { + size: { + width: 11906, // A4: 210mm em DXA + height: 16838 // A4: 297mm em DXA + }, + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 polegada de margens + } + }, + children: [/* conteudo */] +}] +``` + +**Tamanhos de pagina comuns (unidades DXA, 1440 DXA = 1 polegada):** + +| Papel | Largura | Altura | Largura conteudo (margens 1") | +|-------|---------|--------|-------------------------------| +| A4 (padrao PT/EU) | 11.906 | 16.838 | 9.026 | +| US Letter | 12.240 | 15.840 | 9.360 | + +**Orientacao paisagem:** docx-js troca largura/altura internamente, por isso passar dimensoes retrato e deixar a biblioteca tratar: +```javascript +size: { + width: 11906, // Passar lado CURTO como width + height: 16838, // Passar lado LONGO como height + orientation: PageOrientation.LANDSCAPE // docx-js troca no XML +}, +// Largura de conteudo = 16838 - margem esquerda - margem direita (usa o lado longo) +``` + +### Estilos (Override Built-in Headings) + +Usar Arial como fonte por defeito (suporte universal). Manter titulos a preto para legibilidade. + +```javascript +const doc = new Document({ + styles: { + default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt por defeito + paragraphStyles: [ + // IMPORTANTE: Usar IDs exactos para override dos estilos built-in + { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 32, bold: true, font: "Arial" }, + paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel necessario para TOC + { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 28, bold: true, font: "Arial" }, + paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, + ] + }, + sections: [{ + children: [ + new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Titulo")] }), + ] + }] +}); +``` + +### Listas (NUNCA usar bullets unicode) + +```javascript +// ERRADO - nunca inserir caracteres de bullet manualmente +new Paragraph({ children: [new TextRun("* Item")] }) // MAU +new Paragraph({ children: [new TextRun("\u2022 Item")] \ No newline at end of file diff --git a/dev-tools/skills/docx/scripts/__init__.py b/dev-tools/skills/docx/scripts/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/dev-tools/skills/docx/scripts/__init__.py @@ -0,0 +1 @@ + diff --git a/dev-tools/skills/docx/scripts/accept_changes.py b/dev-tools/skills/docx/scripts/accept_changes.py new file mode 100644 index 0000000..8e36316 --- /dev/null +++ b/dev-tools/skills/docx/scripts/accept_changes.py @@ -0,0 +1,135 @@ +"""Accept all tracked changes in a DOCX file using LibreOffice. + +Requires LibreOffice (soffice) to be installed. +""" + +import argparse +import logging +import shutil +import subprocess +from pathlib import Path + +from office.soffice import get_soffice_env + +logger = logging.getLogger(__name__) + +LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile" +MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard" + +ACCEPT_CHANGES_MACRO = """ + + + Sub AcceptAllTrackedChanges() + Dim document As Object + Dim dispatcher As Object + + document = ThisComponent.CurrentController.Frame + dispatcher = createUnoService("com.sun.star.frame.DispatchHelper") + + dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array()) + ThisComponent.store() + ThisComponent.close(True) + End Sub +""" + + +def accept_changes( + input_file: str, + output_file: str, +) -> tuple[None, str]: + input_path = Path(input_file) + output_path = Path(output_file) + + if not input_path.exists(): + return None, f"Error: Input file not found: {input_file}" + + if not input_path.suffix.lower() == ".docx": + return None, f"Error: Input file is not a DOCX file: {input_file}" + + try: + output_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(input_path, output_path) + except Exception as e: + return None, f"Error: Failed to copy input file to output location: {e}" + + if not _setup_libreoffice_macro(): + return None, "Error: Failed to setup LibreOffice macro" + + cmd = [ + "soffice", + "--headless", + f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}", + "--norestore", + "vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application", + str(output_path.absolute()), + ] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + check=False, + env=get_soffice_env(), + ) + except subprocess.TimeoutExpired: + return ( + None, + f"Successfully accepted all tracked changes: {input_file} -> {output_file}", + ) + + if result.returncode != 0: + return None, f"Error: LibreOffice failed: {result.stderr}" + + return ( + None, + f"Successfully accepted all tracked changes: {input_file} -> {output_file}", + ) + + +def _setup_libreoffice_macro() -> bool: + macro_dir = Path(MACRO_DIR) + macro_file = macro_dir / "Module1.xba" + + if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text(): + return True + + if not macro_dir.exists(): + subprocess.run( + [ + "soffice", + "--headless", + f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}", + "--terminate_after_init", + ], + capture_output=True, + timeout=10, + check=False, + env=get_soffice_env(), + ) + macro_dir.mkdir(parents=True, exist_ok=True) + + try: + macro_file.write_text(ACCEPT_CHANGES_MACRO) + return True + except Exception as e: + logger.warning(f"Failed to setup LibreOffice macro: {e}") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Accept all tracked changes in a DOCX file" + ) + parser.add_argument("input_file", help="Input DOCX file with tracked changes") + parser.add_argument( + "output_file", help="Output DOCX file (clean, no tracked changes)" + ) + args = parser.parse_args() + + _, message = accept_changes(args.input_file, args.output_file) + print(message) + + if "Error" in message: + raise SystemExit(1) diff --git a/dev-tools/skills/docx/scripts/comment.py b/dev-tools/skills/docx/scripts/comment.py new file mode 100644 index 0000000..36e1c93 --- /dev/null +++ b/dev-tools/skills/docx/scripts/comment.py @@ -0,0 +1,318 @@ +"""Add comments to DOCX documents. + +Usage: + python comment.py unpacked/ 0 "Comment text" + python comment.py unpacked/ 1 "Reply text" --parent 0 + +Text should be pre-escaped XML (e.g., & for &, ’ for smart quotes). + +After running, add markers to document.xml: + + ... commented content ... + + +""" + +import argparse +import random +import shutil +import sys +from datetime import datetime, timezone +from pathlib import Path + +import defusedxml.minidom + +TEMPLATE_DIR = Path(__file__).parent / "templates" +NS = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "w15": "http://schemas.microsoft.com/office/word/2012/wordml", + "w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid", + "w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex", +} + +COMMENT_XML = """\ + + + + + + + + + + + + + {text} + + +""" + +COMMENT_MARKER_TEMPLATE = """ +Add to document.xml (markers must be direct children of w:p, never inside w:r): + + ... + + """ + +REPLY_MARKER_TEMPLATE = """ +Nest markers inside parent {pid}'s markers (markers must be direct children of w:p, never inside w:r): + + ... + + + """ + + +def _generate_hex_id() -> str: + return f"{random.randint(0, 0x7FFFFFFE):08X}" + + +SMART_QUOTE_ENTITIES = { + "\u201c": "“", + "\u201d": "”", + "\u2018": "‘", + "\u2019": "’", +} + + +def _encode_smart_quotes(text: str) -> str: + for char, entity in SMART_QUOTE_ENTITIES.items(): + text = text.replace(char, entity) + return text + + +def _append_xml(xml_path: Path, root_tag: str, content: str) -> None: + dom = defusedxml.minidom.parseString(xml_path.read_text(encoding="utf-8")) + root = dom.getElementsByTagName(root_tag)[0] + ns_attrs = " ".join(f'xmlns:{k}="{v}"' for k, v in NS.items()) + wrapper_dom = defusedxml.minidom.parseString(f"{content}") + for child in wrapper_dom.documentElement.childNodes: + if child.nodeType == child.ELEMENT_NODE: + root.appendChild(dom.importNode(child, True)) + output = _encode_smart_quotes(dom.toxml(encoding="UTF-8").decode("utf-8")) + xml_path.write_text(output, encoding="utf-8") + + +def _find_para_id(comments_path: Path, comment_id: int) -> str | None: + dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8")) + for c in dom.getElementsByTagName("w:comment"): + if c.getAttribute("w:id") == str(comment_id): + for p in c.getElementsByTagName("w:p"): + if pid := p.getAttribute("w14:paraId"): + return pid + return None + + +def _get_next_rid(rels_path: Path) -> int: + dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8")) + max_rid = 0 + for rel in dom.getElementsByTagName("Relationship"): + rid = rel.getAttribute("Id") + if rid and rid.startswith("rId"): + try: + max_rid = max(max_rid, int(rid[3:])) + except ValueError: + pass + return max_rid + 1 + + +def _has_relationship(rels_path: Path, target: str) -> bool: + dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8")) + for rel in dom.getElementsByTagName("Relationship"): + if rel.getAttribute("Target") == target: + return True + return False + + +def _has_content_type(ct_path: Path, part_name: str) -> bool: + dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8")) + for override in dom.getElementsByTagName("Override"): + if override.getAttribute("PartName") == part_name: + return True + return False + + +def _ensure_comment_relationships(unpacked_dir: Path) -> None: + rels_path = unpacked_dir / "word" / "_rels" / "document.xml.rels" + if not rels_path.exists(): + return + + if _has_relationship(rels_path, "comments.xml"): + return + + dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8")) + root = dom.documentElement + next_rid = _get_next_rid(rels_path) + + rels = [ + ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", + "comments.xml", + ), + ( + "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", + "commentsExtended.xml", + ), + ( + "http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", + "commentsIds.xml", + ), + ( + "http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", + "commentsExtensible.xml", + ), + ] + + for rel_type, target in rels: + rel = dom.createElement("Relationship") + rel.setAttribute("Id", f"rId{next_rid}") + rel.setAttribute("Type", rel_type) + rel.setAttribute("Target", target) + root.appendChild(rel) + next_rid += 1 + + rels_path.write_bytes(dom.toxml(encoding="UTF-8")) + + +def _ensure_comment_content_types(unpacked_dir: Path) -> None: + ct_path = unpacked_dir / "[Content_Types].xml" + if not ct_path.exists(): + return + + if _has_content_type(ct_path, "/word/comments.xml"): + return + + dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8")) + root = dom.documentElement + + overrides = [ + ( + "/word/comments.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", + ), + ( + "/word/commentsExtended.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", + ), + ( + "/word/commentsIds.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml", + ), + ( + "/word/commentsExtensible.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml", + ), + ] + + for part_name, content_type in overrides: + override = dom.createElement("Override") + override.setAttribute("PartName", part_name) + override.setAttribute("ContentType", content_type) + root.appendChild(override) + + ct_path.write_bytes(dom.toxml(encoding="UTF-8")) + + +def add_comment( + unpacked_dir: str, + comment_id: int, + text: str, + author: str = "Claude", + initials: str = "C", + parent_id: int | None = None, +) -> tuple[str, str]: + word = Path(unpacked_dir) / "word" + if not word.exists(): + return "", f"Error: {word} not found" + + para_id, durable_id = _generate_hex_id(), _generate_hex_id() + ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + comments = word / "comments.xml" + first_comment = not comments.exists() + if first_comment: + shutil.copy(TEMPLATE_DIR / "comments.xml", comments) + _ensure_comment_relationships(Path(unpacked_dir)) + _ensure_comment_content_types(Path(unpacked_dir)) + _append_xml( + comments, + "w:comments", + COMMENT_XML.format( + id=comment_id, + author=author, + date=ts, + initials=initials, + para_id=para_id, + text=text, + ), + ) + + ext = word / "commentsExtended.xml" + if not ext.exists(): + shutil.copy(TEMPLATE_DIR / "commentsExtended.xml", ext) + if parent_id is not None: + parent_para = _find_para_id(comments, parent_id) + if not parent_para: + return "", f"Error: Parent comment {parent_id} not found" + _append_xml( + ext, + "w15:commentsEx", + f'', + ) + else: + _append_xml( + ext, + "w15:commentsEx", + f'', + ) + + ids = word / "commentsIds.xml" + if not ids.exists(): + shutil.copy(TEMPLATE_DIR / "commentsIds.xml", ids) + _append_xml( + ids, + "w16cid:commentsIds", + f'', + ) + + extensible = word / "commentsExtensible.xml" + if not extensible.exists(): + shutil.copy(TEMPLATE_DIR / "commentsExtensible.xml", extensible) + _append_xml( + extensible, + "w16cex:commentsExtensible", + f'', + ) + + action = "reply" if parent_id is not None else "comment" + return para_id, f"Added {action} {comment_id} (para_id={para_id})" + + +if __name__ == "__main__": + p = argparse.ArgumentParser(description="Add comments to DOCX documents") + p.add_argument("unpacked_dir", help="Unpacked DOCX directory") + p.add_argument("comment_id", type=int, help="Comment ID (must be unique)") + p.add_argument("text", help="Comment text") + p.add_argument("--author", default="Claude", help="Author name") + p.add_argument("--initials", default="C", help="Author initials") + p.add_argument("--parent", type=int, help="Parent comment ID (for replies)") + args = p.parse_args() + + para_id, msg = add_comment( + args.unpacked_dir, + args.comment_id, + args.text, + args.author, + args.initials, + args.parent, + ) + print(msg) + if "Error" in msg: + sys.exit(1) + cid = args.comment_id + if args.parent is not None: + print(REPLY_MARKER_TEMPLATE.format(pid=args.parent, cid=cid)) + else: + print(COMMENT_MARKER_TEMPLATE.format(cid=cid)) diff --git a/dev-tools/skills/docx/scripts/office/helpers/__init__.py b/dev-tools/skills/docx/scripts/office/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev-tools/skills/docx/scripts/office/helpers/merge_runs.py b/dev-tools/skills/docx/scripts/office/helpers/merge_runs.py new file mode 100644 index 0000000..ad7c25e --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/helpers/merge_runs.py @@ -0,0 +1,199 @@ +"""Merge adjacent runs with identical formatting in DOCX. + +Merges adjacent elements that have identical properties. +Works on runs in paragraphs and inside tracked changes (, ). + +Also: +- Removes rsid attributes from runs (revision metadata that doesn't affect rendering) +- Removes proofErr elements (spell/grammar markers that block merging) +""" + +from pathlib import Path + +import defusedxml.minidom + + +def merge_runs(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + _remove_elements(root, "proofErr") + _strip_run_rsid_attrs(root) + + containers = {run.parentNode for run in _find_elements(root, "r")} + + merge_count = 0 + for container in containers: + merge_count += _merge_runs_in(container) + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Merged {merge_count} runs" + + except Exception as e: + return 0, f"Error: {e}" + + + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def _get_child(parent, tag: str): + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + return child + return None + + +def _get_children(parent, tag: str) -> list: + results = [] + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(child) + return results + + +def _is_adjacent(elem1, elem2) -> bool: + node = elem1.nextSibling + while node: + if node == elem2: + return True + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + return False + + + + +def _remove_elements(root, tag: str): + for elem in _find_elements(root, tag): + if elem.parentNode: + elem.parentNode.removeChild(elem) + + +def _strip_run_rsid_attrs(root): + for run in _find_elements(root, "r"): + for attr in list(run.attributes.values()): + if "rsid" in attr.name.lower(): + run.removeAttribute(attr.name) + + + + +def _merge_runs_in(container) -> int: + merge_count = 0 + run = _first_child_run(container) + + while run: + while True: + next_elem = _next_element_sibling(run) + if next_elem and _is_run(next_elem) and _can_merge(run, next_elem): + _merge_run_content(run, next_elem) + container.removeChild(next_elem) + merge_count += 1 + else: + break + + _consolidate_text(run) + run = _next_sibling_run(run) + + return merge_count + + +def _first_child_run(container): + for child in container.childNodes: + if child.nodeType == child.ELEMENT_NODE and _is_run(child): + return child + return None + + +def _next_element_sibling(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + return sibling + sibling = sibling.nextSibling + return None + + +def _next_sibling_run(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + if _is_run(sibling): + return sibling + sibling = sibling.nextSibling + return None + + +def _is_run(node) -> bool: + name = node.localName or node.tagName + return name == "r" or name.endswith(":r") + + +def _can_merge(run1, run2) -> bool: + rpr1 = _get_child(run1, "rPr") + rpr2 = _get_child(run2, "rPr") + + if (rpr1 is None) != (rpr2 is None): + return False + if rpr1 is None: + return True + return rpr1.toxml() == rpr2.toxml() + + +def _merge_run_content(target, source): + for child in list(source.childNodes): + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name != "rPr" and not name.endswith(":rPr"): + target.appendChild(child) + + +def _consolidate_text(run): + t_elements = _get_children(run, "t") + + for i in range(len(t_elements) - 1, 0, -1): + curr, prev = t_elements[i], t_elements[i - 1] + + if _is_adjacent(prev, curr): + prev_text = prev.firstChild.data if prev.firstChild else "" + curr_text = curr.firstChild.data if curr.firstChild else "" + merged = prev_text + curr_text + + if prev.firstChild: + prev.firstChild.data = merged + else: + prev.appendChild(run.ownerDocument.createTextNode(merged)) + + if merged.startswith(" ") or merged.endswith(" "): + prev.setAttribute("xml:space", "preserve") + elif prev.hasAttribute("xml:space"): + prev.removeAttribute("xml:space") + + run.removeChild(curr) diff --git a/dev-tools/skills/docx/scripts/office/helpers/simplify_redlines.py b/dev-tools/skills/docx/scripts/office/helpers/simplify_redlines.py new file mode 100644 index 0000000..db963bb --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/helpers/simplify_redlines.py @@ -0,0 +1,197 @@ +"""Simplify tracked changes by merging adjacent w:ins or w:del elements. + +Merges adjacent elements from the same author into a single element. +Same for elements. This makes heavily-redlined documents easier to +work with by reducing the number of tracked change wrappers. + +Rules: +- Only merges w:ins with w:ins, w:del with w:del (same element type) +- Only merges if same author (ignores timestamp differences) +- Only merges if truly adjacent (only whitespace between them) +""" + +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path + +import defusedxml.minidom + +WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +def simplify_redlines(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + merge_count = 0 + + containers = _find_elements(root, "p") + _find_elements(root, "tc") + + for container in containers: + merge_count += _merge_tracked_changes_in(container, "ins") + merge_count += _merge_tracked_changes_in(container, "del") + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Simplified {merge_count} tracked changes" + + except Exception as e: + return 0, f"Error: {e}" + + +def _merge_tracked_changes_in(container, tag: str) -> int: + merge_count = 0 + + tracked = [ + child + for child in container.childNodes + if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag) + ] + + if len(tracked) < 2: + return 0 + + i = 0 + while i < len(tracked) - 1: + curr = tracked[i] + next_elem = tracked[i + 1] + + if _can_merge_tracked(curr, next_elem): + _merge_tracked_content(curr, next_elem) + container.removeChild(next_elem) + tracked.pop(i + 1) + merge_count += 1 + else: + i += 1 + + return merge_count + + +def _is_element(node, tag: str) -> bool: + name = node.localName or node.tagName + return name == tag or name.endswith(f":{tag}") + + +def _get_author(elem) -> str: + author = elem.getAttribute("w:author") + if not author: + for attr in elem.attributes.values(): + if attr.localName == "author" or attr.name.endswith(":author"): + return attr.value + return author + + +def _can_merge_tracked(elem1, elem2) -> bool: + if _get_author(elem1) != _get_author(elem2): + return False + + node = elem1.nextSibling + while node and node != elem2: + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + + return True + + +def _merge_tracked_content(target, source): + while source.firstChild: + child = source.firstChild + source.removeChild(child) + target.appendChild(child) + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]: + if not doc_xml_path.exists(): + return {} + + try: + tree = ET.parse(doc_xml_path) + root = tree.getroot() + except ET.ParseError: + return {} + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + + return authors + + +def _get_authors_from_docx(docx_path: Path) -> dict[str, int]: + try: + with zipfile.ZipFile(docx_path, "r") as zf: + if "word/document.xml" not in zf.namelist(): + return {} + with zf.open("word/document.xml") as f: + tree = ET.parse(f) + root = tree.getroot() + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + return authors + except (zipfile.BadZipFile, ET.ParseError): + return {} + + +def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str: + modified_xml = modified_dir / "word" / "document.xml" + modified_authors = get_tracked_change_authors(modified_xml) + + if not modified_authors: + return default + + original_authors = _get_authors_from_docx(original_docx) + + new_changes: dict[str, int] = {} + for author, count in modified_authors.items(): + original_count = original_authors.get(author, 0) + diff = count - original_count + if diff > 0: + new_changes[author] = diff + + if not new_changes: + return default + + if len(new_changes) == 1: + return next(iter(new_changes)) + + raise ValueError( + f"Multiple authors added new changes: {new_changes}. " + "Cannot infer which author to validate." + ) diff --git a/dev-tools/skills/docx/scripts/office/pack.py b/dev-tools/skills/docx/scripts/office/pack.py new file mode 100644 index 0000000..db29ed8 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/pack.py @@ -0,0 +1,159 @@ +"""Pack a directory into a DOCX, PPTX, or XLSX file. + +Validates with auto-repair, condenses XML formatting, and creates the Office file. + +Usage: + python pack.py [--original ] [--validate true|false] + +Examples: + python pack.py unpacked/ output.docx --original input.docx + python pack.py unpacked/ output.pptx --validate false +""" + +import argparse +import sys +import shutil +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + +def pack( + input_directory: str, + output_file: str, + original_file: str | None = None, + validate: bool = True, + infer_author_func=None, +) -> tuple[None, str]: + input_dir = Path(input_directory) + output_path = Path(output_file) + suffix = output_path.suffix.lower() + + if not input_dir.is_dir(): + return None, f"Error: {input_dir} is not a directory" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file" + + if validate and original_file: + original_path = Path(original_file) + if original_path.exists(): + success, output = _run_validation( + input_dir, original_path, suffix, infer_author_func + ) + if output: + print(output) + if not success: + return None, f"Error: Validation failed for {input_dir}" + + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + _condense_xml(xml_file) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: + for f in temp_content_dir.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(temp_content_dir)) + + return None, f"Successfully packed {input_dir} to {output_file}" + + +def _run_validation( + unpacked_dir: Path, + original_file: Path, + suffix: str, + infer_author_func=None, +) -> tuple[bool, str | None]: + output_lines = [] + validators = [] + + if suffix == ".docx": + author = "Claude" + if infer_author_func: + try: + author = infer_author_func(unpacked_dir, original_file) + except ValueError as e: + print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr) + + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file), + RedliningValidator(unpacked_dir, original_file, author=author), + ] + elif suffix == ".pptx": + validators = [PPTXSchemaValidator(unpacked_dir, original_file)] + + if not validators: + return True, None + + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + output_lines.append(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + output_lines.append("All validations PASSED!") + + return success, "\n".join(output_lines) if output_lines else None + + +def _condense_xml(xml_file: Path) -> None: + try: + with open(xml_file, encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + + for element in dom.getElementsByTagName("*"): + if element.tagName.endswith(":t"): + continue + + for child in list(element.childNodes): + if ( + child.nodeType == child.TEXT_NODE + and child.nodeValue + and child.nodeValue.strip() == "" + ) or child.nodeType == child.COMMENT_NODE: + element.removeChild(child) + + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + except Exception as e: + print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Pack a directory into a DOCX, PPTX, or XLSX file" + ) + parser.add_argument("input_directory", help="Unpacked Office document directory") + parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") + parser.add_argument( + "--original", + help="Original file for validation comparison", + ) + parser.add_argument( + "--validate", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Run validation with auto-repair (default: true)", + ) + args = parser.parse_args() + + _, message = pack( + args.input_directory, + args.output_file, + original_file=args.original, + validate=args.validate, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 0000000..6454ef9 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 0000000..afa4f46 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 0000000..64e66b8 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 0000000..687eea8 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 0000000..6ac81b0 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 0000000..1dbf051 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 0000000..f1af17d --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 0000000..0a185ab --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 0000000..14ef488 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 0000000..c20f3bf --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 0000000..ac60252 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 0000000..424b8ba --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 0000000..2bddce2 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 0000000..8a8c18b --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 0000000..5c42706 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 0000000..853c341 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 0000000..da835ee --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 0000000..87ad265 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 0000000..9e86f1b --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 0000000..d0be42e --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 0000000..8821dd1 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 0000000..ca2575c --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 0000000..dd079e6 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 0000000..3dd6cf6 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 0000000..f1041e3 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 0000000..9c5b7a6 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 0000000..0f13678 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 0000000..a6de9d2 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 0000000..10e978b --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 0000000..4248bf7 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 0000000..5649746 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/mce/mc.xsd b/dev-tools/skills/docx/scripts/office/schemas/mce/mc.xsd new file mode 100644 index 0000000..ef72545 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd new file mode 100644 index 0000000..f65f777 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd new file mode 100644 index 0000000..6b00755 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd new file mode 100644 index 0000000..f321d33 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 0000000..364c6a9 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 0000000..fed9d15 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 0000000..680cf15 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 0000000..89ada90 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dev-tools/skills/docx/scripts/office/soffice.py b/dev-tools/skills/docx/scripts/office/soffice.py new file mode 100644 index 0000000..c7f7e32 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/soffice.py @@ -0,0 +1,183 @@ +""" +Helper for running LibreOffice (soffice) in environments where AF_UNIX +sockets may be blocked (e.g., sandboxed VMs). Detects the restriction +at runtime and applies an LD_PRELOAD shim if needed. + +Usage: + from office.soffice import run_soffice, get_soffice_env + + # Option 1 – run soffice directly + result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"]) + + # Option 2 – get env dict for your own subprocess calls + env = get_soffice_env() + subprocess.run(["soffice", ...], env=env) +""" + +import os +import socket +import subprocess +import tempfile +from pathlib import Path + + +def get_soffice_env() -> dict: + env = os.environ.copy() + env["SAL_USE_VCLPLUGIN"] = "svp" + + if _needs_shim(): + shim = _ensure_shim() + env["LD_PRELOAD"] = str(shim) + + return env + + +def run_soffice(args: list[str], **kwargs) -> subprocess.CompletedProcess: + env = get_soffice_env() + return subprocess.run(["soffice"] + args, env=env, **kwargs) + + + +_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so" + + +def _needs_shim() -> bool: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.close() + return False + except OSError: + return True + + +def _ensure_shim() -> Path: + if _SHIM_SO.exists(): + return _SHIM_SO + + src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" + src.write_text(_SHIM_SOURCE) + subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], + check=True, + capture_output=True, + ) + src.unlink() + return _SHIM_SO + + + +_SHIM_SOURCE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +static int (*real_socket)(int, int, int); +static int (*real_socketpair)(int, int, int, int[2]); +static int (*real_listen)(int, int); +static int (*real_accept)(int, struct sockaddr *, socklen_t *); +static int (*real_close)(int); +static int (*real_read)(int, void *, size_t); + +/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */ +static int is_shimmed[1024]; +static int peer_of[1024]; +static int wake_r[1024]; /* accept() blocks reading this */ +static int wake_w[1024]; /* close() writes to this */ +static int listener_fd = -1; /* FD that received listen() */ + +__attribute__((constructor)) +static void init(void) { + real_socket = dlsym(RTLD_NEXT, "socket"); + real_socketpair = dlsym(RTLD_NEXT, "socketpair"); + real_listen = dlsym(RTLD_NEXT, "listen"); + real_accept = dlsym(RTLD_NEXT, "accept"); + real_close = dlsym(RTLD_NEXT, "close"); + real_read = dlsym(RTLD_NEXT, "read"); + for (int i = 0; i < 1024; i++) { + peer_of[i] = -1; + wake_r[i] = -1; + wake_w[i] = -1; + } +} + +/* ---- socket ---------------------------------------------------------- */ +int socket(int domain, int type, int protocol) { + if (domain == AF_UNIX) { + int fd = real_socket(domain, type, protocol); + if (fd >= 0) return fd; + /* socket(AF_UNIX) blocked – fall back to socketpair(). */ + int sv[2]; + if (real_socketpair(domain, type, protocol, sv) == 0) { + if (sv[0] >= 0 && sv[0] < 1024) { + is_shimmed[sv[0]] = 1; + peer_of[sv[0]] = sv[1]; + int wp[2]; + if (pipe(wp) == 0) { + wake_r[sv[0]] = wp[0]; + wake_w[sv[0]] = wp[1]; + } + } + return sv[0]; + } + errno = EPERM; + return -1; + } + return real_socket(domain, type, protocol); +} + +/* ---- listen ---------------------------------------------------------- */ +int listen(int sockfd, int backlog) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + listener_fd = sockfd; + return 0; + } + return real_listen(sockfd, backlog); +} + +/* ---- accept ---------------------------------------------------------- */ +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + /* Block until close() writes to the wake pipe. */ + if (wake_r[sockfd] >= 0) { + char buf; + real_read(wake_r[sockfd], &buf, 1); + } + errno = ECONNABORTED; + return -1; + } + return real_accept(sockfd, addr, addrlen); +} + +/* ---- close ----------------------------------------------------------- */ +int close(int fd) { + if (fd >= 0 && fd < 1024 && is_shimmed[fd]) { + int was_listener = (fd == listener_fd); + is_shimmed[fd] = 0; + + if (wake_w[fd] >= 0) { /* unblock accept() */ + char c = 0; + write(wake_w[fd], &c, 1); + real_close(wake_w[fd]); + wake_w[fd] = -1; + } + if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; } + if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; } + + if (was_listener) + _exit(0); /* conversion done – exit */ + } + return real_close(fd); +} +""" + + + +if __name__ == "__main__": + import sys + result = run_soffice(sys.argv[1:]) + sys.exit(result.returncode) diff --git a/dev-tools/skills/docx/scripts/office/unpack.py b/dev-tools/skills/docx/scripts/office/unpack.py new file mode 100644 index 0000000..0015253 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/unpack.py @@ -0,0 +1,132 @@ +"""Unpack Office files (DOCX, PPTX, XLSX) for editing. + +Extracts the ZIP archive, pretty-prints XML files, and optionally: +- Merges adjacent runs with identical formatting (DOCX only) +- Simplifies adjacent tracked changes from same author (DOCX only) + +Usage: + python unpack.py [options] + +Examples: + python unpack.py document.docx unpacked/ + python unpack.py presentation.pptx unpacked/ + python unpack.py document.docx unpacked/ --merge-runs false +""" + +import argparse +import sys +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from helpers.merge_runs import merge_runs as do_merge_runs +from helpers.simplify_redlines import simplify_redlines as do_simplify_redlines + +SMART_QUOTE_REPLACEMENTS = { + "\u201c": "“", + "\u201d": "”", + "\u2018": "‘", + "\u2019": "’", +} + + +def unpack( + input_file: str, + output_directory: str, + merge_runs: bool = True, + simplify_redlines: bool = True, +) -> tuple[None, str]: + input_path = Path(input_file) + output_path = Path(output_directory) + suffix = input_path.suffix.lower() + + if not input_path.exists(): + return None, f"Error: {input_file} does not exist" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {input_file} must be a .docx, .pptx, or .xlsx file" + + try: + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_path, "r") as zf: + zf.extractall(output_path) + + xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) + for xml_file in xml_files: + _pretty_print_xml(xml_file) + + message = f"Unpacked {input_file} ({len(xml_files)} XML files)" + + if suffix == ".docx": + if simplify_redlines: + simplify_count, _ = do_simplify_redlines(str(output_path)) + message += f", simplified {simplify_count} tracked changes" + + if merge_runs: + merge_count, _ = do_merge_runs(str(output_path)) + message += f", merged {merge_count} runs" + + for xml_file in xml_files: + _escape_smart_quotes(xml_file) + + return None, message + + except zipfile.BadZipFile: + return None, f"Error: {input_file} is not a valid Office file" + except Exception as e: + return None, f"Error unpacking: {e}" + + +def _pretty_print_xml(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="utf-8")) + except Exception: + pass + + +def _escape_smart_quotes(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + for char, entity in SMART_QUOTE_REPLACEMENTS.items(): + content = content.replace(char, entity) + xml_file.write_text(content, encoding="utf-8") + except Exception: + pass + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Unpack an Office file (DOCX, PPTX, XLSX) for editing" + ) + parser.add_argument("input_file", help="Office file to unpack") + parser.add_argument("output_directory", help="Output directory") + parser.add_argument( + "--merge-runs", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent runs with identical formatting (DOCX only, default: true)", + ) + parser.add_argument( + "--simplify-redlines", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent tracked changes from same author (DOCX only, default: true)", + ) + args = parser.parse_args() + + _, message = unpack( + args.input_file, + args.output_directory, + merge_runs=args.merge_runs, + simplify_redlines=args.simplify_redlines, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/dev-tools/skills/docx/scripts/office/validate.py b/dev-tools/skills/docx/scripts/office/validate.py new file mode 100644 index 0000000..03b01f6 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/validate.py @@ -0,0 +1,111 @@ +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py [--original ] [--auto-repair] [--author NAME] + +The first argument can be either: +- An unpacked directory containing the Office document XML files +- A packed Office file (.docx/.pptx/.xlsx) which will be unpacked to a temp directory + +Auto-repair fixes: +- paraId/durableId values that exceed OOXML limits +- Missing xml:space="preserve" on w:t elements with whitespace +""" + +import argparse +import sys +import tempfile +import zipfile +from pathlib import Path + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "path", + help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx)", + ) + parser.add_argument( + "--original", + required=False, + default=None, + help="Path to original file (.docx/.pptx/.xlsx). If omitted, all XSD errors are reported and redlining validation is skipped.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--auto-repair", + action="store_true", + help="Automatically repair common issues (hex IDs, whitespace preservation)", + ) + parser.add_argument( + "--author", + default="Claude", + help="Author name for redlining validation (default: Claude)", + ) + args = parser.parse_args() + + path = Path(args.path) + assert path.exists(), f"Error: {path} does not exist" + + original_file = None + if args.original: + original_file = Path(args.original) + assert original_file.is_file(), f"Error: {original_file} is not a file" + assert original_file.suffix.lower() in [".docx", ".pptx", ".xlsx"], ( + f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" + ) + + file_extension = (original_file or path).suffix.lower() + assert file_extension in [".docx", ".pptx", ".xlsx"], ( + f"Error: Cannot determine file type from {path}. Use --original or provide a .docx/.pptx/.xlsx file." + ) + + if path.is_file() and path.suffix.lower() in [".docx", ".pptx", ".xlsx"]: + temp_dir = tempfile.mkdtemp() + with zipfile.ZipFile(path, "r") as zf: + zf.extractall(temp_dir) + unpacked_dir = Path(temp_dir) + else: + assert path.is_dir(), f"Error: {path} is not a directory or Office file" + unpacked_dir = path + + match file_extension: + case ".docx": + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + if original_file: + validators.append( + RedliningValidator(unpacked_dir, original_file, verbose=args.verbose, author=args.author) + ) + case ".pptx": + validators = [ + PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + case _: + print(f"Error: Validation not supported for file type {file_extension}") + sys.exit(1) + + if args.auto_repair: + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + print(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/dev-tools/skills/docx/scripts/office/validators/__init__.py b/dev-tools/skills/docx/scripts/office/validators/__init__.py new file mode 100644 index 0000000..db092ec --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/validators/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/dev-tools/skills/docx/scripts/office/validators/base.py b/dev-tools/skills/docx/scripts/office/validators/base.py new file mode 100644 index 0000000..db4a06a --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/validators/base.py @@ -0,0 +1,847 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import defusedxml.minidom +import lxml.etree + + +class BaseSchemaValidator: + + IGNORED_VALIDATION_ERRORS = [ + "hyphenationZone", + "purl.org/dc/terms", + ] + + UNIQUE_ID_REQUIREMENTS = { + "comment": ("id", "file"), + "commentrangestart": ("id", "file"), + "commentrangeend": ("id", "file"), + "bookmarkstart": ("id", "file"), + "bookmarkend": ("id", "file"), + "sldid": ("id", "file"), + "sldmasterid": ("id", "global"), + "sldlayoutid": ("id", "global"), + "cm": ("authorid", "file"), + "sheet": ("sheetid", "file"), + "definedname": ("id", "file"), + "cxnsp": ("id", "file"), + "sp": ("id", "file"), + "pic": ("id", "file"), + "grpsp": ("id", "file"), + } + + EXCLUDED_ID_CONTAINERS = { + "sectionlst", + } + + ELEMENT_RELATIONSHIP_TYPES = {} + + SCHEMA_MAPPINGS = { + "word": "ISO-IEC29500-4_2016/wml.xsd", + "ppt": "ISO-IEC29500-4_2016/pml.xsd", + "xl": "ISO-IEC29500-4_2016/sml.xsd", + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file=None, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) if original_file else None + self.verbose = verbose + + self.schemas_dir = Path(__file__).parent.parent / "schemas" + + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + raise NotImplementedError("Subclasses must implement the validate method") + + def repair(self) -> int: + return self.repair_whitespace_preservation() + + def repair_whitespace_preservation(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if elem.tagName.endswith(":t") and elem.firstChild: + text = elem.firstChild.nodeValue + if text and (text.startswith((' ', '\t')) or text.endswith((' ', '\t'))): + if elem.getAttribute("xml:space") != "preserve": + elem.setAttribute("xml:space", "preserve") + text_preview = repr(text[:30]) + "..." if len(text) > 30 else repr(text) + print(f" Repaired: {xml_file.name}: Added xml:space='preserve' to {elem.tagName}: {text_preview}") + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + def validate_xml(self): + errors = [] + + for xml_file in self.xml_files: + try: + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + errors = [] + global_ids = {} + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} + + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + for elem in root.iter(): + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + if tag in self.UNIQUE_ID_REQUIREMENTS: + in_excluded_container = any( + ancestor.tag.split("}")[-1].lower() in self.EXCLUDED_ID_CONTAINERS + for ancestor in elem.iterancestors() + ) + if in_excluded_container: + continue + + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + errors = [] + + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): + all_files.append(file_path.resolve()) + + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + for rels_file in rels_files: + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + rels_dir = rels_file.parent + + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if target and not target.startswith( + ("http", "mailto:") + ): + if target.startswith("/"): + target_path = self.unpacked_dir / target.lstrip("/") + elif rels_file.name == ".rels": + target_path = self.unpacked_dir / target + else: + base_dir = rels_dir.parent + target_path = base_dir / target + + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + import lxml.etree + + errors = [] + + for xml_file in self.xml_files: + if xml_file.suffix == ".rels": + continue + + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + if not rels_file.exists(): + continue + + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + r_ns = self.OFFICE_RELATIONSHIPS_NAMESPACE + rid_attrs_to_check = ["id", "embed", "link"] + for elem in xml_root.iter(): + for attr_name in rid_attrs_to_check: + rid_attr = elem.get(f"{{{r_ns}}}{attr_name}") + if not rid_attr: + continue + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> r:{attr_name} references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + elif attr_name == "id" and self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + elem_lower = element_name.lower() + + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + if elem_lower.endswith("id") and len(elem_lower) > 2: + prefix = elem_lower[:-2] + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + if prefix == "sld": + return "slide" + return prefix.lower() + + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] + return prefix.lower() + + return None + + def validate_content_types(self): + errors = [] + + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", + "document", + "workbook", + "worksheet", + "theme", + } + + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue + + for file_path in all_files: + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() + elif is_valid: + return True, set() + + original_errors = self._get_original_file_errors(xml_file) + + assert current_errors is not None + new_errors = current_errors - original_errors + + new_errors = { + e for e in new_errors + if not any(pattern in e for pattern in self.IGNORED_VALIDATION_ERRORS) + } + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + original_error_count += 1 + valid_count += 1 + continue + + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + for attr in attrs_to_remove: + del elem.attrib[attr] + + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + elements_to_remove = [] + + for elem in list(root): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + self._remove_ignorable_elements(elem) + + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + root = xml_doc.getroot() + + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path): + schema_path = self._get_schema_path(xml_file) + if not schema_path: + return None, None + + try: + with open(schema_path, "rb") as xsd_file: + parser = lxml.etree.XMLParser() + xsd_doc = lxml.etree.parse( + xsd_file, parser=parser, base_url=str(schema_path) + ) + schema = lxml.etree.XMLSchema(xsd_doc) + + with open(xml_file, "r") as f: + xml_doc = lxml.etree.parse(f) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file): + if self.original_file is None: + return set() + + import tempfile + import zipfile + + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_path) + + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + return set() + + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + for elem in xml_copy.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/docx/scripts/office/validators/docx.py b/dev-tools/skills/docx/scripts/office/validators/docx.py new file mode 100644 index 0000000..fec405e --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/validators/docx.py @@ -0,0 +1,446 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import random +import re +import tempfile +import zipfile + +import defusedxml.minidom +import lxml.etree + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml" + W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid" + + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_whitespace_preservation(): + all_valid = False + + if not self.validate_deletions(): + all_valid = False + + if not self.validate_insertions(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_id_constraints(): + all_valid = False + + if not self.validate_comment_markers(): + all_valid = False + + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + if re.search(r"^[ \t\n\r]", text) or re.search( + r"[ \t\n\r]$", text + ): + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + for t_elem in root.xpath(".//w:del//w:t", namespaces=namespaces): + if t_elem.text: + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + for instr_elem in root.xpath( + ".//w:del//w:instrText", namespaces=namespaces + ): + text_preview = ( + repr(instr_elem.text or "")[:50] + "..." + if len(repr(instr_elem.text or "")) > 50 + else repr(instr_elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {instr_elem.sourceline}: found within (use ): {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + count = 0 + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + original = self.original_file + if original is None: + return 0 + + count = 0 + + try: + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(original, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + original_count = self.count_paragraphs_in_original() + new_count = self.count_paragraphs_in_unpacked() + + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})") + + def _parse_id_value(self, val: str, base: int = 16) -> int: + return int(val, base) + + def validate_id_constraints(self): + errors = [] + para_id_attr = f"{{{self.W14_NAMESPACE}}}paraId" + durable_id_attr = f"{{{self.W16CID_NAMESPACE}}}durableId" + + for xml_file in self.xml_files: + try: + for elem in lxml.etree.parse(str(xml_file)).iter(): + if val := elem.get(para_id_attr): + if self._parse_id_value(val, base=16) >= 0x80000000: + errors.append( + f" {xml_file.name}:{elem.sourceline}: paraId={val} >= 0x80000000" + ) + + if val := elem.get(durable_id_attr): + if xml_file.name == "numbering.xml": + try: + if self._parse_id_value(val, base=10) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} must be decimal in numbering.xml" + ) + else: + if self._parse_id_value(val, base=16) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except Exception: + pass + + if errors: + print(f"FAILED - {len(errors)} ID constraint violations:") + for e in errors: + print(e) + elif self.verbose: + print("PASSED - All paraId/durableId values within constraints") + return not errors + + def validate_comment_markers(self): + errors = [] + + document_xml = None + comments_xml = None + for xml_file in self.xml_files: + if xml_file.name == "document.xml" and "word" in str(xml_file): + document_xml = xml_file + elif xml_file.name == "comments.xml": + comments_xml = xml_file + + if not document_xml: + if self.verbose: + print("PASSED - No document.xml found (skipping comment validation)") + return True + + try: + doc_root = lxml.etree.parse(str(document_xml)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + range_starts = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeStart", namespaces=namespaces + ) + } + range_ends = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeEnd", namespaces=namespaces + ) + } + references = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentReference", namespaces=namespaces + ) + } + + orphaned_ends = range_ends - range_starts + for comment_id in sorted( + orphaned_ends, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeEnd id="{comment_id}" has no matching commentRangeStart' + ) + + orphaned_starts = range_starts - range_ends + for comment_id in sorted( + orphaned_starts, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeStart id="{comment_id}" has no matching commentRangeEnd' + ) + + comment_ids = set() + if comments_xml and comments_xml.exists(): + comments_root = lxml.etree.parse(str(comments_xml)).getroot() + comment_ids = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in comments_root.xpath( + ".//w:comment", namespaces=namespaces + ) + } + + marker_ids = range_starts | range_ends | references + invalid_refs = marker_ids - comment_ids + for comment_id in sorted( + invalid_refs, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + if comment_id: + errors.append( + f' document.xml: marker id="{comment_id}" references non-existent comment' + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append(f" Error parsing XML: {e}") + + if errors: + print(f"FAILED - {len(errors)} comment marker violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All comment markers properly paired") + return True + + def repair(self) -> int: + repairs = super().repair() + repairs += self.repair_durableId() + return repairs + + def repair_durableId(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if not elem.hasAttribute("w16cid:durableId"): + continue + + durable_id = elem.getAttribute("w16cid:durableId") + needs_repair = False + + if xml_file.name == "numbering.xml": + try: + needs_repair = ( + self._parse_id_value(durable_id, base=10) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + else: + try: + needs_repair = ( + self._parse_id_value(durable_id, base=16) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + + if needs_repair: + value = random.randint(1, 0x7FFFFFFE) + if xml_file.name == "numbering.xml": + new_id = str(value) + else: + new_id = f"{value:08X}" + + elem.setAttribute("w16cid:durableId", new_id) + print( + f" Repaired: {xml_file.name}: durableId {durable_id} → {new_id}" + ) + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/docx/scripts/office/validators/pptx.py b/dev-tools/skills/docx/scripts/office/validators/pptx.py new file mode 100644 index 0000000..09842aa --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/validators/pptx.py @@ -0,0 +1,275 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_uuid_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_slide_layout_ids(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_notes_slide_references(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + return all_valid + + def validate_uuid_ids(self): + import lxml.etree + + errors = [] + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(): + for attr, value in elem.attrib.items(): + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + if self._looks_like_uuid(value): + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + clean_value = value.strip("{}()").replace("-", "") + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + import lxml.etree + + errors = [] + + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + root = lxml.etree.parse(str(slide_master)).getroot() + + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + import lxml.etree + + errors = [] + notes_slide_references = {} + + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + target = rel.get("Target", "") + if target: + normalized_target = target.replace("../", "") + + slide_name = rels_file.stem.replace( + ".xml", "" + ) + + if normalized_target not in notes_slide_references: + notes_slide_references[normalized_target] = [] + notes_slide_references[normalized_target].append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/docx/scripts/office/validators/redlining.py b/dev-tools/skills/docx/scripts/office/validators/redlining.py new file mode 100644 index 0000000..71c81b6 --- /dev/null +++ b/dev-tools/skills/docx/scripts/office/validators/redlining.py @@ -0,0 +1,247 @@ +""" +Validator for tracked changes in Word documents. +""" + +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +class RedliningValidator: + + def __init__(self, unpacked_dir, original_docx, verbose=False, author="Claude"): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.author = author + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def repair(self) -> int: + return 0 + + def validate(self): + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + try: + import xml.etree.ElementTree as ET + + tree = ET.parse(modified_file) + root = tree.getroot() + + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + author_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + author_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + + if not author_del_elements and not author_ins_elements: + if self.verbose: + print(f"PASSED - No tracked changes by {self.author} found.") + return True + + except Exception: + pass + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + zip_ref.extractall(temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + try: + import xml.etree.ElementTree as ET + + modified_tree = ET.parse(modified_file) + modified_root = modified_tree.getroot() + original_tree = ET.parse(original_file) + original_root = original_tree.getroot() + except ET.ParseError as e: + print(f"FAILED - Error parsing XML files: {e}") + return False + + self._remove_author_tracked_changes(original_root) + self._remove_author_tracked_changes(modified_root) + + modified_text = self._extract_text_content(modified_root) + original_text = self._extract_text_content(original_root) + + if modified_text != original_text: + error_message = self._generate_detailed_diff( + original_text, modified_text + ) + print(error_message) + return False + + if self.verbose: + print(f"PASSED - All changes by {self.author} are properly tracked") + return True + + def _generate_detailed_diff(self, original_text, modified_text): + error_parts = [ + f"FAILED - Document text doesn't match after removing {self.author}'s tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend(["Differences:", "============", git_diff]) + else: + error_parts.append("Unable to generate word diff (git not available)") + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + pass + + return None + + def _remove_author_tracked_changes(self, root): + ins_tag = f"{{{self.namespaces['w']}}}ins" + del_tag = f"{{{self.namespaces['w']}}}del" + author_attr = f"{{{self.namespaces['w']}}}author" + + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag == ins_tag and child.get(author_attr) == self.author: + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + deltext_tag = f"{{{self.namespaces['w']}}}delText" + t_tag = f"{{{self.namespaces['w']}}}t" + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag == del_tag and child.get(author_attr) == self.author: + to_process.append((child, list(parent).index(child))) + + for del_elem, del_index in reversed(to_process): + for elem in del_elem.iter(): + if elem.tag == deltext_tag: + elem.tag = t_tag + + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + def _extract_text_content(self, root): + p_tag = f"{{{self.namespaces['w']}}}p" + t_tag = f"{{{self.namespaces['w']}}}t" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + text_parts = [] + for t_elem in p_elem.findall(f".//{t_tag}"): + if t_elem.text: + text_parts.append(t_elem.text) + paragraph_text = "".join(text_parts) + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/docx/scripts/templates/comments.xml b/dev-tools/skills/docx/scripts/templates/comments.xml new file mode 100644 index 0000000..cd01a7d --- /dev/null +++ b/dev-tools/skills/docx/scripts/templates/comments.xml @@ -0,0 +1,3 @@ + + + diff --git a/dev-tools/skills/docx/scripts/templates/commentsExtended.xml b/dev-tools/skills/docx/scripts/templates/commentsExtended.xml new file mode 100644 index 0000000..411003c --- /dev/null +++ b/dev-tools/skills/docx/scripts/templates/commentsExtended.xml @@ -0,0 +1,3 @@ + + + diff --git a/dev-tools/skills/docx/scripts/templates/commentsExtensible.xml b/dev-tools/skills/docx/scripts/templates/commentsExtensible.xml new file mode 100644 index 0000000..f5572d7 --- /dev/null +++ b/dev-tools/skills/docx/scripts/templates/commentsExtensible.xml @@ -0,0 +1,3 @@ + + + diff --git a/dev-tools/skills/docx/scripts/templates/commentsIds.xml b/dev-tools/skills/docx/scripts/templates/commentsIds.xml new file mode 100644 index 0000000..32f1629 --- /dev/null +++ b/dev-tools/skills/docx/scripts/templates/commentsIds.xml @@ -0,0 +1,3 @@ + + + diff --git a/dev-tools/skills/docx/scripts/templates/people.xml b/dev-tools/skills/docx/scripts/templates/people.xml new file mode 100644 index 0000000..3803d2d --- /dev/null +++ b/dev-tools/skills/docx/scripts/templates/people.xml @@ -0,0 +1,3 @@ + + + diff --git a/dev-tools/skills/index.md b/dev-tools/skills/index.md new file mode 100644 index 0000000..1d7c6b4 --- /dev/null +++ b/dev-tools/skills/index.md @@ -0,0 +1,8 @@ +# skills + +## Documentação + +- [README](README.md) +- [AGENTS](AGENTS.md) +- [STATUS](STATUS.md) +- [MEMORY](MEMORY.md) diff --git a/dev-tools/skills/log.md b/dev-tools/skills/log.md new file mode 100644 index 0000000..66e9c4d --- /dev/null +++ b/dev-tools/skills/log.md @@ -0,0 +1 @@ +## 2026-06-30 init | Estrutura padrão diff --git a/dev-tools/skills/nextjs/SKILL.md b/dev-tools/skills/nextjs/SKILL.md new file mode 100644 index 0000000..16e2451 --- /dev/null +++ b/dev-tools/skills/nextjs/SKILL.md @@ -0,0 +1,233 @@ +--- +name: nextjs +description: Desenvolvimento Next.js moderno (13+) com App Router, Server Components, rotas API e deployment seguindo padroes Descomplicar. +layer: wiki +--- + +# /nextjs - Next.js Development + +Desenvolvimento Next.js moderno (13+) com App Router e Server Components. + +--- + +## Contexto NotebookLM + +``` +mcp__notebooklm__notebook_query({ + notebook_id: "24947ffa-0019-448a-a340-2f4a275d2eb1", + query: "" +}) +``` + +--- + +## Regra #48 - Dev Container (OBRIGATÓRIO) + +**TODOS os projectos Next.js devem ser desenvolvidos no container dev.** + +``` +SSH: server="dev" (mcp__ssh-unified__ssh_execute) +Path: /root/Dev/ +Sync: auto -> /media/ealmeida/Dados/Dev/ (Syncthing) +``` + +**Workflow:** +1. `mcp__ssh-unified__ssh_execute server:"dev" command:"mkdir -p /root/Dev/"` +2. Desenvolver e testar no container +3. `npm run build` no container antes de deploy +4. Deploy via EasyPanel +5. Syncthing propaga automaticamente + +**NUNCA:** `npx create-next-app` directamente no PC local para projectos colaborativos. + +--- + +## App Router Structure + +``` +app/ +├── layout.tsx # Root layout (obrigatório) +├── page.tsx # Home page +├── loading.tsx # Loading UI (Suspense) +├── error.tsx # Error boundary +├── not-found.tsx # 404 page +├── (auth)/ # Route group (não afecta URL) +│ ├── login/page.tsx +│ └── register/page.tsx +├── dashboard/ +│ ├── layout.tsx +│ ├── page.tsx +│ └── [id]/page.tsx # Dynamic route +└── api/route.ts # API route +``` + +--- + +## Server vs Client Components + +### Server Component (default) + +```tsx +async function ProductsPage() { + const products = await db.product.findMany(); + return
{products.map(p => )}
; +} +``` + +### Client Component + +```tsx +'use client'; +import { useState } from 'react'; + +function Counter() { + const [count, setCount] = useState(0); + return ; +} +``` + +--- + +## Data Fetching Patterns + +```tsx +// Static (SSG) - default +const data = await fetch('https://api.example.com/data', { cache: 'force-cache' }); + +// Dynamic (SSR) +const data = await fetch('https://api.example.com/data', { cache: 'no-store' }); + +// ISR - revalidação automática +const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }); +``` + +--- + +## Server Actions + +```tsx +// app/actions.ts +'use server'; +import { revalidatePath } from 'next/cache'; + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string; + await db.post.create({ data: { title } }); + revalidatePath('/posts'); +} + +// Uso no form: +
+ + +
+``` + +--- + +## Routing Avançado + +### Middleware + +```tsx +// middleware.ts +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export function middleware(request: NextRequest) { + const token = request.cookies.get('token'); + if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { + return NextResponse.redirect(new URL('/login', request.url)); + } + return NextResponse.next(); +} + +export const config = { matcher: ['/dashboard/:path*'] }; +``` + +--- + +## Optimizações + +### Images +```tsx +import Image from 'next/image'; +Photo +``` + +### Metadata (SEO) +```tsx +export const metadata = { + title: 'Home', + description: 'Home page', + openGraph: { title: 'Home', images: ['/og-image.jpg'] }, +}; +``` + +--- + +## Performance (Prioridades) + +| Prioridade | Categoria | Ganho | +|---|---|---| +| CRITICAL | Eliminar Waterfalls (Promise.all) | 2-10x latência | +| CRITICAL | Bundle Size (dynamic imports) | 200-800ms load | +| HIGH | Server-Side (auth actions, React.cache) | RSC payload | +| MEDIUM | Re-renders (derived state, functional setState) | Responsividade | + +Para todas as 57 regras com exemplos de código: ver `references/performance-rules.md` + +--- + +## Deployment + +### Docker + +```dockerfile +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:18-alpine +WORKDIR /app +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package*.json ./ +RUN npm ci --production +EXPOSE 3000 +CMD ["npm", "start"] +``` + +--- + +## Datasets Dify + +| Dataset | ID | +|---------|-----| +| Desenvolvimento de Software | `e7c7decc-0ded-4351-ab14-b110b3c38ec9` | +| TI | `7f63ec0c-6321-488c-b107-980140199850` | + +--- + +## Referências + +- `references/performance-rules.md` - 57 regras de performance com exemplos completos + +--- + +*Versão 1.0.0 | Descomplicar®* + +--- + +## Healing Log + +Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar. + +```jsonl +{"date":"","issue":"","fix":"","source":"user|auto"} +``` + +*Adicionar nova linha após cada erro corrigido.* diff --git a/dev-tools/skills/nextjs/references/performance-rules.md b/dev-tools/skills/nextjs/references/performance-rules.md new file mode 100644 index 0000000..480299b --- /dev/null +++ b/dev-tools/skills/nextjs/references/performance-rules.md @@ -0,0 +1,245 @@ +# /nextjs - Performance Rules (Vercel Engineering) + +57 regras priorizadas por impacto. Fonte: Vercel Engineering react-best-practices. + +## 1. Eliminating Waterfalls (CRITICAL) + +```tsx +// ❌ ERRADO: Awaits sequenciais (waterfall) +const session = await getSession() +const config = await getConfig() +const user = await getUser(session.userId) + +// ✅ CORRECTO: Paralelo com Promise.all() +const [session, config] = await Promise.all([ + getSession(), + getConfig() +]) +const user = await getUser(session.userId) // depende de session + +// ✅ CORRECTO: Suspense boundaries estratégicos +async function Page() { + return ( + + }> +
+ + }> + + + + ) +} + +// ✅ CORRECTO: Defer await até necessário +async function handler(req) { + const dataPromise = fetchData() // inicia imediatamente + if (req.method === 'HEAD') return new Response() // early return + const data = await dataPromise // só espera quando precisa + return Response.json(data) +} +``` + +**Regras:** +- `async-defer-await`: Mover await para onde é necessário +- `async-dependency-parallel`: Usar `better-all` para dependências parciais +- `async-prevent-waterfall`: Iniciar promises antes de awaitar +- `async-promise-all`: `Promise.all()` para operações independentes +- `async-suspense-boundaries`: Suspense para UI progressiva + +--- + +## 2. Bundle Size (CRITICAL) + +```tsx +// ❌ ERRADO: Barrel file imports +import { Button, Icon } from '@mui/material' // carrega tudo + +// ✅ CORRECTO: Import directo +import Button from '@mui/material/Button' + +// ✅ CORRECTO: optimizePackageImports (Next.js 13.5+) +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material', 'lodash'] + } +} + +// ✅ CORRECTO: Dynamic imports para componentes pesados +const MonacoEditor = dynamic(() => import('./MonacoEditor'), { + ssr: false, + loading: () => +}) + +// ✅ CORRECTO: Preload em hover/focus +function FeatureButton() { + return ( + + ) +} +``` + +--- + +## 3. Server-Side Performance (HIGH) + +```tsx +// ✅ Server Actions: autenticar SEMPRE +'use server' +async function updateProfile(formData: FormData) { + const session = await auth() // 1. Autenticar + if (!session) throw new Error('Unauthorized') + const data = schema.parse(formData) // 2. Validar + await db.user.update({ where: { id: session.userId }, data }) + revalidatePath('/profile') +} + +// ✅ React.cache() para deduplicação per-request +const getUser = cache(async (id: string) => { + return db.user.findUnique({ where: { id } }) +}) + +// ✅ after() para operações não-bloqueantes +import { after } from 'next/server' +async function submitForm(data: FormData) { + const result = await processForm(data) + after(async () => { + await logAnalytics(result) // executa APÓS resposta enviada + }) + return result +} +``` + +--- + +## 4. Client-Side Data Fetching (MEDIUM-HIGH) + +```tsx +// ✅ SWR para deduplicação automática +const { data, isLoading } = useSWR('/api/user', fetcher) + +// ✅ Passive event listeners para scroll performance +element.addEventListener('scroll', handler, { passive: true }) + +// ✅ localStorage versionado com try-catch +const key = 'userConfig:v2' +try { + const data = JSON.parse(localStorage.getItem(key) ?? '{}') +} catch { /* incognito mode, quota exceeded */ } +``` + +--- + +## 5. Re-render Optimization (MEDIUM) + +```tsx +// ✅ Derived state: calcular durante render, NÃO em useEffect +function FilteredList({ items, query }: Props) { + const filtered = items.filter(i => i.name.includes(query)) // derive! + return +} + +// ✅ Functional setState para evitar stale closures +setItems(curr => [...curr, ...newItems]) // ✅ +// setItems([...items, ...newItems]) // ❌ stale closure + +// ✅ Lazy state initialization +const [index] = useState(() => buildSearchIndex(items)) // computa 1x + +// ✅ useTransition para updates não-urgentes +const [isPending, startTransition] = useTransition() +startTransition(() => setSearchResults(results)) + +// ✅ Extrair defaults não-primitivos para module scope +const EMPTY_ARRAY: string[] = [] +const NOOP = () => {} +function Component({ items = EMPTY_ARRAY, onClick = NOOP }) { ... } + +// ✅ Dependências de effect: primitivos, não objectos +useEffect(() => { fetchUser(userId) }, [userId]) // ✅ primitivo +``` + +--- + +## 6. Rendering Performance (MEDIUM) + +```tsx +// ✅ Hoist static JSX fora do componente +const skeleton =
// module scope + +// ✅ Conditional rendering explícito (evitar && com falsy) +{count > 0 ? {count} : null} // ✅ +// {count && {count}} // ❌ renderiza "0" + +// ✅ useTransition em vez de useState manual para loading +const [isPending, startTransition] = useTransition() +``` + +--- + +## 7. JavaScript Performance (LOW-MEDIUM) + +```tsx +// ✅ Index Maps para lookups repetidos O(1) vs O(n) +const userMap = new Map(users.map(u => [u.id, u])) +orders.map(o => ({ ...o, user: userMap.get(o.userId) })) + +// ✅ Set para membership checks +const allowedIds = new Set(['a', 'b', 'c']) +if (allowedIds.has(id)) { ... } // O(1) + +// ✅ toSorted() para imutabilidade (React-safe) +const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name)) +// items.sort() // ❌ muta o array original + +// ✅ Combinar iterações de array +const { active, expired } = items.reduce((acc, item) => { + if (item.active) acc.active.push(item) + else acc.expired.push(item) + return acc +}, { active: [], expired: [] }) +``` + +--- + +## 8. Advanced Patterns (LOW) + +```tsx +// ✅ Init uma vez, não por mount (Strict Mode remonta) +let didInit = false +function App() { + useEffect(() => { + if (didInit) return + didInit = true + initializeAnalytics() + }, []) +} + +// ✅ useEffectEvent para callbacks estáveis em effects +const onTick = useEffectEvent((value) => { + console.log(value) // sempre valor mais recente +}) +useEffect(() => { + const id = setInterval(() => onTick(count), 1000) + return () => clearInterval(id) +}, []) // sem dependência de count! +``` + +--- + +## Resumo de Impacto + +| Prioridade | Categoria | Ganho | +|---|---|---| +| CRITICAL | Waterfalls | 2-10x latência | +| CRITICAL | Bundle Size | 200-800ms load | +| HIGH | Server-Side | RSC payload, TTI | +| MEDIUM-HIGH | Client Fetch | Request dedup | +| MEDIUM | Re-renders | UI responsiveness | +| MEDIUM | Rendering | Paint performance | +| LOW-MEDIUM | JS Perf | CPU cycles | diff --git a/dev-tools/skills/pdf/SKILL-light.md b/dev-tools/skills/pdf/SKILL-light.md new file mode 100644 index 0000000..4f975d9 --- /dev/null +++ b/dev-tools/skills/pdf/SKILL-light.md @@ -0,0 +1,161 @@ +--- +name: pdf +description: Processamento de ficheiros PDF — leitura, extracção, merge, split, OCR, formulários. Versão light para heartbeats. +layer: wiki +--- + +# PDF — Referência Rápida + +## Bibliotecas Python + +### pypdf — operações básicas + +```python +from pypdf import PdfReader, PdfWriter + +# Ler e extrair texto +reader = PdfReader("documento.pdf") +for page in reader.pages: + text = page.extract_text() + +# Merge +writer = PdfWriter() +for pdf in ["doc1.pdf", "doc2.pdf"]: + for page in PdfReader(pdf).pages: + writer.add_page(page) +with open("merged.pdf", "wb") as f: + writer.write(f) + +# Split (uma página por ficheiro) +for i, page in enumerate(reader.pages): + w = PdfWriter() + w.add_page(page) + with open(f"page_{i+1}.pdf", "wb") as f: + w.write(f) + +# Rotate +page = reader.pages[0] +page.rotate(90) + +# Password +writer.encrypt("userpass", "ownerpass") +``` + +### pdfplumber — tabelas e texto com layout + +```python +import pdfplumber + +with pdfplumber.open("doc.pdf") as pdf: + for page in pdf.pages: + text = page.extract_text() + tables = page.extract_tables() +``` + +### reportlab — criação de PDFs + +```python +from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, TableStyle, PageBreak +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib import colors + +doc = SimpleDocTemplate("report.pdf") +styles = getSampleStyleSheet() +elements = [Paragraph("Título", styles['Title'])] +doc.build(elements) +``` + +**Subscripts/superscripts:** Nunca usar caracteres Unicode. Usar tags XML: `2`, `2`. + +## CLI + +```bash +# Extrair texto (poppler-utils) +pdftotext input.pdf output.txt +pdftotext -layout input.pdf output.txt + +# Merge/split (qpdf) +qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf +qpdf input.pdf --pages . 1-5 -- pages1-5.pdf + +# Encriptar +qpdf --encrypt user_pass owner_pass 256 -- input.pdf encrypted.pdf + +# Reparar +qpdf --check input.pdf + +# Extrair imagens +pdfimages -j input.pdf output_prefix + +# Render para PNG +pdftoppm -png -r 300 document.pdf output_prefix +``` + +## OCR (PDFs digitalizados) + +```python +import pytesseract +from pdf2image import convert_from_path + +images = convert_from_path('scanned.pdf') +text = "" +for image in images: + text += pytesseract.image_to_string(image) +``` + +## Formulários + +### Verificar campos preenchíveis + +```bash +python scripts/check_fillable_fields.py +``` + +### Campos preenchíveis (nativos) + +1. `python scripts/extract_form_field_info.py ` +2. `python scripts/convert_pdf_to_images.py ` +3. Criar `field_values.json` com `field_id`, `page`, `value` +4. `python scripts/fill_fillable_fields.py ` + +### Campos não preenchíveis (anotações) + +1. `python scripts/extract_form_structure.py form_structure.json` +2. **Abordagem A (estrutura):** Analisar JSON, criar `fields.json` com `pdf_width`/`pdf_height` e bounding boxes +3. **Abordagem B (visual):** Converter para imagens, identificar campos, refinar com crop +4. `python scripts/check_bounding_boxes.py fields.json` +5. `python scripts/fill_pdf_form_with_annotations.py fields.json ` +6. `python scripts/create_validation_image.py ` + +## Referência rápida + +| Tarefa | Ferramenta | Comando | +|--------|-----------|---------| +| Merge | pypdf/qpdf | `writer.add_page()` / `qpdf --pages` | +| Split | pypdf/qpdf | Uma página por ficheiro | +| Texto | pdfplumber | `page.extract_text()` | +| Tabelas | pdfplumber | `page.extract_tables()` | +| Criar | reportlab | Canvas ou Platypus | +| OCR | pytesseract | Converter para imagem primeiro | +| Formulários | pypdf/scripts | Ver secção acima | + +## Caminhos Descomplicar + +| Local | Caminho | +|-------|---------| +| Documentos empresa | `/media/ealmeida/Dados/GDrive/Cloud/Descomplicar/` | +| Propostas | `/media/ealmeida/Dados/Hub/03-Propostas/` | +| Arquivo clientes | `/media/ealmeida/Dados/GDrive/Arquivo_de_Clientes/` | +| Knowledge Base | `/media/ealmeida/Dados/Hub/06-Operacoes/Knowledge-Base/PDFs/` | +| Temporários | `~/.claude-work/` (limpar ao concluir) | + +## Workflow + +1. Localizar PDF (filesystem ou Google Drive) +2. Descarregar para `~/.claude-work/` se necessário +3. Processar +4. Guardar no destino final +5. Limpar temporários + +--- +**Versão**: 1.0.0-light | **Base**: 1.0.0 diff --git a/dev-tools/skills/pdf/SKILL.md b/dev-tools/skills/pdf/SKILL.md new file mode 100644 index 0000000..2a0c9ec --- /dev/null +++ b/dev-tools/skills/pdf/SKILL.md @@ -0,0 +1,197 @@ +--- +name: pdf +description: Processamento completo de ficheiros PDF — leitura, extraccao de texto/tabelas, merge, split, watermarks, encriptacao, OCR, criacao e preenchimento de formularios. +layer: wiki +--- + +# PDF Processing Guide + +## Resumo + +Guia completo para processamento de PDFs com bibliotecas Python e ferramentas de linha de comandos. Para formularios PDF, seguir as instruccoes na seccao "Preenchimento de formularios". Para funcionalidades avancadas e bibliotecas JavaScript, consultar a seccao "Referencia avancada". + +## Quick Start + +```python +from pypdf import PdfReader, PdfWriter + +# Read a PDF +reader = PdfReader("/media/ealmeida/Dados/GDrive/Cloud/Descomplicar/documento.pdf") +print(f"Pages: {len(reader.pages)}") + +# Extract text +text = "" +for page in reader.pages: + text += page.extract_text() +``` + +## Bibliotecas Python + +### pypdf — operacoes basicas + +#### Merge PDFs +```python +from pypdf import PdfWriter, PdfReader + +writer = PdfWriter() +for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]: + reader = PdfReader(pdf_file) + for page in reader.pages: + writer.add_page(page) + +with open("merged.pdf", "wb") as output: + writer.write(output) +``` + +#### Split PDF +```python +reader = PdfReader("input.pdf") +for i, page in enumerate(reader.pages): + writer = PdfWriter() + writer.add_page(page) + with open(f"page_{i+1}.pdf", "wb") as output: + writer.write(output) +``` + +#### Extract Metadata +```python +reader = PdfReader("document.pdf") +meta = reader.metadata +print(f"Title: {meta.title}") +print(f"Author: {meta.author}") +print(f"Subject: {meta.subject}") +print(f"Creator: {meta.creator}") +``` + +#### Rotate Pages +```python +reader = PdfReader("input.pdf") +writer = PdfWriter() + +page = reader.pages[0] +page.rotate(90) # Rotate 90 degrees clockwise +writer.add_page(page) + +with open("rotated.pdf", "wb") as output: + writer.write(output) +``` + +### pdfplumber — extraccao de texto e tabelas + +#### Extract Text with Layout +```python +import pdfplumber + +with pdfplumber.open("document.pdf") as pdf: + for page in pdf.pages: + text = page.extract_text() + print(text) +``` + +#### Extract Tables +```python +with pdfplumber.open("document.pdf") as pdf: + for i, page in enumerate(pdf.pages): + tables = page.extract_tables() + for j, table in enumerate(tables): + print(f"Table {j+1} on page {i+1}:") + for row in table: + print(row) +``` + +#### Advanced Table Extraction +```python +import pandas as pd + +with pdfplumber.open("document.pdf") as pdf: + all_tables = [] + for page in pdf.pages: + tables = page.extract_tables() + for table in tables: + if table: # Check if table is not empty + df = pd.DataFrame(table[1:], columns=table[0]) + all_tables.append(df) + +# Combine all tables +if all_tables: + combined_df = pd.concat(all_tables, ignore_index=True) + combined_df.to_excel("extracted_tables.xlsx", index=False) +``` + +### reportlab — criacao de PDFs + +#### Basic PDF Creation +```python +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas + +c = canvas.Canvas("hello.pdf", pagesize=letter) +width, height = letter + +# Add text +c.drawString(100, height - 100, "Hello World!") +c.drawString(100, height - 120, "This is a PDF created with reportlab") + +# Add a line +c.line(100, height - 140, 400, height - 140) + +# Save +c.save() +``` + +#### Create PDF with Multiple Pages +```python +from reportlab.lib.pagesizes import letter +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak +from reportlab.lib.styles import getSampleStyleSheet + +doc = SimpleDocTemplate("report.pdf", pagesize=letter) +styles = getSampleStyleSheet() +story = [] + +# Add content +title = Paragraph("Report Title", styles['Title']) +story.append(title) +story.append(Spacer(1, 12)) + +body = Paragraph("This is the body of the report. " * 20, styles['Normal']) +story.append(body) +story.append(PageBreak()) + +# Page 2 +story.append(Paragraph("Page 2", styles['Heading1'])) +story.append(Paragraph("Content for page 2", styles['Normal'])) + +# Build PDF +doc.build(story) +``` + +#### Subscripts and Superscripts + +**Importante**: nunca usar caracteres Unicode subscript/superscript (subscript: 0-9, superscript: 0-9) em PDFs ReportLab. As fontes built-in nao incluem estes glifos, resultando em caixas pretas. + +Usar as tags XML do ReportLab em objectos Paragraph: +```python +from reportlab.platypus import Paragraph +from reportlab.lib.styles import getSampleStyleSheet + +styles = getSampleStyleSheet() + +# Subscripts: use tag +chemical = Paragraph("H2O", styles['Normal']) + +# Superscripts: use tag +squared = Paragraph("x2 + y2", styles['Normal']) +``` + +Para texto desenhado com canvas (nao Paragraph), ajustar manualmente o tamanho da fonte e posicao. + +## Ferramentas de linha de comandos + +### pdftotext (poppler-utils) +```bash +# Extract text +pdftotext input.pdf output.txt + +# Extract text preserving layout +pdftotext -layout \ No newline at end of file diff --git a/dev-tools/skills/pdf/scripts/check_bounding_boxes.py b/dev-tools/skills/pdf/scripts/check_bounding_boxes.py new file mode 100644 index 0000000..2cc5e34 --- /dev/null +++ b/dev-tools/skills/pdf/scripts/check_bounding_boxes.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +import json +import sys + + + + +@dataclass +class RectAndField: + rect: list[float] + rect_type: str + field: dict + + +def get_bounding_box_messages(fields_json_stream) -> list[str]: + messages = [] + fields = json.load(fields_json_stream) + messages.append(f"Read {len(fields['form_fields'])} fields") + + def rects_intersect(r1, r2): + disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0] + disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1] + return not (disjoint_horizontal or disjoint_vertical) + + rects_and_fields = [] + for f in fields["form_fields"]: + rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f)) + rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f)) + + has_error = False + for i, ri in enumerate(rects_and_fields): + for j in range(i + 1, len(rects_and_fields)): + rj = rects_and_fields[j] + if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect): + has_error = True + if ri.field is rj.field: + messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})") + else: + messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + if ri.rect_type == "entry": + if "entry_text" in ri.field: + font_size = ri.field["entry_text"].get("font_size", 14) + entry_height = ri.rect[3] - ri.rect[1] + if entry_height < font_size: + has_error = True + messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + + if not has_error: + messages.append("SUCCESS: All bounding boxes are valid") + return messages + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: check_bounding_boxes.py [fields.json]") + sys.exit(1) + with open(sys.argv[1]) as f: + messages = get_bounding_box_messages(f) + for msg in messages: + print(msg) diff --git a/dev-tools/skills/pdf/scripts/check_fillable_fields.py b/dev-tools/skills/pdf/scripts/check_fillable_fields.py new file mode 100644 index 0000000..36dfb95 --- /dev/null +++ b/dev-tools/skills/pdf/scripts/check_fillable_fields.py @@ -0,0 +1,11 @@ +import sys +from pypdf import PdfReader + + + + +reader = PdfReader(sys.argv[1]) +if (reader.get_fields()): + print("This PDF has fillable form fields") +else: + print("This PDF does not have fillable form fields; you will need to visually determine where to enter data") diff --git a/dev-tools/skills/pdf/scripts/convert_pdf_to_images.py b/dev-tools/skills/pdf/scripts/convert_pdf_to_images.py new file mode 100644 index 0000000..7939cef --- /dev/null +++ b/dev-tools/skills/pdf/scripts/convert_pdf_to_images.py @@ -0,0 +1,33 @@ +import os +import sys + +from pdf2image import convert_from_path + + + + +def convert(pdf_path, output_dir, max_dim=1000): + images = convert_from_path(pdf_path, dpi=200) + + for i, image in enumerate(images): + width, height = image.size + if width > max_dim or height > max_dim: + scale_factor = min(max_dim / width, max_dim / height) + new_width = int(width * scale_factor) + new_height = int(height * scale_factor) + image = image.resize((new_width, new_height)) + + image_path = os.path.join(output_dir, f"page_{i+1}.png") + image.save(image_path) + print(f"Saved page {i+1} as {image_path} (size: {image.size})") + + print(f"Converted {len(images)} pages to PNG images") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: convert_pdf_to_images.py [input pdf] [output directory]") + sys.exit(1) + pdf_path = sys.argv[1] + output_directory = sys.argv[2] + convert(pdf_path, output_directory) diff --git a/dev-tools/skills/pdf/scripts/create_validation_image.py b/dev-tools/skills/pdf/scripts/create_validation_image.py new file mode 100644 index 0000000..10eadd8 --- /dev/null +++ b/dev-tools/skills/pdf/scripts/create_validation_image.py @@ -0,0 +1,37 @@ +import json +import sys + +from PIL import Image, ImageDraw + + + + +def create_validation_image(page_number, fields_json_path, input_path, output_path): + with open(fields_json_path, 'r') as f: + data = json.load(f) + + img = Image.open(input_path) + draw = ImageDraw.Draw(img) + num_boxes = 0 + + for field in data["form_fields"]: + if field["page_number"] == page_number: + entry_box = field['entry_bounding_box'] + label_box = field['label_bounding_box'] + draw.rectangle(entry_box, outline='red', width=2) + draw.rectangle(label_box, outline='blue', width=2) + num_boxes += 2 + + img.save(output_path) + print(f"Created validation image at {output_path} with {num_boxes} bounding boxes") + + +if __name__ == "__main__": + if len(sys.argv) != 5: + print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") + sys.exit(1) + page_number = int(sys.argv[1]) + fields_json_path = sys.argv[2] + input_image_path = sys.argv[3] + output_image_path = sys.argv[4] + create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/dev-tools/skills/pdf/scripts/extract_form_field_info.py b/dev-tools/skills/pdf/scripts/extract_form_field_info.py new file mode 100644 index 0000000..64cd470 --- /dev/null +++ b/dev-tools/skills/pdf/scripts/extract_form_field_info.py @@ -0,0 +1,122 @@ +import json +import sys + +from pypdf import PdfReader + + + + +def get_full_annotation_field_id(annotation): + components = [] + while annotation: + field_name = annotation.get('/T') + if field_name: + components.append(field_name) + annotation = annotation.get('/Parent') + return ".".join(reversed(components)) if components else None + + +def make_field_dict(field, field_id): + field_dict = {"field_id": field_id} + ft = field.get('/FT') + if ft == "/Tx": + field_dict["type"] = "text" + elif ft == "/Btn": + field_dict["type"] = "checkbox" + states = field.get("/_States_", []) + if len(states) == 2: + if "/Off" in states: + field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1] + field_dict["unchecked_value"] = "/Off" + else: + print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.") + field_dict["checked_value"] = states[0] + field_dict["unchecked_value"] = states[1] + elif ft == "/Ch": + field_dict["type"] = "choice" + states = field.get("/_States_", []) + field_dict["choice_options"] = [{ + "value": state[0], + "text": state[1], + } for state in states] + else: + field_dict["type"] = f"unknown ({ft})" + return field_dict + + +def get_field_info(reader: PdfReader): + fields = reader.get_fields() + + field_info_by_id = {} + possible_radio_names = set() + + for field_id, field in fields.items(): + if field.get("/Kids"): + if field.get("/FT") == "/Btn": + possible_radio_names.add(field_id) + continue + field_info_by_id[field_id] = make_field_dict(field, field_id) + + + radio_fields_by_id = {} + + for page_index, page in enumerate(reader.pages): + annotations = page.get('/Annots', []) + for ann in annotations: + field_id = get_full_annotation_field_id(ann) + if field_id in field_info_by_id: + field_info_by_id[field_id]["page"] = page_index + 1 + field_info_by_id[field_id]["rect"] = ann.get('/Rect') + elif field_id in possible_radio_names: + try: + on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"] + except KeyError: + continue + if len(on_values) == 1: + rect = ann.get("/Rect") + if field_id not in radio_fields_by_id: + radio_fields_by_id[field_id] = { + "field_id": field_id, + "type": "radio_group", + "page": page_index + 1, + "radio_options": [], + } + radio_fields_by_id[field_id]["radio_options"].append({ + "value": on_values[0], + "rect": rect, + }) + + fields_with_location = [] + for field_info in field_info_by_id.values(): + if "page" in field_info: + fields_with_location.append(field_info) + else: + print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring") + + def sort_key(f): + if "radio_options" in f: + rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0] + else: + rect = f.get("rect") or [0, 0, 0, 0] + adjusted_position = [-rect[1], rect[0]] + return [f.get("page"), adjusted_position] + + sorted_fields = fields_with_location + list(radio_fields_by_id.values()) + sorted_fields.sort(key=sort_key) + + return sorted_fields + + +def write_field_info(pdf_path: str, json_output_path: str): + reader = PdfReader(pdf_path) + field_info = get_field_info(reader) + with open(json_output_path, "w") as f: + json.dump(field_info, f, indent=2) + print(f"Wrote {len(field_info)} fields to {json_output_path}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: extract_form_field_info.py [input pdf] [output json]") + sys.exit(1) + write_field_info(sys.argv[1], sys.argv[2]) diff --git a/dev-tools/skills/pdf/scripts/extract_form_structure.py b/dev-tools/skills/pdf/scripts/extract_form_structure.py new file mode 100644 index 0000000..f219e7d --- /dev/null +++ b/dev-tools/skills/pdf/scripts/extract_form_structure.py @@ -0,0 +1,115 @@ +""" +Extract form structure from a non-fillable PDF. + +This script analyzes the PDF to find: +- Text labels with their exact coordinates +- Horizontal lines (row boundaries) +- Checkboxes (small rectangles) + +Output: A JSON file with the form structure that can be used to generate +accurate field coordinates for filling. + +Usage: python extract_form_structure.py +""" + +import json +import sys +import pdfplumber + + +def extract_form_structure(pdf_path): + structure = { + "pages": [], + "labels": [], + "lines": [], + "checkboxes": [], + "row_boundaries": [] + } + + with pdfplumber.open(pdf_path) as pdf: + for page_num, page in enumerate(pdf.pages, 1): + structure["pages"].append({ + "page_number": page_num, + "width": float(page.width), + "height": float(page.height) + }) + + words = page.extract_words() + for word in words: + structure["labels"].append({ + "page": page_num, + "text": word["text"], + "x0": round(float(word["x0"]), 1), + "top": round(float(word["top"]), 1), + "x1": round(float(word["x1"]), 1), + "bottom": round(float(word["bottom"]), 1) + }) + + for line in page.lines: + if abs(float(line["x1"]) - float(line["x0"])) > page.width * 0.5: + structure["lines"].append({ + "page": page_num, + "y": round(float(line["top"]), 1), + "x0": round(float(line["x0"]), 1), + "x1": round(float(line["x1"]), 1) + }) + + for rect in page.rects: + width = float(rect["x1"]) - float(rect["x0"]) + height = float(rect["bottom"]) - float(rect["top"]) + if 5 <= width <= 15 and 5 <= height <= 15 and abs(width - height) < 2: + structure["checkboxes"].append({ + "page": page_num, + "x0": round(float(rect["x0"]), 1), + "top": round(float(rect["top"]), 1), + "x1": round(float(rect["x1"]), 1), + "bottom": round(float(rect["bottom"]), 1), + "center_x": round((float(rect["x0"]) + float(rect["x1"])) / 2, 1), + "center_y": round((float(rect["top"]) + float(rect["bottom"])) / 2, 1) + }) + + lines_by_page = {} + for line in structure["lines"]: + page = line["page"] + if page not in lines_by_page: + lines_by_page[page] = [] + lines_by_page[page].append(line["y"]) + + for page, y_coords in lines_by_page.items(): + y_coords = sorted(set(y_coords)) + for i in range(len(y_coords) - 1): + structure["row_boundaries"].append({ + "page": page, + "row_top": y_coords[i], + "row_bottom": y_coords[i + 1], + "row_height": round(y_coords[i + 1] - y_coords[i], 1) + }) + + return structure + + +def main(): + if len(sys.argv) != 3: + print("Usage: extract_form_structure.py ") + sys.exit(1) + + pdf_path = sys.argv[1] + output_path = sys.argv[2] + + print(f"Extracting structure from {pdf_path}...") + structure = extract_form_structure(pdf_path) + + with open(output_path, "w") as f: + json.dump(structure, f, indent=2) + + print(f"Found:") + print(f" - {len(structure['pages'])} pages") + print(f" - {len(structure['labels'])} text labels") + print(f" - {len(structure['lines'])} horizontal lines") + print(f" - {len(structure['checkboxes'])} checkboxes") + print(f" - {len(structure['row_boundaries'])} row boundaries") + print(f"Saved to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/dev-tools/skills/pdf/scripts/fill_fillable_fields.py b/dev-tools/skills/pdf/scripts/fill_fillable_fields.py new file mode 100644 index 0000000..51c2600 --- /dev/null +++ b/dev-tools/skills/pdf/scripts/fill_fillable_fields.py @@ -0,0 +1,98 @@ +import json +import sys + +from pypdf import PdfReader, PdfWriter + +from extract_form_field_info import get_field_info + + + + +def fill_pdf_fields(input_pdf_path: str, fields_json_path: str, output_pdf_path: str): + with open(fields_json_path) as f: + fields = json.load(f) + fields_by_page = {} + for field in fields: + if "value" in field: + field_id = field["field_id"] + page = field["page"] + if page not in fields_by_page: + fields_by_page[page] = {} + fields_by_page[page][field_id] = field["value"] + + reader = PdfReader(input_pdf_path) + + has_error = False + field_info = get_field_info(reader) + fields_by_ids = {f["field_id"]: f for f in field_info} + for field in fields: + existing_field = fields_by_ids.get(field["field_id"]) + if not existing_field: + has_error = True + print(f"ERROR: `{field['field_id']}` is not a valid field ID") + elif field["page"] != existing_field["page"]: + has_error = True + print(f"ERROR: Incorrect page number for `{field['field_id']}` (got {field['page']}, expected {existing_field['page']})") + else: + if "value" in field: + err = validation_error_for_field_value(existing_field, field["value"]) + if err: + print(err) + has_error = True + if has_error: + sys.exit(1) + + writer = PdfWriter(clone_from=reader) + for page, field_values in fields_by_page.items(): + writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerate=False) + + writer.set_need_appearances_writer(True) + + with open(output_pdf_path, "wb") as f: + writer.write(f) + + +def validation_error_for_field_value(field_info, field_value): + field_type = field_info["type"] + field_id = field_info["field_id"] + if field_type == "checkbox": + checked_val = field_info["checked_value"] + unchecked_val = field_info["unchecked_value"] + if field_value != checked_val and field_value != unchecked_val: + return f'ERROR: Invalid value "{field_value}" for checkbox field "{field_id}". The checked value is "{checked_val}" and the unchecked value is "{unchecked_val}"' + elif field_type == "radio_group": + option_values = [opt["value"] for opt in field_info["radio_options"]] + if field_value not in option_values: + return f'ERROR: Invalid value "{field_value}" for radio group field "{field_id}". Valid values are: {option_values}' + elif field_type == "choice": + choice_values = [opt["value"] for opt in field_info["choice_options"]] + if field_value not in choice_values: + return f'ERROR: Invalid value "{field_value}" for choice field "{field_id}". Valid values are: {choice_values}' + return None + + +def monkeypatch_pydpf_method(): + from pypdf.generic import DictionaryObject + from pypdf.constants import FieldDictionaryAttributes + + original_get_inherited = DictionaryObject.get_inherited + + def patched_get_inherited(self, key: str, default = None): + result = original_get_inherited(self, key, default) + if key == FieldDictionaryAttributes.Opt: + if isinstance(result, list) and all(isinstance(v, list) and len(v) == 2 for v in result): + result = [r[0] for r in result] + return result + + DictionaryObject.get_inherited = patched_get_inherited + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: fill_fillable_fields.py [input pdf] [field_values.json] [output pdf]") + sys.exit(1) + monkeypatch_pydpf_method() + input_pdf = sys.argv[1] + fields_json = sys.argv[2] + output_pdf = sys.argv[3] + fill_pdf_fields(input_pdf, fields_json, output_pdf) diff --git a/dev-tools/skills/pdf/scripts/fill_pdf_form_with_annotations.py b/dev-tools/skills/pdf/scripts/fill_pdf_form_with_annotations.py new file mode 100644 index 0000000..b430069 --- /dev/null +++ b/dev-tools/skills/pdf/scripts/fill_pdf_form_with_annotations.py @@ -0,0 +1,107 @@ +import json +import sys + +from pypdf import PdfReader, PdfWriter +from pypdf.annotations import FreeText + + + + +def transform_from_image_coords(bbox, image_width, image_height, pdf_width, pdf_height): + x_scale = pdf_width / image_width + y_scale = pdf_height / image_height + + left = bbox[0] * x_scale + right = bbox[2] * x_scale + + top = pdf_height - (bbox[1] * y_scale) + bottom = pdf_height - (bbox[3] * y_scale) + + return left, bottom, right, top + + +def transform_from_pdf_coords(bbox, pdf_height): + left = bbox[0] + right = bbox[2] + + pypdf_top = pdf_height - bbox[1] + pypdf_bottom = pdf_height - bbox[3] + + return left, pypdf_bottom, right, pypdf_top + + +def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path): + + with open(fields_json_path, "r") as f: + fields_data = json.load(f) + + reader = PdfReader(input_pdf_path) + writer = PdfWriter() + + writer.append(reader) + + pdf_dimensions = {} + for i, page in enumerate(reader.pages): + mediabox = page.mediabox + pdf_dimensions[i + 1] = [mediabox.width, mediabox.height] + + annotations = [] + for field in fields_data["form_fields"]: + page_num = field["page_number"] + + page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num) + pdf_width, pdf_height = pdf_dimensions[page_num] + + if "pdf_width" in page_info: + transformed_entry_box = transform_from_pdf_coords( + field["entry_bounding_box"], + float(pdf_height) + ) + else: + image_width = page_info["image_width"] + image_height = page_info["image_height"] + transformed_entry_box = transform_from_image_coords( + field["entry_bounding_box"], + image_width, image_height, + float(pdf_width), float(pdf_height) + ) + + if "entry_text" not in field or "text" not in field["entry_text"]: + continue + entry_text = field["entry_text"] + text = entry_text["text"] + if not text: + continue + + font_name = entry_text.get("font", "Arial") + font_size = str(entry_text.get("font_size", 14)) + "pt" + font_color = entry_text.get("font_color", "000000") + + annotation = FreeText( + text=text, + rect=transformed_entry_box, + font=font_name, + font_size=font_size, + font_color=font_color, + border_color=None, + background_color=None, + ) + annotations.append(annotation) + writer.add_annotation(page_number=page_num - 1, annotation=annotation) + + with open(output_pdf_path, "wb") as output: + writer.write(output) + + print(f"Successfully filled PDF form and saved to {output_pdf_path}") + print(f"Added {len(annotations)} text annotations") + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: fill_pdf_form_with_annotations.py [input pdf] [fields.json] [output pdf]") + sys.exit(1) + input_pdf = sys.argv[1] + fields_json = sys.argv[2] + output_pdf = sys.argv[3] + + fill_pdf_form(input_pdf, fields_json, output_pdf) diff --git a/dev-tools/skills/php-dev/SKILL.md b/dev-tools/skills/php-dev/SKILL.md new file mode 100644 index 0000000..9ec469d --- /dev/null +++ b/dev-tools/skills/php-dev/SKILL.md @@ -0,0 +1,204 @@ +--- +name: php-dev +description: Desenvolvimento PHP moderno com Laravel, Symfony e APIs RESTful — autenticação (JWT, OAuth, Sanctum), service classes e refactorização de código legacy seguindo padrões PSR. +allowed-tools: Read, Write, Edit, Bash, mcp__mem_search_memories, mcp__context7__get-library-docs, mcp__notebooklm__notebook_query, mcp__dify-kb__dify_kb_retrieve_segments +layer: wiki +--- + +# PHP Fullstack Engineer + +Skill para desenvolvimento PHP moderno seguindo padrões Descomplicar®. + +## Contexto NotebookLM + +ANTES de executar, consultar notebooks para contexto especializado: + +| Notebook | ID | Consultar quando | +|----------|-----|-----------------| +| Programação | 24947ffa-0019-448a-a340-2f4a275d2eb1 | Para padrões gerais | +| Dev PerfexCRM | 80606de8-2783-4d36-b08d-5825e6f9a8da | Para contexto Perfex/CodeIgniter | + +``` +mcp__notebooklm__notebook_query({ + notebook_id: "24947ffa-0019-448a-a340-2f4a275d2eb1", + query: "" +}) +``` + +``` +mcp__notebooklm__notebook_query({ + notebook_id: "80606de8-2783-4d36-b08d-5825e6f9a8da", + query: "" +}) +``` + +Integrar insights do NotebookLM nas recomendações e decisões. + +> **Regra #48:** Projectos PHP/Laravel/Symfony devem ser desenvolvidos no **container dev** (`server:"dev"`, path `/root/Dev/`). Sincroniza automaticamente com o PC local via Syncthing. + +## Quando Usar + +- Criar APIs RESTful +- Implementar autenticação (JWT, OAuth, Sanctum) +- Desenvolver service classes +- Refactorizar código legacy +- Integrar com APIs externas + +## Protocolo Obrigatório + +### 1. Pesquisa Inicial +``` +mcp__mem_search_memories "[keywords php/laravel]" +mcp__wikijs__search_pages "[framework] best practices" +mcp__context7__get-library-docs para documentação actualizada +``` + +### 2. Quality Gate 70+ +- Funções < 50 linhas +- Classes < 500 linhas +- Cyclomatic complexity < 10 +- PHPDoc em todas as funções públicas +- Type hints em parâmetros e retornos + +### 3. Assinatura Obrigatória +```php + 'Activo', + Status::Inactive => 'Inactivo', +}; + +// Named arguments +$this->service->create( + name: $name, + price: $price +); +``` + +### PSR Standards +- **PSR-1**: Basic Coding Standard +- **PSR-4**: Autoloading +- **PSR-12**: Extended Coding Style +- **PSR-7**: HTTP Message Interface +- **PSR-15**: HTTP Handlers + +## Segurança (OBRIGATÓRIO) + +```php +// SQL - SEMPRE prepared statements +$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); +$stmt->execute([$id]); + +// Input validation +$email = filter_var($input, FILTER_VALIDATE_EMAIL); + +// Output escaping +echo htmlspecialchars($output, ENT_QUOTES, 'UTF-8'); + +// Password hashing +$hash = password_hash($password, PASSWORD_ARGON2ID); + +// CSRF em forms + +``` + +## Padrão Laravel + +### Controller +```php +productService->paginate(); + return ProductResource::collection($products)->response(); + } + + public function store(StoreProductRequest $request): JsonResponse + { + $product = $this->productService->create($request->validated()); + return (new ProductResource($product)) + ->response() + ->setStatusCode(201); + } +} +``` + +### Service +```php +repository->paginate($filters); + } + + public function create(array $data): Product diff --git a/dev-tools/skills/prompt-refine/SKILL.md b/dev-tools/skills/prompt-refine/SKILL.md new file mode 100644 index 0000000..591d4eb --- /dev/null +++ b/dev-tools/skills/prompt-refine/SKILL.md @@ -0,0 +1,145 @@ +--- +name: prompt-refine +description: Transforma um pedido vago em prompt de excelência — especificação detalhada, inequívoca e estruturada para agentes de IA. Usar quando "refinar prompt", "melhorar prompt", "engenharia de prompts", "prompt para agente", "estruturar instrução IA", "prompt engineering". +allowed-tools: Read, Write, mcp__mem_search_memories +layer: wiki +--- + +# /prompt-refine — Engenharia de Prompts + +Transforma um pedido em linguagem natural num **prompt de excelência** pronto para ser executado por outro agente de IA. + +## Quando Usar + +- Pedido do utilizador é vago ou incompleto +- Preparar instrução para um agente especializado +- Criar template reutilizável para uma tarefa recorrente +- Garantir qualidade máxima de input antes de executar + +## Protocolo + +### 1. Pesquisar contexto relevante + +``` +mcp__mem_search_memories "" +``` + +Verificar se há padrões ou decisões anteriores relacionadas. + +### 2. Analisar o pedido original + +Identificar: +- **O quê** — resultado final esperado +- **Para quê** — objectivo ou problema a resolver +- **Quem** — agente ou utilizador que vai executar +- **Restrições** — limitações técnicas, de formato ou de negócio +- **Lacunas** — o que está implícito mas não dito + +Se faltar informação crítica → perguntar antes de estruturar. + +### 3. Estruturar o prompt de excelência + +Usar este template como base: + +```markdown +## Função +[Quem é / que papel assume o agente executante] + +## Objectivo +[O que deve ser produzido — resultado concreto e mensurável] + +## Contexto +[Informação de fundo necessária para compreender o pedido] + +## Requisitos +- [Requisito 1 — obrigatório] +- [Requisito 2 — obrigatório] +- [Restrição ou critério de qualidade] + +## Input +[Descrição do que o agente recebe como entrada] + +## Output esperado +[Formato, estrutura e conteúdo exacto do resultado] + +## Exemplos +### Exemplo positivo (fazer) +[Exemplo concreto do resultado desejado] + +### Exemplo negativo (não fazer) +[Exemplo do que deve ser evitado] + +## Critérios de sucesso +- [ ] Critério 1 +- [ ] Critério 2 +``` + +### 4. Rever antes de entregar + +Verificar checklist: +- [ ] Sem ambiguidades — cada instrução tem um único significado possível +- [ ] Completo — nenhuma informação crítica em falta +- [ ] Accionável — o agente pode executar sem fazer suposições +- [ ] Testável — é possível verificar se o output é correcto +- [ ] Conciso — sem redundâncias ou instruções contraditórias + +### 5. Entregar com explicação + +Apresentar: +1. **Prompt refinado** (em bloco de código copiável) +2. **Decisões tomadas** — o que foi inferido vs. o que estava explícito +3. **Questões em aberto** — se houver ambiguidades que o utilizador deve resolver + +## Exemplos de Transformação + +### Input vago +> "Cria um agente que analisa clientes" + +### Output refinado +```markdown +## Função +Analista de CRM especializado em classificação de clientes + +## Objectivo +Gerar um relatório de segmentação de clientes com base em histórico de compras +e interacções, identificando os segmentos de alto, médio e baixo valor. + +## Contexto +Base de dados Desk CRM com clientes activos nos últimos 12 meses. +Acesso via mcp__desk-crm-v3__get_customers e mcp__desk-crm-v3__get_payments. + +## Requisitos +- Segmentar em 3 níveis: Alto Valor (>5000EUR/ano), Médio (1000-5000EUR), Baixo (<1000EUR) +- Incluir número de projectos activos por cliente +- Formato: tabela Markdown ordenada por valor decrescente + +## Input +Lista de IDs de clientes ou "todos" para analisar base completa + +## Output esperado +Tabela com: ID, Nome, Segmento, Volume Anual (EUR), Projectos Activos, Última Interacção +``` + +## Padrões de Qualidade + +| Característica | Mau prompt | Bom prompt | +|---|---|---| +| Especificidade | "Analisa dados" | "Calcula receita mensal por cliente em EUR para Q1 2026" | +| Formato output | "Devolve resultados" | "Tabela Markdown com colunas: ID, Nome, Valor, Delta%" | +| Critério sucesso | "Deve ser bom" | "Score de satisfação >85% em teste com 3 utilizadores reais" | +| Contexto | Nenhum | "Sistema usa Desk CRM v3, moeda EUR, fuso Europe/Lisbon" | + +--- +**Versão**: 1.0.0 | **Autor**: Descomplicar® | **Plugin**: dev-tools + +--- + +## Healing Log + +Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar. + +```jsonl +{"date":"","issue":"","fix":"","source":"user|auto"} +``` + +*Adicionar nova linha após cada erro corrigido.* diff --git a/dev-tools/skills/react-patterns/SKILL.md b/dev-tools/skills/react-patterns/SKILL.md new file mode 100644 index 0000000..845d10b --- /dev/null +++ b/dev-tools/skills/react-patterns/SKILL.md @@ -0,0 +1,181 @@ +--- +name: react-patterns +description: Padroes modernos React (18+/19) -- hooks, context, composicao, memoizacao, lazy loading e Server Components. +layer: wiki +--- + +# React Patterns + +Skill para desenvolvimento React seguindo padroes modernos (React 18+/19). + +## Contexto NotebookLM + +ANTES de executar, consultar notebooks para contexto especializado: + +| Notebook | ID | Consultar quando | +|----------|-----|-----------------| +| Programacao | 24947ffa-0019-448a-a340-2f4a275d2eb1 | Sempre | + +``` +mcp__notebooklm__notebook_query({ + notebook_id: "24947ffa-0019-448a-a340-2f4a275d2eb1", + query: "" +}) +``` + +> **Regra #48:** Projectos React devem ser desenvolvidos no **container dev** (`server:"dev"`, path `/root/Dev/`). Sincroniza via Syncthing. + +## Quando Usar + +- Desenvolver componentes React +- Implementar state management +- Optimizar performance (memoization, lazy loading) +- Migrar de class components para hooks +- Usar Server Components (Next.js App Router) + +## Hooks Fundamentais + +```jsx +// useState - estado simples +const [count, setCount] = useState(0) +const [user, setUser] = useState(null) + +// useEffect - side effects +useEffect(() => { + fetchData() + return () => cleanup() +}, [dependency]) + +// useCallback - memoizar funcoes +const handleClick = useCallback(() => { doSomething(id) }, [id]) + +// useMemo - memoizar valores computados +const expensiveValue = useMemo(() => computeExpensive(data), [data]) + +// useRef - referencias mutaveis +const inputRef = useRef(null) +``` + +## Component Patterns + +```jsx +// SIM: Functional components +function UserCard({ user, onSelect }: Props) { + return
onSelect(user.id)}>{user.name}
+} + +// SIM: Composition over props drilling +function Layout({ children }) { + return
{children}
+} + +// NAO: Class components (legacy) +``` + +## State Management + +```jsx +// Local state: useState (estado de um componente) + +// Shared state: Context + useReducer +const AppContext = createContext(null) +function AppProvider({ children }) { + const [state, dispatch] = useReducer(reducer, initialState) + return ( + + {children} + + ) +} + +// Server state: React Query / SWR +const { data, isLoading } = useQuery({ + queryKey: ['users'], + queryFn: fetchUsers, +}) +``` + +**NAO:** Redux para tudo. Estado global para estado local. + +## Server Components (Next.js 13+) + +```jsx +// Server Component (default) - async, data fetching +async function UserList() { + const users = await db.users.findMany() + return
    {users.map(u =>
  • {u.name}
  • )}
+} + +// Client Component - interactividade +'use client' +function Counter() { + const [count, setCount] = useState(0) + return +} + +// Padrao: Server parent + Client children +// page.tsx (Server) -> InteractiveSection.tsx ('use client') +``` + +## Performance + +```jsx +// Lazy loading componentes +const HeavyComponent = lazy(() => import('./HeavyComponent')) +}> + + + +// React.memo para componentes puros +const MemoizedCard = memo(function Card({ data }) { + return
{data.title}
+}) + +// Virtualizacao para listas grandes +import { useVirtualizer } from '@tanstack/react-virtual' +``` + +## Composition Patterns (Vercel Engineering) + +Padroes de composicao para componentes React escalaveis. Ver `references/composition-patterns.md` para codigo detalhado. + +**Regras principais:** + +| Regra | Prioridade | Resumo | +|-------|-----------|--------| +| Evitar Boolean Props | CRITICAL | Variantes explicitas em vez de booleans | +| Compound Components | HIGH | Context partilhado, composicao explicita | +| State em Providers | HIGH | Boundary do Provider e o que importa | +| Interface Generica | HIGH | state/actions/meta para dependency injection | +| Children > Render Props | MEDIUM | Composicao natural com children | +| React 19 APIs | MEDIUM | `use()` e ref como prop | + +## Anti-Patterns + +| Anti-Pattern | Problema | Solucao | +|--------------|----------|---------| +| useEffect para tudo | Complexidade | Server Components / React Query | +| Props drilling | Manutencao | Context ou Composition | +| Inline functions em JSX | Re-renders | useCallback | +| State em URL | Perda estado | URL state com hooks | +| Class components | Legacy | Functional + Hooks | +| Index como key | Bugs listas | ID unico | + +## Checklist Componente + +- [ ] Functional component (nao class) +- [ ] TypeScript types/interfaces +- [ ] Props destructuring com defaults +- [ ] Hooks no topo (nao condicionais) +- [ ] Keys unicos em listas +- [ ] Error boundaries para erros +- [ ] Loading states considerados + +## Estrutura Ficheiros + +``` +components/ + ui/ # Componentes base (Button, Input, Card) + features/ # Componentes feature-specific + layouts/ # Layout components + providers/ # Context \ No newline at end of file diff --git a/dev-tools/skills/react-patterns/references/composition-patterns.md b/dev-tools/skills/react-patterns/references/composition-patterns.md new file mode 100644 index 0000000..860da1c --- /dev/null +++ b/dev-tools/skills/react-patterns/references/composition-patterns.md @@ -0,0 +1,183 @@ +# React - Composition Patterns (Vercel Engineering) + +Padroes de composicao para componentes React escalaveis. Fonte: Vercel composition-patterns. + +## Regra #1: Evitar Boolean Props (CRITICAL) + +```tsx +// NAO: Booleans duplicam estados possiveis + + +// SIM: Variantes explicitas + + + +``` + +Cada boolean duplica os estados possiveis. 5 booleans = 32 combinacoes, maioria impossivel. + +--- + +## Regra #2: Compound Components com Context (HIGH) + +```tsx +// Compound Components: context partilhado, composicao explicita +const ComposerContext = createContext(null) + +function ComposerFrame({ children }: { children: React.ReactNode }) { + return
{children}
+} + +function ComposerInput() { + const { state, actions: { update }, meta } = use(ComposerContext) + return ( + update(s => ({ ...s, input: text }))} + /> + ) +} + +// Exportar como compound +const Composer = { + Frame: ComposerFrame, + Input: ComposerInput, + Submit: ComposerSubmit, + Footer: ComposerFooter, +} + +// Uso: composicao clara + + + + + + +``` + +--- + +## Regra #3: State em Providers (HIGH) + +```tsx +// NAO: Estado preso dentro do componente +function ForwardComposer() { + const [state, setState] = useState(initialState) + return ... +} + +// SIM: State lifted para Provider +function ForwardProvider({ children }: { children: React.ReactNode }) { + const [state, setState] = useState(initialState) + const submit = useForwardMessage() + const inputRef = useRef(null) + return ( + + {children} + + ) +} + +// Qualquer componente dentro do Provider acede ao state +function ForwardDialog() { + return ( + + + + + + + + ) +} + +function ForwardButton() { + const { actions } = use(ComposerContext) + return +} +``` + +**Principio chave:** O boundary do Provider e o que importa, nao o nesting visual. + +--- + +## Regra #4: Interface Generica para Dependency Injection (HIGH) + +```tsx +// Definir interface generica: state + actions + meta +interface ComposerContextValue { + state: { input: string; attachments: Attachment[]; isSubmitting: boolean } + actions: { update: (fn: (s: State) => State) => void; submit: () => void } + meta: { inputRef: React.RefObject } +} + +// Multiplos providers implementam a MESMA interface +function ChannelProvider({ channelId, children }) { + const { state, update, submit } = useGlobalChannel(channelId) + return {children} +} + +function ForwardProvider({ children }) { + const [state, setState] = useState(initialState) + return {children} +} + +// O mesmo Composer.Input funciona com AMBOS os providers +``` + +**Principio:** UI = pecas reutilizaveis. State = injectado pelo provider. Trocar provider, manter UI. + +--- + +## Regra #5: Children > Render Props (MEDIUM) + +```tsx +// NAO: Render props (inflexivel) +
} + renderFooter={() => <>} +/> + +// SIM: Children (composicao natural) + +
+ + + + + + + +// Excepcao: render props quando parent passa dados + } /> +``` + +--- + +## Regra #6: React 19 APIs (MEDIUM) + +```tsx +// React 19: ref como prop normal (sem forwardRef) +function Input({ ref, ...props }: Props & { ref?: React.Ref }) { + return +} + +// React 19: use() em vez de useContext() +const value = use(MyContext) // pode ser chamado condicionalmente +``` + +--- + +## Checklist Composicao + +- [ ] Zero boolean props para variantes (usar componentes explicitos) +- [ ] Compound components com context partilhado +- [ ] State em Provider (nao dentro do componente UI) +- [ ] Interface generica (state/actions/meta) +- [ ] Children para composicao, render props so para data passthrough +- [ ] React 19: `use()` e ref como prop diff --git a/dev-tools/skills/react-patterns/references/custom-hooks.md b/dev-tools/skills/react-patterns/references/custom-hooks.md new file mode 100644 index 0000000..e4c6924 --- /dev/null +++ b/dev-tools/skills/react-patterns/references/custom-hooks.md @@ -0,0 +1,66 @@ +# React - Custom Hooks Uteis + +## useDebounce + +```tsx +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +} + +// Uso +const searchTerm = useDebounce(input, 500); +``` + +--- + +## useLocalStorage + +```tsx +function useLocalStorage(key: string, initialValue: T) { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch { + return initialValue; + } + }); + + const setValue = (value: T | ((val: T) => T)) => { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + }; + + return [storedValue, setValue] as const; +} +``` + +--- + +## useFetch + +```tsx +function useFetch(url: string) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then(setData) + .catch(setError) + .finally(() => setLoading(false)); + }, [url]); + + return { data, loading, error }; +} +``` diff --git a/dev-tools/skills/skill-creator/SKILL.md b/dev-tools/skills/skill-creator/SKILL.md new file mode 100644 index 0000000..dfc2dda --- /dev/null +++ b/dev-tools/skills/skill-creator/SKILL.md @@ -0,0 +1,140 @@ +--- +name: skill-creator +description: Criar, melhorar e empacotar skills para o sistema de plugins Descomplicar. Usar quando o utilizador pede para criar uma nova skill, actualizar uma skill existente, ou quando é necessário estruturar conhecimento especializado em formato skill. Inclui padrões Anthropic oficiais (progressive disclosure, graus de liberdade) adaptados ao ecossistema Descomplicar com PT-PT estrito. +layer: wiki +--- + +# Skill Creator + +Guia para criar skills eficazes no ecossistema Descomplicar, seguindo padrões Anthropic com adaptações Descomplicar. + +## O Que é uma Skill + +Skills são pacotes modulares e auto-suficientes que estendem as capacidades do agente com conhecimento especializado, workflows e integrações de ferramentas. Funcionam como "guias de integração" para domínios específicos — transformam um agente geral num especialista equipado com conhecimento processual. + +## Processo de Criação (6 Passos) + +**Visão geral:** +1. Compreender o contexto com exemplos concretos +2. Planear recursos reutilizáveis (scripts, references, assets) +3. Inicializar a estrutura de directórios +4. Implementar conteúdo e escrever SKILL.md +5. Validar com checklist de qualidade +6. Iterar com base em uso real + +--- + +### Passo 1: Compreender com Exemplos Concretos + +Antes de escrever qualquer conteúdo, clarificar: +- Que tarefas concretas a skill vai executar? +- Que diria o utilizador para activar esta skill? +- Que informação o agente não tem por defeito? + +Exemplo de perguntas úteis: "Podes dar exemplos de como usarias esta skill?" ou "O que é que o agente deve fazer que hoje não faz?" + +Concluir este passo quando os padrões de uso estiverem claros. + +--- + +### Passo 2: Planear Recursos Reutilizáveis + +Para cada exemplo concreto, identificar: +- Código que seria reescrito repetidamente -> `scripts/` +- Documentação de referência, schemas, APIs -> `references/` +- Templates, assets, ficheiros de output -> `assets/` + +--- + +### Passo 3: Inicializar Estrutura + +Estrutura de directório padrão Descomplicar: + +``` +nome-da-skill/ +├── SKILL.md (obrigatório) +├── references/ (quando há docs detalhadas) +│ ├── anthropic-patterns.md +│ ├── descomplicar-standards.md +│ ├── output-patterns.md +│ └── workflows.md +├── scripts/ (quando há código reutilizável) +│ └── script.py +└── assets/ (quando há templates/ficheiros de output) + └── template.ext +``` + +**Script de inicialização Anthropic** (quando disponível): +```bash +python scripts/init_skill.py --path +``` + +O script gera SKILL.md com frontmatter e placeholders, mais directórios de exemplo. + +--- + +### Passo 4: Implementar Conteúdo + +#### Frontmatter (obrigatório, mínimo) + +```yaml +--- +name: nome-da-skill +description: > + Descrição clara do que a skill faz e quando usar. + Incluir triggers explícitos: "Usar quando [contexto específico]". + Listar casos de uso principais: (1) caso A, (2) caso B, (3) caso C. +--- +``` + +**Regras do frontmatter:** +- Apenas `name` e `description` (padrão Anthropic) +- A `description` é o mecanismo de activação — deve ser completa e incluir triggers explícitos +- Nunca incluir "Quando Usar" no corpo — só é lido após activação + +#### Corpo do SKILL.md + +- Manter abaixo de 500 linhas +- Usar forma imperativa/infinitivo nas instruções +- Conteúdo detalhado vai para `references/` +- Referenciar ficheiros de references com contexto sobre quando ler + +**Para conteúdo detalhado, ver:** +- `references/anthropic-patterns.md` — padrões Anthropic (graus de liberdade, progressive disclosure) +- `references/descomplicar-standards.md` — standards Descomplicar adicionais (PT-PT, MCPs, assinatura) +- `references/output-patterns.md` — templates de output e padrões de exemplos +- `references/workflows.md` — padrões de workflows sequenciais e condicionais + +#### Graus de Liberdade + +Calibrar o nível de especificidade ao risco da tarefa: + +| Grau | Quando usar | Forma | +|------|-------------|-------| +| **Alta** | Múltiplas abordagens válidas, decisão contextual | Instruções textuais, heurísticas | +| **Média** | Padrão preferido, alguma variação aceitável | Pseudocódigo, scripts com parâmetros | +| **Baixa** | Operação frágil, consistência crítica, sequência obrigatória | Scripts específicos, poucos parâmetros | + +--- + +### Passo 5: Checklist de Qualidade + +**Frontmatter:** +- [ ] Apenas `name` e `description` (sem campos extra) +- [ ] `description` inclui triggers explícitos de quando usar +- [ ] `description` lista casos de uso concretos + +**SKILL.md corpo:** +- [ ] Abaixo de 500 linhas +- [ ] Sem secção "Quando Usar" no corpo +- [ ] References documentadas com contexto sobre quando ler +- [ ] Forma imperativa/infinitivo nas instruções +- [ ] PT-PT estrito (sem brasileirismos, sem emojis nos ficheiros) + +**Conteúdo:** +- [ ] Graus de liberdade calibrados ao risco +- [ ] Exemplos concretos de uso (bom vs mau) +- [ ] Scripts testados (se incluídos) +- [ ] Sem ficheiros auxiliares (README.md, CHANGELOG.md, etc.) + +**Padrões Descompl \ No newline at end of file diff --git a/dev-tools/skills/skill-creator/references/anthropic-patterns.md b/dev-tools/skills/skill-creator/references/anthropic-patterns.md new file mode 100644 index 0000000..5c17c49 --- /dev/null +++ b/dev-tools/skills/skill-creator/references/anthropic-patterns.md @@ -0,0 +1,258 @@ +# Padrões Oficiais Anthropic para Skills + +Referência baseada na documentação oficial do marketplace `anthropic-agent-skills`. + +## Sumário + +1. Progressive Disclosure (3 níveis de carregamento) +2. Graus de Liberdade (alta/média/baixa) +3. Anatomia de uma Skill +4. Padrões de Progressive Disclosure +5. O Que Não Incluir +6. Scripts init_skill.py e package_skill.py + +--- + +## 1. Progressive Disclosure + +Skills usam um sistema de carregamento em 3 níveis para gerir o contexto de forma eficiente: + +| Nível | O que carrega | Quando | Tamanho | +|-------|--------------|--------|---------| +| **Metadata** | `name` + `description` | Sempre em contexto | ~100 palavras | +| **SKILL.md corpo** | Instruções e workflow | Quando a skill é activada | <500 linhas | +| **Bundled resources** | Scripts, references, assets | Conforme necessário | Ilimitado | + +**Princípio fundamental:** O contexto é um bem público. Skills partilham o contexto com o system prompt, histórico de conversa, metadata de outras skills e o pedido actual do utilizador. + +**Assunção base:** O agente já é inteligente. Incluir apenas contexto que o agente não possui. Questionar cada parágrafo: "O agente realmente precisa desta explicação?" e "Este parágrafo justifica o custo em tokens?" + +--- + +## 2. Graus de Liberdade + +Calibrar o nível de especificidade ao risco e variabilidade da tarefa: + +### Alta Liberdade — Instruções Textuais + +Usar quando múltiplas abordagens são válidas, as decisões dependem do contexto, ou heurísticas guiam a abordagem. + +```markdown +## Análise de logs + +Examinar os logs do servidor para identificar padrões de erro. +Priorizar erros críticos e repetidos. Correlacionar timestamps com eventos +conhecidos. Sugerir acções correctivas com base nos padrões encontrados. +``` + +### Média Liberdade — Pseudocódigo ou Scripts com Parâmetros + +Usar quando existe um padrão preferido, alguma variação é aceitável, ou a configuração afecta o comportamento. + +```markdown +## Criar endpoint API + +1. Definir rota em `routes/api.php` +2. Criar FormRequest para validação +3. Implementar Controller com Service injectado +4. Retornar Resource para transformação +5. Documentar com PHPDoc + +Adaptar ao contexto: GET sem body, POST/PUT com FormRequest. +``` + +### Baixa Liberdade — Scripts Específicos, Poucos Parâmetros + +Usar quando as operações são frágeis e propensas a erro, a consistência é crítica, ou uma sequência específica deve ser seguida. + +```bash +# SEMPRE usar exactamente esta sequência para deploy: +git pull origin main +composer install --no-dev --optimize-autoloader +php artisan config:cache +php artisan route:cache +php artisan migrate --force +sudo systemctl reload php-fpm +``` + +**Analogia:** O agente é como um explorador num caminho — uma ponte estreita com precipícios precisa de guardas específicos (baixa liberdade), enquanto um campo aberto permite muitas rotas (alta liberdade). + +--- + +## 3. Anatomia de uma Skill + +``` +nome-da-skill/ +├── SKILL.md (obrigatório) +│ ├── Frontmatter YAML (obrigatório: name + description) +│ └── Corpo Markdown (obrigatório: instruções) +└── Recursos Bundled (opcional) + ├── scripts/ Código executável (Python/Bash/etc.) + ├── references/ Documentação carregada conforme necessário + └── assets/ Ficheiros usados no output (templates, ícones, fonts) +``` + +### SKILL.md + +**Frontmatter:** Contém apenas `name` e `description`. Estes são os únicos campos que o sistema lê para determinar quando a skill é activada. A `description` deve ser clara e abrangente — é o mecanismo primário de triggering. + +**Corpo:** Instruções e orientações para usar a skill. Só é carregado DEPOIS de a skill ser activada. + +### scripts/ + +Código executável para tarefas que requerem fiabilidade determinística ou são reescritas repetidamente. + +- Quando incluir: quando o mesmo código é reescrito repetidamente ou fiabilidade determinística é necessária +- Exemplo: `scripts/rotate_pdf.py` para rotação de PDFs +- Vantagem: eficiente em tokens, determinístico, pode ser executado sem carregar para contexto + +### references/ + +Documentação e material de referência para ser carregado conforme necessário. + +- Quando incluir: para documentação que o agente deve consultar durante o trabalho +- Exemplos: schemas de BD, documentação de API, políticas da empresa, guias detalhados de workflow +- Se ficheiros grandes (>10k palavras): incluir padrões de pesquisa grep em SKILL.md +- Evitar duplicação: informação deve existir em SKILL.md OU em references, nunca em ambos + +### assets/ + +Ficheiros não destinados a ser carregados em contexto, mas usados no output produzido. + +- Quando incluir: quando a skill precisa de ficheiros que serão usados no output final +- Exemplos: `assets/logo.png`, `assets/slides.pptx`, `assets/frontend-template/` + +--- + +## 4. Padrões de Progressive Disclosure + +### Padrão 1: Guia de Alto Nível com References + +```markdown +# Processamento de PDF + +## Início rápido + +Extrair texto com pdfplumber: +[exemplo de código] + +## Funcionalidades avançadas + +- **Preenchimento de formulários**: Ver references/forms.md para guia completo +- **Referência da API**: Ver references/reference.md para todos os métodos +- **Exemplos**: Ver references/examples.md para padrões comuns +``` + +Claude carrega `forms.md`, `reference.md`, ou `examples.md` apenas quando necessário. + +### Padrão 2: Organização por Domínio + +Para skills com múltiplos domínios, organizar por domínio para evitar carregar contexto irrelevante: + +``` +bigquery-skill/ +├── SKILL.md (visão geral e navegação) +└── references/ + ├── finance.md (métricas de receita, facturação) + ├── sales.md (oportunidades, pipeline) + ├── product.md (uso da API, funcionalidades) + └── marketing.md (campanhas, atribuição) +``` + +Quando o utilizador pergunta sobre métricas de vendas, Claude lê apenas `sales.md`. + +### Padrão 3: Detalhes Condicionais + +Mostrar conteúdo básico, ligar a conteúdo avançado: + +```markdown +# Processamento DOCX + +## Criar documentos + +Usar docx-js para novos documentos. Ver references/docx-js.md. + +## Editar documentos + +Para edições simples, modificar o XML directamente. + +**Para tracked changes**: Ver references/redlining.md +**Para detalhes OOXML**: Ver references/ooxml.md +``` + +**Directrizes importantes:** +- Evitar references aninhadas — manter um nível de profundidade a partir de SKILL.md +- Para ficheiros de reference com mais de 100 linhas: incluir índice no topo + +--- + +## 5. O Que Não Incluir + +Uma skill deve conter apenas ficheiros essenciais que suportam directamente a sua funcionalidade. **Não criar:** + +- README.md +- INSTALLATION_GUIDE.md +- QUICK_REFERENCE.md +- CHANGELOG.md +- Qualquer outro ficheiro auxiliar de documentação + +A skill deve conter apenas a informação que um agente precisa para executar o trabalho. Não deve conter contexto sobre o processo de criação, procedimentos de setup/teste, ou documentação orientada ao utilizador. + +--- + +## 6. Scripts init_skill.py e package_skill.py + +### init_skill.py + +Gera um directório de skill template com tudo o que é necessário: + +```bash +python scripts/init_skill.py --path +``` + +O script: +- Cria o directório da skill no path especificado +- Gera SKILL.md template com frontmatter correcto e placeholders TODO +- Cria directórios de exemplo: `scripts/`, `references/`, `assets/` +- Adiciona ficheiros de exemplo em cada directório + +Após inicialização, personalizar ou remover os ficheiros gerados conforme necessário. + +### package_skill.py + +Empacota a skill num ficheiro `.skill` distribuível: + +```bash +python scripts/package_skill.py +# Com directório de output opcional: +python scripts/package_skill.py ./dist +``` + +O script de packaging: + +1. **Valida** automaticamente: + - Formato do frontmatter YAML e campos obrigatórios + - Convenções de naming e estrutura de directórios + - Completude e qualidade da description + - Organização de ficheiros e referências a recursos + +2. **Empacota** se a validação passar, criando um ficheiro `.skill` (zip com extensão .skill) + +Se a validação falhar, o script reporta os erros e sai sem criar o pacote. + +--- + +## Processo Completo de Criação + +1. **Compreender** a skill com exemplos concretos +2. **Planear** conteúdos reutilizáveis (scripts, references, assets) +3. **Inicializar** a skill (executar init_skill.py) +4. **Implementar** recursos e escrever SKILL.md +5. **Empacotar** a skill (executar package_skill.py) +6. **Iterar** com base em uso real + +**Workflow de iteração:** +1. Usar a skill em tarefas reais +2. Notar dificuldades ou ineficiências +3. Identificar como SKILL.md ou recursos devem ser actualizados +4. Implementar alterações e testar novamente diff --git a/dev-tools/skills/skill-creator/references/descomplicar-standards.md b/dev-tools/skills/skill-creator/references/descomplicar-standards.md new file mode 100644 index 0000000..3a0d9d9 --- /dev/null +++ b/dev-tools/skills/skill-creator/references/descomplicar-standards.md @@ -0,0 +1,318 @@ +# Standards Descomplicar para Skills + +Standards adicionais aplicados sobre os padrões Anthropic base. + +## Sumário + +1. Frontmatter Descomplicar +2. Língua e Estilo PT-PT +3. Integração com MCPs +4. Assinatura de Código +5. Localização no Sistema de Plugins +6. Campos Opcionais do Frontmatter +7. Exemplos de Skills Bem e Mal Estruturadas + +--- + +## 1. Frontmatter Descomplicar + +O padrão mínimo segue o Anthropic (apenas `name` + `description`): + +```yaml +--- +name: nome-da-skill +description: > + Descrição completa do que a skill faz. Incluir domínio, tecnologias chave + e triggers explícitos. Usar quando: (1) caso de uso A, (2) caso de uso B, + (3) caso de uso C. +--- +``` + +**Campo opcional `allowed-tools`** (quando a skill usa ferramentas específicas): + +```yaml +--- +name: nome-da-skill +description: > + Descrição da skill. Usar quando [contexto]. +allowed-tools: Read, Write, Edit, Bash, mcp__mem_search_memories +--- +``` + +Incluir `allowed-tools` apenas quando a skill tem dependências de ferramentas não óbvias ou quando restringir o acesso é importante para segurança. + +--- + +## 2. Língua e Estilo PT-PT + +**Regra absoluta:** Português europeu estrito em tudo — código, comentários, documentação, output. + +### Acentuação obrigatória + +| Errado (BR) | Correcto (PT) | +|-------------|---------------| +| descrição | descrição | +| criacao | criação | +| informacao | informação | +| configuracao | configuração | +| autenticacao | autenticação | + +### Vocabulário técnico + +| Errado | Correcto | +|--------|----------| +| você | tu / o utilizador | +| usuário | utilizador | +| senha | palavra-passe | +| agendar | calendarizar | +| deletar | eliminar | +| implementar (como "deploy") | publicar / implementar | + +### Monetário + +``` +95EUR — correcto +95,00EUR — evitar casas decimais desnecessárias +R$95 — nunca +``` + +### Estilo + +- Usar **negrito** para ênfase (não MAIÚSCULAS) +- Excepção: siglas (API, MCP, CRM, JWT) +- Sem emojis nos ficheiros de skill +- Datas em DD-MM-YYYY nos ficheiros e notas + +--- + +## 3. Integração com MCPs + +Skills que necessitam de contexto especializado devem referenciar MCPs relevantes: + +### Padrão de consulta NotebookLM + +```markdown +## Contexto NotebookLM + +Consultar notebooks relevantes antes de executar: + +| Notebook | ID | Consultar quando | +|----------|-----|-----------------| +| Nome | id-do-notebook | situação específica | +``` + +### Notebooks principais por domínio + +| Domínio | Notebook ID | +|---------|-------------| +| Programação geral | `24947ffa-0019-448a-a340-2f4a275d2eb1` | +| Arquitectura / Delivery | `24947ffa-0019-448a-a340-2f4a275d2eb1` | +| Roadmap / Prioridades | `79d43410-0e29-4be1-881d-84db6bdc239a` | +| Gestão de Projectos / Agile | `0c9c079c-a426-486c-99eb-1564d42d37ad` | +| Claude Code / IA | `2876d1fe-5cea-4d98-8140-b0e1a81c6bc4` | +| MCPs / Desenvolvimento | `73102308-70ef-403e-9be9-eae0cfc62d55` | + +### Padrão de memória + +Para skills que aprendem com execuções anteriores: + +```markdown +## Memória + +Pesquisar experiência anterior antes de começar: +`mcp__mem_search_memories "[keywords relevantes]"` + +Guardar aprendizagens ao concluir: +`mcp__mem_save_memory { text: "[domínio/skill] ..." }` +``` + +--- + +## 4. Assinatura de Código + +Código produzido por skills deve incluir a assinatura Descomplicar: + +### PHP + +```php +//skills// +``` + +**Plugins disponíveis e as suas skills:** + +| Plugin | Path | Skills típicas | +|--------|------|----------------| +| dev-tools | `dev-tools/1.0.0/skills/` | php-dev, db-design, react-patterns, nextjs, security-check | +| gestao | `gestao/*/skills/` | worklog, desk, today | +| crm-ops | `crm-ops/*/skills/` | orcamento, lead-approach | +| core-tools | `core-tools/*/skills/` | _core, reflect | + +**Skill nova para plugin existente:** +1. Identificar o plugin mais adequado pelo domínio +2. Criar pasta em `skills//` +3. Verificar se o `plugin.json` precisa de ser actualizado + +**Skill nova para novo plugin:** +Coordenar com o processo de criação de plugin (fora do âmbito desta skill). + +--- + +## 6. Campos Opcionais do Frontmatter + +O Anthropic define apenas `name` e `description` como obrigatórios. O sistema Descomplicar aceita `allowed-tools` como campo adicional documentado: + +```yaml +--- +name: nome-da-skill +description: > + Descrição completa com triggers. +allowed-tools: Read, Write, Edit, Bash, mcp__mem_search_memories, mcp__notebooklm__notebook_query +--- +``` + +**Não adicionar** outros campos não documentados — quebra a compatibilidade com o packaging script. + +--- + +## 7. Exemplos de Skills Bem e Mal Estruturadas + +### Má: Description vaga sem triggers + +```yaml +--- +name: php-helper +description: Ajuda com PHP. +--- +``` + +**Problema:** O agente não sabe quando activar esta skill. + +### Má: Corpo do SKILL.md com "Quando Usar" + +```markdown +# PHP Helper + +## Quando Usar Esta Skill + +- Quando precisar de criar APIs +- Quando precisar de autenticação +``` + +**Problema:** A secção "Quando Usar" no corpo nunca é lida pelo sistema de triggering (que usa apenas a `description`). Esta informação pertence à `description`. + +### Má: Conteúdo duplicado + +```yaml +description: Usar quando criar APIs RESTful com Laravel. +``` + +```markdown +# PHP Dev + +## Quando usar + +- Criar APIs RESTful com Laravel ← duplicado da description +``` + +### Má: Ficheiros auxiliares desnecessários + +``` +php-dev/ +├── SKILL.md +├── README.md ← não deve existir +├── CHANGELOG.md ← não deve existir +└── references/ +``` + +### Boa: Description completa com triggers explícitos + +```yaml +--- +name: php-dev +description: > + Desenvolvimento PHP moderno com Laravel, Symfony e APIs RESTful — autenticação + (JWT, OAuth, Sanctum), service classes e refactorização de código legacy + seguindo padrões PSR. Usar quando: (1) criar APIs RESTful, (2) implementar + autenticação, (3) desenvolver service classes, (4) refactorizar código PHP, + (5) integrar com APIs externas. +--- +``` + +### Boa: Progressive disclosure correcta + +```markdown +# PHP Dev + +## Protocolo obrigatório + +1. Pesquisar memória: `mcp__mem_search_memories "[keywords]"` +2. Consultar NotebookLM: ver tabela em references/notebooklm-context.md +3. Implementar com padrões Laravel: ver references/laravel-patterns.md + +## Checklist de entrega + +- [ ] Assinatura Descomplicar® em todos os ficheiros +- [ ] Type hints completos +- [ ] PSR-12 compliant +``` + +O corpo do SKILL.md é conciso e delega detalhes para references. + +### Boa: Estrutura de directório mínima e limpa + +``` +php-dev/ +├── SKILL.md +└── references/ + ├── laravel-patterns.md + └── security-checklist.md +``` + +Apenas o necessário, sem ficheiros auxiliares. diff --git a/dev-tools/skills/skill-creator/references/output-patterns.md b/dev-tools/skills/skill-creator/references/output-patterns.md new file mode 100644 index 0000000..1b3a05b --- /dev/null +++ b/dev-tools/skills/skill-creator/references/output-patterns.md @@ -0,0 +1,184 @@ +# Padrões de Output para Skills + +Usar estes padrões quando skills precisam de produzir output consistente e de qualidade. + +## Sumário + +1. Padrão de Template +2. Padrão de Exemplos +3. Templates de Output Específicos Descomplicar + +--- + +## 1. Padrão de Template + +Fornecer templates para o formato de output. Calibrar o nível de rigor às necessidades. + +### Para requisitos estritos (respostas de API, formatos de dados) + +```markdown +## Estrutura do relatório + +SEMPRE usar exactamente esta estrutura: + +# [Título da Análise] + +## Sumário executivo +[Parágrafo único com as principais conclusões] + +## Principais conclusões +- Conclusão 1 com dados de suporte +- Conclusão 2 com dados de suporte + +## Recomendações +1. Recomendação específica e accionável +2. Recomendação específica e accionável +``` + +### Para orientação flexível (quando a adaptação é útil) + +```markdown +## Estrutura do relatório + +Formato sensato por defeito, adaptar com bom senso: + +# [Título da Análise] + +## Sumário executivo +[Visão geral] + +## Principais conclusões +[Adaptar secções com base no que for descoberto] + +## Recomendações +[Adaptar ao contexto específico] + +Ajustar secções conforme necessário para o tipo de análise. +``` + +--- + +## 2. Padrão de Exemplos + +Para skills em que a qualidade do output depende de ver exemplos, fornecer pares input/output: + +```markdown +## Formato de mensagem de commit + +Gerar mensagens de commit seguindo estes exemplos: + +**Exemplo 1:** +Input: Adicionada autenticação de utilizador com tokens JWT +Output: +``` +feat(auth): implementar autenticação JWT + +Adicionar endpoint de login e middleware de validação de tokens +``` + +**Exemplo 2:** +Input: Corrigido erro em que as datas eram apresentadas incorrectamente nos relatórios +Output: +``` +fix(reports): corrigir formatação de datas na conversão de timezone + +Usar timestamps UTC consistentemente em toda a geração de relatórios +``` + +Seguir este estilo: tipo(âmbito): descrição breve, depois explicação detalhada. +``` + +Exemplos ajudam o agente a compreender o estilo e nível de detalhe pretendidos melhor do que descrições isoladas. + +--- + +## 3. Templates de Output Específicos Descomplicar + +### Template de comentário Desk CRM (HTML) + +```html +

Descrição

+
    +
  • [Alteração realizada]
  • +
+

Ficheiros: [path do ficheiro]

+
+

Skill: /nome-da-skill | Data: YYYY-MM-DD

+``` + +### Template de relatório de análise técnica + +```markdown +# Análise: [Tema] + +## Contexto +[Situação actual e motivação] + +## Conclusões +- [Conclusão com evidência] +- [Conclusão com evidência] + +## Recomendações +1. [Acção específica] — Prioridade: Alta/Média/Baixa +2. [Acção específica] — Prioridade: Alta/Média/Baixa + +## Próximos passos +- [ ] [Tarefa concreta] — Responsável: [quem] +- [ ] [Tarefa concreta] — Data limite: DD-MM-YYYY + +--- +**Gerado por:** /nome-da-skill | **Data:** DD-MM-YYYY +``` + +### Template de user story (para skill-creator / planeamento) + +```markdown +## User Story: [Título] + +**Como** [tipo de utilizador], +**Quero** [acção/funcionalidade], +**Para que** [benefício/valor]. + +### Critérios de aceitação (GIVEN-WHEN-THEN) + +- **DADO** [contexto inicial] + **QUANDO** [acção do utilizador] + **ENTÃO** [resultado esperado] + +### Estimativa: [story points Fibonacci] +### Prioridade: Must/Should/Could/Won't +### Dependências: [lista de dependências] +``` + +### Template de output de skill para output de código + +Quando a skill produz código, estruturar o output assim: + +``` +[Breve explicação do que foi feito] + +**Ficheiro:** `path/para/ficheiro.ext` + +```[linguagem] +[código com assinatura Descomplicar] +``` + +**Notas de implementação:** +- [nota importante 1] +- [nota importante 2] + +**Próximos passos:** +- [ ] [verificação ou acção necessária] +``` + +--- + +## Calibragem de Templates por Risco + +| Situação | Template | Grau de liberdade | +|----------|----------|-------------------| +| Output para API / integração | Estrito, exacto | Baixo | +| Relatório técnico interno | Flexível com estrutura | Médio | +| Análise exploratória | Orientação geral | Alto | +| Código de produção | Estrito com assinatura | Baixo | +| Documentação interna | Flexível | Alto | diff --git a/dev-tools/skills/skill-creator/references/workflows.md b/dev-tools/skills/skill-creator/references/workflows.md new file mode 100644 index 0000000..88acfff --- /dev/null +++ b/dev-tools/skills/skill-creator/references/workflows.md @@ -0,0 +1,181 @@ +# Padrões de Workflow para Skills + +## Sumário + +1. Workflows Sequenciais +2. Workflows Condicionais +3. Workflow Completo de Criação de Skill Descomplicar +4. Checklist de Entrega Final + +--- + +## 1. Workflows Sequenciais + +Para tarefas complexas, dividir operações em passos claros e sequenciais. É útil dar ao agente uma visão geral do processo no início do SKILL.md: + +```markdown +Preencher um formulário PDF envolve estes passos: + +1. Analisar o formulário (executar analyze_form.py) +2. Criar mapeamento de campos (editar fields.json) +3. Validar mapeamento (executar validate_fields.py) +4. Preencher o formulário (executar fill_form.py) +5. Verificar output (executar verify_output.py) +``` + +### Características de um bom workflow sequencial + +- Cada passo é concreto e verificável +- Dependências entre passos são explícitas +- Artefactos de cada passo estão identificados +- Passos opcionais estão claramente marcados + +--- + +## 2. Workflows Condicionais + +Para tarefas com lógica de ramificação, guiar o agente pelos pontos de decisão: + +```markdown +1. Determinar o tipo de modificação: + **Criar novo conteúdo?** -> Seguir "Workflow de criação" abaixo + **Editar conteúdo existente?** -> Seguir "Workflow de edição" abaixo + +2. Workflow de criação: + a. [passo 1] + b. [passo 2] + +3. Workflow de edição: + a. [passo 1] + b. [passo 2] +``` + +### Árvore de decisão para tipo de skill + +Ao criar uma nova skill, determinar primeiro o tipo: + +``` +Skill nova ou actualização? +├── Nova skill +│ ├── Tem scripts reutilizáveis? -> incluir scripts/ +│ ├── Tem docs de referência detalhadas? -> incluir references/ +│ └── Tem templates/assets de output? -> incluir assets/ +└── Actualização de skill existente + ├── SKILL.md está >500 linhas? -> mover conteúdo para references/ + ├── Description é vaga? -> melhorar com triggers explícitos + └── Há duplicação SKILL.md / references? -> consolidar +``` + +--- + +## 3. Workflow Completo de Criação de Skill Descomplicar + +### Fase 1: Preparação (antes de escrever qualquer ficheiro) + +``` +1. Clarificar com o utilizador: + - Que tarefas concretas a skill executa? + - Que diria o utilizador para activar a skill? + - Que conhecimento o agente não tem por defeito? + +2. Identificar recursos reutilizáveis: + - Código que é reescrito repetidamente -> scripts/ + - Docs de referência, schemas, APIs -> references/ + - Templates, assets de output -> assets/ + +3. Escolher o plugin de destino: + - dev-tools: skills de desenvolvimento (PHP, JS, DB, segurança) + - gestao: skills operacionais (worklog, desk, today) + - crm-ops: skills comerciais (orçamento, leads) + - core-tools: skills core do sistema +``` + +### Fase 2: Estrutura + +``` +4. Criar directório da skill: + ~/.claude/plugins/cache/descomplicar-plugins//1.0.0/skills// + +5. Criar SKILL.md com: + - Frontmatter: name + description (com triggers explícitos) + - Corpo: workflow core, <500 linhas + - Referencias a ficheiros de references/ (quando e por quê) + +6. Criar references/ se necessário: + - Um ficheiro por domínio/tema + - Incluir índice no topo se >100 linhas + - Sem duplicação com SKILL.md +``` + +### Fase 3: Validação + +``` +7. Verificar checklist de qualidade: + [ ] Frontmatter: apenas name + description + [ ] Description: triggers explícitos incluídos + [ ] Corpo SKILL.md: <500 linhas + [ ] Sem secção "Quando Usar" no corpo + [ ] References documentadas com contexto sobre quando ler + [ ] PT-PT estrito (verificar acentuação e vocabulário) + [ ] Sem emojis nos ficheiros + [ ] Sem ficheiros auxiliares (README, CHANGELOG, etc.) + [ ] Graus de liberdade calibrados ao risco + [ ] Assinatura Descomplicar® no código (se aplicável) + +8. Empacotar (se scripts disponíveis): + python scripts/package_skill.py +``` + +### Fase 4: Iteração + +``` +9. Testar em uso real: + - Activar a skill numa tarefa real + - Notar onde o agente hesita ou erra + - Identificar informação em falta ou excessiva + +10. Actualizar baseado em feedback: + - Se o agente pede informação -> adicionar a references/ + - Se o agente ignora informação -> remover ou mover para references/ + - Se a skill activa nos momentos errados -> refinar description +``` + +--- + +## 4. Checklist de Entrega Final + +Antes de considerar uma skill completa, validar: + +### Frontmatter +- [ ] Apenas `name` e `description` (sem campos extra não documentados) +- [ ] `description` inclui o domínio e tecnologias principais +- [ ] `description` inclui triggers explícitos ("Usar quando...") +- [ ] `description` lista casos de uso concretos numerados + +### Corpo SKILL.md +- [ ] Abaixo de 500 linhas (contar com `wc -l SKILL.md`) +- [ ] Sem secção "Quando Usar" no corpo +- [ ] References documentadas: nome do ficheiro + quando ler +- [ ] Forma imperativa/infinitivo nas instruções +- [ ] Sem emojis + +### Língua e estilo +- [ ] PT-PT estrito (verificar: utilizador, palavra-passe, eliminar, calendarizar) +- [ ] Acentuação correcta (verificar: descrição, criação, configuração, autenticação) +- [ ] Monetário sem casas decimais desnecessárias +- [ ] Negrito para ênfase, não MAIÚSCULAS + +### Estrutura de ficheiros +- [ ] Apenas ficheiros que o agente precisa para o trabalho +- [ ] Sem README.md, CHANGELOG.md, INSTALLATION_GUIDE.md +- [ ] References com índice se >100 linhas +- [ ] Scripts testados se incluídos + +### Padrões Descomplicar +- [ ] MCPs relevantes referenciados (se aplicável) +- [ ] Assinatura Descomplicar® no código produzido (se aplicável) +- [ ] Graus de liberdade calibrados ao risco da operação + +### Validação técnica (Regra #49) +- [ ] Código auditado: segurança, erros, boas práticas, performance +- [ ] Score 10/10 antes de entregar diff --git a/dev-tools/skills/webapp-testing/SKILL.md b/dev-tools/skills/webapp-testing/SKILL.md new file mode 100644 index 0000000..703e2f5 --- /dev/null +++ b/dev-tools/skills/webapp-testing/SKILL.md @@ -0,0 +1,117 @@ +--- +name: webapp-testing +description: Testes e automacao de aplicacoes web locais com Playwright — verificacao de frontend, debug de UI, screenshots, logs de consola e gestao de ciclo de vida de servidores. +layer: wiki +--- + +# Web Application Testing + +Testar aplicacoes web locais escrevendo scripts Python nativos com Playwright. + +**Complementaridade com /chrome:** Esta skill foca-se em **scripts Playwright headless** para testes automatizados e CI/CD. Para automacao de browser com sessao activa (sites autenticados, GIF recording, debug visual interactivo), usar a skill `/chrome` que integra os MCPs `chrome-devtools` e `claude-in-chrome`. + +| Cenario | Skill | +|---------|-------| +| Testes automatizados headless | **/webapp-testing** (Playwright) | +| CI/CD pipeline testing | **/webapp-testing** (Playwright) | +| Debug visual com sessao activa | /chrome (MCP backends) | +| Performance traces (LCP/TBT/CLS) | /chrome (DevTools MCP) | +| Sites autenticados (Google, CRMs) | /chrome (Chrome extensao) | +| Scraping com interaccao DOM programatica | **/webapp-testing** (Playwright) | + +**Scripts auxiliares disponiveis:** +- `scripts/with_server.py` - Gestao de ciclo de vida de servidores (suporta multiplos servidores) + +**Executar sempre scripts com `--help` primeiro** para ver utilizacao. NAO ler o codigo fonte ate tentar executar o script e confirmar que uma solucao personalizada e absolutamente necessaria. Estes scripts podem ser extensos e poluir a context window. Existem para ser invocados como black-box. + +## Arvore de decisao: Escolher abordagem + +``` +Tarefa -> E HTML estatico? + |-- Sim -> Ler ficheiro HTML directamente para identificar selectores + | |-- Sucesso -> Escrever script Playwright com selectores + | |-- Falha/Incompleto -> Tratar como dinamico (abaixo) + | + |-- Nao (webapp dinamica) -> Servidor ja esta a correr? + |-- Nao -> Executar: python scripts/with_server.py --help + | Depois usar helper + escrever script Playwright simplificado + | + |-- Sim -> Reconhecimento-depois-accao: + 1. Navegar e esperar por networkidle + 2. Tirar screenshot ou inspeccionar DOM + 3. Identificar selectores a partir do estado renderizado + 4. Executar accoes com selectores descobertos +``` + +## Exemplo: Usar with_server.py + +Iniciar um servidor: executar `--help` primeiro, depois usar o helper. + +**Servidor unico:** +```bash +python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py +``` + +**Multiplos servidores (ex: backend + frontend):** +```bash +python scripts/with_server.py \ + --server "cd backend && python server.py" --port 3000 \ + --server "cd frontend && npm run dev" --port 5173 \ + -- python your_automation.py +``` + +Para criar um script de automacao, incluir apenas logica Playwright (servidores sao geridos automaticamente): +```python +from playwright.sync_api import sync_playwright + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) # Sempre lancar chromium em modo headless + page = browser.new_page() + page.goto('http://localhost:5173') # Servidor ja a correr e pronto + page.wait_for_load_state('networkidle') # CRITICO: Esperar JS executar + # ... logica de automacao + browser.close() +``` + +## Padrao reconhecimento-depois-accao + +1. **Inspeccionar DOM renderizado**: + ```python + page.screenshot(path='/tmp/inspect.png', full_page=True) + content = page.content() + page.locator('button').all() + ``` + +2. **Identificar selectores** a partir dos resultados da inspeccao + +3. **Executar accoes** usando selectores descobertos + +## Erro comum + +**NAO** inspeccionar o DOM antes de esperar por `networkidle` em apps dinamicas +**SIM** esperar por `page.wait_for_load_state('networkidle')` antes de inspeccionar + +## Boas praticas + +- **Usar scripts bundled como black boxes** - Para cumprir uma tarefa, considerar se um dos scripts em `scripts/` pode ajudar. Estes scripts tratam workflows comuns e complexos de forma fiavel sem poluir a context window. Usar `--help` para ver utilizacao, depois invocar directamente. +- Usar `sync_playwright()` para scripts sincronos +- Fechar sempre o browser quando terminar +- Usar selectores descritivos: `text=`, `role=`, selectores CSS, ou IDs +- Adicionar waits apropriados: `page.wait_for_selector()` ou `page.wait_for_timeout()` +- Screenshots guardar em `/tmp/` (nunca em directorio de projecto) +- Para testes de aplicacoes Descomplicar: verificar `localhost:3000` (Next.js) ou `localhost:5173` (Vite) + +## Ficheiros de referencia + +- **examples/** - Exemplos com padroes comuns: + - `element_discovery.py` - Descobrir botoes, links e inputs numa pagina + - `static_html_automation.py` - Usar URLs file:// para HTML local + - `console_logging.py` - Capturar logs de consola durante automacao + +## Integracao Descomplicar + +### MCPs complementares + +| MCP | Uso com webapp-testing | +|-----|----------------------| +| `chrome-devtools` | Performance traces, network inspection \ No newline at end of file diff --git a/dev-tools/skills/webapp-testing/examples/console_logging.py b/dev-tools/skills/webapp-testing/examples/console_logging.py new file mode 100644 index 0000000..9329b5e --- /dev/null +++ b/dev-tools/skills/webapp-testing/examples/console_logging.py @@ -0,0 +1,35 @@ +from playwright.sync_api import sync_playwright + +# Example: Capturing console logs during browser automation + +url = 'http://localhost:5173' # Replace with your URL + +console_logs = [] + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={'width': 1920, 'height': 1080}) + + # Set up console log capture + def handle_console_message(msg): + console_logs.append(f"[{msg.type}] {msg.text}") + print(f"Console: [{msg.type}] {msg.text}") + + page.on("console", handle_console_message) + + # Navigate to page + page.goto(url) + page.wait_for_load_state('networkidle') + + # Interact with the page (triggers console logs) + page.click('text=Dashboard') + page.wait_for_timeout(1000) + + browser.close() + +# Save console logs to file +with open('/mnt/user-data/outputs/console.log', 'w') as f: + f.write('\n'.join(console_logs)) + +print(f"\nCaptured {len(console_logs)} console messages") +print(f"Logs saved to: /mnt/user-data/outputs/console.log") \ No newline at end of file diff --git a/dev-tools/skills/webapp-testing/examples/element_discovery.py b/dev-tools/skills/webapp-testing/examples/element_discovery.py new file mode 100644 index 0000000..917ba72 --- /dev/null +++ b/dev-tools/skills/webapp-testing/examples/element_discovery.py @@ -0,0 +1,40 @@ +from playwright.sync_api import sync_playwright + +# Example: Discovering buttons and other elements on a page + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # Navigate to page and wait for it to fully load + page.goto('http://localhost:5173') + page.wait_for_load_state('networkidle') + + # Discover all buttons on the page + buttons = page.locator('button').all() + print(f"Found {len(buttons)} buttons:") + for i, button in enumerate(buttons): + text = button.inner_text() if button.is_visible() else "[hidden]" + print(f" [{i}] {text}") + + # Discover links + links = page.locator('a[href]').all() + print(f"\nFound {len(links)} links:") + for link in links[:5]: # Show first 5 + text = link.inner_text().strip() + href = link.get_attribute('href') + print(f" - {text} -> {href}") + + # Discover input fields + inputs = page.locator('input, textarea, select').all() + print(f"\nFound {len(inputs)} input fields:") + for input_elem in inputs: + name = input_elem.get_attribute('name') or input_elem.get_attribute('id') or "[unnamed]" + input_type = input_elem.get_attribute('type') or 'text' + print(f" - {name} ({input_type})") + + # Take screenshot for visual reference + page.screenshot(path='/tmp/page_discovery.png', full_page=True) + print("\nScreenshot saved to /tmp/page_discovery.png") + + browser.close() \ No newline at end of file diff --git a/dev-tools/skills/webapp-testing/examples/static_html_automation.py b/dev-tools/skills/webapp-testing/examples/static_html_automation.py new file mode 100644 index 0000000..90bbedc --- /dev/null +++ b/dev-tools/skills/webapp-testing/examples/static_html_automation.py @@ -0,0 +1,33 @@ +from playwright.sync_api import sync_playwright +import os + +# Example: Automating interaction with static HTML files using file:// URLs + +html_file_path = os.path.abspath('path/to/your/file.html') +file_url = f'file://{html_file_path}' + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={'width': 1920, 'height': 1080}) + + # Navigate to local HTML file + page.goto(file_url) + + # Take screenshot + page.screenshot(path='/mnt/user-data/outputs/static_page.png', full_page=True) + + # Interact with elements + page.click('text=Click Me') + page.fill('#name', 'John Doe') + page.fill('#email', 'john@example.com') + + # Submit form + page.click('button[type="submit"]') + page.wait_for_timeout(500) + + # Take final screenshot + page.screenshot(path='/mnt/user-data/outputs/after_submit.png', full_page=True) + + browser.close() + +print("Static HTML automation completed!") \ No newline at end of file diff --git a/dev-tools/skills/webapp-testing/scripts/with_server.py b/dev-tools/skills/webapp-testing/scripts/with_server.py new file mode 100644 index 0000000..431f2eb --- /dev/null +++ b/dev-tools/skills/webapp-testing/scripts/with_server.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Start one or more servers, wait for them to be ready, run a command, then clean up. + +Usage: + # Single server + python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py + python scripts/with_server.py --server "npm start" --port 3000 -- python test.py + + # Multiple servers + python scripts/with_server.py \ + --server "cd backend && python server.py" --port 3000 \ + --server "cd frontend && npm run dev" --port 5173 \ + -- python test.py +""" + +import subprocess +import socket +import time +import sys +import argparse + +def is_server_ready(port, timeout=30): + """Wait for server to be ready by polling the port.""" + start_time = time.time() + while time.time() - start_time < timeout: + try: + with socket.create_connection(('localhost', port), timeout=1): + return True + except (socket.error, ConnectionRefusedError): + time.sleep(0.5) + return False + + +def main(): + parser = argparse.ArgumentParser(description='Run command with one or more servers') + parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)') + parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)') + parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)') + parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready') + + args = parser.parse_args() + + # Remove the '--' separator if present + if args.command and args.command[0] == '--': + args.command = args.command[1:] + + if not args.command: + print("Error: No command specified to run") + sys.exit(1) + + # Parse server configurations + if len(args.servers) != len(args.ports): + print("Error: Number of --server and --port arguments must match") + sys.exit(1) + + servers = [] + for cmd, port in zip(args.servers, args.ports): + servers.append({'cmd': cmd, 'port': port}) + + server_processes = [] + + try: + # Start all servers + for i, server in enumerate(servers): + print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}") + + # Use shell=True to support commands with cd and && + process = subprocess.Popen( + server['cmd'], + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + server_processes.append(process) + + # Wait for this server to be ready + print(f"Waiting for server on port {server['port']}...") + if not is_server_ready(server['port'], timeout=args.timeout): + raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s") + + print(f"Server ready on port {server['port']}") + + print(f"\nAll {len(servers)} server(s) ready") + + # Run the command + print(f"Running: {' '.join(args.command)}\n") + result = subprocess.run(args.command) + sys.exit(result.returncode) + + finally: + # Clean up all servers + print(f"\nStopping {len(server_processes)} server(s)...") + for i, process in enumerate(server_processes): + try: + process.terminate() + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + print(f"Server {i+1} stopped") + print("All servers stopped") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/dev-tools/skills/xlsx/SKILL.md b/dev-tools/skills/xlsx/SKILL.md new file mode 100644 index 0000000..6ac8898 --- /dev/null +++ b/dev-tools/skills/xlsx/SKILL.md @@ -0,0 +1,134 @@ +--- +name: xlsx +description: Criacao, edicao e analise de ficheiros Excel (.xlsx, .xlsm, .csv, .tsv) — formulas, formatacao, modelos financeiros e integracao com facturacao Moloni. +layer: wiki +--- + +# Excel / XLSX Specialist + +Skill para manipulacao completa de folhas de calculo seguindo padroes Descomplicar®. + +## Quando Usar + +- Abrir, ler, editar ou corrigir ficheiros .xlsx, .xlsm, .csv ou .tsv +- Criar nova folha de calculo a partir de dados ou de raiz +- Converter entre formatos tabulares +- Limpar ou reestruturar dados tabulares (linhas malformadas, cabecalhos deslocados, dados residuais) +- Exportar dados do Desk CRM ou Moloni para Excel +- O entregavel final deve ser um ficheiro de folha de calculo + +## Quando NAO Usar + +- Quando o entregavel e um documento Word, relatorio HTML, script Python isolado ou pipeline de base de dados +- Para integracoes Google Sheets API +- Quando outra skill mais especifica esta disponivel + +--- + +# Requisitos para Outputs + +## Todos os ficheiros Excel + +### Fonte profissional +- Usar fonte consistente e profissional (ex.: Arial, Times New Roman) salvo indicacao contraria + +### Zero erros de formula +- Todo ficheiro Excel DEVE ser entregue com ZERO erros de formula (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?) + +### Preservar templates existentes (ao actualizar) +- Estudar e reproduzir EXACTAMENTE o formato, estilo e convencoes do ficheiro existente +- Nunca impor formatacao padronizada sobre ficheiros com padroes estabelecidos +- Convencoes do template existente SEMPRE prevalecem sobre estas directrizes + +## Modelos financeiros + +### Convencoes financeiras Portugal + +#### Formato monetario +- **Moeda**: EUR (nunca USD por defeito) +- **Formato numerico**: `#.##0,00 EUR` (separador milhares: ponto, separador decimal: virgula) +- **Exemplo**: 1.234,56 EUR (nao $1,234.56) +- **Negativos**: Parenteses (1.234,56) e nao -1.234,56 +- **Formato Excel**: `#.##0,00 €;(#.##0,00 €);"-"` + +#### Formato data +- **Padrao**: DD-MM-YYYY (ex.: 06-03-2026) +- **Excel format code**: `DD-MM-YYYY` +- **Nunca**: MM/DD/YYYY + +#### Formato percentagem +- Por defeito 0,0% (uma casa decimal) + +### Codigo de cores padrao +Excepto se indicado pelo utilizador ou template existente + +#### Convencoes standard da industria +- **Texto azul (RGB: 0,0,255)**: Inputs hardcoded, numeros que o utilizador pode alterar para cenarios +- **Texto preto (RGB: 0,0,0)**: TODAS as formulas e calculos +- **Texto verde (RGB: 0,128,0)**: Links a outras folhas dentro do mesmo workbook +- **Texto vermelho (RGB: 255,0,0)**: Links externos a outros ficheiros +- **Fundo amarelo (RGB: 255,255,0)**: Pressupostos-chave ou celulas que precisam de actualizacao + +### Formato numerico + +#### Regras obrigatorias +- **Anos**: Formatar como texto (ex.: "2024" e nao "2.024") +- **Moeda**: Usar `#.##0,00 €`; SEMPRE especificar unidades nos cabecalhos ("Receita (EUR)") +- **Zeros**: Usar formatacao para apresentar zeros como "-", incluindo percentagens +- **Multiplos**: Formato 0,0x para multiplos de avaliacao (EV/EBITDA, P/E) + +### Regras de construcao de formulas + +#### Colocacao de pressupostos +- Colocar TODOS os pressupostos (taxas de crescimento, margens, multiplos, etc.) em celulas separadas +- Usar referencias a celulas em vez de valores hardcoded nas formulas +- Exemplo: Usar =B5*(1+$B$6) em vez de =B5*1,05 + +#### Prevencao de erros de formula +- Verificar que todas as referencias estao correctas +- Verificar erros off-by-one em intervalos +- Garantir formulas consistentes em todos os periodos de projecao +- Testar com edge cases (valores zero, numeros negativos) +- Verificar que nao existem referencias circulares involuntarias + +#### Documentacao de hardcodes +- Comentar ou colocar em celulas adjacentes. Formato: "Fonte: [Sistema/Documento], [Data], [Referencia], [URL se aplicavel]" +- Exemplos: + - "Fonte: Relatorio Anual, FY2024, Pagina 45, Nota Receitas" + - "Fonte: Moloni, 06-03-2026, Extracto Vendas" + - "Fonte: Desk CRM, 06-03-2026, Relatorio Clientes" + +--- + +# Criacao, edicao e analise de XLSX + +## Overview + +O utilizador pode pedir para criar, editar ou analisar o conteudo de um ficheiro .xlsx. Existem ferramentas e workflows diferentes para tarefas diferentes. + +## Requisitos importantes + +**LibreOffice obrigatorio para recalculo de formulas**: O LibreOffice deve estar instalado para recalcular valores de formula usando o script `scripts/recalc.py`. O script configura o LibreOffice automaticamente na primeira execucao, incluindo ambientes sandboxed onde Unix sockets sao restritos (tratado por `scripts/office/soffice.py`). + +## Leitura e analise de dados + +### Analise com pandas +Para analise de dados, visualizacao e operacoes basicas, usar **pandas**: + +```python +import pandas as pd + +# Ler Excel +df = pd.read_excel('ficheiro.xlsx') # Default: primeira folha +all_sheets = pd.read_excel('ficheiro.xlsx', sheet_name=None) # Todas as folhas como dict + +# Analisar +df.head() # Preview dados +df.info() # Info colunas +df.describe() # Estatisticas + +# Escrever Excel +df.to_excel('output.xlsx', index=False) +``` + +# \ No newline at end of file diff --git a/dev-tools/skills/xlsx/scripts/office/helpers/__init__.py b/dev-tools/skills/xlsx/scripts/office/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev-tools/skills/xlsx/scripts/office/helpers/merge_runs.py b/dev-tools/skills/xlsx/scripts/office/helpers/merge_runs.py new file mode 100644 index 0000000..ad7c25e --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/helpers/merge_runs.py @@ -0,0 +1,199 @@ +"""Merge adjacent runs with identical formatting in DOCX. + +Merges adjacent elements that have identical properties. +Works on runs in paragraphs and inside tracked changes (, ). + +Also: +- Removes rsid attributes from runs (revision metadata that doesn't affect rendering) +- Removes proofErr elements (spell/grammar markers that block merging) +""" + +from pathlib import Path + +import defusedxml.minidom + + +def merge_runs(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + _remove_elements(root, "proofErr") + _strip_run_rsid_attrs(root) + + containers = {run.parentNode for run in _find_elements(root, "r")} + + merge_count = 0 + for container in containers: + merge_count += _merge_runs_in(container) + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Merged {merge_count} runs" + + except Exception as e: + return 0, f"Error: {e}" + + + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def _get_child(parent, tag: str): + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + return child + return None + + +def _get_children(parent, tag: str) -> list: + results = [] + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(child) + return results + + +def _is_adjacent(elem1, elem2) -> bool: + node = elem1.nextSibling + while node: + if node == elem2: + return True + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + return False + + + + +def _remove_elements(root, tag: str): + for elem in _find_elements(root, tag): + if elem.parentNode: + elem.parentNode.removeChild(elem) + + +def _strip_run_rsid_attrs(root): + for run in _find_elements(root, "r"): + for attr in list(run.attributes.values()): + if "rsid" in attr.name.lower(): + run.removeAttribute(attr.name) + + + + +def _merge_runs_in(container) -> int: + merge_count = 0 + run = _first_child_run(container) + + while run: + while True: + next_elem = _next_element_sibling(run) + if next_elem and _is_run(next_elem) and _can_merge(run, next_elem): + _merge_run_content(run, next_elem) + container.removeChild(next_elem) + merge_count += 1 + else: + break + + _consolidate_text(run) + run = _next_sibling_run(run) + + return merge_count + + +def _first_child_run(container): + for child in container.childNodes: + if child.nodeType == child.ELEMENT_NODE and _is_run(child): + return child + return None + + +def _next_element_sibling(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + return sibling + sibling = sibling.nextSibling + return None + + +def _next_sibling_run(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + if _is_run(sibling): + return sibling + sibling = sibling.nextSibling + return None + + +def _is_run(node) -> bool: + name = node.localName or node.tagName + return name == "r" or name.endswith(":r") + + +def _can_merge(run1, run2) -> bool: + rpr1 = _get_child(run1, "rPr") + rpr2 = _get_child(run2, "rPr") + + if (rpr1 is None) != (rpr2 is None): + return False + if rpr1 is None: + return True + return rpr1.toxml() == rpr2.toxml() + + +def _merge_run_content(target, source): + for child in list(source.childNodes): + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name != "rPr" and not name.endswith(":rPr"): + target.appendChild(child) + + +def _consolidate_text(run): + t_elements = _get_children(run, "t") + + for i in range(len(t_elements) - 1, 0, -1): + curr, prev = t_elements[i], t_elements[i - 1] + + if _is_adjacent(prev, curr): + prev_text = prev.firstChild.data if prev.firstChild else "" + curr_text = curr.firstChild.data if curr.firstChild else "" + merged = prev_text + curr_text + + if prev.firstChild: + prev.firstChild.data = merged + else: + prev.appendChild(run.ownerDocument.createTextNode(merged)) + + if merged.startswith(" ") or merged.endswith(" "): + prev.setAttribute("xml:space", "preserve") + elif prev.hasAttribute("xml:space"): + prev.removeAttribute("xml:space") + + run.removeChild(curr) diff --git a/dev-tools/skills/xlsx/scripts/office/helpers/simplify_redlines.py b/dev-tools/skills/xlsx/scripts/office/helpers/simplify_redlines.py new file mode 100644 index 0000000..db963bb --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/helpers/simplify_redlines.py @@ -0,0 +1,197 @@ +"""Simplify tracked changes by merging adjacent w:ins or w:del elements. + +Merges adjacent elements from the same author into a single element. +Same for elements. This makes heavily-redlined documents easier to +work with by reducing the number of tracked change wrappers. + +Rules: +- Only merges w:ins with w:ins, w:del with w:del (same element type) +- Only merges if same author (ignores timestamp differences) +- Only merges if truly adjacent (only whitespace between them) +""" + +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path + +import defusedxml.minidom + +WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +def simplify_redlines(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + merge_count = 0 + + containers = _find_elements(root, "p") + _find_elements(root, "tc") + + for container in containers: + merge_count += _merge_tracked_changes_in(container, "ins") + merge_count += _merge_tracked_changes_in(container, "del") + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Simplified {merge_count} tracked changes" + + except Exception as e: + return 0, f"Error: {e}" + + +def _merge_tracked_changes_in(container, tag: str) -> int: + merge_count = 0 + + tracked = [ + child + for child in container.childNodes + if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag) + ] + + if len(tracked) < 2: + return 0 + + i = 0 + while i < len(tracked) - 1: + curr = tracked[i] + next_elem = tracked[i + 1] + + if _can_merge_tracked(curr, next_elem): + _merge_tracked_content(curr, next_elem) + container.removeChild(next_elem) + tracked.pop(i + 1) + merge_count += 1 + else: + i += 1 + + return merge_count + + +def _is_element(node, tag: str) -> bool: + name = node.localName or node.tagName + return name == tag or name.endswith(f":{tag}") + + +def _get_author(elem) -> str: + author = elem.getAttribute("w:author") + if not author: + for attr in elem.attributes.values(): + if attr.localName == "author" or attr.name.endswith(":author"): + return attr.value + return author + + +def _can_merge_tracked(elem1, elem2) -> bool: + if _get_author(elem1) != _get_author(elem2): + return False + + node = elem1.nextSibling + while node and node != elem2: + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + + return True + + +def _merge_tracked_content(target, source): + while source.firstChild: + child = source.firstChild + source.removeChild(child) + target.appendChild(child) + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]: + if not doc_xml_path.exists(): + return {} + + try: + tree = ET.parse(doc_xml_path) + root = tree.getroot() + except ET.ParseError: + return {} + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + + return authors + + +def _get_authors_from_docx(docx_path: Path) -> dict[str, int]: + try: + with zipfile.ZipFile(docx_path, "r") as zf: + if "word/document.xml" not in zf.namelist(): + return {} + with zf.open("word/document.xml") as f: + tree = ET.parse(f) + root = tree.getroot() + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + return authors + except (zipfile.BadZipFile, ET.ParseError): + return {} + + +def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str: + modified_xml = modified_dir / "word" / "document.xml" + modified_authors = get_tracked_change_authors(modified_xml) + + if not modified_authors: + return default + + original_authors = _get_authors_from_docx(original_docx) + + new_changes: dict[str, int] = {} + for author, count in modified_authors.items(): + original_count = original_authors.get(author, 0) + diff = count - original_count + if diff > 0: + new_changes[author] = diff + + if not new_changes: + return default + + if len(new_changes) == 1: + return next(iter(new_changes)) + + raise ValueError( + f"Multiple authors added new changes: {new_changes}. " + "Cannot infer which author to validate." + ) diff --git a/dev-tools/skills/xlsx/scripts/office/pack.py b/dev-tools/skills/xlsx/scripts/office/pack.py new file mode 100644 index 0000000..db29ed8 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/pack.py @@ -0,0 +1,159 @@ +"""Pack a directory into a DOCX, PPTX, or XLSX file. + +Validates with auto-repair, condenses XML formatting, and creates the Office file. + +Usage: + python pack.py [--original ] [--validate true|false] + +Examples: + python pack.py unpacked/ output.docx --original input.docx + python pack.py unpacked/ output.pptx --validate false +""" + +import argparse +import sys +import shutil +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + +def pack( + input_directory: str, + output_file: str, + original_file: str | None = None, + validate: bool = True, + infer_author_func=None, +) -> tuple[None, str]: + input_dir = Path(input_directory) + output_path = Path(output_file) + suffix = output_path.suffix.lower() + + if not input_dir.is_dir(): + return None, f"Error: {input_dir} is not a directory" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file" + + if validate and original_file: + original_path = Path(original_file) + if original_path.exists(): + success, output = _run_validation( + input_dir, original_path, suffix, infer_author_func + ) + if output: + print(output) + if not success: + return None, f"Error: Validation failed for {input_dir}" + + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + _condense_xml(xml_file) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: + for f in temp_content_dir.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(temp_content_dir)) + + return None, f"Successfully packed {input_dir} to {output_file}" + + +def _run_validation( + unpacked_dir: Path, + original_file: Path, + suffix: str, + infer_author_func=None, +) -> tuple[bool, str | None]: + output_lines = [] + validators = [] + + if suffix == ".docx": + author = "Claude" + if infer_author_func: + try: + author = infer_author_func(unpacked_dir, original_file) + except ValueError as e: + print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr) + + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file), + RedliningValidator(unpacked_dir, original_file, author=author), + ] + elif suffix == ".pptx": + validators = [PPTXSchemaValidator(unpacked_dir, original_file)] + + if not validators: + return True, None + + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + output_lines.append(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + output_lines.append("All validations PASSED!") + + return success, "\n".join(output_lines) if output_lines else None + + +def _condense_xml(xml_file: Path) -> None: + try: + with open(xml_file, encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + + for element in dom.getElementsByTagName("*"): + if element.tagName.endswith(":t"): + continue + + for child in list(element.childNodes): + if ( + child.nodeType == child.TEXT_NODE + and child.nodeValue + and child.nodeValue.strip() == "" + ) or child.nodeType == child.COMMENT_NODE: + element.removeChild(child) + + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + except Exception as e: + print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Pack a directory into a DOCX, PPTX, or XLSX file" + ) + parser.add_argument("input_directory", help="Unpacked Office document directory") + parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") + parser.add_argument( + "--original", + help="Original file for validation comparison", + ) + parser.add_argument( + "--validate", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Run validation with auto-repair (default: true)", + ) + args = parser.parse_args() + + _, message = pack( + args.input_directory, + args.output_file, + original_file=args.original, + validate=args.validate, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 0000000..6454ef9 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 0000000..afa4f46 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 0000000..64e66b8 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 0000000..687eea8 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 0000000..6ac81b0 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 0000000..1dbf051 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 0000000..f1af17d --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 0000000..0a185ab --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 0000000..14ef488 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 0000000..c20f3bf --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 0000000..ac60252 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 0000000..424b8ba --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 0000000..2bddce2 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 0000000..8a8c18b --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 0000000..5c42706 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 0000000..853c341 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 0000000..da835ee --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 0000000..87ad265 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 0000000..9e86f1b --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 0000000..d0be42e --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 0000000..8821dd1 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 0000000..ca2575c --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 0000000..dd079e6 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 0000000..3dd6cf6 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 0000000..f1041e3 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 0000000..9c5b7a6 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 0000000..0f13678 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 0000000..a6de9d2 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 0000000..10e978b --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 0000000..4248bf7 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 0000000..5649746 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/mce/mc.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/mce/mc.xsd new file mode 100644 index 0000000..ef72545 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd new file mode 100644 index 0000000..f65f777 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd new file mode 100644 index 0000000..6b00755 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd new file mode 100644 index 0000000..f321d33 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 0000000..364c6a9 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 0000000..fed9d15 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 0000000..680cf15 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 0000000..89ada90 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dev-tools/skills/xlsx/scripts/office/soffice.py b/dev-tools/skills/xlsx/scripts/office/soffice.py new file mode 100644 index 0000000..c7f7e32 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/soffice.py @@ -0,0 +1,183 @@ +""" +Helper for running LibreOffice (soffice) in environments where AF_UNIX +sockets may be blocked (e.g., sandboxed VMs). Detects the restriction +at runtime and applies an LD_PRELOAD shim if needed. + +Usage: + from office.soffice import run_soffice, get_soffice_env + + # Option 1 – run soffice directly + result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"]) + + # Option 2 – get env dict for your own subprocess calls + env = get_soffice_env() + subprocess.run(["soffice", ...], env=env) +""" + +import os +import socket +import subprocess +import tempfile +from pathlib import Path + + +def get_soffice_env() -> dict: + env = os.environ.copy() + env["SAL_USE_VCLPLUGIN"] = "svp" + + if _needs_shim(): + shim = _ensure_shim() + env["LD_PRELOAD"] = str(shim) + + return env + + +def run_soffice(args: list[str], **kwargs) -> subprocess.CompletedProcess: + env = get_soffice_env() + return subprocess.run(["soffice"] + args, env=env, **kwargs) + + + +_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so" + + +def _needs_shim() -> bool: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.close() + return False + except OSError: + return True + + +def _ensure_shim() -> Path: + if _SHIM_SO.exists(): + return _SHIM_SO + + src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" + src.write_text(_SHIM_SOURCE) + subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], + check=True, + capture_output=True, + ) + src.unlink() + return _SHIM_SO + + + +_SHIM_SOURCE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +static int (*real_socket)(int, int, int); +static int (*real_socketpair)(int, int, int, int[2]); +static int (*real_listen)(int, int); +static int (*real_accept)(int, struct sockaddr *, socklen_t *); +static int (*real_close)(int); +static int (*real_read)(int, void *, size_t); + +/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */ +static int is_shimmed[1024]; +static int peer_of[1024]; +static int wake_r[1024]; /* accept() blocks reading this */ +static int wake_w[1024]; /* close() writes to this */ +static int listener_fd = -1; /* FD that received listen() */ + +__attribute__((constructor)) +static void init(void) { + real_socket = dlsym(RTLD_NEXT, "socket"); + real_socketpair = dlsym(RTLD_NEXT, "socketpair"); + real_listen = dlsym(RTLD_NEXT, "listen"); + real_accept = dlsym(RTLD_NEXT, "accept"); + real_close = dlsym(RTLD_NEXT, "close"); + real_read = dlsym(RTLD_NEXT, "read"); + for (int i = 0; i < 1024; i++) { + peer_of[i] = -1; + wake_r[i] = -1; + wake_w[i] = -1; + } +} + +/* ---- socket ---------------------------------------------------------- */ +int socket(int domain, int type, int protocol) { + if (domain == AF_UNIX) { + int fd = real_socket(domain, type, protocol); + if (fd >= 0) return fd; + /* socket(AF_UNIX) blocked – fall back to socketpair(). */ + int sv[2]; + if (real_socketpair(domain, type, protocol, sv) == 0) { + if (sv[0] >= 0 && sv[0] < 1024) { + is_shimmed[sv[0]] = 1; + peer_of[sv[0]] = sv[1]; + int wp[2]; + if (pipe(wp) == 0) { + wake_r[sv[0]] = wp[0]; + wake_w[sv[0]] = wp[1]; + } + } + return sv[0]; + } + errno = EPERM; + return -1; + } + return real_socket(domain, type, protocol); +} + +/* ---- listen ---------------------------------------------------------- */ +int listen(int sockfd, int backlog) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + listener_fd = sockfd; + return 0; + } + return real_listen(sockfd, backlog); +} + +/* ---- accept ---------------------------------------------------------- */ +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + /* Block until close() writes to the wake pipe. */ + if (wake_r[sockfd] >= 0) { + char buf; + real_read(wake_r[sockfd], &buf, 1); + } + errno = ECONNABORTED; + return -1; + } + return real_accept(sockfd, addr, addrlen); +} + +/* ---- close ----------------------------------------------------------- */ +int close(int fd) { + if (fd >= 0 && fd < 1024 && is_shimmed[fd]) { + int was_listener = (fd == listener_fd); + is_shimmed[fd] = 0; + + if (wake_w[fd] >= 0) { /* unblock accept() */ + char c = 0; + write(wake_w[fd], &c, 1); + real_close(wake_w[fd]); + wake_w[fd] = -1; + } + if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; } + if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; } + + if (was_listener) + _exit(0); /* conversion done – exit */ + } + return real_close(fd); +} +""" + + + +if __name__ == "__main__": + import sys + result = run_soffice(sys.argv[1:]) + sys.exit(result.returncode) diff --git a/dev-tools/skills/xlsx/scripts/office/unpack.py b/dev-tools/skills/xlsx/scripts/office/unpack.py new file mode 100644 index 0000000..0015253 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/unpack.py @@ -0,0 +1,132 @@ +"""Unpack Office files (DOCX, PPTX, XLSX) for editing. + +Extracts the ZIP archive, pretty-prints XML files, and optionally: +- Merges adjacent runs with identical formatting (DOCX only) +- Simplifies adjacent tracked changes from same author (DOCX only) + +Usage: + python unpack.py [options] + +Examples: + python unpack.py document.docx unpacked/ + python unpack.py presentation.pptx unpacked/ + python unpack.py document.docx unpacked/ --merge-runs false +""" + +import argparse +import sys +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from helpers.merge_runs import merge_runs as do_merge_runs +from helpers.simplify_redlines import simplify_redlines as do_simplify_redlines + +SMART_QUOTE_REPLACEMENTS = { + "\u201c": "“", + "\u201d": "”", + "\u2018": "‘", + "\u2019": "’", +} + + +def unpack( + input_file: str, + output_directory: str, + merge_runs: bool = True, + simplify_redlines: bool = True, +) -> tuple[None, str]: + input_path = Path(input_file) + output_path = Path(output_directory) + suffix = input_path.suffix.lower() + + if not input_path.exists(): + return None, f"Error: {input_file} does not exist" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {input_file} must be a .docx, .pptx, or .xlsx file" + + try: + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_path, "r") as zf: + zf.extractall(output_path) + + xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) + for xml_file in xml_files: + _pretty_print_xml(xml_file) + + message = f"Unpacked {input_file} ({len(xml_files)} XML files)" + + if suffix == ".docx": + if simplify_redlines: + simplify_count, _ = do_simplify_redlines(str(output_path)) + message += f", simplified {simplify_count} tracked changes" + + if merge_runs: + merge_count, _ = do_merge_runs(str(output_path)) + message += f", merged {merge_count} runs" + + for xml_file in xml_files: + _escape_smart_quotes(xml_file) + + return None, message + + except zipfile.BadZipFile: + return None, f"Error: {input_file} is not a valid Office file" + except Exception as e: + return None, f"Error unpacking: {e}" + + +def _pretty_print_xml(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="utf-8")) + except Exception: + pass + + +def _escape_smart_quotes(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + for char, entity in SMART_QUOTE_REPLACEMENTS.items(): + content = content.replace(char, entity) + xml_file.write_text(content, encoding="utf-8") + except Exception: + pass + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Unpack an Office file (DOCX, PPTX, XLSX) for editing" + ) + parser.add_argument("input_file", help="Office file to unpack") + parser.add_argument("output_directory", help="Output directory") + parser.add_argument( + "--merge-runs", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent runs with identical formatting (DOCX only, default: true)", + ) + parser.add_argument( + "--simplify-redlines", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent tracked changes from same author (DOCX only, default: true)", + ) + args = parser.parse_args() + + _, message = unpack( + args.input_file, + args.output_directory, + merge_runs=args.merge_runs, + simplify_redlines=args.simplify_redlines, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/dev-tools/skills/xlsx/scripts/office/validate.py b/dev-tools/skills/xlsx/scripts/office/validate.py new file mode 100644 index 0000000..03b01f6 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/validate.py @@ -0,0 +1,111 @@ +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py [--original ] [--auto-repair] [--author NAME] + +The first argument can be either: +- An unpacked directory containing the Office document XML files +- A packed Office file (.docx/.pptx/.xlsx) which will be unpacked to a temp directory + +Auto-repair fixes: +- paraId/durableId values that exceed OOXML limits +- Missing xml:space="preserve" on w:t elements with whitespace +""" + +import argparse +import sys +import tempfile +import zipfile +from pathlib import Path + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "path", + help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx)", + ) + parser.add_argument( + "--original", + required=False, + default=None, + help="Path to original file (.docx/.pptx/.xlsx). If omitted, all XSD errors are reported and redlining validation is skipped.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--auto-repair", + action="store_true", + help="Automatically repair common issues (hex IDs, whitespace preservation)", + ) + parser.add_argument( + "--author", + default="Claude", + help="Author name for redlining validation (default: Claude)", + ) + args = parser.parse_args() + + path = Path(args.path) + assert path.exists(), f"Error: {path} does not exist" + + original_file = None + if args.original: + original_file = Path(args.original) + assert original_file.is_file(), f"Error: {original_file} is not a file" + assert original_file.suffix.lower() in [".docx", ".pptx", ".xlsx"], ( + f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" + ) + + file_extension = (original_file or path).suffix.lower() + assert file_extension in [".docx", ".pptx", ".xlsx"], ( + f"Error: Cannot determine file type from {path}. Use --original or provide a .docx/.pptx/.xlsx file." + ) + + if path.is_file() and path.suffix.lower() in [".docx", ".pptx", ".xlsx"]: + temp_dir = tempfile.mkdtemp() + with zipfile.ZipFile(path, "r") as zf: + zf.extractall(temp_dir) + unpacked_dir = Path(temp_dir) + else: + assert path.is_dir(), f"Error: {path} is not a directory or Office file" + unpacked_dir = path + + match file_extension: + case ".docx": + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + if original_file: + validators.append( + RedliningValidator(unpacked_dir, original_file, verbose=args.verbose, author=args.author) + ) + case ".pptx": + validators = [ + PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + case _: + print(f"Error: Validation not supported for file type {file_extension}") + sys.exit(1) + + if args.auto_repair: + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + print(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/dev-tools/skills/xlsx/scripts/office/validators/__init__.py b/dev-tools/skills/xlsx/scripts/office/validators/__init__.py new file mode 100644 index 0000000..db092ec --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/validators/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/dev-tools/skills/xlsx/scripts/office/validators/base.py b/dev-tools/skills/xlsx/scripts/office/validators/base.py new file mode 100644 index 0000000..db4a06a --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/validators/base.py @@ -0,0 +1,847 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import defusedxml.minidom +import lxml.etree + + +class BaseSchemaValidator: + + IGNORED_VALIDATION_ERRORS = [ + "hyphenationZone", + "purl.org/dc/terms", + ] + + UNIQUE_ID_REQUIREMENTS = { + "comment": ("id", "file"), + "commentrangestart": ("id", "file"), + "commentrangeend": ("id", "file"), + "bookmarkstart": ("id", "file"), + "bookmarkend": ("id", "file"), + "sldid": ("id", "file"), + "sldmasterid": ("id", "global"), + "sldlayoutid": ("id", "global"), + "cm": ("authorid", "file"), + "sheet": ("sheetid", "file"), + "definedname": ("id", "file"), + "cxnsp": ("id", "file"), + "sp": ("id", "file"), + "pic": ("id", "file"), + "grpsp": ("id", "file"), + } + + EXCLUDED_ID_CONTAINERS = { + "sectionlst", + } + + ELEMENT_RELATIONSHIP_TYPES = {} + + SCHEMA_MAPPINGS = { + "word": "ISO-IEC29500-4_2016/wml.xsd", + "ppt": "ISO-IEC29500-4_2016/pml.xsd", + "xl": "ISO-IEC29500-4_2016/sml.xsd", + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file=None, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) if original_file else None + self.verbose = verbose + + self.schemas_dir = Path(__file__).parent.parent / "schemas" + + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + raise NotImplementedError("Subclasses must implement the validate method") + + def repair(self) -> int: + return self.repair_whitespace_preservation() + + def repair_whitespace_preservation(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if elem.tagName.endswith(":t") and elem.firstChild: + text = elem.firstChild.nodeValue + if text and (text.startswith((' ', '\t')) or text.endswith((' ', '\t'))): + if elem.getAttribute("xml:space") != "preserve": + elem.setAttribute("xml:space", "preserve") + text_preview = repr(text[:30]) + "..." if len(text) > 30 else repr(text) + print(f" Repaired: {xml_file.name}: Added xml:space='preserve' to {elem.tagName}: {text_preview}") + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + def validate_xml(self): + errors = [] + + for xml_file in self.xml_files: + try: + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + errors = [] + global_ids = {} + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} + + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + for elem in root.iter(): + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + if tag in self.UNIQUE_ID_REQUIREMENTS: + in_excluded_container = any( + ancestor.tag.split("}")[-1].lower() in self.EXCLUDED_ID_CONTAINERS + for ancestor in elem.iterancestors() + ) + if in_excluded_container: + continue + + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + errors = [] + + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): + all_files.append(file_path.resolve()) + + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + for rels_file in rels_files: + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + rels_dir = rels_file.parent + + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if target and not target.startswith( + ("http", "mailto:") + ): + if target.startswith("/"): + target_path = self.unpacked_dir / target.lstrip("/") + elif rels_file.name == ".rels": + target_path = self.unpacked_dir / target + else: + base_dir = rels_dir.parent + target_path = base_dir / target + + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + import lxml.etree + + errors = [] + + for xml_file in self.xml_files: + if xml_file.suffix == ".rels": + continue + + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + if not rels_file.exists(): + continue + + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + r_ns = self.OFFICE_RELATIONSHIPS_NAMESPACE + rid_attrs_to_check = ["id", "embed", "link"] + for elem in xml_root.iter(): + for attr_name in rid_attrs_to_check: + rid_attr = elem.get(f"{{{r_ns}}}{attr_name}") + if not rid_attr: + continue + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> r:{attr_name} references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + elif attr_name == "id" and self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + elem_lower = element_name.lower() + + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + if elem_lower.endswith("id") and len(elem_lower) > 2: + prefix = elem_lower[:-2] + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + if prefix == "sld": + return "slide" + return prefix.lower() + + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] + return prefix.lower() + + return None + + def validate_content_types(self): + errors = [] + + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", + "document", + "workbook", + "worksheet", + "theme", + } + + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue + + for file_path in all_files: + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() + elif is_valid: + return True, set() + + original_errors = self._get_original_file_errors(xml_file) + + assert current_errors is not None + new_errors = current_errors - original_errors + + new_errors = { + e for e in new_errors + if not any(pattern in e for pattern in self.IGNORED_VALIDATION_ERRORS) + } + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + original_error_count += 1 + valid_count += 1 + continue + + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + for attr in attrs_to_remove: + del elem.attrib[attr] + + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + elements_to_remove = [] + + for elem in list(root): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + self._remove_ignorable_elements(elem) + + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + root = xml_doc.getroot() + + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path): + schema_path = self._get_schema_path(xml_file) + if not schema_path: + return None, None + + try: + with open(schema_path, "rb") as xsd_file: + parser = lxml.etree.XMLParser() + xsd_doc = lxml.etree.parse( + xsd_file, parser=parser, base_url=str(schema_path) + ) + schema = lxml.etree.XMLSchema(xsd_doc) + + with open(xml_file, "r") as f: + xml_doc = lxml.etree.parse(f) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file): + if self.original_file is None: + return set() + + import tempfile + import zipfile + + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_path) + + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + return set() + + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + for elem in xml_copy.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/xlsx/scripts/office/validators/docx.py b/dev-tools/skills/xlsx/scripts/office/validators/docx.py new file mode 100644 index 0000000..fec405e --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/validators/docx.py @@ -0,0 +1,446 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import random +import re +import tempfile +import zipfile + +import defusedxml.minidom +import lxml.etree + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml" + W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid" + + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_whitespace_preservation(): + all_valid = False + + if not self.validate_deletions(): + all_valid = False + + if not self.validate_insertions(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_id_constraints(): + all_valid = False + + if not self.validate_comment_markers(): + all_valid = False + + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + if re.search(r"^[ \t\n\r]", text) or re.search( + r"[ \t\n\r]$", text + ): + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + for t_elem in root.xpath(".//w:del//w:t", namespaces=namespaces): + if t_elem.text: + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + for instr_elem in root.xpath( + ".//w:del//w:instrText", namespaces=namespaces + ): + text_preview = ( + repr(instr_elem.text or "")[:50] + "..." + if len(repr(instr_elem.text or "")) > 50 + else repr(instr_elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {instr_elem.sourceline}: found within (use ): {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + count = 0 + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + original = self.original_file + if original is None: + return 0 + + count = 0 + + try: + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(original, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + original_count = self.count_paragraphs_in_original() + new_count = self.count_paragraphs_in_unpacked() + + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})") + + def _parse_id_value(self, val: str, base: int = 16) -> int: + return int(val, base) + + def validate_id_constraints(self): + errors = [] + para_id_attr = f"{{{self.W14_NAMESPACE}}}paraId" + durable_id_attr = f"{{{self.W16CID_NAMESPACE}}}durableId" + + for xml_file in self.xml_files: + try: + for elem in lxml.etree.parse(str(xml_file)).iter(): + if val := elem.get(para_id_attr): + if self._parse_id_value(val, base=16) >= 0x80000000: + errors.append( + f" {xml_file.name}:{elem.sourceline}: paraId={val} >= 0x80000000" + ) + + if val := elem.get(durable_id_attr): + if xml_file.name == "numbering.xml": + try: + if self._parse_id_value(val, base=10) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} must be decimal in numbering.xml" + ) + else: + if self._parse_id_value(val, base=16) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except Exception: + pass + + if errors: + print(f"FAILED - {len(errors)} ID constraint violations:") + for e in errors: + print(e) + elif self.verbose: + print("PASSED - All paraId/durableId values within constraints") + return not errors + + def validate_comment_markers(self): + errors = [] + + document_xml = None + comments_xml = None + for xml_file in self.xml_files: + if xml_file.name == "document.xml" and "word" in str(xml_file): + document_xml = xml_file + elif xml_file.name == "comments.xml": + comments_xml = xml_file + + if not document_xml: + if self.verbose: + print("PASSED - No document.xml found (skipping comment validation)") + return True + + try: + doc_root = lxml.etree.parse(str(document_xml)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + range_starts = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeStart", namespaces=namespaces + ) + } + range_ends = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeEnd", namespaces=namespaces + ) + } + references = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentReference", namespaces=namespaces + ) + } + + orphaned_ends = range_ends - range_starts + for comment_id in sorted( + orphaned_ends, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeEnd id="{comment_id}" has no matching commentRangeStart' + ) + + orphaned_starts = range_starts - range_ends + for comment_id in sorted( + orphaned_starts, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeStart id="{comment_id}" has no matching commentRangeEnd' + ) + + comment_ids = set() + if comments_xml and comments_xml.exists(): + comments_root = lxml.etree.parse(str(comments_xml)).getroot() + comment_ids = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in comments_root.xpath( + ".//w:comment", namespaces=namespaces + ) + } + + marker_ids = range_starts | range_ends | references + invalid_refs = marker_ids - comment_ids + for comment_id in sorted( + invalid_refs, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + if comment_id: + errors.append( + f' document.xml: marker id="{comment_id}" references non-existent comment' + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append(f" Error parsing XML: {e}") + + if errors: + print(f"FAILED - {len(errors)} comment marker violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All comment markers properly paired") + return True + + def repair(self) -> int: + repairs = super().repair() + repairs += self.repair_durableId() + return repairs + + def repair_durableId(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if not elem.hasAttribute("w16cid:durableId"): + continue + + durable_id = elem.getAttribute("w16cid:durableId") + needs_repair = False + + if xml_file.name == "numbering.xml": + try: + needs_repair = ( + self._parse_id_value(durable_id, base=10) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + else: + try: + needs_repair = ( + self._parse_id_value(durable_id, base=16) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + + if needs_repair: + value = random.randint(1, 0x7FFFFFFE) + if xml_file.name == "numbering.xml": + new_id = str(value) + else: + new_id = f"{value:08X}" + + elem.setAttribute("w16cid:durableId", new_id) + print( + f" Repaired: {xml_file.name}: durableId {durable_id} → {new_id}" + ) + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/xlsx/scripts/office/validators/pptx.py b/dev-tools/skills/xlsx/scripts/office/validators/pptx.py new file mode 100644 index 0000000..09842aa --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/validators/pptx.py @@ -0,0 +1,275 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_uuid_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_slide_layout_ids(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_notes_slide_references(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + return all_valid + + def validate_uuid_ids(self): + import lxml.etree + + errors = [] + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(): + for attr, value in elem.attrib.items(): + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + if self._looks_like_uuid(value): + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + clean_value = value.strip("{}()").replace("-", "") + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + import lxml.etree + + errors = [] + + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + root = lxml.etree.parse(str(slide_master)).getroot() + + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + import lxml.etree + + errors = [] + notes_slide_references = {} + + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + target = rel.get("Target", "") + if target: + normalized_target = target.replace("../", "") + + slide_name = rels_file.stem.replace( + ".xml", "" + ) + + if normalized_target not in notes_slide_references: + notes_slide_references[normalized_target] = [] + notes_slide_references[normalized_target].append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/xlsx/scripts/office/validators/redlining.py b/dev-tools/skills/xlsx/scripts/office/validators/redlining.py new file mode 100644 index 0000000..71c81b6 --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/office/validators/redlining.py @@ -0,0 +1,247 @@ +""" +Validator for tracked changes in Word documents. +""" + +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +class RedliningValidator: + + def __init__(self, unpacked_dir, original_docx, verbose=False, author="Claude"): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.author = author + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def repair(self) -> int: + return 0 + + def validate(self): + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + try: + import xml.etree.ElementTree as ET + + tree = ET.parse(modified_file) + root = tree.getroot() + + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + author_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + author_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + + if not author_del_elements and not author_ins_elements: + if self.verbose: + print(f"PASSED - No tracked changes by {self.author} found.") + return True + + except Exception: + pass + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + zip_ref.extractall(temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + try: + import xml.etree.ElementTree as ET + + modified_tree = ET.parse(modified_file) + modified_root = modified_tree.getroot() + original_tree = ET.parse(original_file) + original_root = original_tree.getroot() + except ET.ParseError as e: + print(f"FAILED - Error parsing XML files: {e}") + return False + + self._remove_author_tracked_changes(original_root) + self._remove_author_tracked_changes(modified_root) + + modified_text = self._extract_text_content(modified_root) + original_text = self._extract_text_content(original_root) + + if modified_text != original_text: + error_message = self._generate_detailed_diff( + original_text, modified_text + ) + print(error_message) + return False + + if self.verbose: + print(f"PASSED - All changes by {self.author} are properly tracked") + return True + + def _generate_detailed_diff(self, original_text, modified_text): + error_parts = [ + f"FAILED - Document text doesn't match after removing {self.author}'s tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend(["Differences:", "============", git_diff]) + else: + error_parts.append("Unable to generate word diff (git not available)") + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + pass + + return None + + def _remove_author_tracked_changes(self, root): + ins_tag = f"{{{self.namespaces['w']}}}ins" + del_tag = f"{{{self.namespaces['w']}}}del" + author_attr = f"{{{self.namespaces['w']}}}author" + + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag == ins_tag and child.get(author_attr) == self.author: + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + deltext_tag = f"{{{self.namespaces['w']}}}delText" + t_tag = f"{{{self.namespaces['w']}}}t" + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag == del_tag and child.get(author_attr) == self.author: + to_process.append((child, list(parent).index(child))) + + for del_elem, del_index in reversed(to_process): + for elem in del_elem.iter(): + if elem.tag == deltext_tag: + elem.tag = t_tag + + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + def _extract_text_content(self, root): + p_tag = f"{{{self.namespaces['w']}}}p" + t_tag = f"{{{self.namespaces['w']}}}t" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + text_parts = [] + for t_elem in p_elem.findall(f".//{t_tag}"): + if t_elem.text: + text_parts.append(t_elem.text) + paragraph_text = "".join(text_parts) + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/dev-tools/skills/xlsx/scripts/recalc.py b/dev-tools/skills/xlsx/scripts/recalc.py new file mode 100644 index 0000000..f472e9a --- /dev/null +++ b/dev-tools/skills/xlsx/scripts/recalc.py @@ -0,0 +1,184 @@ +""" +Excel Formula Recalculation Script +Recalculates all formulas in an Excel file using LibreOffice +""" + +import json +import os +import platform +import subprocess +import sys +from pathlib import Path + +from office.soffice import get_soffice_env + +from openpyxl import load_workbook + +MACRO_DIR_MACOS = "~/Library/Application Support/LibreOffice/4/user/basic/Standard" +MACRO_DIR_LINUX = "~/.config/libreoffice/4/user/basic/Standard" +MACRO_FILENAME = "Module1.xba" + +RECALCULATE_MACRO = """ + + + Sub RecalculateAndSave() + ThisComponent.calculateAll() + ThisComponent.store() + ThisComponent.close(True) + End Sub +""" + + +def has_gtimeout(): + try: + subprocess.run( + ["gtimeout", "--version"], capture_output=True, timeout=1, check=False + ) + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def setup_libreoffice_macro(): + macro_dir = os.path.expanduser( + MACRO_DIR_MACOS if platform.system() == "Darwin" else MACRO_DIR_LINUX + ) + macro_file = os.path.join(macro_dir, MACRO_FILENAME) + + if ( + os.path.exists(macro_file) + and "RecalculateAndSave" in Path(macro_file).read_text() + ): + return True + + if not os.path.exists(macro_dir): + subprocess.run( + ["soffice", "--headless", "--terminate_after_init"], + capture_output=True, + timeout=10, + env=get_soffice_env(), + ) + os.makedirs(macro_dir, exist_ok=True) + + try: + Path(macro_file).write_text(RECALCULATE_MACRO) + return True + except Exception: + return False + + +def recalc(filename, timeout=30): + if not Path(filename).exists(): + return {"error": f"File {filename} does not exist"} + + abs_path = str(Path(filename).absolute()) + + if not setup_libreoffice_macro(): + return {"error": "Failed to setup LibreOffice macro"} + + cmd = [ + "soffice", + "--headless", + "--norestore", + "vnd.sun.star.script:Standard.Module1.RecalculateAndSave?language=Basic&location=application", + abs_path, + ] + + if platform.system() == "Linux": + cmd = ["timeout", str(timeout)] + cmd + elif platform.system() == "Darwin" and has_gtimeout(): + cmd = ["gtimeout", str(timeout)] + cmd + + result = subprocess.run(cmd, capture_output=True, text=True, env=get_soffice_env()) + + if result.returncode != 0 and result.returncode != 124: + error_msg = result.stderr or "Unknown error during recalculation" + if "Module1" in error_msg or "RecalculateAndSave" not in error_msg: + return {"error": "LibreOffice macro not configured properly"} + return {"error": error_msg} + + try: + wb = load_workbook(filename, data_only=True) + + excel_errors = [ + "#VALUE!", + "#DIV/0!", + "#REF!", + "#NAME?", + "#NULL!", + "#NUM!", + "#N/A", + ] + error_details = {err: [] for err in excel_errors} + total_errors = 0 + + for sheet_name in wb.sheetnames: + ws = wb[sheet_name] + for row in ws.iter_rows(): + for cell in row: + if cell.value is not None and isinstance(cell.value, str): + for err in excel_errors: + if err in cell.value: + location = f"{sheet_name}!{cell.coordinate}" + error_details[err].append(location) + total_errors += 1 + break + + wb.close() + + result = { + "status": "success" if total_errors == 0 else "errors_found", + "total_errors": total_errors, + "error_summary": {}, + } + + for err_type, locations in error_details.items(): + if locations: + result["error_summary"][err_type] = { + "count": len(locations), + "locations": locations[:20], + } + + wb_formulas = load_workbook(filename, data_only=False) + formula_count = 0 + for sheet_name in wb_formulas.sheetnames: + ws = wb_formulas[sheet_name] + for row in ws.iter_rows(): + for cell in row: + if ( + cell.value + and isinstance(cell.value, str) + and cell.value.startswith("=") + ): + formula_count += 1 + wb_formulas.close() + + result["total_formulas"] = formula_count + + return result + + except Exception as e: + return {"error": str(e)} + + +def main(): + if len(sys.argv) < 2: + print("Usage: python recalc.py [timeout_seconds]") + print("\nRecalculates all formulas in an Excel file using LibreOffice") + print("\nReturns JSON with error details:") + print(" - status: 'success' or 'errors_found'") + print(" - total_errors: Total number of Excel errors found") + print(" - total_formulas: Number of formulas in the file") + print(" - error_summary: Breakdown by error type with locations") + print(" - #VALUE!, #DIV/0!, #REF!, #NAME?, #NULL!, #NUM!, #N/A") + sys.exit(1) + + filename = sys.argv[1] + timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 30 + + result = recalc(filename, timeout) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/dev-tools/skills/youtube-extract/SKILL.md b/dev-tools/skills/youtube-extract/SKILL.md new file mode 100644 index 0000000..c345666 --- /dev/null +++ b/dev-tools/skills/youtube-extract/SKILL.md @@ -0,0 +1,155 @@ +--- +name: youtube-extract +category: dev-tools +description: "Extrai transcrições e metadados de vídeos, playlists e canais YouTube para o ficheiro de pesquisa Stack_Pesquisa_Videos.md. Usar quando 'youtube', 'vídeo', 'playlist', 'canal', 'transcrição', 'extrair vídeo', 'pesquisa vídeos'." +version: "1.0.0" +created: 2026-04-04 +tools: [Bash] +layer: wiki +--- + +# Skill: /youtube-extract + +Extrai transcrições e metadados de vídeos, playlists e canais YouTube usando o script `youtube-pesquisa-videos.py`. Adiciona automaticamente ao ficheiro de pesquisa do StackDescomplicar. + +--- + +## Ficheiros relevantes + +| Ficheiro | Caminho | +|----------|---------| +| **Script** | `/media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py` | +| **Destino padrão** | `/media/ealmeida/Dados/Hub/04-Stack/Stack_Pesquisa_Videos.md` | + +--- + +## Processo de execução + +### 1. Identificar o pedido + +Detectar na mensagem do utilizador: +- **URLs** — vídeos individuais, playlists (`?list=`), canais (`@canal`, `/c/`, `/channel/`) +- **Limite** — "primeiros 10", "máx 5", etc. → usar `--max N` +- **Destino** — ficheiro alternativo? → usar `--output caminho` +- **Modo** — adicionar ao ficheiro (`--append`) vs mostrar no terminal (sem flag) + +### 2. Construir o comando + +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + URL1 URL2 ... \ + [--max N] \ + [--append] \ + [--output /caminho/alternativo.md] \ + [--file lista.txt] +``` + +### 3. Exemplos por tipo de URL + +**Vídeo individual:** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtu.be/dQw4w9WgXcQ" --append +``` + +**Playlist completa:** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtube.com/playlist?list=PLxxx" --append +``` + +**Playlist com limite:** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtube.com/playlist?list=PLxxx" --max 10 --append +``` + +**Canal completo (máx. 20):** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtube.com/@NomeCanal" --max 20 --append +``` + +**Mix de URLs:** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtu.be/ID1" \ + "https://youtube.com/playlist?list=PLxxx" \ + --max 5 --append +``` + +**Ficheiro de lista:** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + --file /caminho/lista.txt --append +``` + +**Pré-visualizar sem gravar:** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtu.be/ID1" +``` + +--- + +## Comportamento padrão + +| Situação | Acção | +|----------|-------| +| URL única de vídeo | Extrair e perguntar se adiciona ao ficheiro | +| Múltiplos vídeos | Extrair todos e adicionar com `--append` | +| Playlist/canal sem limite | Alertar o utilizador — perguntar limite antes de executar | +| Ficheiro de destino não especificado | Usar `Stack_Pesquisa_Videos.md` | + +> **Aviso obrigatório para playlists/canais sem `--max`:** Informar o utilizador do número de vídeos antes de processar — canais podem ter centenas. + +--- + +## Após a extracção + +1. Informar quantos vídeos foram adicionados e os números `#N` atribuídos +2. Sugerir ao utilizador que complete os campos `*(a completar)*` no ficheiro +3. Se houver erros (transcrição indisponível), mencionar quais os vídeos afectados +4. Opcional: abrir o ficheiro no editor — `xdg-open "Stack_Pesquisa_Videos.md"` + +--- + +## Pré-requisitos + +- `yt-dlp` instalado: `pip install yt-dlp` ou `brew install yt-dlp` +- Python 3.8+ +- Acesso à internet + +Verificar disponibilidade: +```bash +yt-dlp --version && python3 --version +``` + +--- + +## Casos especiais + +**ID curto do YouTube (11 chars):** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py dQw4w9WgXcQ --append +``` + +**Destino alternativo (outro projecto):** +```bash +python3 /media/ealmeida/Dados/Dev/ClaudeDev/youtube-pesquisa-videos.py \ + "https://youtu.be/ID1" \ + --output "/media/ealmeida/Dados/Hub/05-Projectos/MeuProjecto/videos.md" \ + --append +``` + +--- + +## Healing Log + +Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar. + +```jsonl +{"date":"","issue":"","fix":"","source":"user|auto"} +``` + +*Adicionar nova linha após cada erro corrigido.*