dimcode-darwin-x64 0.1.2-beta.1 → 0.1.2

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.
Files changed (148) hide show
  1. package/bin/dimcode +0 -0
  2. package/package.json +1 -1
  3. package/bin/runtime/sandbox/dim-sandbox-runner +0 -0
  4. package/bin/runtime/sandbox/manifest.json +0 -15
  5. package/bin/skills-assets/deep-investigate/SKILL.md +0 -101
  6. package/bin/skills-assets/deep-investigate/references/prompts.md +0 -75
  7. package/bin/skills-assets/deep-investigate/references/templates.md +0 -73
  8. package/bin/skills-assets/deep-investigate/references/thinking-tools.md +0 -36
  9. package/bin/skills-assets/docs-sprint/SKILL.md +0 -73
  10. package/bin/skills-assets/docs-sprint/agents/openai.yaml +0 -4
  11. package/bin/skills-assets/docs-sprint/references/contract-discipline.md +0 -30
  12. package/bin/skills-assets/docs-sprint/references/delivery-plan.md +0 -162
  13. package/bin/skills-assets/docs-sprint/references/documentation-system.md +0 -109
  14. package/bin/skills-assets/docs-sprint/references/ui-layout.md +0 -73
  15. package/bin/skills-assets/docs-sprint/references/worktree-guide.md +0 -45
  16. package/bin/skills-assets/docx/SKILL.md +0 -273
  17. package/bin/skills-assets/docx/assets/styles/academic_styles.xml +0 -250
  18. package/bin/skills-assets/docx/assets/styles/corporate_styles.xml +0 -284
  19. package/bin/skills-assets/docx/assets/styles/default_styles.xml +0 -449
  20. package/bin/skills-assets/docx/assets/xsd/aesthetic-rules.xsd +0 -470
  21. package/bin/skills-assets/docx/assets/xsd/business-rules.xsd +0 -130
  22. package/bin/skills-assets/docx/assets/xsd/common-types.xsd +0 -159
  23. package/bin/skills-assets/docx/assets/xsd/wml-subset.xsd +0 -589
  24. package/bin/skills-assets/docx/references/cjk_typography.md +0 -357
  25. package/bin/skills-assets/docx/references/cjk_university_template_guide.md +0 -184
  26. package/bin/skills-assets/docx/references/comments_guide.md +0 -191
  27. package/bin/skills-assets/docx/references/design_good_bad_examples.md +0 -829
  28. package/bin/skills-assets/docx/references/design_principles.md +0 -819
  29. package/bin/skills-assets/docx/references/openxml_element_order.md +0 -308
  30. package/bin/skills-assets/docx/references/openxml_encyclopedia_part1.md +0 -4061
  31. package/bin/skills-assets/docx/references/openxml_encyclopedia_part2.md +0 -2820
  32. package/bin/skills-assets/docx/references/openxml_encyclopedia_part3.md +0 -3381
  33. package/bin/skills-assets/docx/references/openxml_namespaces.md +0 -82
  34. package/bin/skills-assets/docx/references/openxml_units.md +0 -72
  35. package/bin/skills-assets/docx/references/scenario_a_create.md +0 -284
  36. package/bin/skills-assets/docx/references/scenario_b_edit_content.md +0 -295
  37. package/bin/skills-assets/docx/references/scenario_c_apply_template.md +0 -456
  38. package/bin/skills-assets/docx/references/track_changes_guide.md +0 -200
  39. package/bin/skills-assets/docx/references/troubleshooting.md +0 -506
  40. package/bin/skills-assets/docx/references/typography_guide.md +0 -294
  41. package/bin/skills-assets/docx/references/xsd_validation_guide.md +0 -158
  42. package/bin/skills-assets/docx/scripts/doc_to_docx.sh +0 -40
  43. package/bin/skills-assets/docx/scripts/docx_preview.sh +0 -37
  44. package/bin/skills-assets/docx/scripts/dotnet/Docx.Cli/Docx.Cli.csproj +0 -19
  45. package/bin/skills-assets/docx/scripts/dotnet/Docx.Cli/Program.cs +0 -18
  46. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/AnalyzeCommand.cs +0 -147
  47. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/ApplyTemplateCommand.cs +0 -322
  48. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/CreateCommand.cs +0 -324
  49. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/DiffCommand.cs +0 -155
  50. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/EditContentCommand.cs +0 -487
  51. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/FixOrderCommand.cs +0 -108
  52. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/MergeRunsCommand.cs +0 -122
  53. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Commands/ValidateCommand.cs +0 -107
  54. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Docx.Core.csproj +0 -15
  55. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/CommentSynchronizer.cs +0 -169
  56. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/ElementOrder.cs +0 -80
  57. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/NamespaceConstants.cs +0 -42
  58. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/RunMerger.cs +0 -81
  59. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/StyleAnalyzer.cs +0 -81
  60. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/TrackChangesHelper.cs +0 -99
  61. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/OpenXml/UnitConverter.cs +0 -23
  62. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/AestheticRecipeSamples.cs +0 -1832
  63. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/AestheticRecipeSamples_Batch1.cs +0 -910
  64. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/AestheticRecipeSamples_Batch2.cs +0 -999
  65. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/AestheticRecipeSamples_Batch3.cs +0 -1048
  66. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/AestheticRecipeSamples_Batch4.cs +0 -1038
  67. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/CharacterFormattingSamples.cs +0 -1020
  68. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/DocumentCreationSamples.cs +0 -1121
  69. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/FieldAndTocSamples.cs +0 -624
  70. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/FootnoteAndCommentSamples.cs +0 -675
  71. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/HeaderFooterSamples.cs +0 -838
  72. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/ImageSamples.cs +0 -917
  73. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/ListAndNumberingSamples.cs +0 -826
  74. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/ParagraphFormattingSamples.cs +0 -1199
  75. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/StyleSystemSamples.cs +0 -1487
  76. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/TableSamples.cs +0 -1163
  77. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Samples/TrackChangesSamples.cs +0 -595
  78. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Typography/CjkHelper.cs +0 -39
  79. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Typography/FontDefaults.cs +0 -24
  80. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Typography/PageSizes.cs +0 -20
  81. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Validation/BusinessRuleValidator.cs +0 -224
  82. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Validation/GateCheckValidator.cs +0 -148
  83. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Validation/ValidationResult.cs +0 -23
  84. package/bin/skills-assets/docx/scripts/dotnet/Docx.Core/Validation/XsdValidator.cs +0 -69
  85. package/bin/skills-assets/docx/scripts/dotnet/Docx.slnx +0 -4
  86. package/bin/skills-assets/docx/scripts/env_check.sh +0 -196
  87. package/bin/skills-assets/docx/scripts/setup.ps1 +0 -274
  88. package/bin/skills-assets/docx/scripts/setup.sh +0 -504
  89. package/bin/skills-assets/pdf/README.md +0 -222
  90. package/bin/skills-assets/pdf/SKILL.md +0 -191
  91. package/bin/skills-assets/pdf/design/design.md +0 -381
  92. package/bin/skills-assets/pdf/scripts/cover.py +0 -1579
  93. package/bin/skills-assets/pdf/scripts/fill_inspect.py +0 -200
  94. package/bin/skills-assets/pdf/scripts/fill_write.py +0 -242
  95. package/bin/skills-assets/pdf/scripts/make.sh +0 -491
  96. package/bin/skills-assets/pdf/scripts/merge.py +0 -112
  97. package/bin/skills-assets/pdf/scripts/palette.py +0 -521
  98. package/bin/skills-assets/pdf/scripts/reformat_parse.py +0 -374
  99. package/bin/skills-assets/pdf/scripts/render_body.py +0 -1052
  100. package/bin/skills-assets/pdf/scripts/render_cover.js +0 -111
  101. package/bin/skills-assets/pptx-generator/SKILL.md +0 -248
  102. package/bin/skills-assets/pptx-generator/references/design-system.md +0 -392
  103. package/bin/skills-assets/pptx-generator/references/editing.md +0 -162
  104. package/bin/skills-assets/pptx-generator/references/pitfalls.md +0 -112
  105. package/bin/skills-assets/pptx-generator/references/pptxgenjs.md +0 -420
  106. package/bin/skills-assets/pptx-generator/references/slide-types.md +0 -413
  107. package/bin/skills-assets/skill-creator/SKILL.md +0 -368
  108. package/bin/skills-assets/skill-creator/agents/openai.yaml +0 -5
  109. package/bin/skills-assets/skill-creator/assets/skill-creator-small.svg +0 -3
  110. package/bin/skills-assets/skill-creator/assets/skill-creator.png +0 -0
  111. package/bin/skills-assets/skill-creator/license.txt +0 -202
  112. package/bin/skills-assets/skill-creator/references/openai_yaml.md +0 -49
  113. package/bin/skills-assets/skill-creator/scripts/generate_openai_yaml.py +0 -226
  114. package/bin/skills-assets/skill-creator/scripts/init_skill.py +0 -397
  115. package/bin/skills-assets/skill-creator/scripts/quick_validate.py +0 -101
  116. package/bin/skills-assets/skill-installer/LICENSE.txt +0 -202
  117. package/bin/skills-assets/skill-installer/SKILL.md +0 -58
  118. package/bin/skills-assets/skill-installer/agents/openai.yaml +0 -5
  119. package/bin/skills-assets/skill-installer/assets/skill-installer-small.svg +0 -3
  120. package/bin/skills-assets/skill-installer/assets/skill-installer.png +0 -0
  121. package/bin/skills-assets/skill-installer/scripts/github_utils.py +0 -21
  122. package/bin/skills-assets/skill-installer/scripts/install-skill-from-github.py +0 -308
  123. package/bin/skills-assets/skill-installer/scripts/list-skills.py +0 -107
  124. package/bin/skills-assets/xlsx/SKILL.md +0 -137
  125. package/bin/skills-assets/xlsx/references/create.md +0 -691
  126. package/bin/skills-assets/xlsx/references/edit.md +0 -684
  127. package/bin/skills-assets/xlsx/references/fix.md +0 -37
  128. package/bin/skills-assets/xlsx/references/format.md +0 -768
  129. package/bin/skills-assets/xlsx/references/ooxml-cheatsheet.md +0 -231
  130. package/bin/skills-assets/xlsx/references/read-analyze.md +0 -97
  131. package/bin/skills-assets/xlsx/references/validate.md +0 -772
  132. package/bin/skills-assets/xlsx/scripts/formula_check.py +0 -422
  133. package/bin/skills-assets/xlsx/scripts/libreoffice_recalc.py +0 -248
  134. package/bin/skills-assets/xlsx/scripts/shared_strings_builder.py +0 -163
  135. package/bin/skills-assets/xlsx/scripts/style_audit.py +0 -575
  136. package/bin/skills-assets/xlsx/scripts/xlsx_add_column.py +0 -395
  137. package/bin/skills-assets/xlsx/scripts/xlsx_insert_row.py +0 -274
  138. package/bin/skills-assets/xlsx/scripts/xlsx_pack.py +0 -87
  139. package/bin/skills-assets/xlsx/scripts/xlsx_reader.py +0 -362
  140. package/bin/skills-assets/xlsx/scripts/xlsx_shift_rows.py +0 -396
  141. package/bin/skills-assets/xlsx/scripts/xlsx_unpack.py +0 -130
  142. package/bin/skills-assets/xlsx/templates/minimal_xlsx/[Content_Types].xml +0 -9
  143. package/bin/skills-assets/xlsx/templates/minimal_xlsx/_rels/.rels +0 -6
  144. package/bin/skills-assets/xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +0 -19
  145. package/bin/skills-assets/xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +0 -33
  146. package/bin/skills-assets/xlsx/templates/minimal_xlsx/xl/styles.xml +0 -160
  147. package/bin/skills-assets/xlsx/templates/minimal_xlsx/xl/workbook.xml +0 -30
  148. package/bin/skills-assets/xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +0 -70
@@ -1,575 +0,0 @@
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()