@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.
- package/CHANGELOG.md +76 -0
- package/README.md +9 -3
- package/bin/tms-skills.js +200 -160
- package/package.json +12 -9
- package/top-ppt-html/README.md +162 -254
- package/top-ppt-html/SKILL.md +126 -111
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
- package/top-ppt-html/assets/pptx-export.js +1944 -1944
- package/top-ppt-html/assets/style-gallery.html +589 -589
- package/top-ppt-html/assets/templates/architecture.html +3728 -3728
- package/top-ppt-html/assets/templates/engine.css +840 -840
- package/top-ppt-html/assets/templates/presentation.html +3738 -3738
- package/top-ppt-html/assets/templates/research.html +4017 -4017
- package/top-ppt-html/assets/templates/ui.js +520 -520
- package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
- package/top-ppt-html/assets/theme-overview-research.png +0 -0
- package/top-ppt-html/assets/theme-overview.png +0 -0
- package/top-ppt-html/evals/prompts.csv +15 -15
- package/top-ppt-html/evals/rubric.schema.json +25 -25
- package/top-ppt-html/evals/run_evals.py +220 -220
- package/top-ppt-html/evals/trace.example.json +16 -16
- package/top-ppt-html/package-lock.json +186 -0
- package/top-ppt-html/package.json +11 -11
- package/top-ppt-html/references/charts-basic.md +624 -624
- package/top-ppt-html/references/charts-discipline.md +110 -108
- package/top-ppt-html/references/charts-extended.md +482 -482
- package/top-ppt-html/references/charts.md +37 -28
- package/top-ppt-html/references/components-atoms.md +624 -624
- package/top-ppt-html/references/components.md +30 -30
- package/top-ppt-html/references/content-rules.md +510 -490
- package/top-ppt-html/references/default-surface.md +61 -0
- package/top-ppt-html/references/design-system.md +478 -478
- package/top-ppt-html/references/failure-modes.md +227 -214
- package/top-ppt-html/references/high-fidelity.md +127 -127
- package/top-ppt-html/references/icons.md +176 -397
- package/top-ppt-html/references/infographics-stats.md +308 -308
- package/top-ppt-html/references/infographics-structure.md +226 -226
- package/top-ppt-html/references/infographics.md +43 -43
- package/top-ppt-html/references/layout-grammar.md +325 -315
- package/top-ppt-html/references/layouts-architecture.md +108 -108
- package/top-ppt-html/references/layouts-combo.md +600 -600
- package/top-ppt-html/references/layouts-research.md +160 -160
- package/top-ppt-html/references/modes.md +259 -254
- package/top-ppt-html/references/outline-design.md +275 -275
- package/top-ppt-html/references/playbook.md +284 -266
- package/top-ppt-html/references/pptx-export.md +227 -209
- package/top-ppt-html/references/presentation-craft.md +69 -0
- package/top-ppt-html/references/styles.md +370 -370
- package/top-ppt-html/references/tech-design.md +138 -138
- package/top-ppt-html/scripts/audit_css.py +109 -109
- package/top-ppt-html/scripts/audit_docs.py +176 -176
- package/top-ppt-html/scripts/audit_skill.py +231 -220
- package/top-ppt-html/scripts/audit_styles.py +351 -351
- package/top-ppt-html/scripts/build_examples.py +55 -2276
- package/top-ppt-html/scripts/build_pptx.js +2386 -2380
- package/top-ppt-html/scripts/capture_theme_overview.js +79 -78
- package/top-ppt-html/scripts/checks_html.py +166 -127
- package/top-ppt-html/scripts/cross_verify.py +298 -294
- package/top-ppt-html/scripts/env_probe.py +158 -158
- package/top-ppt-html/scripts/extract_model.py +210 -210
- package/top-ppt-html/scripts/extract_snippet.py +374 -374
- package/top-ppt-html/scripts/gen_channel_a.js +214 -214
- package/top-ppt-html/scripts/layout-constants.json +111 -18
- package/top-ppt-html/scripts/lib_layout_regions.js +410 -412
- package/top-ppt-html/scripts/measure_height.py +178 -178
- package/top-ppt-html/scripts/negative_tests.py +433 -307
- package/top-ppt-html/scripts/package_skill.py +294 -291
- package/top-ppt-html/scripts/prepare_images.py +341 -341
- package/top-ppt-html/scripts/probe_image_export.py +187 -188
- package/top-ppt-html/scripts/quality_gate.py +309 -301
- package/top-ppt-html/scripts/recommend_layout.py +384 -0
- package/top-ppt-html/scripts/regression.py +308 -308
- package/top-ppt-html/scripts/render_compare.py +275 -275
- package/top-ppt-html/scripts/render_from_model.py +698 -698
- package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
- package/top-ppt-html/scripts/section-file-map.json +104 -104
- package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
- package/top-ppt-html/scripts/sync_runtime.py +659 -662
- package/top-ppt-html/scripts/validate_pptx.py +1579 -1510
- package/top-ppt-html/scripts/validate_report.py +1755 -1456
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
- package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
- package/top-ppt-html/references/design-system-engine.md +0 -235
- package/top-ppt-html/references/industry-benchmark.md +0 -105
- package/top-ppt-html/references/reform-plan.md +0 -252
- 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
|
-
|
|
166
|
-
chk('④ Gate 0
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
print('
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 参考图先行」,且其出现位置在「六项问询」之前;若声明 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())
|