@lark-apaas/coding-steering 0.1.32 → 0.1.33-beta.0

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 (20) hide show
  1. package/package.json +1 -1
  2. package/steering/design-html/skills/pptx-style-extract/SKILL.md +58 -22
  3. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  4. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +18 -12
  5. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  6. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +1768 -241
  7. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +325 -22
  8. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -1
  9. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +379 -156
  10. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +6 -3
  11. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  12. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +16 -10
  13. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  15. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +62 -0
  16. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +378 -0
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +98 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +168 -0
  20. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for the consumer-facing layout slot CSS contract."""
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import unittest
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from check_v2 import Pack, rule_v2_16 # noqa: E402
11
+ from draft import emit_layouts, slot_style # noqa: E402
12
+ from package import FONTSIZE_RE, build_layouts_md, split_top_blocks # noqa: E402
13
+
14
+
15
+ class LayoutCssTest(unittest.TestCase):
16
+ def test_text_slot_emits_rendering_style_as_css_only(self):
17
+ shape = {
18
+ 'text': {
19
+ 'bodyPr': {
20
+ 'anchor': 'ctr',
21
+ 'insets_px': {'lIns': 12, 'tIns': 8, 'rIns': 10, 'bIns': 6},
22
+ 'rot': '900000',
23
+ },
24
+ 'lstStyle': {
25
+ 'lvl1pPr': {
26
+ 'sz_px': 48,
27
+ 'weight': 600,
28
+ 'italic': True,
29
+ 'underline': 'sng',
30
+ 'strike': 'sngStrike',
31
+ 'spc_px': 1.5,
32
+ 'color': {'resolved': '#123456'},
33
+ 'algn': 'ctr',
34
+ 'lnSpc': {'mult': 1.0},
35
+ },
36
+ },
37
+ 'paragraphs': [],
38
+ },
39
+ }
40
+ slot = {
41
+ 'role': 'title',
42
+ 'type': 'title',
43
+ 'box': [100, 80, 800, 160],
44
+ 'sz': 48,
45
+ 'txt': '标题',
46
+ }
47
+ slot.update(slot_style(shape))
48
+ archetype = {
49
+ 'name': 'layout-1',
50
+ 'zh': '标题页',
51
+ 'role': 'content',
52
+ 'bg': None,
53
+ 'slots': [slot],
54
+ 'decor': [],
55
+ 'pages': [1],
56
+ 'rep': 1,
57
+ 'pic_n': 0,
58
+ 'confidence': 'high',
59
+ }
60
+
61
+ with tempfile.TemporaryDirectory() as output_dir:
62
+ emit_layouts([archetype], output_dir)
63
+ with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
64
+ layouts_yaml = stream.read()
65
+ layouts_md = build_layouts_md(split_top_blocks(layouts_yaml), (1920, 1080))
66
+
67
+ self.assertIn(
68
+ 'css: "box-sizing: border-box; padding: 8px 10px 6px 12px; '
69
+ 'font-size: 48px; font-weight: 600; font-style: italic; '
70
+ 'text-decoration: underline line-through; letter-spacing: 1.5px; color: #123456; '
71
+ 'text-align: center; line-height: 1.2; display: flex; '
72
+ 'flex-direction: column; justify-content: center; rotate: 15deg"',
73
+ layouts_md,
74
+ )
75
+ for legacy_key in ('size', 'weight', 'color', 'align', 'valign', 'insets_px'):
76
+ self.assertNotRegex(layouts_md, rf'[,{{]\s*{legacy_key}:')
77
+ self.assertEqual(FONTSIZE_RE.findall(layouts_md), ['48'])
78
+
79
+ def test_layout_gate_rejects_legacy_slot_style_keys(self):
80
+ with tempfile.TemporaryDirectory() as pack_dir:
81
+ with open(os.path.join(pack_dir, 'design.md'), 'w', encoding='utf-8') as stream:
82
+ stream.write('---\nversion: alpha\nlayouts: layouts.md\n---\n\nRead layouts.md.\n')
83
+ with open(os.path.join(pack_dir, 'layouts.md'), 'w', encoding='utf-8') as stream:
84
+ stream.write(
85
+ '---\ncanvas: 1920x1080\nlayouts:\n cover:\n role: cover\n'
86
+ ' slots:\n - {role: title, type: title, box: [0, 0, 800, 100], '
87
+ 'size: 48, align: center}\n confidence: high\n---\n'
88
+ )
89
+
90
+ result = rule_v2_16(Pack(pack_dir))
91
+
92
+ self.assertEqual(result.level, 'FAIL')
93
+ self.assertEqual(len(result.fails), 1)
94
+ self.assertIn('旧样式键 align/size', result.fails[0])
95
+
96
+
97
+ if __name__ == '__main__':
98
+ unittest.main()
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for preserving per-container radii."""
3
+ import os
4
+ import tempfile
5
+ import unittest
6
+
7
+ from draft import emit_body, emit_frontmatter
8
+ from query import _recipe_css
9
+
10
+
11
+ def render_frontmatter(radii):
12
+ data = {
13
+ 'radii_census': radii,
14
+ 'spacing_candidates': {},
15
+ }
16
+ tokens = [('surface', {'hex': '#FFFFFF'})]
17
+
18
+ with tempfile.TemporaryDirectory() as output_dir:
19
+ emit_frontmatter(
20
+ data,
21
+ tokens,
22
+ [],
23
+ {},
24
+ [],
25
+ [],
26
+ output_dir,
27
+ )
28
+ with open(os.path.join(output_dir, 'frontmatter.yaml'), encoding='utf-8') as stream:
29
+ return stream.read()
30
+
31
+
32
+ def render_body():
33
+ data = {
34
+ 'canvas': {'px': [1920, 1080]},
35
+ 'form_hint': {'form': 2},
36
+ 'counts': {'slides': 1},
37
+ }
38
+
39
+ with tempfile.TemporaryDirectory() as output_dir:
40
+ emit_body(
41
+ data,
42
+ [],
43
+ [],
44
+ {},
45
+ [],
46
+ [],
47
+ [],
48
+ {},
49
+ output_dir,
50
+ )
51
+ with open(os.path.join(output_dir, 'body.md'), encoding='utf-8') as stream:
52
+ return stream.read()
53
+
54
+
55
+ class RoundedContractTest(unittest.TestCase):
56
+ def test_multiple_radius_tiers_are_not_collapsed_into_one_card_token(self):
57
+ frontmatter = render_frontmatter(
58
+ [
59
+ {'px': 6.9, 'n': 2},
60
+ {'px': 11.9, 'n': 2},
61
+ {'px': 14.4, 'n': 3},
62
+ ]
63
+ )
64
+
65
+ self.assertNotIn('rounded:', frontmatter)
66
+
67
+ def test_single_radius_tier_can_remain_a_global_token(self):
68
+ frontmatter = render_frontmatter([{'px': 12, 'n': 9}])
69
+
70
+ self.assertIn('rounded:\n card: 12px', frontmatter)
71
+
72
+ def test_rare_rounded_exceptions_do_not_override_a_zero_radius_majority(self):
73
+ frontmatter = render_frontmatter(
74
+ [
75
+ {'px': 0, 'n': 241},
76
+ {'px': 3.4, 'n': 4},
77
+ {'px': 6.9, 'n': 3},
78
+ {'px': 50.5, 'n': 1},
79
+ ]
80
+ )
81
+
82
+ self.assertNotIn('rounded:', frontmatter)
83
+
84
+ def test_generated_usage_defaults_unspecified_container_radius_to_zero(self):
85
+ body = render_body()
86
+
87
+ self.assertIn('没有 `border-radius` 就按 `0`', body)
88
+ self.assertIn('不得自行补圆角', body)
89
+
90
+ def test_recipe_does_not_promote_one_rounded_exception_to_the_whole_group(self):
91
+ css = _recipe_css(
92
+ {'type': 'solid', 'color': {'hex': '#FFFFFF'}},
93
+ None,
94
+ [0] * 70 + [50.5],
95
+ None,
96
+ )
97
+
98
+ self.assertFalse(any('border-radius' in declaration for declaration in css))
99
+
100
+ def test_recipe_keeps_a_radius_shared_by_the_whole_group(self):
101
+ css = _recipe_css(
102
+ {'type': 'solid', 'color': {'hex': '#FFFFFF'}},
103
+ None,
104
+ [6.9] * 17,
105
+ None,
106
+ )
107
+
108
+ self.assertIn('border-radius: 6.9px', css)
109
+
110
+
111
+ if __name__ == '__main__':
112
+ unittest.main()
@@ -0,0 +1,168 @@
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
+
167
+ if __name__ == '__main__':
168
+ unittest.main()
@@ -16,7 +16,7 @@
16
16
 
