openyida 2026.9.11 → 2026.9.13

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/README.md +7 -5
  2. package/bin/yida.js +1 -0
  3. package/lib/ai/ai.js +2 -0
  4. package/lib/app/canvas-compile.js +29 -26
  5. package/lib/app/compile.js +39 -1
  6. package/lib/app/services/canvas-page-compiler.js +3 -3
  7. package/lib/app/update-app.js +17 -2
  8. package/lib/asset/ai-image.js +37 -65
  9. package/lib/asset/asset-cmd.js +151 -110
  10. package/lib/asset/asset-plan.js +105 -0
  11. package/lib/asset/asset-resolve.js +343 -225
  12. package/lib/asset/asset-status.js +20 -31
  13. package/lib/asset/attachment-upload.js +29 -0
  14. package/lib/asset/host-capabilities.js +91 -0
  15. package/lib/asset/image-metadata.js +104 -0
  16. package/lib/asset/url-verify.js +90 -130
  17. package/lib/core/agent-capabilities.js +11 -0
  18. package/lib/core/command-manifest.js +25 -11
  19. package/lib/core/locales/en.js +6 -2
  20. package/lib/core/locales/zh.js +6 -2
  21. package/lib/design-plan/materialize.js +7 -9
  22. package/lib/design-plan/normalize.js +11 -1
  23. package/lib/process/services/process-actions.js +145 -0
  24. package/lib/process/services/process-compiler.js +4 -30
  25. package/lib/process/services/process-view-verifier.js +31 -0
  26. package/package.json +1 -1
  27. package/scripts/postinstall.js +1 -1
  28. package/yida-skills/SKILL.md +3 -2
  29. package/yida-skills/skills/yida-app/SKILL.md +1 -1
  30. package/yida-skills/skills/yida-app/workflow/step-2-design.md +11 -1
  31. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +21 -1
  32. package/yida-skills/skills/yida-app/workflow/step-5-seed-records.md +11 -7
  33. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +4 -0
  34. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -3
  35. package/yida-skills/skills/yida-canvas-custom-page/references/component-library-guide.md +1 -1
  36. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +3 -27
  37. package/yida-skills/skills/yida-canvas-table-form/SKILL.md +1 -1
  38. package/yida-skills/skills/yida-create-app/SKILL.md +4 -4
  39. package/yida-skills/skills/yida-create-process/SKILL.md +2 -0
  40. package/yida-skills/skills/yida-custom-page/references/assets-guide.md +9 -12
  41. package/yida-skills/skills/yida-design/SKILL.md +1 -0
  42. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +4 -4
  43. package/yida-skills/skills/yida-design/references/asset-workflow.md +14 -53
  44. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
  45. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +21 -4
  46. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +1 -2
  47. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/validate_design_themes.py +1 -1
  48. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/README.md +1 -1
  49. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +1 -1
  50. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +1 -1
  51. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +1 -1
  52. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +1 -1
  53. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +1 -1
  54. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +1 -1
  55. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +1 -1
  56. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +1 -1
  57. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +1 -1
  58. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +1 -1
  59. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +1 -1
  60. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +1 -1
  61. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +1 -1
  62. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +1 -1
  63. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +2 -2
  64. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +1 -1
  65. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +1 -1
  66. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +1 -1
  67. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +1 -1
  68. package/yida-skills/skills/yida-design/workflow/output-design.md +4 -0
  69. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
  70. package/yida-skills/skills/yida-design/workflow/step-5-visual-states.md +2 -1
  71. package/yida-skills/skills/yida-image-assets/SKILL.md +91 -0
  72. package/yida-skills/skills/yida-image-assets/references/manifest-contract.md +87 -0
  73. package/yida-skills/skills/yida-image-assets/references/source-policy.md +19 -0
  74. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +1 -1
  75. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  76. package/yida-skills/skills/yida-process-rule/SKILL.md +6 -1
  77. package/yida-skills/skills/yida-process-rule/references/approval-actions.md +48 -0
  78. package/yida-skills/skills/yida-publish-page/SKILL.md +5 -4
  79. package/yida-skills/skills/yida-rechart/SKILL.md +1 -1
  80. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
  81. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +22 -13
  82. package/yida-skills/skills-index.json +18 -1
