scene-capability-engine 3.6.32 → 3.6.36

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 (83) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +119 -122
  3. package/README.zh.md +123 -121
  4. package/bin/scene-capability-engine.js +11 -0
  5. package/docs/README.md +21 -32
  6. package/docs/auto-refactor-index.md +384 -0
  7. package/docs/command-reference.md +94 -2
  8. package/docs/magicball-adaptation-task-checklist-v1.md +385 -0
  9. package/docs/magicball-app-bundle-sqlite-and-command-draft.md +539 -0
  10. package/docs/magicball-capability-iteration-api.md +2 -0
  11. package/docs/magicball-capability-iteration-ui.md +2 -0
  12. package/docs/magicball-capability-library.md +2 -0
  13. package/docs/magicball-cli-invocation-examples.md +336 -0
  14. package/docs/magicball-frontend-state-and-command-mapping.md +244 -0
  15. package/docs/magicball-integration-doc-index.md +137 -0
  16. package/docs/magicball-integration-issue-tracker.md +218 -0
  17. package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +249 -0
  18. package/docs/magicball-sce-adaptation-guide.md +203 -0
  19. package/docs/magicball-three-mode-alignment-plan.md +551 -0
  20. package/docs/magicball-ui-surface-checklist.md +126 -0
  21. package/docs/magicball-write-auth-adaptation-guide.md +328 -0
  22. package/docs/refactor-completion-roadmap.md +116 -0
  23. package/docs/zh/README.md +27 -30
  24. package/docs/zh/refactor-completion-roadmap.md +116 -0
  25. package/lib/app/registry-config.js +73 -0
  26. package/lib/app/registry-sync-service.js +228 -0
  27. package/lib/auto/archive-schema-service.js +276 -0
  28. package/lib/auto/archive-summary.js +60 -0
  29. package/lib/auto/batch-goal-input-service.js +543 -0
  30. package/lib/auto/batch-output.js +201 -0
  31. package/lib/auto/batch-summary-storage-service.js +110 -0
  32. package/lib/auto/close-loop-batch-service.js +116 -0
  33. package/lib/auto/close-loop-controller-service.js +287 -0
  34. package/lib/auto/close-loop-program-service.js +283 -0
  35. package/lib/auto/close-loop-recovery-service.js +191 -0
  36. package/lib/auto/close-loop-session-storage-service.js +50 -0
  37. package/lib/auto/controller-lock-service.js +55 -0
  38. package/lib/auto/controller-output.js +32 -0
  39. package/lib/auto/controller-queue-service.js +127 -0
  40. package/lib/auto/controller-session-storage-service.js +105 -0
  41. package/lib/auto/governance-advisory-service.js +208 -0
  42. package/lib/auto/governance-close-loop-service.js +411 -0
  43. package/lib/auto/governance-maintenance-presenter.js +162 -0
  44. package/lib/auto/governance-maintenance-service.js +112 -0
  45. package/lib/auto/governance-session-presenter.js +70 -0
  46. package/lib/auto/governance-session-storage-service.js +198 -0
  47. package/lib/auto/governance-signals.js +139 -0
  48. package/lib/auto/governance-stats-presenter.js +337 -0
  49. package/lib/auto/governance-stats-service.js +115 -0
  50. package/lib/auto/governance-summary.js +703 -0
  51. package/lib/auto/handoff-capability-matrix-service.js +281 -0
  52. package/lib/auto/handoff-evidence-review-service.js +251 -0
  53. package/lib/auto/handoff-release-evidence-service.js +190 -0
  54. package/lib/auto/handoff-release-gate-history-loaders-service.js +502 -0
  55. package/lib/auto/handoff-release-gate-history-service.js +257 -0
  56. package/lib/auto/handoff-reporting-service.js +1407 -0
  57. package/lib/auto/handoff-run-service.js +486 -0
  58. package/lib/auto/handoff-snapshots-service.js +645 -0
  59. package/lib/auto/observability-service.js +132 -0
  60. package/lib/auto/output-writer.js +34 -0
  61. package/lib/auto/program-auto-remediation-service.js +130 -0
  62. package/lib/auto/program-diagnostics.js +138 -0
  63. package/lib/auto/program-governance-helpers.js +306 -0
  64. package/lib/auto/program-governance-loop-service.js +413 -0
  65. package/lib/auto/program-output.js +106 -0
  66. package/lib/auto/program-summary.js +183 -0
  67. package/lib/auto/recovery-memory-service.js +684 -0
  68. package/lib/auto/recovery-selection-service.js +52 -0
  69. package/lib/auto/retention-policy.js +98 -0
  70. package/lib/auto/session-persistence-service.js +106 -0
  71. package/lib/auto/session-presenter.js +105 -0
  72. package/lib/auto/session-prune-service.js +190 -0
  73. package/lib/auto/session-query-service.js +249 -0
  74. package/lib/auto/spec-protection.js +141 -0
  75. package/lib/commands/app.js +911 -0
  76. package/lib/commands/assurance.js +212 -0
  77. package/lib/commands/auto.js +1091 -11063
  78. package/lib/commands/mode.js +321 -0
  79. package/lib/commands/ontology.js +415 -0
  80. package/lib/commands/pm.js +422 -0
  81. package/lib/ontology/seed-profiles.js +160 -0
  82. package/lib/state/sce-state-store.js +3369 -1200
  83. package/package.json +1 -1
