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.
- package/README.md +124 -0
- package/agents/aggregator.md +27 -0
- package/agents/cli-explore-agent.md +188 -0
- package/agents/cross-role-reviewer.md +171 -0
- package/agents/delegate.md +19 -0
- package/agents/explorer.md +23 -0
- package/agents/impeccable-agent.md +99 -0
- package/agents/ralph-executor.md +81 -0
- package/agents/reference.md +24 -0
- package/agents/role-design-author.md +220 -0
- package/agents/team-supervisor.md +143 -0
- package/agents/team-worker.md +237 -0
- package/agents/ui-design-agent.md +270 -0
- package/agents/workflow-analyzer.md +116 -0
- package/agents/workflow-codebase-mapper.md +77 -0
- package/agents/workflow-collab-planner.md +147 -0
- package/agents/workflow-debugger.md +104 -0
- package/agents/workflow-executor.md +133 -0
- package/agents/workflow-external-researcher.md +86 -0
- package/agents/workflow-integration-checker.md +83 -0
- package/agents/workflow-nyquist-auditor.md +85 -0
- package/agents/workflow-phase-researcher.md +85 -0
- package/agents/workflow-plan-checker.md +100 -0
- package/agents/workflow-planner.md +200 -0
- package/agents/workflow-project-researcher.md +74 -0
- package/agents/workflow-research-synthesizer.md +70 -0
- package/agents/workflow-reviewer.md +82 -0
- package/agents/workflow-roadmapper.md +81 -0
- package/agents/workflow-verifier.md +120 -0
- package/package.json +58 -0
- package/skills/codify-to-knowhow/SKILL.md +167 -0
- package/skills/delegation-check/SKILL.md +297 -0
- package/skills/domain-add/SKILL.md +75 -0
- package/skills/insight-challenge/SKILL.md +226 -0
- package/skills/learn-decompose/SKILL.md +124 -0
- package/skills/learn-follow/SKILL.md +163 -0
- package/skills/learn-investigate/SKILL.md +160 -0
- package/skills/learn-second-opinion/SKILL.md +128 -0
- package/skills/maestro/SKILL.md +239 -0
- package/skills/maestro-amend/SKILL.md +169 -0
- package/skills/maestro-analyze/SKILL.md +173 -0
- package/skills/maestro-blueprint/SKILL.md +159 -0
- package/skills/maestro-brainstorm/SKILL.md +193 -0
- package/skills/maestro-collab/SKILL.md +181 -0
- package/skills/maestro-companion/SKILL.md +536 -0
- package/skills/maestro-composer/SKILL.md +192 -0
- package/skills/maestro-execute/SKILL.md +193 -0
- package/skills/maestro-fork/SKILL.md +104 -0
- package/skills/maestro-grill/SKILL.md +151 -0
- package/skills/maestro-guard/SKILL.md +124 -0
- package/skills/maestro-help/SKILL.md +328 -0
- package/skills/maestro-impeccable/SKILL.md +302 -0
- package/skills/maestro-init/SKILL.md +151 -0
- package/skills/maestro-merge/SKILL.md +90 -0
- package/skills/maestro-milestone-audit/SKILL.md +117 -0
- package/skills/maestro-milestone-complete/SKILL.md +133 -0
- package/skills/maestro-milestone-release/SKILL.md +131 -0
- package/skills/maestro-next/SKILL.md +269 -0
- package/skills/maestro-overlay/SKILL.md +203 -0
- package/skills/maestro-plan/SKILL.md +194 -0
- package/skills/maestro-player/SKILL.md +184 -0
- package/skills/maestro-quick/SKILL.md +117 -0
- package/skills/maestro-ralph/SKILL.md +905 -0
- package/skills/maestro-ralph-cli/SKILL.md +976 -0
- package/skills/maestro-ralph-cli-execute/SKILL.md +229 -0
- package/skills/maestro-ralph-execute/SKILL.md +409 -0
- package/skills/maestro-ralph-v2/SKILL.md +1198 -0
- package/skills/maestro-roadmap/SKILL.md +157 -0
- package/skills/maestro-swarm-workflow/SKILL.md +272 -0
- package/skills/maestro-tools-execute/SKILL.md +143 -0
- package/skills/maestro-tools-register/SKILL.md +192 -0
- package/skills/maestro-ui-codify/SKILL.md +127 -0
- package/skills/maestro-universal-workflow/SKILL.md +622 -0
- package/skills/maestro-update/SKILL.md +156 -0
- package/skills/manage-codebase-rebuild/SKILL.md +111 -0
- package/skills/manage-drift-realign/SKILL.md +159 -0
- package/skills/manage-harvest/SKILL.md +121 -0
- package/skills/manage-issue/SKILL.md +88 -0
- package/skills/manage-issue-discover/SKILL.md +104 -0
- package/skills/manage-kg-extractors/SKILL.md +147 -0
- package/skills/manage-knowhow/SKILL.md +102 -0
- package/skills/manage-knowhow-capture/SKILL.md +109 -0
- package/skills/manage-knowledge-audit/SKILL.md +147 -0
- package/skills/manage-status/SKILL.md +80 -0
- package/skills/manage-wiki/SKILL.md +104 -0
- package/skills/odyssey-debug/SKILL.md +344 -0
- package/skills/odyssey-improve/SKILL.md +423 -0
- package/skills/odyssey-planex/SKILL.md +542 -0
- package/skills/odyssey-review-test-fix/SKILL.md +392 -0
- package/skills/odyssey-ui/SKILL.md +388 -0
- package/skills/prompt-generator/SKILL.md +470 -0
- package/skills/quality-auto-test/SKILL.md +168 -0
- package/skills/quality-debug/SKILL.md +176 -0
- package/skills/quality-refactor/SKILL.md +108 -0
- package/skills/quality-retrospective/SKILL.md +134 -0
- package/skills/quality-review/SKILL.md +169 -0
- package/skills/quality-sync/SKILL.md +90 -0
- package/skills/quality-test/SKILL.md +183 -0
- package/skills/scholar-anti-ai-writing/SKILL.md +173 -0
- package/skills/scholar-anti-ai-writing/references/patterns-chinese.md +242 -0
- package/skills/scholar-anti-ai-writing/references/patterns-english.md +242 -0
- package/skills/scholar-citation-verify/SKILL.md +166 -0
- package/skills/scholar-citation-verify/references/api-usage.md +372 -0
- package/skills/scholar-citation-verify/references/common-errors.md +384 -0
- package/skills/scholar-citation-verify/references/verification-rules.md +399 -0
- package/skills/scholar-experiment/SKILL.md +321 -0
- package/skills/scholar-ideation/SKILL.md +288 -0
- package/skills/scholar-latex-organizer/SKILL.md +186 -0
- package/skills/scholar-publish/SKILL.md +220 -0
- package/skills/scholar-rebuttal-pro/README.md +313 -0
- package/skills/scholar-rebuttal-pro/SKILL.md +511 -0
- package/skills/scholar-review/SKILL.md +227 -0
- package/skills/scholar-thesis-docx/README.md +111 -0
- package/skills/scholar-thesis-docx/README_EN.md +108 -0
- package/skills/scholar-thesis-docx/SKILL.md +212 -0
- package/skills/scholar-thesis-docx/references/failure-patterns-and-quality-gates.md +178 -0
- package/skills/scholar-thesis-docx/references/figure-and-code-rules.md +33 -0
- package/skills/scholar-thesis-docx/references/paper-format-workflow.md +68 -0
- package/skills/scholar-thesis-docx/references/script-usage.md +157 -0
- package/skills/scholar-thesis-docx/scripts/audit_docx_ooxml.py +429 -0
- package/skills/scholar-thesis-docx/scripts/check_word_com.ps1 +50 -0
- package/skills/scholar-thesis-docx/scripts/export_word_pdf.ps1 +62 -0
- package/skills/scholar-thesis-docx/scripts/normalize_word_styles.ps1 +498 -0
- package/skills/scholar-thesis-docx/scripts/render_mermaid_figure.ps1 +148 -0
- package/skills/scholar-writing/SKILL.md +296 -0
- package/skills/security-audit/SKILL.md +244 -0
- package/skills/skill-generator/SKILL.md +472 -0
- package/skills/skill-iter-tune/SKILL.md +382 -0
- package/skills/skill-simplify/SKILL.md +63 -0
- package/skills/skill-tuning/SKILL.md +174 -0
- package/skills/spec-add/SKILL.md +105 -0
- package/skills/spec-load/SKILL.md +98 -0
- package/skills/spec-remove/SKILL.md +74 -0
- package/skills/spec-setup/SKILL.md +75 -0
- package/skills/team-adversarial-swarm/SKILL.md +233 -0
- package/skills/team-adversarial-swarm/scripts/__pycache__/pheromone.cpython-313.pyc +0 -0
- package/skills/team-adversarial-swarm/scripts/__pycache__/scoring.cpython-313.pyc +0 -0
- package/skills/team-adversarial-swarm/scripts/aco.py +473 -0
- package/skills/team-adversarial-swarm/scripts/pheromone.py +144 -0
- package/skills/team-adversarial-swarm/scripts/scoring.py +92 -0
- package/skills/team-adversarial-swarm/scripts/test_aco.py +475 -0
- package/skills/team-arch-opt/SKILL.md +158 -0
- package/skills/team-brainstorm/SKILL.md +171 -0
- package/skills/team-coordinate/SKILL.md +266 -0
- package/skills/team-designer/SKILL.md +160 -0
- package/skills/team-executor/SKILL.md +189 -0
- package/skills/team-frontend/SKILL.md +136 -0
- package/skills/team-frontend-debug/SKILL.md +198 -0
- package/skills/team-interactive-craft/SKILL.md +141 -0
- package/skills/team-issue/SKILL.md +171 -0
- package/skills/team-lifecycle-v4/SKILL.md +209 -0
- package/skills/team-motion-design/SKILL.md +142 -0
- package/skills/team-perf-opt/SKILL.md +175 -0
- package/skills/team-planex/SKILL.md +137 -0
- package/skills/team-quality-assurance/SKILL.md +147 -0
- package/skills/team-review/SKILL.md +147 -0
- package/skills/team-roadmap-dev/SKILL.md +169 -0
- package/skills/team-swarm/SKILL.md +178 -0
- package/skills/team-swarm/scripts/aco.py +473 -0
- package/skills/team-swarm/scripts/pheromone.py +144 -0
- package/skills/team-swarm/scripts/scoring.py +92 -0
- package/skills/team-swarm/scripts/test_aco.py +475 -0
- package/skills/team-tech-debt/SKILL.md +128 -0
- package/skills/team-testing/SKILL.md +143 -0
- package/skills/team-ui-polish/SKILL.md +141 -0
- package/skills/team-uidesign/SKILL.md +144 -0
- package/skills/team-ultra-analyze/SKILL.md +173 -0
- package/skills/team-ux-improve/SKILL.md +151 -0
- package/skills/team-visual-a11y/SKILL.md +156 -0
- package/skills/workflow-skill-designer/SKILL.md +496 -0
- package/src/extension/index.ts +222 -0
- package/src/extension/schemas.ts +131 -0
- package/src/providers/cli-tools-loader.ts +74 -0
- package/src/providers/provider-registry.ts +130 -0
- package/src/tools/delegate.ts +85 -0
- package/src/tools/explore.ts +134 -0
- package/src/tools/moa.ts +213 -0
- package/src/tools/status.ts +99 -0
- 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
|
+
}
|