17
17
  - 消费方直接读 design.md,靠目录约定找 sidecar 与资产;frontmatter `path` 是唯一文件引用点。
18
18
  - manifest(`manifest.json`,识别靠内部 `schemaVersion` 字面量不靠文件名)服务存储、索引、校验和迁移;消费模型不需要读它。
19
- - `ref/` 是包内可剥离子目录:抽取产出必含(check_v2 不校验其存在性,归位是打包步骤的责任),入库/下发时链路剥离,不进消费上下文。
19
+ - `ref/` 只放 `audit.yaml`(数值出处的人工复核记录,几 KB)。频次原表、聚类原始数据、`extract.json`、重建图、logo 候选图**留在抽取工作目录,不进交付包**——原设计是「下发时链路剥离」,但链路上没有环节真的做剥离,审计材料会连带进消费上下文并占掉包体的大头。
20
20
 
21
21
  ## 1. design.md frontmatter 新增键
22
22
 
@@ -53,10 +53,10 @@ canvas: 1920x1080 # layouts.md 首键;px = round(EMU / sldSz_cx * 1920)
53
53
  themes: [dark, light]
54
54
  default-theme: dark # 双主题包必填(V2-13)
55
55
  colors: # v1 单层扁平,主题进 token 名
56
- dark-surface: "#0A0E1E"
57
- dark-on-surface: "#FFFFFF"
58
- light-surface: "#FFFFFF"
59
- primary: "#3C7FFF" # 共用色不加前缀
56
+ dark-surface: "#RRGGBB"
57
+ dark-on-surface: "#RRGGBB"
58
+ light-surface: "#RRGGBB"
59
+ primary: "#RRGGBB" # 共用色不加前缀
60
60
  ```
61
61
 
62
62
  - token 前缀约定(`dark-X`/`light-X` = 主题专属,无前缀 = 共用)在 `## Usage` 里向消费者写一句;主题机制溯源(clrMap 反转等)落 `ref/audit.yaml`。
