openyida 2026.9.9 → 2026.9.11-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/create-form/args.js +4 -0
- package/lib/app/create-form/batch.js +193 -13
- package/lib/app/create-form.js +45 -14
- package/lib/app/update-app.js +17 -2
- package/lib/core/cli-error.js +10 -1
- package/lib/core/command-manifest.js +2 -2
- package/lib/design-plan/init.js +90 -12
- package/lib/design-plan/materialize.js +12 -6
- package/lib/design-plan/normalize.js +88 -1
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/SKILL.md +11 -2
- package/yida-skills/skills/yida-app/workflow/parallel-work.md +3 -1
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +6 -4
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +8 -5
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +5 -1
- package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +21 -1
- package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +3 -1
- package/yida-skills/skills/yida-create-app/SKILL.md +4 -4
- package/yida-skills/skills/yida-create-form-page/SKILL.md +52 -1
- package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +18 -2
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +4 -4
- package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +3 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +1 -1
- package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
- package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +1 -1
- package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
- package/yida-skills/skills/yida-prd/workflow/plan-business.md +3 -1
- package/yida-skills/skills/yida-requirement-analysis/SKILL.md +9 -8
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +44 -13
|
@@ -143,8 +143,10 @@ function createParseArgs(dependencies) {
|
|
|
143
143
|
if (args.includes('--help') || args.includes('-h')) {
|
|
144
144
|
usage(t('create_form.usage_create_short'));
|
|
145
145
|
hint(t('create_form.usage_update_short'));
|
|
146
|
+
hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
|
|
146
147
|
hint(t('create_form.example_create'));
|
|
147
148
|
hint(t('create_form.example_update'));
|
|
149
|
+
hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
|
|
148
150
|
return {
|
|
149
151
|
mode: 'help',
|
|
150
152
|
help: true,
|
|
@@ -347,8 +349,10 @@ function createParseArgs(dependencies) {
|
|
|
347
349
|
|
|
348
350
|
usage(t('create_form.usage_create_short'));
|
|
349
351
|
hint(t('create_form.usage_update_short'));
|
|
352
|
+
hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
|
|
350
353
|
hint(t('create_form.example_create'));
|
|
351
354
|
hint(t('create_form.example_update'));
|
|
355
|
+
hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
|
|
352
356
|
throwCreateFormError(t('create_form.usage_create_short'), 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
353
357
|
};
|
|
354
358
|
}
|
|
@@ -6,12 +6,50 @@ const crypto = require('crypto');
|
|
|
6
6
|
const { execFile } = require('child_process');
|
|
7
7
|
const { CliError } = require('../../core/cli-error');
|
|
8
8
|
const { t } = require('../../core/i18n');
|
|
9
|
+
const { usage, hint } = require('../../core/chalk');
|
|
10
|
+
const { assertNoEmojiInText, assertNoEmojiInValue } = require('../../core/no-emoji-guard');
|
|
11
|
+
const { normalizeYidaLocale } = require('../../core/yida-i18n');
|
|
12
|
+
const { FORM_NAV_ICONS, resolveFormNavIcon } = require('./nav-icon');
|
|
13
|
+
|
|
14
|
+
const BATCH_USAGE = 'openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]';
|
|
15
|
+
const BATCH_PLAN_EXAMPLE = '{"forms":[{"key":"customer","title":"客户","fieldsFile":"customer-fields.json"},{"key":"order","title":"订单","fieldsFile":"order-fields.json","dependsOn":["customer"]}]}';
|
|
9
16
|
|
|
10
17
|
function invalid(reason) {
|
|
11
18
|
throw new CliError(t('create_form.batch_invalid'), { code: 'FORM_BATCH_INVALID', details: { reason } });
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
function validateStaticDefinition(form, fields) {
|
|
22
|
+
try {
|
|
23
|
+
assertNoEmojiInText(form.title, { artifact: `forms.${form.key}.title` });
|
|
24
|
+
assertNoEmojiInValue(fields, { artifact: `forms.${form.key}.fields` });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
invalid({
|
|
27
|
+
formKey: form.key,
|
|
28
|
+
errorCode: error?.code || 'OPENYIDA_ARTIFACT_EMOJI_FORBIDDEN',
|
|
29
|
+
errorMsg: error?.message || String(error),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (form.icon !== undefined && !resolveFormNavIcon(form.icon, form.title, fields).icon) {
|
|
33
|
+
invalid({
|
|
34
|
+
formKey: form.key,
|
|
35
|
+
errorCode: 'CREATE_FORM_NAV_ICON_INVALID',
|
|
36
|
+
errorMsg: `Unsupported form navigation icon: ${form.icon}`,
|
|
37
|
+
icon: form.icon,
|
|
38
|
+
availableIconCount: FORM_NAV_ICONS.length,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (form.locale !== undefined && !normalizeYidaLocale(form.locale)) {
|
|
42
|
+
invalid({
|
|
43
|
+
formKey: form.key,
|
|
44
|
+
errorCode: 'CREATE_FORM_INVALID_ARGUMENTS',
|
|
45
|
+
errorMsg: `Unsupported locale: ${form.locale}`,
|
|
46
|
+
locale: form.locale,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
function parseArgs(args) {
|
|
52
|
+
if (args.includes('--help') || args.includes('-h')) { return { help: true }; }
|
|
15
53
|
const options = { concurrency: 3, check: false };
|
|
16
54
|
const positional = [];
|
|
17
55
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -25,11 +63,19 @@ function parseArgs(args) {
|
|
|
25
63
|
}
|
|
26
64
|
}
|
|
27
65
|
if (positional.length !== 2 || !Number.isInteger(options.concurrency) || options.concurrency < 1 || options.concurrency > 4) {
|
|
28
|
-
invalid(
|
|
66
|
+
invalid(BATCH_USAGE.replace(/^openyida /, ''));
|
|
29
67
|
}
|
|
30
68
|
return { ...options, appType: positional[0], file: path.resolve(positional[1]) };
|
|
31
69
|
}
|
|
32
70
|
|
|
71
|
+
function showHelp() {
|
|
72
|
+
usage(BATCH_USAGE);
|
|
73
|
+
hint('plan.json: ' + BATCH_PLAN_EXAMPLE);
|
|
74
|
+
hint('fieldsFile is resolved relative to plan.json; use dependsOn and {"$form":"key","field":"label"} for dependencies.');
|
|
75
|
+
hint('Run one authoritative batch. If it continues in background, do not stop it, delete its state, or fall back to create/update/resume.');
|
|
76
|
+
return { success: true, help: true, usage: BATCH_USAGE };
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
// References occupy a complete JSON value; ordinary text is preserved.
|
|
34
80
|
function mapReferences(value, resolve) {
|
|
35
81
|
if (Array.isArray(value)) { return value.map(item => mapReferences(item, resolve)); }
|
|
@@ -42,7 +88,38 @@ function mapReferences(value, resolve) {
|
|
|
42
88
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, mapReferences(item, resolve)]));
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
function
|
|
91
|
+
function normalizeAssociationReferences(value, appType) {
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.map(item => normalizeAssociationReferences(item, appType));
|
|
94
|
+
}
|
|
95
|
+
if (!value || typeof value !== 'object') { return value; }
|
|
96
|
+
const normalized = Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
97
|
+
key,
|
|
98
|
+
normalizeAssociationReferences(item, appType),
|
|
99
|
+
]));
|
|
100
|
+
const association = value.associationForm;
|
|
101
|
+
if (
|
|
102
|
+
value.type === 'AssociationFormField' &&
|
|
103
|
+
association &&
|
|
104
|
+
typeof association === 'object' &&
|
|
105
|
+
!Array.isArray(association) &&
|
|
106
|
+
typeof association.$form === 'string'
|
|
107
|
+
) {
|
|
108
|
+
const reference = { $form: association.$form };
|
|
109
|
+
normalized.associationForm = {
|
|
110
|
+
...(appType ? { appType } : {}),
|
|
111
|
+
formUuid: reference,
|
|
112
|
+
...(typeof association.field === 'string' && association.field.trim() ? {
|
|
113
|
+
mainFieldId: { ...reference, field: association.field },
|
|
114
|
+
mainFieldLabel: association.field,
|
|
115
|
+
mainComponentName: 'TextField',
|
|
116
|
+
} : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadPlan(file, options = {}) {
|
|
46
123
|
const input = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
124
|
if (!Array.isArray(input.forms) || !input.forms.length) { invalid('forms'); }
|
|
48
125
|
const forms = input.forms.map(form => {
|
|
@@ -52,7 +129,9 @@ function loadPlan(file) {
|
|
|
52
129
|
if (Object.keys(form).some(key => !['key', 'title', 'fields', 'fieldsFile', 'dependsOn', 'formUuid', 'icon', 'locale'].includes(key))) { invalid(form.key); }
|
|
53
130
|
if (form.formUuid !== undefined && (typeof form.formUuid !== 'string' || !form.formUuid.startsWith('FORM'))) { invalid(form.key); }
|
|
54
131
|
for (const key of ['icon', 'locale']) { if (form[key] !== undefined && typeof form[key] !== 'string') { invalid(key); } }
|
|
55
|
-
const
|
|
132
|
+
const rawFields = form.fieldsFile === undefined ? form.fields : JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), form.fieldsFile), 'utf8'));
|
|
133
|
+
const fields = normalizeAssociationReferences(rawFields, options.appType);
|
|
134
|
+
validateStaticDefinition(form, fields);
|
|
56
135
|
const dependencies = new Set(form.dependsOn || []);
|
|
57
136
|
mapReferences(fields, key => { dependencies.add(key); return 'FORM-PRECHECK'; });
|
|
58
137
|
return { ...form, fields, dependsOn: [...dependencies] };
|
|
@@ -87,14 +166,75 @@ function parseOutput(stdout) {
|
|
|
87
166
|
return output;
|
|
88
167
|
}
|
|
89
168
|
|
|
90
|
-
function
|
|
169
|
+
function mergeCommandOutput(stdout, stderr, failed) {
|
|
170
|
+
const stdoutOutput = parseOutput(stdout);
|
|
171
|
+
const stderrOutput = parseOutput(stderr);
|
|
172
|
+
if (!failed) { return stdoutOutput || stderrOutput; }
|
|
173
|
+
if (!stdoutOutput) { return stderrOutput; }
|
|
174
|
+
if (!stderrOutput) { return stdoutOutput; }
|
|
175
|
+
return { ...stdoutOutput, ...stderrOutput };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function boundedDiagnostic(value) {
|
|
179
|
+
const text = String(value || '').trim();
|
|
180
|
+
return text.length > 4000 ? text.slice(-4000) : text;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function expectedReadbackFields(fields) {
|
|
184
|
+
const expected = [];
|
|
185
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
186
|
+
if (Array.isArray(field)) {
|
|
187
|
+
expected.push(...expectedReadbackFields(field));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!field || typeof field !== 'object') { continue; }
|
|
191
|
+
if (field.type === 'ColumnContainer') {
|
|
192
|
+
expected.push(...expectedReadbackFields(field.children));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (['Divider', 'RichText', 'PageSection', 'GroupContainer'].includes(field.type)) { continue; }
|
|
196
|
+
if (typeof field.label === 'string' && field.label.trim()) {
|
|
197
|
+
expected.push({ label: field.label.trim(), componentName: field.type });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return expected;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readbackMatchesExpectedFields(schema, fields) {
|
|
204
|
+
if (!schema || typeof schema.formUuid !== 'string' || !Array.isArray(schema.fields)) { return false; }
|
|
205
|
+
const actual = schema.fields.filter(field => field && typeof field === 'object');
|
|
206
|
+
return expectedReadbackFields(fields).every(expected => actual.some(field =>
|
|
207
|
+
field.label === expected.label && (!expected.componentName || field.componentName === expected.componentName)
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readbackMismatch(formUuid, form, schema) {
|
|
212
|
+
const expected = expectedReadbackFields(form.fields);
|
|
213
|
+
const actual = Array.isArray(schema?.fields) ? schema.fields.map(field => ({
|
|
214
|
+
label: field?.label,
|
|
215
|
+
componentName: field?.componentName,
|
|
216
|
+
})) : [];
|
|
217
|
+
return Object.assign(new Error(`Form batch readback mismatch: ${form.key}`), {
|
|
218
|
+
output: {
|
|
219
|
+
success: false,
|
|
220
|
+
errorCode: 'FORM_BATCH_READBACK_MISMATCH',
|
|
221
|
+
errorMsg: `Created form ${form.key} did not contain all expected fields`,
|
|
222
|
+
formUuid,
|
|
223
|
+
details: { formKey: form.key, formUuid, expected, actual },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function execute(args, { execFile: execFileImpl = execFile } = {}) {
|
|
91
229
|
return new Promise((resolve, reject) => {
|
|
92
|
-
|
|
230
|
+
execFileImpl(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
|
|
93
231
|
env: { ...process.env, YIDA_QUIET: '1' }, timeout: 180000, maxBuffer: 8 * 1024 * 1024,
|
|
94
|
-
}, (error, stdout) => {
|
|
95
|
-
const output =
|
|
232
|
+
}, (error, stdout, stderr) => {
|
|
233
|
+
const output = mergeCommandOutput(stdout, stderr, Boolean(error));
|
|
96
234
|
if (error || !output || typeof output !== 'object' || output.success === false) {
|
|
97
|
-
reject(Object.assign(new Error(
|
|
235
|
+
reject(Object.assign(new Error(
|
|
236
|
+
output?.errorMsg || output?.error || output?.message || boundedDiagnostic(stderr) || error?.message || 'Missing command result'
|
|
237
|
+
), { output }));
|
|
98
238
|
} else { resolve(output); }
|
|
99
239
|
});
|
|
100
240
|
});
|
|
@@ -106,6 +246,9 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
106
246
|
return !result || result.status === 'blocked' || (result.status === 'failed' && result.formUuid);
|
|
107
247
|
}).map(form => form.key));
|
|
108
248
|
const active = new Map();
|
|
249
|
+
const recoveryAttempted = new Set(forms.filter(form =>
|
|
250
|
+
form.formUuid || (results[form.key]?.status === 'failed' && results[form.key]?.formUuid)
|
|
251
|
+
).map(form => form.key));
|
|
109
252
|
try {
|
|
110
253
|
while (pending.size || active.size) {
|
|
111
254
|
const pendingBefore = pending.size;
|
|
@@ -117,6 +260,8 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
117
260
|
}
|
|
118
261
|
if (active.size >= concurrency || deps.some(status => status !== 'success')) { continue; }
|
|
119
262
|
pending.delete(form.key);
|
|
263
|
+
const hadKnownFormUuid = Boolean(form.formUuid || results[form.key]?.formUuid);
|
|
264
|
+
if (hadKnownFormUuid) { recoveryAttempted.add(form.key); }
|
|
120
265
|
results[form.key] = { ...results[form.key], status: 'running' };
|
|
121
266
|
save(); // Record intent before the request; an interrupted create is never retried automatically.
|
|
122
267
|
const task = Promise.resolve().then(() => worker(form)).then(output => {
|
|
@@ -124,6 +269,13 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
124
269
|
}, error => {
|
|
125
270
|
results[form.key] = { ...results[form.key], status: 'failed', error: error.message,
|
|
126
271
|
...((error.output?.formUuid || error.output?.details?.formUuid) ? { formUuid: error.output.formUuid || error.output.details.formUuid } : {}) };
|
|
272
|
+
// A create may persist the form before a later schema/readback stage fails.
|
|
273
|
+
// Retry that known UUID once inside the same authoritative batch so its
|
|
274
|
+
// dependents remain pending and can run after conservative resume.
|
|
275
|
+
if (!hadKnownFormUuid && results[form.key].formUuid && !recoveryAttempted.has(form.key)) {
|
|
276
|
+
recoveryAttempted.add(form.key);
|
|
277
|
+
pending.add(form.key);
|
|
278
|
+
}
|
|
127
279
|
}).then(() => { save(); }).finally(() => active.delete(form.key));
|
|
128
280
|
active.set(form.key, task);
|
|
129
281
|
}
|
|
@@ -140,7 +292,8 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
140
292
|
|
|
141
293
|
async function run(args, dependencies = {}) {
|
|
142
294
|
const options = parseArgs(args);
|
|
143
|
-
|
|
295
|
+
if (options.help) { return showHelp(); }
|
|
296
|
+
const { forms, groups } = loadPlan(options.file, { appType: options.appType });
|
|
144
297
|
const call = dependencies.execute || execute;
|
|
145
298
|
// Preflight all definitions before the first remote mutation.
|
|
146
299
|
await schedule(forms.map(form => ({ ...form, dependsOn: [] })), options.concurrency, {}, async form => {
|
|
@@ -202,14 +355,28 @@ async function run(args, dependencies = {}) {
|
|
|
202
355
|
}
|
|
203
356
|
state.results[form.key].formUuid = formUuid;
|
|
204
357
|
save(state);
|
|
358
|
+
let resumed = false;
|
|
205
359
|
if (shouldResume) {
|
|
206
360
|
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
361
|
+
resumed = true;
|
|
362
|
+
}
|
|
363
|
+
let schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
364
|
+
if (!readbackMatchesExpectedFields(schema, resolvedFields) && !resumed && !form.formUuid) {
|
|
365
|
+
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
366
|
+
resumed = true;
|
|
367
|
+
schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
368
|
+
}
|
|
369
|
+
if (schema.formUuid !== formUuid || !readbackMatchesExpectedFields(schema, resolvedFields)) {
|
|
370
|
+
throw readbackMismatch(formUuid, { ...form, fields: resolvedFields }, schema);
|
|
207
371
|
}
|
|
208
|
-
const schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
209
|
-
if (schema.formUuid !== formUuid || !Array.isArray(schema.fields)) { invalid(`schema: ${form.key}`); }
|
|
210
372
|
return { formUuid, fields: schema.fields };
|
|
211
373
|
}, () => save(state));
|
|
212
|
-
const
|
|
374
|
+
const success = Object.values(state.results).every(item => item.status === 'success');
|
|
375
|
+
const output = { success, groups, stateFile, results: state.results };
|
|
376
|
+
if (!success) {
|
|
377
|
+
output.errorCode = 'FORM_BATCH_PARTIAL_FAILURE';
|
|
378
|
+
output.nextAction = 'Inspect the saved state and child error, then fix the batch input or recover known formUuid values. Do not fall back to create-form create.';
|
|
379
|
+
}
|
|
213
380
|
console.log(JSON.stringify(output));
|
|
214
381
|
if (!output.success) { process.exitCode = 1; }
|
|
215
382
|
return output;
|
|
@@ -218,4 +385,17 @@ async function run(args, dependencies = {}) {
|
|
|
218
385
|
}
|
|
219
386
|
}
|
|
220
387
|
|
|
221
|
-
module.exports = {
|
|
388
|
+
module.exports = {
|
|
389
|
+
run,
|
|
390
|
+
parseArgs,
|
|
391
|
+
loadPlan,
|
|
392
|
+
mapReferences,
|
|
393
|
+
normalizeAssociationReferences,
|
|
394
|
+
schedule,
|
|
395
|
+
parseOutput,
|
|
396
|
+
mergeCommandOutput,
|
|
397
|
+
execute,
|
|
398
|
+
expectedReadbackFields,
|
|
399
|
+
readbackMatchesExpectedFields,
|
|
400
|
+
validateStaticDefinition,
|
|
401
|
+
};
|
package/lib/app/create-form.js
CHANGED
|
@@ -123,6 +123,29 @@ function createCreateFormError(message, code, details) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function createFormPreflightError(errorObject, appType, formTitle) {
|
|
127
|
+
const error = errorObject && errorObject.isCliError
|
|
128
|
+
? errorObject
|
|
129
|
+
: createCreateFormError(
|
|
130
|
+
errorObject?.message || String(errorObject || 'Create form preflight failed'),
|
|
131
|
+
'CREATE_FORM_PREFLIGHT_FAILED'
|
|
132
|
+
);
|
|
133
|
+
const existing = error.details && typeof error.details === 'object' && !Array.isArray(error.details)
|
|
134
|
+
? error.details
|
|
135
|
+
: {};
|
|
136
|
+
error.details = {
|
|
137
|
+
...existing,
|
|
138
|
+
stage: 'preflight',
|
|
139
|
+
retryable: true,
|
|
140
|
+
retrySafe: true,
|
|
141
|
+
sideEffectState: 'not_started',
|
|
142
|
+
mutationAccepted: false,
|
|
143
|
+
mutationPerformed: false,
|
|
144
|
+
target: { resource: 'form', appType, formTitle },
|
|
145
|
+
};
|
|
146
|
+
return error;
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
const parseArgs = createParseArgs({
|
|
127
150
|
parseOpenOption,
|
|
128
151
|
normalizeYidaLocale,
|
|
@@ -4902,20 +4925,28 @@ async function mainCreate(parsedArgs, authRef) {
|
|
|
4902
4925
|
label('Fields:', fieldsJsonOrFile);
|
|
4903
4926
|
|
|
4904
4927
|
step(2, t('create_form.step_read_fields', 2));
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4928
|
+
let fields;
|
|
4929
|
+
let columns;
|
|
4930
|
+
let validations;
|
|
4931
|
+
let iconResolution;
|
|
4932
|
+
try {
|
|
4933
|
+
assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
|
|
4934
|
+
({ fields, columns, validations } = readFieldsDefinition(fieldsJsonOrFile));
|
|
4935
|
+
assertNoEmojiInFormDefinition(formTitle, fields, validations);
|
|
4936
|
+
validateFormFieldDefinitions(fields);
|
|
4937
|
+
iconResolution = resolveFormNavIcon(parsedArgs.icon, formTitle, fields);
|
|
4938
|
+
if (!iconResolution.icon) {
|
|
4939
|
+
throwCreateFormError(
|
|
4940
|
+
`Unsupported form navigation icon: ${parsedArgs.icon}. Run openyida create-form icons --json for the supported catalog.`,
|
|
4941
|
+
'CREATE_FORM_NAV_ICON_INVALID',
|
|
4942
|
+
{
|
|
4943
|
+
icon: parsedArgs.icon,
|
|
4944
|
+
availableIconCount: FORM_NAV_ICONS.length,
|
|
4945
|
+
}
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4948
|
+
} catch (preflightError) {
|
|
4949
|
+
throw createFormPreflightError(preflightError, appType, formTitle);
|
|
4919
4950
|
}
|
|
4920
4951
|
const formIcon = iconResolution.icon;
|
|
4921
4952
|
const fieldCount = countDataFieldDefinitions(fields);
|
package/lib/app/update-app.js
CHANGED
|
@@ -221,6 +221,16 @@ function normalizeLayoutDirection(layoutDirection) {
|
|
|
221
221
|
return normalized;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/** 与 yc-utils 的应用配置归一化一致;不应用详情页 Shell 的 top_fold 覆盖。 */
|
|
225
|
+
function normalizeStoredLayoutDirection(layoutDirection, navType) {
|
|
226
|
+
if (SUPPORTED_LAYOUT_DIRECTIONS.includes(layoutDirection)) {return layoutDirection;}
|
|
227
|
+
if (layoutDirection === 'hoz') {return navType === 'top_side' ? 'l_shape' : 'top';}
|
|
228
|
+
if (layoutDirection === 'ver') {return 'side';}
|
|
229
|
+
if (navType === 'top_fold') {return 'top';}
|
|
230
|
+
if (navType === 'top_side') {return 'l_shape';}
|
|
231
|
+
return 'side';
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
function pickAppField(currentApp, fieldName, configName) {
|
|
225
235
|
if (currentApp[fieldName] !== undefined && currentApp[fieldName] !== null) {
|
|
226
236
|
return currentApp[fieldName];
|
|
@@ -296,6 +306,8 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
|
296
306
|
const currentCustomThemeStyle = pickAppField(currentApp, 'customThemeStyle', 'CUSTOM_THEME_STYLE');
|
|
297
307
|
const addWaterMark = pickAppField(currentApp, 'addWaterMark', 'ADDWATERMARK');
|
|
298
308
|
const sentryMode = pickAppField(currentApp, 'sentryMode', 'SENTRY_MODE');
|
|
309
|
+
const currentNavType = currentApp.navType || (currentApp.config && currentApp.config.NAVTYPE);
|
|
310
|
+
const currentLayoutDirection = currentApp.layoutDirection || (currentApp.config && currentApp.config.LAY_OUT_DIRECTION);
|
|
299
311
|
if (params.colour === 'custom' && !params.themeColor && !currentThemeColor) {
|
|
300
312
|
throw new Error(t('update_app.custom_theme_color_required'));
|
|
301
313
|
}
|
|
@@ -319,9 +331,10 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
|
319
331
|
mode: currentApp.mode || (currentApp.config && currentApp.config.APPMODE) || 'normal',
|
|
320
332
|
type: currentApp.type || (currentApp.config && currentApp.config.APPTYPE) || 'single',
|
|
321
333
|
navTheme: params.navTheme || currentApp.navTheme || (currentApp.config && currentApp.config.NAV_THEME) || 'light',
|
|
322
|
-
navType: currentApp.navType || (currentApp.config && currentApp.config.NAVTYPE) || 'top_side',
|
|
323
334
|
navLayout: currentApp.navLayout || (currentApp.config && currentApp.config.NAVLAYOUT) || 'auto',
|
|
324
|
-
layoutDirection: params.layoutDirection
|
|
335
|
+
layoutDirection: params.layoutDirection
|
|
336
|
+
? normalizeLayoutDirection(params.layoutDirection)
|
|
337
|
+
: normalizeStoredLayoutDirection(currentLayoutDirection, currentNavType),
|
|
325
338
|
homepageLogo: currentApp.homepageLogo || (currentApp.config && currentApp.config.HOMEPAGELOGO) || '',
|
|
326
339
|
logoSource: params.logoSource || currentApp.logoSource || (currentApp.config && currentApp.config.LOGO_SOURCE) || 'appIcon',
|
|
327
340
|
logoLink: currentApp.logoLink || (currentApp.config && currentApp.config.LOGOLINK) || '',
|
|
@@ -346,6 +359,8 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
|
|
|
346
359
|
pageFooter: currentApp.pageFooter || '',
|
|
347
360
|
};
|
|
348
361
|
|
|
362
|
+
// ThemeNavSetting 保存现有 navType;新布局由 layoutDirection 控制,不反写或补造旧字段。
|
|
363
|
+
if (currentNavType) {postDataObj.navType = currentNavType;}
|
|
349
364
|
if (addWaterMark !== undefined && addWaterMark !== null) {
|
|
350
365
|
postDataObj.addWaterMark = addWaterMark;
|
|
351
366
|
}
|
package/lib/core/cli-error.js
CHANGED
|
@@ -90,8 +90,17 @@ function shouldUseStructuredErrorOutput(error, args = []) {
|
|
|
90
90
|
&& details.retrySafe === false
|
|
91
91
|
&& details.sideEffectState === 'unknown'
|
|
92
92
|
);
|
|
93
|
+
const deterministicPreflightFailure = !!(
|
|
94
|
+
details
|
|
95
|
+
&& typeof details === 'object'
|
|
96
|
+
&& !Array.isArray(details)
|
|
97
|
+
&& details.stage === 'preflight'
|
|
98
|
+
&& details.retrySafe === true
|
|
99
|
+
&& details.sideEffectState === 'not_started'
|
|
100
|
+
&& details.mutationPerformed === false
|
|
101
|
+
);
|
|
93
102
|
const navigationOrderFailure = /^NAV_ORDER_/.test(error.code || '');
|
|
94
|
-
return ownedPartial || mutationOutcomeUnknown || navigationOrderFailure;
|
|
103
|
+
return ownedPartial || mutationOutcomeUnknown || deterministicPreflightFailure || navigationOrderFailure;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
module.exports = {
|
|
@@ -1555,8 +1555,8 @@ function summarizeLocalizedCommands(commands) {
|
|
|
1555
1555
|
plan_command_ids: ['design-plan.init', 'design-plan.preview', 'design-plan.materialize', 'design-plan.patch'],
|
|
1556
1556
|
theme_command_ids: ['sample', 'create-app', 'update-app'],
|
|
1557
1557
|
navigation_command_ids: { platform: ['update-app', 'nav-group'], custom: ['update-app', 'update-form-config', 'get-form-config'] },
|
|
1558
|
-
navigation_policy: 'Before PRD planning, reuse explicit navigation choices. During first-time intake, ask only when unknown: platform
|
|
1559
|
-
design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved navigation
|
|
1558
|
+
navigation_policy: 'Before PRD planning, reuse explicit navigation choices. During first-time intake, ask only when ownership is unknown and offer exactly two neutral options with no preselection: "宜搭原生导航" (使用宜搭自带的导航菜单。) and "自定义导航" (在自定义页面里定制菜单的样式和操作方式,替代宜搭自带的导航菜单。). Native means platform navigation. The agent chooses layout from module count, hierarchy, switching frequency, content width and target device; do not ask layout or presentation-style questions. Prefer top for few shallow modules and wide content, side for many frequently accessed modules, L-shaped/mixed for business domains with child modules, and custom dock for lightweight mobile entries. Preserve user-specified layouts. Store resolved type/source/reason/variant in the brief, distinguishing user ownership from agent layout reasoning. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, which writes layoutDirection=top|side|l_shape and hideAppNav=n. Preserve stored navType unchanged; do not derive or invent it and do not add a --nav-type option. Without --layout, normalize stored legacy layoutDirection and navType using the yc-utils application rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Shell page-level top_fold/none overrides do not belong in app settings. Custom side/top/mixed/dock is a page layout and requires app and per-page navigation hiding with readback. Verify persisted layoutDirection and hideAppNav; updatedFields and themeVerification do not prove navigation persistence. Navigation tone is separate from layout.',
|
|
1559
|
+
design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved navigation ownership, visual style and page scope before planning or resource creation; the agent determines navigation layout from the business context without a layout question. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false skips unrelated navigation and visual questions. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Confirm the displayed revision before building, and do not materialize or patch again after confirmation.',
|
|
1560
1560
|
product_design_policy: 'yida-requirement-analysis owns shared facts and first-time intake. yida-prd owns business planning; yida-design owns visual design. Reuse the same confirmed brief and supplied details. Prepare business and base visuals concurrently, then bind visuals to settled page tasks. yida-app merges and checks the artifacts before creating resources. Plan uses the compact authoring contract and selected theme context; the CLI reads full templates and renders all artifacts.',
|
|
1561
1561
|
ui_guidance_policy: 'Page implementation consumes yida-prd prd.md for positioning, information architecture, page prototype, native form entry policy, material strategy, business-specific checks, resource creation order, page implementation delivery order, navigation order, and acceptance criteria; it consumes yida-design design.md for app custom theme CSS delivery, themeColor/navTheme, visual states, visualScaffold, surface material, rounded rules, density rules, components, and state styling. prd.md and design.md are the only design sources of truth. Page implementation may extract page-spec.json as a derived implementation handoff from prd.md + design.md; conflicts are resolved by sending business conflicts to yida-prd and visual conflicts to yida-design before regenerating the spec. Core normal forms default to 1-3 business sample records before page implementation, followed by query readback. An explicit opt-out, configuration dictionary, sensitive data, or lack of safely constructible values requires a recorded skip reason. Screenshots, public sharing, data-source deep binding, and fine navigation grouping are optional after explicit user request or PRD acceptance criteria.',
|
|
1562
1562
|
default_nav_order_policy: 'For custom navigation, implement PRD navigation order in the custom shell and verify app/page navigation hiding; skip platform nav-group ordering. For platform navigation: after the primary page is successfully published, perform exactly one navigation order operation. If the PRD names a navigation order, publish without --auto-nav-order and then call openyida nav-group order <appType> <items...>. If the PRD only gives broad groups or is missing navigation order, use openyida publish ... --auto-nav-order and do not call nav-group order or auto-order afterward. Explicit and automatic ordering are mutually exclusive; never generate per-item move loops. The fallback priority is portal/home/workbench entry > business handling > data management > business analytics > system configuration.',
|
package/lib/design-plan/init.js
CHANGED
|
@@ -8,14 +8,49 @@ const { planBase } = require('./parallel');
|
|
|
8
8
|
|
|
9
9
|
const ROOT = path.resolve(__dirname, '../../yida-skills/skills/yida-design/sub_skill/yida-design-plan');
|
|
10
10
|
|
|
11
|
+
function list(value) {
|
|
12
|
+
return Array.isArray(value) ? value : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function customScene(scene) {
|
|
16
|
+
return !['form', 'process-form', 'report'].includes(scene?.kind);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resourceOnlyScope(brief) {
|
|
20
|
+
if (brief.explicitScope?.allowInferredResources === false) {return true;}
|
|
21
|
+
const scopedForms = list(brief.explicitScope?.forms);
|
|
22
|
+
const confirmedCustomPages = list(brief.pageScenes).filter(customScene);
|
|
23
|
+
return scopedForms.length > 0 && confirmedCustomPages.length === 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scopeEntryMatchesScene(entry, scene) {
|
|
27
|
+
const candidate = typeof entry === 'string' ? entry : entry?.key || entry?.name;
|
|
28
|
+
return candidate && [scene?.key, scene?.name].includes(candidate);
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
function initialize(inputPath, options = {}) {
|
|
12
32
|
const input = path.resolve(inputPath);
|
|
13
33
|
const brief = readJson(input, 'requirement-brief.json');
|
|
14
34
|
const project = brief.projectName;
|
|
35
|
+
if ((project === undefined || project === null || project === '')
|
|
36
|
+
&& typeof brief.meta?.projectName === 'string'
|
|
37
|
+
&& brief.meta.projectName.trim()) {
|
|
38
|
+
throw new CliError('projectName 必须位于 requirement-brief.json 根级;检测到 meta.projectName,请将该字段移动到根级后重试,不要更换项目名', {
|
|
39
|
+
code: 'DESIGN_PLAN_PROJECT_NAME_MISPLACED',
|
|
40
|
+
details: {
|
|
41
|
+
expectedPath: 'projectName',
|
|
42
|
+
actualPath: 'meta.projectName',
|
|
43
|
+
nextAction: 'move_project_name_to_root',
|
|
44
|
+
retryable: true,
|
|
45
|
+
retrySafe: true,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
15
49
|
if (typeof project !== 'string' || !project.trim() || /[/\\]/.test(project) || ['.', '..'].includes(project)) {
|
|
16
50
|
throw new CliError('projectName 必须是单层项目目录名', { code: 'DESIGN_PLAN_INVALID_PROJECT' });
|
|
17
51
|
}
|
|
18
|
-
|
|
52
|
+
const resourceOnly = resourceOnlyScope(brief);
|
|
53
|
+
if ((!resourceOnly && brief.intake?.confirmed !== true) || brief.openQuestions?.length) {
|
|
19
54
|
throw new CliError('请先完成需求确认并记录 intake.confirmed=true', { code: 'DESIGN_PLAN_INTAKE_REQUIRED' });
|
|
20
55
|
}
|
|
21
56
|
if (!['platform-l-shape', 'platform-top', 'platform-side', 'custom'].includes(brief.navigation?.type)
|
|
@@ -38,7 +73,10 @@ function initialize(inputPath, options = {}) {
|
|
|
38
73
|
if ([output, guide, business, visual].some(file => fs.existsSync(file))) {
|
|
39
74
|
throw new CliError('计划或编写上下文已存在,请继续补充现有文件', { code: 'DESIGN_PLAN_ALREADY_EXISTS' });
|
|
40
75
|
}
|
|
41
|
-
const
|
|
76
|
+
const scopedPages = list(brief.explicitScope?.pages);
|
|
77
|
+
const scenes = list(brief.pageScenes)
|
|
78
|
+
.filter(customScene)
|
|
79
|
+
.filter(scene => !resourceOnly || scopedPages.some(entry => scopeEntryMatchesScene(entry, scene)));
|
|
42
80
|
const pages = scenes.map((scene, index) => {
|
|
43
81
|
const source = typeof scene === 'string' ? { name: scene } : scene;
|
|
44
82
|
const { key, ...facts } = source;
|
|
@@ -49,19 +87,49 @@ function initialize(inputPath, options = {}) {
|
|
|
49
87
|
density: '', permissionSummary: '', dataBinding: '', dataSources: [], ...facts };
|
|
50
88
|
});
|
|
51
89
|
const choice = brief.visualSelection || {};
|
|
52
|
-
const app = brief.resourceContext?.app || {};
|
|
90
|
+
const app = brief.resourceContext?.app || brief.resourceContext || {};
|
|
53
91
|
const appConfig = { navigationType: brief.navigation.type };
|
|
54
92
|
for (const key of ['appType', 'corpId', 'baseUrl']) {if (app[key]) {appConfig[key] = app[key];}}
|
|
93
|
+
const businessGoals = Array.isArray(brief.businessGoals)
|
|
94
|
+
? brief.businessGoals
|
|
95
|
+
: (typeof brief.businessGoals === 'string' && brief.businessGoals.trim() ? [brief.businessGoals] : []);
|
|
96
|
+
const modelInputs = (resourceOnly && list(brief.explicitScope?.forms).length > 0
|
|
97
|
+
? list(brief.explicitScope.forms)
|
|
98
|
+
: list(brief.businessObjects)).map(item => {
|
|
99
|
+
if (typeof item !== 'string') {return item;}
|
|
100
|
+
return list(brief.businessObjects).find(candidate => candidate?.name === item) || item;
|
|
101
|
+
});
|
|
102
|
+
const normalizedScope = {
|
|
103
|
+
...brief.explicitScope,
|
|
104
|
+
...(resourceOnly ? {
|
|
105
|
+
allowInferredResources: false,
|
|
106
|
+
pages: scopedPages.filter(entry => scenes.some(scene => scopeEntryMatchesScene(entry, scene))),
|
|
107
|
+
} : {}),
|
|
108
|
+
};
|
|
109
|
+
const rolePermissionSummary = list(brief.targetUsers)
|
|
110
|
+
.map(user => typeof user === 'string' ? `${user}:使用本轮范围内已授权资源` : '')
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
if (resourceOnly && rolePermissionSummary.length === 0) {
|
|
113
|
+
rolePermissionSummary.push('应用成员:使用本轮范围内已授权资源');
|
|
114
|
+
}
|
|
55
115
|
const plan = {
|
|
56
116
|
schemaVersion: '2.0',
|
|
57
|
-
meta: { projectName: project, appName: brief.appName || project, revision: '1', status: 'draft', source: input,
|
|
117
|
+
meta: { projectName: project, appName: brief.appName || brief.projectDisplayName || brief.meta?.displayName || project, revision: '1', status: 'draft', source: input,
|
|
58
118
|
businessDomain: brief.industry || '', experienceTopology: '',
|
|
59
119
|
planState: { planConfirmed: false, presentedRevision: null, confirmedRevision: null } },
|
|
60
|
-
overview: { summary:
|
|
61
|
-
dataModels:
|
|
120
|
+
overview: { summary: businessGoals.join(';'), businessGraph: { relations: [] }, rolePermissionSummary },
|
|
121
|
+
dataModels: modelInputs.map(item => {
|
|
62
122
|
const model = typeof item === 'string' ? { name: item } : item;
|
|
63
123
|
return { ...model, name: model.name, formType: model.formType || '宜搭表单', description: model.description || '', fields: Array.isArray(model.fields) ? model.fields : [] };
|
|
64
|
-
}), businessFlows: [], pages: { customPageDetails: pages }, execution: {
|
|
124
|
+
}), businessFlows: [], pages: { customPageDetails: pages }, execution: {
|
|
125
|
+
appConfig,
|
|
126
|
+
explicitScope: { ...normalizedScope, navigation: brief.navigation },
|
|
127
|
+
...(resourceOnly ? {
|
|
128
|
+
sampleDataPlan: modelInputs
|
|
129
|
+
.filter(item => !String(item?.formType || '').includes('流程'))
|
|
130
|
+
.map(item => ({ form: typeof item === 'string' ? item : item.name, skipReason: '本轮显式窄范围不包含示例数据' })),
|
|
131
|
+
} : {}),
|
|
132
|
+
},
|
|
65
133
|
visualStyle: {
|
|
66
134
|
...(choice.tokens ? { tokens: choice.tokens } : {}),
|
|
67
135
|
...(choice.evidence ? { evidence: choice.evidence } : {}),
|
|
@@ -82,14 +150,23 @@ function initialize(inputPath, options = {}) {
|
|
|
82
150
|
const patterns = readJson(path.join(ROOT, 'templates/page-patterns/index.json')).patterns;
|
|
83
151
|
const context = [
|
|
84
152
|
'# 计划编写上下文', '', `需求事实:${input}`, `计划目录:${outputDir}`, '',
|
|
85
|
-
|
|
153
|
+
`先读 build-plan-compact-schema.md,再读 init 已创建的 business.json;保留 base 并一次补完 facts 后写回。business facts 只能包含 overview、dataModels、businessFlows、pages 和可选 execution,不得写 visualStyle。写完后只执行一次:openyida design-plan materialize ${output} --business-file ${business} --visual-file ${visual} --json。标准首版禁止先调用 --from-preview、preview、--check 或无参数 materialize,也不要用 Glob 或额外 Read 检查产物;成功结果已返回 HTML 路径和 revision。只有视觉选择不完整或存在品牌稿、参考图、逐页特殊视觉要求时才补充视觉。保留页面 ID 与已有需求细节。`, '',
|
|
86
154
|
'## 页面模式', ...patterns.map(item => `- ${item.id}(${item.label}):${item.mustKeep.join(';')}`), '',
|
|
87
155
|
`## 已选风格:${theme.label}`, overview.trim(), '',
|
|
88
156
|
'用户确认的配色优先于模板固定灰阶:surfaceTone=brand-tinted 将浅色页面、填充和边界与主色协调,正文保留中性层级;用户明确只改强调色、保持中性或忠实参考配色时设为 theme。深色表面保留原层级。不要为了模板的无彩限制撤掉用户要求的品牌氛围。', '',
|
|
89
157
|
'完整主题的 token、组件、状态和响应式规则由 CLI 注入 design.md;调整具体组件时按需读取对应模板章节。', '',
|
|
90
158
|
].join('\n');
|
|
91
159
|
const base = planBase(plan);
|
|
92
|
-
const
|
|
160
|
+
const businessFacts = Object.fromEntries(
|
|
161
|
+
['overview', 'dataModels', 'businessFlows', 'pages', 'execution'].map(key => [key, plan[key]]),
|
|
162
|
+
);
|
|
163
|
+
const businessReady = resourceOnly
|
|
164
|
+
&& businessGoals.length > 0
|
|
165
|
+
&& rolePermissionSummary.length > 0
|
|
166
|
+
&& plan.dataModels.length > 0
|
|
167
|
+
&& plan.dataModels.every(model => model.name && list(model.fields).length > 0
|
|
168
|
+
&& model.fields.every(field => field?.name && field?.type && typeof field.required === 'boolean'));
|
|
169
|
+
const businessPart = `${JSON.stringify({ base, ready: businessReady, facts: businessFacts }, null, 2)}\n`;
|
|
93
170
|
// The intake already records an atomic visual selection. Seed it here so a
|
|
94
171
|
// standard first Plan only needs one model planning pass for business facts.
|
|
95
172
|
const selectedVisual = plan.visualStyle.forUser;
|
|
@@ -100,10 +177,11 @@ function initialize(inputPath, options = {}) {
|
|
|
100
177
|
const visualPart = `${JSON.stringify({ base, ready: visualReady, facts: { visualStyle: plan.visualStyle } }, null, 2)}\n`;
|
|
101
178
|
writeFiles([[output, `${JSON.stringify(plan, null, 2)}\n`], [guide, context], [business, businessPart], [visual, visualPart]]);
|
|
102
179
|
return { success: true, input, output, context: guide, contract: path.join(ROOT, 'references/build-plan-compact-schema.md'), draft: true,
|
|
103
|
-
|
|
104
|
-
|
|
180
|
+
materialize: { mode: 'complete_files_once', maxCalls: 1,
|
|
181
|
+
command: `openyida design-plan materialize ${output} --business-file ${business} --visual-file ${visual} --json` },
|
|
182
|
+
parallelTasks: [...(!businessReady ? [{ id: 'business', skill: 'yida-prd', output: business, dependsOn: [] }] : []),
|
|
105
183
|
...(!visualReady ? [{ id: 'visual-selection', skill: 'yida-design', output: visual, dependsOn: [] }] : [])],
|
|
106
|
-
preparedInputs: { visual, visualReady },
|
|
184
|
+
preparedInputs: { business, businessReady, visual, visualReady },
|
|
107
185
|
optionalTasks: [{ id: 'visual-refinement', skill: 'yida-design', input: business, output: visual, dependsOn: ['business'],
|
|
108
186
|
when: 'brand material, reference images, or explicit page-specific visual differences require refinement' }],
|
|
109
187
|
};
|