@@ -0,0 +1,645 @@
1
+ async function buildAutoHandoffMoquiBaselineSnapshot(projectPath, dependencies = {}) {
2
+ const { fs, path, spawnSync, toAutoHandoffCliPath, buildAutoHandoffMoquiCoverageRegressions, AUTO_HANDOFF_MOQUI_BASELINE_JSON_FILE, AUTO_HANDOFF_MOQUI_BASELINE_MARKDOWN_FILE } = dependencies;
3
+ const scriptPath = path.join(projectPath, 'scripts', 'moqui-template-baseline-report.js');
4
+ if (!(await fs.pathExists(scriptPath))) {
5
+ return {
6
+ status: 'skipped',
7
+ generated: false,
8
+ reason: `baseline script missing: ${toAutoHandoffCliPath(projectPath, scriptPath)}`
9
+ };
10
+ }
11
+
12
+ const outputJsonPath = path.join(projectPath, AUTO_HANDOFF_MOQUI_BASELINE_JSON_FILE);
13
+ const outputMarkdownPath = path.join(projectPath, AUTO_HANDOFF_MOQUI_BASELINE_MARKDOWN_FILE);
14
+ await fs.ensureDir(path.dirname(outputJsonPath));
15
+
16
+ const scriptArgs = [
17
+ scriptPath,
18
+ '--out', outputJsonPath,
19
+ '--markdown-out', outputMarkdownPath,
20
+ '--json'
21
+ ];
22
+
23
+ if (await fs.pathExists(outputJsonPath)) {
24
+ scriptArgs.push('--compare-with', outputJsonPath);
25
+ }
26
+
27
+ const execution = spawnSync(process.execPath, scriptArgs, {
28
+ cwd: projectPath,
29
+ encoding: 'utf8'
30
+ });
31
+
32
+ const stdout = typeof execution.stdout === 'string' ? execution.stdout.trim() : '';
33
+ const stderr = typeof execution.stderr === 'string' ? execution.stderr.trim() : '';
34
+
35
+ if (execution.error) {
36
+ return {
37
+ status: 'error',
38
+ generated: false,
39
+ error: execution.error.message
40
+ };
41
+ }
42
+
43
+ if (execution.status !== 0) {
44
+ return {
45
+ status: 'error',
46
+ generated: false,
47
+ error: stderr || stdout || `baseline script exited with code ${execution.status}`
48
+ };
49
+ }
50
+
51
+ let reportPayload = null;
52
+ try {
53
+ reportPayload = stdout ? JSON.parse(stdout) : await fs.readJson(outputJsonPath);
54
+ } catch (error) {
55
+ return {
56
+ status: 'error',
57
+ generated: false,
58
+ error: `failed to parse baseline payload: ${error.message}`
59
+ };
60
+ }
61
+
62
+ const summary = reportPayload && reportPayload.summary && typeof reportPayload.summary === 'object'
63
+ ? reportPayload.summary
64
+ : {};
65
+ const compare = reportPayload && reportPayload.compare && typeof reportPayload.compare === 'object'
66
+ ? reportPayload.compare
67
+ : null;
68
+ const failedTemplates = compare && compare.failed_templates && typeof compare.failed_templates === 'object'
69
+ ? compare.failed_templates
70
+ : {};
71
+ const scopeBreakdown = summary && summary.scope_breakdown && typeof summary.scope_breakdown === 'object'
72
+ ? summary.scope_breakdown
73
+ : {};
74
+ const coverageMatrix = summary && summary.coverage_matrix && typeof summary.coverage_matrix === 'object'
75
+ ? summary.coverage_matrix
76
+ : {};
77
+ const gapFrequency = summary && Array.isArray(summary.gap_frequency)
78
+ ? summary.gap_frequency
79
+ : [];
80
+
81
+ return {
82
+ status: summary.portfolio_passed === true ? 'passed' : 'failed',
83
+ generated: true,
84
+ summary: {
85
+ total_templates: Number(summary.total_templates) || 0,
86
+ scoped_templates: Number(summary.scoped_templates) || 0,
87
+ avg_score: Number.isFinite(Number(summary.avg_score)) ? Number(summary.avg_score) : null,
88
+ valid_rate_percent: Number.isFinite(Number(summary.valid_rate_percent)) ? Number(summary.valid_rate_percent) : null,
89
+ baseline_passed: Number(summary.baseline_passed) || 0,
90
+ baseline_failed: Number(summary.baseline_failed) || 0,
91
+ portfolio_passed: summary.portfolio_passed === true,
92
+ scope_breakdown: {
93
+ moqui_erp: Number(scopeBreakdown.moqui_erp) || 0,
94
+ scene_orchestration: Number(scopeBreakdown.scene_orchestration) || 0,
95
+ other: Number(scopeBreakdown.other) || 0
96
+ },
97
+ coverage_matrix: coverageMatrix,
98
+ gap_frequency: gapFrequency
99
+ },
100
+ compare: compare
101
+ ? {
102
+ previous_generated_at: compare.previous_generated_at || null,
103
+ previous_template_root: compare.previous_template_root || null,
104
+ deltas: compare.deltas || null,
105
+ coverage_matrix_deltas: compare.coverage_matrix_deltas || null,
106
+ coverage_matrix_regressions: buildAutoHandoffMoquiCoverageRegressions(compare),
107
+ failed_templates: {
108
+ previous: Array.isArray(failedTemplates.previous) ? failedTemplates.previous : [],
109
+ current: Array.isArray(failedTemplates.current) ? failedTemplates.current : [],
110
+ newly_failed: Array.isArray(failedTemplates.newly_failed) ? failedTemplates.newly_failed : [],
111
+ recovered: Array.isArray(failedTemplates.recovered) ? failedTemplates.recovered : []
112
+ }
113
+ }
114
+ : null,
115
+ output: {
116
+ json: toAutoHandoffCliPath(projectPath, outputJsonPath),
117
+ markdown: toAutoHandoffCliPath(projectPath, outputMarkdownPath)
118
+ },
119
+ warnings: stderr ? [stderr] : []
120
+ };
121
+ }
122
+
123
+ async function buildAutoHandoffScenePackageBatchSnapshot(projectPath, manifestPath, dependencies = {}) {
124
+ const { fs, path, spawnSync, normalizeHandoffText, toAutoHandoffCliPath, parseAutoHandoffJsonFromCommandStdout, AUTO_HANDOFF_CLI_SCRIPT_FILE, AUTO_HANDOFF_SCENE_PACKAGE_BATCH_JSON_FILE, AUTO_HANDOFF_SCENE_PACKAGE_BATCH_TASK_QUEUE_FILE } = dependencies;
125
+ const manifestFile = normalizeHandoffText(manifestPath);
126
+ if (!manifestFile) {
127
+ return {
128
+ status: 'skipped',
129
+ generated: false,
130
+ reason: 'manifest path unavailable for scene package batch gate'
131
+ };
132
+ }
133
+ if (!(await fs.pathExists(AUTO_HANDOFF_CLI_SCRIPT_FILE))) {
134
+ return {
135
+ status: 'skipped',
136
+ generated: false,
137
+ reason: `sce cli script missing: ${toAutoHandoffCliPath(projectPath, AUTO_HANDOFF_CLI_SCRIPT_FILE)}`
138
+ };
139
+ }
140
+
141
+ const outputJsonPath = path.join(projectPath, AUTO_HANDOFF_SCENE_PACKAGE_BATCH_JSON_FILE);
142
+ const taskQueuePath = path.join(projectPath, AUTO_HANDOFF_SCENE_PACKAGE_BATCH_TASK_QUEUE_FILE);
143
+ await fs.ensureDir(path.dirname(outputJsonPath));
144
+
145
+ const execution = spawnSync(
146
+ process.execPath,
147
+ [
148
+ AUTO_HANDOFF_CLI_SCRIPT_FILE,
149
+ 'scene',
150
+ 'package-publish-batch',
151
+ '--manifest', manifestFile,
152
+ '--dry-run',
153
+ '--ontology-report-out', outputJsonPath,
154
+ '--ontology-task-queue-out', taskQueuePath,
155
+ '--json'
156
+ ],
157
+ {
158
+ cwd: projectPath,
159
+ encoding: 'utf8'
160
+ }
161
+ );
162
+
163
+ const stdout = typeof execution.stdout === 'string' ? execution.stdout.trim() : '';
164
+ const stderr = typeof execution.stderr === 'string' ? execution.stderr.trim() : '';
165
+
166
+ if (execution.error) {
167
+ return {
168
+ status: 'error',
169
+ generated: false,
170
+ error: execution.error.message
171
+ };
172
+ }
173
+
174
+ const payload = parseAutoHandoffJsonFromCommandStdout(stdout);
175
+ if (!payload || typeof payload !== 'object') {
176
+ const missingSpecArray = /manifest spec array (not found|is empty)/i.test(stderr);
177
+ if (missingSpecArray) {
178
+ return {
179
+ status: 'skipped',
180
+ generated: false,
181
+ reason: 'manifest specs are not scene package batch compatible',
182
+ warnings: stderr ? [stderr] : []
183
+ };
184
+ }
185
+ return {
186
+ status: 'error',
187
+ generated: false,
188
+ error: stderr || stdout || `scene package publish-batch exited with code ${execution.status}`,
189
+ warnings: stderr ? [stderr] : []
190
+ };
191
+ }
192
+
193
+ const summary = payload.summary && typeof payload.summary === 'object'
194
+ ? payload.summary
195
+ : {};
196
+ const ontologySummary = payload.ontology_summary && typeof payload.ontology_summary === 'object'
197
+ ? payload.ontology_summary
198
+ : {};
199
+ const batchGate = payload.batch_ontology_gate && typeof payload.batch_ontology_gate === 'object'
200
+ ? payload.batch_ontology_gate
201
+ : {};
202
+ const batchGateFailures = Array.isArray(batchGate.failures) ? batchGate.failures : [];
203
+ const selected = Number(summary.selected) || 0;
204
+ const failed = Number(summary.failed) || 0;
205
+
206
+ if (selected <= 0 && failed <= 0) {
207
+ return {
208
+ status: 'skipped',
209
+ generated: false,
210
+ reason: 'no scene package publish candidates were selected from handoff manifest',
211
+ summary: {
212
+ selected,
213
+ published: Number(summary.published) || 0,
214
+ planned: Number(summary.planned) || 0,
215
+ failed,
216
+ skipped: Number(summary.skipped) || 0,
217
+ batch_gate_passed: batchGate.passed === true,
218
+ batch_gate_failure_count: batchGateFailures.length
219
+ },
220
+ output: {
221
+ json: toAutoHandoffCliPath(projectPath, outputJsonPath)
222
+ },
223
+ warnings: stderr ? [stderr] : []
224
+ };
225
+ }
226
+
227
+ return {
228
+ status: payload.success === true ? 'passed' : 'failed',
229
+ generated: true,
230
+ mode: payload.mode || 'dry-run',
231
+ success: payload.success === true,
232
+ manifest: normalizeHandoffText(payload.manifest),
233
+ summary: {
234
+ selected,
235
+ published: Number(summary.published) || 0,
236
+ planned: Number(summary.planned) || 0,
237
+ failed,
238
+ skipped: Number(summary.skipped) || 0,
239
+ batch_gate_passed: batchGate.passed === true,
240
+ batch_gate_failure_count: batchGateFailures.length,
241
+ ontology_average_score: Number.isFinite(Number(ontologySummary.average_score))
242
+ ? Number(ontologySummary.average_score)
243
+ : null,
244
+ ontology_valid_rate_percent: Number.isFinite(Number(ontologySummary.valid_rate_percent))
245
+ ? Number(ontologySummary.valid_rate_percent)
246
+ : null
247
+ },
248
+ failures: Array.isArray(payload.failures)
249
+ ? payload.failures.map(item => ({
250
+ spec: normalizeHandoffText(item && item.spec),
251
+ error: normalizeHandoffText(item && item.error)
252
+ }))
253
+ : [],
254
+ batch_ontology_gate: {
255
+ passed: batchGate.passed === true,
256
+ failures: batchGateFailures.map(item => ({
257
+ id: normalizeHandoffText(item && item.id),
258
+ message: normalizeHandoffText(item && item.message)
259
+ }))
260
+ },
261
+ task_queue: payload.ontology_task_queue && typeof payload.ontology_task_queue === 'object'
262
+ ? {
263
+ output_path: normalizeHandoffText(payload.ontology_task_queue.output_path),
264
+ task_count: Number(payload.ontology_task_queue.task_count) || 0
265
+ }
266
+ : null,
267
+ output: {
268
+ json: toAutoHandoffCliPath(projectPath, outputJsonPath)
269
+ },
270
+ warnings: stderr ? [stderr] : []
271
+ };
272
+ }
273
+
274
+ async function buildAutoHandoffCapabilityCoverageSnapshot(projectPath, handoff = null, policy = {}, dependencies = {}) {
275
+ const { fs, path, normalizeHandoffText, toAutoHandoffCliPath, resolveMoquiCapabilityDescriptor, MOQUI_CAPABILITY_LEXICON_INDEX, moquiCapabilityMatch, AUTO_HANDOFF_MOQUI_CAPABILITY_COVERAGE_JSON_FILE, AUTO_HANDOFF_MOQUI_CAPABILITY_COVERAGE_MARKDOWN_FILE, renderMoquiCapabilityCoverageMarkdown } = dependencies;
276
+ const loadLatestMoquiCapabilityCoverageReport = dependencies.loadLatestMoquiCapabilityCoverageReport || (async () => null);
277
+ const buildCapabilityCoverageComparison = dependencies.buildCapabilityCoverageComparison || (() => null);
278
+ const expectedRaw = Array.isArray(handoff && handoff.capabilities)
279
+ ? handoff.capabilities
280
+ : [];
281
+ const normalization = {
282
+ lexicon_version: MOQUI_CAPABILITY_LEXICON_INDEX && MOQUI_CAPABILITY_LEXICON_INDEX.version
283
+ ? MOQUI_CAPABILITY_LEXICON_INDEX.version
284
+ : null,
285
+ expected_alias_mapped: [],
286
+ expected_deprecated_aliases: [],
287
+ expected_unknown: [],
288
+ provided_alias_mapped: [],
289
+ provided_deprecated_aliases: [],
290
+ provided_unknown: []
291
+ };
292
+ const warnings = [];
293
+ const minRequiredPercentPolicy = Number(policy.min_capability_coverage_percent);
294
+ const minRequiredPercentValue = Number.isFinite(minRequiredPercentPolicy)
295
+ ? Number(minRequiredPercentPolicy.toFixed(2))
296
+ : 100;
297
+ const minSemanticRequiredPolicy = Number(policy.min_capability_semantic_percent);
298
+ const minSemanticRequiredValue = Number.isFinite(minSemanticRequiredPolicy)
299
+ ? Number(minSemanticRequiredPolicy.toFixed(2))
300
+ : 100;
301
+ const addNormalizationRecord = (target, descriptor) => {
302
+ const list = Array.isArray(normalization[target]) ? normalization[target] : [];
303
+ const item = {
304
+ raw: descriptor.raw,
305
+ normalized: descriptor.normalized,
306
+ canonical: descriptor.canonical
307
+ };
308
+ const key = `${item.raw}|${item.normalized}|${item.canonical}`;
309
+ if (!list.some(existing => `${existing.raw}|${existing.normalized}|${existing.canonical}` === key)) {
310
+ list.push(item);
311
+ }
312
+ normalization[target] = list;
313
+ };
314
+ const expectedMap = new Map();
315
+ for (const rawCapability of expectedRaw) {
316
+ const descriptor = resolveMoquiCapabilityDescriptor(rawCapability, MOQUI_CAPABILITY_LEXICON_INDEX);
317
+ if (!descriptor) {
318
+ continue;
319
+ }
320
+ if (descriptor.is_alias) {
321
+ addNormalizationRecord('expected_alias_mapped', descriptor);
322
+ }
323
+ if (descriptor.is_deprecated_alias) {
324
+ addNormalizationRecord('expected_deprecated_aliases', descriptor);
325
+ warnings.push(
326
+ `manifest capability "${descriptor.raw}" is deprecated; use "${descriptor.deprecated_replacement || descriptor.canonical}" instead`
327
+ );
328
+ }
329
+ if (!descriptor.is_known) {
330
+ addNormalizationRecord('expected_unknown', descriptor);
331
+ warnings.push(`manifest capability "${descriptor.raw}" is unknown to Moqui lexicon`);
332
+ }
333
+ if (!expectedMap.has(descriptor.canonical)) {
334
+ expectedMap.set(descriptor.canonical, {
335
+ capability: descriptor.canonical,
336
+ source_values: [descriptor.normalized]
337
+ });
338
+ } else {
339
+ const existing = expectedMap.get(descriptor.canonical);
340
+ if (!existing.source_values.includes(descriptor.normalized)) {
341
+ existing.source_values.push(descriptor.normalized);
342
+ }
343
+ }
344
+ }
345
+ const expected = Array.from(expectedMap.keys());
346
+ if (expected.length === 0) {
347
+ return {
348
+ status: 'skipped',
349
+ generated: false,
350
+ reason: 'manifest capabilities not declared',
351
+ summary: {
352
+ total_capabilities: 0,
353
+ covered_capabilities: 0,
354
+ uncovered_capabilities: 0,
355
+ coverage_percent: null,
356
+ min_required_percent: minRequiredPercentValue,
357
+ semantic_complete_capabilities: 0,
358
+ semantic_incomplete_capabilities: 0,
359
+ semantic_complete_percent: null,
360
+ min_semantic_required_percent: minSemanticRequiredValue,
361
+ semantic_passed: true,
362
+ passed: true
363
+ },
364
+ coverage: [],
365
+ gaps: [],
366
+ normalization,
367
+ warnings
368
+ };
369
+ }
370
+
371
+ const templateRoot = path.join(projectPath, '.sce', 'templates', 'scene-packages');
372
+ if (!(await fs.pathExists(templateRoot))) {
373
+ return {
374
+ status: 'skipped',
375
+ generated: false,
376
+ reason: `template library not found: ${toAutoHandoffCliPath(projectPath, templateRoot)}`,
377
+ summary: {
378
+ total_capabilities: expected.length,
379
+ covered_capabilities: 0,
380
+ uncovered_capabilities: expected.length,
381
+ coverage_percent: 0,
382
+ min_required_percent: minRequiredPercentValue,
383
+ semantic_complete_capabilities: 0,
384
+ semantic_incomplete_capabilities: expected.length,
385
+ semantic_complete_percent: 0,
386
+ min_semantic_required_percent: minSemanticRequiredValue,
387
+ semantic_passed: false,
388
+ passed: false
389
+ },
390
+ coverage: expected.map(item => ({
391
+ capability: item,
392
+ covered: false,
393
+ matched_templates: [],
394
+ matched_provides: [],
395
+ matched_template_semantics: [],
396
+ semantic_complete: false,
397
+ semantic_missing_dimensions: [
398
+ 'ontology.entities',
399
+ 'ontology.relations',
400
+ 'governance.business_rules',
401
+ 'governance.decision_logic'
402
+ ],
403
+ source_values: expectedMap.get(item).source_values
404
+ })),
405
+ gaps: expected,
406
+ normalization,
407
+ warnings
408
+ };
409
+ }
410
+
411
+ const templateEntries = await fs.readdir(templateRoot);
412
+ const templates = [];
413
+ for (const entry of templateEntries) {
414
+ const templateDir = path.join(templateRoot, entry);
415
+ let stat = null;
416
+ try {
417
+ stat = await fs.stat(templateDir);
418
+ } catch (_error) {
419
+ stat = null;
420
+ }
421
+ if (!stat || !stat.isDirectory()) {
422
+ continue;
423
+ }
424
+ const contractFile = path.join(templateDir, 'scene-package.json');
425
+ if (!(await fs.pathExists(contractFile))) {
426
+ continue;
427
+ }
428
+ try {
429
+ const payload = await fs.readJson(contractFile);
430
+ const providesRaw = [];
431
+ const contractProvides = payload && payload.contract && payload.contract.capabilities && payload.contract.capabilities.provides;
432
+ const rootProvides = payload && payload.capabilities && payload.capabilities.provides;
433
+ if (Array.isArray(contractProvides)) {
434
+ providesRaw.push(...contractProvides);
435
+ }
436
+ if (Array.isArray(rootProvides)) {
437
+ providesRaw.push(...rootProvides);
438
+ }
439
+ const provides = [];
440
+ for (const providedCapability of providesRaw) {
441
+ const descriptor = resolveMoquiCapabilityDescriptor(providedCapability, MOQUI_CAPABILITY_LEXICON_INDEX);
442
+ if (!descriptor) {
443
+ continue;
444
+ }
445
+ if (descriptor.is_alias) {
446
+ addNormalizationRecord('provided_alias_mapped', descriptor);
447
+ }
448
+ if (descriptor.is_deprecated_alias) {
449
+ addNormalizationRecord('provided_deprecated_aliases', descriptor);
450
+ warnings.push(
451
+ `template "${entry}" uses deprecated capability "${descriptor.raw}" (canonical "${descriptor.deprecated_replacement || descriptor.canonical}")`
452
+ );
453
+ }
454
+ if (!descriptor.is_known) {
455
+ addNormalizationRecord('provided_unknown', descriptor);
456
+ }
457
+ provides.push(descriptor.canonical);
458
+ }
459
+ const governanceContract = payload && payload.governance_contract && typeof payload.governance_contract === 'object'
460
+ ? payload.governance_contract
461
+ : {};
462
+ const ontologyModel = payload && payload.ontology_model && typeof payload.ontology_model === 'object'
463
+ ? payload.ontology_model
464
+ : {};
465
+ const businessRules = Array.isArray(governanceContract.business_rules)
466
+ ? governanceContract.business_rules
467
+ : [];
468
+ const decisionLogic = Array.isArray(governanceContract.decision_logic)
469
+ ? governanceContract.decision_logic
470
+ : [];
471
+ const ontologyEntities = Array.isArray(ontologyModel.entities)
472
+ ? ontologyModel.entities
473
+ : [];
474
+ const ontologyRelations = Array.isArray(ontologyModel.relations)
475
+ ? ontologyModel.relations
476
+ : [];
477
+ const semanticMissingDimensions = [];
478
+ if (ontologyEntities.length <= 0) {
479
+ semanticMissingDimensions.push('ontology.entities');
480
+ }
481
+ if (ontologyRelations.length <= 0) {
482
+ semanticMissingDimensions.push('ontology.relations');
483
+ }
484
+ if (businessRules.length <= 0) {
485
+ semanticMissingDimensions.push('governance.business_rules');
486
+ }
487
+ if (decisionLogic.length <= 0) {
488
+ semanticMissingDimensions.push('governance.decision_logic');
489
+ }
490
+ const uniqueProvides = Array.from(new Set(provides));
491
+ if (uniqueProvides.length > 0 && semanticMissingDimensions.length > 0) {
492
+ warnings.push(
493
+ `template "${entry}" semantic coverage missing: ${semanticMissingDimensions.join(', ')}`
494
+ );
495
+ }
496
+ templates.push({
497
+ template_id: entry,
498
+ provides: uniqueProvides,
499
+ semantic: {
500
+ ontology_entities_count: ontologyEntities.length,
501
+ ontology_relations_count: ontologyRelations.length,
502
+ business_rules_count: businessRules.length,
503
+ decision_logic_count: decisionLogic.length,
504
+ missing_dimensions: semanticMissingDimensions,
505
+ complete: semanticMissingDimensions.length === 0
506
+ }
507
+ });
508
+ } catch (_error) {
509
+ // Ignore malformed template package entries.
510
+ }
511
+ }
512
+
513
+ const coverage = expected.map(capability => {
514
+ const matchedTemplates = [];
515
+ const matchedProvides = [];
516
+ const matchedTemplateSemantics = [];
517
+ let hasOntologyEntities = false;
518
+ let hasOntologyRelations = false;
519
+ let hasBusinessRules = false;
520
+ let hasDecisionLogic = false;
521
+ for (const template of templates) {
522
+ const providedMatched = template.provides.filter(item => moquiCapabilityMatch(capability, item));
523
+ if (providedMatched.length > 0) {
524
+ matchedTemplates.push(template.template_id);
525
+ matchedProvides.push(...providedMatched);
526
+ const semantic = template.semantic && typeof template.semantic === 'object'
527
+ ? template.semantic
528
+ : {};
529
+ const templateSemantic = {
530
+ template_id: template.template_id,
531
+ ontology_entities_count: Number(semantic.ontology_entities_count) || 0,
532
+ ontology_relations_count: Number(semantic.ontology_relations_count) || 0,
533
+ business_rules_count: Number(semantic.business_rules_count) || 0,
534
+ decision_logic_count: Number(semantic.decision_logic_count) || 0,
535
+ missing_dimensions: Array.isArray(semantic.missing_dimensions) ? semantic.missing_dimensions : [],
536
+ complete: semantic.complete === true
537
+ };
538
+ matchedTemplateSemantics.push(templateSemantic);
539
+ hasOntologyEntities = hasOntologyEntities || templateSemantic.ontology_entities_count > 0;
540
+ hasOntologyRelations = hasOntologyRelations || templateSemantic.ontology_relations_count > 0;
541
+ hasBusinessRules = hasBusinessRules || templateSemantic.business_rules_count > 0;
542
+ hasDecisionLogic = hasDecisionLogic || templateSemantic.decision_logic_count > 0;
543
+ }
544
+ }
545
+ const semanticMissingDimensions = [];
546
+ if (!hasOntologyEntities) {
547
+ semanticMissingDimensions.push('ontology.entities');
548
+ }
549
+ if (!hasOntologyRelations) {
550
+ semanticMissingDimensions.push('ontology.relations');
551
+ }
552
+ if (!hasBusinessRules) {
553
+ semanticMissingDimensions.push('governance.business_rules');
554
+ }
555
+ if (!hasDecisionLogic) {
556
+ semanticMissingDimensions.push('governance.decision_logic');
557
+ }
558
+ const uniqueProvides = Array.from(new Set(matchedProvides)).sort();
559
+ return {
560
+ capability,
561
+ covered: matchedTemplates.length > 0,
562
+ source_values: expectedMap.has(capability) ? expectedMap.get(capability).source_values : [],
563
+ matched_templates: Array.from(new Set(matchedTemplates)).sort(),
564
+ matched_provides: uniqueProvides,
565
+ matched_template_semantics: matchedTemplateSemantics,
566
+ semantic_complete: semanticMissingDimensions.length === 0,
567
+ semantic_missing_dimensions: semanticMissingDimensions
568
+ };
569
+ });
570
+
571
+ const coveredCount = coverage.filter(item => item.covered).length;
572
+ const semanticCompleteCount = coverage.filter(item => item.semantic_complete).length;
573
+ const uncovered = coverage.filter(item => !item.covered).map(item => item.capability);
574
+ const coveragePercent = expected.length > 0
575
+ ? Number(((coveredCount / expected.length) * 100).toFixed(2))
576
+ : null;
577
+ const semanticCompletePercent = expected.length > 0
578
+ ? Number(((semanticCompleteCount / expected.length) * 100).toFixed(2))
579
+ : null;
580
+ const minRequiredPercent = minRequiredPercentValue;
581
+ const minSemanticRequiredPercent = minSemanticRequiredValue;
582
+ const passed = Number.isFinite(coveragePercent) && Number.isFinite(minRequiredPercent)
583
+ ? coveragePercent >= minRequiredPercent
584
+ : false;
585
+ const semanticPassed = Number.isFinite(semanticCompletePercent) && Number.isFinite(minSemanticRequiredPercent)
586
+ ? semanticCompletePercent >= minSemanticRequiredPercent
587
+ : false;
588
+
589
+ const payload = {
590
+ mode: 'moqui-capability-coverage',
591
+ generated_at: new Date().toISOString(),
592
+ expected_capabilities: expected,
593
+ summary: {
594
+ total_capabilities: expected.length,
595
+ covered_capabilities: coveredCount,
596
+ uncovered_capabilities: expected.length - coveredCount,
597
+ coverage_percent: coveragePercent,
598
+ min_required_percent: minRequiredPercent,
599
+ semantic_complete_capabilities: semanticCompleteCount,
600
+ semantic_incomplete_capabilities: expected.length - semanticCompleteCount,
601
+ semantic_complete_percent: semanticCompletePercent,
602
+ min_semantic_required_percent: minSemanticRequiredPercent,
603
+ semantic_passed: semanticPassed,
604
+ passed
605
+ },
606
+ coverage,
607
+ gaps: uncovered,
608
+ normalization,
609
+ warnings: Array.from(new Set(warnings))
610
+ };
611
+
612
+ const previousPayload = await loadLatestMoquiCapabilityCoverageReport(projectPath);
613
+ if (previousPayload) {
614
+ payload.compare = buildCapabilityCoverageComparison(payload, previousPayload);
615
+ }
616
+
617
+ const outputJsonPath = path.join(projectPath, AUTO_HANDOFF_MOQUI_CAPABILITY_COVERAGE_JSON_FILE);
618
+ const outputMarkdownPath = path.join(projectPath, AUTO_HANDOFF_MOQUI_CAPABILITY_COVERAGE_MARKDOWN_FILE);
619
+ await fs.ensureDir(path.dirname(outputJsonPath));
620
+ await fs.writeJson(outputJsonPath, payload, { spaces: 2 });
621
+ await fs.writeFile(outputMarkdownPath, renderMoquiCapabilityCoverageMarkdown(payload), 'utf8');
622
+
623
+ return {
624
+ status: 'evaluated',
625
+ generated: true,
626
+ summary: payload.summary,
627
+ coverage: payload.coverage,
628
+ gaps: payload.gaps,
629
+ normalization: payload.normalization,
630
+ warnings: payload.warnings,
631
+ compare: payload.compare || null,
632
+ output: {
633
+ json: toAutoHandoffCliPath(projectPath, outputJsonPath),
634
+ markdown: toAutoHandoffCliPath(projectPath, outputMarkdownPath)
635
+ }
636
+ };
637
+ }
638
+
639
+ module.exports = {
640
+ buildAutoHandoffMoquiBaselineSnapshot,
641
+ buildAutoHandoffScenePackageBatchSnapshot,
642
+ buildAutoHandoffCapabilityCoverageSnapshot
643
+ };
644
+
645
+