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

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 (82) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/charts/SKILL.md +15 -4
  3. package/steering/design-html/skills/data-viz/SKILL.md +1 -1
  4. package/steering/design-html/skills/hi-fi-design/SKILL.md +1 -1
  5. package/steering/design-html/skills/interactive-prototype/SKILL.md +1 -17
  6. package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
  7. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  8. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
  9. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  10. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2831 -293
  11. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
  12. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
  13. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
  14. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
  15. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  16. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  25. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
  26. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
  27. package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
  28. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
  29. package/steering/design-html/skills/slide-deck/SKILL.md +5 -3
  30. package/steering/design-html/skills/social-card/SKILL.md +263 -0
  31. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +4 -0
  32. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
  33. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
  34. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
  35. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +38 -113
  36. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +15 -2
  37. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +13 -7
  38. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +205 -38
  39. package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
  40. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +67 -4
  41. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +254 -0
  42. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +75 -0
  43. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +164 -543
  44. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +72 -15
  45. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
  46. package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
  47. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
  48. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
  49. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
  50. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
  51. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +90 -0
  52. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
  53. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
  54. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +248 -0
  55. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
  56. package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +3 -1
  57. package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +2 -0
  58. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +6 -0
  59. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +49 -22
  60. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +11 -3
  61. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +57 -14
  62. package/steering/vite-react/skills/plugin-guide/SKILL.md +4 -1
  63. package/steering/vite-react/skills/react-three-fiber/SKILL.md +5 -0
  64. package/steering/vite-react/tech.md +3 -0
  65. package/steering/design-html/skills/preflight/SKILL.md +0 -51
  66. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -108
  67. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  68. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
  69. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  70. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  71. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
  72. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  73. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  74. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
  75. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
  76. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
  77. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  78. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  79. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
  80. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
  81. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
  82. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env python3
