pi-maestro-flow 0.2.0

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 (179) hide show
  1. package/README.md +124 -0
  2. package/agents/aggregator.md +27 -0
  3. package/agents/cli-explore-agent.md +188 -0
  4. package/agents/cross-role-reviewer.md +171 -0
  5. package/agents/delegate.md +19 -0
  6. package/agents/explorer.md +23 -0
  7. package/agents/impeccable-agent.md +99 -0
  8. package/agents/ralph-executor.md +81 -0
  9. package/agents/reference.md +24 -0
  10. package/agents/role-design-author.md +220 -0
  11. package/agents/team-supervisor.md +143 -0
  12. package/agents/team-worker.md +237 -0
  13. package/agents/ui-design-agent.md +270 -0
  14. package/agents/workflow-analyzer.md +116 -0
  15. package/agents/workflow-codebase-mapper.md +77 -0
  16. package/agents/workflow-collab-planner.md +147 -0
  17. package/agents/workflow-debugger.md +104 -0
  18. package/agents/workflow-executor.md +133 -0
  19. package/agents/workflow-external-researcher.md +86 -0
  20. package/agents/workflow-integration-checker.md +83 -0
  21. package/agents/workflow-nyquist-auditor.md +85 -0
  22. package/agents/workflow-phase-researcher.md +85 -0
  23. package/agents/workflow-plan-checker.md +100 -0
  24. package/agents/workflow-planner.md +200 -0
  25. package/agents/workflow-project-researcher.md +74 -0
  26. package/agents/workflow-research-synthesizer.md +70 -0
  27. package/agents/workflow-reviewer.md +82 -0
  28. package/agents/workflow-roadmapper.md +81 -0
  29. package/agents/workflow-verifier.md +120 -0
  30. package/package.json +58 -0
  31. package/skills/codify-to-knowhow/SKILL.md +167 -0
  32. package/skills/delegation-check/SKILL.md +297 -0
  33. package/skills/domain-add/SKILL.md +75 -0
  34. package/skills/insight-challenge/SKILL.md +226 -0
  35. package/skills/learn-decompose/SKILL.md +124 -0
  36. package/skills/learn-follow/SKILL.md +163 -0
  37. package/skills/learn-investigate/SKILL.md +160 -0
  38. package/skills/learn-second-opinion/SKILL.md +128 -0
  39. package/skills/maestro/SKILL.md +239 -0
  40. package/skills/maestro-amend/SKILL.md +169 -0
  41. package/skills/maestro-analyze/SKILL.md +173 -0
  42. package/skills/maestro-blueprint/SKILL.md +159 -0
  43. package/skills/maestro-brainstorm/SKILL.md +193 -0
  44. package/skills/maestro-collab/SKILL.md +181 -0
  45. package/skills/maestro-companion/SKILL.md +536 -0
  46. package/skills/maestro-composer/SKILL.md +192 -0
  47. package/skills/maestro-execute/SKILL.md +193 -0
  48. package/skills/maestro-fork/SKILL.md +104 -0
  49. package/skills/maestro-grill/SKILL.md +151 -0
  50. package/skills/maestro-guard/SKILL.md +124 -0
  51. package/skills/maestro-help/SKILL.md +328 -0
  52. package/skills/maestro-impeccable/SKILL.md +302 -0
  53. package/skills/maestro-init/SKILL.md +151 -0
  54. package/skills/maestro-merge/SKILL.md +90 -0
  55. package/skills/maestro-milestone-audit/SKILL.md +117 -0
  56. package/skills/maestro-milestone-complete/SKILL.md +133 -0
  57. package/skills/maestro-milestone-release/SKILL.md +131 -0
  58. package/skills/maestro-next/SKILL.md +269 -0
  59. package/skills/maestro-overlay/SKILL.md +203 -0
  60. package/skills/maestro-plan/SKILL.md +194 -0
  61. package/skills/maestro-player/SKILL.md +184 -0
  62. package/skills/maestro-quick/SKILL.md +117 -0
  63. package/skills/maestro-ralph/SKILL.md +905 -0
  64. package/skills/maestro-ralph-cli/SKILL.md +976 -0
  65. package/skills/maestro-ralph-cli-execute/SKILL.md +229 -0
  66. package/skills/maestro-ralph-execute/SKILL.md +409 -0
  67. package/skills/maestro-ralph-v2/SKILL.md +1198 -0
  68. package/skills/maestro-roadmap/SKILL.md +157 -0
  69. package/skills/maestro-swarm-workflow/SKILL.md +272 -0
  70. package/skills/maestro-tools-execute/SKILL.md +143 -0
  71. package/skills/maestro-tools-register/SKILL.md +192 -0
  72. package/skills/maestro-ui-codify/SKILL.md +127 -0
  73. package/skills/maestro-universal-workflow/SKILL.md +622 -0
  74. package/skills/maestro-update/SKILL.md +156 -0
  75. package/skills/manage-codebase-rebuild/SKILL.md +111 -0
  76. package/skills/manage-drift-realign/SKILL.md +159 -0
  77. package/skills/manage-harvest/SKILL.md +121 -0
  78. package/skills/manage-issue/SKILL.md +88 -0
  79. package/skills/manage-issue-discover/SKILL.md +104 -0
  80. package/skills/manage-kg-extractors/SKILL.md +147 -0
  81. package/skills/manage-knowhow/SKILL.md +102 -0
  82. package/skills/manage-knowhow-capture/SKILL.md +109 -0
  83. package/skills/manage-knowledge-audit/SKILL.md +147 -0
  84. package/skills/manage-status/SKILL.md +80 -0
  85. package/skills/manage-wiki/SKILL.md +104 -0
  86. package/skills/odyssey-debug/SKILL.md +344 -0
  87. package/skills/odyssey-improve/SKILL.md +423 -0
  88. package/skills/odyssey-planex/SKILL.md +542 -0
  89. package/skills/odyssey-review-test-fix/SKILL.md +392 -0
  90. package/skills/odyssey-ui/SKILL.md +388 -0
  91. package/skills/prompt-generator/SKILL.md +470 -0
  92. package/skills/quality-auto-test/SKILL.md +168 -0
  93. package/skills/quality-debug/SKILL.md +176 -0
  94. package/skills/quality-refactor/SKILL.md +108 -0
  95. package/skills/quality-retrospective/SKILL.md +134 -0
  96. package/skills/quality-review/SKILL.md +169 -0
  97. package/skills/quality-sync/SKILL.md +90 -0
  98. package/skills/quality-test/SKILL.md +183 -0
  99. package/skills/scholar-anti-ai-writing/SKILL.md +173 -0
  100. package/skills/scholar-anti-ai-writing/references/patterns-chinese.md +242 -0
  101. package/skills/scholar-anti-ai-writing/references/patterns-english.md +242 -0
  102. package/skills/scholar-citation-verify/SKILL.md +166 -0
  103. package/skills/scholar-citation-verify/references/api-usage.md +372 -0
  104. package/skills/scholar-citation-verify/references/common-errors.md +384 -0
  105. package/skills/scholar-citation-verify/references/verification-rules.md +399 -0
  106. package/skills/scholar-experiment/SKILL.md +321 -0
  107. package/skills/scholar-ideation/SKILL.md +288 -0
  108. package/skills/scholar-latex-organizer/SKILL.md +186 -0
  109. package/skills/scholar-publish/SKILL.md +220 -0
  110. package/skills/scholar-rebuttal-pro/README.md +313 -0
  111. package/skills/scholar-rebuttal-pro/SKILL.md +511 -0
  112. package/skills/scholar-review/SKILL.md +227 -0
  113. package/skills/scholar-thesis-docx/README.md +111 -0
  114. package/skills/scholar-thesis-docx/README_EN.md +108 -0
  115. package/skills/scholar-thesis-docx/SKILL.md +212 -0
  116. package/skills/scholar-thesis-docx/references/failure-patterns-and-quality-gates.md +178 -0
  117. package/skills/scholar-thesis-docx/references/figure-and-code-rules.md +33 -0
  118. package/skills/scholar-thesis-docx/references/paper-format-workflow.md +68 -0
  119. package/skills/scholar-thesis-docx/references/script-usage.md +157 -0
  120. package/skills/scholar-thesis-docx/scripts/audit_docx_ooxml.py +429 -0
  121. package/skills/scholar-thesis-docx/scripts/check_word_com.ps1 +50 -0
  122. package/skills/scholar-thesis-docx/scripts/export_word_pdf.ps1 +62 -0
  123. package/skills/scholar-thesis-docx/scripts/normalize_word_styles.ps1 +498 -0
  124. package/skills/scholar-thesis-docx/scripts/render_mermaid_figure.ps1 +148 -0
  125. package/skills/scholar-writing/SKILL.md +296 -0
  126. package/skills/security-audit/SKILL.md +244 -0
  127. package/skills/skill-generator/SKILL.md +472 -0
  128. package/skills/skill-iter-tune/SKILL.md +382 -0
  129. package/skills/skill-simplify/SKILL.md +63 -0
  130. package/skills/skill-tuning/SKILL.md +174 -0
  131. package/skills/spec-add/SKILL.md +105 -0
  132. package/skills/spec-load/SKILL.md +98 -0
  133. package/skills/spec-remove/SKILL.md +74 -0
  134. package/skills/spec-setup/SKILL.md +75 -0
  135. package/skills/team-adversarial-swarm/SKILL.md +233 -0
  136. package/skills/team-adversarial-swarm/scripts/__pycache__/pheromone.cpython-313.pyc +0 -0
  137. package/skills/team-adversarial-swarm/scripts/__pycache__/scoring.cpython-313.pyc +0 -0
  138. package/skills/team-adversarial-swarm/scripts/aco.py +473 -0
  139. package/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
  140. package/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
  141. package/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
  142. package/skills/team-arch-opt/SKILL.md +158 -0
  143. package/skills/team-brainstorm/SKILL.md +171 -0
  144. package/skills/team-coordinate/SKILL.md +266 -0
  145. package/skills/team-designer/SKILL.md +160 -0
  146. package/skills/team-executor/SKILL.md +189 -0
  147. package/skills/team-frontend/SKILL.md +136 -0
  148. package/skills/team-frontend-debug/SKILL.md +198 -0
  149. package/skills/team-interactive-craft/SKILL.md +141 -0
  150. package/skills/team-issue/SKILL.md +171 -0
  151. package/skills/team-lifecycle-v4/SKILL.md +209 -0
  152. package/skills/team-motion-design/SKILL.md +142 -0
  153. package/skills/team-perf-opt/SKILL.md +175 -0
  154. package/skills/team-planex/SKILL.md +137 -0
  155. package/skills/team-quality-assurance/SKILL.md +147 -0
  156. package/skills/team-review/SKILL.md +147 -0
  157. package/skills/team-roadmap-dev/SKILL.md +169 -0
  158. package/skills/team-swarm/SKILL.md +178 -0
  159. package/skills/team-swarm/scripts/aco.py +473 -0
  160. package/skills/team-swarm/scripts/pheromone.py +144 -0
  161. package/skills/team-swarm/scripts/scoring.py +92 -0
  162. package/skills/team-swarm/scripts/test_aco.py +475 -0
  163. package/skills/team-tech-debt/SKILL.md +128 -0
  164. package/skills/team-testing/SKILL.md +143 -0
  165. package/skills/team-ui-polish/SKILL.md +141 -0
  166. package/skills/team-uidesign/SKILL.md +144 -0
  167. package/skills/team-ultra-analyze/SKILL.md +173 -0
  168. package/skills/team-ux-improve/SKILL.md +151 -0
  169. package/skills/team-visual-a11y/SKILL.md +156 -0
  170. package/skills/workflow-skill-designer/SKILL.md +496 -0
  171. package/src/extension/index.ts +222 -0
  172. package/src/extension/schemas.ts +131 -0
  173. package/src/providers/cli-tools-loader.ts +74 -0
  174. package/src/providers/provider-registry.ts +130 -0
  175. package/src/tools/delegate.ts +85 -0
  176. package/src/tools/explore.ts +134 -0
  177. package/src/tools/moa.ts +213 -0
  178. package/src/tools/status.ts +99 -0
  179. package/src/tools/wait.ts +166 -0