@@ -65,7 +65,7 @@ colors: # v1 单层扁平,主题进 token 名
65
65
 
66
66
  ### 1.3 排除色
67
67
 
68
- - **排除色进 `## Hard Rules` 带证据计数**,正向给替代(如「`#FBAE40` 为编辑器参考线色(出现 72 次),非设计色」)。频次原表与 color-confidence 证据进 `ref/`。
68
+ - **排除色进 `## Hard Rules` 带证据计数**,正向给替代(如「`<hex>` 为编辑器参考线色(出现 <N> 次),非设计色」)。频次原表与 color-confidence 证据进 `ref/`。
69
69
  - 排除色断言必须以**解析后频次**为准,`styleRef` 主题兜底引用(不渲染)与真实设计用色分开。
70
70
 
71
71
  ## 2. `assets` 段
@@ -84,7 +84,7 @@ assets:
84
84
  kind: background
85
85
  role: cover # cover | content | section | closing | accent
86
86
  theme: dark
87
- recipe: "linear-gradient(135deg, #0A0E1E 0%, #12204A 100%)" # 可选:CSS 重绘配方
87
+ recipe: "linear-gradient(<angle>, <color> 0%, <color> 100%)" # 可选:CSS 重绘配方
88
88
  bg-content-solid: # 纯色背景:无 path/url,引 colors token
89
89
  kind: background
90
90
  role: content
@@ -105,11 +105,12 @@ assets:
105
105
 
106
106
  design.md 是消费模型的操作文档,不是抽取记录。`## Usage` 承载三件事,全部**可执行**(具体文件、具体坐标、具体顺序):
107
107
 
108
- 1. **三步指引**:① 搭任何一页之前先读 `layouts.md`,从页型清单里选 archetypeslots 坐标照抄;② 按资产用法表给该页铺底图/放 logo;③ 双主题包写明默认主题与 token 前缀切换法。
108
+ 1. **消费步骤**:① 画布取 `layouts.md` 的 `canvas`;② 从页型清单选 archetype,按其 flow / slots / decor / background 原样落版;③ `design.md` frontmatter 的 colors / typography / spacing / rounded / components 作为全局 token,局部 CSS 优先;④ 包内资产复制到项目相对目录后引用,字体使用完整 fallback 栈且不在运行时安装;⑤ 双主题包写明默认主题与 token 前缀切换法。
109
109
  2. **资产用法表**:每个资产一行——id、文件路径、用在哪类页、怎么摆(logo 给坐标,背景给首选序——如「封面首选 cover-art,无主视觉需求用 cover-dark」)。
