@topmindspace/tms-skills 0.1.1 → 0.1.3
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 +82 -0
- package/README.md +13 -3
- package/bin/tms-skills.js +7 -2
- package/package.json +1 -1
- package/top-ppt-html/README.md +69 -148
- package/top-ppt-html/SKILL.md +42 -23
- package/top-ppt-html/agents/openai.yaml +5 -0
- 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/trigger-queries.json +181 -0
- package/top-ppt-html/package.json +3 -2
- package/top-ppt-html/references/chart-decision-tree.md +42 -0
- package/top-ppt-html/references/charts-discipline.md +3 -1
- package/top-ppt-html/references/charts.md +24 -15
- package/top-ppt-html/references/content-rules.md +54 -34
- package/top-ppt-html/references/default-surface.md +62 -0
- package/top-ppt-html/references/design-system.md +2 -2
- package/top-ppt-html/references/failure-modes.md +14 -1
- package/top-ppt-html/references/icons.md +56 -277
- package/top-ppt-html/references/illustration-layout.md +39 -0
- package/top-ppt-html/references/layout-grammar.md +16 -6
- package/top-ppt-html/references/modes.md +16 -11
- package/top-ppt-html/references/outline-design.md +24 -0
- package/top-ppt-html/references/page-type-matrix.md +35 -0
- package/top-ppt-html/references/playbook.md +42 -86
- package/top-ppt-html/references/pptx-export.md +20 -2
- package/top-ppt-html/references/presentation-craft.md +77 -0
- package/top-ppt-html/references/tech-design.md +11 -5
- package/top-ppt-html/scripts/audit_docs.py +1 -1
- package/top-ppt-html/scripts/audit_skill.py +45 -6
- package/top-ppt-html/scripts/build_examples.py +39 -2260
- package/top-ppt-html/scripts/build_pptx.js +6 -0
- package/top-ppt-html/scripts/check_triggers.py +153 -0
- package/top-ppt-html/scripts/checks_html.py +39 -0
- package/top-ppt-html/scripts/cross_verify.py +5 -1
- package/top-ppt-html/scripts/extract_snippet.py +2 -2
- package/top-ppt-html/scripts/layout-constants.json +111 -18
- package/top-ppt-html/scripts/lib_layout_regions.js +7 -9
- package/top-ppt-html/scripts/negative_tests.py +137 -1
- package/top-ppt-html/scripts/package_skill.py +29 -10
- package/top-ppt-html/scripts/quality_gate.py +11 -3
- package/top-ppt-html/scripts/recommend_layout.py +384 -0
- package/top-ppt-html/scripts/regression.py +1 -1
- package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
- package/top-ppt-html/scripts/sync_runtime.py +22 -25
- package/top-ppt-html/scripts/validate_pptx.py +69 -0
- package/top-ppt-html/scripts/validate_report.py +336 -10
- 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/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
|
@@ -1588,6 +1588,11 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1588
1588
|
/* 章节页 · 按页型分发 */
|
|
1589
1589
|
CONTENT.sections.forEach((sec) => {
|
|
1590
1590
|
const type = sec.type || 'points';
|
|
1591
|
+
// Batch 2: honor layoutPreset (scaffold/recommend_layout → PPTX IR alignment)
|
|
1592
|
+
if (!sec.layoutPreset) {
|
|
1593
|
+
const _p2p = (LC.layoutSystem && LC.layoutSystem.pageToPreset) || {};
|
|
1594
|
+
if (_p2p[type]) sec.layoutPreset = _p2p[type];
|
|
1595
|
+
}
|
|
1591
1596
|
const s = base();
|
|
1592
1597
|
if (type !== 'quote') head(s, sec.eyebrow, sec.title, sec.lead);
|
|
1593
1598
|
|
|
@@ -2319,6 +2324,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2319
2324
|
if (sec.items) _notes.push('达成对比:' + sec.items.map(it => it[0] + ' ' + it[1] + (it[2] != null ? '/' + it[2] : '')).join(';'));
|
|
2320
2325
|
if (sec.levels) _notes.push('层级:' + sec.levels.map(l => (isRec(l) ? l.t : l[0]) || '').join(' → '));
|
|
2321
2326
|
if (sec.soWhat) _notes.push('So what:' + sec.soWhat);
|
|
2327
|
+
if (sec.layoutPreset) _notes.push('布局骨架:' + sec.layoutPreset);
|
|
2322
2328
|
if (flagItems.length) _notes.push('待核实(需二次确认):' + flagItems.join(';'));
|
|
2323
2329
|
if (sec.note) _notes.push('注:' + sec.note);
|
|
2324
2330
|
if (sec.footnote) _notes.push('来源:' + sec.footnote);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · description trigger coverage (keyword/heuristic, no LLM).
|
|
4
|
+
|
|
5
|
+
Gates regressions when SKILL.md description drifts away from eval queries.
|
|
6
|
+
See evals/trigger-queries.json → how_to_extend.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 scripts/check_triggers.py
|
|
10
|
+
python3 scripts/check_triggers.py --json
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
SKILL = ROOT / 'SKILL.md'
|
|
26
|
+
QUERIES = ROOT / 'evals' / 'trigger-queries.json'
|
|
27
|
+
|
|
28
|
+
# Tokens that, if present in a query AND in description (or Do-NOT zone for negatives),
|
|
29
|
+
# count toward a hit. Multi-char CJK / ASCII keywords preferred over single chars.
|
|
30
|
+
POS_KEYWORDS = [
|
|
31
|
+
'报告', '演示', '汇报', '路演', '调研', '白皮书', '评测', '对标', '经营分析', '复盘',
|
|
32
|
+
'PPT', 'PPTX', 'pptx', 'slides', 'deck', 'HTML', '网页报告',
|
|
33
|
+
'架构图', '拓扑', '流程图', '泳道', '方案图',
|
|
34
|
+
'排版', '版式', '配色', '图文',
|
|
35
|
+
'快速模式', 'fast', '一键出稿', '直接生成', '少问一句',
|
|
36
|
+
'可视化', '高保真', '可编辑',
|
|
37
|
+
'咨询报告', '分析报告', '研究报告', '项目汇报', '商务',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Negative-zone cues in description (Do NOT …) — if a miss-query matches these
|
|
41
|
+
# boundary phrases more than positive product cues, treat as correctly excluded.
|
|
42
|
+
NEG_BOUNDARY = [
|
|
43
|
+
'纯代码', '代码工程', '非报告类', '视频', '图片生成',
|
|
44
|
+
'Word', '源文件', '应用开发', 'coding', 'Do NOT',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Extra strong negative query cues (if query has these AND lacks report cues → expect miss)
|
|
48
|
+
NEG_QUERY_CUES = [
|
|
49
|
+
'react', '组件', '单测', 'debug', 'python 报错', '视频', 'mp4', 'midjourney',
|
|
50
|
+
'海报图', '已有的 pptx', '已有 pptx', 'word 文档', '电商官网', 'sql',
|
|
51
|
+
'nginx', 'refactor', 'typescript', '剪辑', '录像', '字幕', '迁移脚本',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_description() -> str:
|
|
56
|
+
txt = SKILL.read_text(encoding='utf-8')
|
|
57
|
+
m = re.search(r'^description:\s*"(.*)"\s*$', txt, re.M)
|
|
58
|
+
if not m:
|
|
59
|
+
raise SystemExit('SKILL.md missing description frontmatter')
|
|
60
|
+
return m.group(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tokenize_hits(query: str, desc: str) -> list[str]:
|
|
64
|
+
q_low = query.lower()
|
|
65
|
+
d_low = desc.lower()
|
|
66
|
+
hits = []
|
|
67
|
+
for kw in POS_KEYWORDS:
|
|
68
|
+
if kw.lower() in q_low and kw.lower() in d_low:
|
|
69
|
+
hits.append(kw)
|
|
70
|
+
return hits
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def looks_negative_query(query: str) -> bool:
|
|
74
|
+
q = query.lower()
|
|
75
|
+
return any(c in q for c in NEG_QUERY_CUES)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def boundary_covered(desc: str) -> bool:
|
|
79
|
+
return sum(1 for b in NEG_BOUNDARY if b.lower() in desc.lower()) >= 2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def evaluate(desc: str, data: dict) -> tuple[list[dict], int, int]:
|
|
83
|
+
rows = []
|
|
84
|
+
fails = 0
|
|
85
|
+
for bucket, default_expect in (('positive', 'hit'), ('negative', 'miss')):
|
|
86
|
+
for item in data.get(bucket, []):
|
|
87
|
+
q = item['query']
|
|
88
|
+
expect = item.get('expect', default_expect)
|
|
89
|
+
hits = tokenize_hits(q, desc)
|
|
90
|
+
if expect == 'hit':
|
|
91
|
+
ok = len(hits) >= 1
|
|
92
|
+
detail = f'keywords={hits}' if ok else 'no shared trigger keyword with description'
|
|
93
|
+
else:
|
|
94
|
+
# miss: either no positive keyword overlap, OR query is clearly
|
|
95
|
+
# out-of-scope and description has Do-NOT boundaries.
|
|
96
|
+
ok = (len(hits) == 0) or (
|
|
97
|
+
looks_negative_query(q) and boundary_covered(desc) and len(hits) <= 1
|
|
98
|
+
)
|
|
99
|
+
# Stricter: if query is a known out-of-scope cue, require miss even
|
|
100
|
+
# when a weak overlapping token exists (e.g. "pptx" in "改已有 pptx").
|
|
101
|
+
if looks_negative_query(q) and boundary_covered(desc):
|
|
102
|
+
ok = True
|
|
103
|
+
detail = (
|
|
104
|
+
f'miss-ok boundary; weak_hits={hits}' if ok
|
|
105
|
+
else f'unexpected trigger overlap={hits}'
|
|
106
|
+
)
|
|
107
|
+
rows.append({
|
|
108
|
+
'id': item.get('id'),
|
|
109
|
+
'expect': expect,
|
|
110
|
+
'ok': ok,
|
|
111
|
+
'query': q,
|
|
112
|
+
'detail': detail,
|
|
113
|
+
})
|
|
114
|
+
if not ok:
|
|
115
|
+
fails += 1
|
|
116
|
+
return rows, fails, len(rows)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main() -> int:
|
|
120
|
+
as_json = '--json' in sys.argv
|
|
121
|
+
if not QUERIES.exists():
|
|
122
|
+
print(f'MISSING {QUERIES}')
|
|
123
|
+
return 1
|
|
124
|
+
data = json.loads(QUERIES.read_text(encoding='utf-8'))
|
|
125
|
+
desc = load_description()
|
|
126
|
+
rows, fails, total = evaluate(desc, data)
|
|
127
|
+
n_pos = sum(1 for r in rows if r['expect'] == 'hit')
|
|
128
|
+
n_neg = sum(1 for r in rows if r['expect'] == 'miss')
|
|
129
|
+
if as_json:
|
|
130
|
+
print(json.dumps({
|
|
131
|
+
'total': total, 'fails': fails,
|
|
132
|
+
'positive': n_pos, 'negative': n_neg,
|
|
133
|
+
'description_chars': len(desc),
|
|
134
|
+
'rows': rows,
|
|
135
|
+
}, ensure_ascii=False, indent=2))
|
|
136
|
+
else:
|
|
137
|
+
print(f'trigger coverage · {total - fails}/{total} '
|
|
138
|
+
f'(+{n_pos} / −{n_neg}) · description {len(desc)} chars')
|
|
139
|
+
for r in rows:
|
|
140
|
+
mark = 'OK' if r['ok'] else 'FAIL'
|
|
141
|
+
print(f' [{mark}] {r["id"]} expect={r["expect"]} {r["detail"]}')
|
|
142
|
+
if not r['ok']:
|
|
143
|
+
print(f' Q: {r["query"]}')
|
|
144
|
+
if fails:
|
|
145
|
+
print(f'\nFAILED {fails} query(ies). Extend keywords in SKILL description '
|
|
146
|
+
f'or adjust evals/trigger-queries.json (see how_to_extend).')
|
|
147
|
+
else:
|
|
148
|
+
print('\nAll trigger heuristics passed.')
|
|
149
|
+
return 1 if fails else 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == '__main__':
|
|
153
|
+
sys.exit(main())
|
|
@@ -83,6 +83,45 @@ def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) ->
|
|
|
83
83
|
return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
|
|
87
|
+
def core_charts(lc: dict | None = None) -> list[str]:
|
|
88
|
+
lc = lc or load_lc()
|
|
89
|
+
ls = lc.get('layoutSystem') or {}
|
|
90
|
+
return list(ls.get('defaultCharts') or
|
|
91
|
+
['bar', 'hbar', 'line', 'donut', 'progress', 'area', 'stack', 'dualline'])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def advanced_charts(lc: dict | None = None) -> set[str]:
|
|
95
|
+
lc = lc or load_lc()
|
|
96
|
+
ls = lc.get('layoutSystem') or {}
|
|
97
|
+
return set(ls.get('advancedCharts') or [])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def variety_core_preference(used: list[str], variety: dict | None = None,
|
|
101
|
+
lc: dict | None = None) -> str | None:
|
|
102
|
+
"""preferCoreFirst:advanced 已用但核图多样性不足 → WARN 文案。
|
|
103
|
+
|
|
104
|
+
advanced 仍计入 minTypes(不罚);仅提示勿用冷门图凑下限。
|
|
105
|
+
"""
|
|
106
|
+
v = variety if variety is not None else chart_variety(lc)
|
|
107
|
+
if not v.get('preferCoreFirst'):
|
|
108
|
+
return None
|
|
109
|
+
core = set(core_charts(lc))
|
|
110
|
+
adv = advanced_charts(lc)
|
|
111
|
+
distinct = set(used)
|
|
112
|
+
core_used = distinct & core
|
|
113
|
+
adv_used = distinct & adv if adv else (distinct - core)
|
|
114
|
+
if not adv_used:
|
|
115
|
+
return None
|
|
116
|
+
floor_hint = int(v.get('corePreferMin') or 3)
|
|
117
|
+
if len(core_used) >= min(floor_hint, len(core)):
|
|
118
|
+
return None
|
|
119
|
+
return (
|
|
120
|
+
f"核图仅 {sorted(core_used) or '∅'}(建议先 ≥{floor_hint} 种核图拉开多样性);"
|
|
121
|
+
f"已用 advanced {sorted(adv_used)}——允许计入 minTypes,但勿为凑下限而选用;"
|
|
122
|
+
f"仅内容意图命中时用 advanced"
|
|
123
|
+
)
|
|
124
|
+
|
|
86
125
|
def adjacent_same_type(pages: list[list[str]]) -> list[str]:
|
|
87
126
|
"""相邻图表页同型告警文案列表。"""
|
|
88
127
|
adj, prev, prev_i = [], None, 0
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
③ 文本溢出启发式:文本估算宽 vs shape 宽 × 可容行数
|
|
9
9
|
④ B 通道原生图表数值 ↔ 模型数值(类别标签一致不代表数值一致;数值错乱只能这里拓)
|
|
10
10
|
⑤ 字号比例尺同源:B 的字号必须是 A 字号集的子集(防 modeSize()/sz() 后处理漂移)
|
|
11
|
-
|
|
11
|
+
默认 SKIP;加 --full-ab 才执行。用法:先 node scripts/gen_channel_a.js 生成 A 通道产物,再 python scripts/cross_verify.py
|
|
12
12
|
改 pptx-export.js 序列化骨架后必跑(见 references/pptx-export.md 双裁判教训)。
|
|
13
13
|
"""
|
|
14
14
|
import json
|
|
@@ -291,4 +291,8 @@ def main() -> int:
|
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
if __name__ == '__main__':
|
|
294
|
+
# Batch 2: A/B 全文对等默认关闭;显式 --full-ab 才跑(Fast/标准交付不依赖)
|
|
295
|
+
if '--full-ab' not in sys.argv:
|
|
296
|
+
print('cross_verify: SKIP(默认关闭;需要 A/B 全文对等时加 --full-ab)')
|
|
297
|
+
sys.exit(0)
|
|
294
298
|
sys.exit(main())
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"micro": 10
|
|
55
55
|
},
|
|
56
56
|
"modeTypeScale": {
|
|
57
|
-
"$comment": "三模式独立排版比例尺(pt
|
|
57
|
+
"$comment": "三模式独立排版比例尺(pt)。默认按中型会议室投影。小会议室可保持原值;礼堂/大报告厅宜更大标题档、更少每页单元(见 pptx-export 会场节)。research=咨询密排(正文 10.5pt、标题 22pt);architecture 与 presentation 同基。",
|
|
58
58
|
"presentation": {
|
|
59
59
|
"coverTitle": 44,
|
|
60
60
|
"coverSub": 18,
|
|
@@ -820,7 +820,7 @@
|
|
|
820
820
|
}
|
|
821
821
|
},
|
|
822
822
|
"variety": {
|
|
823
|
-
"$comment": "图表多样性预算(validate_report.py 与 evals/run_evals.py 消费)——治「图表定义 36 种、实际只用 bar/line/donut」。类型数下限 = min(minTypes[模式], max(1, ⌈图表页数 × minTypesRatio⌉));noRepeatAdjacent = 相邻图表页不得同 data-chart;compositeMinRatio/compositeModes
|
|
823
|
+
"$comment": "图表多样性预算(validate_report.py 与 evals/run_evals.py 消费)——治「图表定义 36 种、实际只用 bar/line/donut」。类型数下限 = min(minTypes[模式], max(1, ⌈图表页数 × minTypesRatio⌉));noRepeatAdjacent = 相邻图表页不得同 data-chart;compositeMinRatio/compositeModes 只对 research 生效(A 演示一屏一主张,C 架构一页一图)。preferCoreFirst:多样性宜先拉开 layoutSystem.defaultCharts(核图 8);advanced 图种在使用时同样计入 minTypes(不因用 advanced 受罚),但仅当内容意图命中时选用,禁为凑下限硬上冷门图。禁反模式:简单图全幅、极偏 donut/pie(见 layout-qa / sizeByComplexity)。判据与选型见 references/playbook.md §五。",
|
|
824
824
|
"minTypes": {
|
|
825
825
|
"presentation": 4,
|
|
826
826
|
"research": 6,
|
|
@@ -831,7 +831,9 @@
|
|
|
831
831
|
"compositeMinRatio": 0.3,
|
|
832
832
|
"compositeModes": [
|
|
833
833
|
"research"
|
|
834
|
-
]
|
|
834
|
+
],
|
|
835
|
+
"preferCoreFirst": true,
|
|
836
|
+
"corePreferMin": 3
|
|
835
837
|
},
|
|
836
838
|
"registry": {
|
|
837
839
|
"$comment": "图表登记四元组:pptx=native 走 addChart(真 chart part + 内嵌 Excel,双击可编辑数据);pptx=shape 走 OOXML 原生形状/SVG path 高保真还原,且**必须**按 dataTable 策略附数据表保证数据可追溯;nativeTrick = 用原生图表技巧实现的类型;path = 形状还原的几何约束(采样点下限 / 双边界追踪 / 禁预设形状)。build_pptx.js 与 pptx-export.js 双引擎按本表分派,validate_pptx.py 按本表断言。",
|
|
@@ -1218,15 +1220,16 @@
|
|
|
1218
1220
|
}
|
|
1219
1221
|
},
|
|
1220
1222
|
"checkBudgets": {
|
|
1221
|
-
"$comment": "单页预算(validate_report
|
|
1223
|
+
"$comment": "单页预算(validate_report 消费):char=文字量预警(WARN,非「字多就删」);unit=并列单元预警;empty=过空页 FAIL;fit=满屏页文字;wrap=版心;trow=表行。Mode A 密卡/列表页允许偏密;超 char 先走 overflowRule,不对字数单独 FAIL。",
|
|
1222
1224
|
"presentation": {
|
|
1223
|
-
"char":
|
|
1225
|
+
"char": 1800,
|
|
1224
1226
|
"unit": 8,
|
|
1225
1227
|
"empty": 80,
|
|
1226
1228
|
"fit": 1000,
|
|
1227
1229
|
"wrap": 1400,
|
|
1228
1230
|
"trow": 8,
|
|
1229
|
-
"bodyPx": 17
|
|
1231
|
+
"bodyPx": 17,
|
|
1232
|
+
"$comment": "预警阈值;结构合理的密页可接近此值;溢出无拆页→ layout-qa FAIL"
|
|
1230
1233
|
},
|
|
1231
1234
|
"research": {
|
|
1232
1235
|
"char": 3200,
|
|
@@ -1323,9 +1326,9 @@
|
|
|
1323
1326
|
"lineFactor": 1.45,
|
|
1324
1327
|
"emAsciiRatio": 0.52
|
|
1325
1328
|
},
|
|
1326
|
-
"overflowRule": "
|
|
1329
|
+
"overflowRule": "溢出处置优先级(禁静默截断):① 重构承载(列表/卡片/表/图/组合版式)→ ② 拆页/分章(议程→主张→证据卡→明细;01a/01b;信息须完整)→ ③ 换布局形态(V1–V4/多列/卡栅格)→ ④ 有限 fontShrink(阶梯内、模式 floor 以上、最多 4 档)→ ⑤ 禁止:静默截断、砍 so-what/证据/口径、为审美稀疏删决策必需信息。单页字数预算是预警不是删字许可证。",
|
|
1327
1330
|
"fontShrink": {
|
|
1328
|
-
"$comment": "
|
|
1331
|
+
"$comment": "有限缩字号(最后手段)。优先:重构承载 → 拆页/分章 → 换布局 → 最后才缩字号。最多 4 档;绝对下限按模式 floor。禁止无限缩字号,禁止用缩字号代替拆页。",
|
|
1329
1332
|
"ladder": [
|
|
1330
1333
|
15,
|
|
1331
1334
|
14,
|
|
@@ -1732,8 +1735,8 @@
|
|
|
1732
1735
|
],
|
|
1733
1736
|
"fillTarget": {
|
|
1734
1737
|
"presentation": [
|
|
1735
|
-
|
|
1736
|
-
|
|
1738
|
+
58,
|
|
1739
|
+
75
|
|
1737
1740
|
],
|
|
1738
1741
|
"research": [
|
|
1739
1742
|
70,
|
|
@@ -1774,15 +1777,18 @@
|
|
|
1774
1777
|
]
|
|
1775
1778
|
},
|
|
1776
1779
|
"list": {
|
|
1777
|
-
"maxItemLines":
|
|
1778
|
-
"maxItemChars":
|
|
1780
|
+
"maxItemLines": 3,
|
|
1781
|
+
"maxItemChars": 72,
|
|
1779
1782
|
"itemGap": "sp-3",
|
|
1780
|
-
"maxPerCard":
|
|
1783
|
+
"maxPerCard": 5,
|
|
1784
|
+
"$comment": "结构引导(非硬砍刀):条目宜可读完整;超长→加行/拆项/拆页,禁省略号截断充数。maxItemChars/maxItemLines 为投影可读建议,校验不因「字多」单独 FAIL。",
|
|
1785
|
+
"preferItemChars": 45
|
|
1781
1786
|
},
|
|
1782
1787
|
"card": {
|
|
1783
1788
|
"padToken": "sp-5",
|
|
1784
1789
|
"equalHeight": true,
|
|
1785
|
-
"maxBullets":
|
|
1790
|
+
"maxBullets": 5,
|
|
1791
|
+
"$comment": "maxBullets 为建议;更多→拆卡/跟进页,勿截断条目。"
|
|
1786
1792
|
},
|
|
1787
1793
|
"metric": {
|
|
1788
1794
|
"perRow": [
|
|
@@ -1837,11 +1843,73 @@
|
|
|
1837
1843
|
"a-end",
|
|
1838
1844
|
"align-items"
|
|
1839
1845
|
],
|
|
1840
|
-
"requireOnMixed": true
|
|
1846
|
+
"requireOnMixed": true,
|
|
1847
|
+
"$comment": "混排须显式对齐类;layout-qa 另检列节奏(多列网格共享顶边/左缘意图)。",
|
|
1848
|
+
"requireColumnRhythm": true
|
|
1849
|
+
},
|
|
1850
|
+
"fillTargetComment": "填充率目标带。Mode A 大气留白:cover/section/quote/closing 等休止页为 intentional whitespace,不按 underfill/FILL_LOW 惩罚(见 qualityGates.underfill.exemptIds + validate LAYOUT_FILL 轻量页豁免)。内容页半空仍属 underfill。硬规则守舞台;自适应留给内容。",
|
|
1851
|
+
"modeSkels": {
|
|
1852
|
+
"$comment": "Mode A 主力骨架 = P1–P4 + P6/P10;P7–P9、P11–P12 为 A 次级(research/architecture 场景下可升主力)。P5 三卡为 A 常用扩展(非默认轮换)。次级仍合法、文档与画廊可引用,禁止删除。",
|
|
1853
|
+
"presentation": {
|
|
1854
|
+
"primary": [
|
|
1855
|
+
"P1",
|
|
1856
|
+
"P2",
|
|
1857
|
+
"P3",
|
|
1858
|
+
"P4",
|
|
1859
|
+
"P6",
|
|
1860
|
+
"P10"
|
|
1861
|
+
],
|
|
1862
|
+
"secondary": [
|
|
1863
|
+
"P7",
|
|
1864
|
+
"P8",
|
|
1865
|
+
"P9",
|
|
1866
|
+
"P11",
|
|
1867
|
+
"P12"
|
|
1868
|
+
],
|
|
1869
|
+
"extended": [
|
|
1870
|
+
"P5"
|
|
1871
|
+
]
|
|
1872
|
+
},
|
|
1873
|
+
"research": {
|
|
1874
|
+
"primary": [
|
|
1875
|
+
"P5",
|
|
1876
|
+
"P7",
|
|
1877
|
+
"P8",
|
|
1878
|
+
"P4",
|
|
1879
|
+
"P6"
|
|
1880
|
+
],
|
|
1881
|
+
"secondary": [
|
|
1882
|
+
"P1",
|
|
1883
|
+
"P2",
|
|
1884
|
+
"P3",
|
|
1885
|
+
"P9",
|
|
1886
|
+
"P10",
|
|
1887
|
+
"P11",
|
|
1888
|
+
"P12"
|
|
1889
|
+
]
|
|
1890
|
+
},
|
|
1891
|
+
"architecture": {
|
|
1892
|
+
"primary": [
|
|
1893
|
+
"P9",
|
|
1894
|
+
"P10",
|
|
1895
|
+
"P11"
|
|
1896
|
+
],
|
|
1897
|
+
"secondary": [
|
|
1898
|
+
"P1",
|
|
1899
|
+
"P2",
|
|
1900
|
+
"P3",
|
|
1901
|
+
"P4",
|
|
1902
|
+
"P5",
|
|
1903
|
+
"P6",
|
|
1904
|
+
"P7",
|
|
1905
|
+
"P8",
|
|
1906
|
+
"P12"
|
|
1907
|
+
]
|
|
1908
|
+
}
|
|
1841
1909
|
}
|
|
1842
1910
|
},
|
|
1843
1911
|
"qualityGates": {
|
|
1844
|
-
"$comment": "v9
|
|
1912
|
+
"$comment": "v9+ 硬门禁。tagLeakPatterns / titleOnly / underfill / roundtripMin / chartSkew / chartOversize 同前。antiTruncation:截断迹象与溢出未拆页;chromeDriftIn:跨页 chrome y 容差(英寸)。",
|
|
1845
1913
|
"tagLeakPatterns": [
|
|
1846
1914
|
"<a ",
|
|
1847
1915
|
"</a>",
|
|
@@ -1866,8 +1934,11 @@
|
|
|
1866
1934
|
"refs",
|
|
1867
1935
|
"appendix",
|
|
1868
1936
|
"next",
|
|
1869
|
-
"closing"
|
|
1870
|
-
|
|
1937
|
+
"closing",
|
|
1938
|
+
"section",
|
|
1939
|
+
"chapter"
|
|
1940
|
+
],
|
|
1941
|
+
"$comment": "intentional whitespace 豁免:封面/章节幕/金句/收尾/参考资料等休止页不要求承载数下限;其余内容页半空 = underfill FAIL。"
|
|
1871
1942
|
},
|
|
1872
1943
|
"roundtripMin": 0.8,
|
|
1873
1944
|
"chartSkew": {
|
|
@@ -1885,6 +1956,28 @@
|
|
|
1885
1956
|
"simpleMaxCats": 2,
|
|
1886
1957
|
"simpleMaxPts": 3,
|
|
1887
1958
|
"maxContentAreaPct": 55
|
|
1959
|
+
},
|
|
1960
|
+
"antiTruncation": {
|
|
1961
|
+
"$comment": "截断/内容损失启发式。ellipsisInContent=正文/列表中的截断省略号;overflowNoSplit 与 layout-qa 同源。",
|
|
1962
|
+
"ellipsisPatterns": [
|
|
1963
|
+
"…",
|
|
1964
|
+
"...",
|
|
1965
|
+
"……",
|
|
1966
|
+
"…。",
|
|
1967
|
+
"...。"
|
|
1968
|
+
],
|
|
1969
|
+
"forbidEllipsisTruncate": true,
|
|
1970
|
+
"overflowNoSplitFail": true
|
|
1971
|
+
},
|
|
1972
|
+
"chromeDriftIn": {
|
|
1973
|
+
"$comment": "PPTX 内容页页脚/页码 y 相对中位数漂移超过此值(英寸)→ WARN CHROME_DRIFT。封面/收尾豁免。默认 0.2in,抓明显漂移、不挡正常页型微差。",
|
|
1974
|
+
"maxAbsDeltaIn": 0.2
|
|
1975
|
+
},
|
|
1976
|
+
"halfEmpty": {
|
|
1977
|
+
"$comment": "半空卡:同页多卡时,正文过短的卡占比过高 → layout-qa WARN/FAIL。",
|
|
1978
|
+
"minCards": 3,
|
|
1979
|
+
"shortPlainChars": 12,
|
|
1980
|
+
"maxShortRatio": 0.5
|
|
1888
1981
|
}
|
|
1889
1982
|
},
|
|
1890
1983
|
"imageAdmission": {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TopPPT HTML · 页型布局区域 IR(双通道几何收敛起点)
|
|
3
3
|
*
|
|
4
|
-
* 从 layout-constants.json
|
|
5
|
-
* B 通道(build_pptx.js)直接 require;A 通道消费同一 LC
|
|
6
|
-
* 本模块是把「槽位语义」从散落坐标收拢成可测接口的第一步。
|
|
4
|
+
* 从 layout-constants.json(含 layoutSlots)派生「语义槽位 → 英寸矩形」。
|
|
5
|
+
* B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源)。
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* 新增页型先扩
|
|
7
|
+
* 纪律:改几何与槽位只改 layout-constants.json(layoutSlots 已并入)。
|
|
8
|
+
* 新增页型先扩 LC.layoutSlots,再在此补 regionOf 分支,最后写双引擎。
|
|
10
9
|
*/
|
|
11
10
|
'use strict';
|
|
12
11
|
|
|
@@ -15,10 +14,9 @@ const path = require('path');
|
|
|
15
14
|
|
|
16
15
|
const ROOT = path.join(__dirname, '..');
|
|
17
16
|
const LC = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout-constants.json'), 'utf8'));
|
|
18
|
-
|
|
19
|
-
let SLOTS = LC.layoutSlots;
|
|
17
|
+
const SLOTS = LC.layoutSlots;
|
|
20
18
|
if (!SLOTS || !SLOTS.pageTypes) {
|
|
21
|
-
|
|
19
|
+
throw new Error('layout-constants.json 缺 layoutSlots.pageTypes(Batch 3 单源)');
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
const INFO_TYPES = {
|
|
@@ -387,7 +385,7 @@ function regionOf(pageType, slotId, opts) {
|
|
|
387
385
|
return null;
|
|
388
386
|
}
|
|
389
387
|
|
|
390
|
-
/** 槽位是否在
|
|
388
|
+
/** 槽位是否在 LC.layoutSlots 中声明为 required */
|
|
391
389
|
function slotRequired(pageType, slotId) {
|
|
392
390
|
const def = (SLOTS.pageTypes || {})[pageType];
|
|
393
391
|
if (!def) return false;
|