@@ -0,0 +1,429 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import re
6
+ from collections import Counter
7
+ from pathlib import Path
8
+ from zipfile import ZipFile
9
+
10
+ from docx import Document
11
+ from lxml import etree
12
+
13
+
14
+ NS = {
15
+ "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
16
+ "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
17
+ }
18
+
19
+
20
+ HEADING_STYLE_IDS = {"2", "3", "4"}
21
+
22
+
23
+ def cm_from_twips(raw: str | None) -> float | None:
24
+ if raw is None:
25
+ return None
26
+ return round(int(raw) / 567.0, 3)
27
+
28
+
29
+ def pt_from_half_points(raw: str | None) -> float | None:
30
+ if raw is None:
31
+ return None
32
+ return round(int(raw) / 2.0, 3)
33
+
34
+
35
+ def get_rel_targets(zf: ZipFile) -> dict[str, str]:
36
+ rels = etree.fromstring(zf.read("word/_rels/document.xml.rels"))
37
+ return {rel.get("Id"): rel.get("Target", "") for rel in rels}
38
+
39
+
40
+ def load_styles(zf: ZipFile) -> dict[str, dict]:
41
+ styles_root = etree.fromstring(zf.read("word/styles.xml"))
42
+ styles: dict[str, dict] = {}
43
+ for node in styles_root.xpath("//w:style[@w:type='paragraph']", namespaces=NS):
44
+ style_id = node.get(f"{{{NS['w']}}}styleId", "")
45
+ name = node.xpath("string(w:name/@w:val)", namespaces=NS)
46
+ based_on = node.xpath("string(w:basedOn/@w:val)", namespaces=NS) or None
47
+ ppr = node.find("w:pPr", namespaces=NS)
48
+ rpr = node.find("w:rPr", namespaces=NS)
49
+ ind = ppr.find("w:ind", namespaces=NS) if ppr is not None else None
50
+ spacing = ppr.find("w:spacing", namespaces=NS) if ppr is not None else None
51
+ styles[style_id] = {
52
+ "style_id": style_id,
53
+ "name": name,
54
+ "based_on": based_on,
55
+ "first_line": ind.get(f"{{{NS['w']}}}firstLine") if ind is not None else None,
56
+ "first_line_chars": ind.get(f"{{{NS['w']}}}firstLineChars") if ind is not None else None,
57
+ "left": ind.get(f"{{{NS['w']}}}left") if ind is not None else None,
58
+ "right": ind.get(f"{{{NS['w']}}}right") if ind is not None else None,
59
+ "space_before": spacing.get(f"{{{NS['w']}}}before") if spacing is not None else None,
60
+ "space_after": spacing.get(f"{{{NS['w']}}}after") if spacing is not None else None,
61
+ "line": spacing.get(f"{{{NS['w']}}}line") if spacing is not None else None,
62
+ "line_rule": spacing.get(f"{{{NS['w']}}}lineRule") if spacing is not None else None,
63
+ "font_ascii": (
64
+ rpr.find("w:rFonts", namespaces=NS).get(f"{{{NS['w']}}}ascii")
65
+ if rpr is not None and rpr.find("w:rFonts", namespaces=NS) is not None
66
+ else None
67
+ ),
68
+ "font_east_asia": (
69
+ rpr.find("w:rFonts", namespaces=NS).get(f"{{{NS['w']}}}eastAsia")
70
+ if rpr is not None and rpr.find("w:rFonts", namespaces=NS) is not None
71
+ else None
72
+ ),
73
+ "size_pt": (
74
+ pt_from_half_points(rpr.find("w:sz", namespaces=NS).get(f"{{{NS['w']}}}val"))
75
+ if rpr is not None and rpr.find("w:sz", namespaces=NS) is not None
76
+ else None
77
+ ),
78
+ }
79
+ return styles
80
+
81
+
82
+ def load_numbering(zf: ZipFile) -> dict[str, dict]:
83
+ if "word/numbering.xml" not in zf.namelist():
84
+ return {}
85
+ numbering_root = etree.fromstring(zf.read("word/numbering.xml"))
86
+ abstract_by_id: dict[str, etree._Element] = {}
87
+ for abstract in numbering_root.xpath("//w:abstractNum", namespaces=NS):
88
+ abstract_by_id[abstract.get(f"{{{NS['w']}}}abstractNumId", "")] = abstract
89
+
90
+ numbering: dict[str, dict] = {}
91
+ for num in numbering_root.xpath("//w:num", namespaces=NS):
92
+ num_id = num.get(f"{{{NS['w']}}}numId", "")
93
+ abstract_id = num.xpath("string(w:abstractNumId/@w:val)", namespaces=NS)
94
+ abstract = abstract_by_id.get(abstract_id)
95
+ levels = {}
96
+ if abstract is not None:
97
+ for lvl in abstract.xpath("./w:lvl", namespaces=NS):
98
+ ilvl = lvl.get(f"{{{NS['w']}}}ilvl", "")
99
+ ppr = lvl.find("w:pPr", namespaces=NS)
100
+ ind = ppr.find("w:ind", namespaces=NS) if ppr is not None else None
101
+ levels[ilvl] = {
102
+ "pstyle": lvl.xpath("string(w:pStyle/@w:val)", namespaces=NS) or None,
103
+ "left": ind.get(f"{{{NS['w']}}}left") if ind is not None else None,
104
+ "first_line": ind.get(f"{{{NS['w']}}}firstLine") if ind is not None else None,
105
+ "first_line_chars": ind.get(f"{{{NS['w']}}}firstLineChars") if ind is not None else None,
106
+ "hanging": ind.get(f"{{{NS['w']}}}hanging") if ind is not None else None,
107
+ }
108
+ numbering[num_id] = {
109
+ "num_id": num_id,
110
+ "abstract_num_id": abstract_id,
111
+ "levels": levels,
112
+ }
113
+ return numbering
114
+
115
+
116
+ def load_sections(zf: ZipFile, rel_targets: dict[str, str]) -> list[dict]:
117
+ document_root = etree.fromstring(zf.read("word/document.xml"))
118
+ sections = []
119
+ for idx, sect in enumerate(document_root.xpath("//w:sectPr", namespaces=NS)):
120
+ pg_sz = sect.find("w:pgSz", namespaces=NS)
121
+ pg_mar = sect.find("w:pgMar", namespaces=NS)
122
+ pg_num = sect.find("w:pgNumType", namespaces=NS)
123
+ sections.append(
124
+ {
125
+ "index": idx,
126
+ "title_pg": sect.find("w:titlePg", namespaces=NS) is not None,
127
+ "page_width_cm": cm_from_twips(pg_sz.get(f"{{{NS['w']}}}w")) if pg_sz is not None else None,
128
+ "page_height_cm": cm_from_twips(pg_sz.get(f"{{{NS['w']}}}h")) if pg_sz is not None else None,
129
+ "top_margin_cm": cm_from_twips(pg_mar.get(f"{{{NS['w']}}}top")) if pg_mar is not None else None,
130
+ "bottom_margin_cm": cm_from_twips(pg_mar.get(f"{{{NS['w']}}}bottom")) if pg_mar is not None else None,
131
+ "left_margin_cm": cm_from_twips(pg_mar.get(f"{{{NS['w']}}}left")) if pg_mar is not None else None,
132
+ "right_margin_cm": cm_from_twips(pg_mar.get(f"{{{NS['w']}}}right")) if pg_mar is not None else None,
133
+ "pg_num_type": dict(pg_num.attrib) if pg_num is not None else None,
134
+ "header_refs": [
135
+ {
136
+ "type": ref.get(f"{{{NS['w']}}}type"),
137
+ "target": rel_targets.get(ref.get(f"{{{NS['r']}}}id"), ""),
138
+ }
139
+ for ref in sect.findall("w:headerReference", namespaces=NS)
140
+ ],
141
+ "footer_refs": [
142
+ {
143
+ "type": ref.get(f"{{{NS['w']}}}type"),
144
+ "target": rel_targets.get(ref.get(f"{{{NS['r']}}}id"), ""),
145
+ }
146
+ for ref in sect.findall("w:footerReference", namespaces=NS)
147
+ ],
148
+ }
149
+ )
150
+ return sections
151
+
152
+
153
+ def count_ref_result_runs(p_element) -> list[dict]:
154
+ runs = list(p_element.xpath("./w:r", namespaces=NS))
155
+ in_ref = False
156
+ current_instr = None
157
+ result_count = 0
158
+ results = []
159
+ for run in runs:
160
+ instr = "".join(run.xpath(".//w:instrText/text()", namespaces=NS))
161
+ if instr and " REF " in instr:
162
+ current_instr = instr.strip()
163
+ fld = run.find("w:fldChar", namespaces=NS)
164
+ if fld is not None:
165
+ fld_type = fld.get(f"{{{NS['w']}}}fldCharType")
166
+ if fld_type == "separate" and current_instr:
167
+ in_ref = True
168
+ result_count = 0
169
+ elif fld_type == "end" and current_instr:
170
+ results.append(
171
+ {
172
+ "instr": current_instr,
173
+ "result_run_count": result_count,
174
+ }
175
+ )
176
+ in_ref = False
177
+ current_instr = None
178
+ continue
179
+ if in_ref:
180
+ text_nodes = run.xpath(".//w:t/text()", namespaces=NS)
181
+ if any(t for t in text_nodes):
182
+ result_count += 1
183
+ return results
184
+
185
+
186
+ def load_paragraphs(docx_path: Path, zf: ZipFile) -> list[dict]:
187
+ doc = Document(docx_path)
188
+ document_root = etree.fromstring(zf.read("word/document.xml"))
189
+ xml_paragraphs = document_root.xpath("//w:body/w:p", namespaces=NS)
190
+
191
+ results = []
192
+ for idx, para in enumerate(doc.paragraphs):
193
+ if idx >= len(xml_paragraphs):
194
+ break
195
+ pxml = xml_paragraphs[idx]
196
+ ppr = pxml.find("w:pPr", namespaces=NS)
197
+ ind = ppr.find("w:ind", namespaces=NS) if ppr is not None else None
198
+ style_id = pxml.xpath("string(w:pPr/w:pStyle/@w:val)", namespaces=NS) or None
199
+ ref_runs = count_ref_result_runs(pxml)
200
+ results.append(
201
+ {
202
+ "index": idx,
203
+ "text": para.text.strip(),
204
+ "style_name": para.style.name,
205
+ "style_id": style_id,
206
+ "direct_ind": {
207
+ "left": ind.get(f"{{{NS['w']}}}left") if ind is not None else None,
208
+ "right": ind.get(f"{{{NS['w']}}}right") if ind is not None else None,
209
+ "firstLine": ind.get(f"{{{NS['w']}}}firstLine") if ind is not None else None,
210
+ "firstLineChars": ind.get(f"{{{NS['w']}}}firstLineChars") if ind is not None else None,
211
+ "hanging": ind.get(f"{{{NS['w']}}}hanging") if ind is not None else None,
212
+ },
213
+ "has_ref_field": bool(ref_runs),
214
+ "ref_field_results": ref_runs,
215
+ "has_drawing": bool(para._p.xpath(".//w:drawing")),
216
+ }
217
+ )
218
+ return results
219
+
220
+
221
+ def build_flags(paragraphs: list[dict], sections: list[dict]) -> dict[str, list]:
222
+ heading_indent_overrides = []
223
+ paragraphs_with_firstlinechars = []
224
+ suspicious_ref_fields = []
225
+ title_pg_sections = []
226
+
227
+ for para in paragraphs:
228
+ ind = para["direct_ind"]
229
+ if ind["firstLineChars"] not in (None, "0"):
230
+ paragraphs_with_firstlinechars.append(
231
+ {
232
+ "index": para["index"],
233
+ "style_name": para["style_name"],
234
+ "text": para["text"][:120],
235
+ "firstLineChars": ind["firstLineChars"],
236
+ }
237
+ )
238
+
239
+ if para["style_id"] in HEADING_STYLE_IDS:
240
+ if any(ind[k] not in (None, "0") for k in ("firstLine", "firstLineChars", "hanging")):
241
+ heading_indent_overrides.append(
242
+ {
243
+ "index": para["index"],
244
+ "style_name": para["style_name"],
245
+ "style_id": para["style_id"],
246
+ "text": para["text"][:120],
247
+ "direct_ind": ind,
248
+ }
249
+ )
250
+
251
+ for ref in para["ref_field_results"]:
252
+ if ref["result_run_count"] > 1:
253
+ suspicious_ref_fields.append(
254
+ {
255
+ "index": para["index"],
256
+ "text": para["text"][:120],
257
+ "instr": ref["instr"],
258
+ "result_run_count": ref["result_run_count"],
259
+ }
260
+ )
261
+
262
+ for sec in sections:
263
+ if sec["title_pg"]:
264
+ title_pg_sections.append(sec)
265
+
266
+ return {
267
+ "heading_indent_overrides": heading_indent_overrides,
268
+ "paragraphs_with_firstLineChars": paragraphs_with_firstlinechars,
269
+ "suspicious_ref_fields": suspicious_ref_fields,
270
+ "title_pg_sections": title_pg_sections,
271
+ }
272
+
273
+
274
+ def build_report(docx_path: Path) -> dict:
275
+ with ZipFile(docx_path) as zf:
276
+ rel_targets = get_rel_targets(zf)
277
+ styles = load_styles(zf)
278
+ numbering = load_numbering(zf)
279
+ sections = load_sections(zf, rel_targets)
280
+ paragraphs = load_paragraphs(docx_path, zf)
281
+
282
+ style_counts = Counter(p["style_name"] for p in paragraphs if p["text"])
283
+ flags = build_flags(paragraphs, sections)
284
+
285
+ return {
286
+ "input": str(docx_path),
287
+ "summary": {
288
+ "paragraph_count": len(paragraphs),
289
+ "nonempty_style_counts": dict(style_counts),
290
+ "section_count": len(sections),
291
+ "style_count": len(styles),
292
+ "numbering_count": len(numbering),
293
+ },
294
+ "styles": {
295
+ sid: styles[sid]
296
+ for sid in sorted(styles)
297
+ if sid in {"1", "2", "3", "4"} or styles[sid]["name"] in {
298
+ "目录标题",
299
+ "中文摘要标题",
300
+ "英文摘要标题",
301
+ "参考文献",
302
+ "参考文献标题",
303
+ "致谢标题",
304
+ }
305
+ },
306
+ "numbering": numbering,
307
+ "sections": sections,
308
+ "flags": flags,
309
+ "paragraph_samples": [
310
+ p
311
+ for p in paragraphs
312
+ if p["style_id"] in {"2", "3", "4"} or p["text"] in {"目录", "摘 要", "Abstract", "参考文献", "致谢"}
313
+ ],
314
+ }
315
+
316
+
317
+ def build_text_report(report: dict) -> str:
318
+ lines: list[str] = []
319
+ lines.append("DOCX / OOXML Thesis Audit Report")
320
+ lines.append(f"input: {report['input']}")
321
+ lines.append("")
322
+
323
+ summary = report["summary"]
324
+ lines.append("Summary")
325
+ lines.append(f"- paragraphs: {summary['paragraph_count']}")
326
+ lines.append(f"- sections: {summary['section_count']}")
327
+ lines.append(f"- paragraph styles: {summary['style_count']}")
328
+ lines.append(f"- numbering definitions: {summary['numbering_count']}")
329
+ lines.append("")
330
+
331
+ lines.append("Style Counts")
332
+ for name, count in sorted(summary["nonempty_style_counts"].items()):
333
+ lines.append(f"- {name}: {count}")
334
+ lines.append("")
335
+
336
+ flags = report["flags"]
337
+
338
+ lines.append("Risk Flags")
339
+ lines.append(f"- heading_indent_overrides: {len(flags['heading_indent_overrides'])}")
340
+ lines.append(f"- paragraphs_with_firstLineChars: {len(flags['paragraphs_with_firstLineChars'])}")
341
+ lines.append(f"- suspicious_ref_fields: {len(flags['suspicious_ref_fields'])}")
342
+ lines.append(f"- title_pg_sections: {len(flags['title_pg_sections'])}")
343
+ lines.append("")
344
+
345
+ if flags["heading_indent_overrides"]:
346
+ lines.append("Heading Indent Overrides")
347
+ for row in flags["heading_indent_overrides"][:30]:
348
+ lines.append(
349
+ f"- para {row['index']} | {row['style_name']} | {row['text']} | direct_ind={row['direct_ind']}"
350
+ )
351
+ lines.append("")
352
+
353
+ if flags["paragraphs_with_firstLineChars"]:
354
+ lines.append("Paragraphs With firstLineChars")
355
+ for row in flags["paragraphs_with_firstLineChars"][:30]:
356
+ lines.append(
357
+ f"- para {row['index']} | {row['style_name']} | firstLineChars={row['firstLineChars']} | {row['text']}"
358
+ )
359
+ lines.append("")
360
+
361
+ if flags["suspicious_ref_fields"]:
362
+ lines.append("Suspicious REF Fields")
363
+ for row in flags["suspicious_ref_fields"][:30]:
364
+ lines.append(
365
+ f"- para {row['index']} | result_run_count={row['result_run_count']} | instr={row['instr']} | {row['text']}"
366
+ )
367
+ lines.append("")
368
+
369
+ if flags["title_pg_sections"]:
370
+ lines.append("Sections With titlePg")
371
+ for sec in flags["title_pg_sections"]:
372
+ lines.append(
373
+ f"- section {sec['index']} | page_num={sec['pg_num_type']} | header_refs={sec['header_refs']} | footer_refs={sec['footer_refs']}"
374
+ )
375
+ lines.append("")
376
+
377
+ lines.append("Tracked Styles")
378
+ for sid, style in sorted(report["styles"].items()):
379
+ lines.append(
380
+ f"- styleId {sid} | {style['name']} | basedOn={style['based_on']} | firstLine={style['first_line']} | firstLineChars={style['first_line_chars']} | left={style['left']} | right={style['right']}"
381
+ )
382
+ lines.append("")
383
+
384
+ lines.append("Section Summary")
385
+ for sec in report["sections"]:
386
+ lines.append(
387
+ f"- section {sec['index']} | titlePg={sec['title_pg']} | margins(cm)={sec['top_margin_cm']},{sec['right_margin_cm']},{sec['bottom_margin_cm']},{sec['left_margin_cm']} | pageNum={sec['pg_num_type']}"
388
+ )
389
+ lines.append("")
390
+
391
+ lines.append("Paragraph Samples")
392
+ for row in report["paragraph_samples"][:60]:
393
+ lines.append(
394
+ f"- para {row['index']} | style={row['style_name']}({row['style_id']}) | direct_ind={row['direct_ind']} | text={row['text']}"
395
+ )
396
+
397
+ return "\n".join(lines) + "\n"
398
+
399
+
400
+ def main() -> None:
401
+ parser = argparse.ArgumentParser(description="Audit a thesis DOCX for hidden OOXML formatting pitfalls.")
402
+ parser.add_argument("input_path", type=Path, help="Path to the DOCX file.")
403
+ parser.add_argument(
404
+ "--output_json",
405
+ type=Path,
406
+ default=None,
407
+ help="Optional path for the JSON report. Defaults next to the input as <name>.audit.json",
408
+ )
409
+ parser.add_argument(
410
+ "--output_txt",
411
+ type=Path,
412
+ default=None,
413
+ help="Optional path for a human-readable text report. Defaults next to the input as <name>.audit.txt",
414
+ )
415
+ args = parser.parse_args()
416
+
417
+ input_path = args.input_path.resolve()
418
+ output_json = args.output_json or input_path.with_suffix(".audit.json")
419
+ output_txt = args.output_txt or input_path.with_suffix(".audit.txt")
420
+
421
+ report = build_report(input_path)
422
+ output_json.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
423
+ output_txt.write_text(build_text_report(report), encoding="utf-8")
424
+ print(output_json)
425
+ print(output_txt)
426
+
427
+
428
+ if __name__ == "__main__":
429
+ main()
@@ -0,0 +1,50 @@
1
+ param(
2
+ [switch]$Json
3
+ )
4
+
5
+ $result = [ordered]@{
6
+ platform = $env:OS
7
+ wordInstalled = $false
8
+ comAvailable = $false
9
+ domAvailable = $false
10
+ version = $null
11
+ message = $null
12
+ }
13
+
14
+ if ($env:OS -ne "Windows_NT") {
15
+ $result.message = "Current system is not Windows, so Word COM automation is unavailable."
16
+ }
17
+ else {
18
+ try {
19
+ $word = New-Object -ComObject Word.Application
20
+ $result.wordInstalled = $true
21
+ $result.comAvailable = $true
22
+ $result.version = [string]$word.Version
23
+
24
+ try {
25
+ $doc = $word.Documents.Add()
26
+ $null = $doc.Content
27
+ $result.domAvailable = $true
28
+ $doc.Close([ref]0)
29
+ $result.message = "Microsoft Word is installed and COM/DOM automation is available."
30
+ }
31
+ catch {
32
+ $result.message = "Microsoft Word was detected, but document DOM automation failed: $($_.Exception.Message)"
33
+ }
34
+
35
+ $word.Quit()
36
+ [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($word)
37
+ }
38
+ catch {
39
+ $result.message = "No usable Microsoft Word COM automation environment was detected. Install desktop Microsoft Word."
40
+ }
41
+ }
42
+
43
+ if ($Json) {
44
+ $result | ConvertTo-Json -Depth 4
45
+ }
46
+ else {
47
+ $result.GetEnumerator() | ForEach-Object {
48
+ "{0}: {1}" -f $_.Key, $_.Value
49
+ }
50
+ }
@@ -0,0 +1,62 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [string]$DocPath,
4
+
5
+ [Parameter(Mandatory = $true)]
6
+ [string]$PdfPath
7
+ )
8
+
9
+ Set-StrictMode -Version Latest
10
+ $ErrorActionPreference = "Stop"
11
+
12
+ $word = $null
13
+ $document = $null
14
+
15
+ try {
16
+ $resolvedDoc = (Resolve-Path -LiteralPath $DocPath).Path
17
+ $resolvedPdf = [System.IO.Path]::GetFullPath($PdfPath)
18
+ $outDir = Split-Path -Parent $resolvedPdf
19
+ if (-not (Test-Path -LiteralPath $outDir)) {
20
+ New-Item -ItemType Directory -Path $outDir -Force | Out-Null
21
+ }
22
+
23
+ $word = New-Object -ComObject Word.Application
24
+ $word.Visible = $false
25
+ $word.DisplayAlerts = 0
26
+
27
+ $document = $word.Documents.Open($resolvedDoc, $false, $false)
28
+
29
+ foreach ($toc in $document.TablesOfContents) {
30
+ try { $toc.Update() } catch {}
31
+ try { $toc.UpdatePageNumbers() } catch {}
32
+ }
33
+
34
+ $story = $document.StoryRanges
35
+ while ($story -ne $null) {
36
+ try { $null = $story.Fields.Update() } catch {}
37
+ try { $story = $story.NextStoryRange } catch { $story = $null }
38
+ }
39
+
40
+ $wdExportFormatPDF = 17
41
+ $document.ExportAsFixedFormat($resolvedPdf, $wdExportFormatPDF)
42
+
43
+ [pscustomobject]@{
44
+ input = $resolvedDoc
45
+ output = $resolvedPdf
46
+ exported = $true
47
+ } | ConvertTo-Json -Depth 4
48
+ }
49
+ finally {
50
+ if ($document) {
51
+ $document.Close([ref]0)
52
+ [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($document)
53
+ }
54
+
55
+ if ($word) {
56
+ $word.Quit()
57
+ [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($word)
58
+ }
59
+
60
+ [gc]::Collect()
61
+ [gc]::WaitForPendingFinalizers()
62
+ }