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