@topmindspace/tms-skills 0.1.0 → 0.1.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 (102) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +9 -3
  3. package/bin/tms-skills.js +200 -160
  4. package/package.json +12 -9
  5. package/top-ppt-html/README.md +162 -254
  6. package/top-ppt-html/SKILL.md +126 -111
  7. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -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-presentation-business-blue.html +4325 -4325
  10. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
  11. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -5527
  12. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
  13. package/top-ppt-html/assets/pptx-export.js +1944 -1944
  14. package/top-ppt-html/assets/style-gallery.html +589 -589
  15. package/top-ppt-html/assets/templates/architecture.html +3728 -3728
  16. package/top-ppt-html/assets/templates/engine.css +840 -840
  17. package/top-ppt-html/assets/templates/presentation.html +3738 -3738
  18. package/top-ppt-html/assets/templates/research.html +4017 -4017
  19. package/top-ppt-html/assets/templates/ui.js +520 -520
  20. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  21. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  22. package/top-ppt-html/assets/theme-overview.png +0 -0
  23. package/top-ppt-html/evals/prompts.csv +15 -15
  24. package/top-ppt-html/evals/rubric.schema.json +25 -25
  25. package/top-ppt-html/evals/run_evals.py +220 -220
  26. package/top-ppt-html/evals/trace.example.json +16 -16
  27. package/top-ppt-html/package-lock.json +186 -0
  28. package/top-ppt-html/package.json +11 -11
  29. package/top-ppt-html/references/charts-basic.md +624 -624
  30. package/top-ppt-html/references/charts-discipline.md +110 -108
  31. package/top-ppt-html/references/charts-extended.md +482 -482
  32. package/top-ppt-html/references/charts.md +37 -28
  33. package/top-ppt-html/references/components-atoms.md +624 -624
  34. package/top-ppt-html/references/components.md +30 -30
  35. package/top-ppt-html/references/content-rules.md +510 -490
  36. package/top-ppt-html/references/default-surface.md +61 -0
  37. package/top-ppt-html/references/design-system.md +478 -478
  38. package/top-ppt-html/references/failure-modes.md +227 -214
  39. package/top-ppt-html/references/high-fidelity.md +127 -127
  40. package/top-ppt-html/references/icons.md +176 -397
  41. package/top-ppt-html/references/infographics-stats.md +308 -308
  42. package/top-ppt-html/references/infographics-structure.md +226 -226
  43. package/top-ppt-html/references/infographics.md +43 -43
  44. package/top-ppt-html/references/layout-grammar.md +325 -315
  45. package/top-ppt-html/references/layouts-architecture.md +108 -108
  46. package/top-ppt-html/references/layouts-combo.md +600 -600
  47. package/top-ppt-html/references/layouts-research.md +160 -160
  48. package/top-ppt-html/references/modes.md +259 -254
  49. package/top-ppt-html/references/outline-design.md +275 -275
  50. package/top-ppt-html/references/playbook.md +284 -266
  51. package/top-ppt-html/references/pptx-export.md +227 -209
  52. package/top-ppt-html/references/presentation-craft.md +69 -0
  53. package/top-ppt-html/references/styles.md +370 -370
  54. package/top-ppt-html/references/tech-design.md +138 -138
  55. package/top-ppt-html/scripts/audit_css.py +109 -109
  56. package/top-ppt-html/scripts/audit_docs.py +176 -176
  57. package/top-ppt-html/scripts/audit_skill.py +231 -220
  58. package/top-ppt-html/scripts/audit_styles.py +351 -351
  59. package/top-ppt-html/scripts/build_examples.py +55 -2276
  60. package/top-ppt-html/scripts/build_pptx.js +2386 -2380
  61. package/top-ppt-html/scripts/capture_theme_overview.js +79 -78
  62. package/top-ppt-html/scripts/checks_html.py +166 -127
  63. package/top-ppt-html/scripts/cross_verify.py +298 -294
  64. package/top-ppt-html/scripts/env_probe.py +158 -158
  65. package/top-ppt-html/scripts/extract_model.py +210 -210
  66. package/top-ppt-html/scripts/extract_snippet.py +374 -374
  67. package/top-ppt-html/scripts/gen_channel_a.js +214 -214
  68. package/top-ppt-html/scripts/layout-constants.json +111 -18
  69. package/top-ppt-html/scripts/lib_layout_regions.js +410 -412
  70. package/top-ppt-html/scripts/measure_height.py +178 -178
  71. package/top-ppt-html/scripts/negative_tests.py +433 -307
  72. package/top-ppt-html/scripts/package_skill.py +294 -291
  73. package/top-ppt-html/scripts/prepare_images.py +341 -341
  74. package/top-ppt-html/scripts/probe_image_export.py +187 -188
  75. package/top-ppt-html/scripts/quality_gate.py +309 -301
  76. package/top-ppt-html/scripts/recommend_layout.py +384 -0
  77. package/top-ppt-html/scripts/regression.py +308 -308
  78. package/top-ppt-html/scripts/render_compare.py +275 -275
  79. package/top-ppt-html/scripts/render_from_model.py +698 -698
  80. package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
  81. package/top-ppt-html/scripts/section-file-map.json +104 -104
  82. package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
  83. package/top-ppt-html/scripts/sync_runtime.py +659 -662
  84. package/top-ppt-html/scripts/validate_pptx.py +1579 -1510
  85. package/top-ppt-html/scripts/validate_report.py +1755 -1456
  86. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
  87. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
  88. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
  89. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
  90. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
  91. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
  92. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
  93. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
  94. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
  95. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
  96. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
  97. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
  98. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  99. package/top-ppt-html/references/design-system-engine.md +0 -235
  100. package/top-ppt-html/references/industry-benchmark.md +0 -105
  101. package/top-ppt-html/references/reform-plan.md +0 -252
  102. package/top-ppt-html/scripts/layout_slots.json +0 -830
