@topmindspace/tms-skills 2.0.0 → 2.0.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 (99) hide show
  1. package/CHANGELOG.md +139 -123
  2. package/README.md +126 -103
  3. package/bin/tms-skills.js +160 -160
  4. package/package.json +48 -48
  5. package/top-ppt-html/README.md +254 -253
  6. package/top-ppt-html/SKILL.md +111 -111
  7. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3873 -3926
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3873 -3926
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +167 -167
  11. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4272 -4325
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +320 -320
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4272 -4325
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +320 -320
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4272 -4325
  16. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
  17. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5474 -5527
  18. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +913 -913
  19. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5474 -5527
  20. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +913 -913
  21. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5474 -5527
  22. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
  23. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5474 -5527
  24. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +913 -913
  25. package/top-ppt-html/assets/pptx-export.js +1944 -1944
  26. package/top-ppt-html/assets/style-gallery.html +559 -589
  27. package/top-ppt-html/assets/templates/architecture.html +3675 -3728
  28. package/top-ppt-html/assets/templates/engine.css +787 -840
  29. package/top-ppt-html/assets/templates/presentation.html +3685 -3738
  30. package/top-ppt-html/assets/templates/research.html +3964 -4017
  31. package/top-ppt-html/assets/templates/ui.js +520 -520
  32. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  33. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  34. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  35. package/top-ppt-html/assets/theme-overview.png +0 -0
  36. package/top-ppt-html/evals/prompts.csv +15 -15
  37. package/top-ppt-html/evals/rubric.schema.json +25 -25
  38. package/top-ppt-html/evals/run_evals.py +220 -220
  39. package/top-ppt-html/evals/trace.example.json +16 -16
  40. package/top-ppt-html/package-lock.json +175 -0
  41. package/top-ppt-html/package.json +30 -35
  42. package/top-ppt-html/references/charts-basic.md +624 -624
  43. package/top-ppt-html/references/charts-discipline.md +108 -108
  44. package/top-ppt-html/references/charts-extended.md +482 -482
  45. package/top-ppt-html/references/charts.md +28 -28
  46. package/top-ppt-html/references/components-atoms.md +624 -624
  47. package/top-ppt-html/references/components.md +30 -30
  48. package/top-ppt-html/references/content-rules.md +490 -490
  49. package/top-ppt-html/references/design-system-engine.md +235 -235
  50. package/top-ppt-html/references/design-system.md +471 -478
  51. package/top-ppt-html/references/failure-modes.md +214 -214
  52. package/top-ppt-html/references/high-fidelity.md +127 -127
  53. package/top-ppt-html/references/icons.md +397 -397
  54. package/top-ppt-html/references/industry-benchmark.md +105 -105
  55. package/top-ppt-html/references/infographics-stats.md +308 -308
  56. package/top-ppt-html/references/infographics-structure.md +226 -226
  57. package/top-ppt-html/references/infographics.md +43 -43
  58. package/top-ppt-html/references/layout-grammar.md +315 -315
  59. package/top-ppt-html/references/layouts-architecture.md +108 -108
  60. package/top-ppt-html/references/layouts-combo.md +600 -600
  61. package/top-ppt-html/references/layouts-research.md +160 -160
  62. package/top-ppt-html/references/modes.md +254 -254
  63. package/top-ppt-html/references/outline-design.md +275 -275
  64. package/top-ppt-html/references/playbook.md +266 -266
  65. package/top-ppt-html/references/pptx-export.md +209 -209
  66. package/top-ppt-html/references/reform-plan.md +252 -252
  67. package/top-ppt-html/references/styles.md +336 -370
  68. package/top-ppt-html/references/tech-design.md +138 -138
  69. package/top-ppt-html/scripts/audit_css.py +109 -109
  70. package/top-ppt-html/scripts/audit_docs.py +176 -176
  71. package/top-ppt-html/scripts/audit_skill.py +220 -220
  72. package/top-ppt-html/scripts/audit_styles.py +293 -351
  73. package/top-ppt-html/scripts/build_examples.py +2276 -2276
  74. package/top-ppt-html/scripts/build_pptx.js +2380 -2380
  75. package/top-ppt-html/scripts/capture_theme_overview.js +78 -78
  76. package/top-ppt-html/scripts/checks_html.py +127 -127
  77. package/top-ppt-html/scripts/cross_verify.py +294 -294
  78. package/top-ppt-html/scripts/env_probe.py +158 -158
  79. package/top-ppt-html/scripts/extract_model.py +210 -210
  80. package/top-ppt-html/scripts/extract_snippet.py +374 -374
  81. package/top-ppt-html/scripts/gen_channel_a.js +214 -214
  82. package/top-ppt-html/scripts/layout-constants.json +3309 -3377
  83. package/top-ppt-html/scripts/layout_slots.json +830 -830
  84. package/top-ppt-html/scripts/lib_layout_regions.js +412 -412
  85. package/top-ppt-html/scripts/measure_height.py +178 -178
  86. package/top-ppt-html/scripts/model-schema.json +547 -547
  87. package/top-ppt-html/scripts/negative_tests.py +307 -307
  88. package/top-ppt-html/scripts/package_skill.py +291 -291
  89. package/top-ppt-html/scripts/prepare_images.py +341 -341
  90. package/top-ppt-html/scripts/probe_image_export.py +188 -188
  91. package/top-ppt-html/scripts/quality_gate.py +301 -301
  92. package/top-ppt-html/scripts/regression.py +307 -308
  93. package/top-ppt-html/scripts/render_compare.py +275 -275
  94. package/top-ppt-html/scripts/render_from_model.py +698 -698
  95. package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
  96. package/top-ppt-html/scripts/section-file-map.json +104 -104
  97. package/top-ppt-html/scripts/sync_runtime.py +662 -662
  98. package/top-ppt-html/scripts/validate_pptx.py +1510 -1510
  99. package/top-ppt-html/scripts/validate_report.py +1456 -1456
@@ -1,220 +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())
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())