@nguyenphp/antigravity-marketing 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -78
- package/package.json +4 -3
- package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
- package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
- package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
- package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
- package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
- package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
- package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
- package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
- package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
- package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
- package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
- package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
- package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
- package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
- package/templates/.agent/skills/minimax-pdf/README.md +222 -0
- package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
- package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
- package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
- package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
- package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
- package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
- package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
- package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
- package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
- package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
- package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
- package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
- package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
- package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
- package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
- package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
- package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
- package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
- package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
- package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
- package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
- package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
- package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
- package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
- package/templates/.agent/workflows/analyze.md +3 -0
- package/templates/.agent/workflows/brand-report.md +44 -0
- package/templates/.agent/workflows/report.md +49 -0
- package/templates/.agent/agents/backend-specialist.md +0 -263
- package/templates/.agent/agents/database-architect.md +0 -226
- package/templates/.agent/agents/debugger.md +0 -225
- package/templates/.agent/agents/devops-engineer.md +0 -242
- package/templates/.agent/agents/frontend-specialist.md +0 -527
- package/templates/.agent/agents/game-developer.md +0 -162
- package/templates/.agent/agents/mobile-developer.md +0 -377
- package/templates/.agent/agents/penetration-tester.md +0 -188
- package/templates/.agent/agents/security-auditor.md +0 -170
- package/templates/.agent/agents/test-engineer.md +0 -158
- package/templates/.agent/skills/api-patterns/SKILL.md +0 -81
- package/templates/.agent/skills/api-patterns/api-style.md +0 -42
- package/templates/.agent/skills/api-patterns/auth.md +0 -24
- package/templates/.agent/skills/api-patterns/documentation.md +0 -26
- package/templates/.agent/skills/api-patterns/graphql.md +0 -41
- package/templates/.agent/skills/api-patterns/rate-limiting.md +0 -31
- package/templates/.agent/skills/api-patterns/response.md +0 -37
- package/templates/.agent/skills/api-patterns/rest.md +0 -40
- package/templates/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
- package/templates/.agent/skills/api-patterns/security-testing.md +0 -122
- package/templates/.agent/skills/api-patterns/trpc.md +0 -41
- package/templates/.agent/skills/api-patterns/versioning.md +0 -22
- package/templates/.agent/skills/app-builder/SKILL.md +0 -75
- package/templates/.agent/skills/app-builder/agent-coordination.md +0 -71
- package/templates/.agent/skills/app-builder/feature-building.md +0 -53
- package/templates/.agent/skills/app-builder/project-detection.md +0 -34
- package/templates/.agent/skills/app-builder/scaffolding.md +0 -118
- package/templates/.agent/skills/app-builder/tech-stack.md +0 -40
- package/templates/.agent/skills/app-builder/templates/SKILL.md +0 -39
- package/templates/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
- package/templates/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
- package/templates/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
- package/templates/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
- package/templates/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
- package/templates/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
- package/templates/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
- package/templates/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -82
- package/templates/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -100
- package/templates/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -106
- package/templates/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -101
- package/templates/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
- package/templates/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -93
- package/templates/.agent/skills/architecture/SKILL.md +0 -55
- package/templates/.agent/skills/architecture/context-discovery.md +0 -43
- package/templates/.agent/skills/architecture/examples.md +0 -94
- package/templates/.agent/skills/architecture/pattern-selection.md +0 -68
- package/templates/.agent/skills/architecture/patterns-reference.md +0 -50
- package/templates/.agent/skills/architecture/trade-off-analysis.md +0 -77
- package/templates/.agent/skills/bash-linux/SKILL.md +0 -199
- package/templates/.agent/skills/behavioral-modes/SKILL.md +0 -242
- package/templates/.agent/skills/clean-code/SKILL.md +0 -201
- package/templates/.agent/skills/code-review-checklist/SKILL.md +0 -109
- package/templates/.agent/skills/database-design/SKILL.md +0 -52
- package/templates/.agent/skills/database-design/database-selection.md +0 -43
- package/templates/.agent/skills/database-design/indexing.md +0 -39
- package/templates/.agent/skills/database-design/migrations.md +0 -48
- package/templates/.agent/skills/database-design/optimization.md +0 -36
- package/templates/.agent/skills/database-design/orm-selection.md +0 -30
- package/templates/.agent/skills/database-design/schema-design.md +0 -56
- package/templates/.agent/skills/database-design/scripts/schema_validator.py +0 -172
- package/templates/.agent/skills/deployment-procedures/SKILL.md +0 -241
- package/templates/.agent/skills/docker-expert/SKILL.md +0 -409
- package/templates/.agent/skills/game-development/2d-games/SKILL.md +0 -119
- package/templates/.agent/skills/game-development/3d-games/SKILL.md +0 -135
- package/templates/.agent/skills/game-development/SKILL.md +0 -167
- package/templates/.agent/skills/game-development/game-art/SKILL.md +0 -185
- package/templates/.agent/skills/game-development/game-audio/SKILL.md +0 -190
- package/templates/.agent/skills/game-development/game-design/SKILL.md +0 -129
- package/templates/.agent/skills/game-development/mobile-games/SKILL.md +0 -108
- package/templates/.agent/skills/game-development/multiplayer/SKILL.md +0 -132
- package/templates/.agent/skills/game-development/pc-games/SKILL.md +0 -144
- package/templates/.agent/skills/game-development/vr-ar/SKILL.md +0 -123
- package/templates/.agent/skills/game-development/web-games/SKILL.md +0 -150
- package/templates/.agent/skills/lint-and-validate/SKILL.md +0 -45
- package/templates/.agent/skills/lint-and-validate/scripts/lint_runner.py +0 -172
- package/templates/.agent/skills/lint-and-validate/scripts/type_coverage.py +0 -173
- package/templates/.agent/skills/mcp-builder/SKILL.md +0 -176
- package/templates/.agent/skills/nestjs-expert/SKILL.md +0 -552
- package/templates/.agent/skills/nextjs-best-practices/SKILL.md +0 -203
- package/templates/.agent/skills/nodejs-best-practices/SKILL.md +0 -333
- package/templates/.agent/skills/parallel-agents/SKILL.md +0 -175
- package/templates/.agent/skills/performance-profiling/SKILL.md +0 -143
- package/templates/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +0 -76
- package/templates/.agent/skills/powershell-windows/SKILL.md +0 -167
- package/templates/.agent/skills/prisma-expert/SKILL.md +0 -355
- package/templates/.agent/skills/python-patterns/SKILL.md +0 -441
- package/templates/.agent/skills/react-patterns/SKILL.md +0 -198
- package/templates/.agent/skills/red-team-tactics/SKILL.md +0 -199
- package/templates/.agent/skills/server-management/SKILL.md +0 -161
- package/templates/.agent/skills/systematic-debugging/SKILL.md +0 -109
- package/templates/.agent/skills/tdd-workflow/SKILL.md +0 -149
- package/templates/.agent/skills/testing-patterns/SKILL.md +0 -178
- package/templates/.agent/skills/testing-patterns/scripts/test_runner.py +0 -219
- package/templates/.agent/skills/typescript-expert/SKILL.md +0 -429
- package/templates/.agent/skills/vue-expert/SKILL.md +0 -374
- package/templates/.agent/skills/vulnerability-scanner/SKILL.md +0 -276
- package/templates/.agent/skills/vulnerability-scanner/checklists.md +0 -121
- package/templates/.agent/skills/vulnerability-scanner/scripts/security_scan.py +0 -458
- package/templates/.agent/skills/webapp-testing/SKILL.md +0 -187
- package/templates/.agent/skills/webapp-testing/scripts/playwright_runner.py +0 -173
- package/templates/.agent/workflows/debug.md +0 -103
- package/templates/.agent/workflows/deploy.md +0 -176
- package/templates/.agent/workflows/enhance.md +0 -63
- package/templates/.agent/workflows/test.md +0 -144
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""
|
|
4
|
+
style_audit.py — Financial formatting compliance checker for xlsx files.
|
|
5
|
+
|
|
6
|
+
Audits an xlsx file (or an unpacked xlsx directory) and reports:
|
|
7
|
+
1. Style system integrity: count attributes match actual element counts
|
|
8
|
+
2. Color-role violations: formula cells with blue font, input cells with black font
|
|
9
|
+
3. Year-format violations: cells containing 4-digit years using comma-format
|
|
10
|
+
4. Percentage value violations: percentage-formatted cells with values > 1 (likely meant 0.08 not 8)
|
|
11
|
+
5. Style index out-of-range: s attribute exceeds cellXfs count
|
|
12
|
+
6. fills[0]/fills[1] presence check (OOXML spec requirement)
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 style_audit.py input.xlsx # audit a packed xlsx
|
|
16
|
+
python3 style_audit.py /tmp/xlsx_work/ # audit an unpacked directory
|
|
17
|
+
python3 style_audit.py input.xlsx --json # machine-readable output
|
|
18
|
+
python3 style_audit.py input.xlsx --summary # counts only, no detail
|
|
19
|
+
|
|
20
|
+
Exit code:
|
|
21
|
+
0 — no violations found
|
|
22
|
+
1 — violations detected (or file cannot be opened)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
import zipfile
|
|
28
|
+
import xml.etree.ElementTree as ET
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import tempfile
|
|
32
|
+
import shutil
|
|
33
|
+
|
|
34
|
+
NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
35
|
+
NSP = f"{{{NS}}}"
|
|
36
|
+
|
|
37
|
+
# Predefined style index semantics from minimal_xlsx template.
|
|
38
|
+
# Maps cellXfs index -> (role, font_color_expectation, numFmt_type)
|
|
39
|
+
# role: "input" = blue expected, "formula" = black/green expected, "header" = any, "any" = skip
|
|
40
|
+
TEMPLATE_SLOT_ROLES = {
|
|
41
|
+
0: ("any", None, None),
|
|
42
|
+
1: ("input", "blue", "general"),
|
|
43
|
+
2: ("formula", "black", "general"),
|
|
44
|
+
3: ("formula", "green", "general"),
|
|
45
|
+
4: ("any", None, "general"), # header
|
|
46
|
+
5: ("input", "blue", "currency"),
|
|
47
|
+
6: ("formula", "black", "currency"),
|
|
48
|
+
7: ("input", "blue", "percent"),
|
|
49
|
+
8: ("formula", "black", "percent"),
|
|
50
|
+
9: ("input", "blue", "integer"),
|
|
51
|
+
10: ("formula", "black", "integer"),
|
|
52
|
+
11: ("input", "blue", "year"),
|
|
53
|
+
12: ("input", "blue", "general"), # highlight
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# AARRGGBB values for each role color
|
|
57
|
+
BLUE_RGB = "000000ff"
|
|
58
|
+
BLACK_RGB = "00000000"
|
|
59
|
+
GREEN_RGB = "00008000"
|
|
60
|
+
RED_RGB = "00ff0000"
|
|
61
|
+
|
|
62
|
+
# numFmtIds that represent percentage formats (built-in + common custom)
|
|
63
|
+
PERCENT_FMT_IDS = {9, 10, 165, 170}
|
|
64
|
+
|
|
65
|
+
# numFmtIds that use comma separator (would corrupt year display)
|
|
66
|
+
COMMA_FMT_IDS = {3, 4, 167, 168} # #,##0 style — 4-digit years would show as 2,024
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_styles(styles_xml: bytes) -> dict:
|
|
70
|
+
"""Parse styles.xml and return structured data."""
|
|
71
|
+
root = ET.fromstring(styles_xml)
|
|
72
|
+
|
|
73
|
+
def find(tag):
|
|
74
|
+
return root.find(f"{NSP}{tag}")
|
|
75
|
+
|
|
76
|
+
# numFmts
|
|
77
|
+
num_fmts = {} # id -> formatCode
|
|
78
|
+
nf_elem = find("numFmts")
|
|
79
|
+
if nf_elem is not None:
|
|
80
|
+
declared_count = int(nf_elem.get("count", "0"))
|
|
81
|
+
actual_count = len(nf_elem)
|
|
82
|
+
for nf in nf_elem:
|
|
83
|
+
fid = int(nf.get("numFmtId", "0"))
|
|
84
|
+
num_fmts[fid] = nf.get("formatCode", "")
|
|
85
|
+
else:
|
|
86
|
+
declared_count = 0
|
|
87
|
+
actual_count = 0
|
|
88
|
+
|
|
89
|
+
# fonts — extract color and bold flag
|
|
90
|
+
fonts = []
|
|
91
|
+
fonts_elem = find("fonts")
|
|
92
|
+
fonts_declared = 0
|
|
93
|
+
if fonts_elem is not None:
|
|
94
|
+
fonts_declared = int(fonts_elem.get("count", "0"))
|
|
95
|
+
for font in fonts_elem:
|
|
96
|
+
color_elem = font.find(f"{NSP}color")
|
|
97
|
+
bold_elem = font.find(f"{NSP}b")
|
|
98
|
+
if color_elem is not None:
|
|
99
|
+
rgb = color_elem.get("rgb", "").lower()
|
|
100
|
+
theme = color_elem.get("theme")
|
|
101
|
+
else:
|
|
102
|
+
rgb = ""
|
|
103
|
+
theme = None
|
|
104
|
+
fonts.append({
|
|
105
|
+
"rgb": rgb,
|
|
106
|
+
"theme": theme,
|
|
107
|
+
"bold": bold_elem is not None,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# fills
|
|
111
|
+
fills = []
|
|
112
|
+
fills_elem = find("fills")
|
|
113
|
+
fills_declared = 0
|
|
114
|
+
if fills_elem is not None:
|
|
115
|
+
fills_declared = int(fills_elem.get("count", "0"))
|
|
116
|
+
for fill in fills_elem:
|
|
117
|
+
pf = fill.find(f"{NSP}patternFill")
|
|
118
|
+
pattern_type = pf.get("patternType", "") if pf is not None else ""
|
|
119
|
+
fills.append({"patternType": pattern_type})
|
|
120
|
+
|
|
121
|
+
# cellXfs
|
|
122
|
+
xfs = []
|
|
123
|
+
xfs_elem = find("cellXfs")
|
|
124
|
+
xfs_declared = 0
|
|
125
|
+
if xfs_elem is not None:
|
|
126
|
+
xfs_declared = int(xfs_elem.get("count", "0"))
|
|
127
|
+
for xf in xfs_elem:
|
|
128
|
+
xfs.append({
|
|
129
|
+
"numFmtId": int(xf.get("numFmtId", "0")),
|
|
130
|
+
"fontId": int(xf.get("fontId", "0")),
|
|
131
|
+
"fillId": int(xf.get("fillId", "0")),
|
|
132
|
+
"borderId": int(xf.get("borderId", "0")),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"num_fmts": num_fmts,
|
|
137
|
+
"num_fmts_declared": declared_count,
|
|
138
|
+
"num_fmts_actual": actual_count,
|
|
139
|
+
"fonts": fonts,
|
|
140
|
+
"fonts_declared": fonts_declared,
|
|
141
|
+
"fonts_actual": len(fonts),
|
|
142
|
+
"fills": fills,
|
|
143
|
+
"fills_declared": fills_declared,
|
|
144
|
+
"fills_actual": len(fills),
|
|
145
|
+
"xfs": xfs,
|
|
146
|
+
"xfs_declared": xfs_declared,
|
|
147
|
+
"xfs_actual": len(xfs),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_blue_font(font: dict) -> bool:
|
|
152
|
+
return font["rgb"] == BLUE_RGB
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_black_font(font: dict) -> bool:
|
|
156
|
+
return font["rgb"] == BLACK_RGB or (font["rgb"] == "" and font["theme"] is not None)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_green_font(font: dict) -> bool:
|
|
160
|
+
return font["rgb"] == GREEN_RGB
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _fmt_is_percent(num_fmt_id: int, num_fmts: dict) -> bool:
|
|
164
|
+
if num_fmt_id in PERCENT_FMT_IDS:
|
|
165
|
+
return True
|
|
166
|
+
fmt_code = num_fmts.get(num_fmt_id, "")
|
|
167
|
+
return "%" in fmt_code
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _fmt_is_comma(num_fmt_id: int, num_fmts: dict) -> bool:
|
|
171
|
+
if num_fmt_id in COMMA_FMT_IDS:
|
|
172
|
+
return True
|
|
173
|
+
fmt_code = num_fmts.get(num_fmt_id, "")
|
|
174
|
+
# formatCode has comma separator if it contains #,##0 but not a trailing , (scale)
|
|
175
|
+
return "#,##" in fmt_code and not fmt_code.endswith(",") and not fmt_code.endswith(",\"M\"") and not fmt_code.endswith(",\"K\"")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _looks_like_year(value_text: str) -> bool:
|
|
179
|
+
"""True if value is a 4-digit year between 1900 and 2100."""
|
|
180
|
+
try:
|
|
181
|
+
v = int(float(value_text))
|
|
182
|
+
return 1900 <= v <= 2100
|
|
183
|
+
except (ValueError, TypeError):
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _audit(styles_xml: bytes, sheet_xmls: list[tuple[str, bytes]]) -> dict:
|
|
188
|
+
"""
|
|
189
|
+
Run all formatting compliance checks.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
styles_xml: content of xl/styles.xml
|
|
193
|
+
sheet_xmls: list of (sheet_name, xml_bytes) for each worksheet
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
dict with violations and summary
|
|
197
|
+
"""
|
|
198
|
+
results = {
|
|
199
|
+
"violations": [],
|
|
200
|
+
"warnings": [],
|
|
201
|
+
"summary": {},
|
|
202
|
+
}
|
|
203
|
+
v = results["violations"]
|
|
204
|
+
w = results["warnings"]
|
|
205
|
+
|
|
206
|
+
styles = _parse_styles(styles_xml)
|
|
207
|
+
fonts = styles["fonts"]
|
|
208
|
+
xfs = styles["xfs"]
|
|
209
|
+
num_fmts = styles["num_fmts"]
|
|
210
|
+
|
|
211
|
+
# ── Check A: count attribute integrity ──────────────────────────────────
|
|
212
|
+
if styles["fonts_declared"] != styles["fonts_actual"]:
|
|
213
|
+
v.append({
|
|
214
|
+
"type": "count_mismatch",
|
|
215
|
+
"element": "fonts",
|
|
216
|
+
"declared": styles["fonts_declared"],
|
|
217
|
+
"actual": styles["fonts_actual"],
|
|
218
|
+
"fix": f"Update <fonts count=\"{styles['fonts_actual']}\">",
|
|
219
|
+
})
|
|
220
|
+
if styles["fills_declared"] != styles["fills_actual"]:
|
|
221
|
+
v.append({
|
|
222
|
+
"type": "count_mismatch",
|
|
223
|
+
"element": "fills",
|
|
224
|
+
"declared": styles["fills_declared"],
|
|
225
|
+
"actual": styles["fills_actual"],
|
|
226
|
+
"fix": f"Update <fills count=\"{styles['fills_actual']}\">",
|
|
227
|
+
})
|
|
228
|
+
if styles["xfs_declared"] != styles["xfs_actual"]:
|
|
229
|
+
v.append({
|
|
230
|
+
"type": "count_mismatch",
|
|
231
|
+
"element": "cellXfs",
|
|
232
|
+
"declared": styles["xfs_declared"],
|
|
233
|
+
"actual": styles["xfs_actual"],
|
|
234
|
+
"fix": f"Update <cellXfs count=\"{styles['xfs_actual']}\">",
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
# ── Check B: fills[0] and fills[1] presence ──────────────────────────────
|
|
238
|
+
fills = styles["fills"]
|
|
239
|
+
if len(fills) < 2:
|
|
240
|
+
v.append({
|
|
241
|
+
"type": "missing_required_fills",
|
|
242
|
+
"detail": "fills[0] (none) and fills[1] (gray125) are required by OOXML spec",
|
|
243
|
+
"fix": "Prepend <fill><patternFill patternType='none'/></fill> and <fill><patternFill patternType='gray125'/></fill>",
|
|
244
|
+
})
|
|
245
|
+
else:
|
|
246
|
+
if fills[0].get("patternType") != "none":
|
|
247
|
+
v.append({
|
|
248
|
+
"type": "fills_0_corrupted",
|
|
249
|
+
"detail": f"fills[0] patternType='{fills[0].get('patternType')}', must be 'none'",
|
|
250
|
+
"fix": "Set fills[0] patternFill patternType to 'none'",
|
|
251
|
+
})
|
|
252
|
+
if fills[1].get("patternType") != "gray125":
|
|
253
|
+
v.append({
|
|
254
|
+
"type": "fills_1_corrupted",
|
|
255
|
+
"detail": f"fills[1] patternType='{fills[1].get('patternType')}', must be 'gray125'",
|
|
256
|
+
"fix": "Set fills[1] patternFill patternType to 'gray125'",
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
# ── Check C: per-cell style violations ───────────────────────────────────
|
|
260
|
+
total_cells = 0
|
|
261
|
+
formula_cells = 0
|
|
262
|
+
input_cells = 0
|
|
263
|
+
|
|
264
|
+
for sheet_name, sheet_xml in sheet_xmls:
|
|
265
|
+
ws = ET.fromstring(sheet_xml)
|
|
266
|
+
|
|
267
|
+
for cell in ws.findall(f".//{NSP}c"):
|
|
268
|
+
cell_ref = cell.get("r", "?")
|
|
269
|
+
s_attr = cell.get("s")
|
|
270
|
+
has_formula = cell.find(f"{NSP}f") is not None
|
|
271
|
+
v_elem = cell.find(f"{NSP}v")
|
|
272
|
+
value_text = v_elem.text if v_elem is not None else None
|
|
273
|
+
total_cells += 1
|
|
274
|
+
|
|
275
|
+
# Skip cells with no style
|
|
276
|
+
if s_attr is None:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
s_idx = int(s_attr)
|
|
281
|
+
except ValueError:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
# Check C1: s index out of range
|
|
285
|
+
if s_idx >= len(xfs):
|
|
286
|
+
v.append({
|
|
287
|
+
"type": "style_index_out_of_range",
|
|
288
|
+
"sheet": sheet_name,
|
|
289
|
+
"cell": cell_ref,
|
|
290
|
+
"s": s_idx,
|
|
291
|
+
"cellXfs_count": len(xfs),
|
|
292
|
+
"fix": f"s={s_idx} exceeds cellXfs count={len(xfs)}; add missing <xf> entries or lower s value",
|
|
293
|
+
})
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
xf = xfs[s_idx]
|
|
297
|
+
font_id = xf["fontId"]
|
|
298
|
+
num_fmt_id = xf["numFmtId"]
|
|
299
|
+
|
|
300
|
+
if font_id >= len(fonts):
|
|
301
|
+
v.append({
|
|
302
|
+
"type": "font_index_out_of_range",
|
|
303
|
+
"sheet": sheet_name,
|
|
304
|
+
"cell": cell_ref,
|
|
305
|
+
"fontId": font_id,
|
|
306
|
+
"fonts_count": len(fonts),
|
|
307
|
+
"fix": f"fontId={font_id} exceeds fonts count={len(fonts)}; add missing <font> entries",
|
|
308
|
+
})
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
font = fonts[font_id]
|
|
312
|
+
|
|
313
|
+
# Check C2: color-role violation — formula cell with blue font
|
|
314
|
+
if has_formula and _is_blue_font(font):
|
|
315
|
+
formula_cells += 1
|
|
316
|
+
f_elem = cell.find(f"{NSP}f")
|
|
317
|
+
formula_text = f_elem.text if f_elem is not None else ""
|
|
318
|
+
v.append({
|
|
319
|
+
"type": "formula_cell_blue_font",
|
|
320
|
+
"sheet": sheet_name,
|
|
321
|
+
"cell": cell_ref,
|
|
322
|
+
"s": s_idx,
|
|
323
|
+
"formula": formula_text,
|
|
324
|
+
"fix": "Formula cells must use black font (formula) or green font (cross-sheet ref). "
|
|
325
|
+
"Use style index 2/6/8/10 (black) or 3/13 (green) instead.",
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
# Check C3: color-role violation — non-formula cell with explicit black
|
|
329
|
+
# (only flag if it looks like it should be an input — has a numeric value)
|
|
330
|
+
if (not has_formula and _is_black_font(font)
|
|
331
|
+
and value_text is not None
|
|
332
|
+
and not font.get("bold")
|
|
333
|
+
and num_fmt_id not in (0,) # skip general-format black (could be label)
|
|
334
|
+
):
|
|
335
|
+
try:
|
|
336
|
+
float(value_text)
|
|
337
|
+
# It's a numeric value with black font — possible missing blue input marker
|
|
338
|
+
w.append({
|
|
339
|
+
"type": "numeric_input_may_lack_blue",
|
|
340
|
+
"sheet": sheet_name,
|
|
341
|
+
"cell": cell_ref,
|
|
342
|
+
"s": s_idx,
|
|
343
|
+
"value": value_text,
|
|
344
|
+
"note": "Hardcoded numeric value has black font — if this is a user-editable "
|
|
345
|
+
"assumption, change to blue-font input style (e.g. s=1/5/7/9/11/12).",
|
|
346
|
+
})
|
|
347
|
+
except (ValueError, TypeError):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
# Check C4: year value with comma-formatted numFmt
|
|
351
|
+
if value_text and _looks_like_year(value_text) and _fmt_is_comma(num_fmt_id, num_fmts):
|
|
352
|
+
v.append({
|
|
353
|
+
"type": "year_with_comma_format",
|
|
354
|
+
"sheet": sheet_name,
|
|
355
|
+
"cell": cell_ref,
|
|
356
|
+
"s": s_idx,
|
|
357
|
+
"value": value_text,
|
|
358
|
+
"numFmtId": num_fmt_id,
|
|
359
|
+
"fix": "Year values must use numFmtId=1 (format '0') to display as 2024 not 2,024. "
|
|
360
|
+
"Use style index 11 or a custom xf with numFmtId=1.",
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
# Check C5: percentage format with value > 1 (likely 8 instead of 0.08)
|
|
364
|
+
if value_text and _fmt_is_percent(num_fmt_id, num_fmts):
|
|
365
|
+
try:
|
|
366
|
+
pct_val = float(value_text)
|
|
367
|
+
if pct_val > 1.0:
|
|
368
|
+
w.append({
|
|
369
|
+
"type": "percent_value_gt_1",
|
|
370
|
+
"sheet": sheet_name,
|
|
371
|
+
"cell": cell_ref,
|
|
372
|
+
"s": s_idx,
|
|
373
|
+
"value": value_text,
|
|
374
|
+
"displayed_as": f"{pct_val * 100:.0f}%",
|
|
375
|
+
"note": f"Value {value_text} with percentage format displays as {pct_val*100:.0f}%. "
|
|
376
|
+
"If intended rate is ~{:.0f}%, store as {:.4f} instead.".format(
|
|
377
|
+
pct_val, pct_val / 100
|
|
378
|
+
),
|
|
379
|
+
})
|
|
380
|
+
except (ValueError, TypeError):
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
if has_formula:
|
|
384
|
+
formula_cells += 1
|
|
385
|
+
elif value_text is not None:
|
|
386
|
+
input_cells += 1
|
|
387
|
+
|
|
388
|
+
results["summary"] = {
|
|
389
|
+
"total_cells_inspected": total_cells,
|
|
390
|
+
"formula_cells": formula_cells,
|
|
391
|
+
"input_cells": input_cells,
|
|
392
|
+
"violations": len(v),
|
|
393
|
+
"warnings": len(w),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return results
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _load_from_xlsx(xlsx_path: str) -> tuple[bytes, list[tuple[str, bytes]]]:
|
|
400
|
+
"""Load styles.xml and all sheet XMLs from a packed xlsx file."""
|
|
401
|
+
with zipfile.ZipFile(xlsx_path, "r") as z:
|
|
402
|
+
styles_xml = z.read("xl/styles.xml")
|
|
403
|
+
|
|
404
|
+
# Get sheet name mapping
|
|
405
|
+
wb_xml = z.read("xl/workbook.xml")
|
|
406
|
+
wb = ET.fromstring(wb_xml)
|
|
407
|
+
rel_ns = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
408
|
+
rels_xml = z.read("xl/_rels/workbook.xml.rels")
|
|
409
|
+
rels = ET.fromstring(rels_xml)
|
|
410
|
+
|
|
411
|
+
rid_to_name = {}
|
|
412
|
+
for sheet in wb.findall(f".//{{{NS}}}sheet"):
|
|
413
|
+
rid = sheet.get(f"{{{rel_ns}}}id", "")
|
|
414
|
+
name = sheet.get("name", "")
|
|
415
|
+
rid_to_name[rid] = name
|
|
416
|
+
|
|
417
|
+
rid_to_path = {}
|
|
418
|
+
for rel in rels:
|
|
419
|
+
rid = rel.get("Id", "")
|
|
420
|
+
target = rel.get("Target", "")
|
|
421
|
+
if "worksheets" in target:
|
|
422
|
+
if not target.startswith("xl/"):
|
|
423
|
+
target = "xl/" + target
|
|
424
|
+
rid_to_path[rid] = target
|
|
425
|
+
|
|
426
|
+
sheet_xmls = []
|
|
427
|
+
for rid, name in rid_to_name.items():
|
|
428
|
+
path = rid_to_path.get(rid)
|
|
429
|
+
if path and path in z.namelist():
|
|
430
|
+
sheet_xmls.append((name, z.read(path)))
|
|
431
|
+
|
|
432
|
+
return styles_xml, sheet_xmls
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _load_from_dir(unpacked_dir: str) -> tuple[bytes, list[tuple[str, bytes]]]:
|
|
436
|
+
"""Load styles.xml and all sheet XMLs from an unpacked directory."""
|
|
437
|
+
styles_path = os.path.join(unpacked_dir, "xl", "styles.xml")
|
|
438
|
+
with open(styles_path, "rb") as f:
|
|
439
|
+
styles_xml = f.read()
|
|
440
|
+
|
|
441
|
+
# Get sheet names from workbook.xml
|
|
442
|
+
wb_path = os.path.join(unpacked_dir, "xl", "workbook.xml")
|
|
443
|
+
wb = ET.fromstring(open(wb_path, "rb").read())
|
|
444
|
+
rels_path = os.path.join(unpacked_dir, "xl", "_rels", "workbook.xml.rels")
|
|
445
|
+
rels = ET.fromstring(open(rels_path, "rb").read())
|
|
446
|
+
|
|
447
|
+
rel_ns = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
448
|
+
rid_to_name = {}
|
|
449
|
+
for sheet in wb.findall(f".//{{{NS}}}sheet"):
|
|
450
|
+
rid = sheet.get(f"{{{rel_ns}}}id", "")
|
|
451
|
+
name = sheet.get("name", "")
|
|
452
|
+
rid_to_name[rid] = name
|
|
453
|
+
|
|
454
|
+
rid_to_path = {}
|
|
455
|
+
for rel in rels:
|
|
456
|
+
rid = rel.get("Id", "")
|
|
457
|
+
target = rel.get("Target", "")
|
|
458
|
+
if "worksheets" in target:
|
|
459
|
+
rid_to_path[rid] = target
|
|
460
|
+
|
|
461
|
+
sheet_xmls = []
|
|
462
|
+
ws_dir = os.path.join(unpacked_dir, "xl", "worksheets")
|
|
463
|
+
for rid, name in rid_to_name.items():
|
|
464
|
+
rel_path = rid_to_path.get(rid, "")
|
|
465
|
+
# rel_path may be "worksheets/sheet1.xml" or absolute path
|
|
466
|
+
if rel_path.startswith("worksheets/"):
|
|
467
|
+
full = os.path.join(unpacked_dir, "xl", rel_path)
|
|
468
|
+
else:
|
|
469
|
+
full = os.path.join(unpacked_dir, "xl", "worksheets", os.path.basename(rel_path))
|
|
470
|
+
if os.path.exists(full):
|
|
471
|
+
with open(full, "rb") as f:
|
|
472
|
+
sheet_xmls.append((name, f.read()))
|
|
473
|
+
|
|
474
|
+
return styles_xml, sheet_xmls
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def main() -> None:
|
|
478
|
+
use_json = "--json" in sys.argv
|
|
479
|
+
summary_only = "--summary" in sys.argv
|
|
480
|
+
|
|
481
|
+
args_clean = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
482
|
+
if not args_clean:
|
|
483
|
+
print("Usage: style_audit.py <input.xlsx | unpacked_dir/> [--json] [--summary]")
|
|
484
|
+
sys.exit(1)
|
|
485
|
+
|
|
486
|
+
target = args_clean[0]
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
if os.path.isdir(target):
|
|
490
|
+
styles_xml, sheet_xmls = _load_from_dir(target)
|
|
491
|
+
elif target.endswith(".xlsx") or target.endswith(".xlsm"):
|
|
492
|
+
styles_xml, sheet_xmls = _load_from_xlsx(target)
|
|
493
|
+
else:
|
|
494
|
+
print(f"ERROR: unrecognized target '{target}' — must be .xlsx file or unpacked directory")
|
|
495
|
+
sys.exit(1)
|
|
496
|
+
except Exception as e:
|
|
497
|
+
print(f"ERROR loading file: {e}")
|
|
498
|
+
sys.exit(1)
|
|
499
|
+
|
|
500
|
+
results = _audit(styles_xml, sheet_xmls)
|
|
501
|
+
|
|
502
|
+
if use_json:
|
|
503
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
504
|
+
sys.exit(1 if results["summary"]["violations"] > 0 else 0)
|
|
505
|
+
|
|
506
|
+
# Human-readable output
|
|
507
|
+
s = results["summary"]
|
|
508
|
+
print(f"Target : {target}")
|
|
509
|
+
print(f"Cells : {s['total_cells_inspected']} inspected "
|
|
510
|
+
f"({s['formula_cells']} formula, {s['input_cells']} input)")
|
|
511
|
+
print(f"Violations : {s['violations']}")
|
|
512
|
+
print(f"Warnings : {s['warnings']}")
|
|
513
|
+
|
|
514
|
+
if not summary_only:
|
|
515
|
+
if results["violations"]:
|
|
516
|
+
print("\n── Violations (must fix) ──")
|
|
517
|
+
for item in results["violations"]:
|
|
518
|
+
t = item["type"]
|
|
519
|
+
if t == "count_mismatch":
|
|
520
|
+
print(f" [FAIL] {item['element']} count mismatch: declared={item['declared']}, "
|
|
521
|
+
f"actual={item['actual']}")
|
|
522
|
+
print(f" Fix: {item['fix']}")
|
|
523
|
+
elif t == "missing_required_fills":
|
|
524
|
+
print(f" [FAIL] {item['detail']}")
|
|
525
|
+
print(f" Fix: {item['fix']}")
|
|
526
|
+
elif t in ("fills_0_corrupted", "fills_1_corrupted"):
|
|
527
|
+
print(f" [FAIL] {item['detail']}")
|
|
528
|
+
print(f" Fix: {item['fix']}")
|
|
529
|
+
elif t == "formula_cell_blue_font":
|
|
530
|
+
print(f" [FAIL] [{item['sheet']}!{item['cell']}] formula cell has blue font "
|
|
531
|
+
f"(role=input, but cell contains formula: {item.get('formula', '')})")
|
|
532
|
+
print(f" Fix: {item['fix']}")
|
|
533
|
+
elif t == "style_index_out_of_range":
|
|
534
|
+
print(f" [FAIL] [{item['sheet']}!{item['cell']}] s={item['s']} but "
|
|
535
|
+
f"cellXfs count={item['cellXfs_count']}")
|
|
536
|
+
print(f" Fix: {item['fix']}")
|
|
537
|
+
elif t == "font_index_out_of_range":
|
|
538
|
+
print(f" [FAIL] [{item['sheet']}!{item['cell']}] fontId={item['fontId']} but "
|
|
539
|
+
f"fonts count={item['fonts_count']}")
|
|
540
|
+
print(f" Fix: {item['fix']}")
|
|
541
|
+
elif t == "year_with_comma_format":
|
|
542
|
+
print(f" [FAIL] [{item['sheet']}!{item['cell']}] year value {item['value']} "
|
|
543
|
+
f"uses comma-format (numFmtId={item['numFmtId']}) — will display as "
|
|
544
|
+
f"{int(float(item['value'])):,}")
|
|
545
|
+
print(f" Fix: {item['fix']}")
|
|
546
|
+
else:
|
|
547
|
+
print(f" [FAIL] {item}")
|
|
548
|
+
|
|
549
|
+
if results["warnings"] and not summary_only:
|
|
550
|
+
print("\n── Warnings (review recommended) ──")
|
|
551
|
+
for item in results["warnings"]:
|
|
552
|
+
t = item["type"]
|
|
553
|
+
if t == "numeric_input_may_lack_blue":
|
|
554
|
+
print(f" [WARN] [{item['sheet']}!{item['cell']}] numeric value={item['value']} "
|
|
555
|
+
f"has black font — if user-editable assumption, use blue-font input style")
|
|
556
|
+
elif t == "percent_value_gt_1":
|
|
557
|
+
print(f" [WARN] [{item['sheet']}!{item['cell']}] percent-format cell has "
|
|
558
|
+
f"value={item['value']} (displays as {item['displayed_as']}) — "
|
|
559
|
+
f"likely should be stored as decimal (e.g. 0.08 for 8%)")
|
|
560
|
+
else:
|
|
561
|
+
print(f" [WARN] {item}")
|
|
562
|
+
|
|
563
|
+
print()
|
|
564
|
+
if s["violations"] == 0:
|
|
565
|
+
if s["warnings"] == 0:
|
|
566
|
+
print("PASS — Financial formatting is compliant")
|
|
567
|
+
else:
|
|
568
|
+
print(f"PASS with WARN — {s['warnings']} warning(s) need review")
|
|
569
|
+
else:
|
|
570
|
+
print(f"FAIL — {s['violations']} violation(s) must be fixed before delivery")
|
|
571
|
+
sys.exit(1)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
if __name__ == "__main__":
|
|
575
|
+
main()
|