@@ -1,82 +1,104 @@
1
-
2
- /**
3
- * asset-cmd.js - `openyida asset` 素材能力命令
4
- *
5
- * 素材工具的统一入口,把「检测素材能力 / 解析回填 / 素材来源引导」
6
- * 暴露为可脚本化命令,供本地智能体在生成官网/落地页前调用,
7
- * 从而做到:无 CDN 不谎称已上传、无可用素材不交付「最终版」。
8
- *
9
- * 子命令:
10
- * status 检测当前素材能力(CDN / 生成 / 推荐策略)
11
- * resolve --hero <..> [--product <..>] [--upload-assets] 解析并回填一组素材,输出 materialStatus/缺口
12
- * generate 输出素材来源引导(免费素材库清单 + 纪律)
13
- *
14
- * 所有子命令均支持 --json 便于智能体解析。
15
- */
16
-
17
1
  'use strict';
18
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readDesignAssetStrategy } = require('./asset-plan');
19
6
  const { getAssetStatus } = require('./asset-status');
20
- const { resolveAssets } = require('./asset-resolve');
21
- const {
22
- detectImageGenerator,
23
- getMaterialSourcingGuidance,
24
- } = require('./ai-image');
25
- const { throwCommandError, throwStatus } = require('../core/command-errors');
7
+ const { resolveAssets, ASSET_SOURCES } = require('./asset-resolve');
8
+ const { detectImageGenerator, getMaterialSourcingGuidance } = require('./ai-image');
9
+ const { throwCommandError, throwStatus, throwUsage } = require('../core/command-errors');
10
+
11
+ const RESOLVE_USAGE = '用法: openyida asset resolve (--input <manifest-draft.json> | --slot <slotId>=<路径或URL> ...) [--manifest <asset-manifest.json>] [--design <design.md>] [--source search|user|generated] [--app-type <appType>] [--upload-assets] [--offline] [--json]';
26
12
 
27
13
  function printHelp() {
28
14
  console.log(`
29
15
  用法:
30
16
  openyida asset status [--offline] [--json]
31
- openyida asset resolve --hero <路径或URL> [--product <路径或URL> ...] [--require-hero] [--upload-assets] [--offline] [--json]
32
- openyida asset generate [--json]
17
+ openyida asset resolve (--input <manifest-draft.json> | --slot <slotId>=<路径或URL> ...) [--manifest <asset-manifest.json>] [--design <design.md>] [--source search|user|generated] [--app-type <appType>] [--upload-assets] [--offline] [--json]
18
+ openyida asset sources [--json]
33
19
 
34
20
  子命令:
35
- status 检测当前素材能力:是否配置 CDN、是否可生成、推荐素材策略
36
- resolve 把本地/外链素材解析为稳定 URL 并回填,输出 materialStatus(final|draft|none) 与缺口
37
- generate 输出素材来源引导(免费可商用素材库清单 + 使用纪律,供智能体检索真实图片)
21
+ status 检测素材能力
22
+ resolve 校验图片、默认上传宜搭图片附件并生成清单
38
23
 
39
- 说明:
40
- openyida 无内置文生图能力;图片应由智能体自行生成或到免费素材库检索,
41
- 再通过 resolve 解析,并在配置 CDN 时转存后回填 spec。
24
+ 图片不超过 20 MiB 时上传;超过限制的外链保留原地址。
25
+ --app-type 指定目标应用,省略时读取项目 config.json。
26
+ --upload-assets 仅兼容旧命令,默认已上传;--offline 不联网、不上传。
27
+ sources 输出 Unsplash/Pexels 使用规则
42
28
  `);
43
29
  }
44
30
 
