scene-capability-engine 3.6.44 → 3.6.46

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 (61) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/scene-capability-engine.js +36 -2
  3. package/docs/command-reference.md +5 -0
  4. package/docs/releases/README.md +2 -0
  5. package/docs/releases/v3.6.45.md +18 -0
  6. package/docs/releases/v3.6.46.md +23 -0
  7. package/docs/zh/releases/README.md +2 -0
  8. package/docs/zh/releases/v3.6.45.md +18 -0
  9. package/docs/zh/releases/v3.6.46.md +23 -0
  10. package/lib/workspace/collab-governance-audit.js +575 -0
  11. package/package.json +4 -2
  12. package/scripts/auto-strategy-router.js +231 -0
  13. package/scripts/capability-mapping-report.js +339 -0
  14. package/scripts/check-branding-consistency.js +140 -0
  15. package/scripts/check-sce-tracking.js +54 -0
  16. package/scripts/check-skip-allowlist.js +94 -0
  17. package/scripts/errorbook-registry-health-gate.js +172 -0
  18. package/scripts/errorbook-release-gate.js +132 -0
  19. package/scripts/failure-attribution-repair.js +317 -0
  20. package/scripts/git-managed-gate.js +464 -0
  21. package/scripts/interactive-approval-event-projection.js +400 -0
  22. package/scripts/interactive-approval-workflow.js +829 -0
  23. package/scripts/interactive-authorization-tier-evaluate.js +413 -0
  24. package/scripts/interactive-change-plan-gate.js +225 -0
  25. package/scripts/interactive-context-bridge.js +617 -0
  26. package/scripts/interactive-customization-loop.js +1690 -0
  27. package/scripts/interactive-dialogue-governance.js +842 -0
  28. package/scripts/interactive-feedback-log.js +253 -0
  29. package/scripts/interactive-flow-smoke.js +238 -0
  30. package/scripts/interactive-flow.js +1059 -0
  31. package/scripts/interactive-governance-report.js +1112 -0
  32. package/scripts/interactive-intent-build.js +707 -0
  33. package/scripts/interactive-loop-smoke.js +215 -0
  34. package/scripts/interactive-moqui-adapter.js +304 -0
  35. package/scripts/interactive-plan-build.js +426 -0
  36. package/scripts/interactive-runtime-policy-evaluate.js +495 -0
  37. package/scripts/interactive-work-order-build.js +552 -0
  38. package/scripts/matrix-regression-gate.js +167 -0
  39. package/scripts/moqui-core-regression-suite.js +397 -0
  40. package/scripts/moqui-lexicon-audit.js +651 -0
  41. package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
  42. package/scripts/moqui-matrix-remediation-queue.js +852 -0
  43. package/scripts/moqui-metadata-extract.js +1340 -0
  44. package/scripts/moqui-rebuild-gate.js +167 -0
  45. package/scripts/moqui-release-summary.js +729 -0
  46. package/scripts/moqui-standard-rebuild.js +1370 -0
  47. package/scripts/moqui-template-baseline-report.js +682 -0
  48. package/scripts/npm-package-runtime-asset-check.js +221 -0
  49. package/scripts/problem-closure-gate.js +441 -0
  50. package/scripts/release-asset-integrity-check.js +216 -0
  51. package/scripts/release-asset-nonempty-normalize.js +166 -0
  52. package/scripts/release-drift-evaluate.js +223 -0
  53. package/scripts/release-drift-signals.js +255 -0
  54. package/scripts/release-governance-snapshot-export.js +132 -0
  55. package/scripts/release-ops-weekly-summary.js +934 -0
  56. package/scripts/release-risk-remediation-bundle.js +315 -0
  57. package/scripts/release-weekly-ops-gate.js +423 -0
  58. package/scripts/state-migration-reconciliation-gate.js +110 -0
  59. package/scripts/state-storage-tiering-audit.js +337 -0
  60. package/scripts/steering-content-audit.js +393 -0
  61. package/scripts/symbol-evidence-locate.js +366 -0
