scene-capability-engine 3.6.28 → 3.6.32

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.
@@ -0,0 +1,3541 @@
1
+ function normalizeString(value) {
2
+ if (typeof value !== 'string') {
3
+ return '';
4
+ }
5
+ return value.trim();
6
+ }
7
+
8
+ function buildManifestSummary(sceneManifest) {
9
+ const metadata = sceneManifest.metadata || {};
10
+ const spec = sceneManifest.spec || {};
11
+ const capability = spec.capability_contract || {};
12
+ const governance = spec.governance_contract || {};
13
+ const approval = governance.approval || {};
14
+ const bindings = Array.isArray(capability.bindings) ? capability.bindings : [];
15
+ const sideEffectBindings = bindings.filter((binding) => binding && binding.side_effect === true).length;
16
+
17
+ return {
18
+ valid: true,
19
+ scene_ref: metadata.obj_id || null,
20
+ scene_version: metadata.obj_version || null,
21
+ title: metadata.title || null,
22
+ domain: spec.domain || 'erp',
23
+ risk_level: governance.risk_level || 'medium',
24
+ approval_required: approval.required === true,
25
+ binding_count: bindings.length,
26
+ side_effect_binding_count: sideEffectBindings
27
+ };
28
+ }
29
+
30
+ function normalizeBindingPluginReport(bindingPluginLoad) {
31
+ if (!bindingPluginLoad || typeof bindingPluginLoad !== 'object') {
32
+ return null;
33
+ }
34
+
35
+ return {
36
+ handlers_loaded: Number.isFinite(bindingPluginLoad.handlers_loaded) ? Number(bindingPluginLoad.handlers_loaded) : 0,
37
+ plugin_dirs: Array.isArray(bindingPluginLoad.plugin_dirs)
38
+ ? bindingPluginLoad.plugin_dirs.filter((item) => typeof item === 'string')
39
+ : [],
40
+ plugin_files: Array.isArray(bindingPluginLoad.plugin_files)
41
+ ? bindingPluginLoad.plugin_files.filter((item) => typeof item === 'string')
42
+ : [],
43
+ manifest_path: typeof bindingPluginLoad.manifest_path === 'string' && bindingPluginLoad.manifest_path.trim().length > 0
44
+ ? bindingPluginLoad.manifest_path
45
+ : null,
46
+ manifest_loaded: bindingPluginLoad.manifest_loaded === true,
47
+ warnings: Array.isArray(bindingPluginLoad.warnings)
48
+ ? bindingPluginLoad.warnings.map((warning) => String(warning))
49
+ : []
50
+ };
51
+ }
52
+
53
+ function buildDoctorSummary(sceneManifest, diagnostics) {
54
+ const manifestSummary = buildManifestSummary(sceneManifest);
55
+ const blockers = [];
56
+
57
+ if (diagnostics.planError) {
58
+ blockers.push(`plan validation failed: ${diagnostics.planError}`);
59
+ }
60
+
61
+ if (diagnostics.policy && diagnostics.policy.allowed === false) {
62
+ for (const reason of diagnostics.policy.reasons || []) {
63
+ blockers.push(`policy blocked: ${reason}`);
64
+ }
65
+ }
66
+
67
+ if (diagnostics.adapterReadiness && diagnostics.adapterReadiness.ready === false) {
68
+ if (diagnostics.adapterReadiness.error) {
69
+ blockers.push(`adapter readiness failed: ${diagnostics.adapterReadiness.error}`);
70
+ }
71
+
72
+ const failedChecks = (diagnostics.adapterReadiness.checks || [])
73
+ .filter((item) => item && item.passed === false)
74
+ .map((item) => item.name);
75
+
76
+ if (failedChecks.length > 0) {
77
+ blockers.push(`adapter checks failed: ${failedChecks.join(', ')}`);
78
+ }
79
+ }
80
+
81
+ return {
82
+ status: blockers.length === 0 ? 'healthy' : 'blocked',
83
+ trace_id: diagnostics.traceId || null,
84
+ scene_ref: manifestSummary.scene_ref,
85
+ scene_version: manifestSummary.scene_version,
86
+ domain: manifestSummary.domain,
87
+ risk_level: manifestSummary.risk_level,
88
+ mode: diagnostics.mode,
89
+ plan: {
90
+ valid: !diagnostics.planError,
91
+ node_count: diagnostics.plan ? diagnostics.plan.nodes.length : 0,
92
+ error: diagnostics.planError || null
93
+ },
94
+ policy: diagnostics.policy,
95
+ adapter_readiness: diagnostics.adapterReadiness || null,
96
+ binding_plugins: normalizeBindingPluginReport(diagnostics.bindingPlugins),
97
+ blockers
98
+ };
99
+ }
100
+
101
+
102
+ function createDoctorSuggestion(code, title, action, priority = 'medium') {
103
+ return { code, title, action, priority };
104
+ }
105
+
106
+ function dedupeDoctorSuggestions(suggestions) {
107
+ const seen = new Set();
108
+ return suggestions.filter((suggestion) => {
109
+ const key = `${suggestion.code}:${suggestion.action}`;
110
+ if (seen.has(key)) {
111
+ return false;
112
+ }
113
+ seen.add(key);
114
+ return true;
115
+ });
116
+ }
117
+
118
+ function buildDoctorSuggestions(report, sceneManifest) {
119
+ const suggestions = [];
120
+ const domain = ((sceneManifest.spec || {}).domain || report.domain || 'erp').toLowerCase();
121
+ const policyReasons = report.policy && Array.isArray(report.policy.reasons)
122
+ ? report.policy.reasons
123
+ : [];
124
+
125
+ if (report.plan && !report.plan.valid) {
126
+ suggestions.push(createDoctorSuggestion(
127
+ 'plan-invalid',
128
+ 'Fix scene bindings and idempotency fields',
129
+ 'Run `sce scene validate` and ensure side-effect bindings have idempotency key.',
130
+ 'high'
131
+ ));
132
+ }
133
+
134
+ for (const reason of policyReasons) {
135
+ const normalizedReason = String(reason || '').toLowerCase();
136
+
137
+ if (normalizedReason.includes('approval is required for commit')) {
138
+ suggestions.push(createDoctorSuggestion(
139
+ 'approval-required',
140
+ 'Collect approval before commit',
141
+ 'After approval workflow completes, rerun with `--approved`.',
142
+ 'high'
143
+ ));
144
+ continue;
145
+ }
146
+
147
+ if (normalizedReason.includes('high-risk commit requires approval')) {
148
+ suggestions.push(createDoctorSuggestion(
149
+ 'high-risk-approval',
150
+ 'Escalate high-risk approval gate',
151
+ 'Keep run mode in dry_run until explicit approval evidence is recorded.',
152
+ 'high'
153
+ ));
154
+ continue;
155
+ }
156
+
157
+ if (normalizedReason.includes('hybrid commit is disabled in runtime pilot')) {
158
+ suggestions.push(createDoctorSuggestion(
159
+ 'hybrid-commit-disabled',
160
+ 'Use hybrid dry_run in current pilot',
161
+ 'Run hybrid scene with `--mode dry_run` and collect readiness evidence only.',
162
+ 'high'
163
+ ));
164
+ continue;
165
+ }
166
+
167
+ if (normalizedReason.includes('robot safety preflight check failed')) {
168
+ suggestions.push(createDoctorSuggestion(
169
+ 'robot-preflight',
170
+ 'Repair robot preflight checks',
171
+ 'Verify robot adapter preflight pipeline and rerun with `--safety-preflight` when available.',
172
+ 'critical'
173
+ ));
174
+ continue;
175
+ }
176
+
177
+ if (normalizedReason.includes('robot stop channel is unavailable')) {
178
+ suggestions.push(createDoctorSuggestion(
179
+ 'robot-stop-channel',
180
+ 'Restore emergency stop channel',
181
+ 'Validate stop-channel connectivity before any robot or hybrid commit.',
182
+ 'critical'
183
+ ));
184
+ continue;
185
+ }
186
+
187
+ if (normalizedReason.includes('critical robot commit requires dual approval')) {
188
+ suggestions.push(createDoctorSuggestion(
189
+ 'dual-approval-required',
190
+ 'Collect dual approval for critical robot change',
191
+ 'Set dual-approval context only after two approvers sign off.',
192
+ 'critical'
193
+ ));
194
+ continue;
195
+ }
196
+
197
+ suggestions.push(createDoctorSuggestion(
198
+ 'policy-blocked',
199
+ 'Resolve policy blocker',
200
+ `Review policy reason: ${reason}`,
201
+ 'high'
202
+ ));
203
+ }
204
+
205
+ if (report.adapter_readiness && report.adapter_readiness.ready === false) {
206
+ const checks = Array.isArray(report.adapter_readiness.checks)
207
+ ? report.adapter_readiness.checks
208
+ : [];
209
+
210
+ for (const check of checks) {
211
+ if (check && check.passed === false) {
212
+ suggestions.push(createDoctorSuggestion(
213
+ 'adapter-readiness',
214
+ 'Fix adapter readiness checks',
215
+ `Repair adapter check "${check.name}" and rerun \`sce scene doctor --check-adapter\`.`,
216
+ domain === 'erp' ? 'medium' : 'high'
217
+ ));
218
+ }
219
+ }
220
+
221
+ if (report.adapter_readiness.error) {
222
+ suggestions.push(createDoctorSuggestion(
223
+ 'adapter-runtime-error',
224
+ 'Stabilize adapter readiness probe',
225
+ `Handle adapter probe error: ${report.adapter_readiness.error}`,
226
+ 'high'
227
+ ));
228
+ }
229
+ }
230
+
231
+ const pluginWarnings = report.binding_plugins && Array.isArray(report.binding_plugins.warnings)
232
+ ? report.binding_plugins.warnings
233
+ : [];
234
+
235
+ if (pluginWarnings.some((warning) => String(warning || '').toLowerCase().includes('manifest not found'))) {
236
+ suggestions.push(createDoctorSuggestion(
237
+ 'binding-plugin-manifest-missing',
238
+ 'Provide binding plugin manifest or disable manifest load',
239
+ 'Create manifest via `.sce/config/scene-binding-plugins.json` or rerun doctor with `--no-binding-plugin-manifest-load`.',
240
+ 'medium'
241
+ ));
242
+ }
243
+
244
+ if (pluginWarnings.some((warning) => {
245
+ const normalized = String(warning || '').toLowerCase();
246
+ return normalized.includes('failed to load binding plugin') || normalized.includes('invalid binding handler in plugin');
247
+ })) {
248
+ suggestions.push(createDoctorSuggestion(
249
+ 'binding-plugin-load-failed',
250
+ 'Repair failed binding plugin modules',
251
+ 'Inspect plugin warnings and fix plugin exports/handlers before commit execution.',
252
+ 'high'
253
+ ));
254
+ }
255
+
256
+ if (suggestions.length === 0) {
257
+ suggestions.push(createDoctorSuggestion(
258
+ 'ready-to-run',
259
+ 'Scene is healthy for next execution step',
260
+ report.mode === 'commit'
261
+ ? 'Proceed with `sce scene run --mode commit` under normal approval flow.'
262
+ : 'Proceed with `sce scene run --mode dry_run` to capture execution evidence.',
263
+ 'low'
264
+ ));
265
+ }
266
+
267
+ return dedupeDoctorSuggestions(suggestions);
268
+ }
269
+
270
+ function buildDoctorTodoMarkdown(report, suggestions) {
271
+ const lines = [
272
+ '# Scene Doctor Remediation Checklist',
273
+ '',
274
+ `- Scene: ${report.scene_ref}@${report.scene_version}`,
275
+ `- Domain: ${report.domain}`,
276
+ `- Mode: ${report.mode}`,
277
+ `- Status: ${report.status}`,
278
+ `- Generated At: ${new Date().toISOString()}`,
279
+ ''
280
+ ];
281
+
282
+ if (report.blockers.length > 0) {
283
+ lines.push('## Blockers');
284
+ for (const blocker of report.blockers) {
285
+ lines.push(`- ${blocker}`);
286
+ }
287
+ lines.push('');
288
+ }
289
+
290
+ lines.push('## Suggested Actions');
291
+ for (const suggestion of suggestions) {
292
+ lines.push(`- [ ] [${suggestion.priority}] ${suggestion.title}`);
293
+ lines.push(` - ${suggestion.action}`);
294
+ }
295
+ lines.push('');
296
+
297
+ return lines.join('\n');
298
+ }
299
+
300
+ async function writeDoctorTodo(options, report, projectRoot, fileSystem = fs) {
301
+ if (!options.todoOut) {
302
+ return null;
303
+ }
304
+
305
+ const todoPath = resolvePath(projectRoot, options.todoOut);
306
+ const markdown = buildDoctorTodoMarkdown(report, report.suggestions || []);
307
+
308
+ await fileSystem.ensureDir(path.dirname(todoPath));
309
+ await fileSystem.writeFile(todoPath, markdown, 'utf8');
310
+
311
+ return todoPath;
312
+ }
313
+
314
+ function buildDoctorTaskDraft(report, suggestions) {
315
+ const ordered = [...suggestions].sort((left, right) => {
316
+ const weights = { critical: 0, high: 1, medium: 2, low: 3 };
317
+ const leftWeight = Object.prototype.hasOwnProperty.call(weights, left.priority) ? weights[left.priority] : 99;
318
+ const rightWeight = Object.prototype.hasOwnProperty.call(weights, right.priority) ? weights[right.priority] : 99;
319
+
320
+ if (leftWeight !== rightWeight) {
321
+ return leftWeight - rightWeight;
322
+ }
323
+
324
+ return left.title.localeCompare(right.title);
325
+ });
326
+
327
+ const lines = [
328
+ '# Doctor Task Draft',
329
+ '',
330
+ `Scene: ${report.scene_ref}@${report.scene_version}`,
331
+ `Domain: ${report.domain}`,
332
+ `Mode: ${report.mode}`,
333
+ `Status: ${report.status}`,
334
+ `Trace: ${report.trace_id || 'n/a'}`,
335
+ '',
336
+ '## Suggested Tasks',
337
+ ''
338
+ ];
339
+
340
+ ordered.forEach((suggestion, index) => {
341
+ const taskId = index + 1;
342
+ const code = suggestion.code || 'unknown';
343
+ lines.push(`- [ ] ${taskId} [${suggestion.priority}] [${code}] ${suggestion.title}`);
344
+ lines.push(` - ${suggestion.action}`);
345
+ });
346
+
347
+ lines.push('');
348
+ return lines.join('\n');
349
+ }
350
+
351
+ async function writeDoctorTaskDraft(options, report, projectRoot, fileSystem = fs) {
352
+ if (!options.taskOut) {
353
+ return null;
354
+ }
355
+
356
+ const taskPath = resolvePath(projectRoot, options.taskOut);
357
+ const markdown = buildDoctorTaskDraft(report, report.suggestions || []);
358
+
359
+ await fileSystem.ensureDir(path.dirname(taskPath));
360
+ await fileSystem.writeFile(taskPath, markdown, 'utf8');
361
+
362
+ return taskPath;
363
+ }
364
+
365
+ function buildDoctorFeedbackTemplate(report) {
366
+ const lines = [
367
+ '# Doctor Execution Feedback Template',
368
+ '',
369
+ `Scene: ${report.scene_ref}@${report.scene_version}`,
370
+ `Domain: ${report.domain}`,
371
+ `Mode: ${report.mode}`,
372
+ `Status: ${report.status}`,
373
+ `Trace: ${report.trace_id || 'n/a'}`,
374
+ '',
375
+ '## Task Feedback Records',
376
+ ''
377
+ ];
378
+
379
+ const suggestions = Array.isArray(report.suggestions) ? report.suggestions : [];
380
+ const suggestionByCode = new Map();
381
+ for (const suggestion of suggestions) {
382
+ if (suggestion && suggestion.code && !suggestionByCode.has(suggestion.code)) {
383
+ suggestionByCode.set(suggestion.code, suggestion);
384
+ }
385
+ }
386
+
387
+ const taskSync = report.task_sync || null;
388
+ const addedTasks = taskSync && Array.isArray(taskSync.added_tasks) ? taskSync.added_tasks : [];
389
+
390
+ if (addedTasks.length === 0) {
391
+ lines.push('- No synced actionable tasks in this doctor run.');
392
+ lines.push('');
393
+ return lines.join('\n');
394
+ }
395
+
396
+ for (const task of addedTasks) {
397
+ const suggestionCode = task.suggestion_code || 'unknown';
398
+ const suggestion = suggestionByCode.get(suggestionCode) || null;
399
+
400
+ lines.push(`### Task ${task.task_id}: ${task.title}`);
401
+ lines.push(`- Priority: ${task.priority}`);
402
+ lines.push(`- Suggestion Code: ${suggestionCode}`);
403
+ lines.push(`- Trace ID: ${task.trace_id || report.trace_id || 'n/a'}`);
404
+ lines.push(`- Scene Ref: ${report.scene_ref}`);
405
+ if (suggestion && suggestion.action) {
406
+ lines.push(`- Planned Action: ${suggestion.action}`);
407
+ }
408
+ lines.push('');
409
+ lines.push('- [ ] Status: pending | in_progress | done | blocked');
410
+ lines.push('- [ ] Owner:');
411
+ lines.push('- [ ] Evidence Paths:');
412
+ lines.push('- [ ] Completion Notes:');
413
+ lines.push('- [ ] Eval Update:');
414
+ lines.push(' - cycle_time_ms:');
415
+ lines.push(' - policy_violation_count:');
416
+ lines.push(' - node_failure_count:');
417
+ lines.push(' - manual_takeover_rate:');
418
+ lines.push('');
419
+ }
420
+
421
+ return lines.join('\n');
422
+ }
423
+
424
+ async function writeDoctorFeedbackTemplate(options, report, projectRoot, fileSystem = fs) {
425
+ if (!options.feedbackOut) {
426
+ return null;
427
+ }
428
+
429
+ const feedbackPath = resolvePath(projectRoot, options.feedbackOut);
430
+ const markdown = buildDoctorFeedbackTemplate(report);
431
+
432
+ await fileSystem.ensureDir(path.dirname(feedbackPath));
433
+ await fileSystem.writeFile(feedbackPath, markdown, 'utf8');
434
+
435
+ return feedbackPath;
436
+ }
437
+
438
+ function parseSceneDescriptor(rawValue) {
439
+ const value = String(rawValue || '').trim();
440
+ const atIndex = value.lastIndexOf('@');
441
+
442
+ if (atIndex <= 0 || atIndex === value.length - 1) {
443
+ return {
444
+ scene_ref: value || null,
445
+ scene_version: null
446
+ };
447
+ }
448
+
449
+ return {
450
+ scene_ref: value.slice(0, atIndex).trim() || null,
451
+ scene_version: value.slice(atIndex + 1).trim() || null
452
+ };
453
+ }
454
+
455
+ function normalizeFeedbackStatus(rawStatus) {
456
+ const status = String(rawStatus || '').trim().toLowerCase();
457
+
458
+ if (!status || status.includes('|')) {
459
+ return null;
460
+ }
461
+
462
+ if (status.startsWith('done')) {
463
+ return 'done';
464
+ }
465
+
466
+ if (status.startsWith('in_progress') || status.startsWith('in-progress')) {
467
+ return 'in_progress';
468
+ }
469
+
470
+ if (status.startsWith('blocked')) {
471
+ return 'blocked';
472
+ }
473
+
474
+ if (status.startsWith('pending')) {
475
+ return 'pending';
476
+ }
477
+
478
+ return status;
479
+ }
480
+
481
+ function parseFeedbackNumber(rawValue) {
482
+ const match = String(rawValue || '').match(/-?\d+(?:\.\d+)?/);
483
+ if (!match) {
484
+ return null;
485
+ }
486
+
487
+ const value = Number.parseFloat(match[0]);
488
+ return Number.isFinite(value) ? value : null;
489
+ }
490
+
491
+ function parseDoctorFeedbackTemplate(markdown = '') {
492
+ const lines = String(markdown || '').split(/\r?\n/);
493
+ const feedback = {
494
+ scene_ref: null,
495
+ scene_version: null,
496
+ domain: null,
497
+ mode: null,
498
+ status: null,
499
+ trace_id: null,
500
+ tasks: []
501
+ };
502
+
503
+ let currentTask = null;
504
+
505
+ const pushTask = () => {
506
+ if (!currentTask) {
507
+ return;
508
+ }
509
+
510
+ feedback.tasks.push(currentTask);
511
+ currentTask = null;
512
+ };
513
+
514
+ for (const rawLine of lines) {
515
+ const line = String(rawLine || '').trim();
516
+
517
+ if (!line) {
518
+ continue;
519
+ }
520
+
521
+ if (line.startsWith('Scene:')) {
522
+ const parsed = parseSceneDescriptor(line.slice('Scene:'.length));
523
+ feedback.scene_ref = parsed.scene_ref;
524
+ feedback.scene_version = parsed.scene_version;
525
+ continue;
526
+ }
527
+
528
+ if (line.startsWith('Domain:')) {
529
+ feedback.domain = line.slice('Domain:'.length).trim() || null;
530
+ continue;
531
+ }
532
+
533
+ if (line.startsWith('Mode:')) {
534
+ feedback.mode = line.slice('Mode:'.length).trim() || null;
535
+ continue;
536
+ }
537
+
538
+ if (line.startsWith('Status:')) {
539
+ feedback.status = line.slice('Status:'.length).trim() || null;
540
+ continue;
541
+ }
542
+
543
+ if (line.startsWith('Trace:')) {
544
+ const traceId = line.slice('Trace:'.length).trim();
545
+ feedback.trace_id = traceId && traceId !== 'n/a' ? traceId : null;
546
+ continue;
547
+ }
548
+
549
+ const taskHeadingMatch = line.match(/^###\s+Task\s+(\d+)\s*:\s*(.+)$/i);
550
+ if (taskHeadingMatch) {
551
+ pushTask();
552
+
553
+ const taskId = Number.parseInt(taskHeadingMatch[1], 10);
554
+ currentTask = {
555
+ task_id: Number.isFinite(taskId) ? taskId : null,
556
+ title: taskHeadingMatch[2].trim(),
557
+ priority: null,
558
+ suggestion_code: null,
559
+ trace_id: feedback.trace_id,
560
+ scene_ref: feedback.scene_ref,
561
+ planned_action: null,
562
+ status: null,
563
+ owner: null,
564
+ evidence_paths: null,
565
+ completion_notes: null,
566
+ eval_update: {
567
+ cycle_time_ms: null,
568
+ policy_violation_count: null,
569
+ node_failure_count: null,
570
+ manual_takeover_rate: null
571
+ }
572
+ };
573
+ continue;
574
+ }
575
+
576
+ if (!currentTask) {
577
+ continue;
578
+ }
579
+
580
+ if (line.startsWith('- Priority:')) {
581
+ currentTask.priority = line.slice('- Priority:'.length).trim() || null;
582
+ continue;
583
+ }
584
+
585
+ if (line.startsWith('- Suggestion Code:')) {
586
+ currentTask.suggestion_code = line.slice('- Suggestion Code:'.length).trim() || null;
587
+ continue;
588
+ }
589
+
590
+ if (line.startsWith('- Trace ID:')) {
591
+ const traceId = line.slice('- Trace ID:'.length).trim();
592
+ currentTask.trace_id = traceId && traceId !== 'n/a' ? traceId : null;
593
+ continue;
594
+ }
595
+
596
+ if (line.startsWith('- Scene Ref:')) {
597
+ currentTask.scene_ref = line.slice('- Scene Ref:'.length).trim() || null;
598
+ continue;
599
+ }
600
+
601
+ if (line.startsWith('- Planned Action:')) {
602
+ currentTask.planned_action = line.slice('- Planned Action:'.length).trim() || null;
603
+ continue;
604
+ }
605
+
606
+ const normalizedChecklistLine = line.replace(/^-\s*\[[ xX~-]\]\s*/, '- ');
607
+
608
+ if (normalizedChecklistLine.startsWith('- Status:')) {
609
+ const statusValue = normalizedChecklistLine.slice('- Status:'.length).trim();
610
+ currentTask.status = normalizeFeedbackStatus(statusValue);
611
+ continue;
612
+ }
613
+
614
+ if (normalizedChecklistLine.startsWith('- Owner:')) {
615
+ const ownerValue = normalizedChecklistLine.slice('- Owner:'.length).trim();
616
+ currentTask.owner = ownerValue || null;
617
+ continue;
618
+ }
619
+
620
+ if (normalizedChecklistLine.startsWith('- Evidence Paths:')) {
621
+ const evidencePathsValue = normalizedChecklistLine.slice('- Evidence Paths:'.length).trim();
622
+ currentTask.evidence_paths = evidencePathsValue || null;
623
+ continue;
624
+ }
625
+
626
+ if (normalizedChecklistLine.startsWith('- Completion Notes:')) {
627
+ const completionNotesValue = normalizedChecklistLine.slice('- Completion Notes:'.length).trim();
628
+ currentTask.completion_notes = completionNotesValue || null;
629
+ continue;
630
+ }
631
+
632
+ const cycleMatch = line.match(/^[-*]\s*cycle_time_ms:\s*(.*)$/i);
633
+ if (cycleMatch) {
634
+ currentTask.eval_update.cycle_time_ms = parseFeedbackNumber(cycleMatch[1]);
635
+ continue;
636
+ }
637
+
638
+ const policyMatch = line.match(/^[-*]\s*policy_violation_count:\s*(.*)$/i);
639
+ if (policyMatch) {
640
+ currentTask.eval_update.policy_violation_count = parseFeedbackNumber(policyMatch[1]);
641
+ continue;
642
+ }
643
+
644
+ const nodeFailureMatch = line.match(/^[-*]\s*node_failure_count:\s*(.*)$/i);
645
+ if (nodeFailureMatch) {
646
+ currentTask.eval_update.node_failure_count = parseFeedbackNumber(nodeFailureMatch[1]);
647
+ continue;
648
+ }
649
+
650
+ const manualTakeoverMatch = line.match(/^[-*]\s*manual_takeover_rate:\s*(.*)$/i);
651
+ if (manualTakeoverMatch) {
652
+ currentTask.eval_update.manual_takeover_rate = parseFeedbackNumber(manualTakeoverMatch[1]);
653
+ }
654
+ }
655
+
656
+ pushTask();
657
+ return feedback;
658
+ }
659
+
660
+ function averageOrNull(values) {
661
+ const numericValues = values.filter((value) => typeof value === 'number' && Number.isFinite(value));
662
+ if (numericValues.length === 0) {
663
+ return null;
664
+ }
665
+
666
+ const sum = numericValues.reduce((acc, current) => acc + current, 0);
667
+ return Number((sum / numericValues.length).toFixed(3));
668
+ }
669
+
670
+ function buildFeedbackTaskSummary(tasks = []) {
671
+ const summary = {
672
+ total: tasks.length,
673
+ done: 0,
674
+ in_progress: 0,
675
+ pending: 0,
676
+ blocked: 0,
677
+ unknown: 0,
678
+ completion_rate: 0,
679
+ blocked_rate: 0,
680
+ evidence_coverage_rate: 0
681
+ };
682
+
683
+ if (tasks.length === 0) {
684
+ return summary;
685
+ }
686
+
687
+ let evidenceCount = 0;
688
+
689
+ for (const task of tasks) {
690
+ const status = task && task.status ? task.status : 'unknown';
691
+
692
+ if (Object.prototype.hasOwnProperty.call(summary, status)) {
693
+ summary[status] += 1;
694
+ } else {
695
+ summary.unknown += 1;
696
+ }
697
+
698
+ if (task && task.evidence_paths) {
699
+ evidenceCount += 1;
700
+ }
701
+ }
702
+
703
+ summary.completion_rate = Number((summary.done / tasks.length).toFixed(3));
704
+ summary.blocked_rate = Number((summary.blocked / tasks.length).toFixed(3));
705
+ summary.evidence_coverage_rate = Number((evidenceCount / tasks.length).toFixed(3));
706
+
707
+ return summary;
708
+ }
709
+
710
+ function buildFeedbackMetricSummary(tasks = []) {
711
+ const cycleTimes = [];
712
+ const policyViolations = [];
713
+ const nodeFailures = [];
714
+ const manualTakeoverRates = [];
715
+
716
+ for (const task of tasks) {
717
+ if (!task || !task.eval_update) {
718
+ continue;
719
+ }
720
+
721
+ cycleTimes.push(task.eval_update.cycle_time_ms);
722
+ policyViolations.push(task.eval_update.policy_violation_count);
723
+ nodeFailures.push(task.eval_update.node_failure_count);
724
+ manualTakeoverRates.push(task.eval_update.manual_takeover_rate);
725
+ }
726
+
727
+ return {
728
+ avg_cycle_time_ms: averageOrNull(cycleTimes),
729
+ avg_policy_violation_count: averageOrNull(policyViolations),
730
+ avg_node_failure_count: averageOrNull(nodeFailures),
731
+ avg_manual_takeover_rate: averageOrNull(manualTakeoverRates)
732
+ };
733
+ }
734
+
735
+ function evaluateFeedbackScore(taskSummary, metricSummary, target = {}) {
736
+ if (!taskSummary || taskSummary.total === 0) {
737
+ return {
738
+ score: null,
739
+ recommendations: ['No feedback tasks found. Sync doctor tasks and fill feedback template before evaluation.']
740
+ };
741
+ }
742
+
743
+ let score = 1;
744
+ const recommendations = [];
745
+
746
+ const minCompletionRate = typeof target.min_completion_rate === 'number' ? target.min_completion_rate : 0.8;
747
+ const maxBlockedRate = typeof target.max_blocked_rate === 'number' ? target.max_blocked_rate : 0;
748
+ const maxPolicyViolationCount = typeof target.max_policy_violation_count === 'number' ? target.max_policy_violation_count : 0;
749
+ const maxNodeFailureCount = typeof target.max_node_failure_count === 'number' ? target.max_node_failure_count : 0;
750
+ const maxManualTakeoverRate = typeof target.max_manual_takeover_rate === 'number' ? target.max_manual_takeover_rate : 0.2;
751
+ const maxCycleTimeMs = typeof target.max_cycle_time_ms === 'number' ? target.max_cycle_time_ms : null;
752
+
753
+ if (taskSummary.completion_rate < minCompletionRate) {
754
+ score -= 0.2;
755
+ recommendations.push(`Increase completion rate to at least ${minCompletionRate}.`);
756
+ }
757
+
758
+ if (taskSummary.blocked_rate > maxBlockedRate) {
759
+ score -= 0.2;
760
+ recommendations.push(`Reduce blocked task rate to ${maxBlockedRate} or lower.`);
761
+ }
762
+
763
+ if (
764
+ typeof metricSummary.avg_policy_violation_count === 'number'
765
+ && metricSummary.avg_policy_violation_count > maxPolicyViolationCount
766
+ ) {
767
+ score -= 0.2;
768
+ recommendations.push('Lower average policy_violation_count in feedback records.');
769
+ }
770
+
771
+ if (
772
+ typeof metricSummary.avg_node_failure_count === 'number'
773
+ && metricSummary.avg_node_failure_count > maxNodeFailureCount
774
+ ) {
775
+ score -= 0.2;
776
+ recommendations.push('Lower average node_failure_count in feedback records.');
777
+ }
778
+
779
+ if (
780
+ typeof metricSummary.avg_manual_takeover_rate === 'number'
781
+ && metricSummary.avg_manual_takeover_rate > maxManualTakeoverRate
782
+ ) {
783
+ score -= 0.1;
784
+ recommendations.push(`Reduce manual_takeover_rate to ${maxManualTakeoverRate} or lower.`);
785
+ }
786
+
787
+ if (
788
+ typeof maxCycleTimeMs === 'number'
789
+ && typeof metricSummary.avg_cycle_time_ms === 'number'
790
+ && metricSummary.avg_cycle_time_ms > maxCycleTimeMs
791
+ ) {
792
+ score -= 0.1;
793
+ recommendations.push(`Reduce cycle_time_ms to ${maxCycleTimeMs} or lower.`);
794
+ }
795
+
796
+ return {
797
+ score: Math.max(0, Number(score.toFixed(2))),
798
+ recommendations
799
+ };
800
+ }
801
+
802
+ function classifyEvalGrade(score) {
803
+ if (typeof score !== 'number' || !Number.isFinite(score)) {
804
+ return 'insufficient_data';
805
+ }
806
+
807
+ if (score >= 0.85) {
808
+ return 'good';
809
+ }
810
+
811
+ if (score >= 0.7) {
812
+ return 'watch';
813
+ }
814
+
815
+ if (score >= 0.5) {
816
+ return 'at_risk';
817
+ }
818
+
819
+ return 'critical';
820
+ }
821
+
822
+ function normalizeTaskPriority(priority, fallback = 'medium') {
823
+ const normalized = String(priority || '').trim().toLowerCase();
824
+ if (TASK_PRIORITIES.has(normalized)) {
825
+ return normalized;
826
+ }
827
+
828
+ return fallback;
829
+ }
830
+
831
+ function cloneDefaultEvalTaskSyncPolicy() {
832
+ return JSON.parse(JSON.stringify(DEFAULT_EVAL_TASK_SYNC_POLICY));
833
+ }
834
+
835
+ function cloneDefaultEvalProfileInferenceRules() {
836
+ return JSON.parse(JSON.stringify(DEFAULT_EVAL_PROFILE_INFERENCE_RULES));
837
+ }
838
+
839
+ function cloneDefaultSceneRoutePolicy() {
840
+ return JSON.parse(JSON.stringify(DEFAULT_SCENE_ROUTE_POLICY));
841
+ }
842
+
843
+ function isPlainObject(value) {
844
+ return value && typeof value === 'object' && !Array.isArray(value);
845
+ }
846
+
847
+ function mergePlainObject(base = {}, override = {}) {
848
+ const next = { ...(isPlainObject(base) ? base : {}) };
849
+
850
+ if (!isPlainObject(override)) {
851
+ return next;
852
+ }
853
+
854
+ for (const [key, value] of Object.entries(override)) {
855
+ if (isPlainObject(value) && isPlainObject(next[key])) {
856
+ next[key] = mergePlainObject(next[key], value);
857
+ continue;
858
+ }
859
+
860
+ next[key] = value;
861
+ }
862
+
863
+ return next;
864
+ }
865
+
866
+ function normalizeRoutePolicyNumber(value, fallback) {
867
+ if (typeof value === 'number' && Number.isFinite(value)) {
868
+ return value;
869
+ }
870
+
871
+ const parsed = Number(value);
872
+ if (Number.isFinite(parsed)) {
873
+ return parsed;
874
+ }
875
+
876
+ return fallback;
877
+ }
878
+
879
+ function normalizeSceneRoutePolicy(policy = {}) {
880
+ const merged = cloneDefaultSceneRoutePolicy();
881
+
882
+ if (!isPlainObject(policy)) {
883
+ return merged;
884
+ }
885
+
886
+ if (isPlainObject(policy.weights)) {
887
+ for (const [key, fallback] of Object.entries(merged.weights)) {
888
+ if (Object.prototype.hasOwnProperty.call(policy.weights, key)) {
889
+ merged.weights[key] = normalizeRoutePolicyNumber(policy.weights[key], fallback);
890
+ }
891
+ }
892
+ }
893
+
894
+ if (isPlainObject(policy.mode_bias) && isPlainObject(policy.mode_bias.commit)) {
895
+ const commitBias = policy.mode_bias.commit;
896
+ for (const [riskLevel, fallback] of Object.entries(merged.mode_bias.commit)) {
897
+ if (Object.prototype.hasOwnProperty.call(commitBias, riskLevel)) {
898
+ merged.mode_bias.commit[riskLevel] = normalizeRoutePolicyNumber(commitBias[riskLevel], fallback);
899
+ }
900
+ }
901
+ }
902
+
903
+ if (Object.prototype.hasOwnProperty.call(policy, 'max_alternatives')) {
904
+ const normalizedMaxAlternatives = Math.max(0, Math.trunc(normalizeRoutePolicyNumber(policy.max_alternatives, merged.max_alternatives)));
905
+ merged.max_alternatives = normalizedMaxAlternatives;
906
+ }
907
+
908
+ return merged;
909
+ }
910
+
911
+ function createSceneRoutePolicyTemplateByProfile(profile = 'default') {
912
+ const normalizedProfile = String(profile || '').trim().toLowerCase();
913
+ const base = cloneDefaultSceneRoutePolicy();
914
+
915
+ const profilePatches = {
916
+ erp: {
917
+ weights: {
918
+ query_token_match: 7
919
+ },
920
+ max_alternatives: 5
921
+ },
922
+ hybrid: {
923
+ weights: {
924
+ query_token_match: 10
925
+ },
926
+ mode_bias: {
927
+ commit: {
928
+ high: -8,
929
+ critical: -10
930
+ }
931
+ },
932
+ max_alternatives: 6
933
+ },
934
+ robot: {
935
+ weights: {
936
+ query_token_match: 10
937
+ },
938
+ mode_bias: {
939
+ commit: {
940
+ medium: -2,
941
+ high: -10,
942
+ critical: -12
943
+ }
944
+ },
945
+ max_alternatives: 6
946
+ }
947
+ };
948
+
949
+ if (!Object.prototype.hasOwnProperty.call(profilePatches, normalizedProfile)) {
950
+ return base;
951
+ }
952
+
953
+ return normalizeSceneRoutePolicy(mergePlainObject(base, profilePatches[normalizedProfile]));
954
+ }
955
+
956
+ async function loadSceneRoutePolicy(options = {}, projectRoot, fileSystem = fs) {
957
+ const defaultPolicy = cloneDefaultSceneRoutePolicy();
958
+
959
+ if (!options.routePolicy) {
960
+ return {
961
+ policy: defaultPolicy,
962
+ source: 'default'
963
+ };
964
+ }
965
+
966
+ const readJson = typeof fileSystem.readJson === 'function'
967
+ ? fileSystem.readJson.bind(fileSystem)
968
+ : fs.readJson.bind(fs);
969
+
970
+ const routePolicyPath = resolvePath(projectRoot, options.routePolicy);
971
+ const routePolicyRaw = await readJson(routePolicyPath);
972
+
973
+ if (!isPlainObject(routePolicyRaw)) {
974
+ throw new Error('route policy file must contain a JSON object');
975
+ }
976
+
977
+ return {
978
+ policy: normalizeSceneRoutePolicy(mergePlainObject(defaultPolicy, routePolicyRaw)),
979
+ source: options.routePolicy
980
+ };
981
+ }
982
+
983
+ function incrementCounter(counter, key) {
984
+ if (!counter || typeof counter !== 'object') {
985
+ return;
986
+ }
987
+
988
+ counter[key] = (counter[key] || 0) + 1;
989
+ }
990
+
991
+ function normalizeRoutePolicySuggestGrade(rawGrade) {
992
+ const normalized = String(rawGrade || '').trim().toLowerCase();
993
+ switch (normalized) {
994
+ case 'good':
995
+ case 'watch':
996
+ case 'at_risk':
997
+ case 'critical':
998
+ case 'insufficient_data':
999
+ return normalized;
1000
+ default:
1001
+ return 'unknown';
1002
+ }
1003
+ }
1004
+
1005
+ function normalizeRoutePolicySuggestRunStatus(rawStatus) {
1006
+ const normalized = String(rawStatus || '').trim().toLowerCase();
1007
+ switch (normalized) {
1008
+ case 'success':
1009
+ case 'denied':
1010
+ case 'failed':
1011
+ case 'blocked':
1012
+ return normalized;
1013
+ default:
1014
+ return 'unknown';
1015
+ }
1016
+ }
1017
+
1018
+ function normalizeRoutePolicySuggestProfileName(rawProfile) {
1019
+ const normalized = String(rawProfile || '').trim().toLowerCase();
1020
+
1021
+ if (ROUTE_POLICY_TEMPLATE_PROFILES.has(normalized)) {
1022
+ return normalized;
1023
+ }
1024
+
1025
+ if (normalized === 'ops') {
1026
+ return 'hybrid';
1027
+ }
1028
+
1029
+ return null;
1030
+ }
1031
+
1032
+ function inferRoutePolicySuggestProfile(report = {}) {
1033
+ const inputProfile = normalizeRoutePolicySuggestProfileName(report && report.inputs ? report.inputs.profile : null);
1034
+ if (inputProfile) {
1035
+ return inputProfile;
1036
+ }
1037
+
1038
+ const sceneRef = String(report.scene_ref || '').trim().toLowerCase();
1039
+ if (!sceneRef) {
1040
+ return 'default';
1041
+ }
1042
+
1043
+ const firstDomain = sceneRef.split(/[.]/).slice(1, 2)[0];
1044
+ const inferredFromSceneRef = normalizeRoutePolicySuggestProfileName(firstDomain);
1045
+ if (inferredFromSceneRef) {
1046
+ return inferredFromSceneRef;
1047
+ }
1048
+
1049
+ if (sceneRef.includes('.hybrid.') || sceneRef.includes('.robot.')) {
1050
+ return 'hybrid';
1051
+ }
1052
+
1053
+ if (sceneRef.includes('.erp.')) {
1054
+ return 'erp';
1055
+ }
1056
+
1057
+ return 'default';
1058
+ }
1059
+
1060
+ function resolveDominantRoutePolicySuggestProfile(profileCounts = {}) {
1061
+ const profileOrder = ['erp', 'hybrid', 'robot', 'default'];
1062
+ let selected = 'default';
1063
+ let selectedCount = 0;
1064
+
1065
+ for (const profile of profileOrder) {
1066
+ const count = Number(profileCounts[profile] || 0);
1067
+ if (count > selectedCount) {
1068
+ selected = profile;
1069
+ selectedCount = count;
1070
+ }
1071
+ }
1072
+
1073
+ return selected;
1074
+ }
1075
+
1076
+ function summarizeSceneRoutePolicySuggestReports(evalReports = []) {
1077
+ const gradeCounts = {
1078
+ good: 0,
1079
+ watch: 0,
1080
+ at_risk: 0,
1081
+ critical: 0,
1082
+ insufficient_data: 0,
1083
+ unknown: 0
1084
+ };
1085
+ const runStatusCounts = {
1086
+ success: 0,
1087
+ denied: 0,
1088
+ failed: 0,
1089
+ blocked: 0,
1090
+ unknown: 0
1091
+ };
1092
+ const profileCounts = {
1093
+ default: 0,
1094
+ erp: 0,
1095
+ hybrid: 0,
1096
+ robot: 0
1097
+ };
1098
+ const recommendationSignals = {
1099
+ policy_denial: 0,
1100
+ runtime_failure: 0,
1101
+ manual_takeover: 0
1102
+ };
1103
+
1104
+ for (const item of evalReports) {
1105
+ const report = item && item.report && typeof item.report === 'object'
1106
+ ? item.report
1107
+ : {};
1108
+
1109
+ const grade = normalizeRoutePolicySuggestGrade(report && report.overall ? report.overall.grade : null);
1110
+ incrementCounter(gradeCounts, grade);
1111
+
1112
+ const runStatus = normalizeRoutePolicySuggestRunStatus(report && report.run_evaluation ? report.run_evaluation.status : null);
1113
+ incrementCounter(runStatusCounts, runStatus);
1114
+
1115
+ const profile = inferRoutePolicySuggestProfile(report);
1116
+ incrementCounter(profileCounts, profile);
1117
+
1118
+ const recommendations = report && report.overall && Array.isArray(report.overall.recommendations)
1119
+ ? report.overall.recommendations
1120
+ : [];
1121
+
1122
+ for (const recommendation of recommendations) {
1123
+ const normalized = String(recommendation || '').toLowerCase();
1124
+ if (!normalized) {
1125
+ continue;
1126
+ }
1127
+
1128
+ if (/policy denial|denied/.test(normalized)) {
1129
+ recommendationSignals.policy_denial += 1;
1130
+ }
1131
+
1132
+ if (/failed runtime|node failure|compensation/.test(normalized)) {
1133
+ recommendationSignals.runtime_failure += 1;
1134
+ }
1135
+
1136
+ if (/manual takeover|manual_takeover/.test(normalized)) {
1137
+ recommendationSignals.manual_takeover += 1;
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ const totalReports = evalReports.length;
1143
+ const safeDivisor = totalReports > 0 ? totalReports : 1;
1144
+ const severeCount = gradeCounts.critical + gradeCounts.at_risk;
1145
+ const unstableCount = runStatusCounts.failed + runStatusCounts.denied;
1146
+
1147
+ return {
1148
+ total_reports: totalReports,
1149
+ grade_counts: gradeCounts,
1150
+ run_status_counts: runStatusCounts,
1151
+ profile_counts: profileCounts,
1152
+ dominant_profile: resolveDominantRoutePolicySuggestProfile(profileCounts),
1153
+ recommendation_signals: recommendationSignals,
1154
+ rates: {
1155
+ severe_rate: Number((severeCount / safeDivisor).toFixed(2)),
1156
+ unstable_rate: Number((unstableCount / safeDivisor).toFixed(2)),
1157
+ insufficient_rate: Number((gradeCounts.insufficient_data / safeDivisor).toFixed(2)),
1158
+ good_rate: Number((gradeCounts.good / safeDivisor).toFixed(2)),
1159
+ denied_rate: Number((runStatusCounts.denied / safeDivisor).toFixed(2)),
1160
+ failed_rate: Number((runStatusCounts.failed / safeDivisor).toFixed(2))
1161
+ }
1162
+ };
1163
+ }
1164
+
1165
+ function clampRoutePolicyValue(value, minimum, maximum) {
1166
+ let nextValue = value;
1167
+
1168
+ if (Number.isFinite(minimum)) {
1169
+ nextValue = Math.max(minimum, nextValue);
1170
+ }
1171
+
1172
+ if (Number.isFinite(maximum)) {
1173
+ nextValue = Math.min(maximum, nextValue);
1174
+ }
1175
+
1176
+ return nextValue;
1177
+ }
1178
+
1179
+ function getObjectValueByPath(target, pathKey) {
1180
+ if (!target || typeof target !== 'object' || typeof pathKey !== 'string') {
1181
+ return undefined;
1182
+ }
1183
+
1184
+ const parts = pathKey.split('.');
1185
+ let cursor = target;
1186
+
1187
+ for (const part of parts) {
1188
+ if (!cursor || typeof cursor !== 'object' || !Object.prototype.hasOwnProperty.call(cursor, part)) {
1189
+ return undefined;
1190
+ }
1191
+
1192
+ cursor = cursor[part];
1193
+ }
1194
+
1195
+ return cursor;
1196
+ }
1197
+
1198
+ function setObjectValueByPath(target, pathKey, value) {
1199
+ if (!target || typeof target !== 'object' || typeof pathKey !== 'string') {
1200
+ return;
1201
+ }
1202
+
1203
+ const parts = pathKey.split('.');
1204
+ let cursor = target;
1205
+
1206
+ for (let index = 0; index < parts.length - 1; index += 1) {
1207
+ const part = parts[index];
1208
+ if (!cursor[part] || typeof cursor[part] !== 'object') {
1209
+ cursor[part] = {};
1210
+ }
1211
+
1212
+ cursor = cursor[part];
1213
+ }
1214
+
1215
+ cursor[parts[parts.length - 1]] = value;
1216
+ }
1217
+
1218
+ function applyRoutePolicyDelta(policy, pathKey, delta, metadata = {}) {
1219
+ if (!Number.isFinite(delta) || delta === 0) {
1220
+ return null;
1221
+ }
1222
+
1223
+ const fallback = Object.prototype.hasOwnProperty.call(metadata, 'fallback') ? metadata.fallback : 0;
1224
+ const currentValue = normalizeRoutePolicyNumber(getObjectValueByPath(policy, pathKey), fallback);
1225
+ let nextValue = currentValue + delta;
1226
+
1227
+ if (metadata.integer === true) {
1228
+ nextValue = Math.trunc(nextValue);
1229
+ }
1230
+
1231
+ nextValue = clampRoutePolicyValue(nextValue, metadata.min, metadata.max);
1232
+
1233
+ if (nextValue === currentValue) {
1234
+ return null;
1235
+ }
1236
+
1237
+ setObjectValueByPath(policy, pathKey, nextValue);
1238
+
1239
+ return {
1240
+ path: pathKey,
1241
+ from: currentValue,
1242
+ to: nextValue,
1243
+ delta: Number((nextValue - currentValue).toFixed(2)),
1244
+ rationale: metadata.rationale || null
1245
+ };
1246
+ }
1247
+
1248
+ function buildSceneRoutePolicySuggestion(basePolicy, reportSummary, options = {}) {
1249
+ const suggestedPolicy = normalizeSceneRoutePolicy(basePolicy);
1250
+ const maxAdjustment = Math.max(0, Math.trunc(normalizeRoutePolicyNumber(
1251
+ options.maxAdjustment,
1252
+ ROUTE_POLICY_SUGGEST_MAX_ADJUSTMENT_DEFAULT
1253
+ )));
1254
+
1255
+ const summary = reportSummary && typeof reportSummary === 'object'
1256
+ ? reportSummary
1257
+ : summarizeSceneRoutePolicySuggestReports([]);
1258
+
1259
+ const rates = summary.rates || {};
1260
+ const recommendationSignals = summary.recommendation_signals || {};
1261
+ const totalReports = Number(summary.total_reports || 0);
1262
+
1263
+ const deltaByPath = new Map();
1264
+ const reasonsByPath = new Map();
1265
+
1266
+ const queueDelta = (pathKey, delta, reason) => {
1267
+ if (!Number.isFinite(delta) || delta === 0) {
1268
+ return;
1269
+ }
1270
+
1271
+ deltaByPath.set(pathKey, (deltaByPath.get(pathKey) || 0) + delta);
1272
+
1273
+ if (reason) {
1274
+ const reasons = reasonsByPath.get(pathKey) || [];
1275
+ reasons.push(reason);
1276
+ reasonsByPath.set(pathKey, reasons);
1277
+ }
1278
+ };
1279
+
1280
+ const stepFromRate = (rate, multiplier = 1) => {
1281
+ if (maxAdjustment <= 0) {
1282
+ return 0;
1283
+ }
1284
+
1285
+ const normalizedRate = Math.max(0, normalizeRoutePolicyNumber(rate, 0));
1286
+ if (normalizedRate <= 0) {
1287
+ return 0;
1288
+ }
1289
+
1290
+ return Math.max(1, Math.min(maxAdjustment, Math.ceil(normalizedRate * maxAdjustment * multiplier)));
1291
+ };
1292
+
1293
+ const severeRate = Math.max(0, normalizeRoutePolicyNumber(rates.severe_rate, 0));
1294
+ const unstableRate = Math.max(0, normalizeRoutePolicyNumber(rates.unstable_rate, 0));
1295
+ const insufficientRate = Math.max(0, normalizeRoutePolicyNumber(rates.insufficient_rate, 0));
1296
+ const goodRate = Math.max(0, normalizeRoutePolicyNumber(rates.good_rate, 0));
1297
+
1298
+ const stressRate = Math.max(severeRate, unstableRate);
1299
+ if (stressRate >= 0.2) {
1300
+ const stressStep = stepFromRate(stressRate, 1);
1301
+ if (stressStep > 0) {
1302
+ const rationale = `stress_rate=${stressRate}`;
1303
+ queueDelta('mode_bias.commit.high', -stressStep, rationale);
1304
+ queueDelta('mode_bias.commit.critical', -Math.min(maxAdjustment, stressStep + 1), rationale);
1305
+ queueDelta('weights.scene_ref_mismatch', -Math.max(1, Math.ceil(stressStep / 2)), rationale);
1306
+ queueDelta('weights.invalid_manifest', -Math.max(1, Math.ceil(stressStep / 2)), rationale);
1307
+ }
1308
+ }
1309
+
1310
+ if (insufficientRate >= 0.3) {
1311
+ const discoveryStep = stepFromRate(insufficientRate, 0.8);
1312
+ if (discoveryStep > 0) {
1313
+ const rationale = `insufficient_rate=${insufficientRate}`;
1314
+ queueDelta('weights.query_token_match', discoveryStep, rationale);
1315
+ queueDelta('max_alternatives', Math.max(1, Math.ceil(discoveryStep / 2)), rationale);
1316
+ }
1317
+ }
1318
+
1319
+ if (goodRate >= 0.65 && stressRate <= 0.2 && insufficientRate <= 0.25) {
1320
+ const precisionStep = stepFromRate(goodRate, 0.6);
1321
+ if (precisionStep > 0) {
1322
+ const rationale = `good_rate=${goodRate}`;
1323
+ queueDelta('weights.scene_ref_exact', precisionStep, rationale);
1324
+ queueDelta('weights.scene_ref_contains', Math.max(1, Math.ceil(precisionStep / 2)), rationale);
1325
+ queueDelta('max_alternatives', -1, rationale);
1326
+ }
1327
+ }
1328
+
1329
+ const policyDenialRate = totalReports > 0
1330
+ ? Number((recommendationSignals.policy_denial / totalReports).toFixed(2))
1331
+ : 0;
1332
+ if (policyDenialRate >= 0.15) {
1333
+ const denialStep = stepFromRate(policyDenialRate, 0.8);
1334
+ if (denialStep > 0) {
1335
+ queueDelta('mode_bias.commit.medium', -denialStep, `policy_denial_rate=${policyDenialRate}`);
1336
+ }
1337
+ }
1338
+
1339
+ const runtimeFailureSignalRate = totalReports > 0
1340
+ ? Number((recommendationSignals.runtime_failure / totalReports).toFixed(2))
1341
+ : 0;
1342
+ if (runtimeFailureSignalRate >= 0.15) {
1343
+ const failureStep = stepFromRate(runtimeFailureSignalRate, 0.7);
1344
+ if (failureStep > 0) {
1345
+ queueDelta('weights.scene_ref_mismatch', -Math.max(1, Math.ceil(failureStep / 2)), `runtime_failure_signal_rate=${runtimeFailureSignalRate}`);
1346
+ }
1347
+ }
1348
+
1349
+ const boundsByPath = {
1350
+ 'weights.valid_manifest': { min: -200, max: 200, fallback: 5 },
1351
+ 'weights.invalid_manifest': { min: -200, max: 200, fallback: -10 },
1352
+ 'weights.scene_ref_exact': { min: -200, max: 200, fallback: 100 },
1353
+ 'weights.scene_ref_contains': { min: -200, max: 200, fallback: 45 },
1354
+ 'weights.scene_ref_mismatch': { min: -200, max: 200, fallback: -20 },
1355
+ 'weights.query_token_match': { min: -200, max: 200, fallback: 8 },
1356
+ 'mode_bias.commit.low': { min: -50, max: 50, fallback: 2 },
1357
+ 'mode_bias.commit.medium': { min: -50, max: 50, fallback: 0 },
1358
+ 'mode_bias.commit.high': { min: -50, max: 50, fallback: -5 },
1359
+ 'mode_bias.commit.critical': { min: -50, max: 50, fallback: -5 },
1360
+ max_alternatives: { min: 0, max: 12, fallback: 4, integer: true }
1361
+ };
1362
+
1363
+ const adjustments = [];
1364
+ for (const [pathKey, delta] of deltaByPath.entries()) {
1365
+ const reasons = Array.from(new Set(reasonsByPath.get(pathKey) || []));
1366
+ const adjustment = applyRoutePolicyDelta(suggestedPolicy, pathKey, delta, {
1367
+ ...(boundsByPath[pathKey] || {}),
1368
+ rationale: reasons.join('; ') || null
1369
+ });
1370
+
1371
+ if (adjustment) {
1372
+ adjustments.push(adjustment);
1373
+ }
1374
+ }
1375
+
1376
+ return {
1377
+ max_adjustment: maxAdjustment,
1378
+ adjustments,
1379
+ suggested_policy: normalizeSceneRoutePolicy(suggestedPolicy)
1380
+ };
1381
+ }
1382
+
1383
+ function formatSceneRoutePolicySuggestSourcePath(projectRoot, absolutePath) {
1384
+ const normalizedRelative = normalizeRelativePath(path.relative(projectRoot, absolutePath));
1385
+ if (normalizedRelative && !normalizedRelative.startsWith('..')) {
1386
+ return normalizedRelative;
1387
+ }
1388
+
1389
+ return normalizeRelativePath(absolutePath);
1390
+ }
1391
+
1392
+ async function resolveSceneRoutePolicySuggestEvalPaths(options, projectRoot, fileSystem = fs) {
1393
+ const readdir = typeof fileSystem.readdir === 'function'
1394
+ ? fileSystem.readdir.bind(fileSystem)
1395
+ : fs.readdir.bind(fs);
1396
+
1397
+ const collected = [];
1398
+
1399
+ for (const evalPath of options.eval || []) {
1400
+ collected.push(resolvePath(projectRoot, evalPath));
1401
+ }
1402
+
1403
+ if (options.evalDir) {
1404
+ const evalDirPath = resolvePath(projectRoot, options.evalDir);
1405
+ let entries = [];
1406
+
1407
+ try {
1408
+ entries = await readdir(evalDirPath, { withFileTypes: true });
1409
+ } catch (error) {
1410
+ throw new Error(`failed to read eval directory: ${evalDirPath} (${error.message})`);
1411
+ }
1412
+
1413
+ for (const entry of entries) {
1414
+ const entryName = typeof entry === 'string' ? entry : entry && entry.name ? entry.name : null;
1415
+ if (!entryName || !entryName.toLowerCase().endsWith('.json')) {
1416
+ continue;
1417
+ }
1418
+
1419
+ const isFileEntry = typeof entry === 'string'
1420
+ ? true
1421
+ : (typeof entry.isFile === 'function' ? entry.isFile() : true);
1422
+
1423
+ if (!isFileEntry) {
1424
+ continue;
1425
+ }
1426
+
1427
+ collected.push(path.join(evalDirPath, entryName));
1428
+ }
1429
+ }
1430
+
1431
+ const deduped = [];
1432
+ const seen = new Set();
1433
+
1434
+ for (const candidate of collected) {
1435
+ const normalizedCandidate = normalizeRelativePath(candidate);
1436
+ const dedupeKey = process.platform === 'win32'
1437
+ ? normalizedCandidate.toLowerCase()
1438
+ : normalizedCandidate;
1439
+
1440
+ if (seen.has(dedupeKey)) {
1441
+ continue;
1442
+ }
1443
+
1444
+ seen.add(dedupeKey);
1445
+ deduped.push(candidate);
1446
+ }
1447
+
1448
+ if (deduped.length === 0) {
1449
+ throw new Error('no eval report JSON files resolved from current options');
1450
+ }
1451
+
1452
+ return deduped;
1453
+ }
1454
+
1455
+ async function loadSceneRoutePolicySuggestReports(reportPaths, fileSystem = fs) {
1456
+ const readJson = typeof fileSystem.readJson === 'function'
1457
+ ? fileSystem.readJson.bind(fileSystem)
1458
+ : fs.readJson.bind(fs);
1459
+
1460
+ const reports = [];
1461
+
1462
+ for (const reportPath of reportPaths) {
1463
+ const reportPayload = await readJson(reportPath);
1464
+
1465
+ if (!isPlainObject(reportPayload)) {
1466
+ throw new Error(`eval report must contain a JSON object: ${reportPath}`);
1467
+ }
1468
+
1469
+ reports.push({
1470
+ sourcePath: reportPath,
1471
+ report: reportPayload
1472
+ });
1473
+ }
1474
+
1475
+ return reports;
1476
+ }
1477
+
1478
+ async function loadSceneRoutePolicySuggestBaseline(options = {}, projectRoot, reportSummary, fileSystem = fs) {
1479
+ if (!options.routePolicy) {
1480
+ let resolvedProfile = options.profile || 'default';
1481
+ let source = `profile:${resolvedProfile}`;
1482
+
1483
+ if (resolvedProfile === 'default') {
1484
+ const dominantProfile = reportSummary && reportSummary.dominant_profile
1485
+ ? String(reportSummary.dominant_profile).trim().toLowerCase()
1486
+ : 'default';
1487
+
1488
+ if (ROUTE_POLICY_TEMPLATE_PROFILES.has(dominantProfile) && dominantProfile !== 'default') {
1489
+ resolvedProfile = dominantProfile;
1490
+ source = `profile:auto:${resolvedProfile}`;
1491
+ }
1492
+ }
1493
+
1494
+ return {
1495
+ policy: createSceneRoutePolicyTemplateByProfile(resolvedProfile),
1496
+ source,
1497
+ profile: resolvedProfile
1498
+ };
1499
+ }
1500
+
1501
+ const readJson = typeof fileSystem.readJson === 'function'
1502
+ ? fileSystem.readJson.bind(fileSystem)
1503
+ : fs.readJson.bind(fs);
1504
+
1505
+ const routePolicyPath = resolvePath(projectRoot, options.routePolicy);
1506
+ const routePolicyRaw = await readJson(routePolicyPath);
1507
+
1508
+ if (!isPlainObject(routePolicyRaw)) {
1509
+ throw new Error('route policy file must contain a JSON object');
1510
+ }
1511
+
1512
+ return {
1513
+ policy: normalizeSceneRoutePolicy(mergePlainObject(cloneDefaultSceneRoutePolicy(), routePolicyRaw)),
1514
+ source: options.routePolicy,
1515
+ profile: options.profile || 'default'
1516
+ };
1517
+ }
1518
+
1519
+ function sanitizeSceneRoutePolicyRolloutName(rawName = '') {
1520
+ return String(rawName || '')
1521
+ .trim()
1522
+ .toLowerCase()
1523
+ .replace(/[^a-z0-9._-]+/g, '-')
1524
+ .replace(/^-+|-+$/g, '');
1525
+ }
1526
+
1527
+ function resolveSceneRoutePolicyRolloutName(explicitName, generatedAt = new Date().toISOString()) {
1528
+ const normalizedExplicit = sanitizeSceneRoutePolicyRolloutName(explicitName || '');
1529
+ if (normalizedExplicit) {
1530
+ return normalizedExplicit;
1531
+ }
1532
+
1533
+ const timestamp = String(generatedAt || '')
1534
+ .replace(/[-:TZ.]/g, '')
1535
+ .slice(0, 14);
1536
+
1537
+ const fallbackTimestamp = timestamp || `${Date.now()}`;
1538
+ return `route-policy-${fallbackTimestamp}`;
1539
+ }
1540
+
1541
+ function collectSceneRoutePolicyDiff(baselinePolicy = {}, candidatePolicy = {}) {
1542
+ const normalizedBaseline = normalizeSceneRoutePolicy(mergePlainObject(cloneDefaultSceneRoutePolicy(), baselinePolicy));
1543
+ const normalizedCandidate = normalizeSceneRoutePolicy(mergePlainObject(cloneDefaultSceneRoutePolicy(), candidatePolicy));
1544
+
1545
+ const changes = [];
1546
+
1547
+ for (const pathKey of SCENE_ROUTE_POLICY_DIFF_KEYS) {
1548
+ const baselineValueRaw = getObjectValueByPath(normalizedBaseline, pathKey);
1549
+ const candidateValueRaw = getObjectValueByPath(normalizedCandidate, pathKey);
1550
+
1551
+ const fallback = 0;
1552
+ const baselineValue = pathKey === 'max_alternatives'
1553
+ ? Math.trunc(normalizeRoutePolicyNumber(baselineValueRaw, fallback))
1554
+ : normalizeRoutePolicyNumber(baselineValueRaw, fallback);
1555
+ const candidateValue = pathKey === 'max_alternatives'
1556
+ ? Math.trunc(normalizeRoutePolicyNumber(candidateValueRaw, fallback))
1557
+ : normalizeRoutePolicyNumber(candidateValueRaw, fallback);
1558
+
1559
+ if (baselineValue === candidateValue) {
1560
+ continue;
1561
+ }
1562
+
1563
+ const deltaValue = candidateValue - baselineValue;
1564
+
1565
+ changes.push({
1566
+ path: pathKey,
1567
+ from: baselineValue,
1568
+ to: candidateValue,
1569
+ delta: pathKey === 'max_alternatives'
1570
+ ? deltaValue
1571
+ : Number(deltaValue.toFixed(2))
1572
+ });
1573
+ }
1574
+
1575
+ return changes;
1576
+ }
1577
+
1578
+ function buildSceneRoutePolicyRolloutCommands(targetPolicyPath, candidatePolicyPath, rollbackPolicyPath) {
1579
+ return {
1580
+ verify_candidate_route: `sce scene route --query routing --mode dry_run --route-policy ${candidatePolicyPath}`,
1581
+ verify_target_route: `sce scene route --query routing --mode dry_run --route-policy ${targetPolicyPath}`,
1582
+ apply: `Replace ${targetPolicyPath} with ${candidatePolicyPath} after verification.`,
1583
+ rollback: `Replace ${targetPolicyPath} with ${rollbackPolicyPath} if regression appears.`
1584
+ };
1585
+ }
1586
+
1587
+ function buildSceneRoutePolicyRolloutRunbook(payload) {
1588
+ const lines = [
1589
+ '# Scene Route Policy Rollout Runbook',
1590
+ '',
1591
+ `- Rollout: ${payload.rollout_name}`,
1592
+ `- Generated: ${payload.generated_at}`,
1593
+ `- Suggestion Source: ${payload.source_suggestion}`,
1594
+ `- Target Policy: ${payload.target_policy_path}`,
1595
+ `- Changed Fields: ${payload.summary.changed_fields}`,
1596
+ '',
1597
+ '## Verification Commands',
1598
+ '',
1599
+ `1. ${payload.commands.verify_target_route}`,
1600
+ `2. ${payload.commands.verify_candidate_route}`,
1601
+ '',
1602
+ '## Apply and Rollback',
1603
+ '',
1604
+ `- Apply: ${payload.commands.apply}`,
1605
+ `- Rollback: ${payload.commands.rollback}`,
1606
+ ''
1607
+ ];
1608
+
1609
+ if (Array.isArray(payload.changed_fields) && payload.changed_fields.length > 0) {
1610
+ lines.push('## Changed Fields', '');
1611
+
1612
+ for (const item of payload.changed_fields) {
1613
+ lines.push(`- ${item.path}: ${item.from} -> ${item.to} (delta=${item.delta})`);
1614
+ }
1615
+
1616
+ lines.push('');
1617
+ }
1618
+
1619
+ return lines.join('\n');
1620
+ }
1621
+
1622
+ function sanitizeScenePackageName(rawValue = '') {
1623
+ return String(rawValue || '')
1624
+ .trim()
1625
+ .toLowerCase()
1626
+ .replace(/[^a-z0-9._-]+/g, '-')
1627
+ .replace(/^-+|-+$/g, '');
1628
+ }
1629
+
1630
+ function deriveScenePackageName(options = {}) {
1631
+ if (options.name) {
1632
+ return sanitizeScenePackageName(options.name);
1633
+ }
1634
+
1635
+ if (options.spec) {
1636
+ return sanitizeScenePackageName(String(options.spec).replace(/^\d{2}-\d{2}-/, ''));
1637
+ }
1638
+
1639
+ if (options.out) {
1640
+ const parsed = path.parse(options.out);
1641
+ if (parsed.name) {
1642
+ return sanitizeScenePackageName(parsed.name.replace(/^scene-package$/, 'scene-template'));
1643
+ }
1644
+ }
1645
+
1646
+ return 'scene-template';
1647
+ }
1648
+
1649
+ function buildScenePackageCoordinate(contract = {}) {
1650
+ const metadata = isPlainObject(contract.metadata) ? contract.metadata : {};
1651
+ const group = String(metadata.group || '').trim();
1652
+ const name = String(metadata.name || '').trim();
1653
+ const version = String(metadata.version || '').trim();
1654
+
1655
+ if (!group || !name || !version) {
1656
+ return null;
1657
+ }
1658
+
1659
+ return `${group}/${name}@${version}`;
1660
+ }
1661
+
1662
+ function buildScenePackagePublishTemplateManifest(packageContract = {}, context = {}) {
1663
+ const artifacts = isPlainObject(packageContract.artifacts) ? packageContract.artifacts : {};
1664
+ const compatibility = isPlainObject(packageContract.compatibility) ? packageContract.compatibility : {};
1665
+ const minSceVersion = String(compatibility.min_sce_version || '').trim();
1666
+
1667
+ return {
1668
+ apiVersion: SCENE_PACKAGE_TEMPLATE_API_VERSION,
1669
+ kind: 'scene-package-template',
1670
+ metadata: {
1671
+ template_id: context.templateId || null,
1672
+ source_spec: context.spec || null,
1673
+ package_coordinate: buildScenePackageCoordinate(packageContract),
1674
+ package_kind: packageContract.kind || null,
1675
+ published_at: context.publishedAt || new Date().toISOString()
1676
+ },
1677
+ compatibility: {
1678
+ min_sce_version: minSceVersion || '>=1.24.0',
1679
+ scene_api_version: String(compatibility.scene_api_version || '').trim() || 'sce.scene/v0.2'
1680
+ },
1681
+ parameters: Array.isArray(packageContract.parameters)
1682
+ ? JSON.parse(JSON.stringify(packageContract.parameters))
1683
+ : [],
1684
+ template: {
1685
+ package_contract: 'scene-package.json',
1686
+ scene_manifest: 'scene.template.yaml'
1687
+ },
1688
+ artifacts: {
1689
+ entry_scene: String(artifacts.entry_scene || 'custom/scene.yaml') || 'custom/scene.yaml',
1690
+ generates: Array.isArray(artifacts.generates)
1691
+ ? artifacts.generates.filter((item) => typeof item === 'string' && item.trim().length > 0)
1692
+ : []
1693
+ }
1694
+ };
1695
+ }
1696
+
1697
+ function createScenePackageTemplate(options = {}) {
1698
+ const packageName = deriveScenePackageName(options);
1699
+ const kind = SCENE_PACKAGE_KINDS.has(options.kind) ? options.kind : 'scene-template';
1700
+ const group = options.group || 'sce.scene';
1701
+ const version = options.version || '0.1.0';
1702
+
1703
+ return {
1704
+ apiVersion: SCENE_PACKAGE_API_VERSION,
1705
+ kind,
1706
+ metadata: {
1707
+ group,
1708
+ name: packageName || 'scene-template',
1709
+ version,
1710
+ summary: `Template contract for ${packageName || 'scene-template'}`
1711
+ },
1712
+ compatibility: {
1713
+ min_sce_version: '>=1.24.0',
1714
+ scene_api_version: 'sce.scene/v0.2',
1715
+ moqui_model_version: '3.x',
1716
+ adapter_api_version: 'v1'
1717
+ },
1718
+ capabilities: {
1719
+ provides: [
1720
+ `scene.${kind}.core`
1721
+ ],
1722
+ requires: [
1723
+ 'binding:http',
1724
+ 'profile:erp'
1725
+ ]
1726
+ },
1727
+ parameters: [
1728
+ {
1729
+ id: 'entity_name',
1730
+ type: 'string',
1731
+ required: true,
1732
+ description: 'Primary entity name for generated scene flow'
1733
+ },
1734
+ {
1735
+ id: 'service_name',
1736
+ type: 'string',
1737
+ required: false,
1738
+ default: 'queryService',
1739
+ description: 'Optional service binding reference'
1740
+ }
1741
+ ],
1742
+ artifacts: {
1743
+ entry_scene: 'custom/scene.yaml',
1744
+ generates: [
1745
+ 'requirements.md',
1746
+ 'design.md',
1747
+ 'tasks.md',
1748
+ 'custom/scene.yaml'
1749
+ ]
1750
+ },
1751
+ governance: {
1752
+ risk_level: 'low',
1753
+ approval_required: false,
1754
+ rollback_supported: true
1755
+ }
1756
+ };
1757
+ }
1758
+
1759
+ function resolveScenePackageTemplateOutputPath(options = {}, projectRoot = process.cwd()) {
1760
+ if (options.spec) {
1761
+ return path.join(projectRoot, '.sce', 'specs', options.spec, options.out);
1762
+ }
1763
+
1764
+ return resolvePath(projectRoot, options.out);
1765
+ }
1766
+
1767
+ function resolveScenePackageValidateInputPath(options = {}, projectRoot = process.cwd()) {
1768
+ if (options.spec) {
1769
+ return path.join(projectRoot, '.sce', 'specs', options.spec, options.specPackage);
1770
+ }
1771
+
1772
+ return resolvePath(projectRoot, options.packagePath);
1773
+ }
1774
+
1775
+ function deriveScenePackagePublishSourceFromManifestEntry(entry = {}) {
1776
+ const extractSpecRelative = (rawPath) => {
1777
+ const normalized = normalizeRelativePath(rawPath);
1778
+ if (!normalized) {
1779
+ return null;
1780
+ }
1781
+ const marker = '.sce/specs/';
1782
+ const markerIndex = normalized.indexOf(marker);
1783
+ if (markerIndex < 0) {
1784
+ return null;
1785
+ }
1786
+ const suffix = normalized.slice(markerIndex + marker.length);
1787
+ const firstSlash = suffix.indexOf('/');
1788
+ if (firstSlash < 0) {
1789
+ return null;
1790
+ }
1791
+
1792
+ const spec = suffix.slice(0, firstSlash).trim();
1793
+ const relativePath = suffix.slice(firstSlash + 1).trim();
1794
+ if (!spec || !relativePath) {
1795
+ return null;
1796
+ }
1797
+
1798
+ return {
1799
+ spec,
1800
+ relativePath: normalizeRelativePath(relativePath) || relativePath
1801
+ };
1802
+ };
1803
+
1804
+ const explicitSpec = String(entry.id || entry.spec || '').trim();
1805
+ const packageSource = extractSpecRelative(entry.scene_package);
1806
+ const manifestSource = extractSpecRelative(entry.scene_manifest);
1807
+ const spec = explicitSpec || (packageSource ? packageSource.spec : '') || (manifestSource ? manifestSource.spec : '');
1808
+
1809
+ let specPackage = packageSource ? packageSource.relativePath : null;
1810
+ let sceneManifest = manifestSource ? manifestSource.relativePath : null;
1811
+
1812
+ if (spec && packageSource && packageSource.spec !== spec) {
1813
+ specPackage = null;
1814
+ }
1815
+ if (spec && manifestSource && manifestSource.spec !== spec) {
1816
+ sceneManifest = null;
1817
+ }
1818
+
1819
+ return {
1820
+ spec: spec || null,
1821
+ specPackage,
1822
+ sceneManifest
1823
+ };
1824
+ }
1825
+
1826
+ function resolveManifestSpecEntries(manifest = {}, rawSpecPath = 'specs') {
1827
+ const specPath = typeof rawSpecPath === 'string' ? rawSpecPath.trim() : '';
1828
+ if (!specPath) {
1829
+ return null;
1830
+ }
1831
+
1832
+ const pathSegments = specPath
1833
+ .split('.')
1834
+ .map((segment) => segment.trim())
1835
+ .filter((segment) => segment.length > 0);
1836
+
1837
+ if (pathSegments.length === 0) {
1838
+ return null;
1839
+ }
1840
+
1841
+ let cursor = manifest;
1842
+ for (const segment of pathSegments) {
1843
+ if (!cursor || typeof cursor !== 'object' || !Object.prototype.hasOwnProperty.call(cursor, segment)) {
1844
+ return null;
1845
+ }
1846
+ cursor = cursor[segment];
1847
+ }
1848
+
1849
+ if (!Array.isArray(cursor)) {
1850
+ return null;
1851
+ }
1852
+
1853
+ return cursor;
1854
+ }
1855
+
1856
+ function resolveScenePackageTemplateLibraryPath(options = {}, projectRoot = process.cwd()) {
1857
+ return resolvePath(projectRoot, options.outDir);
1858
+ }
1859
+
1860
+ function resolveScenePackageTemplateManifestPath(options = {}, projectRoot = process.cwd()) {
1861
+ return resolvePath(projectRoot, options.template);
1862
+ }
1863
+
1864
+ function resolveScenePackageInstantiateValuesPath(options = {}, projectRoot = process.cwd()) {
1865
+ if (!options.values) {
1866
+ return null;
1867
+ }
1868
+
1869
+ return resolvePath(projectRoot, options.values);
1870
+ }
1871
+
1872
+ function formatScenePackagePath(projectRoot, absolutePath) {
1873
+ const normalizedRelative = normalizeRelativePath(path.relative(projectRoot, absolutePath));
1874
+ if (normalizedRelative && !normalizedRelative.startsWith('..')) {
1875
+ return normalizedRelative;
1876
+ }
1877
+
1878
+ return normalizeRelativePath(absolutePath);
1879
+ }
1880
+
1881
+ function validateScenePackageTemplateManifest(templateManifest = {}) {
1882
+ const errors = [];
1883
+ const warnings = [];
1884
+
1885
+ if (!isPlainObject(templateManifest)) {
1886
+ return {
1887
+ valid: false,
1888
+ errors: ['template manifest must be a JSON object'],
1889
+ warnings
1890
+ };
1891
+ }
1892
+
1893
+ if (templateManifest.apiVersion !== SCENE_PACKAGE_TEMPLATE_API_VERSION) {
1894
+ errors.push(`apiVersion must be ${SCENE_PACKAGE_TEMPLATE_API_VERSION}`);
1895
+ }
1896
+
1897
+ if (String(templateManifest.kind || '').trim() !== 'scene-package-template') {
1898
+ errors.push('kind must be scene-package-template');
1899
+ }
1900
+
1901
+ const metadata = isPlainObject(templateManifest.metadata) ? templateManifest.metadata : null;
1902
+ if (!metadata) {
1903
+ errors.push('metadata object is required');
1904
+ } else if (!String(metadata.template_id || '').trim()) {
1905
+ errors.push('metadata.template_id is required');
1906
+ }
1907
+
1908
+ if (!isPlainObject(templateManifest.template)) {
1909
+ errors.push('template object is required');
1910
+ } else {
1911
+ if (!String(templateManifest.template.package_contract || '').trim()) {
1912
+ errors.push('template.package_contract is required');
1913
+ }
1914
+
1915
+ if (!String(templateManifest.template.scene_manifest || '').trim()) {
1916
+ errors.push('template.scene_manifest is required');
1917
+ }
1918
+ }
1919
+
1920
+ if (!Array.isArray(templateManifest.parameters)) {
1921
+ warnings.push('parameters should be an array');
1922
+ }
1923
+
1924
+ return {
1925
+ valid: errors.length === 0,
1926
+ errors,
1927
+ warnings
1928
+ };
1929
+ }
1930
+
1931
+ function classifyScenePackageLayer(kind) {
1932
+ const normalizedKind = String(kind || '').trim().toLowerCase();
1933
+ return SCENE_PACKAGE_KIND_LAYER_MAP[normalizedKind] || 'unknown';
1934
+ }
1935
+
1936
+ function createScenePackageGatePolicyTemplate(profile = 'baseline') {
1937
+ const normalizedProfile = String(profile || '').trim().toLowerCase();
1938
+
1939
+ const templates = {
1940
+ baseline: {
1941
+ apiVersion: SCENE_PACKAGE_GATE_API_VERSION,
1942
+ profile: 'baseline',
1943
+ rules: {
1944
+ max_invalid_templates: 0,
1945
+ min_valid_templates: 1,
1946
+ required_layers: [],
1947
+ forbid_unknown_layer: false
1948
+ }
1949
+ },
1950
+ 'three-layer': {
1951
+ apiVersion: SCENE_PACKAGE_GATE_API_VERSION,
1952
+ profile: 'three-layer',
1953
+ rules: {
1954
+ max_invalid_templates: 0,
1955
+ min_valid_templates: 3,
1956
+ required_layers: ['l1-capability', 'l2-domain', 'l3-instance'],
1957
+ forbid_unknown_layer: true
1958
+ }
1959
+ }
1960
+ };
1961
+
1962
+ return JSON.parse(JSON.stringify(templates[normalizedProfile] || templates.baseline));
1963
+ }
1964
+
1965
+ function normalizeScenePackageGatePolicy(policy = {}) {
1966
+ const baseline = createScenePackageGatePolicyTemplate('baseline');
1967
+
1968
+ const nextPolicy = isPlainObject(policy) ? JSON.parse(JSON.stringify(policy)) : {};
1969
+ if (!String(nextPolicy.apiVersion || '').trim()) {
1970
+ nextPolicy.apiVersion = baseline.apiVersion;
1971
+ }
1972
+
1973
+ if (!String(nextPolicy.profile || '').trim()) {
1974
+ nextPolicy.profile = baseline.profile;
1975
+ }
1976
+
1977
+ const rules = isPlainObject(nextPolicy.rules) ? nextPolicy.rules : {};
1978
+
1979
+ const maxInvalidTemplates = Number(rules.max_invalid_templates);
1980
+ const minValidTemplates = Number(rules.min_valid_templates);
1981
+
1982
+ nextPolicy.rules = {
1983
+ max_invalid_templates: Number.isFinite(maxInvalidTemplates) && maxInvalidTemplates >= 0
1984
+ ? Math.floor(maxInvalidTemplates)
1985
+ : baseline.rules.max_invalid_templates,
1986
+ min_valid_templates: Number.isFinite(minValidTemplates) && minValidTemplates >= 0
1987
+ ? Math.floor(minValidTemplates)
1988
+ : baseline.rules.min_valid_templates,
1989
+ required_layers: Array.isArray(rules.required_layers)
1990
+ ? rules.required_layers
1991
+ .map((item) => String(item || '').trim())
1992
+ .filter((item) => item.length > 0)
1993
+ : [],
1994
+ forbid_unknown_layer: rules.forbid_unknown_layer === true
1995
+ };
1996
+
1997
+ return nextPolicy;
1998
+ }
1999
+
2000
+ function evaluateScenePackageGate(registryPayload = {}, policy = {}) {
2001
+ const summary = isPlainObject(registryPayload.summary) ? registryPayload.summary : {};
2002
+ const layerCounts = isPlainObject(summary.layer_counts) ? summary.layer_counts : {};
2003
+ const normalizedPolicy = normalizeScenePackageGatePolicy(policy);
2004
+
2005
+ const metrics = {
2006
+ total_templates: Number(summary.total_templates || 0),
2007
+ valid_templates: Number(summary.valid_templates || 0),
2008
+ invalid_templates: Number(summary.invalid_templates || 0),
2009
+ layer_counts: {
2010
+ l1_capability: Number(layerCounts.l1_capability || 0),
2011
+ l2_domain: Number(layerCounts.l2_domain || 0),
2012
+ l3_instance: Number(layerCounts.l3_instance || 0),
2013
+ unknown: Number(layerCounts.unknown || 0)
2014
+ }
2015
+ };
2016
+
2017
+ const checks = [];
2018
+
2019
+ checks.push({
2020
+ id: 'max-invalid-templates',
2021
+ expected: `<= ${normalizedPolicy.rules.max_invalid_templates}`,
2022
+ actual: metrics.invalid_templates,
2023
+ passed: metrics.invalid_templates <= normalizedPolicy.rules.max_invalid_templates
2024
+ });
2025
+
2026
+ checks.push({
2027
+ id: 'min-valid-templates',
2028
+ expected: `>= ${normalizedPolicy.rules.min_valid_templates}`,
2029
+ actual: metrics.valid_templates,
2030
+ passed: metrics.valid_templates >= normalizedPolicy.rules.min_valid_templates
2031
+ });
2032
+
2033
+ for (const layer of normalizedPolicy.rules.required_layers) {
2034
+ const key = layer.replace(/-/g, '_');
2035
+ const count = Number(metrics.layer_counts[key] || 0);
2036
+
2037
+ checks.push({
2038
+ id: `required-layer:${layer}`,
2039
+ expected: '>= 1',
2040
+ actual: count,
2041
+ passed: count >= 1
2042
+ });
2043
+ }
2044
+
2045
+ if (normalizedPolicy.rules.forbid_unknown_layer) {
2046
+ checks.push({
2047
+ id: 'unknown-layer-forbidden',
2048
+ expected: 0,
2049
+ actual: metrics.layer_counts.unknown,
2050
+ passed: metrics.layer_counts.unknown === 0
2051
+ });
2052
+ }
2053
+
2054
+ const failedChecks = checks.filter((item) => item.passed === false);
2055
+
2056
+ return {
2057
+ policy: normalizedPolicy,
2058
+ metrics,
2059
+ checks,
2060
+ summary: {
2061
+ passed: failedChecks.length === 0,
2062
+ total_checks: checks.length,
2063
+ failed_checks: failedChecks.length
2064
+ }
2065
+ };
2066
+ }
2067
+
2068
+ function parseScenePackageGateExpectedInteger(expected, fallback = 0) {
2069
+ const match = String(expected || '').match(/-?\d+/);
2070
+ if (!match) {
2071
+ return fallback;
2072
+ }
2073
+
2074
+ const parsed = Number(match[0]);
2075
+ return Number.isFinite(parsed) ? parsed : fallback;
2076
+ }
2077
+
2078
+ function buildScenePackageGateRemediationPlan(evaluation = {}) {
2079
+ const failedChecks = Array.isArray(evaluation.checks)
2080
+ ? evaluation.checks.filter((item) => item && item.passed === false)
2081
+ : [];
2082
+
2083
+ const actions = [];
2084
+ const seen = new Map();
2085
+
2086
+ const pushAction = (action, sourceCheckId = null) => {
2087
+ if (!isPlainObject(action) || !String(action.id || '').trim()) {
2088
+ return;
2089
+ }
2090
+
2091
+ const actionId = String(action.id).trim();
2092
+ const sourceIds = Array.isArray(action.source_check_ids)
2093
+ ? action.source_check_ids
2094
+ .map((checkId) => String(checkId || '').trim())
2095
+ .filter((checkId) => checkId.length > 0)
2096
+ : [];
2097
+
2098
+ if (sourceCheckId && !sourceIds.includes(sourceCheckId)) {
2099
+ sourceIds.push(sourceCheckId);
2100
+ }
2101
+
2102
+ if (seen.has(actionId)) {
2103
+ const index = seen.get(actionId);
2104
+ const existing = actions[index];
2105
+ const mergedIds = new Set([
2106
+ ...(Array.isArray(existing.source_check_ids) ? existing.source_check_ids : []),
2107
+ ...sourceIds
2108
+ ]);
2109
+ existing.source_check_ids = Array.from(mergedIds);
2110
+ return;
2111
+ }
2112
+
2113
+ seen.set(actionId, actions.length);
2114
+ actions.push({
2115
+ ...action,
2116
+ source_check_ids: sourceIds
2117
+ });
2118
+ };
2119
+
2120
+ for (const check of failedChecks) {
2121
+ const checkId = String(check.id || '').trim();
2122
+
2123
+ if (checkId.startsWith('required-layer:')) {
2124
+ const layer = checkId.slice('required-layer:'.length);
2125
+ const layerKindMap = {
2126
+ 'l1-capability': 'scene-capability',
2127
+ 'l2-domain': 'scene-domain-profile',
2128
+ 'l3-instance': 'scene-template'
2129
+ };
2130
+ const kind = layerKindMap[layer] || 'scene-template';
2131
+
2132
+ pushAction({
2133
+ id: `cover-${layer}`,
2134
+ priority: 'medium',
2135
+ title: `Add at least one ${layer} template package`,
2136
+ recommendation: `Create and publish a ${kind} package to satisfy ${layer} coverage.`,
2137
+ command_hint: `sce scene package-template --kind ${kind} --spec <spec-name> && sce scene package-publish --spec <spec-name>`
2138
+ }, checkId);
2139
+ continue;
2140
+ }
2141
+
2142
+ if (checkId === 'min-valid-templates') {
2143
+ const expectedCount = parseScenePackageGateExpectedInteger(check.expected, 0);
2144
+ const actualCount = Number(check.actual || 0);
2145
+ const gap = Math.max(0, expectedCount - actualCount);
2146
+
2147
+ pushAction({
2148
+ id: 'increase-valid-templates',
2149
+ priority: 'high',
2150
+ title: `Increase valid template count by at least ${gap || 1}`,
2151
+ recommendation: 'Promote additional template packages via package-publish until gate threshold is met.',
2152
+ command_hint: 'sce scene package-registry --template-dir .sce/templates/scene-packages --json'
2153
+ }, checkId);
2154
+ continue;
2155
+ }
2156
+
2157
+ if (checkId === 'max-invalid-templates') {
2158
+ pushAction({
2159
+ id: 'reduce-invalid-templates',
2160
+ priority: 'high',
2161
+ title: 'Reduce invalid template count to gate threshold',
2162
+ recommendation: 'Repair or deprecate invalid templates and rerun registry validation.',
2163
+ command_hint: 'sce scene package-registry --template-dir .sce/templates/scene-packages --strict --json'
2164
+ }, checkId);
2165
+ continue;
2166
+ }
2167
+
2168
+ if (checkId === 'unknown-layer-forbidden') {
2169
+ pushAction({
2170
+ id: 'remove-unknown-layer-templates',
2171
+ priority: 'high',
2172
+ title: 'Eliminate unknown-layer template classifications',
2173
+ recommendation: 'Align package kind declarations with supported scene layers and republish.',
2174
+ command_hint: 'sce scene package-template --kind <scene-capability|scene-domain-profile|scene-template> --spec <spec-name>'
2175
+ }, checkId);
2176
+ continue;
2177
+ }
2178
+
2179
+ pushAction({
2180
+ id: `resolve-${sanitizeScenePackageName(checkId) || 'gate-check'}`,
2181
+ priority: 'medium',
2182
+ title: `Resolve gate check ${checkId || 'unknown'}`,
2183
+ recommendation: 'Inspect gate details and apply corrective template actions.',
2184
+ command_hint: 'sce scene package-gate --registry <path> --policy <path> --json'
2185
+ }, checkId);
2186
+ }
2187
+
2188
+ return {
2189
+ action_count: actions.length,
2190
+ actions
2191
+ };
2192
+ }
2193
+
2194
+ function validateScenePackageContract(contract = {}) {
2195
+ const errors = [];
2196
+ const warnings = [];
2197
+
2198
+ if (!isPlainObject(contract)) {
2199
+ return {
2200
+ valid: false,
2201
+ errors: ['scene package contract must be a JSON object'],
2202
+ warnings: [],
2203
+ summary: {
2204
+ coordinate: null,
2205
+ kind: null,
2206
+ parameter_count: 0,
2207
+ provides_count: 0,
2208
+ requires_count: 0
2209
+ }
2210
+ };
2211
+ }
2212
+
2213
+ if (contract.apiVersion !== SCENE_PACKAGE_API_VERSION) {
2214
+ errors.push(`apiVersion must be ${SCENE_PACKAGE_API_VERSION}`);
2215
+ }
2216
+
2217
+ if (!SCENE_PACKAGE_KINDS.has(contract.kind)) {
2218
+ errors.push(`kind must be one of ${Array.from(SCENE_PACKAGE_KINDS).join(', ')}`);
2219
+ }
2220
+
2221
+ const metadata = isPlainObject(contract.metadata) ? contract.metadata : null;
2222
+ if (!metadata) {
2223
+ errors.push('metadata object is required');
2224
+ }
2225
+
2226
+ const group = metadata ? String(metadata.group || '').trim() : '';
2227
+ const name = metadata ? String(metadata.name || '').trim() : '';
2228
+ const version = metadata ? String(metadata.version || '').trim() : '';
2229
+
2230
+ if (!group) {
2231
+ errors.push('metadata.group is required');
2232
+ }
2233
+
2234
+ if (!name) {
2235
+ errors.push('metadata.name is required');
2236
+ }
2237
+
2238
+ if (!version) {
2239
+ errors.push('metadata.version is required');
2240
+ } else if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
2241
+ errors.push('metadata.version must be semantic version (x.y.z)');
2242
+ }
2243
+
2244
+ const compatibility = isPlainObject(contract.compatibility) ? contract.compatibility : null;
2245
+ if (!compatibility) {
2246
+ errors.push('compatibility object is required');
2247
+ } else {
2248
+ const minSceVersion = String(compatibility.min_sce_version || '').trim();
2249
+
2250
+ if (!minSceVersion) {
2251
+ errors.push('compatibility.min_sce_version is required');
2252
+ }
2253
+
2254
+ if (!String(compatibility.scene_api_version || '').trim()) {
2255
+ errors.push('compatibility.scene_api_version is required');
2256
+ }
2257
+ }
2258
+
2259
+ const capabilities = isPlainObject(contract.capabilities) ? contract.capabilities : null;
2260
+ if (!capabilities) {
2261
+ errors.push('capabilities object is required');
2262
+ }
2263
+
2264
+ const provides = capabilities && Array.isArray(capabilities.provides) ? capabilities.provides : [];
2265
+ const requires = capabilities && Array.isArray(capabilities.requires) ? capabilities.requires : [];
2266
+
2267
+ if (provides.length === 0) {
2268
+ warnings.push('capabilities.provides is empty');
2269
+ }
2270
+
2271
+ if (provides.some((item) => typeof item !== 'string' || item.trim().length === 0)) {
2272
+ errors.push('capabilities.provides must contain non-empty strings');
2273
+ }
2274
+
2275
+ if (requires.some((item) => typeof item !== 'string' || item.trim().length === 0)) {
2276
+ errors.push('capabilities.requires must contain non-empty strings');
2277
+ }
2278
+
2279
+ const parameters = Array.isArray(contract.parameters) ? contract.parameters : [];
2280
+ for (const [index, parameter] of parameters.entries()) {
2281
+ if (!isPlainObject(parameter)) {
2282
+ errors.push(`parameters[${index}] must be an object`);
2283
+ continue;
2284
+ }
2285
+
2286
+ if (!String(parameter.id || '').trim()) {
2287
+ errors.push(`parameters[${index}].id is required`);
2288
+ }
2289
+
2290
+ if (!String(parameter.type || '').trim()) {
2291
+ errors.push(`parameters[${index}].type is required`);
2292
+ }
2293
+
2294
+ if (Object.prototype.hasOwnProperty.call(parameter, 'required') && typeof parameter.required !== 'boolean') {
2295
+ errors.push(`parameters[${index}].required must be boolean when provided`);
2296
+ }
2297
+ }
2298
+
2299
+ const artifacts = isPlainObject(contract.artifacts) ? contract.artifacts : null;
2300
+ if (!artifacts) {
2301
+ errors.push('artifacts object is required');
2302
+ } else {
2303
+ if (!String(artifacts.entry_scene || '').trim()) {
2304
+ errors.push('artifacts.entry_scene is required');
2305
+ }
2306
+
2307
+ if (!Array.isArray(artifacts.generates) || artifacts.generates.length === 0) {
2308
+ errors.push('artifacts.generates must contain at least one output path');
2309
+ } else if (artifacts.generates.some((item) => typeof item !== 'string' || item.trim().length === 0)) {
2310
+ errors.push('artifacts.generates must contain non-empty strings');
2311
+ }
2312
+ }
2313
+
2314
+ const governance = isPlainObject(contract.governance) ? contract.governance : null;
2315
+ if (!governance) {
2316
+ errors.push('governance object is required');
2317
+ } else {
2318
+ const riskLevel = String(governance.risk_level || '').trim().toLowerCase();
2319
+ if (!SCENE_PACKAGE_RISK_LEVELS.has(riskLevel)) {
2320
+ errors.push(`governance.risk_level must be one of ${Array.from(SCENE_PACKAGE_RISK_LEVELS).join(', ')}`);
2321
+ }
2322
+
2323
+ if (typeof governance.approval_required !== 'boolean') {
2324
+ errors.push('governance.approval_required must be boolean');
2325
+ }
2326
+
2327
+ if (typeof governance.rollback_supported !== 'boolean') {
2328
+ errors.push('governance.rollback_supported must be boolean');
2329
+ }
2330
+ }
2331
+
2332
+ return {
2333
+ valid: errors.length === 0,
2334
+ errors,
2335
+ warnings,
2336
+ summary: {
2337
+ coordinate: buildScenePackageCoordinate(contract),
2338
+ kind: SCENE_PACKAGE_KINDS.has(contract.kind) ? contract.kind : null,
2339
+ parameter_count: parameters.length,
2340
+ provides_count: provides.length,
2341
+ requires_count: requires.length
2342
+ }
2343
+ };
2344
+ }
2345
+
2346
+ function buildScenePackageTemplateId(packageContract = {}, explicitTemplateId) {
2347
+ const explicit = sanitizeScenePackageName(explicitTemplateId || '');
2348
+ if (explicit) {
2349
+ return explicit;
2350
+ }
2351
+
2352
+ const metadata = isPlainObject(packageContract.metadata) ? packageContract.metadata : {};
2353
+ const group = sanitizeScenePackageName(metadata.group || 'sce.scene') || 'sce.scene';
2354
+ const name = sanitizeScenePackageName(metadata.name || 'scene-template') || 'scene-template';
2355
+ const version = sanitizeScenePackageName(metadata.version || '0.1.0') || '0.1.0';
2356
+
2357
+ return `${group}--${name}--${version}`;
2358
+ }
2359
+
2360
+ function normalizeScenePackageTemplateValueMap(values) {
2361
+ if (!isPlainObject(values)) {
2362
+ return {};
2363
+ }
2364
+
2365
+ const normalized = {};
2366
+ for (const [key, value] of Object.entries(values)) {
2367
+ const normalizedKey = String(key || '').trim();
2368
+ if (!normalizedKey) {
2369
+ continue;
2370
+ }
2371
+
2372
+ normalized[normalizedKey] = value === null || value === undefined
2373
+ ? ''
2374
+ : String(value);
2375
+ }
2376
+
2377
+ return normalized;
2378
+ }
2379
+
2380
+ function renderScenePackageTemplateContent(content, valueMap = {}) {
2381
+ let rendered = String(content || '');
2382
+
2383
+ for (const [key, value] of Object.entries(valueMap)) {
2384
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2385
+ rendered = rendered
2386
+ .replace(new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, 'g'), String(value))
2387
+ .replace(new RegExp(`\\$\\{${escapedKey}\\}`, 'g'), String(value));
2388
+ }
2389
+
2390
+ return rendered;
2391
+ }
2392
+
2393
+ function resolveScenePackageTemplateParameterValues(packageContract = {}, rawValues = {}) {
2394
+ const valueMap = normalizeScenePackageTemplateValueMap(rawValues);
2395
+ const parameters = Array.isArray(packageContract.parameters) ? packageContract.parameters : [];
2396
+ const resolved = {};
2397
+ const missing = [];
2398
+
2399
+ for (const parameter of parameters) {
2400
+ if (!isPlainObject(parameter)) {
2401
+ continue;
2402
+ }
2403
+
2404
+ const parameterId = String(parameter.id || '').trim();
2405
+ if (!parameterId) {
2406
+ continue;
2407
+ }
2408
+
2409
+ const hasInput = Object.prototype.hasOwnProperty.call(valueMap, parameterId);
2410
+ if (hasInput) {
2411
+ resolved[parameterId] = valueMap[parameterId];
2412
+ continue;
2413
+ }
2414
+
2415
+ if (Object.prototype.hasOwnProperty.call(parameter, 'default')) {
2416
+ resolved[parameterId] = parameter.default === null || parameter.default === undefined
2417
+ ? ''
2418
+ : String(parameter.default);
2419
+ continue;
2420
+ }
2421
+
2422
+ if (parameter.required === true) {
2423
+ missing.push(parameterId);
2424
+ continue;
2425
+ }
2426
+
2427
+ resolved[parameterId] = '';
2428
+ }
2429
+
2430
+ return {
2431
+ values: resolved,
2432
+ missing
2433
+ };
2434
+ }
2435
+
2436
+ function buildScenePackageInstantiateContract(packageContract = {}, targetSpec) {
2437
+ const contractCopy = JSON.parse(JSON.stringify(packageContract || {}));
2438
+
2439
+ if (!isPlainObject(contractCopy.metadata)) {
2440
+ contractCopy.metadata = {};
2441
+ }
2442
+
2443
+ const targetName = sanitizeScenePackageName(String(targetSpec || '').replace(/^\d{2}-\d{2}-/, '')) || 'scene-instance';
2444
+ contractCopy.metadata.name = targetName;
2445
+
2446
+ return contractCopy;
2447
+ }
2448
+
2449
+ function buildScenePackageInstantiateManifest(manifestContent, valueMap, targetSpec) {
2450
+ if (!manifestContent) {
2451
+ const fallbackRef = sanitizeScenePackageName(String(targetSpec || '').replace(/^\d{2}-\d{2}-/, '')) || 'scene-instance';
2452
+ return [
2453
+ 'apiVersion: sce.scene/v0.2',
2454
+ 'kind: scene',
2455
+ 'metadata:',
2456
+ ` obj_id: scene.erp.${fallbackRef}`,
2457
+ ' obj_version: 0.2.0',
2458
+ ` title: ${fallbackRef}`,
2459
+ 'spec:',
2460
+ ' domain: erp',
2461
+ ' intent:',
2462
+ ' goal: Generated from scene package template',
2463
+ ' capability_contract:',
2464
+ ' bindings:',
2465
+ ' - type: query',
2466
+ ' ref: spec.erp.generated.query',
2467
+ ' governance_contract:',
2468
+ ' risk_level: low',
2469
+ ' approval:',
2470
+ ' required: false',
2471
+ ' idempotency:',
2472
+ ' required: true',
2473
+ ' key: requestId',
2474
+ ''
2475
+ ].join('\n');
2476
+ }
2477
+
2478
+ return renderScenePackageTemplateContent(manifestContent, valueMap);
2479
+ }
2480
+
2481
+ function normalizeProfileName(profile) {
2482
+ const normalized = String(profile || '').trim().toLowerCase();
2483
+ if (EVAL_CONFIG_TEMPLATE_PROFILES.has(normalized)) {
2484
+ return normalized;
2485
+ }
2486
+
2487
+ return null;
2488
+ }
2489
+
2490
+ function normalizeEvalProfileInferenceRules(rules = {}) {
2491
+ const merged = cloneDefaultEvalProfileInferenceRules();
2492
+
2493
+ if (!isPlainObject(rules)) {
2494
+ return merged;
2495
+ }
2496
+
2497
+ if (isPlainObject(rules.domain_aliases)) {
2498
+ const nextAliases = { ...merged.domain_aliases };
2499
+
2500
+ for (const [rawDomain, rawProfile] of Object.entries(rules.domain_aliases)) {
2501
+ const domainKey = String(rawDomain || '').trim().toLowerCase();
2502
+ const profileName = normalizeProfileName(rawProfile);
2503
+
2504
+ if (!domainKey || !profileName) {
2505
+ continue;
2506
+ }
2507
+
2508
+ nextAliases[domainKey] = profileName;
2509
+ }
2510
+
2511
+ merged.domain_aliases = nextAliases;
2512
+ }
2513
+
2514
+ if (Array.isArray(rules.scene_ref_rules)) {
2515
+ const normalizedRules = [];
2516
+
2517
+ for (const item of rules.scene_ref_rules) {
2518
+ if (!isPlainObject(item)) {
2519
+ continue;
2520
+ }
2521
+
2522
+ const pattern = String(item.pattern || '').trim();
2523
+ const profileName = normalizeProfileName(item.profile);
2524
+
2525
+ if (!pattern || !profileName) {
2526
+ continue;
2527
+ }
2528
+
2529
+ try {
2530
+ new RegExp(pattern, 'i');
2531
+ } catch (error) {
2532
+ continue;
2533
+ }
2534
+
2535
+ normalizedRules.push({
2536
+ pattern,
2537
+ profile: profileName
2538
+ });
2539
+ }
2540
+
2541
+ merged.scene_ref_rules = normalizedRules;
2542
+ }
2543
+
2544
+ return merged;
2545
+ }
2546
+
2547
+ function createDefaultSceneEvalProfileRulesTemplate() {
2548
+ return cloneDefaultEvalProfileInferenceRules();
2549
+ }
2550
+
2551
+ async function loadSceneEvalProfileRules(options = {}, projectRoot, fileSystem = fs) {
2552
+ const pathExists = typeof fileSystem.pathExists === 'function'
2553
+ ? fileSystem.pathExists.bind(fileSystem)
2554
+ : fs.pathExists.bind(fs);
2555
+ const readJson = typeof fileSystem.readJson === 'function'
2556
+ ? fileSystem.readJson.bind(fileSystem)
2557
+ : fs.readJson.bind(fs);
2558
+
2559
+ const warnings = [];
2560
+ const defaultRules = createDefaultSceneEvalProfileRulesTemplate();
2561
+
2562
+ const explicitRulesPath = options.profileRules ? resolvePath(projectRoot, options.profileRules) : null;
2563
+ const implicitRulesPath = path.join(projectRoot, '.sce', 'config', 'scene-eval-profile-rules.json');
2564
+ const hasExplicitRulesPath = !!explicitRulesPath;
2565
+
2566
+ let rulesPath = null;
2567
+ let rulesSource = 'default';
2568
+
2569
+ if (hasExplicitRulesPath) {
2570
+ rulesPath = explicitRulesPath;
2571
+ rulesSource = options.profileRules;
2572
+ } else {
2573
+ try {
2574
+ if (await pathExists(implicitRulesPath)) {
2575
+ rulesPath = implicitRulesPath;
2576
+ rulesSource = '.sce/config/scene-eval-profile-rules.json';
2577
+ }
2578
+ } catch (error) {
2579
+ warnings.push(`profile rules path check failed: ${error.message}`);
2580
+ }
2581
+ }
2582
+
2583
+ if (!rulesPath) {
2584
+ return {
2585
+ rules: defaultRules,
2586
+ source: rulesSource,
2587
+ warnings
2588
+ };
2589
+ }
2590
+
2591
+ let rawRules = null;
2592
+
2593
+ try {
2594
+ rawRules = await readJson(rulesPath);
2595
+ } catch (error) {
2596
+ if (hasExplicitRulesPath) {
2597
+ throw new Error(`failed to load profile rules file: ${rulesPath} (${error.message})`);
2598
+ }
2599
+
2600
+ warnings.push(`failed to load implicit profile rules file: ${rulesSource}`);
2601
+ return {
2602
+ rules: defaultRules,
2603
+ source: 'default',
2604
+ warnings
2605
+ };
2606
+ }
2607
+
2608
+ if (!isPlainObject(rawRules)) {
2609
+ if (hasExplicitRulesPath) {
2610
+ throw new Error(`profile rules file must contain a JSON object: ${rulesPath}`);
2611
+ }
2612
+
2613
+ warnings.push(`invalid implicit profile rules file: ${rulesSource}`);
2614
+ return {
2615
+ rules: defaultRules,
2616
+ source: 'default',
2617
+ warnings
2618
+ };
2619
+ }
2620
+
2621
+ return {
2622
+ rules: normalizeEvalProfileInferenceRules(rawRules),
2623
+ source: rulesSource,
2624
+ warnings
2625
+ };
2626
+ }
2627
+
2628
+ function resolveSceneEvalConfigProfile(config = {}, envName = null) {
2629
+ if (!isPlainObject(config)) {
2630
+ throw new Error('eval config must be a JSON object');
2631
+ }
2632
+
2633
+ let targetConfig = isPlainObject(config.target) ? mergePlainObject({}, config.target) : {};
2634
+ let taskSyncPolicy = isPlainObject(config.task_sync_policy) ? mergePlainObject({}, config.task_sync_policy) : {};
2635
+
2636
+ if (envName) {
2637
+ const envs = isPlainObject(config.envs) ? config.envs : {};
2638
+ const envConfig = envs[envName];
2639
+
2640
+ if (!isPlainObject(envConfig)) {
2641
+ throw new Error(`eval config env profile not found: ${envName}`);
2642
+ }
2643
+
2644
+ if (isPlainObject(envConfig.target)) {
2645
+ targetConfig = mergePlainObject(targetConfig, envConfig.target);
2646
+ }
2647
+
2648
+ if (isPlainObject(envConfig.task_sync_policy)) {
2649
+ taskSyncPolicy = mergePlainObject(taskSyncPolicy, envConfig.task_sync_policy);
2650
+ }
2651
+ }
2652
+
2653
+ return {
2654
+ targetConfig,
2655
+ taskSyncPolicy
2656
+ };
2657
+ }
2658
+
2659
+ function createDefaultSceneEvalConfigTemplate() {
2660
+ return {
2661
+ target: {
2662
+ max_cycle_time_ms: 2500,
2663
+ max_manual_takeover_rate: 0.25,
2664
+ max_policy_violation_count: 0,
2665
+ max_node_failure_count: 0,
2666
+ min_completion_rate: 0.8,
2667
+ max_blocked_rate: 0.1
2668
+ },
2669
+ task_sync_policy: cloneDefaultEvalTaskSyncPolicy(),
2670
+ envs: {
2671
+ dev: {
2672
+ target: {
2673
+ max_cycle_time_ms: 4000,
2674
+ max_manual_takeover_rate: 0.5,
2675
+ min_completion_rate: 0.6,
2676
+ max_blocked_rate: 0.4
2677
+ },
2678
+ task_sync_policy: {
2679
+ default_priority: 'medium'
2680
+ }
2681
+ },
2682
+ staging: {
2683
+ target: {
2684
+ max_cycle_time_ms: 2800,
2685
+ max_manual_takeover_rate: 0.3,
2686
+ min_completion_rate: 0.75,
2687
+ max_blocked_rate: 0.2
2688
+ },
2689
+ task_sync_policy: {
2690
+ default_priority: 'medium'
2691
+ }
2692
+ },
2693
+ prod: {
2694
+ target: {
2695
+ max_cycle_time_ms: 1800,
2696
+ max_manual_takeover_rate: 0.15,
2697
+ min_completion_rate: 0.9,
2698
+ max_blocked_rate: 0.05
2699
+ },
2700
+ task_sync_policy: {
2701
+ default_priority: 'high',
2702
+ priority_by_grade: {
2703
+ good: 'medium',
2704
+ watch: 'high',
2705
+ at_risk: 'high',
2706
+ critical: 'critical'
2707
+ }
2708
+ }
2709
+ }
2710
+ }
2711
+ };
2712
+ }
2713
+
2714
+ function createSceneEvalConfigTemplateByProfile(profile = 'default') {
2715
+ const normalizedProfile = String(profile || 'default').trim().toLowerCase();
2716
+ const base = createDefaultSceneEvalConfigTemplate();
2717
+
2718
+ const profilePatches = {
2719
+ erp: {
2720
+ target: {
2721
+ max_cycle_time_ms: 2000,
2722
+ max_manual_takeover_rate: 0.2,
2723
+ min_completion_rate: 0.85,
2724
+ max_blocked_rate: 0.08
2725
+ },
2726
+ task_sync_policy: {
2727
+ default_priority: 'medium',
2728
+ keyword_priority_overrides: [
2729
+ {
2730
+ pattern: 'invoice|payment|tax|ledger|cost|inventory|order',
2731
+ priority: 'high'
2732
+ }
2733
+ ]
2734
+ },
2735
+ envs: {
2736
+ prod: {
2737
+ target: {
2738
+ max_cycle_time_ms: 1500,
2739
+ max_manual_takeover_rate: 0.12,
2740
+ min_completion_rate: 0.92,
2741
+ max_blocked_rate: 0.04
2742
+ }
2743
+ }
2744
+ }
2745
+ },
2746
+ ops: {
2747
+ target: {
2748
+ max_cycle_time_ms: 1800,
2749
+ max_manual_takeover_rate: 0.12,
2750
+ max_policy_violation_count: 0,
2751
+ max_node_failure_count: 0,
2752
+ min_completion_rate: 0.9,
2753
+ max_blocked_rate: 0.06
2754
+ },
2755
+ task_sync_policy: {
2756
+ default_priority: 'high',
2757
+ priority_by_grade: {
2758
+ good: 'medium',
2759
+ watch: 'high',
2760
+ at_risk: 'high',
2761
+ critical: 'critical'
2762
+ },
2763
+ keyword_priority_overrides: [
2764
+ {
2765
+ pattern: 'incident|outage|rollback|security|credential|breach|degrade',
2766
+ priority: 'critical'
2767
+ }
2768
+ ]
2769
+ },
2770
+ envs: {
2771
+ prod: {
2772
+ target: {
2773
+ max_cycle_time_ms: 1200,
2774
+ max_manual_takeover_rate: 0.08,
2775
+ min_completion_rate: 0.95,
2776
+ max_blocked_rate: 0.03
2777
+ }
2778
+ }
2779
+ }
2780
+ },
2781
+ robot: {
2782
+ target: {
2783
+ max_cycle_time_ms: 2200,
2784
+ max_manual_takeover_rate: 0.1,
2785
+ max_policy_violation_count: 0,
2786
+ max_node_failure_count: 0,
2787
+ min_completion_rate: 0.9,
2788
+ max_blocked_rate: 0.05
2789
+ },
2790
+ task_sync_policy: {
2791
+ default_priority: 'high',
2792
+ priority_by_grade: {
2793
+ good: 'medium',
2794
+ watch: 'high',
2795
+ at_risk: 'critical',
2796
+ critical: 'critical'
2797
+ },
2798
+ keyword_priority_overrides: [
2799
+ {
2800
+ pattern: 'safety|collision|emergency|hardware|robot|stop channel|preflight',
2801
+ priority: 'critical'
2802
+ }
2803
+ ]
2804
+ },
2805
+ envs: {
2806
+ prod: {
2807
+ target: {
2808
+ max_cycle_time_ms: 1500,
2809
+ max_manual_takeover_rate: 0.06,
2810
+ min_completion_rate: 0.96,
2811
+ max_blocked_rate: 0.02
2812
+ }
2813
+ }
2814
+ }
2815
+ }
2816
+ };
2817
+
2818
+ if (!Object.prototype.hasOwnProperty.call(profilePatches, normalizedProfile)) {
2819
+ return base;
2820
+ }
2821
+
2822
+ return mergePlainObject(base, profilePatches[normalizedProfile]);
2823
+ }
2824
+
2825
+ function normalizeRelativePath(targetPath = '') {
2826
+ return String(targetPath || '').replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '');
2827
+ }
2828
+
2829
+ function collectManifestDiscoveryCandidates(preferredPath = 'custom/scene.yaml') {
2830
+ const ordered = [];
2831
+ const seen = new Set();
2832
+
2833
+ const appendCandidate = (candidate) => {
2834
+ const normalized = normalizeRelativePath(candidate || '').trim();
2835
+ if (!normalized || seen.has(normalized)) {
2836
+ return;
2837
+ }
2838
+
2839
+ seen.add(normalized);
2840
+ ordered.push(normalized);
2841
+ };
2842
+
2843
+ appendCandidate(preferredPath);
2844
+
2845
+ for (const candidate of SCENE_MANIFEST_DISCOVERY_CANDIDATES) {
2846
+ appendCandidate(candidate);
2847
+ }
2848
+
2849
+ return ordered;
2850
+ }
2851
+
2852
+ async function discoverSpecSceneManifestPath(projectRoot, specName, preferredPath = 'custom/scene.yaml', fileSystem = fs) {
2853
+ const specRoot = path.join(projectRoot, '.sce', 'specs', specName);
2854
+ const pathExists = typeof fileSystem.pathExists === 'function'
2855
+ ? fileSystem.pathExists.bind(fileSystem)
2856
+ : fs.pathExists.bind(fs);
2857
+
2858
+ const candidates = collectManifestDiscoveryCandidates(preferredPath);
2859
+
2860
+ for (const candidate of candidates) {
2861
+ const absolutePath = path.join(specRoot, candidate);
2862
+
2863
+ try {
2864
+ if (await pathExists(absolutePath)) {
2865
+ return candidate;
2866
+ }
2867
+ } catch (error) {
2868
+ // Ignore path check failures and continue discovery.
2869
+ }
2870
+ }
2871
+
2872
+ const readDirectory = typeof fileSystem.readdir === 'function'
2873
+ ? fileSystem.readdir.bind(fileSystem)
2874
+ : fs.readdir.bind(fs);
2875
+
2876
+ const queue = [{ absolutePath: specRoot, relativePath: '', depth: 0 }];
2877
+ const visited = new Set();
2878
+
2879
+ while (queue.length > 0) {
2880
+ const current = queue.shift();
2881
+
2882
+ if (!current || current.depth > SCENE_MANIFEST_DISCOVERY_MAX_DEPTH) {
2883
+ continue;
2884
+ }
2885
+
2886
+ if (visited.has(current.absolutePath)) {
2887
+ continue;
2888
+ }
2889
+
2890
+ visited.add(current.absolutePath);
2891
+
2892
+ let entries = [];
2893
+
2894
+ try {
2895
+ entries = await readDirectory(current.absolutePath, { withFileTypes: true });
2896
+ } catch (error) {
2897
+ continue;
2898
+ }
2899
+
2900
+ for (const entry of entries) {
2901
+ if (!entry || !entry.name) {
2902
+ continue;
2903
+ }
2904
+
2905
+ const nextRelativePath = current.relativePath
2906
+ ? `${current.relativePath}/${entry.name}`
2907
+ : entry.name;
2908
+
2909
+ if (typeof entry.isFile === 'function' && entry.isFile() && /^scene\.(yaml|yml|json)$/i.test(entry.name)) {
2910
+ return normalizeRelativePath(nextRelativePath);
2911
+ }
2912
+
2913
+ if (typeof entry.isDirectory === 'function' && entry.isDirectory()) {
2914
+ if (entry.name === '.git' || entry.name === 'node_modules') {
2915
+ continue;
2916
+ }
2917
+
2918
+ queue.push({
2919
+ absolutePath: path.join(current.absolutePath, entry.name),
2920
+ relativePath: nextRelativePath,
2921
+ depth: current.depth + 1
2922
+ });
2923
+ }
2924
+ }
2925
+ }
2926
+
2927
+ return null;
2928
+ }
2929
+
2930
+ async function loadSceneManifestForEvalProfile(options = {}, sceneLoader, projectRoot, fileSystem = fs) {
2931
+ if (!options.spec || options.profile) {
2932
+ return {
2933
+ sceneManifest: null,
2934
+ manifestPath: null,
2935
+ manifestSource: null,
2936
+ warnings: []
2937
+ };
2938
+ }
2939
+
2940
+ const requestedManifestPath = normalizeRelativePath(options.specManifest || 'custom/scene.yaml');
2941
+ const warnings = [];
2942
+
2943
+ try {
2944
+ const sceneManifest = await sceneLoader.loadFromSpec(options.spec, requestedManifestPath);
2945
+
2946
+ return {
2947
+ sceneManifest,
2948
+ manifestPath: requestedManifestPath,
2949
+ manifestSource: 'requested',
2950
+ warnings
2951
+ };
2952
+ } catch (error) {
2953
+ warnings.push(`requested manifest unavailable: ${requestedManifestPath}`);
2954
+
2955
+ if (options.profileManifestAutoDiscovery === false) {
2956
+ return {
2957
+ sceneManifest: null,
2958
+ manifestPath: null,
2959
+ manifestSource: null,
2960
+ warnings
2961
+ };
2962
+ }
2963
+ }
2964
+
2965
+ const discoveredManifestPath = await discoverSpecSceneManifestPath(
2966
+ projectRoot,
2967
+ options.spec,
2968
+ requestedManifestPath,
2969
+ fileSystem
2970
+ );
2971
+
2972
+ if (!discoveredManifestPath || discoveredManifestPath === requestedManifestPath) {
2973
+ warnings.push('profile manifest auto-discovery did not find an alternative manifest');
2974
+
2975
+ return {
2976
+ sceneManifest: null,
2977
+ manifestPath: null,
2978
+ manifestSource: null,
2979
+ warnings
2980
+ };
2981
+ }
2982
+
2983
+ try {
2984
+ const sceneManifest = await sceneLoader.loadFromSpec(options.spec, discoveredManifestPath);
2985
+ warnings.push(`profile manifest auto-discovery selected: ${discoveredManifestPath}`);
2986
+
2987
+ return {
2988
+ sceneManifest,
2989
+ manifestPath: discoveredManifestPath,
2990
+ manifestSource: 'auto-discovered',
2991
+ warnings
2992
+ };
2993
+ } catch (error) {
2994
+ warnings.push(`auto-discovered manifest unavailable: ${discoveredManifestPath}`);
2995
+
2996
+ return {
2997
+ sceneManifest: null,
2998
+ manifestPath: null,
2999
+ manifestSource: null,
3000
+ warnings
3001
+ };
3002
+ }
3003
+ }
3004
+
3005
+ function buildSceneCatalogEntry(specName, manifestPath, sceneManifest, errors = []) {
3006
+ const metadata = sceneManifest && typeof sceneManifest === 'object' ? sceneManifest.metadata || {} : {};
3007
+ const spec = sceneManifest && typeof sceneManifest === 'object' ? sceneManifest.spec || {} : {};
3008
+ const governanceContract = spec && typeof spec === 'object' ? spec.governance_contract || {} : {};
3009
+ const capabilityContract = spec && typeof spec === 'object' ? spec.capability_contract || {} : {};
3010
+ const bindings = Array.isArray(capabilityContract.bindings) ? capabilityContract.bindings : [];
3011
+
3012
+ const entry = {
3013
+ spec: specName,
3014
+ manifest_path: manifestPath,
3015
+ valid: errors.length === 0,
3016
+ errors: errors.length > 0 ? errors : [],
3017
+ kind: typeof sceneManifest.kind === 'string' ? sceneManifest.kind : null,
3018
+ api_version: typeof sceneManifest.apiVersion === 'string' ? sceneManifest.apiVersion : null,
3019
+ scene_ref: typeof metadata.obj_id === 'string' ? metadata.obj_id : null,
3020
+ scene_version: typeof metadata.obj_version === 'string' ? metadata.obj_version : null,
3021
+ title: typeof metadata.title === 'string' ? metadata.title : null,
3022
+ domain: typeof spec.domain === 'string' ? spec.domain : null,
3023
+ risk_level: typeof governanceContract.risk_level === 'string' ? governanceContract.risk_level : null,
3024
+ binding_count: bindings.length
3025
+ };
3026
+
3027
+ const tags = Array.isArray(metadata.tags)
3028
+ ? metadata.tags.filter((tag) => typeof tag === 'string' && tag.trim().length > 0).map((tag) => tag.trim())
3029
+ : [];
3030
+
3031
+ if (tags.length > 0) {
3032
+ entry.tags = tags;
3033
+ }
3034
+
3035
+ return entry;
3036
+ }
3037
+
3038
+ function matchesSceneCatalogFilters(entry, options) {
3039
+ if (options.domain) {
3040
+ const entryDomain = typeof entry.domain === 'string' ? entry.domain.toLowerCase() : '';
3041
+ if (entryDomain !== options.domain) {
3042
+ return false;
3043
+ }
3044
+ }
3045
+
3046
+ if (options.kind) {
3047
+ const entryKind = typeof entry.kind === 'string' ? entry.kind.toLowerCase() : '';
3048
+ if (entryKind !== options.kind) {
3049
+ return false;
3050
+ }
3051
+ }
3052
+
3053
+ return true;
3054
+ }
3055
+
3056
+ async function listSpecDirectoryNames(projectRoot, fileSystem = fs) {
3057
+ const specsPath = path.join(projectRoot, '.sce', 'specs');
3058
+ const readDirectory = typeof fileSystem.readdir === 'function'
3059
+ ? fileSystem.readdir.bind(fileSystem)
3060
+ : fs.readdir.bind(fs);
3061
+
3062
+ const entries = await readDirectory(specsPath, { withFileTypes: true });
3063
+
3064
+ return entries
3065
+ .filter((entry) => entry && typeof entry.isDirectory === 'function' && entry.isDirectory())
3066
+ .map((entry) => entry.name)
3067
+ .sort((left, right) => left.localeCompare(right));
3068
+ }
3069
+
3070
+ async function buildSceneCatalog(options = {}, dependencies = {}) {
3071
+ const projectRoot = dependencies.projectRoot || process.cwd();
3072
+ const fileSystem = dependencies.fileSystem || fs;
3073
+ const sceneLoader = dependencies.sceneLoader || new SceneLoader({ projectPath: projectRoot });
3074
+
3075
+ const readFile = typeof fileSystem.readFile === 'function'
3076
+ ? fileSystem.readFile.bind(fileSystem)
3077
+ : fs.readFile.bind(fs);
3078
+
3079
+ const pathExists = typeof fileSystem.pathExists === 'function'
3080
+ ? fileSystem.pathExists.bind(fileSystem)
3081
+ : fs.pathExists.bind(fs);
3082
+
3083
+ const specNames = options.spec
3084
+ ? [options.spec]
3085
+ : await listSpecDirectoryNames(projectRoot, fileSystem);
3086
+
3087
+ if (options.spec) {
3088
+ const specPath = path.join(projectRoot, '.sce', 'specs', options.spec);
3089
+ if (!await pathExists(specPath)) {
3090
+ throw new Error(`target spec not found: ${options.spec}`);
3091
+ }
3092
+ }
3093
+
3094
+ const entries = [];
3095
+ const summary = {
3096
+ specs_scanned: specNames.length,
3097
+ manifests_discovered: 0,
3098
+ skipped_no_manifest: 0,
3099
+ filtered_out: 0,
3100
+ entries_returned: 0,
3101
+ valid_entries: 0,
3102
+ invalid_entries: 0
3103
+ };
3104
+
3105
+ for (const specName of specNames) {
3106
+ const manifestPath = await discoverSpecSceneManifestPath(projectRoot, specName, options.specManifest, fileSystem);
3107
+
3108
+ if (!manifestPath) {
3109
+ summary.skipped_no_manifest += 1;
3110
+
3111
+ if (options.includeInvalid) {
3112
+ const missingEntry = buildSceneCatalogEntry(specName, null, {}, ['scene manifest not found']);
3113
+ if (matchesSceneCatalogFilters(missingEntry, options)) {
3114
+ entries.push(missingEntry);
3115
+ } else {
3116
+ summary.filtered_out += 1;
3117
+ }
3118
+ }
3119
+
3120
+ continue;
3121
+ }
3122
+
3123
+ summary.manifests_discovered += 1;
3124
+
3125
+ const manifestAbsolutePath = path.join(projectRoot, '.sce', 'specs', specName, manifestPath);
3126
+ let sceneManifest = {};
3127
+ let validationErrors = [];
3128
+
3129
+ try {
3130
+ const rawContent = await readFile(manifestAbsolutePath, 'utf8');
3131
+ sceneManifest = sceneLoader.parseManifest(rawContent, manifestAbsolutePath);
3132
+
3133
+ const manifestValidation = sceneLoader.validateManifest(sceneManifest);
3134
+ if (!manifestValidation.valid) {
3135
+ validationErrors = Array.isArray(manifestValidation.errors) ? manifestValidation.errors : ['invalid scene manifest'];
3136
+ }
3137
+ } catch (error) {
3138
+ validationErrors = [error.message || 'failed to parse scene manifest'];
3139
+ }
3140
+
3141
+ const catalogEntry = buildSceneCatalogEntry(specName, manifestPath, sceneManifest, validationErrors);
3142
+
3143
+ if (!catalogEntry.valid && !options.includeInvalid) {
3144
+ summary.filtered_out += 1;
3145
+ continue;
3146
+ }
3147
+
3148
+ if (!matchesSceneCatalogFilters(catalogEntry, options)) {
3149
+ summary.filtered_out += 1;
3150
+ continue;
3151
+ }
3152
+
3153
+ entries.push(catalogEntry);
3154
+ }
3155
+
3156
+ entries.sort((left, right) => {
3157
+ const refCompare = String(left.scene_ref || '').localeCompare(String(right.scene_ref || ''));
3158
+ if (refCompare !== 0) {
3159
+ return refCompare;
3160
+ }
3161
+
3162
+ const specCompare = String(left.spec || '').localeCompare(String(right.spec || ''));
3163
+ if (specCompare !== 0) {
3164
+ return specCompare;
3165
+ }
3166
+
3167
+ return String(left.manifest_path || '').localeCompare(String(right.manifest_path || ''));
3168
+ });
3169
+
3170
+ summary.entries_returned = entries.length;
3171
+ summary.valid_entries = entries.filter((entry) => entry.valid).length;
3172
+ summary.invalid_entries = entries.length - summary.valid_entries;
3173
+
3174
+ return {
3175
+ generated_at: new Date().toISOString(),
3176
+ filters: {
3177
+ spec: options.spec || null,
3178
+ spec_manifest: options.specManifest,
3179
+ domain: options.domain || null,
3180
+ kind: options.kind || null,
3181
+ include_invalid: options.includeInvalid === true
3182
+ },
3183
+ summary,
3184
+ entries
3185
+ };
3186
+ }
3187
+
3188
+ function tokenizeRouteQuery(text) {
3189
+ return String(text || '')
3190
+ .toLowerCase()
3191
+ .split(/[^a-z0-9]+/)
3192
+ .map((token) => token.trim())
3193
+ .filter((token) => token.length > 0);
3194
+ }
3195
+
3196
+ function buildSceneRouteCommands(entry, options) {
3197
+ if (!entry || !entry.spec || !entry.manifest_path) {
3198
+ return null;
3199
+ }
3200
+
3201
+ const runMode = options.mode || 'dry_run';
3202
+
3203
+ return {
3204
+ validate: `sce scene validate --spec ${entry.spec} --spec-manifest ${entry.manifest_path}`,
3205
+ doctor: `sce scene doctor --spec ${entry.spec} --spec-manifest ${entry.manifest_path} --mode ${runMode}`,
3206
+ run: `sce scene run --spec ${entry.spec} --spec-manifest ${entry.manifest_path} --mode ${runMode}`
3207
+ };
3208
+ }
3209
+
3210
+ function scoreSceneRouteEntry(entry, options = {}, routePolicy = DEFAULT_SCENE_ROUTE_POLICY) {
3211
+ const normalizedPolicy = normalizeSceneRoutePolicy(routePolicy);
3212
+ const weights = normalizedPolicy.weights || {};
3213
+ const modeBias = normalizedPolicy.mode_bias && normalizedPolicy.mode_bias.commit
3214
+ ? normalizedPolicy.mode_bias.commit
3215
+ : {};
3216
+
3217
+ let score = 0;
3218
+ const reasons = [];
3219
+ const sceneRef = String(entry.scene_ref || '').toLowerCase();
3220
+ const title = String(entry.title || '').toLowerCase();
3221
+ const spec = String(entry.spec || '').toLowerCase();
3222
+
3223
+ if (entry.valid) {
3224
+ score += weights.valid_manifest;
3225
+ reasons.push('valid_manifest');
3226
+ } else {
3227
+ score += weights.invalid_manifest;
3228
+ reasons.push('invalid_manifest');
3229
+ }
3230
+
3231
+ if (options.sceneRef) {
3232
+ const targetSceneRef = String(options.sceneRef).toLowerCase();
3233
+
3234
+ if (sceneRef === targetSceneRef) {
3235
+ score += weights.scene_ref_exact;
3236
+ reasons.push('scene_ref_exact');
3237
+ } else if (sceneRef.includes(targetSceneRef)) {
3238
+ score += weights.scene_ref_contains;
3239
+ reasons.push('scene_ref_contains');
3240
+ } else {
3241
+ score += weights.scene_ref_mismatch;
3242
+ reasons.push('scene_ref_mismatch');
3243
+ }
3244
+ }
3245
+
3246
+ if (options.query) {
3247
+ const tokens = tokenizeRouteQuery(options.query);
3248
+ const searchIndex = `${sceneRef} ${title} ${spec}`;
3249
+
3250
+ let matchedTokens = 0;
3251
+ for (const token of tokens) {
3252
+ if (searchIndex.includes(token)) {
3253
+ matchedTokens += 1;
3254
+ }
3255
+ }
3256
+
3257
+ if (tokens.length > 0) {
3258
+ score += matchedTokens * weights.query_token_match;
3259
+
3260
+ if (matchedTokens > 0) {
3261
+ reasons.push(`query_tokens:${matchedTokens}/${tokens.length}`);
3262
+ } else {
3263
+ reasons.push('query_tokens:0');
3264
+ }
3265
+ }
3266
+ }
3267
+
3268
+ if (options.mode === 'commit') {
3269
+ const riskLevel = String(entry.risk_level || '').toLowerCase();
3270
+ const riskBias = normalizeRoutePolicyNumber(modeBias[riskLevel], 0);
3271
+
3272
+ if (riskBias !== 0) {
3273
+ score += riskBias;
3274
+
3275
+ if (riskLevel === 'low' && riskBias > 0) {
3276
+ reasons.push('commit_low_risk');
3277
+ } else if ((riskLevel === 'high' || riskLevel === 'critical') && riskBias < 0) {
3278
+ reasons.push('commit_high_risk');
3279
+ } else {
3280
+ reasons.push(`commit_risk_bias:${riskLevel}`);
3281
+ }
3282
+ }
3283
+ }
3284
+
3285
+ return { score, reasons };
3286
+ }
3287
+
3288
+ function buildSceneRouteDecision(catalog, options = {}, routePolicy = DEFAULT_SCENE_ROUTE_POLICY) {
3289
+ const normalizedPolicy = normalizeSceneRoutePolicy(routePolicy);
3290
+ const maxAlternatives = Math.max(0, Math.trunc(normalizeRoutePolicyNumber(normalizedPolicy.max_alternatives, 4)));
3291
+
3292
+ const candidates = Array.isArray(catalog.entries) ? catalog.entries : [];
3293
+ const ranked = candidates
3294
+ .map((entry) => {
3295
+ const scoring = scoreSceneRouteEntry(entry, options, normalizedPolicy);
3296
+ return {
3297
+ ...entry,
3298
+ score: scoring.score,
3299
+ route_reasons: scoring.reasons,
3300
+ commands: buildSceneRouteCommands(entry, options)
3301
+ };
3302
+ })
3303
+ .sort((left, right) => {
3304
+ if (right.score !== left.score) {
3305
+ return right.score - left.score;
3306
+ }
3307
+
3308
+ return String(left.scene_ref || '').localeCompare(String(right.scene_ref || ''));
3309
+ });
3310
+
3311
+ const selected = ranked.length > 0 ? ranked[0] : null;
3312
+ const second = ranked.length > 1 ? ranked[1] : null;
3313
+ const hasTie = Boolean(
3314
+ options.requireUnique
3315
+ && selected
3316
+ && second
3317
+ && selected.score === second.score
3318
+ );
3319
+
3320
+ return {
3321
+ selected,
3322
+ alternatives: selected ? ranked.slice(1, 1 + maxAlternatives) : ranked.slice(0, maxAlternatives),
3323
+ hasTie,
3324
+ tie_with: hasTie ? second.scene_ref : null,
3325
+ candidates_scored: ranked.length
3326
+ };
3327
+ }
3328
+
3329
+ function inferProfileFromDomain(domain, profileRules = null) {
3330
+ const normalizedDomain = String(domain || '').trim().toLowerCase();
3331
+
3332
+ if (!normalizedDomain) {
3333
+ return 'default';
3334
+ }
3335
+
3336
+ const domainAliases = isPlainObject(profileRules) && isPlainObject(profileRules.domain_aliases)
3337
+ ? profileRules.domain_aliases
3338
+ : DEFAULT_EVAL_PROFILE_INFERENCE_RULES.domain_aliases;
3339
+
3340
+ const mappedProfile = normalizeProfileName(domainAliases[normalizedDomain]);
3341
+ if (mappedProfile) {
3342
+ return mappedProfile;
3343
+ }
3344
+
3345
+ return 'default';
3346
+ }
3347
+
3348
+ function inferProfileFromSceneRef(sceneRef, profileRules = null) {
3349
+ const normalizedSceneRef = String(sceneRef || '').trim().toLowerCase();
3350
+
3351
+ if (!normalizedSceneRef) {
3352
+ return 'default';
3353
+ }
3354
+
3355
+ const tokens = normalizedSceneRef.split(/[.:/_-]+/).filter(Boolean);
3356
+
3357
+ if (tokens.length >= 2 && tokens[0] === 'scene') {
3358
+ const domainToken = tokens[1];
3359
+ const domainProfile = inferProfileFromDomain(domainToken, profileRules);
3360
+
3361
+ if (domainProfile !== 'default') {
3362
+ return domainProfile;
3363
+ }
3364
+ }
3365
+
3366
+ const sceneRefRules = isPlainObject(profileRules) && Array.isArray(profileRules.scene_ref_rules)
3367
+ ? profileRules.scene_ref_rules
3368
+ : DEFAULT_EVAL_PROFILE_INFERENCE_RULES.scene_ref_rules;
3369
+
3370
+ for (const rule of sceneRefRules) {
3371
+ if (!isPlainObject(rule)) {
3372
+ continue;
3373
+ }
3374
+
3375
+ const pattern = String(rule.pattern || '').trim();
3376
+ const profile = normalizeProfileName(rule.profile);
3377
+
3378
+ if (!pattern || !profile) {
3379
+ continue;
3380
+ }
3381
+
3382
+ try {
3383
+ if (new RegExp(pattern, 'i').test(normalizedSceneRef)) {
3384
+ return profile;
3385
+ }
3386
+ } catch (error) {
3387
+ continue;
3388
+ }
3389
+ }
3390
+
3391
+ return 'default';
3392
+ }
3393
+
3394
+ function resolveSceneEvalProfile(options = {}, sceneManifest = null, feedbackPayload = null, resultPayload = null, profileRules = null) {
3395
+ if (options.profile && EVAL_CONFIG_TEMPLATE_PROFILES.has(options.profile)) {
3396
+ return {
3397
+ profile: options.profile ? String(options.profile).trim().toLowerCase() : undefined,
3398
+ source: 'explicit'
3399
+ };
3400
+ }
3401
+
3402
+ const sceneDomain = sceneManifest
3403
+ && sceneManifest.spec
3404
+ && typeof sceneManifest.spec.domain === 'string'
3405
+ ? sceneManifest.spec.domain
3406
+ : null;
3407
+
3408
+ const sceneProfile = inferProfileFromDomain(sceneDomain, profileRules);
3409
+ if (sceneProfile !== 'default') {
3410
+ return {
3411
+ profile: sceneProfile,
3412
+ source: options.spec ? `spec:${options.spec}` : 'spec'
3413
+ };
3414
+ }
3415
+
3416
+ const feedbackDomain = feedbackPayload && typeof feedbackPayload.domain === 'string'
3417
+ ? feedbackPayload.domain
3418
+ : null;
3419
+ const feedbackProfile = inferProfileFromDomain(feedbackDomain, profileRules);
3420
+
3421
+ if (feedbackProfile !== 'default') {
3422
+ return {
3423
+ profile: feedbackProfile,
3424
+ source: 'feedback'
3425
+ };
3426
+ }
3427
+
3428
+ const resultDomain = resultPayload && typeof resultPayload.domain === 'string'
3429
+ ? resultPayload.domain
3430
+ : (resultPayload
3431
+ && resultPayload.eval_payload
3432
+ && typeof resultPayload.eval_payload.domain === 'string'
3433
+ ? resultPayload.eval_payload.domain
3434
+ : null);
3435
+
3436
+ const resultDomainProfile = inferProfileFromDomain(resultDomain, profileRules);
3437
+ if (resultDomainProfile !== 'default') {
3438
+ return {
3439
+ profile: resultDomainProfile,
3440
+ source: 'result:domain'
3441
+ };
3442
+ }
3443
+
3444
+ const resultSceneRef = resultPayload && typeof resultPayload.scene_ref === 'string'
3445
+ ? resultPayload.scene_ref
3446
+ : (resultPayload
3447
+ && resultPayload.eval_payload
3448
+ && typeof resultPayload.eval_payload.scene_ref === 'string'
3449
+ ? resultPayload.eval_payload.scene_ref
3450
+ : null);
3451
+
3452
+ const resultSceneRefProfile = inferProfileFromSceneRef(resultSceneRef, profileRules);
3453
+ if (resultSceneRefProfile !== 'default') {
3454
+ return {
3455
+ profile: resultSceneRefProfile,
3456
+ source: 'result:scene_ref'
3457
+ };
3458
+ }
3459
+
3460
+ const feedbackSceneRef = feedbackPayload && typeof feedbackPayload.scene_ref === 'string'
3461
+ ? feedbackPayload.scene_ref
3462
+ : null;
3463
+
3464
+ const feedbackSceneRefProfile = inferProfileFromSceneRef(feedbackSceneRef, profileRules);
3465
+ if (feedbackSceneRefProfile !== 'default') {
3466
+ return {
3467
+ profile: feedbackSceneRefProfile,
3468
+ source: 'feedback:scene_ref'
3469
+ };
3470
+ }
3471
+
3472
+ return {
3473
+ profile: 'default',
3474
+ source: 'default'
3475
+ };
3476
+ }
3477
+
3478
+ function normalizeEvalTaskSyncPolicy(policy = {}) {
3479
+ const merged = cloneDefaultEvalTaskSyncPolicy();
3480
+
3481
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
3482
+ return merged;
3483
+ }
3484
+
3485
+ merged.default_priority = normalizeTaskPriority(policy.default_priority, merged.default_priority);
3486
+
3487
+ if (policy.priority_by_grade && typeof policy.priority_by_grade === 'object' && !Array.isArray(policy.priority_by_grade)) {
3488
+ const nextGradeMap = { ...merged.priority_by_grade };
3489
+
3490
+ for (const [grade, priority] of Object.entries(policy.priority_by_grade)) {
3491
+ nextGradeMap[grade] = normalizeTaskPriority(priority, nextGradeMap[grade] || merged.default_priority);
3492
+ }
3493
+
3494
+ merged.priority_by_grade = nextGradeMap;
3495
+ }
3496
+
3497
+ if (Array.isArray(policy.keyword_priority_overrides)) {
3498
+ const overrides = [];
3499
+
3500
+ for (const item of policy.keyword_priority_overrides) {
3501
+ if (!item || typeof item !== 'object') {
3502
+ continue;
3503
+ }
3504
+
3505
+ const pattern = String(item.pattern || '').trim();
3506
+ if (!pattern) {
3507
+ continue;
3508
+ }
3509
+
3510
+ overrides.push({
3511
+ pattern,
3512
+ priority: normalizeTaskPriority(item.priority, merged.default_priority)
3513
+ });
3514
+ }
3515
+
3516
+ merged.keyword_priority_overrides = overrides;
3517
+ }
3518
+
3519
+ return merged;
3520
+ }
3521
+
3522
+
3523
+ module.exports = {
3524
+ buildManifestSummary,
3525
+ normalizeBindingPluginReport,
3526
+ buildDoctorSummary,
3527
+ createDoctorSuggestion,
3528
+ dedupeDoctorSuggestions,
3529
+ buildDoctorSuggestions,
3530
+ buildDoctorTodoMarkdown,
3531
+ buildDoctorTaskDraft,
3532
+ buildDoctorFeedbackTemplate,
3533
+ parseSceneDescriptor,
3534
+ normalizeFeedbackStatus,
3535
+ parseFeedbackNumber,
3536
+ parseDoctorFeedbackTemplate,
3537
+ averageOrNull,
3538
+ buildFeedbackTaskSummary,
3539
+ buildFeedbackMetricSummary,
3540
+ evaluateFeedbackScore
3541
+ };