2
+ """Integration regression tests for model-decided image 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 check_v2 import Pack, RULES # noqa: E402
12
+ from package import (Fail, apply_asset_decisions, bind_asset_vision_group_scopes,
13
+ load_layout_blocks, main as package_main, # noqa: E402
14
+ validate_asset_vision_groups)
15
+
16
+
17
+ class AssetJudgmentPackageTest(unittest.TestCase):
18
+ def test_asset_vision_index_requires_selected_candidates(self):
19
+ with tempfile.TemporaryDirectory() as lout:
20
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
21
+ 'w', encoding='utf-8') as stream:
22
+ json.dump({'version': 2}, stream)
23
+
24
+ with self.assertRaisesRegex(Fail, '缺少 selected'):
25
+ validate_asset_vision_groups(lout, {'asset_vision_groups': []})
26
+
27
+ def test_asset_vision_groups_must_keep_every_extracted_candidate(self):
28
+ with tempfile.TemporaryDirectory() as lout:
29
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
30
+ 'w', encoding='utf-8') as stream:
31
+ json.dump({
32
+ 'version': 2,
33
+ 'selected': [{
34
+ 'candidates': [{
35
+ 'id': 'asset-cover',
36
+ 'source_media': 'cover.png',
37
+ 'placements': [{
38
+ 'box': [100, 200, 300, 400],
39
+ 'archetype': 'cover',
40
+ }],
41
+ }, {
42
+ 'id': 'asset-ornament',
43
+ 'source_media': 'ornament.png',
44
+ 'placements': [{
45
+ 'box': [10, 20, 30, 40],
46
+ 'archetype': 'content',
47
+ }],
48
+ }],
49
+ }],
50
+ }, stream)
51
+ manifest = {
52
+ 'asset_vision_groups': [{
53
+ 'id': 'asset-cover',
54
+ 'source_media': 'cover.png',
55
+ 'layout': 'cover',
56
+ 'box': '[100, 200, 300, 400]',
57
+ 'visual_kind': 'illustration',
58
+ }, {
59
+ 'id': 'asset-ornament',
60
+ 'source_media': 'ornament.png',
61
+ 'layout': 'content',
62
+ 'box': '[10, 20, 30, 40]',
63
+ 'visual_kind': 'decorative',
64
+ }],
65
+ }
66
+
67
+ validate_asset_vision_groups(lout, manifest)
68
+ manifest['asset_vision_groups'].pop()
69
+
70
+ with self.assertRaisesRegex(Fail, '逐项保留抽取候选.*asset-ornament'):
71
+ validate_asset_vision_groups(lout, manifest)
72
+
73
+ def test_asset_vision_groups_reject_rewritten_extracted_candidate_scope(self):
74
+ with tempfile.TemporaryDirectory() as lout:
75
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
76
+ 'w', encoding='utf-8') as stream:
77
+ json.dump({
78
+ 'version': 2,
79
+ 'selected': [{
80
+ 'candidates': [{
81
+ 'id': 'asset-ornament',
82
+ 'source_media': 'ornament.png',
83
+ 'placements': [{
84
+ 'box': [10, 20, 30, 40],
85
+ 'archetype': None,
86
+ }],
87
+ }],
88
+ }],
89
+ }, stream)
90
+ manifest = {
91
+ 'asset_vision_groups': [{
92
+ 'id': 'asset-ornament',
93
+ 'source_media': 'other.png',
94
+ 'box': '[10, 20, 30, 40]',
95
+ 'visual_kind': 'decorative',
96
+ }],
97
+ }
98
+
99
+ with self.assertRaisesRegex(Fail, '改写了抽取候选的图片或位置'):
100
+ validate_asset_vision_groups(lout, manifest)
101
+
102
+ def test_v2_candidate_ids_resolve_same_box_on_different_layouts(self):
103
+ with tempfile.TemporaryDirectory() as lout:
104
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
105
+ 'w', encoding='utf-8') as stream:
106
+ json.dump({
107
+ 'version': 2,
108
+ 'selected': [{
109
+ 'candidates': [{
110
+ 'id': 'asset-cover-logo',
111
+ 'source_media': 'shared.png',
112
+ 'placements': [{
113
+ 'box': [100, 200, 80, 80],
114
+ 'archetype': 'cover',
115
+ }],
116
+ }, {
117
+ 'id': 'asset-content-ornament',
118
+ 'source_media': 'shared.png',
119
+ 'placements': [{
120
+ 'box': [100, 200, 80, 80],
121
+ 'archetype': 'content',
122
+ }],
123
+ }],
124
+ }],
125
+ }, stream)
126
+ manifest = {
127
+ 'assets': [],
128
+ 'asset_vision_groups': [{
129
+ 'id': 'asset-cover-logo',
130
+ 'source_media': 'shared.png',
131
+ 'box': '[100, 200, 80, 80]',
132
+ 'visual_kind': 'logo',
133
+ }, {
134
+ 'id': 'asset-content-ornament',
135
+ 'source_media': 'shared.png',
136
+ 'box': '[100, 200, 80, 80]',
137
+ 'visual_kind': 'decorative',
138
+ }],
139
+ }
140
+
141
+ bind_asset_vision_group_scopes(lout, manifest)
142
+ decisions, _ = apply_asset_decisions(
143
+ manifest,
144
+ bound_sources={'shared.png'},
145
+ bound_slots={
146
+ 'shared.png': {
147
+ ('cover', (100, 200, 80, 80)),
148
+ ('content', (100, 200, 80, 80)),
149
+ },
150
+ },
151
+ )
152
+
153
+ self.assertEqual(
154
+ decisions.for_scope('shared.png', 'cover', (100, 200, 80, 80)),
155
+ 'logo',
156
+ )
157
+ self.assertEqual(
158
+ decisions.for_scope('shared.png', 'content', (100, 200, 80, 80)),
159
+ 'texture',
160
+ )
161
+
162
+ def test_legacy_asset_vision_index_accepts_a_group_default(self):
163
+ with tempfile.TemporaryDirectory() as lout:
164
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
165
+ 'w', encoding='utf-8') as stream:
166
+ json.dump({
167
+ 'version': 1,
168
+ 'selected': [{
169
+ 'candidates': [{
170
+ 'id': 'asset-cover',
171
+ 'source_media': 'cover.png',
172
+ 'placements': [{'box': [0, 0, 100, 100]}],
173
+ }, {
174
+ 'id': 'asset-ornament',
175
+ 'source_media': 'ornament.png',
176
+ 'placements': [{'box': [10, 20, 30, 40]}],
177
+ }],
178
+ }],
179
+ }, stream)
180
+ manifest = {
181
+ 'asset_vision_groups': [{
182
+ 'id': 'vision-1',
183
+ 'source_media': '[cover.png, ornament.png]',
184
+ 'visual_kind': 'decorative',
185
+ }],
186
+ }
187
+
188
+ validate_asset_vision_groups(lout, manifest)
189
+
190
+ def test_layout_controls_override_legacy_judgment_without_reading_layout_todos(self):
191
+ with tempfile.TemporaryDirectory() as root:
192
+ lout = os.path.join(root, 'l-out')
193
+ os.makedirs(lout)
194
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
195
+ stream.write(
196
+ 'names:\n'
197
+ ' content: TODO中文名\n'
198
+ 'roles:\n'
199
+ ' content: TODO角色\n'
200
+ 'layouts:\n'
201
+ ' content:\n'
202
+ ' slots:\n'
203
+ ' - {role: body, box: [10, 20, 30, 40], type: body}\n'
204
+ ' confidence: high\n'
205
+ )
206
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
207
+ stream.write(
208
+ 'names:\n'
209
+ ' content: "内容页"\n'
210
+ 'roles:\n'
211
+ ' content: content\n'
212
+ )
213
+
214
+ blocks = load_layout_blocks(lout)
215
+
216
+ self.assertEqual([key for key, _, _ in blocks],
217
+ ['names', 'roles', 'layouts'])
218
+ self.assertIn('内容页', blocks[0][2][0])
219
+ self.assertIn('content', blocks[1][2][0])
220
+ self.assertNotIn('TODO', '\n'.join(
221
+ line for _, _, lines in blocks for line in lines
222
+ ))
223
+
224
+ def test_layout_controls_reject_non_judgment_block(self):
225
+ with tempfile.TemporaryDirectory() as root:
226
+ lout = os.path.join(root, 'l-out')
227
+ os.makedirs(lout)
228
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
229
+ stream.write('layouts:\n content:\n role: content\n')
230
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
231
+ stream.write('layouts:\n cover:\n role: cover\n')
232
+
233
+ with self.assertRaisesRegex(Fail, '只能包含'):
234
+ load_layout_blocks(lout)
235
+
236
+ def test_layout_controls_must_cover_all_legacy_judgment_blocks(self):
237
+ with tempfile.TemporaryDirectory() as root:
238
+ lout = os.path.join(root, 'l-out')
239
+ os.makedirs(lout)
240
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
241
+ stream.write(
242
+ 'names:\n'
243
+ ' content: "内容页"\n'
244
+ 'roles:\n'
245
+ ' content: content\n'
246
+ 'layouts:\n'
247
+ ' content:\n'
248
+ ' role: content\n'
249
+ )
250
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
251
+ encoding='utf-8') as stream:
252
+ stream.write('roles:\n content: content\n')
253
+
254
+ with self.assertRaisesRegex(Fail, '缺:names'):
255
+ load_layout_blocks(lout)
256
+
257
+ def test_layout_controls_must_keep_each_legacy_judgment_entry(self):
258
+ with tempfile.TemporaryDirectory() as root:
259
+ lout = os.path.join(root, 'l-out')
260
+ os.makedirs(lout)
261
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
262
+ stream.write(
263
+ 'names:\n'
264
+ ' cover: "封面"\n'
265
+ ' content: "内容页"\n'
266
+ 'roles:\n'
267
+ ' cover: cover\n'
268
+ ' content: content\n'
269
+ 'layouts:\n'
270
+ ' cover:\n'
271
+ ' role: cover\n'
272
+ ' content:\n'
273
+ ' role: content\n'
274
+ )
275
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
276
+ encoding='utf-8') as stream:
277
+ stream.write(
278
+ 'names:\n'
279
+ ' cover: "封面"\n'
280
+ 'roles:\n'
281
+ ' cover: cover\n'
282
+ ' content: content\n'
283
+ )
284
+
285
+ with self.assertRaisesRegex(Fail, 'names 段缺少旧判断条目:content'):
286
+ load_layout_blocks(lout)
287
+
288
+ def test_package_strips_judgment_fields_and_preserves_content_slot(self):
289
+ try:
290
+ from PIL import Image
291
+ except ImportError:
292
+ self.skipTest('Pillow unavailable')
293
+
294
+ with tempfile.TemporaryDirectory() as root:
295
+ stage1 = os.path.join(root, 'stage1')
296
+ lout = os.path.join(stage1, 'l-out')
297
+ pack = os.path.join(root, 'pack')
298
+ media = os.path.join(stage1, 'media-out')
299
+ ref = os.path.join(stage1, 'ref')
300
+ os.makedirs(lout)
301
+ os.makedirs(media)
302
+ os.makedirs(ref)
303
+
304
+ Image.new('RGBA', (40, 20), (20, 40, 60, 255)).save(
305
+ os.path.join(media, 'partner-logo.png'))
306
+ Image.new('RGBA', (24, 24), (60, 40, 20, 255)).save(
307
+ os.path.join(media, 'ornament.png'))
308
+
309
+ box_rows = [
310
+ ('ppt/media/image1.png', 'partner-logo.png', [100, 200, 400, 200]),
311
+ ('ppt/media/image2.png', 'ornament.png', [40, 40, 120, 120]),
312
+ ]
313
+ extract = {
314
+ 'source': {'filename': 'fixture.pptx'},
315
+ 'canvas': {
316
+ 'px': [1920, 1080],
317
+ 'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
318
+ },
319
+ 'media': [
320
+ {
321
+ 'media': source,
322
+ 'out': 'media-out/' + output,
323
+ 'source_px': [40, 20] if output == 'partner-logo.png' else [24, 24],
324
+ }
325
+ for source, output, _ in box_rows
326
+ ],
327
+ 'images': [
328
+ {
329
+ 'media': source,
330
+ 'boxes': [{
331
+ 'count': 1,
332
+ 'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
333
+ 'parts': ['ppt/slides/slide1.xml'],
334
+ }],
335
+ }
336
+ for source, _, box in box_rows
337
+ ],
338
+ 'color_freq': [],
339
+ 'text_scale': [],
340
+ }
341
+ with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
342
+ json.dump(extract, stream)
343
+ with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
344
+ json.dump({
345
+ 'shapes': [
346
+ {
347
+ 'part': 'ppt/slides/slide1.xml',
348
+ 'kind': 'pic',
349
+ 'name': output,
350
+ 'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
351
+ }
352
+ for _, output, box in box_rows
353
+ ],
354
+ }, stream)
355
+
356
+ with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
357
+ stream.write(
358
+ 'version: alpha\n'
359
+ 'name: asset-judgment-fixture\n'
360
+ 'description: >\n'
361
+ ' A restrained fixture with explicit image-role judgments and traced layout slots.\n'
362
+ 'assets:\n'
363
+ ' - id: logo-primary\n'
364
+ ' source_media: partner-logo.png\n'
365
+ ' kind: logo\n'
366
+ ' on-bg: light\n'
367
+ 'asset_vision_groups:\n'
368
+ ' - id: vision-1\n'
369
+ ' source_media: [partner-logo.png, ornament.png]\n'
370
+ ' visual_kind: content-image\n'
371
+ 'asset_decisions:\n'
372
+ ' - source_media: ornament.png\n'
373
+ ' visual_kind: decorative\n'
374
+ )
375
+ with open(os.path.join(lout, 'frontmatter.yaml'), 'w', encoding='utf-8') as stream:
376
+ stream.write('')
377
+ with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
378
+ stream.write(
379
+ 'roles:\n'
380
+ ' content: TODO角色\n'
381
+ 'layouts:\n'
382
+ ' content:\n'
383
+ ' name: "内容页"\n'
384
+ ' role: content\n'
385
+ ' slots:\n'
386
+ ' - {role: logo, box: [100, 200, 400, 200], type: pic, '
387
+ 'asset: logo-primary, source_media: partner-logo.png}\n'
388
+ ' - {role: asset-candidate, box: [40, 40, 120, 120], type: pic, '
389
+ 'source_media: ornament.png}\n'
390
+ ' confidence: high\n'
391
+ )
392
+ with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
393
+ encoding='utf-8') as stream:
394
+ stream.write('roles:\n content: content\n')
395
+ with open(os.path.join(lout, 'body.md'), 'w', encoding='utf-8') as stream:
396
+ stream.write(
397
+ '## Usage\n\n'
398
+ 'Read `layouts.md` before composing a slide.\n\n'
399
+ '## Assets\n\n'
400
+ '{{ASSET_TABLE}}\n\n'
401
+ '## Hard Rules\n\n'
402
+ '{{LOGO_RULES}}\n'
403
+ )
404
+ check_v1 = os.path.join(root, 'check_v1.py')
405
+ with open(check_v1, 'w', encoding='utf-8') as stream:
406
+ stream.write('raise SystemExit(0)\n')
407
+
408
+ rc = package_main([stage1, lout, pack, '--check-v1', check_v1])
409
+
410
+ self.assertEqual(rc, 0)
411
+ with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
412
+ design = stream.read()
413
+ with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
414
+ layouts = stream.read()
415
+ self.assertNotIn('asset_decisions', design)
416
+ self.assertNotIn('asset_vision_groups', design)
417
+ self.assertNotIn('visual_kind', design)
418
+ self.assertNotIn('source_media', design)
419
+ self.assertNotIn('source_media', layouts)
420
+ self.assertNotIn('TODO', layouts)
421
+ self.assertNotIn('{{LOGO_RULES}}', design)
422
+ self.assertNotIn('logo-primary', design)
423
+ self.assertNotIn('logo-primary', layouts)
424
+ self.assertIn(
425
+ '- {role: pic, box: [100, 200, 400, 200], type: pic}',
426
+ layouts,
427
+ )
428
+ self.assertIn('texture-1:', design)
429
+ self.assertIn('kind: texture', design)
430
+ self.assertIn(
431
+ '- {role: texture, box: [40, 40, 120, 120], type: pic, asset: texture-1}',
432
+ layouts,
433
+ )
434
+ gate = Pack(pack)
435
+ self.assertFalse([
436
+ result
437
+ for result in (rule(gate) for rule in RULES)
438
+ if result.fails
439
+ ])
440
+
441
+ def test_package_places_a_rendered_picture_instance_as_a_logo_asset(self):
442
+ try:
443
+ from PIL import Image
444
+ except ImportError:
445
+ self.skipTest('Pillow unavailable')
446
+
447
+ with tempfile.TemporaryDirectory() as root:
448
+ stage1 = os.path.join(root, 'stage1')
449
+ lout = os.path.join(stage1, 'l-out')
450
+ pack = os.path.join(root, 'pack')
451
+ media = os.path.join(stage1, 'media-out')
452
+ ref = os.path.join(stage1, 'ref')
453
+ os.makedirs(lout)
454
+ os.makedirs(media)
455
+ os.makedirs(ref)
456
+
457
+ source = 'generated/instance/picture-directory-logo.webp'
458
+ output = 'picture-directory-logo.webp'
459
+ box = [1813, 18, 59, 228]
460
+ Image.new('RGBA', (59, 228), (20, 60, 100, 255)).save(
461
+ os.path.join(media, output), 'WEBP')
462
+ extract = {
463
+ 'source': {'filename': 'fixture.pptx'},
464
+ 'canvas': {
465
+ 'px': [1920, 1080],
466
+ 'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
467
+ },
468
+ 'media': [{
469
+ 'media': source,
470
+ 'out': 'media-out/' + output,
471
+ 'generated': True,
472
+ 'rendered_from': 'ppt/media/original-logo.png',
473
+ }],
474
+ 'images': [{
475
+ 'media': source,
476
+ 'boxes': [{
477
+ 'count': 1,
478
+ 'box': dict(zip(('x', 'y', 'w', 'h'), box)),
479
+ 'parts': ['ppt/slides/slide2.xml'],
480
+ }],
481
+ }],
482
+ 'color_freq': [],
483
+ 'text_scale': [],
484
+ }
485
+ with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
486
+ json.dump(extract, stream)
487
+ with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
488
+ json.dump({'shapes': [{
489
+ 'part': 'ppt/slides/slide2.xml',
490
+ 'kind': 'pic',
491
+ 'box': dict(zip(('x', 'y', 'w', 'h'), box)),
492
+ }]}, stream)
493
+ with open(os.path.join(lout, 'asset-vision-groups.json'),
494
+ 'w', encoding='utf-8') as stream:
495
+ json.dump({
496
+ 'version': 2,
497
+ 'selected': [{
498
+ 'candidates': [{
499
+ 'id': 'asset-directory-logo',
500
+ 'source_media': output,
501
+ 'placements': [{
502
+ 'slide': 2,
503
+ 'archetype': 'content',
504
+ 'box': box,
505
+ }],
506
+ }],
507
+ }],
508
+ 'omitted': [],
509
+ }, stream)
510
+ with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
511
+ stream.write(
512
+ 'version: alpha\n'
513
+ 'name: rendered-instance-fixture\n'
514
+ 'description: Rendered local PPT image instances remain reusable assets.\n'
515
+ 'asset_vision_groups:\n'
516
+ ' - id: asset-directory-logo\n'
517
+ ' source_media: picture-directory-logo.webp\n'
518
+ ' box: [1813, 18, 59, 228]\n'
519
+ ' visual_kind: logo\n'
520
+ )
521
+ for name, content in (
522
+ ('frontmatter.yaml', ''),
523
+ ('body.md', 'Read `layouts.md` before composing a slide.\n\n{{ASSET_TABLE}}\n'),
524
+ ('layouts.yaml',
525
+ 'layouts:\n'
526
+ ' content:\n'
527
+ ' name: "目录"\n'
528
+ ' role: content\n'
529
+ ' slots:\n'
530
+ ' - {role: asset-candidate, box: [1813, 18, 59, 228], type: pic, '
531
+ 'source_media: picture-directory-logo.webp}\n'
532
+ ' confidence: high\n'),
533
+ ):
534
+ with open(os.path.join(lout, name), 'w', encoding='utf-8') as stream:
535
+ stream.write(content)
536
+ check_v1 = os.path.join(root, 'check_v1.py')
537
+ with open(check_v1, 'w', encoding='utf-8') as stream:
538
+ stream.write('raise SystemExit(0)\n')
539
+
540
+ self.assertEqual(package_main([stage1, lout, pack, '--check-v1', check_v1]), 0)
541
+
542
+ with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
543
+ design = stream.read()
544
+ with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
545
+ layouts = stream.read()
546
+ self.assertTrue(os.path.isfile(os.path.join(pack, 'assets', 'logos', '1.webp')))
547
+ self.assertIn('logo-1:', design)
548
+ self.assertIn('assets/logos/1.webp', design)
549
+ self.assertIn(
550
+ '- {role: logo, box: [1813, 18, 59, 228], type: pic, asset: logo-1}',
551
+ layouts,
552
+ )
553
+
554
+
555
+ if __name__ == '__main__':
556
+ unittest.main()