@@ -0,0 +1,1370 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ const DEFAULT_METADATA = 'docs/moqui/metadata-catalog.json';
8
+ const DEFAULT_OUT = '.sce/reports/recovery/moqui-standard-rebuild.json';
9
+ const DEFAULT_MARKDOWN_OUT = '.sce/reports/recovery/moqui-standard-rebuild.md';
10
+ const DEFAULT_BUNDLE_OUT = '.sce/reports/recovery/moqui-standard-bundle';
11
+
12
+ function parseArgs(argv) {
13
+ const options = {
14
+ metadata: DEFAULT_METADATA,
15
+ out: DEFAULT_OUT,
16
+ markdownOut: DEFAULT_MARKDOWN_OUT,
17
+ bundleOut: DEFAULT_BUNDLE_OUT,
18
+ json: false
19
+ };
20
+
21
+ for (let i = 0; i < argv.length; i += 1) {
22
+ const token = argv[i];
23
+ const next = argv[i + 1];
24
+ if (token === '--metadata' && next) {
25
+ options.metadata = next;
26
+ i += 1;
27
+ } else if (token === '--out' && next) {
28
+ options.out = next;
29
+ i += 1;
30
+ } else if (token === '--markdown-out' && next) {
31
+ options.markdownOut = next;
32
+ i += 1;
33
+ } else if (token === '--bundle-out' && next) {
34
+ options.bundleOut = next;
35
+ i += 1;
36
+ } else if (token === '--json') {
37
+ options.json = true;
38
+ } else if (token === '--help' || token === '-h') {
39
+ printHelpAndExit(0);
40
+ }
41
+ }
42
+
43
+ return options;
44
+ }
45
+
46
+ function printHelpAndExit(code) {
47
+ const lines = [
48
+ 'Usage: node scripts/moqui-standard-rebuild.js [options]',
49
+ '',
50
+ 'Options:',
51
+ ` --metadata <path> Moqui metadata JSON path (default: ${DEFAULT_METADATA})`,
52
+ ` --out <path> Rebuild JSON report path (default: ${DEFAULT_OUT})`,
53
+ ` --markdown-out <path> Rebuild markdown summary path (default: ${DEFAULT_MARKDOWN_OUT})`,
54
+ ` --bundle-out <path> Generated bundle directory (default: ${DEFAULT_BUNDLE_OUT})`,
55
+ ' --json Print JSON payload to stdout',
56
+ ' -h, --help Show this help'
57
+ ];
58
+ console.log(lines.join('\n'));
59
+ process.exit(code);
60
+ }
61
+
62
+ function readPathValue(payload, pathText) {
63
+ if (!payload || typeof payload !== 'object') {
64
+ return undefined;
65
+ }
66
+ const segments = `${pathText || ''}`
67
+ .split('.')
68
+ .map(token => token.trim())
69
+ .filter(Boolean);
70
+ let cursor = payload;
71
+ for (const segment of segments) {
72
+ if (!cursor || typeof cursor !== 'object') {
73
+ return undefined;
74
+ }
75
+ cursor = cursor[segment];
76
+ }
77
+ return cursor;
78
+ }
79
+
80
+ function normalizeText(value) {
81
+ if (value === undefined || value === null) {
82
+ return null;
83
+ }
84
+ const text = `${value}`.trim();
85
+ return text.length > 0 ? text : null;
86
+ }
87
+
88
+ function normalizeIdentifier(value) {
89
+ const text = normalizeText(value);
90
+ if (!text) {
91
+ return null;
92
+ }
93
+ return text
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9]+/g, '-')
96
+ .replace(/^-+|-+$/g, '');
97
+ }
98
+
99
+ function toArray(value) {
100
+ if (Array.isArray(value)) {
101
+ return value;
102
+ }
103
+ if (value === undefined || value === null) {
104
+ return [];
105
+ }
106
+ return [value];
107
+ }
108
+
109
+ function collectArrayFromPaths(payload, paths) {
110
+ for (const pathText of paths) {
111
+ const raw = readPathValue(payload, pathText);
112
+ if (Array.isArray(raw)) {
113
+ return raw;
114
+ }
115
+ }
116
+ return [];
117
+ }
118
+
119
+ function pickField(source, candidates) {
120
+ for (const candidate of candidates) {
121
+ const value = readPathValue(source, candidate);
122
+ const text = normalizeText(value);
123
+ if (text) {
124
+ return text;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function inferServiceVerbNoun(name) {
131
+ const text = normalizeText(name);
132
+ if (!text) {
133
+ return { verb: null, noun: null };
134
+ }
135
+ const tokenList = text
136
+ .split(/[^a-zA-Z0-9]+/)
137
+ .map(token => token.trim())
138
+ .filter(Boolean);
139
+ if (tokenList.length === 0) {
140
+ return { verb: null, noun: null };
141
+ }
142
+ const lowerTokens = tokenList.map(token => token.toLowerCase());
143
+ const knownVerbs = new Set([
144
+ 'get', 'list', 'query', 'read', 'fetch', 'search',
145
+ 'create', 'add', 'insert', 'upsert', 'update', 'edit',
146
+ 'delete', 'remove', 'cancel',
147
+ 'sync', 'export', 'import',
148
+ 'run', 'execute', 'invoke',
149
+ 'approve', 'reject', 'close', 'open',
150
+ 'validate', 'audit', 'prepare'
151
+ ]);
152
+ let verb = null;
153
+ let noun = null;
154
+ for (let i = 0; i < lowerTokens.length; i += 1) {
155
+ if (knownVerbs.has(lowerTokens[i])) {
156
+ verb = lowerTokens[i];
157
+ noun = tokenList[Math.min(i + 1, tokenList.length - 1)] || tokenList[tokenList.length - 1];
158
+ break;
159
+ }
160
+ }
161
+ if (!verb) {
162
+ if (lowerTokens.length >= 2) {
163
+ verb = lowerTokens[lowerTokens.length - 2];
164
+ noun = tokenList[lowerTokens.length - 1];
165
+ } else {
166
+ verb = 'execute';
167
+ noun = tokenList[0];
168
+ }
169
+ }
170
+ return {
171
+ verb: normalizeText(verb),
172
+ noun: normalizeText(noun)
173
+ };
174
+ }
175
+
176
+ function isGovernanceLikeService(name) {
177
+ const text = normalizeText(name);
178
+ if (!text) {
179
+ return false;
180
+ }
181
+ return /(governance|scene|suite|platform|spec\.|playbook|runbook|audit|observability|closure|program|orchestration)/i.test(text);
182
+ }
183
+
184
+ function collectEntityModels(payload) {
185
+ const entries = collectArrayFromPaths(payload, [
186
+ 'entities',
187
+ 'entity_catalog',
188
+ 'entity_catalog.entities',
189
+ 'catalog.entities'
190
+ ]);
191
+ const models = [];
192
+ const seen = new Set();
193
+ for (const entry of entries) {
194
+ if (!entry || typeof entry !== 'object') {
195
+ continue;
196
+ }
197
+ const name = pickField(entry, ['name', 'entity', 'entity_name', 'id']);
198
+ if (!name) {
199
+ continue;
200
+ }
201
+ const normalized = normalizeIdentifier(name);
202
+ if (!normalized || seen.has(normalized)) {
203
+ continue;
204
+ }
205
+ seen.add(normalized);
206
+ const relations = toArray(entry.relations || entry.relationships || entry.relation_refs)
207
+ .map(item => {
208
+ if (typeof item === 'string') {
209
+ return normalizeText(item);
210
+ }
211
+ if (item && typeof item === 'object') {
212
+ return pickField(item, ['target', 'target_entity', 'entity', 'name', 'id']);
213
+ }
214
+ return null;
215
+ })
216
+ .filter(Boolean);
217
+ models.push({
218
+ name,
219
+ package: pickField(entry, ['package', 'package_name', 'group', 'module']),
220
+ relations,
221
+ source_file: pickField(entry, ['source_file', 'source', 'file'])
222
+ });
223
+ }
224
+ return models;
225
+ }
226
+
227
+ function collectServiceModels(payload) {
228
+ const entries = collectArrayFromPaths(payload, [
229
+ 'services',
230
+ 'service_catalog',
231
+ 'service_catalog.services',
232
+ 'catalog.services'
233
+ ]);
234
+ const models = [];
235
+ const seen = new Set();
236
+ for (const entry of entries) {
237
+ if (!entry || typeof entry !== 'object') {
238
+ continue;
239
+ }
240
+ const name = pickField(entry, ['name', 'service', 'service_name', 'id']);
241
+ if (!name) {
242
+ continue;
243
+ }
244
+ const normalized = normalizeIdentifier(name);
245
+ if (!normalized || seen.has(normalized)) {
246
+ continue;
247
+ }
248
+ seen.add(normalized);
249
+ const entityRefs = toArray(entry.entities || entry.entity_refs || entry.uses_entities)
250
+ .map(item => (typeof item === 'string' ? normalizeText(item) : pickField(item, ['name', 'entity', 'id'])))
251
+ .filter(Boolean);
252
+ const inferred = inferServiceVerbNoun(name);
253
+ const verb = pickField(entry, ['verb']) || inferred.verb;
254
+ const noun = pickField(entry, ['noun']) || inferred.noun;
255
+ models.push({
256
+ name,
257
+ verb,
258
+ noun,
259
+ entities: entityRefs,
260
+ source_file: pickField(entry, ['source_file', 'source', 'file'])
261
+ });
262
+ }
263
+ return models;
264
+ }
265
+
266
+ function collectScreenModels(payload) {
267
+ const entries = collectArrayFromPaths(payload, [
268
+ 'screens',
269
+ 'screen_catalog',
270
+ 'screen_catalog.screens',
271
+ 'catalog.screens'
272
+ ]);
273
+ const models = [];
274
+ const seen = new Set();
275
+ for (const entry of entries) {
276
+ if (!entry || typeof entry !== 'object') {
277
+ continue;
278
+ }
279
+ const screenPath = pickField(entry, ['path', 'screen_path', 'name', 'id']);
280
+ if (!screenPath) {
281
+ continue;
282
+ }
283
+ const normalized = normalizeIdentifier(screenPath);
284
+ if (!normalized || seen.has(normalized)) {
285
+ continue;
286
+ }
287
+ seen.add(normalized);
288
+ const services = toArray(entry.services || entry.service_refs)
289
+ .map(item => (typeof item === 'string' ? normalizeText(item) : pickField(item, ['name', 'service', 'id'])))
290
+ .filter(Boolean);
291
+ const entities = toArray(entry.entities || entry.entity_refs)
292
+ .map(item => (typeof item === 'string' ? normalizeText(item) : pickField(item, ['name', 'entity', 'id'])))
293
+ .filter(Boolean);
294
+ models.push({
295
+ path: screenPath,
296
+ services,
297
+ entities,
298
+ source_file: pickField(entry, ['source_file', 'source', 'file'])
299
+ });
300
+ }
301
+ return models;
302
+ }
303
+
304
+ function collectFormModels(payload) {
305
+ const entries = collectArrayFromPaths(payload, [
306
+ 'forms',
307
+ 'form_catalog',
308
+ 'form_catalog.forms',
309
+ 'catalog.forms'
310
+ ]);
311
+ const models = [];
312
+ const seen = new Set();
313
+ for (const entry of entries) {
314
+ if (!entry || typeof entry !== 'object') {
315
+ continue;
316
+ }
317
+ const name = pickField(entry, ['name', 'form', 'form_name', 'id']);
318
+ if (!name) {
319
+ continue;
320
+ }
321
+ const normalized = normalizeIdentifier(name);
322
+ if (!normalized || seen.has(normalized)) {
323
+ continue;
324
+ }
325
+ seen.add(normalized);
326
+ const explicitFieldCount = Number(readPathValue(entry, 'field_count'));
327
+ const fieldCount = Number.isFinite(explicitFieldCount) && explicitFieldCount >= 0
328
+ ? explicitFieldCount
329
+ : toArray(entry.fields || entry.field_defs || entry.columns).length;
330
+ models.push({
331
+ name,
332
+ screen: pickField(entry, ['screen', 'screen_path', 'screen_ref']),
333
+ field_count: fieldCount,
334
+ source_file: pickField(entry, ['source_file', 'source', 'file'])
335
+ });
336
+ }
337
+ return models;
338
+ }
339
+
340
+ function collectNamedItems(payload, paths, itemLabel) {
341
+ const entries = collectArrayFromPaths(payload, paths);
342
+ const items = [];
343
+ const seen = new Set();
344
+ for (const entry of entries) {
345
+ const name = typeof entry === 'string'
346
+ ? normalizeText(entry)
347
+ : pickField(entry, ['name', itemLabel, `${itemLabel}_name`, 'id']);
348
+ if (!name) {
349
+ continue;
350
+ }
351
+ const normalized = normalizeIdentifier(name);
352
+ if (!normalized || seen.has(normalized)) {
353
+ continue;
354
+ }
355
+ seen.add(normalized);
356
+ items.push(name);
357
+ }
358
+ return items;
359
+ }
360
+
361
+ function detectDomains(entities) {
362
+ const domainSet = new Set();
363
+ for (const entity of entities) {
364
+ const packageName = normalizeText(entity && entity.package);
365
+ if (!packageName) {
366
+ continue;
367
+ }
368
+ const token = packageName.split('.')[0];
369
+ const normalized = normalizeIdentifier(token);
370
+ if (normalized) {
371
+ domainSet.add(normalized);
372
+ }
373
+ }
374
+ return Array.from(domainSet).sort();
375
+ }
376
+
377
+ function toPercent(numerator, denominator) {
378
+ const num = Number(numerator);
379
+ const den = Number(denominator);
380
+ if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) {
381
+ return 0;
382
+ }
383
+ return Number(((num / den) * 100).toFixed(2));
384
+ }
385
+
386
+ function clampScore(value) {
387
+ const numeric = Number(value);
388
+ if (!Number.isFinite(numeric)) {
389
+ return 0;
390
+ }
391
+ if (numeric < 0) {
392
+ return 0;
393
+ }
394
+ if (numeric > 100) {
395
+ return 100;
396
+ }
397
+ return Number(numeric.toFixed(2));
398
+ }
399
+
400
+ function isInferredSource(sourceFile) {
401
+ const source = normalizeText(sourceFile);
402
+ if (!source) {
403
+ return false;
404
+ }
405
+ return /#inferred$/i.test(source) || /[\\/]inferred([\\/]?)/i.test(source);
406
+ }
407
+
408
+ function preferNonInferred(items) {
409
+ const list = Array.isArray(items) ? items : [];
410
+ const filtered = list.filter(item => !isInferredSource(item && item.source_file));
411
+ return filtered.length > 0 ? filtered : list;
412
+ }
413
+
414
+ function scoreStatus(score) {
415
+ if (score >= 70) {
416
+ return 'ready';
417
+ }
418
+ if (score >= 40) {
419
+ return 'partial';
420
+ }
421
+ return 'gap';
422
+ }
423
+
424
+ function buildReadinessMatrix(context) {
425
+ const scoringEntities = preferNonInferred(context.entities);
426
+ const scoringServices = preferNonInferred(context.services);
427
+ const scoringScreens = preferNonInferred(context.screens);
428
+ const scoringForms = preferNonInferred(context.forms);
429
+
430
+ const entityCount = scoringEntities.length;
431
+ const serviceCount = scoringServices.length;
432
+ const screenCount = scoringScreens.length;
433
+ const formCount = scoringForms.length;
434
+ const ruleCount = context.businessRules.length;
435
+ const decisionCount = context.decisions.length;
436
+
437
+ const entityRelationCount = scoringEntities.reduce(
438
+ (sum, item) => sum + toArray(item && item.relations).length,
439
+ 0
440
+ );
441
+ const entityPackageCount = new Set(
442
+ scoringEntities.map(item => normalizeText(item && item.package)).filter(Boolean)
443
+ ).size;
444
+
445
+ const servicesBoundToEntityCount = scoringServices.filter(item => toArray(item && item.entities).length > 0).length;
446
+ const serviceVerbNounCount = scoringServices.filter(item => normalizeText(item && item.verb) || normalizeText(item && item.noun)).length;
447
+ const governanceLikeServiceCount = scoringServices.filter(item => isGovernanceLikeService(item && item.name)).length;
448
+ const businessLikeServiceCount = Math.max(0, serviceCount - governanceLikeServiceCount);
449
+ const businessServiceShare = serviceCount > 0
450
+ ? Number((businessLikeServiceCount / serviceCount).toFixed(4))
451
+ : 0;
452
+ const serviceStructuredNameCount = scoringServices.filter((item) => {
453
+ const text = normalizeText(item && item.name) || '';
454
+ return /[.#:_-]/.test(text) && text.length >= 12;
455
+ }).length;
456
+
457
+ const screensWithServiceCount = scoringScreens.filter(item => toArray(item && item.services).length > 0).length;
458
+ const screensWithEntityCount = scoringScreens.filter(item => toArray(item && item.entities).length > 0).length;
459
+
460
+ const formsWithFieldsCount = scoringForms.filter(item => Number(item && item.field_count) > 0).length;
461
+ const formsLinkedScreenCount = scoringForms.filter(item => normalizeText(item && item.screen)).length;
462
+ const totalFormFields = scoringForms.reduce((sum, item) => sum + (Number(item && item.field_count) || 0), 0);
463
+ const averageFormFields = formCount > 0 ? Number((totalFormFields / formCount).toFixed(2)) : 0;
464
+ const formScreenSet = new Set(
465
+ scoringForms
466
+ .map(item => normalizeText(item && item.screen))
467
+ .filter(Boolean)
468
+ );
469
+
470
+ const matrix = [];
471
+ const pushMatrixItem = (item) => {
472
+ const status = scoreStatus(item.score);
473
+ matrix.push({
474
+ ...item,
475
+ score: clampScore(item.score),
476
+ status
477
+ });
478
+ };
479
+
480
+ let entityScore = 0;
481
+ entityScore += entityCount > 0 ? 50 : 0;
482
+ entityScore += entityRelationCount > 0 ? 25 : 0;
483
+ entityScore += entityPackageCount > 0 ? 25 : 0;
484
+ const entityActions = [];
485
+ if (entityCount === 0) entityActions.push('补齐实体清单(name/package)');
486
+ if (entityRelationCount === 0) entityActions.push('补齐实体关系(relationship/relations)');
487
+ if (entityPackageCount === 0) entityActions.push('补齐实体 package/domain 标注');
488
+ pushMatrixItem({
489
+ template_id: 'sce.scene--moqui-entity-model-core--0.1.0',
490
+ capability: 'moqui-entity-model-core',
491
+ reason: 'Recover entity catalog and relationship baseline.',
492
+ score: entityScore,
493
+ evidence: {
494
+ entities: entityCount,
495
+ relation_edges: entityRelationCount,
496
+ package_count: entityPackageCount
497
+ },
498
+ next_actions: entityActions
499
+ });
500
+
501
+ const serviceEntityBindingRate = toPercent(servicesBoundToEntityCount, serviceCount);
502
+ const serviceVerbNounRate = toPercent(serviceVerbNounCount, serviceCount);
503
+ const serviceStructuredRate = toPercent(serviceStructuredNameCount, serviceCount);
504
+ let serviceScore = 0;
505
+ serviceScore += serviceCount > 0 ? 40 : 0;
506
+ serviceScore += Number((serviceVerbNounRate * 0.35).toFixed(2));
507
+ serviceScore += Number((serviceStructuredRate * 0.25).toFixed(2));
508
+ serviceScore += Number((serviceEntityBindingRate * 0.25 * businessServiceShare).toFixed(2));
509
+ const serviceActions = [];
510
+ if (serviceCount === 0) serviceActions.push('补齐服务契约(service/ref)');
511
+ if (serviceCount > 0 && businessLikeServiceCount > 0 && servicesBoundToEntityCount === 0) serviceActions.push('补齐服务到实体的绑定');
512
+ if (serviceCount > 0 && serviceVerbNounCount === 0) serviceActions.push('补齐服务 verb/noun 或动作语义');
513
+ pushMatrixItem({
514
+ template_id: 'sce.scene--moqui-service-contract-core--0.1.0',
515
+ capability: 'moqui-service-contract-core',
516
+ reason: 'Recover service contracts and entity/service bindings.',
517
+ score: serviceScore,
518
+ evidence: {
519
+ services: serviceCount,
520
+ services_with_entity_binding: servicesBoundToEntityCount,
521
+ service_entity_binding_rate_percent: serviceEntityBindingRate,
522
+ services_with_verb_or_noun: serviceVerbNounCount,
523
+ service_semantic_rate_percent: serviceVerbNounRate,
524
+ services_with_structured_name: serviceStructuredNameCount,
525
+ service_structured_rate_percent: serviceStructuredRate,
526
+ governance_like_services: governanceLikeServiceCount,
527
+ business_like_services: businessLikeServiceCount
528
+ },
529
+ next_actions: serviceActions
530
+ });
531
+
532
+ const screenServiceRate = toPercent(screensWithServiceCount, screenCount);
533
+ const screenEntityRate = toPercent(screensWithEntityCount, screenCount);
534
+ const screenFormLinkRate = toPercent(
535
+ scoringScreens.filter(item => formScreenSet.has(normalizeText(item && item.path))).length,
536
+ screenCount
537
+ );
538
+ let screenScore = 0;
539
+ screenScore += screenCount > 0 ? 35 : 0;
540
+ screenScore += Number((screenServiceRate * 0.4).toFixed(2));
541
+ screenScore += Number((screenEntityRate * 0.1).toFixed(2));
542
+ screenScore += Number((screenFormLinkRate * 0.2).toFixed(2));
543
+ // Governance-oriented scene specs may not carry direct entity refs but still
544
+ // provide complete flow semantics via service links + form/context coupling.
545
+ if (screenServiceRate >= 40 && screenFormLinkRate >= 50) {
546
+ screenScore += 5;
547
+ }
548
+ const screenActions = [];
549
+ if (screenCount === 0) screenActions.push('补齐页面/场景路径');
550
+ if (screenCount > 0 && screensWithServiceCount === 0) screenActions.push('补齐页面到服务调用映射');
551
+ if (screenCount > 0 && screensWithEntityCount === 0 && screensWithServiceCount < Math.max(1, Math.floor(screenCount * 0.6))) {
552
+ screenActions.push('补齐页面到实体读写映射');
553
+ }
554
+ pushMatrixItem({
555
+ template_id: 'sce.scene--moqui-screen-flow-core--0.1.0',
556
+ capability: 'moqui-screen-flow-core',
557
+ reason: 'Recover screen flow and screen/service references.',
558
+ score: screenScore,
559
+ evidence: {
560
+ screens: screenCount,
561
+ screens_with_service_refs: screensWithServiceCount,
562
+ screen_service_link_rate_percent: screenServiceRate,
563
+ screens_with_entity_refs: screensWithEntityCount,
564
+ screen_entity_link_rate_percent: screenEntityRate,
565
+ screen_form_link_rate_percent: screenFormLinkRate
566
+ },
567
+ next_actions: screenActions
568
+ });
569
+
570
+ const formFieldRate = toPercent(formsWithFieldsCount, formCount);
571
+ const formScreenLinkRate = toPercent(formsLinkedScreenCount, formCount);
572
+ let formScore = 0;
573
+ formScore += formCount > 0 ? 35 : 0;
574
+ formScore += Number((formFieldRate * 0.35).toFixed(2));
575
+ formScore += Number((formScreenLinkRate * 0.2).toFixed(2));
576
+ formScore += averageFormFields >= 3 ? 10 : 0;
577
+ const formActions = [];
578
+ if (formCount === 0) formActions.push('补齐表单定义');
579
+ if (formCount > 0 && formsWithFieldsCount === 0) formActions.push('补齐表单字段定义');
580
+ if (formCount > 0 && formsLinkedScreenCount === 0) formActions.push('补齐表单到页面映射');
581
+ if (formCount > 0 && averageFormFields < 3) formActions.push('提升表单字段完备度(平均字段数>=3)');
582
+ pushMatrixItem({
583
+ template_id: 'sce.scene--moqui-form-interaction-core--0.1.0',
584
+ capability: 'moqui-form-interaction-core',
585
+ reason: 'Recover form schema and page interaction fields.',
586
+ score: formScore,
587
+ evidence: {
588
+ forms: formCount,
589
+ forms_with_fields: formsWithFieldsCount,
590
+ form_field_rate_percent: formFieldRate,
591
+ forms_with_screen_link: formsLinkedScreenCount,
592
+ form_screen_link_rate_percent: formScreenLinkRate,
593
+ average_form_fields: averageFormFields
594
+ },
595
+ next_actions: formActions
596
+ });
597
+
598
+ const decisionBalanceRate = (ruleCount > 0 && decisionCount > 0)
599
+ ? toPercent(Math.min(ruleCount, decisionCount), Math.max(ruleCount, decisionCount))
600
+ : 0;
601
+ let ruleDecisionScore = 0;
602
+ ruleDecisionScore += ruleCount > 0 ? 30 : 0;
603
+ ruleDecisionScore += decisionCount > 0 ? 30 : 0;
604
+ ruleDecisionScore += Number((decisionBalanceRate * 0.4).toFixed(2));
605
+ const ruleDecisionActions = [];
606
+ if (ruleCount === 0) ruleDecisionActions.push('补齐业务规则清单');
607
+ if (decisionCount === 0) ruleDecisionActions.push('补齐决策策略/decision_logic');
608
+ if (ruleCount > 0 && decisionCount > 0 && decisionBalanceRate < 60) {
609
+ ruleDecisionActions.push('提升规则与决策配平度(rule/decision 数量比)');
610
+ }
611
+ pushMatrixItem({
612
+ template_id: 'sce.scene--moqui-rule-decision-core--0.1.0',
613
+ capability: 'moqui-rule-decision-core',
614
+ reason: 'Recover business rules and decision policies.',
615
+ score: ruleDecisionScore,
616
+ evidence: {
617
+ business_rules: ruleCount,
618
+ decisions: decisionCount,
619
+ rule_decision_balance_rate_percent: decisionBalanceRate
620
+ },
621
+ next_actions: ruleDecisionActions
622
+ });
623
+
624
+ const copilotReadinessRate = toPercent(
625
+ Number(screenCount > 0) + Number(serviceCount > 0) + Number(formCount > 0) + Number(ruleCount + decisionCount > 0),
626
+ 4
627
+ );
628
+ let copilotScore = 0;
629
+ copilotScore += screenCount > 0 ? 35 : 0;
630
+ copilotScore += serviceCount > 0 ? 20 : 0;
631
+ copilotScore += formCount > 0 ? 20 : 0;
632
+ copilotScore += (ruleCount + decisionCount) > 0 ? 15 : 0;
633
+ copilotScore += Number((screenServiceRate * 0.1).toFixed(2));
634
+ const copilotActions = [];
635
+ if (screenCount === 0) copilotActions.push('补齐页面上下文(screen_path/route/module)');
636
+ if (serviceCount === 0) copilotActions.push('补齐页面服务引用,支持诊断链路');
637
+ if (formCount === 0) copilotActions.push('补齐表单交互上下文,支持页面修复建议');
638
+ if ((ruleCount + decisionCount) === 0) copilotActions.push('补齐规则与决策语义,提升 AI 建议可靠性');
639
+ pushMatrixItem({
640
+ template_id: 'sce.scene--moqui-page-copilot-dialog--0.1.0',
641
+ capability: 'moqui-page-copilot-context-fix',
642
+ reason: 'Inject page-level human/AI copilot dialog for in-context fix guidance.',
643
+ score: copilotScore,
644
+ evidence: {
645
+ screens: screenCount,
646
+ services: serviceCount,
647
+ forms: formCount,
648
+ semantics: ruleCount + decisionCount,
649
+ readiness_rate_percent: copilotReadinessRate
650
+ },
651
+ next_actions: copilotActions
652
+ });
653
+
654
+ matrix.sort((a, b) => {
655
+ if (a.score !== b.score) {
656
+ return b.score - a.score;
657
+ }
658
+ return a.template_id.localeCompare(b.template_id);
659
+ });
660
+ return matrix;
661
+ }
662
+
663
+ function buildRecommendedTemplates(readinessMatrix) {
664
+ return readinessMatrix
665
+ .filter(item => item.status !== 'gap')
666
+ .map(item => ({
667
+ id: item.template_id,
668
+ reason: item.reason
669
+ }));
670
+ }
671
+
672
+ function buildCapabilities(readinessMatrix) {
673
+ return readinessMatrix
674
+ .filter(item => item.status !== 'gap')
675
+ .map(item => item.capability);
676
+ }
677
+
678
+ function buildSpecPlan(readinessMatrix) {
679
+ const specs = [];
680
+ const pushSpec = (specId, goal, dependencies = []) => {
681
+ specs.push({
682
+ spec_id: specId,
683
+ goal,
684
+ depends_on: dependencies
685
+ });
686
+ };
687
+
688
+ const statusByCapability = {};
689
+ for (const item of readinessMatrix) {
690
+ statusByCapability[item.capability] = item.status;
691
+ }
692
+ const hasCapability = (capability) => statusByCapability[capability] && statusByCapability[capability] !== 'gap';
693
+
694
+ if (hasCapability('moqui-entity-model-core')) {
695
+ pushSpec('moqui-01-entity-model-recovery', 'Recover entity model and relations.');
696
+ }
697
+ if (hasCapability('moqui-service-contract-core')) {
698
+ pushSpec(
699
+ 'moqui-02-service-contract-recovery',
700
+ 'Recover service contracts and entity bindings.',
701
+ hasCapability('moqui-entity-model-core') ? ['moqui-01-entity-model-recovery'] : []
702
+ );
703
+ }
704
+ if (hasCapability('moqui-screen-flow-core')) {
705
+ pushSpec(
706
+ 'moqui-03-screen-flow-recovery',
707
+ 'Recover screens and navigation/service linkage.',
708
+ hasCapability('moqui-service-contract-core') ? ['moqui-02-service-contract-recovery'] : []
709
+ );
710
+ }
711
+ if (hasCapability('moqui-form-interaction-core')) {
712
+ pushSpec(
713
+ 'moqui-04-form-interaction-recovery',
714
+ 'Recover form schema and page actions.',
715
+ hasCapability('moqui-screen-flow-core') ? ['moqui-03-screen-flow-recovery'] : []
716
+ );
717
+ }
718
+ if (hasCapability('moqui-rule-decision-core')) {
719
+ pushSpec(
720
+ 'moqui-05-rule-decision-recovery',
721
+ 'Recover business rules and decision strategy mapping.',
722
+ hasCapability('moqui-service-contract-core') ? ['moqui-02-service-contract-recovery'] : []
723
+ );
724
+ }
725
+ if (hasCapability('moqui-page-copilot-context-fix')) {
726
+ pushSpec(
727
+ 'moqui-06-page-copilot-integration',
728
+ 'Integrate page-level copilot dialog for contextual fix guidance.',
729
+ hasCapability('moqui-screen-flow-core') ? ['moqui-03-screen-flow-recovery'] : []
730
+ );
731
+ }
732
+ return specs;
733
+ }
734
+
735
+ function buildOntologySeed(context) {
736
+ const nodes = [];
737
+ const edges = [];
738
+ const nodeSet = new Set();
739
+ const edgeSet = new Set();
740
+
741
+ const addNode = (kind, id, metadata = {}) => {
742
+ const normalized = normalizeIdentifier(id);
743
+ if (!normalized) {
744
+ return null;
745
+ }
746
+ const key = `${kind}:${normalized}`;
747
+ if (!nodeSet.has(key)) {
748
+ nodeSet.add(key);
749
+ nodes.push({
750
+ id: key,
751
+ kind,
752
+ label: id,
753
+ metadata
754
+ });
755
+ }
756
+ return key;
757
+ };
758
+
759
+ const addEdge = (from, to, relation) => {
760
+ if (!from || !to || !relation) {
761
+ return;
762
+ }
763
+ const key = `${from}|${relation}|${to}`;
764
+ if (edgeSet.has(key)) {
765
+ return;
766
+ }
767
+ edgeSet.add(key);
768
+ edges.push({
769
+ from,
770
+ to,
771
+ relation
772
+ });
773
+ };
774
+
775
+ for (const entity of context.entities) {
776
+ addNode('entity', entity.name, { package: entity.package || null });
777
+ }
778
+ for (const service of context.services) {
779
+ const serviceNode = addNode('service', service.name, { verb: service.verb || null, noun: service.noun || null });
780
+ for (const entityName of service.entities) {
781
+ const entityNode = addNode('entity', entityName);
782
+ addEdge(serviceNode, entityNode, 'uses_entity');
783
+ }
784
+ }
785
+ for (const screen of context.screens) {
786
+ const screenNode = addNode('screen', screen.path);
787
+ for (const serviceName of screen.services) {
788
+ const serviceNode = addNode('service', serviceName);
789
+ addEdge(screenNode, serviceNode, 'invokes_service');
790
+ }
791
+ for (const entityName of screen.entities) {
792
+ const entityNode = addNode('entity', entityName);
793
+ addEdge(screenNode, entityNode, 'reads_entity');
794
+ }
795
+ }
796
+ for (const form of context.forms) {
797
+ const formNode = addNode('form', form.name, { field_count: form.field_count || 0 });
798
+ if (form.screen) {
799
+ const screenNode = addNode('screen', form.screen);
800
+ addEdge(formNode, screenNode, 'belongs_screen');
801
+ }
802
+ }
803
+ for (const rule of context.businessRules) {
804
+ addNode('business_rule', rule);
805
+ }
806
+ for (const decision of context.decisions) {
807
+ addNode('decision', decision);
808
+ }
809
+
810
+ return {
811
+ version: '1.0',
812
+ generated_at: new Date().toISOString(),
813
+ summary: {
814
+ nodes: nodes.length,
815
+ edges: edges.length
816
+ },
817
+ nodes,
818
+ edges
819
+ };
820
+ }
821
+
822
+ function buildCopilotContract(context) {
823
+ return {
824
+ mode: 'moqui-page-copilot-context-fix',
825
+ version: '1.0',
826
+ description: (
827
+ 'Page-level human/AI copilot contract. ' +
828
+ 'Keep original Moqui stack, generate advisory/patch responses bound to current page context.'
829
+ ),
830
+ context: {
831
+ page: {
832
+ required: ['screen_path', 'route', 'module'],
833
+ optional: ['form_name', 'widget_id', 'entity_refs', 'service_refs']
834
+ },
835
+ user_action: {
836
+ required: ['intent', 'expected_outcome'],
837
+ optional: ['last_operation', 'selection', 'filters']
838
+ },
839
+ runtime: {
840
+ required: ['timestamp'],
841
+ optional: ['error_message', 'error_stack', 'request_id', 'session_user']
842
+ }
843
+ },
844
+ response: {
845
+ policy: ['advisory', 'patch-proposal'],
846
+ required: ['diagnosis', 'change_plan', 'risk_notes'],
847
+ optional: ['patch_preview', 'validation_steps']
848
+ },
849
+ guardrails: {
850
+ stack_policy: 'preserve-original-stack',
851
+ write_policy: 'no-auto-apply-without-confirm',
852
+ target_scope: 'current-page-and-direct-dependencies'
853
+ },
854
+ starter_prompts: [
855
+ '这个页面为什么报错?请基于当前上下文定位根因并给出修复方案。',
856
+ '不改变现有技术栈,给出最小修复补丁和验证步骤。',
857
+ '如果涉及实体/服务/页面联动,请列出影响面和回滚点。'
858
+ ],
859
+ sample_page_refs: context.screens.slice(0, 5).map(item => item.path)
860
+ };
861
+ }
862
+
863
+ function buildMarkdownReport(report) {
864
+ const lines = [];
865
+ lines.push('# Moqui Standard Rebuild Plan');
866
+ lines.push('');
867
+ lines.push(`- Generated at: ${report.generated_at}`);
868
+ lines.push(`- Metadata file: ${report.metadata_file}`);
869
+ lines.push(`- Bundle output: ${report.output.bundle_dir}`);
870
+ lines.push('');
871
+ lines.push('## Inventory');
872
+ lines.push('');
873
+ lines.push(`- Entities: ${report.inventory.entities}`);
874
+ lines.push(`- Services: ${report.inventory.services}`);
875
+ lines.push(`- Screens: ${report.inventory.screens}`);
876
+ lines.push(`- Forms: ${report.inventory.forms}`);
877
+ lines.push(`- Business rules: ${report.inventory.business_rules}`);
878
+ lines.push(`- Decisions: ${report.inventory.decisions}`);
879
+ lines.push(`- Domains: ${report.inventory.domains.length > 0 ? report.inventory.domains.join(', ') : 'none'}`);
880
+ lines.push('');
881
+ lines.push('## Template Readiness Matrix');
882
+ lines.push('');
883
+ lines.push('| Template | Capability | Score | Status |');
884
+ lines.push('| --- | --- | ---: | --- |');
885
+ for (const item of report.recovery.readiness_matrix || []) {
886
+ lines.push(`| ${item.template_id} | ${item.capability} | ${item.score} | ${item.status} |`);
887
+ }
888
+ lines.push('');
889
+ lines.push('## Prioritized Gaps');
890
+ lines.push('');
891
+ const prioritizedGaps = report.recovery.prioritized_gaps || [];
892
+ if (prioritizedGaps.length === 0) {
893
+ lines.push('- none');
894
+ } else {
895
+ for (const gap of prioritizedGaps) {
896
+ const actions = Array.isArray(gap.next_actions) && gap.next_actions.length > 0
897
+ ? gap.next_actions.join(' | ')
898
+ : 'none';
899
+ lines.push(`- ${gap.template_id} (${gap.score}, ${gap.status}): ${actions}`);
900
+ }
901
+ }
902
+ lines.push('');
903
+ lines.push('## Recommended Templates');
904
+ lines.push('');
905
+ if (report.recovery.recommended_templates.length === 0) {
906
+ lines.push('- none');
907
+ } else {
908
+ for (const item of report.recovery.recommended_templates) {
909
+ lines.push(`- ${item.id}: ${item.reason}`);
910
+ }
911
+ }
912
+ lines.push('');
913
+ lines.push('## Spec Plan');
914
+ lines.push('');
915
+ if (report.recovery.spec_plan.length === 0) {
916
+ lines.push('- none');
917
+ } else {
918
+ for (const item of report.recovery.spec_plan) {
919
+ const deps = Array.isArray(item.depends_on) && item.depends_on.length > 0
920
+ ? item.depends_on.join(', ')
921
+ : 'none';
922
+ lines.push(`- ${item.spec_id}: ${item.goal} (depends_on: ${deps})`);
923
+ }
924
+ }
925
+ lines.push('');
926
+ lines.push('## Output');
927
+ lines.push('');
928
+ lines.push(`- Handoff manifest: ${report.output.handoff_manifest}`);
929
+ lines.push(`- Ontology seed: ${report.output.ontology_seed}`);
930
+ lines.push(`- Copilot contract: ${report.output.copilot_contract}`);
931
+ lines.push(`- Copilot playbook: ${report.output.copilot_playbook}`);
932
+ lines.push(`- Remediation queue: ${report.output.remediation_queue}`);
933
+ lines.push(`- Remediation plan: ${report.output.remediation_plan_json}`);
934
+ return `${lines.join('\n')}\n`;
935
+ }
936
+
937
+ function buildRemediationQueueLines(prioritizedGaps) {
938
+ const lines = [];
939
+ for (const gap of prioritizedGaps) {
940
+ const templateId = normalizeText(gap && gap.template_id) || 'unknown-template';
941
+ const actions = Array.isArray(gap && gap.next_actions) && gap.next_actions.length > 0
942
+ ? gap.next_actions
943
+ : ['补齐模板语义输入'];
944
+ for (const action of actions) {
945
+ lines.push(`[${templateId}] ${action}`);
946
+ }
947
+ }
948
+ return lines;
949
+ }
950
+
951
+ function buildRemediationPlanMarkdown(plan) {
952
+ const lines = [];
953
+ lines.push('# Matrix Remediation Plan');
954
+ lines.push('');
955
+ lines.push(`- Generated at: ${plan.generated_at}`);
956
+ lines.push(`- Coverage summary: ${plan.summary.total_gaps} gap items`);
957
+ lines.push('');
958
+ lines.push('## Items');
959
+ lines.push('');
960
+ if (!Array.isArray(plan.items) || plan.items.length === 0) {
961
+ lines.push('- none');
962
+ return `${lines.join('\n')}\n`;
963
+ }
964
+ for (const item of plan.items) {
965
+ lines.push(`### ${item.template_id}`);
966
+ lines.push('');
967
+ lines.push(`- Status: ${item.status}`);
968
+ lines.push(`- Score: ${item.score}`);
969
+ lines.push(`- Summary: ${item.summary}`);
970
+ lines.push(`- Suggested fields: ${(item.suggested_fields || []).join(', ') || 'none'}`);
971
+ lines.push(`- Source files: ${(item.source_files || []).length}`);
972
+ const files = Array.isArray(item.source_files) ? item.source_files.slice(0, 10) : [];
973
+ for (const file of files) {
974
+ lines.push(
975
+ ` - ${file.source_file}: ${file.missing_count} issue(s), missing=${(file.missing_types || []).join('|') || 'unknown'}`
976
+ );
977
+ }
978
+ lines.push('');
979
+ }
980
+ return `${lines.join('\n')}\n`;
981
+ }
982
+
983
+ function buildSourceIssueSummary(issues, limit = 20) {
984
+ const preferredSourcePattern = /^\.sce\/specs\/[^/]+\/docs\/scene-package\.json$/i;
985
+ const allIssues = Array.isArray(issues) ? issues : [];
986
+ const preferredIssues = allIssues.filter((issue) => {
987
+ const sourceFile = normalizeText(issue && issue.source_file);
988
+ return Boolean(sourceFile && preferredSourcePattern.test(sourceFile.replace(/\\/g, '/')));
989
+ });
990
+ const selectedIssues = preferredIssues.length > 0 ? preferredIssues : allIssues;
991
+
992
+ const bucket = new Map();
993
+ for (const issue of selectedIssues) {
994
+ const sourceFile = normalizeText(issue && issue.source_file) || '(unknown-source)';
995
+ const key = sourceFile;
996
+ const current = bucket.get(key) || {
997
+ source_file: sourceFile,
998
+ missing_count: 0,
999
+ missing_types: new Set(),
1000
+ sample_items: []
1001
+ };
1002
+ current.missing_count += 1;
1003
+ for (const missingType of toArray(issue && issue.missing_types)) {
1004
+ const text = normalizeText(missingType);
1005
+ if (text) {
1006
+ current.missing_types.add(text);
1007
+ }
1008
+ }
1009
+ const sampleName = normalizeText(issue && issue.name);
1010
+ if (sampleName && current.sample_items.length < 5) {
1011
+ current.sample_items.push(sampleName);
1012
+ }
1013
+ bucket.set(key, current);
1014
+ }
1015
+
1016
+ return Array.from(bucket.values())
1017
+ .map(item => ({
1018
+ source_file: item.source_file,
1019
+ missing_count: item.missing_count,
1020
+ missing_types: Array.from(item.missing_types).sort(),
1021
+ sample_items: item.sample_items
1022
+ }))
1023
+ .sort((a, b) => {
1024
+ if (b.missing_count !== a.missing_count) {
1025
+ return b.missing_count - a.missing_count;
1026
+ }
1027
+ return a.source_file.localeCompare(b.source_file);
1028
+ })
1029
+ .slice(0, limit);
1030
+ }
1031
+
1032
+ function buildMatrixRemediationPlan(context, prioritizedGaps) {
1033
+ const items = [];
1034
+ const addItem = (item) => {
1035
+ if (!item) {
1036
+ return;
1037
+ }
1038
+ items.push(item);
1039
+ };
1040
+
1041
+ for (const gap of prioritizedGaps) {
1042
+ const templateId = normalizeText(gap && gap.template_id);
1043
+ if (!templateId) {
1044
+ continue;
1045
+ }
1046
+
1047
+ if (templateId === 'sce.scene--moqui-service-contract-core--0.1.0') {
1048
+ const issues = [];
1049
+ for (const service of context.services) {
1050
+ const missingTypes = [];
1051
+ if (toArray(service && service.entities).length === 0) {
1052
+ missingTypes.push('service.entities');
1053
+ }
1054
+ if (!normalizeText(service && service.verb) && !normalizeText(service && service.noun)) {
1055
+ missingTypes.push('service.verb_noun');
1056
+ }
1057
+ if (missingTypes.length > 0) {
1058
+ issues.push({
1059
+ source_file: service.source_file,
1060
+ name: service.name,
1061
+ missing_types: missingTypes
1062
+ });
1063
+ }
1064
+ }
1065
+ addItem({
1066
+ template_id: templateId,
1067
+ status: gap.status,
1068
+ score: gap.score,
1069
+ summary: '补齐服务契约语义与实体绑定。',
1070
+ suggested_fields: ['services[].entities', 'services[].verb', 'services[].noun'],
1071
+ source_files: buildSourceIssueSummary(issues)
1072
+ });
1073
+ continue;
1074
+ }
1075
+
1076
+ if (templateId === 'sce.scene--moqui-screen-flow-core--0.1.0') {
1077
+ const issues = [];
1078
+ for (const screen of context.screens) {
1079
+ const missingTypes = [];
1080
+ if (toArray(screen && screen.services).length === 0) {
1081
+ missingTypes.push('screens[].services');
1082
+ }
1083
+ if (toArray(screen && screen.entities).length === 0) {
1084
+ missingTypes.push('screens[].entities');
1085
+ }
1086
+ if (missingTypes.length > 0) {
1087
+ issues.push({
1088
+ source_file: screen.source_file,
1089
+ name: screen.path,
1090
+ missing_types: missingTypes
1091
+ });
1092
+ }
1093
+ }
1094
+ addItem({
1095
+ template_id: templateId,
1096
+ status: gap.status,
1097
+ score: gap.score,
1098
+ summary: '补齐页面流转中的服务与实体映射。',
1099
+ suggested_fields: ['screens[].services', 'screens[].entities'],
1100
+ source_files: buildSourceIssueSummary(issues)
1101
+ });
1102
+ continue;
1103
+ }
1104
+
1105
+ if (templateId === 'sce.scene--moqui-form-interaction-core--0.1.0') {
1106
+ const issues = [];
1107
+ for (const form of context.forms) {
1108
+ const missingTypes = [];
1109
+ if ((Number(form && form.field_count) || 0) <= 0) {
1110
+ missingTypes.push('forms[].fields');
1111
+ }
1112
+ if (!normalizeText(form && form.screen)) {
1113
+ missingTypes.push('forms[].screen');
1114
+ }
1115
+ if (missingTypes.length > 0) {
1116
+ issues.push({
1117
+ source_file: form.source_file,
1118
+ name: form.name,
1119
+ missing_types: missingTypes
1120
+ });
1121
+ }
1122
+ }
1123
+ addItem({
1124
+ template_id: templateId,
1125
+ status: gap.status,
1126
+ score: gap.score,
1127
+ summary: '补齐表单字段与页面交互上下文。',
1128
+ suggested_fields: ['forms[].fields', 'forms[].field_count', 'forms[].screen'],
1129
+ source_files: buildSourceIssueSummary(issues)
1130
+ });
1131
+ continue;
1132
+ }
1133
+
1134
+ if (templateId === 'sce.scene--moqui-rule-decision-core--0.1.0') {
1135
+ addItem({
1136
+ template_id: templateId,
1137
+ status: gap.status,
1138
+ score: gap.score,
1139
+ summary: '补齐业务规则与决策策略语义映射。',
1140
+ suggested_fields: ['business_rules[]', 'decisions[]', 'governance_contract.business_rules', 'governance_contract.decision_logic'],
1141
+ source_files: []
1142
+ });
1143
+ continue;
1144
+ }
1145
+
1146
+ if (templateId === 'sce.scene--moqui-page-copilot-dialog--0.1.0') {
1147
+ addItem({
1148
+ template_id: templateId,
1149
+ status: gap.status,
1150
+ score: gap.score,
1151
+ summary: '补齐页面级 copilot 所需上下文。',
1152
+ suggested_fields: ['screens[].services', 'screens[].entities', 'forms[].field_count', 'decisions[]'],
1153
+ source_files: []
1154
+ });
1155
+ continue;
1156
+ }
1157
+ }
1158
+
1159
+ return {
1160
+ mode: 'moqui-matrix-remediation-plan',
1161
+ generated_at: new Date().toISOString(),
1162
+ summary: {
1163
+ total_gaps: items.length
1164
+ },
1165
+ items
1166
+ };
1167
+ }
1168
+
1169
+ async function writeBundle(bundleDir, payload) {
1170
+ const handoffDir = path.join(bundleDir, 'handoff');
1171
+ const ontologyDir = path.join(bundleDir, 'ontology');
1172
+ const copilotDir = path.join(bundleDir, 'copilot');
1173
+ const rebuildDir = path.join(bundleDir, 'rebuild');
1174
+
1175
+ const handoffManifestPath = path.join(handoffDir, 'handoff-manifest.json');
1176
+ const ontologySeedPath = path.join(ontologyDir, 'moqui-ontology-seed.json');
1177
+ const copilotContractPath = path.join(copilotDir, 'page-context-contract.json');
1178
+ const copilotPlaybookPath = path.join(copilotDir, 'conversation-playbook.md');
1179
+ const recoverySpecPath = path.join(rebuildDir, 'recovery-spec-plan.json');
1180
+ const remediationQueuePath = path.join(rebuildDir, 'matrix-remediation.lines');
1181
+ const remediationPlanJsonPath = path.join(rebuildDir, 'matrix-remediation-plan.json');
1182
+ const remediationPlanMarkdownPath = path.join(rebuildDir, 'matrix-remediation-plan.md');
1183
+
1184
+ await fs.ensureDir(handoffDir);
1185
+ await fs.ensureDir(ontologyDir);
1186
+ await fs.ensureDir(copilotDir);
1187
+ await fs.ensureDir(rebuildDir);
1188
+
1189
+ await fs.writeJson(handoffManifestPath, payload.handoff_manifest, { spaces: 2 });
1190
+ await fs.writeJson(ontologySeedPath, payload.ontology_seed, { spaces: 2 });
1191
+ await fs.writeJson(copilotContractPath, payload.copilot_contract, { spaces: 2 });
1192
+ await fs.writeJson(recoverySpecPath, payload.recovery_spec_plan, { spaces: 2 });
1193
+ await fs.writeJson(remediationPlanJsonPath, payload.remediation_plan, { spaces: 2 });
1194
+ await fs.writeFile(
1195
+ remediationPlanMarkdownPath,
1196
+ buildRemediationPlanMarkdown(payload.remediation_plan),
1197
+ 'utf8'
1198
+ );
1199
+ await fs.writeFile(
1200
+ remediationQueuePath,
1201
+ buildRemediationQueueLines(payload.prioritized_gaps).join('\n') + '\n',
1202
+ 'utf8'
1203
+ );
1204
+ await fs.writeFile(
1205
+ copilotPlaybookPath,
1206
+ [
1207
+ '# Page Copilot Conversation Playbook',
1208
+ '',
1209
+ '1. Capture current page context (screen path, form, user action, error).',
1210
+ '2. Ask for diagnosis first, then ask for minimum patch proposal.',
1211
+ '3. Keep response in advisory/patch-proposal mode.',
1212
+ '4. Apply patch only after human confirmation and run validation checks.',
1213
+ '',
1214
+ 'This playbook keeps original Moqui technology stack unchanged.'
1215
+ ].join('\n'),
1216
+ 'utf8'
1217
+ );
1218
+
1219
+ return {
1220
+ handoffManifestPath,
1221
+ ontologySeedPath,
1222
+ copilotContractPath,
1223
+ copilotPlaybookPath,
1224
+ recoverySpecPath,
1225
+ remediationQueuePath,
1226
+ remediationPlanJsonPath,
1227
+ remediationPlanMarkdownPath
1228
+ };
1229
+ }
1230
+
1231
+ async function main() {
1232
+ const options = parseArgs(process.argv.slice(2));
1233
+ const metadataPath = path.resolve(process.cwd(), options.metadata);
1234
+ const outPath = path.resolve(process.cwd(), options.out);
1235
+ const markdownPath = path.resolve(process.cwd(), options.markdownOut);
1236
+ const bundleDir = path.resolve(process.cwd(), options.bundleOut);
1237
+
1238
+ if (!(await fs.pathExists(metadataPath))) {
1239
+ throw new Error(`metadata file not found: ${path.relative(process.cwd(), metadataPath)}`);
1240
+ }
1241
+
1242
+ const metadata = await fs.readJson(metadataPath);
1243
+
1244
+ const entities = collectEntityModels(metadata);
1245
+ const services = collectServiceModels(metadata);
1246
+ const screens = collectScreenModels(metadata);
1247
+ const forms = collectFormModels(metadata);
1248
+ const businessRules = collectNamedItems(metadata, ['business_rules', 'rules', 'rule_catalog'], 'rule');
1249
+ const decisions = collectNamedItems(metadata, ['decisions', 'decision_points', 'decision_catalog'], 'decision');
1250
+ const domains = detectDomains(entities);
1251
+
1252
+ const context = {
1253
+ entities,
1254
+ services,
1255
+ screens,
1256
+ forms,
1257
+ businessRules,
1258
+ decisions
1259
+ };
1260
+
1261
+ const readinessMatrix = buildReadinessMatrix(context);
1262
+ const prioritizedGaps = readinessMatrix
1263
+ .filter(item => item.status !== 'ready')
1264
+ .sort((a, b) => a.score - b.score);
1265
+ const remediationPlan = buildMatrixRemediationPlan(context, prioritizedGaps);
1266
+ const readinessSummary = {
1267
+ average_score: Number((
1268
+ readinessMatrix.reduce((sum, item) => sum + Number(item.score || 0), 0) / Math.max(1, readinessMatrix.length)
1269
+ ).toFixed(2)),
1270
+ ready: readinessMatrix.filter(item => item.status === 'ready').length,
1271
+ partial: readinessMatrix.filter(item => item.status === 'partial').length,
1272
+ gap: readinessMatrix.filter(item => item.status === 'gap').length
1273
+ };
1274
+
1275
+ const recommendedTemplates = buildRecommendedTemplates(readinessMatrix);
1276
+ const capabilities = buildCapabilities(readinessMatrix);
1277
+ const specPlan = buildSpecPlan(readinessMatrix);
1278
+ const ontologySeed = buildOntologySeed(context);
1279
+ const copilotContract = buildCopilotContract(context);
1280
+
1281
+ const handoffManifest = {
1282
+ timestamp: new Date().toISOString(),
1283
+ source_project: normalizeText(metadata.source_project) || normalizeText(metadata.project) || 'moqui-standard-rebuild',
1284
+ specs: specPlan.map(item => item.spec_id),
1285
+ templates: recommendedTemplates.map(item => item.id),
1286
+ capabilities,
1287
+ ontology_validation: {
1288
+ status: 'pending',
1289
+ source: 'moqui-standard-rebuild',
1290
+ generated_at: new Date().toISOString()
1291
+ },
1292
+ known_gaps: prioritizedGaps.map(item => ({
1293
+ template: item.template_id,
1294
+ score: item.score,
1295
+ status: item.status,
1296
+ next_actions: item.next_actions
1297
+ }))
1298
+ };
1299
+
1300
+ const bundleFiles = await writeBundle(bundleDir, {
1301
+ handoff_manifest: handoffManifest,
1302
+ ontology_seed: ontologySeed,
1303
+ copilot_contract: copilotContract,
1304
+ recovery_spec_plan: specPlan,
1305
+ prioritized_gaps: prioritizedGaps,
1306
+ remediation_plan: remediationPlan
1307
+ });
1308
+
1309
+ const report = {
1310
+ mode: 'moqui-standard-rebuild',
1311
+ generated_at: new Date().toISOString(),
1312
+ metadata_file: path.relative(process.cwd(), metadataPath),
1313
+ inventory: {
1314
+ entities: entities.length,
1315
+ services: services.length,
1316
+ screens: screens.length,
1317
+ forms: forms.length,
1318
+ business_rules: businessRules.length,
1319
+ decisions: decisions.length,
1320
+ domains
1321
+ },
1322
+ recovery: {
1323
+ readiness_summary: readinessSummary,
1324
+ readiness_matrix: readinessMatrix,
1325
+ prioritized_gaps: prioritizedGaps,
1326
+ remediation_plan: remediationPlan,
1327
+ recommended_templates: recommendedTemplates,
1328
+ capabilities,
1329
+ spec_plan: specPlan
1330
+ },
1331
+ output: {
1332
+ bundle_dir: path.relative(process.cwd(), bundleDir),
1333
+ handoff_manifest: path.relative(process.cwd(), bundleFiles.handoffManifestPath),
1334
+ ontology_seed: path.relative(process.cwd(), bundleFiles.ontologySeedPath),
1335
+ copilot_contract: path.relative(process.cwd(), bundleFiles.copilotContractPath),
1336
+ copilot_playbook: path.relative(process.cwd(), bundleFiles.copilotPlaybookPath),
1337
+ recovery_spec_plan: path.relative(process.cwd(), bundleFiles.recoverySpecPath),
1338
+ remediation_queue: path.relative(process.cwd(), bundleFiles.remediationQueuePath),
1339
+ remediation_plan_json: path.relative(process.cwd(), bundleFiles.remediationPlanJsonPath),
1340
+ remediation_plan_markdown: path.relative(process.cwd(), bundleFiles.remediationPlanMarkdownPath)
1341
+ }
1342
+ };
1343
+
1344
+ await fs.ensureDir(path.dirname(outPath));
1345
+ await fs.writeJson(outPath, report, { spaces: 2 });
1346
+ await fs.ensureDir(path.dirname(markdownPath));
1347
+ await fs.writeFile(markdownPath, buildMarkdownReport(report), 'utf8');
1348
+
1349
+ const stdoutPayload = {
1350
+ ...report,
1351
+ report_files: {
1352
+ json: path.relative(process.cwd(), outPath),
1353
+ markdown: path.relative(process.cwd(), markdownPath)
1354
+ }
1355
+ };
1356
+
1357
+ if (options.json) {
1358
+ console.log(JSON.stringify(stdoutPayload, null, 2));
1359
+ } else {
1360
+ console.log('Moqui standard rebuild plan generated.');
1361
+ console.log(` JSON: ${path.relative(process.cwd(), outPath)}`);
1362
+ console.log(` Markdown: ${path.relative(process.cwd(), markdownPath)}`);
1363
+ console.log(` Bundle: ${path.relative(process.cwd(), bundleDir)}`);
1364
+ }
1365
+ }
1366
+
1367
+ main().catch((error) => {
1368
+ console.error(`Failed to build Moqui standard rebuild plan: ${error.message}`);
1369
+ process.exitCode = 1;
1370
+ });