31
+ function parseSlot(value) {
32
+ const index = String(value || '').indexOf('=');
33
+ if (index <= 0 || index === String(value).length - 1) {return null;}
34
+ const slotId = String(value).slice(0, index).trim();
35
+ const input = String(value).slice(index + 1).trim();
36
+ if (!slotId || !input) {return null;}
37
+ return {
38
+ slotId,
39
+ input,
40
+ required: true,
41
+ };
42
+ }
43
+
45
44
  function parseArgs(args) {
46
45
  const parsed = {
47
46
  subCommand: args[0],
48
- hero: '',
49
- products: [],
50
- requireHero: false,
47
+ slots: [],
48
+ input: '',
49
+ manifest: '',
50
+ design: '',
51
51
  offline: false,
52
52
  uploadAssets: false,
53
+ appType: '',
54
+ source: 'search',
53
55
  timeout: 0,
54
56
  json: false,
55
57
  help: false,
58
+ errors: [],
59
+ usedOptions: [],
56
60
  };
57
-
58
61
  for (let i = 1; i < args.length; i++) {
59
62
  const arg = args[i];
60
63
  if (arg === '--help' || arg === '-h') {
61
64
  parsed.help = true;
62
65
  } else if (arg === '--json') {
66
+ parsed.usedOptions.push('--json');
63
67
  parsed.json = true;
64
68
  } else if (arg === '--offline') {
69
+ parsed.usedOptions.push('--offline');
65
70
  parsed.offline = true;
66
- } else if (arg === '--require-hero') {
67
- parsed.requireHero = true;
68
- } else if (arg === '--upload-assets' || arg === '--mirror-external') {
71
+ } else if (arg === '--app-type') {
72
+ parsed.usedOptions.push('--app-type');
73
+ parsed.appType = args[++i] || '';
74
+ if (!parsed.appType || parsed.appType.startsWith('--')) {parsed.errors.push('ASSET_APP_TYPE_REQUIRED');}
75
+ } else if (arg === '--upload-assets') {
76
+ parsed.usedOptions.push('--upload-assets');
69
77
  parsed.uploadAssets = true;
70
- } else if (arg === '--hero') {
71
- parsed.hero = args[++i] || '';
72
- } else if (arg === '--product') {
73
- const v = args[++i];
74
- if (v) {parsed.products.push(v);}
78
+ } else if (arg === '--source') {
79
+ parsed.usedOptions.push('--source');
80
+ parsed.source = args[++i] || '';
81
+ } else if (arg === '--input') {
82
+ parsed.usedOptions.push('--input');
83
+ parsed.input = args[++i] || '';
84
+ } else if (arg === '--design') {
85
+ parsed.usedOptions.push('--design');
86
+ parsed.design = args[++i] || '';
87
+ if (!parsed.design || parsed.design.startsWith('--')) {parsed.errors.push('ASSET_DESIGN_REQUIRED');}
88
+ } else if (arg === '--manifest') {
89
+ parsed.usedOptions.push('--manifest');
90
+ parsed.manifest = args[++i] || '';
91
+ } else if (arg === '--slot') {
92
+ parsed.usedOptions.push('--slot');
93
+ const slot = parseSlot(args[++i]);
94
+ if (slot) {parsed.slots.push(slot);} else {parsed.errors.push('--slot 格式应为 <slotId>=<路径或URL>');}
75
95
  } else if (arg === '--timeout') {
96
+ parsed.usedOptions.push('--timeout');
76
97
  parsed.timeout = parseInt(args[++i], 10) || 0;
98
+ } else {
99
+ parsed.errors.push(`未知参数:${arg}`);
77
100
  }
78
101
  }
79
-
80
102
  return parsed;
81
103
  }
82
104
 
@@ -87,74 +109,88 @@ function runStatus(options) {
87
109
  return;
88
110
  }
89
111
  console.log('素材能力检测');
