scene-capability-engine 3.6.45 → 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 (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/docs/releases/README.md +1 -0
  3. package/docs/releases/v3.6.46.md +23 -0
  4. package/docs/zh/releases/README.md +1 -0
  5. package/docs/zh/releases/v3.6.46.md +23 -0
  6. package/package.json +4 -2
  7. package/scripts/auto-strategy-router.js +231 -0
  8. package/scripts/capability-mapping-report.js +339 -0
  9. package/scripts/check-branding-consistency.js +140 -0
  10. package/scripts/check-sce-tracking.js +54 -0
  11. package/scripts/check-skip-allowlist.js +94 -0
  12. package/scripts/errorbook-registry-health-gate.js +172 -0
  13. package/scripts/errorbook-release-gate.js +132 -0
  14. package/scripts/failure-attribution-repair.js +317 -0
  15. package/scripts/git-managed-gate.js +464 -0
  16. package/scripts/interactive-approval-event-projection.js +400 -0
  17. package/scripts/interactive-approval-workflow.js +829 -0
  18. package/scripts/interactive-authorization-tier-evaluate.js +413 -0
  19. package/scripts/interactive-change-plan-gate.js +225 -0
  20. package/scripts/interactive-context-bridge.js +617 -0
  21. package/scripts/interactive-customization-loop.js +1690 -0
  22. package/scripts/interactive-dialogue-governance.js +842 -0
  23. package/scripts/interactive-feedback-log.js +253 -0
  24. package/scripts/interactive-flow-smoke.js +238 -0
  25. package/scripts/interactive-flow.js +1059 -0
  26. package/scripts/interactive-governance-report.js +1112 -0
  27. package/scripts/interactive-intent-build.js +707 -0
  28. package/scripts/interactive-loop-smoke.js +215 -0
  29. package/scripts/interactive-moqui-adapter.js +304 -0
  30. package/scripts/interactive-plan-build.js +426 -0
  31. package/scripts/interactive-runtime-policy-evaluate.js +495 -0
  32. package/scripts/interactive-work-order-build.js +552 -0
  33. package/scripts/matrix-regression-gate.js +167 -0
  34. package/scripts/moqui-core-regression-suite.js +397 -0
  35. package/scripts/moqui-lexicon-audit.js +651 -0
  36. package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
  37. package/scripts/moqui-matrix-remediation-queue.js +852 -0
  38. package/scripts/moqui-metadata-extract.js +1340 -0
  39. package/scripts/moqui-rebuild-gate.js +167 -0
  40. package/scripts/moqui-release-summary.js +729 -0
  41. package/scripts/moqui-standard-rebuild.js +1370 -0
  42. package/scripts/moqui-template-baseline-report.js +682 -0
  43. package/scripts/npm-package-runtime-asset-check.js +221 -0
  44. package/scripts/problem-closure-gate.js +441 -0
  45. package/scripts/release-asset-integrity-check.js +216 -0
  46. package/scripts/release-asset-nonempty-normalize.js +166 -0
  47. package/scripts/release-drift-evaluate.js +223 -0
  48. package/scripts/release-drift-signals.js +255 -0
  49. package/scripts/release-governance-snapshot-export.js +132 -0
  50. package/scripts/release-ops-weekly-summary.js +934 -0
  51. package/scripts/release-risk-remediation-bundle.js +315 -0
  52. package/scripts/release-weekly-ops-gate.js +423 -0
  53. package/scripts/state-migration-reconciliation-gate.js +110 -0
  54. package/scripts/state-storage-tiering-audit.js +337 -0
  55. package/scripts/steering-content-audit.js +393 -0
  56. package/scripts/symbol-evidence-locate.js +366 -0
@@ -0,0 +1,1340 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ const DEFAULT_PROJECT_DIR = '.';
8
+ const DEFAULT_OUT = 'docs/moqui/metadata-catalog.json';
9
+ const DEFAULT_MARKDOWN_OUT = 'docs/moqui/metadata-catalog.md';
10
+ const DEFAULT_HANDOFF_MANIFEST = 'docs/handoffs/handoff-manifest.json';
11
+ const DEFAULT_CAPABILITY_MATRIX = 'docs/handoffs/capability-matrix.md';
12
+ const DEFAULT_EVIDENCE_DIR = 'docs/handoffs/evidence';
13
+ const DEFAULT_SALVAGE_DIR = '.sce/recovery/salvage';
14
+ const MAX_HINT_ITEMS = 64;
15
+
16
+ function parseArgs(argv) {
17
+ const options = {
18
+ projectDir: DEFAULT_PROJECT_DIR,
19
+ out: DEFAULT_OUT,
20
+ markdownOut: DEFAULT_MARKDOWN_OUT,
21
+ json: false
22
+ };
23
+
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const token = argv[i];
26
+ const next = argv[i + 1];
27
+ if (token === '--project-dir' && next) {
28
+ options.projectDir = next;
29
+ i += 1;
30
+ } else if (token === '--out' && next) {
31
+ options.out = next;
32
+ i += 1;
33
+ } else if (token === '--markdown-out' && next) {
34
+ options.markdownOut = 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-metadata-extract.js [options]',
49
+ '',
50
+ 'Options:',
51
+ ` --project-dir <path> Moqui project root to scan (default: ${DEFAULT_PROJECT_DIR})`,
52
+ ` --out <path> Metadata JSON output path (default: ${DEFAULT_OUT})`,
53
+ ` --markdown-out <path> Metadata markdown summary path (default: ${DEFAULT_MARKDOWN_OUT})`,
54
+ ' --json Print JSON payload to stdout',
55
+ ' -h, --help Show this help'
56
+ ];
57
+ console.log(lines.join('\n'));
58
+ process.exit(code);
59
+ }
60
+
61
+ function normalizeText(value) {
62
+ if (value === undefined || value === null) {
63
+ return null;
64
+ }
65
+ const text = `${value}`.trim();
66
+ return text.length > 0 ? text : null;
67
+ }
68
+
69
+ function normalizeIdentifier(value) {
70
+ const text = normalizeText(value);
71
+ if (!text) {
72
+ return null;
73
+ }
74
+ return text
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9]+/g, '-')
77
+ .replace(/^-+|-+$/g, '');
78
+ }
79
+
80
+ function normalizeEntityName(entityName, packageName) {
81
+ const entity = normalizeText(entityName);
82
+ if (!entity) {
83
+ return null;
84
+ }
85
+ if (entity.includes('.')) {
86
+ return entity;
87
+ }
88
+ const pkg = normalizeText(packageName);
89
+ return pkg ? `${pkg}.${entity}` : entity;
90
+ }
91
+
92
+ function parseAttributes(tagText) {
93
+ const attrs = {};
94
+ if (!tagText) {
95
+ return attrs;
96
+ }
97
+ const pattern = /([a-zA-Z0-9:_-]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
98
+ let match = pattern.exec(tagText);
99
+ while (match) {
100
+ const key = `${match[1]}`.trim();
101
+ const value = normalizeText(match[3] !== undefined ? match[3] : match[4]);
102
+ if (key && value !== null) {
103
+ attrs[key] = value;
104
+ }
105
+ match = pattern.exec(tagText);
106
+ }
107
+ return attrs;
108
+ }
109
+
110
+ function firstDefined(source, keys) {
111
+ for (const key of keys) {
112
+ const value = source && Object.prototype.hasOwnProperty.call(source, key)
113
+ ? source[key]
114
+ : undefined;
115
+ const normalized = normalizeText(value);
116
+ if (normalized) {
117
+ return normalized;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function toArray(value) {
124
+ if (Array.isArray(value)) {
125
+ return value;
126
+ }
127
+ if (value === undefined || value === null) {
128
+ return [];
129
+ }
130
+ return [value];
131
+ }
132
+
133
+ function readPathValue(payload, pathText) {
134
+ if (!payload || typeof payload !== 'object') {
135
+ return undefined;
136
+ }
137
+ const segments = `${pathText || ''}`
138
+ .split('.')
139
+ .map(token => token.trim())
140
+ .filter(Boolean);
141
+ let cursor = payload;
142
+ for (const segment of segments) {
143
+ if (!cursor || typeof cursor !== 'object') {
144
+ return undefined;
145
+ }
146
+ cursor = cursor[segment];
147
+ }
148
+ return cursor;
149
+ }
150
+
151
+ function collectArrayFromPaths(payload, paths) {
152
+ for (const pathText of paths) {
153
+ const raw = readPathValue(payload, pathText);
154
+ if (Array.isArray(raw)) {
155
+ return raw;
156
+ }
157
+ }
158
+ return [];
159
+ }
160
+
161
+ function pickField(source, candidates) {
162
+ for (const candidate of candidates) {
163
+ const value = readPathValue(source, candidate);
164
+ const text = normalizeText(value);
165
+ if (text) {
166
+ return text;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function readNumberFirst(payload, paths) {
173
+ for (const pathText of paths) {
174
+ const value = readPathValue(payload, pathText);
175
+ const number = Number(value);
176
+ if (Number.isFinite(number) && number > 0) {
177
+ return number;
178
+ }
179
+ }
180
+ return 0;
181
+ }
182
+
183
+ function collectEntityModels(content, sourceFile) {
184
+ const models = [];
185
+ const blocks = content.match(/<entity\b[\s\S]*?<\/entity>/gi) || [];
186
+ for (const block of blocks) {
187
+ const openTagMatch = block.match(/<entity\b[^>]*>/i);
188
+ if (!openTagMatch) {
189
+ continue;
190
+ }
191
+ const attrs = parseAttributes(openTagMatch[0]);
192
+ const name = firstDefined(attrs, ['entity-name', 'name']);
193
+ const packageName = firstDefined(attrs, ['package-name', 'package']);
194
+ const fullName = normalizeEntityName(name, packageName);
195
+ if (!fullName) {
196
+ continue;
197
+ }
198
+
199
+ const relations = [];
200
+ const relationPattern = /<relationship\b[^>]*>/gi;
201
+ let relationMatch = relationPattern.exec(block);
202
+ while (relationMatch) {
203
+ const relationAttrs = parseAttributes(relationMatch[0]);
204
+ const related = firstDefined(relationAttrs, [
205
+ 'related-entity-name',
206
+ 'related-entity',
207
+ 'related',
208
+ 'entity-name'
209
+ ]);
210
+ const normalizedRelated = normalizeText(related);
211
+ if (normalizedRelated) {
212
+ relations.push(normalizedRelated);
213
+ }
214
+ relationMatch = relationPattern.exec(block);
215
+ }
216
+
217
+ models.push({
218
+ name: fullName,
219
+ package: packageName,
220
+ relations,
221
+ source_file: sourceFile
222
+ });
223
+ }
224
+ return models;
225
+ }
226
+
227
+ function collectServiceModels(content, sourceFile) {
228
+ const models = [];
229
+ const blocks = content.match(/<service\b[\s\S]*?<\/service>/gi) || [];
230
+ for (const block of blocks) {
231
+ const openTagMatch = block.match(/<service\b[^>]*>/i);
232
+ if (!openTagMatch) {
233
+ continue;
234
+ }
235
+ const attrs = parseAttributes(openTagMatch[0]);
236
+ const verb = firstDefined(attrs, ['verb']);
237
+ const noun = firstDefined(attrs, ['noun']);
238
+ const explicitName = firstDefined(attrs, ['service-name', 'name']);
239
+ let name = explicitName;
240
+ if (!name && (verb || noun)) {
241
+ name = `${verb || 'service'}#${noun || 'operation'}`;
242
+ }
243
+ if (!name) {
244
+ continue;
245
+ }
246
+
247
+ const entities = [];
248
+ const entityRefPattern = /entity-name\s*=\s*("([^"]*)"|'([^']*)')/gi;
249
+ let entityMatch = entityRefPattern.exec(block);
250
+ while (entityMatch) {
251
+ const value = normalizeText(entityMatch[2] !== undefined ? entityMatch[2] : entityMatch[3]);
252
+ if (value) {
253
+ entities.push(value);
254
+ }
255
+ entityMatch = entityRefPattern.exec(block);
256
+ }
257
+
258
+ models.push({
259
+ name,
260
+ verb,
261
+ noun,
262
+ entities,
263
+ source_file: sourceFile
264
+ });
265
+ }
266
+ return models;
267
+ }
268
+
269
+ function collectScreenModels(content, sourceFile) {
270
+ const hasScreen = /<screen\b/i.test(content);
271
+ if (!hasScreen) {
272
+ return [];
273
+ }
274
+
275
+ const screenTagMatch = content.match(/<screen\b[^>]*>/i);
276
+ const screenAttrs = parseAttributes(screenTagMatch ? screenTagMatch[0] : '');
277
+ const screenPath = firstDefined(screenAttrs, ['name', 'location']) || sourceFile;
278
+
279
+ const services = [];
280
+ const servicePattern = /service-name\s*=\s*("([^"]*)"|'([^']*)')/gi;
281
+ let serviceMatch = servicePattern.exec(content);
282
+ while (serviceMatch) {
283
+ const value = normalizeText(serviceMatch[2] !== undefined ? serviceMatch[2] : serviceMatch[3]);
284
+ if (value) {
285
+ services.push(value);
286
+ }
287
+ serviceMatch = servicePattern.exec(content);
288
+ }
289
+
290
+ const entities = [];
291
+ const entityPattern = /entity-name\s*=\s*("([^"]*)"|'([^']*)')/gi;
292
+ let entityMatch = entityPattern.exec(content);
293
+ while (entityMatch) {
294
+ const value = normalizeText(entityMatch[2] !== undefined ? entityMatch[2] : entityMatch[3]);
295
+ if (value) {
296
+ entities.push(value);
297
+ }
298
+ entityMatch = entityPattern.exec(content);
299
+ }
300
+
301
+ return [{
302
+ path: screenPath,
303
+ services,
304
+ entities,
305
+ source_file: sourceFile
306
+ }];
307
+ }
308
+
309
+ function collectFormModels(content, sourceFile) {
310
+ const models = [];
311
+ const blocks = content.match(/<(form-single|form-list|form)\b[\s\S]*?<\/\1>/gi) || [];
312
+ for (const block of blocks) {
313
+ const openTagMatch = block.match(/<(form-single|form-list|form)\b[^>]*>/i);
314
+ if (!openTagMatch) {
315
+ continue;
316
+ }
317
+ const attrs = parseAttributes(openTagMatch[0]);
318
+ const name = firstDefined(attrs, ['name']) || `${sourceFile}#form`;
319
+ const fieldCount = (block.match(/<field\b/gi) || []).length;
320
+ models.push({
321
+ name,
322
+ screen: sourceFile,
323
+ field_count: fieldCount,
324
+ source_file: sourceFile
325
+ });
326
+ }
327
+
328
+ const selfClosing = content.match(/<(form-single|form-list|form)\b[^>]*\/>/gi) || [];
329
+ for (const tag of selfClosing) {
330
+ const attrs = parseAttributes(tag);
331
+ const name = firstDefined(attrs, ['name']);
332
+ if (!name) {
333
+ continue;
334
+ }
335
+ models.push({
336
+ name,
337
+ screen: sourceFile,
338
+ field_count: 0,
339
+ source_file: sourceFile
340
+ });
341
+ }
342
+
343
+ return models;
344
+ }
345
+
346
+ function collectNamedTags(content, tagName, fieldLabel, sourceFile) {
347
+ const items = [];
348
+ const pattern = new RegExp(`<${tagName}\\b[^>]*>`, 'gi');
349
+ let match = pattern.exec(content);
350
+ while (match) {
351
+ const attrs = parseAttributes(match[0]);
352
+ const value = firstDefined(attrs, ['name', 'id', fieldLabel]);
353
+ if (value) {
354
+ items.push({
355
+ name: value,
356
+ source_file: sourceFile
357
+ });
358
+ }
359
+ match = pattern.exec(content);
360
+ }
361
+ return items;
362
+ }
363
+
364
+ function parseNamedArray(entries, fieldCandidates, sourceFile) {
365
+ const result = [];
366
+ for (const entry of toArray(entries)) {
367
+ const value = typeof entry === 'string'
368
+ ? normalizeText(entry)
369
+ : pickField(entry, fieldCandidates);
370
+ if (!value) {
371
+ continue;
372
+ }
373
+ result.push({
374
+ name: value,
375
+ source_file: sourceFile
376
+ });
377
+ }
378
+ return result;
379
+ }
380
+
381
+ function collectModelsFromScenePackage(payload, sourceFile) {
382
+ if (!payload || typeof payload !== 'object') {
383
+ return {
384
+ entities: [],
385
+ services: [],
386
+ screens: [],
387
+ forms: [],
388
+ businessRules: [],
389
+ decisions: [],
390
+ templateRefs: [],
391
+ capabilityRefs: []
392
+ };
393
+ }
394
+
395
+ const ontologyModel = payload.ontology_model && typeof payload.ontology_model === 'object'
396
+ ? payload.ontology_model
397
+ : {};
398
+ const capabilityContract = payload.capability_contract && typeof payload.capability_contract === 'object'
399
+ ? payload.capability_contract
400
+ : {};
401
+ const governanceContract = payload.governance_contract && typeof payload.governance_contract === 'object'
402
+ ? payload.governance_contract
403
+ : {};
404
+ const capabilities = payload.capabilities && typeof payload.capabilities === 'object'
405
+ ? payload.capabilities
406
+ : {};
407
+ const artifacts = payload.artifacts && typeof payload.artifacts === 'object'
408
+ ? payload.artifacts
409
+ : {};
410
+ const metadata = payload.metadata && typeof payload.metadata === 'object'
411
+ ? payload.metadata
412
+ : {};
413
+ const agentHints = payload.agent_hints && typeof payload.agent_hints === 'object'
414
+ ? payload.agent_hints
415
+ : {};
416
+
417
+ const entities = [];
418
+ const services = [];
419
+ const screens = [];
420
+ const forms = [];
421
+ const businessRules = [];
422
+ const decisions = [];
423
+ const bindingRefs = [];
424
+ const ontologyEntityRefs = [];
425
+
426
+ const ontologyEntities = toArray(ontologyModel.entities);
427
+ for (const item of ontologyEntities) {
428
+ if (!item || typeof item !== 'object') {
429
+ continue;
430
+ }
431
+ const id = pickField(item, ['id', 'name', 'ref', 'entity']);
432
+ if (!id) {
433
+ continue;
434
+ }
435
+ const type = normalizeIdentifier(pickField(item, ['type'])) || '';
436
+ if (type === 'entity' || /^entity:/i.test(id)) {
437
+ const entityRef = id.replace(/^entity:/i, '');
438
+ ontologyEntityRefs.push(entityRef);
439
+ entities.push({
440
+ name: entityRef,
441
+ package: null,
442
+ relations: [],
443
+ source_file: sourceFile
444
+ });
445
+ continue;
446
+ }
447
+ if (['query', 'invoke', 'service', 'action', 'command', 'operation'].includes(type)) {
448
+ services.push({
449
+ name: id,
450
+ verb: null,
451
+ noun: null,
452
+ entities: [],
453
+ source_file: sourceFile
454
+ });
455
+ }
456
+ }
457
+
458
+ for (const binding of toArray(capabilityContract.bindings)) {
459
+ if (!binding || typeof binding !== 'object') {
460
+ continue;
461
+ }
462
+ const ref = pickField(binding, ['ref', 'service', 'service_name', 'name', 'id']);
463
+ if (!ref) {
464
+ continue;
465
+ }
466
+ services.push({
467
+ name: ref,
468
+ verb: pickField(binding, ['verb']),
469
+ noun: pickField(binding, ['noun']),
470
+ entities: toArray(binding.entities || binding.entity_refs)
471
+ .map(item => (typeof item === 'string' ? normalizeText(item) : pickField(item, ['name', 'id', 'entity'])))
472
+ .filter(Boolean),
473
+ source_file: sourceFile
474
+ });
475
+ bindingRefs.push(ref);
476
+ }
477
+
478
+ const entryScene = pickField(artifacts, ['entry_scene', 'entryScene', 'scene']);
479
+ const resolvedScenePath = entryScene
480
+ ? `${sourceFile}#${entryScene}`
481
+ : null;
482
+ if (entryScene) {
483
+ screens.push({
484
+ path: resolvedScenePath,
485
+ services: bindingRefs.slice(0, MAX_HINT_ITEMS),
486
+ entities: deduplicateBy(ontologyEntityRefs.map(name => ({ name })), item => item.name).map(item => item.name),
487
+ source_file: sourceFile
488
+ });
489
+ } else {
490
+ const sceneName = pickField(metadata, ['name', 'summary']);
491
+ if (sceneName) {
492
+ screens.push({
493
+ path: `scene/${normalizeIdentifier(sceneName)}`,
494
+ services: bindingRefs.slice(0, MAX_HINT_ITEMS),
495
+ entities: deduplicateBy(ontologyEntityRefs.map(name => ({ name })), item => item.name).map(item => item.name),
496
+ source_file: sourceFile
497
+ });
498
+ }
499
+ }
500
+
501
+ const parameterCount = Math.max(
502
+ toArray(payload.parameters).length,
503
+ Math.min(toArray(capabilityContract.bindings).length, 6)
504
+ );
505
+ if (parameterCount > 0) {
506
+ const formName = pickField(metadata, ['name']) || normalizeIdentifier(sourceFile) || 'scene-form';
507
+ forms.push({
508
+ name: `${formName}-input-form`,
509
+ screen: resolvedScenePath || sourceFile,
510
+ field_count: parameterCount,
511
+ source_file: sourceFile
512
+ });
513
+ }
514
+
515
+ businessRules.push(...parseNamedArray(governanceContract.business_rules, ['name', 'id', 'rule', 'description'], sourceFile));
516
+ businessRules.push(...parseNamedArray(ontologyModel.business_rules, ['name', 'id', 'rule', 'description'], sourceFile));
517
+ decisions.push(...parseNamedArray(governanceContract.decision_logic, ['name', 'id', 'decision', 'description'], sourceFile));
518
+ decisions.push(...parseNamedArray(ontologyModel.decision_logic, ['name', 'id', 'decision', 'description'], sourceFile));
519
+ decisions.push(...parseNamedArray(agentHints.decision_logic, ['name', 'id', 'decision'], sourceFile));
520
+
521
+ const templateRefs = [];
522
+ const templateName = pickField(metadata, ['name']);
523
+ if (templateName) {
524
+ templateRefs.push(templateName);
525
+ }
526
+ const capabilityRefs = toArray(capabilities.provides)
527
+ .concat(toArray(capabilities.requires))
528
+ .map(item => (typeof item === 'string' ? normalizeText(item) : pickField(item, ['name', 'id'])))
529
+ .filter(Boolean);
530
+
531
+ return {
532
+ entities,
533
+ services,
534
+ screens,
535
+ forms,
536
+ businessRules,
537
+ decisions,
538
+ templateRefs,
539
+ capabilityRefs
540
+ };
541
+ }
542
+
543
+ function collectModelsFromGenericJson(payload, sourceFile) {
544
+ if (!payload || typeof payload !== 'object') {
545
+ return {
546
+ entities: [],
547
+ services: [],
548
+ screens: [],
549
+ forms: [],
550
+ businessRules: [],
551
+ decisions: [],
552
+ businessRuleTotalHint: 0,
553
+ decisionTotalHint: 0
554
+ };
555
+ }
556
+
557
+ const entities = collectArrayFromPaths(payload, [
558
+ 'entities',
559
+ 'entity_catalog',
560
+ 'entity_catalog.entities',
561
+ 'catalog.entities'
562
+ ]).map((entry) => {
563
+ const name = typeof entry === 'string'
564
+ ? normalizeText(entry)
565
+ : pickField(entry, ['name', 'id', 'entity', 'entity_name', 'ref']);
566
+ if (!name) {
567
+ return null;
568
+ }
569
+ return {
570
+ name,
571
+ package: typeof entry === 'object' ? pickField(entry, ['package', 'package_name']) : null,
572
+ relations: [],
573
+ source_file: sourceFile
574
+ };
575
+ }).filter(Boolean);
576
+
577
+ const services = collectArrayFromPaths(payload, [
578
+ 'services',
579
+ 'service_catalog',
580
+ 'service_catalog.services',
581
+ 'catalog.services',
582
+ 'capability_contract.bindings'
583
+ ]).map((entry) => {
584
+ const name = typeof entry === 'string'
585
+ ? normalizeText(entry)
586
+ : pickField(entry, ['name', 'id', 'service', 'service_name', 'ref']);
587
+ if (!name) {
588
+ return null;
589
+ }
590
+ return {
591
+ name,
592
+ verb: typeof entry === 'object' ? pickField(entry, ['verb']) : null,
593
+ noun: typeof entry === 'object' ? pickField(entry, ['noun']) : null,
594
+ entities: [],
595
+ source_file: sourceFile
596
+ };
597
+ }).filter(Boolean);
598
+
599
+ const screens = collectArrayFromPaths(payload, [
600
+ 'screens',
601
+ 'screen_catalog',
602
+ 'screen_catalog.screens',
603
+ 'catalog.screens',
604
+ 'scenes',
605
+ 'pages'
606
+ ]).map((entry) => {
607
+ const screenPath = typeof entry === 'string'
608
+ ? normalizeText(entry)
609
+ : pickField(entry, ['path', 'screen_path', 'name', 'id', 'ref']);
610
+ if (!screenPath) {
611
+ return null;
612
+ }
613
+ return {
614
+ path: screenPath,
615
+ services: [],
616
+ entities: [],
617
+ source_file: sourceFile
618
+ };
619
+ }).filter(Boolean);
620
+
621
+ const forms = collectArrayFromPaths(payload, [
622
+ 'forms',
623
+ 'form_catalog',
624
+ 'form_catalog.forms',
625
+ 'catalog.forms'
626
+ ]).map((entry) => {
627
+ const name = typeof entry === 'string'
628
+ ? normalizeText(entry)
629
+ : pickField(entry, ['name', 'id', 'form', 'form_name', 'ref']);
630
+ if (!name) {
631
+ return null;
632
+ }
633
+ const fieldCount = typeof entry === 'object'
634
+ ? toArray(entry.fields || entry.field_defs || entry.columns).length
635
+ : 0;
636
+ return {
637
+ name,
638
+ screen: typeof entry === 'object' ? pickField(entry, ['screen', 'screen_path', 'screen_ref']) : null,
639
+ field_count: fieldCount,
640
+ source_file: sourceFile
641
+ };
642
+ }).filter(Boolean);
643
+
644
+ const businessRules = []
645
+ .concat(parseNamedArray(collectArrayFromPaths(payload, ['business_rules', 'rules', 'rule_catalog']), ['name', 'id', 'rule'], sourceFile))
646
+ .concat(parseNamedArray(readPathValue(payload, 'governance_contract.business_rules'), ['name', 'id', 'rule', 'description'], sourceFile))
647
+ .concat(parseNamedArray(readPathValue(payload, 'ontology_model.business_rules'), ['name', 'id', 'rule', 'description'], sourceFile));
648
+ const decisions = []
649
+ .concat(parseNamedArray(collectArrayFromPaths(payload, ['decisions', 'decision_logic', 'decision_catalog']), ['name', 'id', 'decision'], sourceFile))
650
+ .concat(parseNamedArray(readPathValue(payload, 'governance_contract.decision_logic'), ['name', 'id', 'decision', 'description'], sourceFile))
651
+ .concat(parseNamedArray(readPathValue(payload, 'ontology_model.decision_logic'), ['name', 'id', 'decision', 'description'], sourceFile));
652
+
653
+ const businessRuleTotalHint = readNumberFirst(payload, [
654
+ 'business_rules.total',
655
+ 'coverage.business_rules.total',
656
+ 'metrics.business_rules.total',
657
+ 'ontology_validation.business_rules.total'
658
+ ]);
659
+ const decisionTotalHint = readNumberFirst(payload, [
660
+ 'decision_logic.total',
661
+ 'coverage.decision_logic.total',
662
+ 'metrics.decision_logic.total',
663
+ 'ontology_validation.decision_logic.total'
664
+ ]);
665
+
666
+ return {
667
+ entities,
668
+ services,
669
+ screens,
670
+ forms,
671
+ businessRules,
672
+ decisions,
673
+ businessRuleTotalHint,
674
+ decisionTotalHint
675
+ };
676
+ }
677
+
678
+ function collectHandoffHints(payload, sourceFile) {
679
+ if (!payload || typeof payload !== 'object') {
680
+ return {
681
+ templates: [],
682
+ capabilities: [],
683
+ specScenePackagePaths: [],
684
+ businessRuleTotalHint: 0,
685
+ decisionTotalHint: 0
686
+ };
687
+ }
688
+
689
+ const templates = toArray(payload.templates)
690
+ .map(entry => (typeof entry === 'string'
691
+ ? normalizeText(entry)
692
+ : pickField(entry, ['id', 'name', 'template', 'template_id', 'path'])))
693
+ .filter(Boolean);
694
+ const capabilities = toArray(payload.capabilities)
695
+ .map(entry => (typeof entry === 'string'
696
+ ? normalizeText(entry)
697
+ : pickField(entry, ['id', 'name', 'capability', 'ref'])))
698
+ .filter(Boolean);
699
+
700
+ const specScenePackagePaths = toArray(payload.specs)
701
+ .map((entry) => {
702
+ if (!entry || typeof entry !== 'object') {
703
+ return null;
704
+ }
705
+ return pickField(entry, ['scene_package', 'spec_package', 'package', 'specPackage']);
706
+ })
707
+ .filter(Boolean);
708
+
709
+ const businessRuleTotalHint = readNumberFirst(payload, [
710
+ 'ontology_validation.business_rules.total',
711
+ 'business_rules.total',
712
+ 'coverage.business_rules.total',
713
+ 'metrics.business_rules.total'
714
+ ]);
715
+ const decisionTotalHint = readNumberFirst(payload, [
716
+ 'ontology_validation.decision_logic.total',
717
+ 'decision_logic.total',
718
+ 'coverage.decision_logic.total',
719
+ 'metrics.decision_logic.total'
720
+ ]);
721
+
722
+ const knownGaps = parseNamedArray(payload.known_gaps, ['name', 'id', 'gap'], sourceFile)
723
+ .map(item => item.name)
724
+ .filter(Boolean);
725
+
726
+ return {
727
+ templates,
728
+ capabilities,
729
+ specScenePackagePaths,
730
+ businessRuleTotalHint,
731
+ decisionTotalHint,
732
+ knownGaps
733
+ };
734
+ }
735
+
736
+ function classifyMatrixToken(token) {
737
+ const text = normalizeText(token);
738
+ if (!text) {
739
+ return null;
740
+ }
741
+ const lower = text.toLowerCase();
742
+ if (/(scene--|template)/.test(lower)) return 'template';
743
+ if (/(decision|routing|strategy)/.test(lower)) return 'decision';
744
+ if (/(rule|policy|governance|compliance)/.test(lower)) return 'business_rule';
745
+ if (/(form|entry)/.test(lower)) return 'form';
746
+ if (/(screen|scene|ui|page|dashboard|panel|dialog)/.test(lower)) return 'screen';
747
+ if (/(service|workflow|approval|invoke|query|report|ops)/.test(lower)) return 'service';
748
+ return 'entity';
749
+ }
750
+
751
+ function collectMatrixHints(content, sourceFile) {
752
+ const entities = [];
753
+ const services = [];
754
+ const screens = [];
755
+ const forms = [];
756
+ const businessRules = [];
757
+ const decisions = [];
758
+ const templates = [];
759
+ const capabilities = [];
760
+
761
+ const lines = `${content || ''}`.split(/\r?\n/);
762
+ for (const line of lines) {
763
+ const text = normalizeText(line);
764
+ if (!text || !text.startsWith('|')) {
765
+ continue;
766
+ }
767
+ if (/^\|\s*-+\s*\|/.test(text)) {
768
+ continue;
769
+ }
770
+ const columns = text
771
+ .split('|')
772
+ .map(item => item.trim())
773
+ .filter(Boolean);
774
+ if (columns.length < 3) {
775
+ continue;
776
+ }
777
+ const normalizedColumns = columns.map(item => (item || '').toLowerCase());
778
+ const headerLabels = new Set([
779
+ 'priority',
780
+ 'track',
781
+ 'item',
782
+ 'spec',
783
+ 'moqui capability',
784
+ 'capability focus',
785
+ 'sce scene pattern',
786
+ 'template id',
787
+ 'ontology anchors',
788
+ 'governance/gate focus',
789
+ 'status',
790
+ 'result'
791
+ ]);
792
+ const headerMatches = normalizedColumns.filter(item => headerLabels.has(item)).length;
793
+ if (headerMatches >= 2) {
794
+ continue;
795
+ }
796
+ const lowerLine = text.toLowerCase();
797
+ if (/priority|capability|template id|status/.test(lowerLine) && /---/.test(lowerLine)) {
798
+ continue;
799
+ }
800
+
801
+ const inlineTokens = [];
802
+ const tokenPattern = /`([^`]+)`/g;
803
+ let match = tokenPattern.exec(text);
804
+ while (match) {
805
+ const token = normalizeText(match[1]);
806
+ if (token) {
807
+ inlineTokens.push(token);
808
+ }
809
+ match = tokenPattern.exec(text);
810
+ }
811
+
812
+ for (const token of inlineTokens) {
813
+ const kind = classifyMatrixToken(token);
814
+ if (kind === 'template') {
815
+ templates.push(token);
816
+ } else if (kind === 'service') {
817
+ services.push({
818
+ name: token,
819
+ verb: null,
820
+ noun: null,
821
+ entities: [],
822
+ source_file: sourceFile
823
+ });
824
+ } else if (kind === 'screen') {
825
+ screens.push({
826
+ path: token,
827
+ services: [],
828
+ entities: [],
829
+ source_file: sourceFile
830
+ });
831
+ } else if (kind === 'form') {
832
+ forms.push({
833
+ name: token,
834
+ screen: sourceFile,
835
+ field_count: 0,
836
+ source_file: sourceFile
837
+ });
838
+ } else if (kind === 'business_rule') {
839
+ businessRules.push({
840
+ name: token,
841
+ source_file: sourceFile
842
+ });
843
+ } else if (kind === 'decision') {
844
+ decisions.push({
845
+ name: token,
846
+ source_file: sourceFile
847
+ });
848
+ } else if (kind === 'entity') {
849
+ entities.push({
850
+ name: token,
851
+ package: null,
852
+ relations: [],
853
+ source_file: sourceFile
854
+ });
855
+ }
856
+ }
857
+
858
+ if (columns.length >= 3) {
859
+ const capabilityText = normalizeText(columns[2]);
860
+ if (capabilityText && !/^(full|pass|in progress|template-ready|matrix-intake-ready)$/i.test(capabilityText)) {
861
+ capabilities.push(capabilityText);
862
+ }
863
+ }
864
+ }
865
+
866
+ return {
867
+ entities,
868
+ services,
869
+ screens,
870
+ forms,
871
+ businessRules,
872
+ decisions,
873
+ templates,
874
+ capabilities
875
+ };
876
+ }
877
+
878
+ function inferModelsFromHints(hints, sourceFile) {
879
+ const entities = [];
880
+ const services = [];
881
+ const screens = [];
882
+ const forms = [];
883
+ const businessRules = [];
884
+ const decisions = [];
885
+
886
+ const hintRefs = deduplicateBy(
887
+ toArray(hints.templates).concat(toArray(hints.capabilities)).map(item => ({ name: item })),
888
+ item => item && item.name
889
+ )
890
+ .map(item => item.name)
891
+ .slice(0, MAX_HINT_ITEMS);
892
+
893
+ const addRule = (name) => {
894
+ const text = normalizeText(name);
895
+ if (!text) {
896
+ return;
897
+ }
898
+ businessRules.push({ name: text, source_file: sourceFile });
899
+ };
900
+ const addDecision = (name) => {
901
+ const text = normalizeText(name);
902
+ if (!text) {
903
+ return;
904
+ }
905
+ decisions.push({ name: text, source_file: sourceFile });
906
+ };
907
+
908
+ for (const rawRef of hintRefs) {
909
+ const ref = normalizeText(rawRef);
910
+ if (!ref) {
911
+ continue;
912
+ }
913
+ const slug = normalizeIdentifier(ref);
914
+ if (!slug) {
915
+ continue;
916
+ }
917
+ const lower = ref.toLowerCase();
918
+
919
+ if (/(entity|model|master|party|product|order|inventory|procurement|shipment|return|rma|quality|bom|routing|equipment|cost|invoice|employee|calendar|project|wbs|engineering)/.test(lower)) {
920
+ entities.push({
921
+ name: `hint.${slug}`,
922
+ package: 'hint',
923
+ relations: [],
924
+ source_file: sourceFile
925
+ });
926
+ }
927
+ if (/(service|workflow|approval|invoke|action|orchestration|query|report|ops|governance|runtime)/.test(lower)) {
928
+ services.push({
929
+ name: `hint.service.${slug}`,
930
+ verb: null,
931
+ noun: null,
932
+ entities: [],
933
+ source_file: sourceFile
934
+ });
935
+ }
936
+ if (/(screen|scene|ui|page|panel|dialog|dashboard|hub)/.test(lower)) {
937
+ screens.push({
938
+ path: `hint/${slug}`,
939
+ services: [],
940
+ entities: [],
941
+ source_file: sourceFile
942
+ });
943
+ }
944
+ if (/(form|entry)/.test(lower)) {
945
+ forms.push({
946
+ name: `hint-${slug}-form`,
947
+ screen: `hint/${slug}`,
948
+ field_count: 0,
949
+ source_file: sourceFile
950
+ });
951
+ }
952
+ if (/(rule|governance|policy|compliance|audit)/.test(lower)) {
953
+ addRule(`hint.rule.${slug}`);
954
+ }
955
+ if (/(decision|routing|strategy|approval)/.test(lower)) {
956
+ addDecision(`hint.decision.${slug}`);
957
+ }
958
+ }
959
+
960
+ const ruleTotalHint = Number(hints.businessRuleTotalHint) || 0;
961
+ for (let i = businessRules.length; i < ruleTotalHint; i += 1) {
962
+ addRule(`hint.rule.inferred-${i + 1}`);
963
+ }
964
+ const decisionTotalHint = Number(hints.decisionTotalHint) || 0;
965
+ for (let i = decisions.length; i < decisionTotalHint; i += 1) {
966
+ addDecision(`hint.decision.inferred-${i + 1}`);
967
+ }
968
+
969
+ return {
970
+ entities,
971
+ services,
972
+ screens,
973
+ forms,
974
+ businessRules,
975
+ decisions
976
+ };
977
+ }
978
+
979
+ async function listProjectFiles(projectDir) {
980
+ const xmlFiles = [];
981
+ const scenePackageFiles = [];
982
+ const evidenceJsonFiles = [];
983
+ const salvageJsonFiles = [];
984
+ const stack = [projectDir];
985
+
986
+ while (stack.length > 0) {
987
+ const current = stack.pop();
988
+ const entries = await fs.readdir(current).catch(() => []);
989
+ for (const name of entries) {
990
+ const fullPath = path.join(current, name);
991
+ const stat = await fs.stat(fullPath).catch(() => null);
992
+ if (!stat) {
993
+ continue;
994
+ }
995
+ if (stat.isDirectory()) {
996
+ stack.push(fullPath);
997
+ continue;
998
+ }
999
+ if (!stat.isFile()) {
1000
+ continue;
1001
+ }
1002
+ const relativePath = path.relative(projectDir, fullPath).replace(/\\/g, '/');
1003
+ const normalizedRelative = relativePath.toLowerCase();
1004
+ if (/\.xml$/i.test(name)) {
1005
+ xmlFiles.push(fullPath);
1006
+ continue;
1007
+ }
1008
+ if (/^scene-package\.json$/i.test(name)) {
1009
+ scenePackageFiles.push(fullPath);
1010
+ }
1011
+ if (/\.json$/i.test(name) && normalizedRelative.startsWith(`${DEFAULT_EVIDENCE_DIR.toLowerCase()}/`)) {
1012
+ evidenceJsonFiles.push(fullPath);
1013
+ }
1014
+ if (/\.json$/i.test(name) && normalizedRelative.startsWith(`${DEFAULT_SALVAGE_DIR.toLowerCase()}/`)) {
1015
+ salvageJsonFiles.push(fullPath);
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ xmlFiles.sort();
1021
+ scenePackageFiles.sort();
1022
+ evidenceJsonFiles.sort();
1023
+ salvageJsonFiles.sort();
1024
+ return {
1025
+ xmlFiles,
1026
+ scenePackageFiles,
1027
+ evidenceJsonFiles,
1028
+ salvageJsonFiles
1029
+ };
1030
+ }
1031
+
1032
+ function deduplicateBy(items, keySelector) {
1033
+ const result = [];
1034
+ const seen = new Set();
1035
+ for (const item of items) {
1036
+ const key = normalizeIdentifier(keySelector(item));
1037
+ if (!key || seen.has(key)) {
1038
+ continue;
1039
+ }
1040
+ seen.add(key);
1041
+ result.push(item);
1042
+ }
1043
+ return result;
1044
+ }
1045
+
1046
+ function buildMarkdownReport(report) {
1047
+ const lines = [];
1048
+ lines.push('# Moqui Metadata Catalog');
1049
+ lines.push('');
1050
+ lines.push(`- Generated at: ${report.generated_at}`);
1051
+ lines.push(`- Source project: ${report.source_project}`);
1052
+ lines.push(`- XML files scanned: ${report.scan.xml_file_count}`);
1053
+ lines.push(`- Scene packages scanned: ${report.scan.scene_package_file_count}`);
1054
+ lines.push(`- Handoff manifest: ${report.scan.handoff_manifest_found ? 'found' : 'not found'}`);
1055
+ lines.push(`- Capability matrix: ${report.scan.capability_matrix_found ? 'found' : 'not found'}`);
1056
+ lines.push(`- Evidence JSON scanned: ${report.scan.evidence_json_file_count}`);
1057
+ lines.push(`- Salvage JSON scanned: ${report.scan.salvage_json_file_count}`);
1058
+ lines.push('');
1059
+ lines.push('## Summary');
1060
+ lines.push('');
1061
+ lines.push(`- Entities: ${report.summary.entities}`);
1062
+ lines.push(`- Services: ${report.summary.services}`);
1063
+ lines.push(`- Screens: ${report.summary.screens}`);
1064
+ lines.push(`- Forms: ${report.summary.forms}`);
1065
+ lines.push(`- Business rules: ${report.summary.business_rules}`);
1066
+ lines.push(`- Decisions: ${report.summary.decisions}`);
1067
+ lines.push('');
1068
+
1069
+ const samples = [
1070
+ ['Entities', report.entities.map(item => item.name)],
1071
+ ['Services', report.services.map(item => item.name)],
1072
+ ['Screens', report.screens.map(item => item.path)],
1073
+ ['Forms', report.forms.map(item => item.name)],
1074
+ ['Business Rules', report.business_rules.map(item => item.name)],
1075
+ ['Decisions', report.decisions.map(item => item.name)]
1076
+ ];
1077
+
1078
+ for (const [title, values] of samples) {
1079
+ lines.push(`## ${title}`);
1080
+ lines.push('');
1081
+ if (!values || values.length === 0) {
1082
+ lines.push('- none');
1083
+ lines.push('');
1084
+ continue;
1085
+ }
1086
+ for (const value of values.slice(0, 10)) {
1087
+ lines.push(`- ${value}`);
1088
+ }
1089
+ if (values.length > 10) {
1090
+ lines.push(`- ... (+${values.length - 10} more)`);
1091
+ }
1092
+ lines.push('');
1093
+ }
1094
+
1095
+ return `${lines.join('\n')}\n`;
1096
+ }
1097
+
1098
+ async function main() {
1099
+ const options = parseArgs(process.argv.slice(2));
1100
+ const projectDir = path.resolve(process.cwd(), options.projectDir);
1101
+ const outPath = path.resolve(process.cwd(), options.out);
1102
+ const markdownPath = path.resolve(process.cwd(), options.markdownOut);
1103
+ const handoffManifestPath = path.resolve(projectDir, DEFAULT_HANDOFF_MANIFEST);
1104
+ const capabilityMatrixPath = path.resolve(projectDir, DEFAULT_CAPABILITY_MATRIX);
1105
+
1106
+ if (!(await fs.pathExists(projectDir))) {
1107
+ throw new Error(`project directory not found: ${path.relative(process.cwd(), projectDir)}`);
1108
+ }
1109
+
1110
+ const scannedFiles = await listProjectFiles(projectDir);
1111
+ const xmlFiles = scannedFiles.xmlFiles;
1112
+ const entitiesRaw = [];
1113
+ const servicesRaw = [];
1114
+ const screensRaw = [];
1115
+ const formsRaw = [];
1116
+ const businessRulesRaw = [];
1117
+ const decisionsRaw = [];
1118
+ const hintTemplates = [];
1119
+ const hintCapabilities = [];
1120
+ const hintKnownGaps = [];
1121
+ let businessRuleTotalHint = 0;
1122
+ let decisionTotalHint = 0;
1123
+ const parsedScenePackageFiles = new Set();
1124
+
1125
+ for (const filePath of xmlFiles) {
1126
+ const sourceFile = path.relative(projectDir, filePath).replace(/\\/g, '/');
1127
+ const content = await fs.readFile(filePath, 'utf8');
1128
+ entitiesRaw.push(...collectEntityModels(content, sourceFile));
1129
+ servicesRaw.push(...collectServiceModels(content, sourceFile));
1130
+ screensRaw.push(...collectScreenModels(content, sourceFile));
1131
+ formsRaw.push(...collectFormModels(content, sourceFile));
1132
+ businessRulesRaw.push(...collectNamedTags(content, 'rule', 'rule', sourceFile));
1133
+ decisionsRaw.push(...collectNamedTags(content, 'decision', 'decision', sourceFile));
1134
+ }
1135
+
1136
+ for (const filePath of scannedFiles.scenePackageFiles) {
1137
+ const sourceFile = path.relative(projectDir, filePath).replace(/\\/g, '/');
1138
+ const payload = await fs.readJson(filePath).catch(() => null);
1139
+ if (!payload) {
1140
+ continue;
1141
+ }
1142
+ const models = collectModelsFromScenePackage(payload, sourceFile);
1143
+ entitiesRaw.push(...models.entities);
1144
+ servicesRaw.push(...models.services);
1145
+ screensRaw.push(...models.screens);
1146
+ formsRaw.push(...models.forms);
1147
+ businessRulesRaw.push(...models.businessRules);
1148
+ decisionsRaw.push(...models.decisions);
1149
+ hintTemplates.push(...models.templateRefs);
1150
+ hintCapabilities.push(...models.capabilityRefs);
1151
+ parsedScenePackageFiles.add(path.resolve(filePath).toLowerCase());
1152
+ }
1153
+
1154
+ const handoffManifestFound = await fs.pathExists(handoffManifestPath);
1155
+ if (handoffManifestFound) {
1156
+ const payload = await fs.readJson(handoffManifestPath).catch(() => null);
1157
+ if (payload) {
1158
+ const sourceFile = path.relative(projectDir, handoffManifestPath).replace(/\\/g, '/');
1159
+ const hints = collectHandoffHints(payload, sourceFile);
1160
+ hintTemplates.push(...hints.templates);
1161
+ hintCapabilities.push(...hints.capabilities);
1162
+ hintKnownGaps.push(...toArray(hints.knownGaps));
1163
+ businessRuleTotalHint = Math.max(businessRuleTotalHint, Number(hints.businessRuleTotalHint) || 0);
1164
+ decisionTotalHint = Math.max(decisionTotalHint, Number(hints.decisionTotalHint) || 0);
1165
+
1166
+ for (const scenePackageRelative of hints.specScenePackagePaths) {
1167
+ const resolved = path.resolve(projectDir, scenePackageRelative);
1168
+ const resolvedKey = resolved.toLowerCase();
1169
+ if (parsedScenePackageFiles.has(resolvedKey)) {
1170
+ continue;
1171
+ }
1172
+ if (!(await fs.pathExists(resolved))) {
1173
+ continue;
1174
+ }
1175
+ const packagePayload = await fs.readJson(resolved).catch(() => null);
1176
+ if (!packagePayload) {
1177
+ continue;
1178
+ }
1179
+ const sceneSourceFile = path.relative(projectDir, resolved).replace(/\\/g, '/');
1180
+ const models = collectModelsFromScenePackage(packagePayload, sceneSourceFile);
1181
+ entitiesRaw.push(...models.entities);
1182
+ servicesRaw.push(...models.services);
1183
+ screensRaw.push(...models.screens);
1184
+ formsRaw.push(...models.forms);
1185
+ businessRulesRaw.push(...models.businessRules);
1186
+ decisionsRaw.push(...models.decisions);
1187
+ hintTemplates.push(...models.templateRefs);
1188
+ hintCapabilities.push(...models.capabilityRefs);
1189
+ parsedScenePackageFiles.add(resolvedKey);
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ const capabilityMatrixFound = await fs.pathExists(capabilityMatrixPath);
1195
+ if (capabilityMatrixFound) {
1196
+ const content = await fs.readFile(capabilityMatrixPath, 'utf8').catch(() => null);
1197
+ if (content) {
1198
+ const sourceFile = path.relative(projectDir, capabilityMatrixPath).replace(/\\/g, '/');
1199
+ const matrixHints = collectMatrixHints(content, sourceFile);
1200
+ entitiesRaw.push(...matrixHints.entities);
1201
+ servicesRaw.push(...matrixHints.services);
1202
+ screensRaw.push(...matrixHints.screens);
1203
+ formsRaw.push(...matrixHints.forms);
1204
+ businessRulesRaw.push(...matrixHints.businessRules);
1205
+ decisionsRaw.push(...matrixHints.decisions);
1206
+ hintTemplates.push(...matrixHints.templates);
1207
+ hintCapabilities.push(...matrixHints.capabilities);
1208
+ }
1209
+ }
1210
+
1211
+ for (const filePath of scannedFiles.evidenceJsonFiles.concat(scannedFiles.salvageJsonFiles)) {
1212
+ const sourceFile = path.relative(projectDir, filePath).replace(/\\/g, '/');
1213
+ const payload = await fs.readJson(filePath).catch(() => null);
1214
+ if (!payload) {
1215
+ continue;
1216
+ }
1217
+ const models = collectModelsFromGenericJson(payload, sourceFile);
1218
+ entitiesRaw.push(...models.entities);
1219
+ servicesRaw.push(...models.services);
1220
+ screensRaw.push(...models.screens);
1221
+ formsRaw.push(...models.forms);
1222
+ businessRulesRaw.push(...models.businessRules);
1223
+ decisionsRaw.push(...models.decisions);
1224
+ businessRuleTotalHint = Math.max(businessRuleTotalHint, Number(models.businessRuleTotalHint) || 0);
1225
+ decisionTotalHint = Math.max(decisionTotalHint, Number(models.decisionTotalHint) || 0);
1226
+ }
1227
+
1228
+ const inferred = inferModelsFromHints({
1229
+ templates: hintTemplates,
1230
+ capabilities: hintCapabilities,
1231
+ businessRuleTotalHint,
1232
+ decisionTotalHint
1233
+ }, `${DEFAULT_HANDOFF_MANIFEST}#inferred`);
1234
+ entitiesRaw.push(...inferred.entities);
1235
+ servicesRaw.push(...inferred.services);
1236
+ screensRaw.push(...inferred.screens);
1237
+ formsRaw.push(...inferred.forms);
1238
+ businessRulesRaw.push(...inferred.businessRules);
1239
+ decisionsRaw.push(...inferred.decisions);
1240
+
1241
+ const entities = deduplicateBy(entitiesRaw, item => item && item.name).map(item => ({
1242
+ name: item.name,
1243
+ package: item.package || null,
1244
+ relations: deduplicateBy((item.relations || []).map(name => ({ name })), relation => relation.name).map(
1245
+ relation => relation.name
1246
+ ),
1247
+ source_file: item.source_file
1248
+ }));
1249
+ const services = deduplicateBy(servicesRaw, item => item && item.name).map(item => ({
1250
+ name: item.name,
1251
+ verb: item.verb || null,
1252
+ noun: item.noun || null,
1253
+ entities: deduplicateBy((item.entities || []).map(name => ({ name })), ref => ref.name).map(ref => ref.name),
1254
+ source_file: item.source_file
1255
+ }));
1256
+ const screens = deduplicateBy(screensRaw, item => item && item.path).map(item => ({
1257
+ path: item.path,
1258
+ services: deduplicateBy((item.services || []).map(name => ({ name })), ref => ref.name).map(ref => ref.name),
1259
+ entities: deduplicateBy((item.entities || []).map(name => ({ name })), ref => ref.name).map(ref => ref.name),
1260
+ source_file: item.source_file
1261
+ }));
1262
+ const forms = deduplicateBy(formsRaw, item => item && item.name).map(item => ({
1263
+ name: item.name,
1264
+ screen: item.screen || null,
1265
+ field_count: Number(item.field_count) || 0,
1266
+ source_file: item.source_file
1267
+ }));
1268
+ const businessRules = deduplicateBy(businessRulesRaw, item => item && item.name).slice(0, MAX_HINT_ITEMS * 4);
1269
+ const decisions = deduplicateBy(decisionsRaw, item => item && item.name).slice(0, MAX_HINT_ITEMS * 4);
1270
+
1271
+ const report = {
1272
+ mode: 'moqui-metadata-extract',
1273
+ generated_at: new Date().toISOString(),
1274
+ source_project: projectDir.replace(/\\/g, '/'),
1275
+ scan: {
1276
+ xml_file_count: xmlFiles.length,
1277
+ scene_package_file_count: scannedFiles.scenePackageFiles.length,
1278
+ handoff_manifest_found: handoffManifestFound,
1279
+ capability_matrix_found: capabilityMatrixFound,
1280
+ evidence_json_file_count: scannedFiles.evidenceJsonFiles.length,
1281
+ salvage_json_file_count: scannedFiles.salvageJsonFiles.length,
1282
+ xml_files: xmlFiles.map(file => path.relative(projectDir, file).replace(/\\/g, '/')),
1283
+ scene_package_files: scannedFiles.scenePackageFiles.map(file => path.relative(projectDir, file).replace(/\\/g, '/'))
1284
+ },
1285
+ hints: {
1286
+ templates: deduplicateBy(hintTemplates.map(name => ({ name })), item => item.name)
1287
+ .map(item => item.name)
1288
+ .slice(0, MAX_HINT_ITEMS),
1289
+ capabilities: deduplicateBy(hintCapabilities.map(name => ({ name })), item => item.name)
1290
+ .map(item => item.name)
1291
+ .slice(0, MAX_HINT_ITEMS),
1292
+ known_gaps: deduplicateBy(hintKnownGaps.map(name => ({ name })), item => item.name)
1293
+ .map(item => item.name)
1294
+ .slice(0, Math.floor(MAX_HINT_ITEMS / 2)),
1295
+ business_rule_total_hint: businessRuleTotalHint,
1296
+ decision_total_hint: decisionTotalHint
1297
+ },
1298
+ summary: {
1299
+ entities: entities.length,
1300
+ services: services.length,
1301
+ screens: screens.length,
1302
+ forms: forms.length,
1303
+ business_rules: businessRules.length,
1304
+ decisions: decisions.length
1305
+ },
1306
+ entities,
1307
+ services,
1308
+ screens,
1309
+ forms,
1310
+ business_rules: businessRules,
1311
+ decisions
1312
+ };
1313
+
1314
+ await fs.ensureDir(path.dirname(outPath));
1315
+ await fs.writeJson(outPath, report, { spaces: 2 });
1316
+ await fs.ensureDir(path.dirname(markdownPath));
1317
+ await fs.writeFile(markdownPath, buildMarkdownReport(report), 'utf8');
1318
+
1319
+ const stdoutPayload = {
1320
+ ...report,
1321
+ output: {
1322
+ json: path.relative(process.cwd(), outPath),
1323
+ markdown: path.relative(process.cwd(), markdownPath)
1324
+ }
1325
+ };
1326
+
1327
+ if (options.json) {
1328
+ console.log(JSON.stringify(stdoutPayload, null, 2));
1329
+ } else {
1330
+ console.log('Moqui metadata catalog extracted.');
1331
+ console.log(` JSON: ${path.relative(process.cwd(), outPath)}`);
1332
+ console.log(` Markdown: ${path.relative(process.cwd(), markdownPath)}`);
1333
+ console.log(` XML scanned: ${report.scan.xml_file_count}`);
1334
+ }
1335
+ }
1336
+
1337
+ main().catch((error) => {
1338
+ console.error(`Failed to extract Moqui metadata catalog: ${error.message}`);
1339
+ process.exitCode = 1;
1340
+ });