scene-capability-engine 3.4.6 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,12 @@ const {
11
11
  const { findRelatedSpecs } = require('../spec/related-specs');
12
12
  const { captureTimelineCheckpoint } = require('../runtime/project-timeline');
13
13
  const { runProblemEvaluation } = require('../problem/problem-evaluator');
14
+ const {
15
+ loadStudioIntakePolicy,
16
+ runStudioAutoIntake,
17
+ runStudioSpecGovernance,
18
+ runStudioSceneBackfill
19
+ } = require('../studio/spec-intake-governor');
14
20
 
15
21
  const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
16
22
  const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
@@ -1343,6 +1349,9 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1343
1349
  const fromChat = normalizeString(options.fromChat);
1344
1350
  const sceneId = normalizeString(options.scene);
1345
1351
  const specId = normalizeString(options.spec);
1352
+ const goal = normalizeString(options.goal);
1353
+ const manualSpecMode = options.manualSpec === true;
1354
+ const skipSpecGovernance = options.specGovernance === false;
1346
1355
 
1347
1356
  if (!fromChat) {
1348
1357
  throw new Error('--from-chat is required');
@@ -1350,16 +1359,32 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1350
1359
  if (!sceneId) {
1351
1360
  throw new Error('--scene is required');
1352
1361
  }
1353
- const domainChainBinding = await resolveDomainChainBinding({
1362
+
1363
+ const intakePolicyBundle = await loadStudioIntakePolicy(projectPath, fileSystem);
1364
+ const intakePolicy = intakePolicyBundle.policy || {};
1365
+ const governancePolicy = intakePolicy.governance || {};
1366
+ if (manualSpecMode && intakePolicy.allow_manual_spec_override !== true) {
1367
+ throw new Error(
1368
+ '--manual-spec is disabled by studio intake policy (allow_manual_spec_override=false)'
1369
+ );
1370
+ }
1371
+ if (skipSpecGovernance && governancePolicy.require_auto_on_plan !== false) {
1372
+ throw new Error(
1373
+ '--no-spec-governance is disabled by studio intake policy (governance.require_auto_on_plan=true)'
1374
+ );
1375
+ }
1376
+
1377
+ let domainChainBinding = await resolveDomainChainBinding({
1354
1378
  sceneId,
1355
1379
  specId,
1356
- goal: normalizeString(options.goal)
1380
+ goal
1357
1381
  }, {
1358
1382
  projectPath,
1359
1383
  fileSystem
1360
1384
  });
1361
- const relatedSpecLookup = await findRelatedSpecs({
1362
- query: normalizeString(options.goal),
1385
+
1386
+ let relatedSpecLookup = await findRelatedSpecs({
1387
+ query: goal,
1363
1388
  sceneId,
1364
1389
  limit: 8,
1365
1390
  excludeSpecId: domainChainBinding.spec_id || specId || null
@@ -1367,6 +1392,45 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1367
1392
  projectPath,
1368
1393
  fileSystem
1369
1394
  });
1395
+
1396
+ const intake = await runStudioAutoIntake({
1397
+ scene_id: sceneId,
1398
+ from_chat: fromChat,
1399
+ goal,
1400
+ explicit_spec_id: specId,
1401
+ domain_chain_binding: domainChainBinding,
1402
+ related_specs: relatedSpecLookup,
1403
+ apply: !manualSpecMode,
1404
+ skip: manualSpecMode
1405
+ }, {
1406
+ projectPath,
1407
+ fileSystem
1408
+ });
1409
+
1410
+ const intakeSpecId = normalizeString(intake && intake.selected_spec_id);
1411
+ const effectiveSpecId = intakeSpecId || normalizeString(domainChainBinding.spec_id) || specId || null;
1412
+
1413
+ if (effectiveSpecId && effectiveSpecId !== normalizeString(domainChainBinding.spec_id)) {
1414
+ domainChainBinding = await resolveDomainChainBinding({
1415
+ sceneId,
1416
+ specId: effectiveSpecId,
1417
+ goal
1418
+ }, {
1419
+ projectPath,
1420
+ fileSystem
1421
+ });
1422
+ }
1423
+
1424
+ relatedSpecLookup = await findRelatedSpecs({
1425
+ query: goal,
1426
+ sceneId,
1427
+ limit: 8,
1428
+ excludeSpecId: effectiveSpecId || null
1429
+ }, {
1430
+ projectPath,
1431
+ fileSystem
1432
+ });
1433
+
1370
1434
  const relatedSpecItems = Array.isArray(relatedSpecLookup.related_specs)
1371
1435
  ? relatedSpecLookup.related_specs.map((item) => ({
1372
1436
  spec_id: item.spec_id,
@@ -1387,7 +1451,7 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1387
1451
  domainChainBinding.problem_contract || {},
1388
1452
  {
1389
1453
  scene_id: sceneId,
1390
- goal: normalizeString(options.goal),
1454
+ goal,
1391
1455
  problem_statement: normalizeString(domainChainBinding?.summary?.problem_statement),
1392
1456
  verification_plan: normalizeString(domainChainBinding?.summary?.verification_plan)
1393
1457
  }
@@ -1396,11 +1460,11 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1396
1460
  job_id: jobId,
1397
1461
  scene: {
1398
1462
  id: sceneId,
1399
- spec_id: domainChainBinding.spec_id || specId || null
1463
+ spec_id: effectiveSpecId
1400
1464
  },
1401
1465
  source: {
1402
- goal: normalizeString(options.goal) || null,
1403
- spec_id: domainChainBinding.spec_id || specId || null,
1466
+ goal: goal || null,
1467
+ spec_id: effectiveSpecId,
1404
1468
  problem_contract: problemContract,
1405
1469
  problem_contract_path: domainChainBinding.problem_contract_path || null,
1406
1470
  domain_chain: {
@@ -1415,8 +1479,8 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1415
1479
  };
1416
1480
  const planProblemEvaluation = await enforceProblemEvaluationForStage(planShadowJob, 'plan', {
1417
1481
  scene_id: sceneId,
1418
- spec_id: domainChainBinding.spec_id || specId || null,
1419
- goal: normalizeString(options.goal) || null,
1482
+ spec_id: effectiveSpecId,
1483
+ goal: goal || null,
1420
1484
  problem_contract: problemContract,
1421
1485
  domain_chain: {
1422
1486
  resolved: domainChainBinding.resolved === true,
@@ -1444,16 +1508,35 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1444
1508
  const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
1445
1509
  const sceneSessionBinding = await sessionStore.beginSceneSession({
1446
1510
  sceneId,
1447
- objective: normalizeString(options.goal) || `Studio scene cycle for ${sceneId}`,
1511
+ objective: goal || `Studio scene cycle for ${sceneId}`,
1448
1512
  tool: normalizeString(options.tool) || 'generic'
1449
1513
  });
1514
+
1515
+ let governanceSnapshot = null;
1516
+ let governanceWarning = '';
1517
+ const autoRunGovernance = !(skipSpecGovernance)
1518
+ && (!intake || !intake.policy || !intake.policy.governance || intake.policy.governance.auto_run_on_plan !== false);
1519
+ if (autoRunGovernance) {
1520
+ try {
1521
+ governanceSnapshot = await runStudioSpecGovernance({
1522
+ apply: true,
1523
+ scene: sceneId
1524
+ }, {
1525
+ projectPath,
1526
+ fileSystem
1527
+ });
1528
+ } catch (error) {
1529
+ governanceWarning = normalizeString(error && error.message);
1530
+ }
1531
+ }
1532
+
1450
1533
  stages.plan = {
1451
1534
  status: 'completed',
1452
1535
  completed_at: now,
1453
1536
  metadata: {
1454
1537
  from_chat: fromChat,
1455
1538
  scene_id: sceneId,
1456
- spec_id: domainChainBinding.spec_id || specId || null,
1539
+ spec_id: effectiveSpecId,
1457
1540
  scene_session_id: sceneSessionBinding.session.session_id,
1458
1541
  scene_cycle: sceneSessionBinding.scene_cycle,
1459
1542
  domain_chain_resolved: domainChainBinding.resolved === true,
@@ -1463,7 +1546,18 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1463
1546
  domain_chain_summary: domainChainBinding.summary || null,
1464
1547
  domain_chain_reason: domainChainBinding.reason || null,
1465
1548
  problem_contract: problemContract,
1549
+ intake: intake ? {
1550
+ enabled: intake.enabled === true,
1551
+ intent_type: intake.intent ? intake.intent.intent_type : null,
1552
+ decision_action: intake.decision ? intake.decision.action : null,
1553
+ decision_reason: intake.decision ? intake.decision.reason : null,
1554
+ selected_spec_id: intake.selected_spec_id || effectiveSpecId || null,
1555
+ created_spec_id: intake.created_spec && intake.created_spec.created ? intake.created_spec.spec_id : null,
1556
+ policy_path: intake.policy_path || null
1557
+ } : null,
1466
1558
  problem_evaluation: summarizeProblemEvaluation(planProblemEvaluation),
1559
+ spec_governance: governanceSnapshot ? governanceSnapshot.summary : null,
1560
+ spec_governance_warning: governanceWarning || null,
1467
1561
  related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
1468
1562
  related_specs_top: relatedSpecItems
1469
1563
  }
@@ -1477,15 +1571,24 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1477
1571
  status: 'planned',
1478
1572
  source: {
1479
1573
  from_chat: fromChat,
1480
- goal: normalizeString(options.goal) || null,
1481
- spec_id: domainChainBinding.spec_id || specId || null,
1574
+ goal: goal || null,
1575
+ spec_id: effectiveSpecId,
1482
1576
  problem_contract: problemContract,
1483
1577
  problem_contract_path: domainChainBinding.problem_contract_path || null,
1578
+ intake: intake ? {
1579
+ enabled: intake.enabled === true,
1580
+ policy_path: intake.policy_path || null,
1581
+ policy_loaded_from: intake.policy_loaded_from || null,
1582
+ intent: intake.intent || null,
1583
+ decision: intake.decision || null,
1584
+ selected_spec_id: intake.selected_spec_id || effectiveSpecId || null,
1585
+ created_spec: intake.created_spec || null
1586
+ } : null,
1484
1587
  domain_chain: {
1485
1588
  resolved: domainChainBinding.resolved === true,
1486
1589
  source: domainChainBinding.source || 'none',
1487
1590
  reason: domainChainBinding.reason || null,
1488
- spec_id: domainChainBinding.spec_id || null,
1591
+ spec_id: effectiveSpecId || domainChainBinding.spec_id || null,
1489
1592
  chain_path: domainChainBinding.chain_path || null,
1490
1593
  candidate_count: Number.isFinite(Number(domainChainBinding.candidate_count))
1491
1594
  ? Number(domainChainBinding.candidate_count)
@@ -1500,11 +1603,20 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1500
1603
  scene_id: relatedSpecLookup.scene_id || null,
1501
1604
  total_candidates: Number(relatedSpecLookup.total_candidates || 0),
1502
1605
  items: relatedSpecItems
1503
- }
1606
+ },
1607
+ spec_governance: governanceSnapshot
1608
+ ? {
1609
+ status: governanceSnapshot.summary ? governanceSnapshot.summary.status : null,
1610
+ alert_count: governanceSnapshot.summary ? Number(governanceSnapshot.summary.alert_count || 0) : 0,
1611
+ report_file: governanceSnapshot.report_file || null,
1612
+ scene_index_file: governanceSnapshot.scene_index_file || null
1613
+ }
1614
+ : null,
1615
+ spec_governance_warning: governanceWarning || null
1504
1616
  },
1505
1617
  scene: {
1506
1618
  id: sceneId,
1507
- spec_id: domainChainBinding.spec_id || specId || null,
1619
+ spec_id: effectiveSpecId,
1508
1620
  related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
1509
1621
  },
1510
1622
  session: {
@@ -1520,6 +1632,12 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1520
1632
  patch_bundle_id: null,
1521
1633
  verify_report: null,
1522
1634
  release_ref: null,
1635
+ spec_portfolio_report: governanceSnapshot && governanceSnapshot.report_file
1636
+ ? governanceSnapshot.report_file
1637
+ : null,
1638
+ spec_scene_index: governanceSnapshot && governanceSnapshot.scene_index_file
1639
+ ? governanceSnapshot.scene_index_file
1640
+ : null,
1523
1641
  problem_eval_reports: {
1524
1642
  plan: normalizeString(planProblemEvaluation.report_file) || null
1525
1643
  }
@@ -1530,7 +1648,7 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1530
1648
  await appendStudioEvent(paths, job, 'stage.plan.completed', {
1531
1649
  from_chat: fromChat,
1532
1650
  scene_id: sceneId,
1533
- spec_id: domainChainBinding.spec_id || specId || null,
1651
+ spec_id: effectiveSpecId,
1534
1652
  scene_session_id: sceneSessionBinding.session.session_id,
1535
1653
  scene_cycle: sceneSessionBinding.scene_cycle,
1536
1654
  target: job.target,
@@ -1539,13 +1657,27 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1539
1657
  domain_chain_spec_id: domainChainBinding.spec_id || null,
1540
1658
  domain_chain_path: domainChainBinding.chain_path || null,
1541
1659
  problem_contract: problemContract,
1660
+ intake_action: intake && intake.decision ? intake.decision.action : null,
1661
+ intake_reason: intake && intake.decision ? intake.decision.reason : null,
1662
+ intake_selected_spec_id: intake ? intake.selected_spec_id || effectiveSpecId || null : effectiveSpecId,
1663
+ intake_created_spec_id: intake && intake.created_spec && intake.created_spec.created
1664
+ ? intake.created_spec.spec_id
1665
+ : null,
1542
1666
  problem_evaluation: summarizeProblemEvaluation(planProblemEvaluation),
1667
+ spec_governance: governanceSnapshot ? governanceSnapshot.summary : null,
1668
+ spec_governance_warning: governanceWarning || null,
1543
1669
  related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
1544
1670
  related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
1545
1671
  }, fileSystem);
1546
1672
  await writeLatestJob(paths, jobId, fileSystem);
1547
1673
 
1548
1674
  const payload = buildCommandPayload('studio-plan', job);
1675
+ payload.scene = {
1676
+ id: sceneId,
1677
+ spec_id: effectiveSpecId
1678
+ };
1679
+ payload.intake = job.source && job.source.intake ? job.source.intake : null;
1680
+ payload.spec_governance = governanceSnapshot ? governanceSnapshot.summary : null;
1549
1681
  printStudioPayload(payload, options);
1550
1682
  return payload;
1551
1683
  }
@@ -2178,6 +2310,152 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
2178
2310
  return payload;
2179
2311
  }
2180
2312
 
2313
+ function printStudioIntakePayload(payload, options = {}) {
2314
+ if (options.json) {
2315
+ console.log(JSON.stringify(payload, null, 2));
2316
+ return;
2317
+ }
2318
+
2319
+ console.log(chalk.blue('Studio intake'));
2320
+ console.log(` Scene: ${payload.scene_id || 'n/a'}`);
2321
+ console.log(` Goal: ${payload.goal || '(empty)'}`);
2322
+ console.log(` Intent: ${payload.intent && payload.intent.intent_type ? payload.intent.intent_type : 'unknown'}`);
2323
+ console.log(` Decision: ${payload.decision && payload.decision.action ? payload.decision.action : 'none'}`);
2324
+ console.log(` Spec: ${payload.selected_spec_id || 'n/a'}`);
2325
+ }
2326
+
2327
+ async function runStudioIntakeCommand(options = {}, dependencies = {}) {
2328
+ const projectPath = dependencies.projectPath || process.cwd();
2329
+ const fileSystem = dependencies.fileSystem || fs;
2330
+ const sceneId = normalizeString(options.scene);
2331
+ const fromChat = normalizeString(options.fromChat);
2332
+ const goal = normalizeString(options.goal);
2333
+ const specId = normalizeString(options.spec);
2334
+
2335
+ if (!sceneId) {
2336
+ throw new Error('--scene is required');
2337
+ }
2338
+ if (!fromChat) {
2339
+ throw new Error('--from-chat is required');
2340
+ }
2341
+
2342
+ const domainChainBinding = await resolveDomainChainBinding({
2343
+ sceneId,
2344
+ specId,
2345
+ goal
2346
+ }, {
2347
+ projectPath,
2348
+ fileSystem
2349
+ });
2350
+
2351
+ const relatedSpecLookup = await findRelatedSpecs({
2352
+ query: goal,
2353
+ sceneId,
2354
+ limit: 8,
2355
+ excludeSpecId: domainChainBinding.spec_id || specId || null
2356
+ }, {
2357
+ projectPath,
2358
+ fileSystem
2359
+ });
2360
+
2361
+ const intake = await runStudioAutoIntake({
2362
+ scene_id: sceneId,
2363
+ from_chat: fromChat,
2364
+ goal,
2365
+ explicit_spec_id: specId,
2366
+ domain_chain_binding: domainChainBinding,
2367
+ related_specs: relatedSpecLookup,
2368
+ apply: options.apply === true,
2369
+ skip: options.manualSpec === true
2370
+ }, {
2371
+ projectPath,
2372
+ fileSystem
2373
+ });
2374
+
2375
+ const payload = {
2376
+ ...intake,
2377
+ domain_chain_source: domainChainBinding.source || 'none',
2378
+ domain_chain_spec_id: domainChainBinding.spec_id || null,
2379
+ related_specs_total: Number(relatedSpecLookup.total_candidates || 0)
2380
+ };
2381
+ printStudioIntakePayload(payload, options);
2382
+ return payload;
2383
+ }
2384
+
2385
+ function printStudioPortfolioPayload(payload, options = {}) {
2386
+ if (options.json) {
2387
+ console.log(JSON.stringify(payload, null, 2));
2388
+ return;
2389
+ }
2390
+ const summary = payload.summary || {};
2391
+ console.log(chalk.blue('Studio portfolio governance'));
2392
+ console.log(` Status: ${summary.status || 'unknown'}`);
2393
+ console.log(` Scenes: ${summary.scene_count || 0}`);
2394
+ console.log(` Specs: ${summary.total_specs || 0}`);
2395
+ console.log(` Active: ${summary.active_specs || 0}`);
2396
+ console.log(` Completed: ${summary.completed_specs || 0}`);
2397
+ console.log(` Stale: ${summary.stale_specs || 0}`);
2398
+ console.log(` Duplicate pairs: ${summary.duplicate_pairs || 0}`);
2399
+ console.log(` Overflow scenes: ${summary.overflow_scenes || 0}`);
2400
+ }
2401
+
2402
+ function printStudioBackfillPayload(payload, options = {}) {
2403
+ if (options.json) {
2404
+ console.log(JSON.stringify(payload, null, 2));
2405
+ return;
2406
+ }
2407
+ console.log(chalk.blue('Studio scene backfill'));
2408
+ console.log(` Source scene: ${payload.source_scene || 'scene.unassigned'}`);
2409
+ console.log(` Candidates: ${payload.summary ? payload.summary.candidate_count : 0}`);
2410
+ console.log(` Changed: ${payload.summary ? payload.summary.changed_count : 0}`);
2411
+ console.log(` Apply: ${payload.apply ? 'yes' : 'no'}`);
2412
+ console.log(` Override file: ${payload.override_file || 'n/a'}`);
2413
+ }
2414
+
2415
+ async function runStudioPortfolioCommand(options = {}, dependencies = {}) {
2416
+ const projectPath = dependencies.projectPath || process.cwd();
2417
+ const fileSystem = dependencies.fileSystem || fs;
2418
+ const payload = await runStudioSpecGovernance({
2419
+ scene: normalizeString(options.scene),
2420
+ apply: options.apply !== false
2421
+ }, {
2422
+ projectPath,
2423
+ fileSystem
2424
+ });
2425
+
2426
+ if (options.strict && payload.summary && Number(payload.summary.alert_count || 0) > 0) {
2427
+ throw new Error(
2428
+ `studio portfolio governance has alerts: ${payload.summary.alert_count} (duplicate/stale/overflow)`
2429
+ );
2430
+ }
2431
+
2432
+ printStudioPortfolioPayload(payload, options);
2433
+ return payload;
2434
+ }
2435
+
2436
+ async function runStudioBackfillSpecScenesCommand(options = {}, dependencies = {}) {
2437
+ const projectPath = dependencies.projectPath || process.cwd();
2438
+ const fileSystem = dependencies.fileSystem || fs;
2439
+ const backfillOptions = {
2440
+ scene: normalizeString(options.scene),
2441
+ all: options.all === true,
2442
+ limit: options.limit,
2443
+ apply: options.apply === true,
2444
+ refresh_governance: options.refreshGovernance !== false
2445
+ };
2446
+ if (options.activeOnly === true) {
2447
+ backfillOptions.active_only = true;
2448
+ }
2449
+
2450
+ const payload = await runStudioSceneBackfill(backfillOptions, {
2451
+ projectPath,
2452
+ fileSystem
2453
+ });
2454
+
2455
+ printStudioBackfillPayload(payload, options);
2456
+ return payload;
2457
+ }
2458
+
2181
2459
  async function runStudioCommand(handler, options, stageName = '') {
2182
2460
  try {
2183
2461
  const stage = normalizeString(stageName) || 'unknown';
@@ -2221,11 +2499,46 @@ function registerStudioCommands(program) {
2221
2499
  .requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
2222
2500
  .option('--spec <spec-id>', 'Optional spec binding for domain-chain context ingestion')
2223
2501
  .option('--goal <goal>', 'Optional goal summary')
2502
+ .option('--manual-spec', 'Legacy bypass flag (disabled by default policy)')
2224
2503
  .option('--target <target>', 'Target integration profile', 'default')
2504
+ .option('--no-spec-governance', 'Legacy bypass flag (disabled by default policy)')
2225
2505
  .option('--job <job-id>', 'Reuse an explicit studio job id')
2226
2506
  .option('--json', 'Print machine-readable JSON output')
2227
2507
  .action(async (options) => runStudioCommand(runStudioPlanCommand, options, 'plan'));
2228
2508
 
2509
+ studio
2510
+ .command('intake')
2511
+ .description('Analyze chat goal and auto-resolve spec binding/create decision')
2512
+ .requiredOption('--scene <scene-id>', 'Scene identifier')
2513
+ .requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
2514
+ .option('--spec <spec-id>', 'Optional explicit spec id')
2515
+ .option('--goal <goal>', 'Goal text used for intent classification')
2516
+ .option('--apply', 'Create spec when decision is create_spec')
2517
+ .option('--manual-spec', 'Legacy bypass flag (disabled by default policy)')
2518
+ .option('--json', 'Print machine-readable JSON output')
2519
+ .action(async (options) => runStudioCommand(runStudioIntakeCommand, options, 'intake'));
2520
+
2521
+ studio
2522
+ .command('portfolio')
2523
+ .description('Build scene-organized spec governance portfolio')
2524
+ .option('--scene <scene-id>', 'Optional scene filter')
2525
+ .option('--no-apply', 'Do not write portfolio/index artifacts to .sce/spec-governance/')
2526
+ .option('--strict', 'Fail when governance alerts are detected')
2527
+ .option('--json', 'Print machine-readable JSON output')
2528
+ .action(async (options) => runStudioCommand(runStudioPortfolioCommand, options, 'portfolio'));
2529
+
2530
+ studio
2531
+ .command('backfill-spec-scenes')
2532
+ .description('Backfill scene bindings for historical specs (writes override mapping when --apply)')
2533
+ .option('--scene <scene-id>', 'Source scene filter (default: scene.unassigned)')
2534
+ .option('--all', 'Include completed/stale specs (default uses active-only policy)')
2535
+ .option('--active-only', 'Force active-only filtering')
2536
+ .option('--limit <n>', 'Maximum number of specs to process')
2537
+ .option('--apply', 'Write mapping to .sce/spec-governance/spec-scene-overrides.json')
2538
+ .option('--no-refresh-governance', 'Skip portfolio refresh after apply')
2539
+ .option('--json', 'Print machine-readable JSON output')
2540
+ .action(async (options) => runStudioCommand(runStudioBackfillSpecScenesCommand, options, 'backfill-spec-scenes'));
2541
+
2229
2542
  studio
2230
2543
  .command('generate')
2231
2544
  .description('Generate patch bundle metadata for a planned studio job (scene inherited from plan)')
@@ -2310,12 +2623,15 @@ module.exports = {
2310
2623
  resolveNextAction,
2311
2624
  buildProgress,
2312
2625
  runStudioPlanCommand,
2626
+ runStudioIntakeCommand,
2313
2627
  runStudioGenerateCommand,
2314
2628
  runStudioApplyCommand,
2315
2629
  runStudioVerifyCommand,
2316
2630
  runStudioReleaseCommand,
2317
2631
  runStudioRollbackCommand,
2318
2632
  runStudioEventsCommand,
2633
+ runStudioPortfolioCommand,
2634
+ runStudioBackfillSpecScenesCommand,
2319
2635
  runStudioResumeCommand,
2320
2636
  registerStudioCommands
2321
2637
  };
@@ -1,6 +1,10 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { DOMAIN_CHAIN_RELATIVE_PATH } = require('./domain-modeling');
4
+ const {
5
+ loadSceneBindingOverrides,
6
+ resolveSceneIdFromOverrides
7
+ } = require('./scene-binding-overrides');
4
8
 
5
9
  function normalizeText(value) {
6
10
  if (typeof value !== 'string') {
@@ -70,6 +74,8 @@ async function resolveSpecSearchEntries(projectPath, fileSystem = fs) {
70
74
  return [];
71
75
  }
72
76
 
77
+ const overrideContext = await loadSceneBindingOverrides(projectPath, {}, fileSystem);
78
+ const overrides = overrideContext.overrides;
73
79
  const names = await fileSystem.readdir(specsRoot);
74
80
  const entries = [];
75
81
 
@@ -106,7 +112,10 @@ async function resolveSpecSearchEntries(projectPath, fileSystem = fs) {
106
112
  ]);
107
113
 
108
114
  const sceneId = normalizeText(
109
- (domainChain && domainChain.scene_id) || extractSceneIdFromSceneSpec(sceneSpecContent) || ''
115
+ (domainChain && domainChain.scene_id)
116
+ || extractSceneIdFromSceneSpec(sceneSpecContent)
117
+ || resolveSceneIdFromOverrides(specId, overrides)
118
+ || ''
110
119
  ) || null;
111
120
  const problemStatement = normalizeText(
112
121
  (domainChain && domainChain.problem && domainChain.problem.statement) || ''
@@ -257,4 +266,3 @@ module.exports = {
257
266
  calculateSpecRelevance,
258
267
  findRelatedSpecs
259
268
  };
260
-
@@ -0,0 +1,115 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+
4
+ const DEFAULT_SPEC_SCENE_OVERRIDE_PATH = '.sce/spec-governance/spec-scene-overrides.json';
5
+
6
+ function normalizeText(value) {
7
+ if (typeof value !== 'string') {
8
+ return '';
9
+ }
10
+ return value.trim();
11
+ }
12
+
13
+ function normalizeOverrideEntry(specId, payload = {}) {
14
+ const normalizedSpecId = normalizeText(specId);
15
+ if (!normalizedSpecId) {
16
+ return null;
17
+ }
18
+
19
+ if (typeof payload === 'string') {
20
+ const sceneId = normalizeText(payload);
21
+ if (!sceneId) {
22
+ return null;
23
+ }
24
+ return {
25
+ spec_id: normalizedSpecId,
26
+ scene_id: sceneId,
27
+ source: 'override',
28
+ rule_id: null,
29
+ updated_at: null
30
+ };
31
+ }
32
+
33
+ const sceneId = normalizeText(payload && payload.scene_id);
34
+ if (!sceneId) {
35
+ return null;
36
+ }
37
+ return {
38
+ spec_id: normalizedSpecId,
39
+ scene_id: sceneId,
40
+ source: normalizeText(payload.source) || 'override',
41
+ rule_id: normalizeText(payload.rule_id) || null,
42
+ updated_at: normalizeText(payload.updated_at) || null
43
+ };
44
+ }
45
+
46
+ function normalizeSceneBindingOverrides(raw = {}) {
47
+ const payload = raw && typeof raw === 'object' ? raw : {};
48
+ const mappingsRaw = payload.mappings && typeof payload.mappings === 'object'
49
+ ? payload.mappings
50
+ : {};
51
+ const mappings = {};
52
+ for (const [specId, entry] of Object.entries(mappingsRaw)) {
53
+ const normalized = normalizeOverrideEntry(specId, entry);
54
+ if (!normalized) {
55
+ continue;
56
+ }
57
+ mappings[normalized.spec_id] = {
58
+ scene_id: normalized.scene_id,
59
+ source: normalized.source,
60
+ rule_id: normalized.rule_id,
61
+ updated_at: normalized.updated_at
62
+ };
63
+ }
64
+ return {
65
+ schema_version: normalizeText(payload.schema_version) || '1.0',
66
+ generated_at: normalizeText(payload.generated_at) || null,
67
+ updated_at: normalizeText(payload.updated_at) || null,
68
+ mappings
69
+ };
70
+ }
71
+
72
+ async function loadSceneBindingOverrides(projectPath = process.cwd(), options = {}, fileSystem = fs) {
73
+ const overridePath = normalizeText(options.override_path || options.overridePath)
74
+ || DEFAULT_SPEC_SCENE_OVERRIDE_PATH;
75
+ const absolutePath = path.join(projectPath, overridePath);
76
+ let payload = {};
77
+ let loadedFrom = 'default';
78
+ if (await fileSystem.pathExists(absolutePath)) {
79
+ try {
80
+ payload = await fileSystem.readJson(absolutePath);
81
+ loadedFrom = 'file';
82
+ } catch (_error) {
83
+ payload = {};
84
+ loadedFrom = 'default';
85
+ }
86
+ }
87
+ return {
88
+ override_path: overridePath,
89
+ absolute_path: absolutePath,
90
+ loaded_from: loadedFrom,
91
+ overrides: normalizeSceneBindingOverrides(payload)
92
+ };
93
+ }
94
+
95
+ function resolveSceneIdFromOverrides(specId, overrides = {}) {
96
+ const normalizedSpecId = normalizeText(specId);
97
+ if (!normalizedSpecId) {
98
+ return null;
99
+ }
100
+ const mappings = overrides && typeof overrides === 'object' && overrides.mappings
101
+ ? overrides.mappings
102
+ : {};
103
+ const entry = mappings[normalizedSpecId];
104
+ if (!entry || typeof entry !== 'object') {
105
+ return null;
106
+ }
107
+ return normalizeText(entry.scene_id) || null;
108
+ }
109
+
110
+ module.exports = {
111
+ DEFAULT_SPEC_SCENE_OVERRIDE_PATH,
112
+ normalizeSceneBindingOverrides,
113
+ loadSceneBindingOverrides,
114
+ resolveSceneIdFromOverrides
115
+ };