90
- console.log(' CDN 已配置 : ' + (status.cdnConfigured ? '是' : '否'));
91
- console.log(' 可上传转存 : ' + (status.canUpload ? '是' : '否'));
92
- console.log(' 可 AI 生成 : ' + (status.canGenerate ? '是' : '否(由智能体生成/检索素材)'));
93
- console.log(' 推荐素材策略 : ' + status.recommendedStrategy);
94
- console.log(' CDN 配置文件 : ' + status.cdn.configPath);
95
- if (status.cdn.missing.length) {
96
- console.log(' 缺失 CDN 字段 : ' + status.cdn.missing.join(', '));
112
+ console.log(' 上传目标 : 宜搭图片附件(需要登录态和 appType)');
113
+ console.log(' 上传条件 : ' + (status.canUpload ? '联网已启用;实际上传时验证登录态和应用权限' : '离线,不上传'));
114
+ console.log(' 可生成图片 : ' + (status.canGenerate === true ? '是' : status.canGenerate === false ? '否' : '未确认'));
115
+ console.log(' 建议策略 : ' + status.recommendedStrategy);
116
+ status.reasons.forEach(reason => console.log(' - ' + reason));
117
+ }
118
+
119
+ function readDraft(filePath) {
120
+ let draft;
121
+ try {
122
+ draft = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
123
+ } catch (error) {
124
+ throwCommandError(`无法读取素材草稿:${error.message}`, { code: 'ASSET_INPUT_INVALID' });
97
125
  }
98
- console.log('原因/建议:');
99
- status.reasons.forEach((r) => console.log(' - ' + r));
126
+ if (!draft || !Array.isArray(draft.assets)) {
127
+ throwCommandError('素材草稿必须包含 assets 数组', { code: 'ASSET_INPUT_INVALID' });
128
+ }
129
+ return draft;
130
+ }
131
+
132
+ function writeManifest(filePath, manifest) {
133
+ const target = path.resolve(filePath);
134
+ fs.mkdirSync(path.dirname(target), { recursive: true });
135
+ fs.writeFileSync(target, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
136
+ return target;
100
137
  }
101
138
 
102
139
  async function runResolve(options) {
103
- const assets = {
104
- heroImage: options.hero,
105
- productImages: options.products,
106
- };
140
+ if (options.errors.length > 0) {
141
+ throwUsage(options.errors.join('\n'), RESOLVE_USAGE, { code: 'ASSET_INVALID_ARGUMENTS' });
142
+ }
143
+ if (options.input && options.slots.length > 0) {
144
+ throwUsage('--input 与 --slot 不能同时使用', RESOLVE_USAGE, { code: 'ASSET_INPUT_CONFLICT' });
145
+ }
146
+ if (!ASSET_SOURCES.includes(options.source)) {
147
+ throwUsage('--source 仅支持 search、user 或 generated', RESOLVE_USAGE, { code: 'ASSET_INVALID_SOURCE' });
148
+ }
149
+ const draft = options.input ? readDraft(options.input) : {};
150
+ const assetStrategy = options.design ? readDesignAssetStrategy(path.resolve(options.design)) : draft.assetStrategy;
151
+ const assets = [
152
+ ...(draft.assets || []),
153
+ ...options.slots.map(slot => ({ ...slot, source: options.source })),
154
+ ];
155
+ if (assets.length === 0 && !options.input) {
156
+ throwUsage('至少提供一个 --slot 或 --input', RESOLVE_USAGE, { code: 'ASSET_INPUT_REQUIRED' });
157
+ }
158
+
107
159
  const result = await resolveAssets(assets, {
108
- requireHero: options.requireHero,
160
+ assetStrategy,
109
161
  online: !options.offline,
110
- mirrorExternal: !!options.uploadAssets,
162
+ appType: options.appType,
163
+ assetSource: options.source,
111
164
  timeout: options.timeout || undefined,
112
165
  });
113
- if (options.json) {
114
- console.log(JSON.stringify(result, null, 2));
115
- return;
116
- }
117
- console.log('素材解析结果');
118
- console.log(' materialStatus : ' + result.materialStatus +
119
- (result.materialStatus === 'final' ? '(可交付最终版)'
120
- : result.materialStatus === 'draft' ? '(仅可交付标注缺口的草稿)'
121
- : '(无可用素材,只能低保真草稿)'));
122
- console.log(' strategy : ' + result.strategy);
123
- console.log(' Hero : ' + (result.assets.heroImage || '(无)'));
124
- console.log(' 产品图 : ' + (result.assets.productImages.length
125
- ? result.assets.productImages.join(', ') : '(无)'));
126
- if (result.materialGaps.length) {
127
- console.log('素材缺口:');
128
- result.materialGaps.forEach((g) => console.log(' - ' + g));
129
- }
130
- // 非 final 时以非零退出码提示调用方(生成链路可据此拦截「最终版」)
131
- if (result.materialStatus !== 'final') {
166
+ const manifestPath = options.manifest ? writeManifest(options.manifest, result) : '';
167
+ if (result.materialStatus === 'draft') {
132
168
  throwStatus('素材未达到 final 状态', 2, {
133
169
  code: 'ASSET_MATERIAL_NOT_FINAL',
134
- details: result,
170
+ details: { ...result, manifestPath },
135
171
  });
136
172
  }
173
+ if (options.json) {
174
+ console.log(JSON.stringify({ ...result, manifestPath }, null, 2));
175
+ return;
176
+ }
177
+ console.log('素材状态: ' + result.materialStatus);
178
+ console.log('素材数量: ' + result.assets.length);
179
+ if (manifestPath) {console.log('素材清单: ' + manifestPath);}
137
180
  }
138
181
 
139
- function runGenerate(options) {
140
- const generator = detectImageGenerator();
141
- const guidance = getMaterialSourcingGuidance();
182
+ function runSources(options) {
183
+ const status = getAssetStatus();
184
+ const output = {
185
+ generator: detectImageGenerator({ hostCapabilities: status.hostCapabilities }),
186
+ guidance: getMaterialSourcingGuidance(),
187
+ };
142
188
  if (options.json) {
143
- console.log(JSON.stringify({ generator, guidance }, null, 2));
189
+ console.log(JSON.stringify(output, null, 2));
144
190
  return;
145
191
  }
146
- console.log('图片生成能力: ' + (generator.available ? '可用' : '不可用'));
147
- console.log(' ' + generator.reason);
148
- console.log('\n素材来源步骤:');
149
- guidance.steps.forEach((s) => console.log(' ' + s));
150
- console.log('\n免费可商用素材库:');
151
- guidance.libraries.forEach((lib) => {
152
- console.log(' · ' + lib.name + ' (' + lib.site + ')');
153
- console.log(' 授权: ' + lib.license);
154
- console.log(' 适合: ' + lib.bestFor);
155
- });
156
- console.log('\n纪律:');
157
- guidance.rules.forEach((r) => console.log(' - ' + r));
192
+ console.log('图片来源: 用户素材、Unsplash、Pexels 或宿主生成');
193
+ output.guidance.rules.forEach(rule => console.log(' - ' + rule));
158
194
  }
159
195
 
160
196
  async function run(args) {
@@ -163,22 +199,27 @@ async function run(args) {
163
199
  printHelp();
164
200
  return;
165
201
  }
166
- switch (options.subCommand) {
167
- case 'status':
168
- runStatus(options);
169
- break;
170
- case 'resolve':
171
- await runResolve(options);
172
- break;
173
- case 'generate':
174
- runGenerate(options);
175
- break;
176
- default:
177
- printHelp();
178
- throwCommandError('未知子命令: ' + options.subCommand, {
179
- code: 'ASSET_UNKNOWN_SUBCOMMAND',
180
- });
202
+ const allowedOptions = {
203
+ status: new Set(['--offline', '--json']),
204
+ sources: new Set(['--json']),
205
+ };
206
+ if (allowedOptions[options.subCommand]) {
207
+ const invalid = options.usedOptions.filter(option => !allowedOptions[options.subCommand].has(option));
208
+ const errors = [...options.errors, ...invalid.map(option => `参数不适用于 ${options.subCommand}:${option}`)];
209
+ if (errors.length > 0) {
210
+ throwCommandError(errors.join('\n'), { code: 'ASSET_INVALID_ARGUMENTS' });
211
+ }
181
212
  }
213
+ if (options.subCommand === 'status') {runStatus(options); return;}
214
+ if (options.subCommand === 'resolve') {await runResolve(options); return;}
215
+ if (options.subCommand === 'sources') {runSources(options); return;}
216
+ throwCommandError(`未知子命令:${options.subCommand}`, { code: 'ASSET_UNKNOWN_SUBCOMMAND' });
182
217
  }
183
218
 
184
- module.exports = { run, parseArgs };
219
+ module.exports = {
220
+ run,
221
+ parseArgs,
222
+ parseSlot,
223
+ readDraft,
224
+ writeManifest,
225
+ };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { CliError } = require('../core/cli-error');
5
+
6
+ function invalid(code, details = {}) {
7
+ throw new CliError(code, { code, details });
8
+ }
9
+
10
+ function object(value) {
11
+ return value && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+
14
+ function nonempty(value) {
15
+ return typeof value === 'string' && value.trim().length > 0;
16
+ }
17
+
18
+ function minimumSize(asset) {
19
+ const match = typeof asset.minSize === 'string' && asset.minSize.match(/^(\d+)x(\d+)$/i);
20
+ if (asset.minSize !== undefined && !match) {
21
+ invalid('ASSET_INVALID_SIZE', { slotId: asset.slotId, minSize: asset.minSize });
22
+ }
23
+ const values = [asset.minWidth ?? 0, asset.minHeight ?? 0];
24
+ const declared = match ? [Number(match[1]), Number(match[2])] : [0, 0];
25
+ if ([...values, ...declared].some(value => !Number.isSafeInteger(value) || value < 0) || (match && declared.some(value => !value))) {
26
+ invalid('ASSET_INVALID_SIZE', { slotId: asset.slotId });
27
+ }
28
+ return { minWidth: Math.max(values[0], declared[0]), minHeight: Math.max(values[1], declared[1]) };
29
+ }
30
+
31
+ function readDesignAssetStrategy(file) {
32
+ let markdown;
33
+ try { markdown = fs.readFileSync(file, 'utf8'); } catch (error) {
34
+ invalid('ASSET_DESIGN_INVALID', { file, reason: error.message });
35
+ }
36
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(markdown);
37
+ const lines = frontmatter ? frontmatter[1].split(/\r?\n/).filter(line => /^assetStrategy:/.test(line)) : [];
38
+ if (lines.length !== 1) {invalid('ASSET_DESIGN_INVALID', { file, field: 'assetStrategy', format: 'single-line JSON in frontmatter' });}
39
+ try { return JSON.parse(lines[0].slice('assetStrategy:'.length).trim()); } catch {
40
+ invalid('ASSET_DESIGN_INVALID', { file, field: 'assetStrategy', format: 'single-line JSON in frontmatter' });
41
+ }
42
+ }
43
+
44
+ // The design owns page membership, required slots and minimum dimensions.
45
+ function prepareAssetPlan(assets, assetStrategy) {
46
+ if (!Array.isArray(assets)) {invalid('ASSET_INPUT_INVALID', { field: 'assets' });}
47
+ const seen = new Set();
48
+ const entries = assets.map(asset => {
49
+ if (!object(asset) || !nonempty(asset.slotId)) {invalid('ASSET_INPUT_INVALID', { field: 'assets[].slotId' });}
50
+ const slotId = asset.slotId.trim();
51
+ if (seen.has(slotId)) {invalid('ASSET_DUPLICATE_SLOT', { slotId });}
52
+ seen.add(slotId);
53
+ minimumSize(asset);
54
+ return { ...asset, slotId };
55
+ });
56
+ if (assetStrategy === undefined) {return { entries, pages: [], assetStrategy: undefined };}
57
+ if (!object(assetStrategy) || !Array.isArray(assetStrategy.pages)) {
58
+ invalid('ASSET_DESIGN_INVALID', { field: 'assetStrategy.pages' });
59
+ }
60
+ const pageIds = new Set();
61
+ const slots = new Map();
62
+ const pages = assetStrategy.pages.map(page => {
63
+ if (!object(page) || !nonempty(page.pageId) || !['required', 'beneficial', 'none'].includes(page.imageNeed) || !Array.isArray(page.slots)) {
64
+ invalid('ASSET_DESIGN_INVALID', { field: 'assetStrategy.pages[]' });
65
+ }
66
+ const pageId = page.pageId.trim();
67
+ if (pageIds.has(pageId)) {invalid('ASSET_DUPLICATE_PAGE', { pageId });}
68
+ pageIds.add(pageId);
69
+ if (page.imageNeed === 'none' && page.slots.length) {invalid('ASSET_DESIGN_INVALID', { pageId, field: 'slots' });}
70
+ const slotIds = [];
71
+ page.slots.forEach(slot => {
72
+ if (!object(slot) || !nonempty(slot.slotId) || !nonempty(slot.usage)) {
73
+ invalid('ASSET_DESIGN_INVALID', { pageId, field: 'slots[].slotId/usage' });
74
+ }
75
+ const count = slot.count ?? 1;
76
+ if (!Number.isInteger(count) || count < 1 || count > 100) {invalid('ASSET_DESIGN_INVALID', { pageId, slotId: slot.slotId, field: 'count' });}
77
+ minimumSize(slot);
78
+ for (let index = 0; index < count; index++) {
79
+ const slotId = count === 1 ? slot.slotId.trim() : `${slot.slotId.trim()}[${index}]`;
80
+ if (slots.has(slotId)) {invalid('ASSET_DUPLICATE_SLOT', { slotId });}
81
+ slots.set(slotId, { ...slot, slotId, pageId, required: slot.required !== false });
82
+ slotIds.push(slotId);
83
+ }
84
+ });
85
+ return { pageId, imageNeed: page.imageNeed, slotIds };
86
+ });
87
+ for (const entry of entries) {
88
+ if (!slots.has(entry.slotId)) {invalid('ASSET_UNDECLARED_SLOT', { slotId: entry.slotId });}
89
+ }
90
+ const byId = new Map(entries.map(entry => [entry.slotId, entry]));
91
+ const planned = [...slots.values()].map(slot => {
92
+ const entry = byId.get(slot.slotId) || {};
93
+ const designSize = minimumSize(slot);
94
+ const inputSize = minimumSize(entry);
95
+ return {
96
+ ...entry,
97
+ ...slot,
98
+ minWidth: Math.max(designSize.minWidth, inputSize.minWidth),
99
+ minHeight: Math.max(designSize.minHeight, inputSize.minHeight),
100
+ };
101
+ });
102
+ return { entries: planned, pages, assetStrategy };
103
+ }
104
+
105
+ module.exports = { minimumSize, prepareAssetPlan, readDesignAssetStrategy };