@lark-apaas/coding-steering 0.1.18-dev.6de99aa → 0.1.18-dev.7f786ca

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 (43) hide show
  1. package/README.md +19 -21
  2. package/package.json +1 -1
  3. package/steering/design-html/skills/animated-video/SKILL.md +2 -2
  4. package/steering/design-html/skills/charts/SKILL.md +52 -7
  5. package/steering/design-html/skills/{data-report → data-viz}/SKILL.md +65 -9
  6. package/steering/design-html/skills/frontend-design/SKILL.md +2 -2
  7. package/steering/design-html/skills/mini-game/SKILL.md +71 -0
  8. package/steering/design-html/skills/mini-game/references/three-js.md +54 -0
  9. package/steering/design-html/skills/pptx-style-extract/SKILL.md +148 -0
  10. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +129 -0
  11. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +961 -0
  12. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +1052 -0
  13. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2804 -0
  14. package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_md.py +75 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_zip.py +175 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +1068 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +716 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +1464 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +464 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +557 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +685 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +161 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  25. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +63 -0
  26. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +468 -0
  27. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +503 -0
  28. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  29. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +208 -0
  30. package/steering/design-html/skills/pptx-style-extract/scripts/verify_font.py +68 -0
  31. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +205 -0
  32. package/steering/design-html/skills/preflight/SKILL.md +26 -131
  33. package/steering/design-html/skills/preflight/scripts/probe.sh +108 -0
  34. package/steering/design-html/skills/slide-deck/SKILL.md +160 -0
  35. package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
  36. package/steering/design-html/skills/{visual-exposure → visual-report}/SKILL.md +24 -2
  37. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
  38. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +180 -0
  39. package/steering/nestjs-react-fullstack/{skills/trigger-guide/SKILL.md → skills_common/trigger-guide/references/trigger-lifecycle.md} +11 -162
  40. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
  41. package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
  42. package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
  43. package/steering/design-html/skills/make-a-deck/SKILL.md +0 -209
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for inherited layout text and model-decided text roles."""
3
+ import json
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+
9
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
+
11
+ from draft import draft_layouts, emit_layouts, inherited_text_shapes
12
+ from package import build_layouts_md, split_top_blocks
13
+
14
+
15
+ def text_shape(part, layer, shape_id, box, text, placeholder):
16
+ return {
17
+ 'part': part,
18
+ 'layer': layer,
19
+ 'id': shape_id,
20
+ 'kind': 'sp',
21
+ 'name': 'Text Placeholder',
22
+ 'ph': placeholder,
23
+ 'box': box,
24
+ 'text': {
25
+ 'bodyPr': {},
26
+ 'lstStyle': {
27
+ 'lvl1pPr': {
28
+ 'sz_px': 48,
29
+ 'weight': 600,
30
+ 'color': {'resolved': '#C41230'},
31
+ },
32
+ },
33
+ 'paragraphs': [
34
+ {
35
+ 'runs': [{'text': text}],
36
+ },
37
+ ] if text else [],
38
+ },
39
+ }
40
+
41
+
42
+ class TextRoleContractTest(unittest.TestCase):
43
+ def test_empty_non_placeholder_layout_shape_is_not_a_text_slot(self):
44
+ layout_part = 'ppt/slideLayouts/slideLayout2.xml'
45
+ decorative_shape = text_shape(
46
+ layout_part,
47
+ 'layout',
48
+ '9',
49
+ {'x': 0, 'y': 0, 'w': 1920, 'h': 24},
50
+ '',
51
+ None,
52
+ )
53
+ decorative_shape['name'] = 'Decorative bar'
54
+
55
+ self.assertEqual(inherited_text_shapes([decorative_shape], []), [])
56
+
57
+ def test_empty_slide_inherits_text_slot_and_css_from_its_layout(self):
58
+ layout_part = 'ppt/slideLayouts/slideLayout2.xml'
59
+ slide_part = 'ppt/slides/slide1.xml'
60
+ shapes = [
61
+ text_shape(
62
+ layout_part,
63
+ 'layout',
64
+ '10',
65
+ {'x': 120, 'y': 80, 'w': 840, 'h': 120},
66
+ 'Example heading',
67
+ {'type': 'body', 'idx': '10'},
68
+ ),
69
+ text_shape(
70
+ slide_part,
71
+ 'slide',
72
+ '2',
73
+ None,
74
+ '',
75
+ {'type': 'body', 'idx': '10'},
76
+ ),
77
+ ]
78
+ data = {
79
+ 'canvas': {'px': [1920, 1080]},
80
+ 'form_hint': {'form': 0},
81
+ 'slides': [
82
+ {
83
+ 'part': slide_part,
84
+ 'layout': layout_part,
85
+ 'background': None,
86
+ },
87
+ ],
88
+ 'background_composites': {},
89
+ }
90
+
91
+ with tempfile.TemporaryDirectory() as output_dir:
92
+ os.makedirs(os.path.join(output_dir, 'ref'))
93
+ with open(
94
+ os.path.join(output_dir, 'ref', 'shapes.json'),
95
+ 'w',
96
+ encoding='utf-8',
97
+ ) as stream:
98
+ json.dump({'shapes': shapes}, stream)
99
+ archetypes, _, _ = draft_layouts(data, output_dir)
100
+
101
+ self.assertEqual(len(archetypes), 1)
102
+ self.assertEqual(len(archetypes[0]['slots']), 1)
103
+ slot = archetypes[0]['slots'][0]
104
+ self.assertEqual(slot['box'], [120, 80, 840, 120])
105
+ self.assertEqual(slot['txt'], 'Example heading')
106
+ self.assertEqual(slot['role'], 'body')
107
+ self.assertEqual(slot['type'], 'body')
108
+ self.assertTrue(slot['_needs_role'])
109
+ self.assertIn('font-size: 48px', slot['css'])
110
+ self.assertIn('font-weight: 600', slot['css'])
111
+ self.assertIn('color: #C41230', slot['css'])
112
+
113
+ def test_text_role_judgement_changes_semantics_without_dropping_slot(self):
114
+ archetype = {
115
+ 'name': 'layout-1',
116
+ 'zh': None,
117
+ 'role': 'content',
118
+ 'bg': None,
119
+ 'slots': [
120
+ {
121
+ 'role': 'body',
122
+ 'type': 'body',
123
+ 'box': [120, 80, 840, 120],
124
+ 'sz': 48,
125
+ 'txt': 'Example heading',
126
+ 'css': 'font-size: 48px; color: #C41230',
127
+ '_needs_role': True,
128
+ '_source_layer': 'layout',
129
+ '_placeholder': 'body/10',
130
+ },
131
+ ],
132
+ 'decor': [],
133
+ 'pages': [1],
134
+ 'rep': 1,
135
+ 'pic_n': 0,
136
+ 'confidence': 'low',
137
+ }
138
+
139
+ with tempfile.TemporaryDirectory() as output_dir:
140
+ emit_layouts([archetype], output_dir)
141
+ path = os.path.join(output_dir, 'layouts.yaml')
142
+ with open(path, encoding='utf-8') as stream:
143
+ draft = stream.read()
144
+
145
+ self.assertIn('text_roles:', draft)
146
+ self.assertIn(
147
+ 'layout-1-text-1: TODO文本角色',
148
+ draft,
149
+ )
150
+ self.assertEqual(draft.count('box: [120, 80, 840, 120]'), 1)
151
+
152
+ decided = draft.replace(
153
+ 'layout-1-text-1: TODO文本角色',
154
+ 'layout-1-text-1: title',
155
+ )
156
+ layouts_md = build_layouts_md(split_top_blocks(decided), (1920, 1080))
157
+
158
+ self.assertNotIn('text_roles:', layouts_md)
159
+ self.assertEqual(layouts_md.count('box: [120, 80, 840, 120]'), 1)
160
+ self.assertIn(
161
+ 'role: title, box: [120, 80, 840, 120], type: title',
162
+ layouts_md,
163
+ )
164
+ self.assertIn('css: "font-size: 48px; color: #C41230"', layouts_md)
165
+
166
+ def test_template_layout_shape_without_ph_key_does_not_crash(self):
167
+ # form=3 模板里,版式层可能有「带 box、带文字、但没有 ph 键」的普通形状
168
+ # (非占位符的文本/装饰)。layouts_from_template 的入口筛选是
169
+ # `s.get('ph') or shape_text(s)`——有文字就放进来,随后按 ph 判类型时若用
170
+ # s['ph'] 直接下标就会 KeyError: 'ph',整份抽取在草案阶段崩掉(EXTRACT_PARTIAL)。
171
+ layout_part = 'ppt/slideLayouts/slideLayout1.xml'
172
+ shape = {
173
+ 'part': layout_part,
174
+ 'layer': 'layout',
175
+ 'id': '7',
176
+ 'kind': 'sp',
177
+ 'name': '页脚文字',
178
+ # 关键:没有 'ph' 键
179
+ 'box': {'x': 100, 'y': 980, 'w': 800, 'h': 60},
180
+ 'text': {
181
+ 'bodyPr': {},
182
+ 'lstStyle': {'lvl1pPr': {'sz_px': 20}},
183
+ 'paragraphs': [{'runs': [{'text': '内部资料'}]}],
184
+ },
185
+ }
186
+ data = {
187
+ 'canvas': {'px': [1920, 1080]},
188
+ 'form_hint': {'form': 3},
189
+ 'layouts': [{'part': layout_part}],
190
+ 'slides': [{'part': 'ppt/slides/slide1.xml', 'layout': layout_part,
191
+ 'background': None}],
192
+ 'background_composites': {},
193
+ }
194
+
195
+ with tempfile.TemporaryDirectory() as output_dir:
196
+ os.makedirs(os.path.join(output_dir, 'ref'))
197
+ with open(os.path.join(output_dir, 'ref', 'shapes.json'), 'w',
198
+ encoding='utf-8') as stream:
199
+ json.dump({'shapes': [shape]}, stream)
200
+ # 修复前这里抛 KeyError: 'ph'(draft.py 用 s['ph'] 直接下标),
201
+ # 抽取在草案阶段崩掉、退成 EXTRACT_PARTIAL。修复后应正常返回。
202
+ archetypes, pages, leftover = draft_layouts(data, output_dir)
203
+
204
+ self.assertIsInstance(archetypes, list)
205
+
206
+
207
+ if __name__ == '__main__':
208
+ unittest.main()
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """字体镜像可加载性验证(L4 固定流程的脚本形态)。
3
+
4
+ python3 verify_font.py "Noto Sans SC" [--wght "400;500;600;700"] [--repeat 2]
5
+
6
+ 对 https://miaoda.feishu.cn/fonts/css2 请求 N 次(镜像多字重响应不稳定,默认复测 2 次),
7
+ 解析每次返回的 @font-face font-weight 集合,输出:
8
+
9
+ run 1: 400,500,600,700
10
+ run 2: 400
11
+ verdict: usable=400 unstable=500,600,700
12
+
13
+ usable = 每次都返回的字重(可放心用);unstable = 时有时无(按浏览器合成加粗处理并记 gaps);
14
+ 全部请求失败 = 该族不可加载,走 font-fallback.yaml 降级。exit 0 = 至少一档 usable。
15
+ """
16
+ import argparse
17
+ import re
18
+ import sys
19
+ import urllib.parse
20
+ import urllib.request
21
+
22
+ MIRROR = 'https://miaoda.feishu.cn/fonts/css2'
23
+ UA = ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
24
+ '(KHTML, like Gecko) Chrome/120.0 Safari/537.36')
25
+
26
+
27
+ def fetch_weights(family, wght):
28
+ spec = family.replace(' ', '+') + (':wght@' + wght if wght else '')
29
+ url = '%s?%s&display=swap' % (MIRROR, urllib.parse.quote('family=' + spec, safe='=+&:;@'))
30
+ req = urllib.request.Request(url, headers={'User-Agent': UA})
31
+ with urllib.request.urlopen(req, timeout=15) as r:
32
+ css = r.read().decode('utf-8', 'replace')
33
+ if '@font-face' not in css:
34
+ return None
35
+ return sorted(set(re.findall(r'font-weight:\s*(\d+)', css)), key=int)
36
+
37
+
38
+ def main():
39
+ ap = argparse.ArgumentParser()
40
+ ap.add_argument('family')
41
+ ap.add_argument('--wght', default='400;500;600;700')
42
+ ap.add_argument('--repeat', type=int, default=2)
43
+ a = ap.parse_args()
44
+
45
+ runs = []
46
+ for i in range(a.repeat):
47
+ try:
48
+ w = fetch_weights(a.family, a.wght)
49
+ except Exception as exc:
50
+ w = None
51
+ print('run %d: 请求失败(%s)' % (i + 1, exc.__class__.__name__))
52
+ runs.append(set())
53
+ continue
54
+ print('run %d: %s' % (i + 1, ','.join(w) if w else '无 @font-face'))
55
+ runs.append(set(w or []))
56
+
57
+ usable = set.intersection(*runs) if runs else set()
58
+ unstable = set.union(*runs) - usable if runs else set()
59
+ if usable:
60
+ print('verdict: usable=%s%s' % (','.join(sorted(usable, key=int)),
61
+ ' unstable=' + ','.join(sorted(unstable, key=int)) if unstable else ''))
62
+ return 0
63
+ print('verdict: 不可加载 —— 查 font-fallback.yaml 降级,原始名留栈首 + 记 gaps')
64
+ return 1
65
+
66
+
67
+ if __name__ == '__main__':
68
+ sys.exit(main())
@@ -0,0 +1,205 @@
1
+ # 风格包 v2 规范(DSM v1 超集)
2
+
3
+ > 与 `dsm-v1-spec.md`(v1 单文件规范)配套:v1 的全部规则原样生效,本文只定义 v2 新增部分。机器门禁 = `check_v1.py`(v1 部分)+ `scripts/check_v2.py`(本文 §5 十三条)。**纯 design.md(不含任何 v2 新增段)永远是合法 v2 退化态、可独立消费**——存量风格零迁移。
4
+ > 字段名/枚举/阈值以本文为唯一权威;抽取执行步骤在 SKILL.md,本文只写格式。
5
+
6
+ ## 0. 包形态(定稿:独立风格包)
7
+
8
+ ```
9
+ <style-name>/
10
+ manifest.json # 机器清单:id/name/version/files/assets、体积、sha256
11
+ design.md # 消费入口 + 权威:v1 全部 + §1-§4 新增段(含 layouts 指针)
12
+ layouts.md # layouts sidecar(§3)
13
+ assets/ # 二进制资产:<kind>s/<id 去 kind 前缀>.<ext>,如 assets/logos/on-light.svg
14
+ ref/ # 审计层(audit.yaml 元数据、证据、频次原表、溯源),下发时由链路剥离
15
+ ```
16
+
17
+ - 消费方直接读 design.md,靠目录约定找 sidecar 与资产;frontmatter `path` 是唯一文件引用点。
18
+ - manifest(`manifest.json`,识别靠内部 `schemaVersion` 字面量不靠文件名)服务存储、索引、校验和迁移;消费模型不需要读它。
19
+ - `ref/` 只放 `audit.yaml`(数值出处的人工复核记录,几 KB)。频次原表、聚类原始数据、`extract.json`、重建图、logo 候选图**留在抽取工作目录,不进交付包**——原设计是「下发时链路剥离」,但链路上没有环节真的做剥离,审计材料会连带进消费上下文并占掉包体的大头。
20
+
21
+ ## 1. design.md frontmatter 新增键
22
+
23
+ 全部可选(缺 = 退化 v1)。**含 `layouts` 或 `safe-area` 必有 `canvas`**。坐标一律归一化整数 px@1920。
24
+
25
+ **design.md 只装消费者要用的东西**:审计与溯源元数据(`canvas-source`、`theme-mechanism`、`color-confidence`、资产的 boxes/aspect/mark/confidence)一律落 `ref/audit.yaml`;`canvas` 落 layouts.md frontmatter(与坐标数据同处)。
26
+
27
+ **键序强制**(V2-9 机检,8k 截断护栏):
28
+
29
+ ```
30
+ 官方白名单键: version → name → name_zh → description → colors → typography → spacing → rounded → components → omitted
31
+ v1 自造键: anchors → gaps → exceptions
32
+ v2 自造键: themes → default-theme → assets → layouts → safe-area
33
+ ```
34
+
35
+ `omitted` 留手工作者,抽取产物只用 `gaps` 记「抽不出」。
36
+
37
+ **引用硬规则**:`themes / assets / layouts / safe-area` 四段禁止花括号引用(`{assets.x}` = FAIL),正文写反引号裸 id。唯一例外:纯色背景的 `color: "{colors.x}"`(引 colors 命名空间,合法且必须)。
38
+
39
+ **YAML 键名红线**:禁用 YAML 1.1 布尔字面量作键名(`on / off / yes / no / true / false / y / n`)——PyYAML 会把 `on:` 解析成 `True:`。资产的「在什么底上用」字段因此叫 `on-bg`。
40
+
41
+ ### 1.1 `canvas`(在 layouts.md frontmatter,不在 design.md)
42
+
43
+ ```yaml
44
+ canvas: 1920x1080 # layouts.md 首键;px = round(EMU / sldSz_cx * 1920)
45
+ ```
46
+
47
+ - 非 16:9 保宽 1920、高按真实比例,差异记 `gaps`。
48
+ - 原始 EMU(`canvas-source`,PPT 反向生成用)落 `ref/audit.yaml`。
49
+
50
+ ### 1.2 `themes` 与双主题色板(主题前缀 token 名)
51
+
52
+ ```yaml
53
+ themes: [dark, light]
54
+ default-theme: dark # 双主题包必填(V2-13)
55
+ colors: # v1 单层扁平,主题进 token 名
56
+ dark-surface: "#RRGGBB"
57
+ dark-on-surface: "#RRGGBB"
58
+ light-surface: "#RRGGBB"
59
+ primary: "#RRGGBB" # 共用色不加前缀
60
+ ```
61
+
62
+ - token 前缀约定(`dark-X`/`light-X` = 主题专属,无前缀 = 共用)在 `## Usage` 里向消费者写一句;主题机制溯源(clrMap 反转等)落 `ref/audit.yaml`。
63
+ - 嵌套/模式对象形态禁用(官方 lint 0.4.0 下整名引用 broken-ref 致败),前缀是唯一 0-error 形态。
64
+ - **色角色基名优先映射 MD3 词表**(bg→surface/background、text→on-surface、强调→primary/secondary/tertiary、配对一律 `on-X`),抽不出对应再自造。
65
+
66
+ ### 1.3 排除色
67
+
68
+ - **排除色进 `## Hard Rules` 带证据计数**,正向给替代(如「`<hex>` 为编辑器参考线色(出现 <N> 次),非设计色」)。频次原表与 color-confidence 证据进 `ref/`。
69
+ - 排除色断言必须以**解析后频次**为准,`styleRef` 主题兜底引用(不渲染)与真实设计用色分开。
70
+
71
+ ## 2. `assets` 段
72
+
73
+ **条目只留消费字段**——design.md 是给消费模型读的,每个字段都要回答「用什么、用在哪」。boxes / aspect / mark / confidence / 频次注记是审计字段,落 `ref/assets-audit.yaml`,不进 design.md(D10「审计进 ref/」的完整贯彻)。
74
+
75
+ ```yaml
76
+ assets:
77
+ logo-on-light:
78
+ path: assets/logos/on-light.svg # 包内相对路径
79
+ kind: logo # 封闭枚举:logo | slogan | background | texture | icon
80
+ on-bg: light # light | dark
81
+ bg-cover-dark:
82
+ path: assets/backgrounds/cover-dark.webp
83
+ full: assets/backgrounds/cover-dark@full.jpg # 可选:原图(方案甲双产物)
84
+ kind: background
85
+ role: cover # cover | content | section | closing | accent
86
+ theme: dark
87
+ recipe: "linear-gradient(<angle>, <color> 0%, <color> 100%)" # 可选:CSS 重绘配方
88
+ bg-content-solid: # 纯色背景:无 path/url,引 colors token
89
+ kind: background
90
+ role: content
91
+ color: "{colors.dark-bg}"
92
+ ```
93
+
94
+ - **`path` / `url` / `color` 三者恰好存在一个**(V2-12);`color` 仅 `kind: background` 允许;`full` 仅可与 `path` 共存。
95
+ - **每个资产必须在正文 `## Usage` 的资产用法表里有一行用法**(文件、用在哪、怎么摆)——条目字段说明"是什么",用法表说明"怎么用",两者缺一即孤儿(V2-2)。
96
+ - `themes` 每主题应有可用 logo `on-bg` 变体(缺 → V2-8 WARN)。
97
+ - 文件格式:webp 优先、jpg 可接受;svg 保源、**禁内嵌 base64 位图**(假矢量按位图处理)。
98
+ - **二进制承载两方案**:
99
+ - 方案甲·包内(抽取产物默认形态):大图保留原图 + 压缩图(`<name>@full.<ext>` / `<name>.<ext>`,`path` 指压缩图、`full` 指原图),消费侧优先用压缩图;压缩图 >500KB WARN、包内总量 >20MB FAIL。
100
+ - 方案乙·平台云盘(入库后目标形态):条目用 `url`,消费时按云盘图片处理参数取压缩版;包内不落二进制,体积约束不适用。入库时由后端把 `path`/`full` 重写为 `url`(重写版仍须过 V2-1/V2-12)。
101
+ - `url` 必须 http(s) 持久地址,禁 24h TTL 签名 URL。
102
+ - 图片按用途三分:整幅替换底图的满屏主视觉(含封面艺术图)属**背景族**,照收;服务于具体内容的图表/截图/产品说明图是**内容图,不进包**;既非 logo、又不服务内容的纹理/装饰插画/色块/几何点缀是**装饰图,标 `texture`**。`logo` 特指这份 deck 自己的品牌标志——logo 墙里的第三方 logo 算内容图。被遮挡/无用资产不进包;抽不出不编造(记 `gaps`,logo 候选图存 `ref/logo-candidates/`)。
103
+
104
+ ## 2.5 `## Usage` 章节(正文必产,紧随 Overview)
105
+
106
+ design.md 是消费模型的操作文档,不是抽取记录。`## Usage` 承载三件事,全部**可执行**(具体文件、具体坐标、具体顺序):
107
+
108
+ 1. **消费步骤**:① 画布取 `layouts.md` 的 `canvas`;② 从页型清单选 archetype,按其 flow / slots / decor / background 原样落版;③ `design.md` frontmatter 的 colors / typography / spacing / rounded / components 作为全局 token,局部 CSS 优先;④ 包内资产复制到项目相对目录后引用,字体使用完整 fallback 栈且不在运行时安装;⑤ 双主题包写明默认主题与 token 前缀切换法。
109
+ 2. **资产用法表**:每个资产一行——id、文件路径、用在哪类页、怎么摆(logo 给坐标,背景给首选序——如「封面首选 cover-art,无主视觉需求用 cover-dark」)。
110
+ 3. **强调色族纪律**:强调色族以 `colors` 段和 layout slot CSS 为主。必要时可使用其他颜色,但新增颜色须与模板整体的色相、明度和饱和度关系协调,且不能形成与模板主色竞争的第二强调色。中性色、低彩度辅助色或局部语义色可表达正负、风险、警告、状态、图表序列,但须保持辅助层级;新色不得通过高饱和、高对比、大面积、跨页重复,或用于标题、关键数字、图表主序列、卡片底色、渐变来获得主视觉权重。
111
+ 4. **交付检查**:逐页确认色板、字体、版式、背景、资产和 Hard Rules 均来自本包,并检查资源加载、内容溢出与画幅裁切。
112
+
113
+ **Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md 取;以 colors / layout slot CSS 为强调色基准,新增颜色与整体色板协调并保持辅助层级。禁止句只用于无法正向表达的红线,且同句给替代。
114
+
115
+ ## 3. `layouts` 段(默认 sidecar)
116
+
117
+ design.md frontmatter 里 `layouts` 键**类型二义**:值为 string 且 `.md` 结尾 = sidecar 指针(`layouts: layouts.md`);值为 map = 内联。键名统一 `layouts`(`layouts-file` = FAIL)。
118
+
119
+ ```yaml
120
+ layouts:
121
+ cover:
122
+ name: "封面"
123
+ role: cover # 封闭七值:cover | section | content | quote | closing | blank | custom
124
+ themes: [dark, light] # 深浅孪生合并
125
+ background: {dark: bg-cover-dark, light: bg-cover-light}
126
+ slots:
127
+ - {role: title, box: [<x>, <y>, <w>, <h>], type: title, css: "<CSS 声明串>"}
128
+ - {role: logo, box: [<x>, <y>, <w>, <h>], asset: {dark: logo-on-dark, light: logo-on-light}}
129
+ decor:
130
+ - {box: [<x>, <y>, <w>, <h>], geom: ellipse, css: "<CSS 声明串>"}
131
+ confidence: high
132
+ ```
133
+
134
+ - **`background` 三形态**:`<asset-id>` / `{<theme>: <asset-id>}` / `{color: <colors-token>}`(`color` 是保留键,主题名禁止叫 color)。`asset` 两形态:`<asset-id>` / `{<theme>: <asset-id>}`。
135
+ - **背景安全扩展**:有真实背景图的 archetype 建议写 `text_safe: [x,y,w,h]`、`avoid: [{box: [x,y,w,h], reason: "..."}]`、`pairing_rule: "..."`。这些是消费约束,不参与封闭枚举;用于避免标题、正文、图表、卡片、表格、时间线及其容器外接矩形覆盖背景视觉主体、强光斑或深色透明区;透明容器也不能跨进禁放区。
136
+ - **流式页型**:内容长度会变化的内容页可用 `flow.regions` 表达纵向区带。`stack` 表达单列顺序,`grid` 表达并列列组,`free` 中的 item 必须带 `box`,用于 logo、页码、页眉和页脚等固定锚点。并列卡片可在 `grid.items` 中使用一层 `{role: group, css, gap, items}`:group 的 `css` 是卡片容器样式,内部 `items` 按顺序排布;不继续嵌套 group。区带可带自己的 `margin: [左, 右]`,覆盖 `flow` 整块的 `margin`(居中卡片组和贴左标题横向范围本就不同);不带则继承整块 `margin`。纵向位置与留白由消费模型结合实际内容决定,不把样张的 `y` 坐标当作流式硬约束。
137
+ - **`decor`(可选)**:这一页无文字的图形骨架——图标托底的圆、卡片、分隔线。每条 `{box, geom, css}`:`box` 定位,`css` 是可直接写进 style 的声明串,`geom` 取源形状的 prst(`ellipse` 另加 `border-radius: 50%`)。圆角以每条 `css` 为准,没有 `border-radius` 就按 `0`;不得因 `geom: roundRect` 自行补圆角,因为 OOXML 的 roundRect 可以有零圆角调节点。层级在背景之上、`slots` 之下;带 `asset` 的槽落在 decor 之上是版式本意,不算重叠。
138
+ - **slot 样式契约**:`box` 只承载 `[x,y,w,h]` 几何;可渲染属性统一放进 `css`,并可直接写入 HTML `style`。PPTX `bodyPr.insets_px` 转成 `box-sizing: border-box; padding: ...`,字号/字重/颜色/水平与垂直对齐/行高/字距/旋转分别转成标准 CSS。禁止在 slot 中输出 `size` / `weight` / `color` / `align` / `valign` / `insets_px` 等旧字段。
139
+ - **文本角色判断**:脚本把实例页及其引用版式中的现有文本槽、几何和 CSS 完整写入草案;`text_roles` 只供模型把这些槽判断为 `title | subtitle | header | footer | body`,不控制槽位去留。判断不清时用 `body`,不归纳模板中不存在的标题、页眉或页脚。
140
+ - **标题结构**:存在合适的模板页型时,沿用其标题层级和局部 CSS,只渲染该页型已有的文字槽;背景中已经可见的固定标题不重复创建文本,该页型没有副标题槽时不新增副标题。没有合适参考时由模型按模板整体视觉判断。
141
+ - **圆角作用域**:`rounded` 只允许表达全档共同的单一圆角档位;零圆角与非零圆角混用、或存在多个非零档位时不输出该全局 token。此时每个 `role: container` / `decor` 的 `css` 是唯一事实源,逐项原样消费,不得归并或推断。
142
+ - **`type` 封闭枚举**:`title | subtitle | body | pic | table | chart | media | slide-number | footer`。大数字/序号走 `type: title`,语义由 `role`(如 `big-number`)承担。
143
+ - **`slots.*.role` 开放不校验**(语义槽位):优先复用已知词表(OOXML ST_SlideLayoutType / Slidev 20 布局 / Google PredefinedLayout,如 big-number、caption、main-point),确无对应再自造。
144
+ - archetype ≤15(内联降级形态 ≤11);深浅孪生合并为一条;版式溯源/母版取舍进 `ref/`。
145
+ - **sidecar 容器形态**:统一 frontmatter(`---` 包裹 YAML)+ 正文可留说明。sidecar 内禁止重复 `canvas` / `themes` 等 design.md 已有键(冲突以 design.md 为准,机检 WARN)。
146
+
147
+ ## 4. `safe-area` 段
148
+
149
+ ```yaml
150
+ safe-area: # 开放命名 map,可多套边距体系
151
+ content: {top: <px>, right: <px>, bottom: <px>, left: <px>, applies-to: [content, quote]}
152
+ editorial: {left: <px>, right: <px>, applies-to: [cover, section, closing]}
153
+ confidence: medium
154
+ ```
155
+
156
+ 冲突裁决:`slots.box` 是实例真值,`safe-area` 是归纳框架,**以 slots.box 为准**。
157
+
158
+ ## 5. check_v2 校验(19 行:V2-1..V2-16 + V2-R5/R6/R7)
159
+
160
+ check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 design.md + layouts.md 求并集。
161
+
162
+ | # | 规则 | 级别 |
163
+ |---|---|---|
164
+ | V2-1 | `path`/`url`/`full` 引用断链(含 slots 的 by-theme 嵌套形态) | FAIL |
165
+ | V2-2 | 孤儿资产(`full` 指向的文件不算孤儿) | WARN |
166
+ | V2-3 | 坐标出 canvas ±5% 出血容差(四边各 ±5%,受检 = `slots.box`;canvas 读 layouts.md frontmatter) | FAIL |
167
+ | V2-4 | `kind/on-bg/role/theme/type` 枚举合法(role 分语境:assets 五值封闭 / layouts 七值封闭 / slots 开放) | FAIL |
168
+ | V2-5 | 推断段缺 confidence(受检 = layouts 条目 / safe-area 块;assets 的 confidence 在 `ref/audit.yaml`,不受本检) | FAIL |
169
+ | V2-6 | (仅包内承载)压缩图单张 >500KB WARN;包内资产总量 >20MB FAIL(口径 = `assets/**` ∪ 声明 path/full 并集,KB=1024;`full` 文件免单张 WARN、计入总量;url 承载不适用) | WARN/FAIL |
170
+ | V2-7 | 花括号引用新段(`{assets.*}` 等)显式拦截 | FAIL |
171
+ | V2-8 | `themes` 声明主题缺可用 logo `on-bg` 变体 | WARN |
172
+ | V2-9 | frontmatter 键名/键序符合 §1 清单(键序乱 = WARN;`layouts-file` 废弃键 = FAIL,须改用 `layouts`) | WARN/FAIL |
173
+ | V2-10 | 含 layouts/safe-area 但包内(design.md ∪ layouts.md frontmatter)无 canvas | FAIL |
174
+ | V2-11 | 键名命中 YAML 1.1 布尔字面量 | FAIL |
175
+ | V2-12 | `path`/`url`/`color` 恰好存在一个;`color` 仅 background 允许;`full` 仅可伴随 `path` | FAIL |
176
+ | V2-13 | 多主题(`themes` 长度 >1)缺 `default-theme`(只看 design.md frontmatter——冲突以 design.md 为准) | WARN |
177
+ | V2-14 | 版式的 `flow` 与 `slots` 互斥(同时出现 = FAIL);`flow.regions[].kind` 在 `grid`/`stack`/`free` 内,`grid` 必带 `cols` | FAIL |
178
+ | V2-15 | 同段字段自洽:`backgrounds.*` 的 `text_safe` 不得与任一 `avoid` 相交;两者形态须为 `[x, y, w, h]` 四个数且 w/h 为正 | FAIL |
179
+ | V2-16 | slot/flow item 的渲染样式只通过 `css` 承载;出现 `size/weight/color/align/valign/insets_px` 旧键 | FAIL |
180
+ | V2-R5 | sidecar frontmatter 重复 design.md 已有顶层键(`layouts` 载荷键与 `canvas` 除外——canvas 的家就在 sidecar) | WARN |
181
+ | V2-R6 | assets 条目出现审计字段(boxes/aspect/mark/confidence)或 design.md 顶层出现 canvas/canvas-source/theme-mechanism/color-confidence——应移 `ref/audit.yaml` / layouts.md | WARN |
182
+ | V2-R7 | 有 layouts sidecar 指针但正文未出现 `layouts.md` 字样(弱指针,消费者到不了版式数据) | WARN |
183
+
184
+ 补充口径:url 只验格式(非 http(s) FAIL)+ 签名参数启发式 WARN,活性归下发链路。
185
+
186
+ ## 6. 体量与降级链
187
+
188
+ - v1 门限不变:slide 档 22000 字符 FAIL、est-token >6000 WARN;**按 8k token 保守设计**(appType=6 内联 8k 静默截断)。
189
+ - 降级链(顺序执行,不允许交付 FAIL 件):审计字段出包(默认已做)→ **layouts sidecar 化(无损,默认形态)** → archetype 上限收缩(有损,最后手段)。
190
+
191
+ ## 7. `confidence` 契约
192
+
193
+ 消费侧:
194
+
195
+ | level | 消费方行为 |
196
+ |---|---|
197
+ | `high` | 按值执行,视同显式规范 |
198
+ | `medium` | 按值执行;与内容冲突时允许 ±5% 微调,不得改语义角色 |
199
+ | `low` | 建议值,可按版面调整,但必须满足 safe-area 与不出血 |
200
+
201
+ 生产侧初评(脚本执行,与消费契约是两回事):**直读字段 = high、单信号推断 = medium、聚类/看图推断 = low**;LLM 仅可多信号互证升档,升档必须在 `ref/` 写明依据。
202
+
203
+ ## 8. 开放命名治理
204
+
205
+ `safe-area` 键名 / `slots.role` / asset id 采用晋升制:单风格自用 → ≥2 风格需要进推荐词表 → 高频升规范枚举;不支持降级。