110
- 3. **色板纪律一句**:所有颜色取自 `colors` token,强调色只用 primary 家族——风格与内容解耦,内容主题不改变色板。
110
+ 3. **强调色族纪律**:强调色族以 `colors` 段和 layout slot CSS 为主。必要时可使用其他颜色,但新增颜色须与模板整体的色相、明度和饱和度关系协调,且不能形成与模板主色竞争的第二强调色。中性色、低彩度辅助色或局部语义色可表达正负、风险、警告、状态、图表序列,但须保持辅助层级;新色不得通过高饱和、高对比、大面积、跨页重复,或用于标题、关键数字、图表主序列、卡片底色、渐变来获得主视觉权重。
111
+ 4. **交付检查**:逐页确认色板、字体、版式、背景、资产和 Hard Rules 均来自本包,并检查资源加载、内容溢出与画幅裁切。
111
112
 
112
- **Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md 取;颜色只从 colors 取。禁止句只用于无法正向表达的红线,且同句给替代。
113
+ **Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md 取;以 colors / layout slot CSS 为强调色基准,新增颜色与整体色板协调并保持辅助层级。禁止句只用于无法正向表达的红线,且同句给替代。
113
114
 
114
115
  ## 3. `layouts` 段(默认 sidecar)
115
116
 
@@ -123,13 +124,21 @@ layouts:
123
124
  themes: [dark, light] # 深浅孪生合并
124
125
  background: {dark: bg-cover-dark, light: bg-cover-light}
125
126
  slots:
126
- - {role: title, box: [110, 340, 970, 160], type: title}
127
- - {role: logo, box: [120, 121, 235, 74], asset: {dark: logo-on-dark, light: logo-on-light}}
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 声明串>"}
128
131
  confidence: high
129
132
  ```
130
133
 
131
134
  - **`background` 三形态**:`<asset-id>` / `{<theme>: <asset-id>}` / `{color: <colors-token>}`(`color` 是保留键,主题名禁止叫 color)。`asset` 两形态:`<asset-id>` / `{<theme>: <asset-id>}`。
132
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。纵向位置与留白由消费模型结合实际内容决定,不把样张的 `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` 是唯一事实源,逐项原样消费,不得归并或推断。
133
142
  - **`type` 封闭枚举**:`title | subtitle | body | pic | table | chart | media | slide-number | footer`。大数字/序号走 `type: title`,语义由 `role`(如 `big-number`)承担。
134
143
  - **`slots.*.role` 开放不校验**(语义槽位):优先复用已知词表(OOXML ST_SlideLayoutType / Slidev 20 布局 / Google PredefinedLayout,如 big-number、caption、main-point),确无对应再自造。
135
144
  - archetype ≤15(内联降级形态 ≤11);深浅孪生合并为一条;版式溯源/母版取舍进 `ref/`。
@@ -139,14 +148,14 @@ layouts:
139
148
 
140
149
  ```yaml
141
150
  safe-area: # 开放命名 map,可多套边距体系
142
- content: {top: 214, right: 133, bottom: 77, left: 133, applies-to: [content, quote]}
143
- editorial: {left: 120, right: 120, applies-to: [cover, section, closing]}
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]}
144
153
  confidence: medium
145
154
  ```
146
155
 
147
156
  冲突裁决:`slots.box` 是实例真值,`safe-area` 是归纳框架,**以 slots.box 为准**。
148
157
 
149
- ## 5. check_v2 校验(16 行:V2-1..V2-13 + V2-R5/R6/R7)
158
+ ## 5. check_v2 校验(19 行:V2-1..V2-16 + V2-R5/R6/R7)
150
159
 
151
160
  check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 design.md + layouts.md 求并集。
152
161
 
@@ -165,6 +174,9 @@ check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 des
165
174
  | V2-11 | 键名命中 YAML 1.1 布尔字面量 | FAIL |
166
175
  | V2-12 | `path`/`url`/`color` 恰好存在一个;`color` 仅 background 允许;`full` 仅可伴随 `path` | FAIL |
167
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 |
168
180
  | V2-R5 | sidecar frontmatter 重复 design.md 已有顶层键(`layouts` 载荷键与 `canvas` 除外——canvas 的家就在 sidecar) | WARN |
169
181
  | V2-R6 | assets 条目出现审计字段(boxes/aspect/mark/confidence)或 design.md 顶层出现 canvas/canvas-source/theme-mechanism/color-confidence——应移 `ref/audit.yaml` / layouts.md | WARN |
170
182
  | V2-R7 | 有 layouts sidecar 指针但正文未出现 `layouts.md` 字样(弱指针,消费者到不了版式数据) | WARN |