@@ -1,220 +1,231 @@
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 参考图先行」,且其出现位置在「六项问询」之前;若声明 Fast Mode,须同时有豁免句且标准路径仍为硬门禁
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(参考图先行)——标准路径硬门禁;Fast Mode 可声明豁免
163
+ g0 = sk.find('Gate 0')
164
+ ask = sk.find('六项问询')
165
+ has_fast = bool(re.search(r'Fast\s*Mode|快速模式', sk))
166
+ chk('④ Gate 0 存在', g0 >= 0, '未找到「Gate 0」标记', '已就位')
167
+ chk('④ Gate 0 在问询之前', g0 >= 0 and ask >= 0 and g0 < ask,
168
+ f'Gate0@{g0} 未在六项问询@{ask} 之前', f'位置正确({g0} < {ask})')
169
+ if has_fast:
170
+ ok_exempt = bool(re.search(
171
+ r'(?:Fast\s*Mode|快速模式).{0,400}(?:跳过|豁免).{0,120}(?:Gate\s*0|参考图|六项)',
172
+ sk, re.S)) or bool(re.search(
173
+ r'(?:跳过|豁免).{0,60}(?:Gate\s*0|参考图|六项问询)', sk))
174
+ chk('④ Fast Mode 豁免声明', ok_exempt,
175
+ '有 Fast Mode 但未声明 Gate 0/六项豁免', '已声明豁免')
176
+ ok_std = bool(re.search(r'标准.{0,24}(?:硬门禁|强制)|Gate\s*0.{0,48}硬门禁', sk))
177
+ chk('④ 标准路径 Gate 0 仍为硬门禁', ok_std,
178
+ '有 Fast Mode 但标准路径未保留 Gate 0 硬门禁表述', '已保留')
179
+
180
+ # ⑤ 效率预算声明
181
+ chk('⑤ 交互轮次预算', bool(re.search(r'交互轮次|轮次上限|≤\s*3\s*轮', sk)),
182
+ 'SKILL.md 未声明交互轮次上限', '已声明(≤3 轮)')
183
+ chk('⑤ 必读文件预算', bool(re.search(r'必读.{0,6}上限|必读文件数|只读当前需要的|不预读', sk)),
184
+ 'SKILL.md 未声明必读文件预算/不预读纪律', '已声明(不预读纪律)')
185
+
186
+ # ⑥ 引用完整性
187
+ missing: list[str] = []
188
+ n_refs = 0
189
+ for src, text in (('SKILL.md', sk),
190
+ ('references/playbook.md',
191
+ PLAYBOOK.read_text(encoding='utf-8') if PLAYBOOK.exists() else '')):
192
+ for rel in sorted(set(re.findall(r'references/([a-z0-9\-]+\.md)', text))):
193
+ n_refs += 1
194
+ if not (REF / rel).exists():
195
+ missing.append(f'{src} → references/{rel}')
196
+ chk('⑥ 引用文件存在', not missing, '; '.join(missing),
197
+ f'{n_refs} 处引用全部存在')
198
+
199
+ # ⑦ 冗余报告(SKILL.md ↔ 全部 references,含 playbook;归一化滑窗子串)
200
+ refs_all = '\n'.join(p.read_text(encoding='utf-8') for p in sorted(REF.glob('*.md')))
201
+ dup = dup_spans(sk, refs_all)
202
+ chk('⑦ 内容重复(SKILL.md ↔ references · 归一化 ≥%d 字)' % DUP_WIN,
203
+ len(dup) <= DUP_MAX,
204
+ f'{len(dup)} 处重复' + (f':{dup[0][:40]}…' if dup else ''),
205
+ f'0 处重复(≥{DUP_WIN} 字归一化片段)')
206
+
207
+ # ── 输出 ────────────────────────────────────────────────────────────────
208
+ if AS_JSON:
209
+ print(json.dumps({'rows': [{'item': r[0], 'ok': r[1], 'detail': r[2]} for r in rows],
210
+ 'errors': errs, 'notes': notes}, ensure_ascii=False, indent=2))
211
+ return 1 if errs else 0
212
+ if not QUIET:
213
+ print('技能工程审计 · 效率预算与门禁\n' + '-' * 56)
214
+ for n, ok, det in rows:
215
+ print(f' {"✓" if ok else "✗"} {n}:{det}')
216
+ if notes:
217
+ print('\n提示(不判失败):')
218
+ for n in notes:
219
+ print(f' · {n}')
220
+ print('-' * 56)
221
+ if errs:
222
+ print(f'技能工程审计未通过:{len(errs)} 项')
223
+ for e in errs:
224
+ print(f' ✗ {e}')
225
+ return 1
226
+ print('技能工程审计通过:体积达标 · 元数据合规 · 三档披露 · Gate 0 就位 · 预算已声明')
227
+ return 0
228
+
229
+
230
+ if __name__ == '__main__':
231
+ sys.exit(main())