@topmindspace/tms-skills 0.1.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 (99) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/tms-skills.js +160 -0
  5. package/package.json +48 -0
  6. package/top-ppt-html/README.md +254 -0
  7. package/top-ppt-html/SKILL.md +111 -0
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
  11. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
  16. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
  17. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
  18. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
  19. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
  20. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
  21. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
  22. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
  23. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
  24. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
  25. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
  26. package/top-ppt-html/assets/pptx-export.js +1944 -0
  27. package/top-ppt-html/assets/style-gallery.html +589 -0
  28. package/top-ppt-html/assets/templates/architecture.html +3728 -0
  29. package/top-ppt-html/assets/templates/engine.css +840 -0
  30. package/top-ppt-html/assets/templates/presentation.html +3738 -0
  31. package/top-ppt-html/assets/templates/research.html +4017 -0
  32. package/top-ppt-html/assets/templates/ui.js +520 -0
  33. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  34. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  35. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  36. package/top-ppt-html/assets/theme-overview.png +0 -0
  37. package/top-ppt-html/evals/prompts.csv +15 -0
  38. package/top-ppt-html/evals/rubric.schema.json +25 -0
  39. package/top-ppt-html/evals/run_evals.py +220 -0
  40. package/top-ppt-html/evals/trace.example.json +16 -0
  41. package/top-ppt-html/package.json +35 -0
  42. package/top-ppt-html/references/charts-basic.md +624 -0
  43. package/top-ppt-html/references/charts-discipline.md +108 -0
  44. package/top-ppt-html/references/charts-extended.md +482 -0
  45. package/top-ppt-html/references/charts.md +28 -0
  46. package/top-ppt-html/references/components-atoms.md +624 -0
  47. package/top-ppt-html/references/components.md +30 -0
  48. package/top-ppt-html/references/content-rules.md +490 -0
  49. package/top-ppt-html/references/design-system-engine.md +235 -0
  50. package/top-ppt-html/references/design-system.md +478 -0
  51. package/top-ppt-html/references/failure-modes.md +214 -0
  52. package/top-ppt-html/references/high-fidelity.md +127 -0
  53. package/top-ppt-html/references/icons.md +397 -0
  54. package/top-ppt-html/references/industry-benchmark.md +105 -0
  55. package/top-ppt-html/references/infographics-stats.md +308 -0
  56. package/top-ppt-html/references/infographics-structure.md +226 -0
  57. package/top-ppt-html/references/infographics.md +43 -0
  58. package/top-ppt-html/references/layout-grammar.md +315 -0
  59. package/top-ppt-html/references/layouts-architecture.md +108 -0
  60. package/top-ppt-html/references/layouts-combo.md +600 -0
  61. package/top-ppt-html/references/layouts-research.md +160 -0
  62. package/top-ppt-html/references/modes.md +254 -0
  63. package/top-ppt-html/references/outline-design.md +275 -0
  64. package/top-ppt-html/references/playbook.md +266 -0
  65. package/top-ppt-html/references/pptx-export.md +209 -0
  66. package/top-ppt-html/references/reform-plan.md +252 -0
  67. package/top-ppt-html/references/styles.md +370 -0
  68. package/top-ppt-html/references/tech-design.md +138 -0
  69. package/top-ppt-html/scripts/audit_css.py +109 -0
  70. package/top-ppt-html/scripts/audit_docs.py +176 -0
  71. package/top-ppt-html/scripts/audit_skill.py +220 -0
  72. package/top-ppt-html/scripts/audit_styles.py +351 -0
  73. package/top-ppt-html/scripts/build_examples.py +2276 -0
  74. package/top-ppt-html/scripts/build_pptx.js +2380 -0
  75. package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
  76. package/top-ppt-html/scripts/checks_html.py +127 -0
  77. package/top-ppt-html/scripts/cross_verify.py +294 -0
  78. package/top-ppt-html/scripts/env_probe.py +158 -0
  79. package/top-ppt-html/scripts/extract_model.py +210 -0
  80. package/top-ppt-html/scripts/extract_snippet.py +374 -0
  81. package/top-ppt-html/scripts/gen_channel_a.js +214 -0
  82. package/top-ppt-html/scripts/layout-constants.json +3377 -0
  83. package/top-ppt-html/scripts/layout_slots.json +830 -0
  84. package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
  85. package/top-ppt-html/scripts/measure_height.py +178 -0
  86. package/top-ppt-html/scripts/model-schema.json +547 -0
  87. package/top-ppt-html/scripts/negative_tests.py +307 -0
  88. package/top-ppt-html/scripts/package_skill.py +291 -0
  89. package/top-ppt-html/scripts/prepare_images.py +341 -0
  90. package/top-ppt-html/scripts/probe_image_export.py +188 -0
  91. package/top-ppt-html/scripts/quality_gate.py +301 -0
  92. package/top-ppt-html/scripts/regression.py +308 -0
  93. package/top-ppt-html/scripts/render_compare.py +275 -0
  94. package/top-ppt-html/scripts/render_from_model.py +698 -0
  95. package/top-ppt-html/scripts/scaffold_report.py +1054 -0
  96. package/top-ppt-html/scripts/section-file-map.json +105 -0
  97. package/top-ppt-html/scripts/sync_runtime.py +662 -0
  98. package/top-ppt-html/scripts/validate_pptx.py +1510 -0
  99. package/top-ppt-html/scripts/validate_report.py +1456 -0
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML · 文档一致性审计(规范与引用完整性 · 零依赖)
4
+
5
+ 用法:
6
+ python scripts/audit_docs.py # 全量审计
7
+ python scripts/audit_docs.py --quiet # 仅打印结论
8
+
9
+ 审计项(任一不过 → 退出码 1):
10
+ ① § 引用可解析性 —— references 采用**跨文件唯一稳定编号**(§1–§77),
11
+ 所有 `§数字` 引用必须能在三份组件/图表规范中解析到章节
12
+ (白名单:`design-system.md` 自有子节 §1a–§1f / §9b–§9d)
13
+ ② 文件前缀规范性 —— 引用组件/图表规范时须带正确文件名,且不出现
14
+ 重复前缀(`` `charts.md` charts.md §N ``)或裸前缀(`charts.md §N` 未加反引号)
15
+ ③ §46 选型表覆盖度 —— 36 个登记图表类型与 29 个页型都必须在选型表出现
16
+ ④ 图表代码节齐备 —— 每个登记图表类型都要在 charts.md / infographics.md 有 `data-chart` 代码节
17
+ ⑤ 篇数与必需文件 —— references 篇数、SKILL.md frontmatter、进包必需文件齐全
18
+ ⑥ 任务路由可解析性 —— extract_snippet.py 的 TASK_ROUTES 每条 (文件, 节) 必须可
19
+ 实际抽取(防「文字声明同源」漂移:路由声明的节在文件里找不到即失败;
20
+ 空节路由 = 该文件只能整读,≥20KB 的 L2 文件不允许)
21
+
22
+ 何时跑:改 `references/*`、拆分/新增规范文件、调整选型表后(与 regression.py 互补——
23
+ 后者管交付物正确性,本工具管规范自洽性)。
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import re
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ try:
33
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
34
+ except Exception:
35
+ pass
36
+
37
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
38
+ from package_skill import DESC_LIMIT, NAME, REQUIRED # noqa: E402 (复用进包清单,避免两处维护)
39
+ import extract_snippet as ES # noqa: E402 (⑥ 任务路由审计复用同一解析器,避免第三源)
40
+
41
+ ROOT = Path(__file__).resolve().parent.parent
42
+ REF = ROOT / 'references'
43
+ QUIET = '--quiet' in sys.argv[1:]
44
+
45
+ # 组件/图表族物理文件(components.md / charts.md 为逻辑索引;§编号跨文件稳定)
46
+ COMPONENT_FILES = (
47
+ 'components.md', 'components-atoms.md', 'layouts-research.md',
48
+ 'layouts-architecture.md', 'layouts-combo.md',
49
+ )
50
+ CHART_FILES = (
51
+ 'charts.md', 'charts-basic.md', 'charts-extended.md', 'charts-discipline.md',
52
+ )
53
+ OWNER_SRC = tuple(
54
+ [(fn, r'^## (\d+[a-z]?)\. ') for fn in COMPONENT_FILES + CHART_FILES]
55
+ + [('infographics.md', r'^### (7[1-9]|8\d)\.? '),
56
+ ('infographics-stats.md', r'^### (?:§)?(\d+[a-z]?)\.?\s'),
57
+ ('infographics-structure.md', r'^### (?:§)?(\d+[a-z]?)\.?\s')]
58
+ )
59
+ # design-system.md 自有子节编号(不算悬空)
60
+ DS_OWN = {'1a', '1b', '1c', '1d', '1e', '1f', '9b', '9c', '9d'}
61
+ DUP_PRE = re.compile(r'`(components|charts|infographics)\.md`\s*[((]?\s*`?\1\.md')
62
+ BARE_PRE = re.compile(r'(?<![`\w])(components|charts|infographics)\.md\s*§')
63
+ REF_ID = re.compile(r'§\s*(\d+[a-zA-Z]?)') # 节号后缀大小写均收录(§38b / §46C 一并受审计)
64
+
65
+
66
+ def doc_files():
67
+ files = ['SKILL.md', 'README.md']
68
+ files += ['references/%s' % p.name for p in sorted(REF.glob('*.md'))]
69
+ return files
70
+
71
+
72
+ def main() -> int:
73
+ errs: list[str] = []
74
+ lc = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
75
+ ms = json.loads((ROOT / 'scripts' / 'model-schema.json').read_text(encoding='utf-8'))
76
+
77
+ owner: dict[str, str] = {}
78
+ for f, pat in OWNER_SRC:
79
+ p = REF / f
80
+ if not p.exists():
81
+ continue
82
+ for m in re.finditer(pat, p.read_text(encoding='utf-8'), re.M):
83
+ owner[m.group(1)] = f
84
+
85
+ # ① § 引用可解析性
86
+ dangling = 0
87
+ for rel in doc_files():
88
+ for i, ln in enumerate((ROOT / rel).read_text(encoding='utf-8').splitlines()):
89
+ for m in REF_ID.finditer(ln):
90
+ i_d = m.group(1)
91
+ if i_d in owner or i_d in DS_OWN:
92
+ continue
93
+ dangling += 1
94
+ errs.append(f'悬空引用 {rel} L{i+1}: §{i_d}')
95
+ print(f'① § 引用可解析性:悬空 {dangling} 处(编号空间 {len(owner)} 项)')
96
+
97
+ # ② 文件前缀规范性
98
+ prefix_bad = 0
99
+ for rel in doc_files():
100
+ for i, ln in enumerate((ROOT / rel).read_text(encoding='utf-8').splitlines()):
101
+ if DUP_PRE.search(ln):
102
+ prefix_bad += 1
103
+ errs.append(f'重复文件前缀 {rel} L{i+1}')
104
+ if BARE_PRE.search(ln):
105
+ prefix_bad += 1
106
+ errs.append(f'裸文件前缀 {rel} L{i+1}')
107
+ print(f'② 文件前缀规范性:异常 {prefix_bad} 处')
108
+
109
+ # ③ §46 选型表覆盖度(物理文件 layouts-combo.md;逻辑名 components.md 亦可)
110
+ combo = REF / 'layouts-combo.md'
111
+ if not combo.exists():
112
+ combo = REF / 'components.md'
113
+ comp = combo.read_text(encoding='utf-8')
114
+ sec46 = re.search(r'## 46\. .*?\n(.*?)\n## 46b\.', comp, re.S)
115
+ table = sec46.group(1) if sec46 else ''
116
+ chart_types = lc['charts']['types']
117
+ page_types = [k for k in ms['pageTypes'] if not k.startswith('$')]
118
+ miss_chart = [t for t in chart_types if t not in table]
119
+ miss_page = [t for t in page_types if ('`%s`' % t) not in table]
120
+ if miss_chart:
121
+ errs.append(f'§46 未覆盖图表类型:{miss_chart}')
122
+ if miss_page:
123
+ errs.append(f'§46 未覆盖页型:{miss_page}')
124
+ print(f'③ §46 选型表覆盖:图表 {len(chart_types) - len(miss_chart)}/{len(chart_types)} · '
125
+ f'页型 {len(page_types) - len(miss_page)}/{len(page_types)}')
126
+
127
+ # ④ 图表代码节齐备(拆分后的 charts-* + infographics)
128
+ charts_txt = ''
129
+ for fn in CHART_FILES + ('infographics.md', 'infographics-stats.md', 'infographics-structure.md'):
130
+ p = REF / fn
131
+ if p.exists():
132
+ charts_txt += p.read_text(encoding='utf-8')
133
+ no_code = [t for t in chart_types
134
+ if not re.search(r'data-chart="%s"' % re.escape(t), charts_txt)]
135
+ if no_code:
136
+ errs.append(f'缺代码节的图表类型:{no_code}')
137
+ print(f'④ 图表代码节:{len(chart_types) - len(no_code)}/{len(chart_types)} 齐备')
138
+
139
+ # ⑤ 篇数与必需文件
140
+ n_ref = len(list(REF.glob('*.md')))
141
+ sk = (ROOT / 'SKILL.md').read_text(encoding='utf-8')
142
+ d = re.search(r'^description:\s*"(.*)"\s*$', sk, re.M | re.S)
143
+ dlen = len(d.group(1)) if d else -1
144
+ nm = re.search(r'^name:\s*(\S+)\s*$', sk, re.M)
145
+ missing = [r for r in REQUIRED if not (ROOT / r).exists()]
146
+ if dlen < 0 or dlen > DESC_LIMIT:
147
+ errs.append(f'SKILL.md description 长度异常:{dlen}(上限 {DESC_LIMIT})')
148
+ if not nm or nm.group(1) != NAME:
149
+ errs.append(f'SKILL.md name 与技能名 {NAME} 不一致')
150
+ if missing:
151
+ errs.append(f'缺必需文件:{missing}')
152
+ print(f'⑤ 规范篇数 {n_ref} · description {dlen} 字符 · 必需文件缺 {len(missing)} 项')
153
+
154
+ # ⑥ 任务路由可解析性(--task 与 validate_report FIX_GUIDE 的修复链路依赖它;
155
+ # 实现与 regression ⑦-b 共用 ES.verify_routes,防两处维护)
156
+ route_bad = ES.verify_routes()
157
+ unresolved = [x for x in route_bad if not x.endswith('空路由')]
158
+ empty_routes = [x for x in route_bad if x.endswith('空路由')]
159
+ if unresolved:
160
+ errs.append(f'任务路由不可解析(--task/FIX_GUIDE 修复链路断裂):{unresolved}')
161
+ if empty_routes:
162
+ errs.append(f'任务路由空节(整读大文件,应补节号或标题关键词):{empty_routes}')
163
+ print(f'⑥ 任务路由可解析性:{len(ES.TASK_ROUTES)} 条路由 · 不可解析 {len(unresolved)} · 空节 {len(empty_routes)}')
164
+
165
+ print()
166
+ if errs:
167
+ print('审计未通过:')
168
+ for e in errs:
169
+ print(f' ✗ {e}')
170
+ return 1
171
+ print('文档一致性审计通过:引用可解析 · 前缀规范 · 选型表全覆盖 · 代码节齐备 · 元数据合规')
172
+ return 0
173
+
174
+
175
+ if __name__ == '__main__':
176
+ sys.exit(main())
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML · 技能工程审计(零依赖 · 体积预算 / 渐进式披露 / 触发门禁 / 冗余)
4
+
5
+ 用法:
6
+ python scripts/audit_skill.py # 全量审计
7
+ python scripts/audit_skill.py --quiet # 仅打印结论
8
+ python scripts/audit_skill.py --json # 机器可读
9
+
10
+ 为什么需要它(第一性原理):
11
+ 技能的成本 = **加载进上下文的字节数** + **必须执行的步骤数**。这两项在 v7.1 完全无预算、
12
+ 无门禁,导致"技能越做越全 → 单次生成越慢越贵"。本脚本把「效率目标」变成可检查项。
13
+
14
+ 审计项(任一不过 → 退出码 1):
15
+ ① 体积预算 —— SKILL.md / 常读入口 playbook.md / 单份 reference / 模式模板 的上限
16
+ ② 元数据 —— frontmatter name 与技能名一致;description ≤ DESC_HARD,且 ≤ DESC_SOFT(触发精度)
17
+ ③ 披露分层 —— SKILL.md 必须声明 L0/L1/L2 三档,且 L1(常读入口)恰好 1 份
18
+ ④ 交互门禁 —— SKILL.md 必须含「Gate 0 参考图先行」,且其出现位置在「六项问询」之前
19
+ ⑤ 效率预算 —— SKILL.md 必须显式声明交互轮次上限与必读文件数上限
20
+ ⑥ 引用完整性 —— SKILL.md 与 playbook.md 提到的 references/*.md 必须存在
21
+ ⑦ 冗余报告 —— SKILL.md ↔ 全部 references(含 playbook)的归一化共同片段
22
+ (≥20 字含 CJK;命令行/路径等纯 ASCII 重复豁免)
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import re
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ try:
32
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
33
+ except Exception:
34
+ pass
35
+
36
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
37
+ from package_skill import DESC_LIMIT, NAME # noqa: E402 复用进包门禁的同一常量
38
+
39
+ ROOT = Path(__file__).resolve().parent.parent
40
+ REF = ROOT / 'references'
41
+ TPL = ROOT / 'assets' / 'templates'
42
+ QUIET = '--quiet' in sys.argv[1:]
43
+ AS_JSON = '--json' in sys.argv[1:]
44
+
45
+ # ── 效率预算(唯一来源;调整预算改这里)──────────────────────────────────
46
+ BUDGET = {
47
+ 'skill_md': 13 * 1024, # 入口路由:加载即占上下文,必须瘦(v8.3 从 16KB 收紧:SKILL.md 瘦身后 12.4KB + 余量)
48
+ 'playbook': 22 * 1024, # 唯一常读入口(决策层:模式/版式/图表/配色/校验)
49
+ 'reference': 96 * 1024, # 单份深度文件上限
50
+ 'template': 320 * 1024, # 模式模板(引擎+运行时内联,天然大;靠 scaffold 脚本避免被读)
51
+ 'desc_soft': 700, # description 软上限(触发精度)
52
+ 'desc_hard': DESC_LIMIT, # description 硬上限(宿主规范)
53
+ 'l1_files': 1, # 常读入口份数
54
+ }
55
+ PLAYBOOK = REF / 'playbook.md'
56
+
57
+ # 「内容两处维护」检测:归一化滑窗子串(v8.3 起)。
58
+ # 旧实现只切 ≥40 字整句且**排除 playbook** —— 表格行/公式/判定条件全部漏检。
59
+ # 新实现:整库归一化(去空白与 markdown 修饰符)后做滑窗子串匹配,
60
+ # 窗口须含 CJK(纯 ASCII 的命令行/路径重复属工具语义,豁免)。
61
+ _NORM_STRIP = re.compile(r'[`*_#>\[\]()|::,,。;;\s·—\-–/\\="「」『』()]')
62
+ _CJK = re.compile(r'[\u4e00-\u9fff]')
63
+ DUP_WIN = 20 # 共同片段最小归一化长度(<20 的短语级重合属合理摘要,放行)
64
+ DUP_MAX = 0 # 允许的重复片段上限(S2-1 瘦身后应为 0)
65
+
66
+
67
+ def norm_text(text: str) -> str:
68
+ return _NORM_STRIP.sub('', text)
69
+
70
+
71
+ def dup_spans(sk_text: str, refs_text: str) -> list[str]:
72
+ """返回 SKILL.md 与 references 的共同归一化片段(合并相邻窗口)。"""
73
+ a, b = norm_text(sk_text), norm_text(refs_text)
74
+ ranges: list[tuple[int, int]] = []
75
+ i = 0
76
+ while i + DUP_WIN <= len(a):
77
+ w = a[i:i + DUP_WIN]
78
+ if _CJK.search(w) and w in b:
79
+ p = b.find(w)
80
+ lo, hi = i, i + DUP_WIN
81
+ # 命中后向两侧扩展到最长共同片段(闭合步长 4 的对齐缺口)
82
+ while lo > 0 and p > 0 and a[lo - 1] == b[p - 1]:
83
+ lo -= 1
84
+ p -= 1
85
+ hi_b = p + DUP_WIN
86
+ while hi < len(a) and hi_b < len(b) and a[hi] == b[hi_b]:
87
+ hi += 1
88
+ hi_b += 1
89
+ if ranges and lo <= ranges[-1][1]:
90
+ ranges[-1] = (ranges[-1][0], hi)
91
+ else:
92
+ ranges.append((lo, hi))
93
+ i = hi # 跳过已覆盖区间(顺带提速)
94
+ else:
95
+ i += 4
96
+ return [a[lo:hi] for lo, hi in ranges]
97
+
98
+
99
+ def kb(n: int) -> str:
100
+ return f'{n / 1024:.1f}KB'
101
+
102
+
103
+ def main() -> int:
104
+ errs: list[str] = []
105
+ notes: list[str] = []
106
+ rows: list[tuple[str, bool, str]] = []
107
+
108
+ def chk(name: str, ok: bool, bad_detail: str, ok_detail: str | None = None,
109
+ fatal: bool = True) -> None:
110
+ """bad_detail 用于失败说明;ok_detail 用于通过说明(缺省复用 bad_detail)。"""
111
+ detail = (ok_detail if (ok and ok_detail is not None) else bad_detail)
112
+ rows.append((name, ok, detail))
113
+ if not ok:
114
+ (errs if fatal else notes).append(f'{name}: {bad_detail}')
115
+
116
+ sk_path = ROOT / 'SKILL.md'
117
+ sk = sk_path.read_text(encoding='utf-8')
118
+
119
+ # ① 体积预算
120
+ sk_size = sk_path.stat().st_size
121
+ chk('① SKILL.md 体积', sk_size <= BUDGET['skill_md'],
122
+ f'{kb(sk_size)} / 上限 {kb(BUDGET["skill_md"])}')
123
+
124
+ pb_size = PLAYBOOK.stat().st_size if PLAYBOOK.exists() else -1
125
+ chk('① 常读入口 playbook.md', 0 < pb_size <= BUDGET['playbook'],
126
+ f'{kb(pb_size)} / 上限 {kb(BUDGET["playbook"])}' if pb_size > 0
127
+ else f'缺失(应存在且 ≤ {kb(BUDGET["playbook"])})')
128
+
129
+ over_ref = [(p.name, p.stat().st_size) for p in sorted(REF.glob('*.md'))
130
+ if p.stat().st_size > BUDGET['reference']]
131
+ chk('① 单份 reference 体积', not over_ref,
132
+ '; '.join(f'{n} {kb(s)}' for n, s in over_ref) or
133
+ f'最大 {kb(max((p.stat().st_size for p in REF.glob("*.md")), default=0))} / 上限 {kb(BUDGET["reference"])}')
134
+
135
+ over_tpl = [(p.name, p.stat().st_size) for p in sorted(TPL.glob('*.html'))
136
+ if p.stat().st_size > BUDGET['template']]
137
+ chk('① 模式模板体积', not over_tpl,
138
+ '; '.join(f'{n} {kb(s)}' for n, s in over_tpl) or
139
+ f'最大 {kb(max((p.stat().st_size for p in TPL.glob("*.html")), default=0))} / 上限 {kb(BUDGET["template"])}')
140
+
141
+ # ② 元数据
142
+ d = re.search(r'^description:\s*"(.*)"\s*$', sk, re.M | re.S)
143
+ dlen = len(d.group(1)) if d else -1
144
+ nm = re.search(r'^name:\s*(\S+)\s*$', sk, re.M)
145
+ chk('② name 一致', bool(nm) and nm.group(1) == NAME,
146
+ f'name={nm.group(1) if nm else "缺失"}(应为 {NAME})')
147
+ chk('② description 硬上限', 0 < dlen <= BUDGET['desc_hard'],
148
+ f'{dlen} / 上限 {BUDGET["desc_hard"]}')
149
+ chk('② description 软上限(触发精度)', 0 < dlen <= BUDGET['desc_soft'],
150
+ f'{dlen} / 建议 ≤ {BUDGET["desc_soft"]}')
151
+
152
+ # ③ 渐进式披露分层(机器可读标记:L0=… · L1=… · L2=…)
153
+ m_l1 = re.search(r'L1\s*=\s*`?([^`\s·|]+)`?', sk)
154
+ has_l0 = bool(re.search(r'L0\s*=', sk))
155
+ has_l2 = bool(re.search(r'L2\s*=', sk))
156
+ l1_files = [f.strip() for f in re.split(r'[,、]', m_l1.group(1)) if f.strip()] if m_l1 else []
157
+ chk('③ 披露分层 L0/L1/L2', has_l0 and has_l2 and len(l1_files) == BUDGET['l1_files'],
158
+ f'L0={has_l0} L1={l1_files} L2={has_l2}(L1 须恰好 {BUDGET["l1_files"]} 份)')
159
+ chk('③ 常读入口存在', bool(l1_files) and all((ROOT / f).exists() for f in l1_files),
160
+ f'L1={l1_files} 不存在', f'{l1_files} 存在')
161
+
162
+ # ④ Gate 0(参考图先行)
163
+ g0 = sk.find('Gate 0')
164
+ ask = sk.find('六项问询')
165
+ chk('④ Gate 0 存在', g0 >= 0, '未找到「Gate 0」标记', '已就位')
166
+ chk('④ Gate 0 在问询之前', g0 >= 0 and ask >= 0 and g0 < ask,
167
+ f'Gate0@{g0} 未在六项问询@{ask} 之前', f'位置正确({g0} < {ask})')
168
+
169
+ # ⑤ 效率预算声明
170
+ chk('⑤ 交互轮次预算', bool(re.search(r'交互轮次|轮次上限|≤\s*3\s*轮', sk)),
171
+ 'SKILL.md 未声明交互轮次上限', '已声明(≤3 轮)')
172
+ chk('⑤ 必读文件预算', bool(re.search(r'必读.{0,6}上限|必读文件数|只读当前需要的|不预读', sk)),
173
+ 'SKILL.md 未声明必读文件预算/不预读纪律', '已声明(不预读纪律)')
174
+
175
+ # ⑥ 引用完整性
176
+ missing: list[str] = []
177
+ n_refs = 0
178
+ for src, text in (('SKILL.md', sk),
179
+ ('references/playbook.md',
180
+ PLAYBOOK.read_text(encoding='utf-8') if PLAYBOOK.exists() else '')):
181
+ for rel in sorted(set(re.findall(r'references/([a-z0-9\-]+\.md)', text))):
182
+ n_refs += 1
183
+ if not (REF / rel).exists():
184
+ missing.append(f'{src} → references/{rel}')
185
+ chk('⑥ 引用文件存在', not missing, '; '.join(missing),
186
+ f'{n_refs} 处引用全部存在')
187
+
188
+ # ⑦ 冗余报告(SKILL.md ↔ 全部 references,含 playbook;归一化滑窗子串)
189
+ refs_all = '\n'.join(p.read_text(encoding='utf-8') for p in sorted(REF.glob('*.md')))
190
+ dup = dup_spans(sk, refs_all)
191
+ chk('⑦ 内容重复(SKILL.md ↔ references · 归一化 ≥%d 字)' % DUP_WIN,
192
+ len(dup) <= DUP_MAX,
193
+ f'{len(dup)} 处重复' + (f':{dup[0][:40]}…' if dup else ''),
194
+ f'0 处重复(≥{DUP_WIN} 字归一化片段)')
195
+
196
+ # ── 输出 ────────────────────────────────────────────────────────────────
197
+ if AS_JSON:
198
+ print(json.dumps({'rows': [{'item': r[0], 'ok': r[1], 'detail': r[2]} for r in rows],
199
+ 'errors': errs, 'notes': notes}, ensure_ascii=False, indent=2))
200
+ return 1 if errs else 0
201
+ if not QUIET:
202
+ print('技能工程审计 · 效率预算与门禁\n' + '-' * 56)
203
+ for n, ok, det in rows:
204
+ print(f' {"✓" if ok else "✗"} {n}:{det}')
205
+ if notes:
206
+ print('\n提示(不判失败):')
207
+ for n in notes:
208
+ print(f' · {n}')
209
+ print('-' * 56)
210
+ if errs:
211
+ print(f'技能工程审计未通过:{len(errs)} 项')
212
+ for e in errs:
213
+ print(f' ✗ {e}')
214
+ return 1
215
+ print('技能工程审计通过:体积达标 · 元数据合规 · 三档披露 · Gate 0 就位 · 预算已声明')
216
+ return 0
217
+
218
+
219
+ if __name__ == '__main__':
220
+ sys.exit(main())