scene-capability-engine 3.3.26 → 3.4.6

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.
@@ -10,6 +10,7 @@ const {
10
10
  } = require('../spec/domain-modeling');
11
11
  const { findRelatedSpecs } = require('../spec/related-specs');
12
12
  const { captureTimelineCheckpoint } = require('../runtime/project-timeline');
13
+ const { runProblemEvaluation } = require('../problem/problem-evaluator');
13
14
 
14
15
  const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
15
16
  const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
@@ -18,6 +19,8 @@ const STUDIO_EVENT_API_VERSION = 'sce.studio.event/v0.1';
18
19
  const VERIFY_PROFILES = new Set(['fast', 'standard', 'strict']);
19
20
  const RELEASE_PROFILES = new Set(['standard', 'strict']);
20
21
  const STUDIO_REPORTS_DIR = '.sce/reports/studio';
22
+ const DEFAULT_INTERACTIVE_GOVERNANCE_REPORT = '.sce/reports/interactive-governance-report.json';
23
+ const DEFAULT_PROBLEM_CONTRACT_RELATIVE_PATH = path.join('custom', 'problem-contract.json');
21
24
  const MAX_OUTPUT_PREVIEW_LENGTH = 2000;
22
25
  const DEFAULT_STUDIO_SECURITY_POLICY = Object.freeze({
23
26
  enabled: false,
@@ -367,6 +370,7 @@ async function buildVerifyGateSteps(options = {}, dependencies = {}) {
367
370
  const projectPath = dependencies.projectPath || process.cwd();
368
371
  const fileSystem = dependencies.fileSystem || fs;
369
372
  const profile = normalizeString(options.profile) || 'standard';
373
+ const specId = normalizeString(options.specId || dependencies.specId);
370
374
 
371
375
  if (!VERIFY_PROFILES.has(profile)) {
372
376
  throw new Error(`Invalid verify profile "${profile}". Expected one of: ${Array.from(VERIFY_PROFILES).join(', ')}`);
@@ -402,6 +406,21 @@ async function buildVerifyGateSteps(options = {}, dependencies = {}) {
402
406
  }
403
407
 
404
408
  if (profile === 'standard' || profile === 'strict') {
409
+ const problemClosureGateScript = path.join(projectPath, 'scripts', 'problem-closure-gate.js');
410
+ const hasProblemClosureGateScript = await fileSystem.pathExists(problemClosureGateScript);
411
+ const canRunProblemClosureGate = hasProblemClosureGateScript && Boolean(specId);
412
+ steps.push({
413
+ id: 'problem-closure-gate',
414
+ name: 'problem closure gate (verify)',
415
+ command: 'node',
416
+ args: ['scripts/problem-closure-gate.js', '--stage', 'verify', '--spec', specId, '--fail-on-block', '--json'],
417
+ required: Boolean(specId),
418
+ enabled: canRunProblemClosureGate,
419
+ skip_reason: canRunProblemClosureGate
420
+ ? ''
421
+ : (specId ? 'scripts/problem-closure-gate.js not found' : 'spec id unavailable for problem-closure gate')
422
+ });
423
+
405
424
  const governanceScript = path.join(projectPath, 'scripts', 'interactive-governance-report.js');
406
425
  const hasGovernanceScript = await fileSystem.pathExists(governanceScript);
407
426
  steps.push({
@@ -434,11 +453,32 @@ async function buildReleaseGateSteps(options = {}, dependencies = {}) {
434
453
  const projectPath = dependencies.projectPath || process.cwd();
435
454
  const fileSystem = dependencies.fileSystem || fs;
436
455
  const profile = normalizeString(options.profile) || 'standard';
456
+ const specId = normalizeString(options.specId || dependencies.specId);
457
+ const verifyReportPath = normalizeString(options.verifyReportPath || dependencies.verifyReportPath);
437
458
  if (!RELEASE_PROFILES.has(profile)) {
438
459
  throw new Error(`Invalid release profile "${profile}". Expected one of: ${Array.from(RELEASE_PROFILES).join(', ')}`);
439
460
  }
440
461
 
441
462
  const steps = [];
463
+ const problemClosureGateScript = path.join(projectPath, 'scripts', 'problem-closure-gate.js');
464
+ const hasProblemClosureGateScript = await fileSystem.pathExists(problemClosureGateScript);
465
+ const canRunProblemClosureGate = hasProblemClosureGateScript && Boolean(specId);
466
+ const problemClosureArgs = ['scripts/problem-closure-gate.js', '--stage', 'release', '--spec', specId, '--fail-on-block', '--json'];
467
+ if (verifyReportPath) {
468
+ problemClosureArgs.push('--verify-report', verifyReportPath);
469
+ }
470
+ steps.push({
471
+ id: 'problem-closure-gate',
472
+ name: 'problem closure gate (release)',
473
+ command: 'node',
474
+ args: problemClosureArgs,
475
+ required: Boolean(specId),
476
+ enabled: canRunProblemClosureGate,
477
+ skip_reason: canRunProblemClosureGate
478
+ ? ''
479
+ : (specId ? 'scripts/problem-closure-gate.js not found' : 'spec id unavailable for problem-closure gate')
480
+ });
481
+
442
482
  steps.push({
443
483
  id: 'npm-pack-dry-run',
444
484
  name: 'npm pack --dry-run',
@@ -709,6 +749,77 @@ async function readSpecDomainChain(projectPath, specId, fileSystem = fs) {
709
749
  }
710
750
  }
711
751
 
752
+ async function readSpecProblemContract(projectPath, specId, fileSystem = fs) {
753
+ const specRoot = path.join(projectPath, '.sce', 'specs', specId);
754
+ const contractPath = path.join(specRoot, DEFAULT_PROBLEM_CONTRACT_RELATIVE_PATH);
755
+ if (!await fileSystem.pathExists(contractPath)) {
756
+ return null;
757
+ }
758
+ try {
759
+ const payload = await fileSystem.readJson(contractPath);
760
+ const stat = await fileSystem.stat(contractPath);
761
+ return {
762
+ spec_id: specId,
763
+ contract_path: toRelativePosix(projectPath, contractPath),
764
+ payload,
765
+ updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
766
+ mtime_ms: Number(stat && stat.mtimeMs) || 0
767
+ };
768
+ } catch (_error) {
769
+ return null;
770
+ }
771
+ }
772
+
773
+ async function readGovernanceSignals(projectPath, fileSystem = fs) {
774
+ const reportPath = path.join(projectPath, DEFAULT_INTERACTIVE_GOVERNANCE_REPORT);
775
+ if (!await fileSystem.pathExists(reportPath)) {
776
+ return {
777
+ available: false,
778
+ report_path: null,
779
+ high_breach_count: 0,
780
+ medium_breach_count: 0
781
+ };
782
+ }
783
+ const payload = await fileSystem.readJson(reportPath).catch(() => null);
784
+ const summary = extractGovernanceBreachSignals(payload || {});
785
+ return {
786
+ ...summary,
787
+ report_path: toRelativePosix(projectPath, reportPath)
788
+ };
789
+ }
790
+
791
+ async function readVerifyReportSignals(projectPath, verifyReportPath = '', fileSystem = fs) {
792
+ const normalized = normalizeString(verifyReportPath);
793
+ if (!normalized) {
794
+ return {
795
+ available: false,
796
+ report_path: null,
797
+ passed: false,
798
+ failed_step_count: 0
799
+ };
800
+ }
801
+ const absolutePath = path.isAbsolute(normalized)
802
+ ? normalized
803
+ : path.join(projectPath, normalized);
804
+ if (!await fileSystem.pathExists(absolutePath)) {
805
+ return {
806
+ available: false,
807
+ report_path: normalized,
808
+ passed: false,
809
+ failed_step_count: 0
810
+ };
811
+ }
812
+ const payload = await fileSystem.readJson(absolutePath).catch(() => null);
813
+ const steps = Array.isArray(payload && payload.steps) ? payload.steps : [];
814
+ const failedStepCount = steps.filter((step) => normalizeString(step && step.status) === 'failed').length;
815
+ return {
816
+ available: true,
817
+ report_path: toRelativePosix(projectPath, absolutePath),
818
+ passed: payload && payload.passed === true && failedStepCount === 0,
819
+ failed_step_count: failedStepCount
820
+ };
821
+ }
822
+
712
823
  function normalizeChainList(value, limit = 5) {
713
824
  if (!Array.isArray(value)) {
714
825
  return [];
@@ -724,13 +835,97 @@ function normalizeChainList(value, limit = 5) {
724
835
  });
725
836
  }
726
837
 
838
+ function normalizeProblemContract(contract = {}, context = {}) {
839
+ const source = contract && typeof contract === 'object' ? contract : {};
840
+ const issueStatement = normalizeString(
841
+ source.issue_statement
842
+ || source.issue
843
+ || source.problem_statement
844
+ || context.problem_statement
845
+ || context.goal
846
+ );
847
+ const expectedOutcome = normalizeString(
848
+ source.expected_outcome
849
+ || source.expected
850
+ || source.success_criteria
851
+ || context.verification_plan
852
+ || (context.scene_id ? `Scene ${context.scene_id} reaches deterministic verification gates.` : '')
853
+ );
854
+ const reproductionSteps = normalizeChainList(
855
+ source.reproduction_steps || source.repro_steps || source.steps,
856
+ 20
857
+ );
858
+ const fallbackRepro = reproductionSteps.length > 0
859
+ ? reproductionSteps
860
+ : [
861
+ normalizeString(context.goal) || 'Reproduce the reported issue in the target scene.',
862
+ 'Capture logs and gate evidence for the failing path.'
863
+ ].filter(Boolean);
864
+ const forbiddenWorkarounds = normalizeChainList(
865
+ source.forbidden_workarounds || source.prohibited_workarounds || source.disallowed_workarounds,
866
+ 20
867
+ );
868
+ const fallbackForbidden = forbiddenWorkarounds.length > 0
869
+ ? forbiddenWorkarounds
870
+ : [
871
+ 'Do not bypass gates or tests.',
872
+ 'Do not silence runtime errors.'
873
+ ];
874
+
875
+ return {
876
+ issue_statement: issueStatement,
877
+ expected_outcome: expectedOutcome,
878
+ reproduction_steps: fallbackRepro,
879
+ impact_scope: normalizeString(source.impact_scope || source.scope || context.scene_id),
880
+ forbidden_workarounds: fallbackForbidden
881
+ };
882
+ }
883
+
884
+ function extractGovernanceBreachSignals(report = {}) {
885
+ if (!report || typeof report !== 'object') {
886
+ return {
887
+ available: false,
888
+ high_breach_count: 0,
889
+ medium_breach_count: 0
890
+ };
891
+ }
892
+ const alerts = Array.isArray(report.alerts) ? report.alerts : [];
893
+ let highBreachCount = 0;
894
+ let mediumBreachCount = 0;
895
+ for (const alert of alerts) {
896
+ const status = normalizeString(alert && alert.status).toLowerCase();
897
+ const severity = normalizeString(alert && alert.severity).toLowerCase();
898
+ if (status !== 'breach') {
899
+ continue;
900
+ }
901
+ if (severity === 'high') {
902
+ highBreachCount += 1;
903
+ } else if (severity === 'medium') {
904
+ mediumBreachCount += 1;
905
+ }
906
+ }
907
+ return {
908
+ available: true,
909
+ high_breach_count: highBreachCount,
910
+ medium_breach_count: mediumBreachCount
911
+ };
912
+ }
913
+
727
914
  function summarizeDomainChain(payload = {}) {
728
915
  const ontology = payload && typeof payload.ontology === 'object' ? payload.ontology : {};
916
+ const ontologyEvidence = payload && typeof payload.ontology_evidence === 'object' ? payload.ontology_evidence : {};
729
917
  const decisionPath = Array.isArray(payload.decision_execution_path) ? payload.decision_execution_path : [];
730
918
  const correctionLoop = payload && typeof payload.correction_loop === 'object' ? payload.correction_loop : {};
731
919
  const verification = payload && typeof payload.verification === 'object' ? payload.verification : {};
732
920
  const hypotheses = Array.isArray(payload.hypotheses) ? payload.hypotheses : [];
733
921
  const risks = Array.isArray(payload.risks) ? payload.risks : [];
922
+ const evidenceBindingCount = (
923
+ normalizeChainList(ontologyEvidence.entity, 50).length
924
+ + normalizeChainList(ontologyEvidence.relation, 50).length
925
+ + normalizeChainList(ontologyEvidence.business_rule, 50).length
926
+ + normalizeChainList(ontologyEvidence.decision_policy, 50).length
927
+ + normalizeChainList(ontologyEvidence.execution_flow, 50).length
928
+ );
734
929
 
735
930
  return {
736
931
  scene_id: normalizeString(payload.scene_id) || null,
@@ -746,6 +941,8 @@ function summarizeDomainChain(payload = {}) {
746
941
  hypothesis_count: hypotheses.length,
747
942
  risk_count: risks.length,
748
943
  decision_path_steps: decisionPath.length,
944
+ evidence_binding_count: evidenceBindingCount,
945
+ verification_plan: normalizeString(verification.plan) || null,
749
946
  correction_loop: {
750
947
  triggers: normalizeChainList(correctionLoop.triggers, 5),
751
948
  actions: normalizeChainList(correctionLoop.actions, 5)
@@ -780,7 +977,13 @@ function buildDomainChainRuntimeContext(payload = {}) {
780
977
  verification: {
781
978
  plan: normalizeString(payload?.verification?.plan) || null,
782
979
  gates: normalizeChainList(payload?.verification?.gates, 10)
783
- }
980
+ },
981
+ problem_contract: normalizeProblemContract(payload?.problem_contract || {}, {
982
+ scene_id: normalizeString(payload.scene_id) || '',
983
+ goal: normalizeString(payload?.problem?.statement) || '',
984
+ problem_statement: normalizeString(payload?.problem?.statement) || '',
985
+ verification_plan: normalizeString(payload?.verification?.plan) || ''
986
+ })
784
987
  };
785
988
  }
786
989
 
@@ -833,12 +1036,22 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
833
1036
  problemStatement: goal || `Studio scene cycle for ${sceneId}`
834
1037
  });
835
1038
  const chain = await readSpecDomainChain(projectPath, explicitSpec, fileSystem);
1039
+ const problemContract = await readSpecProblemContract(projectPath, explicitSpec, fileSystem);
836
1040
  if (!chain) {
837
1041
  return {
838
1042
  resolved: false,
839
1043
  source: 'explicit-spec',
840
1044
  spec_id: explicitSpec,
841
- reason: 'domain_chain_missing'
1045
+ reason: 'domain_chain_missing',
1046
+ problem_contract: problemContract
1047
+ ? normalizeProblemContract(problemContract.payload, {
1048
+ scene_id: sceneId,
1049
+ goal
1050
+ })
1051
+ : normalizeProblemContract({}, {
1052
+ scene_id: sceneId,
1053
+ goal
1054
+ })
842
1055
  };
843
1056
  }
844
1057
  return {
@@ -848,7 +1061,17 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
848
1061
  chain_path: chain.chain_path,
849
1062
  updated_at: chain.updated_at,
850
1063
  summary: summarizeDomainChain(chain.payload),
851
- context: buildDomainChainRuntimeContext(chain.payload)
1064
+ context: buildDomainChainRuntimeContext(chain.payload),
1065
+ problem_contract: normalizeProblemContract(
1066
+ problemContract && problemContract.payload ? problemContract.payload : chain.payload?.problem_contract || {},
1067
+ {
1068
+ scene_id: sceneId,
1069
+ goal,
1070
+ problem_statement: normalizeString(chain?.payload?.problem?.statement),
1071
+ verification_plan: normalizeString(chain?.payload?.verification?.plan)
1072
+ }
1073
+ ),
1074
+ problem_contract_path: problemContract ? problemContract.contract_path : null
852
1075
  };
853
1076
  }
854
1077
 
@@ -872,6 +1095,7 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
872
1095
  }
873
1096
 
874
1097
  const selected = candidates[0];
1098
+ const selectedContract = await readSpecProblemContract(projectPath, selected.spec_id, fileSystem);
875
1099
  return {
876
1100
  resolved: true,
877
1101
  source: candidates.length === 1 ? 'scene-auto-single' : 'scene-auto-latest',
@@ -885,7 +1109,17 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
885
1109
  updated_at: item.updated_at
886
1110
  })),
887
1111
  summary: summarizeDomainChain(selected.payload),
888
- context: buildDomainChainRuntimeContext(selected.payload)
1112
+ context: buildDomainChainRuntimeContext(selected.payload),
1113
+ problem_contract: normalizeProblemContract(
1114
+ selectedContract && selectedContract.payload ? selectedContract.payload : selected.payload?.problem_contract || {},
1115
+ {
1116
+ scene_id: sceneId,
1117
+ goal,
1118
+ problem_statement: normalizeString(selected?.payload?.problem?.statement),
1119
+ verification_plan: normalizeString(selected?.payload?.verification?.plan)
1120
+ }
1121
+ ),
1122
+ problem_contract_path: selectedContract ? selectedContract.contract_path : null
889
1123
  };
890
1124
  }
891
1125
 
@@ -948,6 +1182,14 @@ function buildJobDomainChainMetadata(job = {}) {
948
1182
  : null;
949
1183
  const summary = domainChain && domainChain.summary ? domainChain.summary : null;
950
1184
  const context = domainChain && domainChain.context ? domainChain.context : null;
1185
+ const problemContract = job?.source?.problem_contract && typeof job.source.problem_contract === 'object'
1186
+ ? job.source.problem_contract
1187
+ : normalizeProblemContract(context && context.problem_contract ? context.problem_contract : {}, {
1188
+ scene_id: normalizeString(job?.scene?.id),
1189
+ goal: normalizeString(job?.source?.goal),
1190
+ problem_statement: normalizeString(summary && summary.problem_statement),
1191
+ verification_plan: normalizeString(summary && summary.verification_plan)
1192
+ });
951
1193
  return {
952
1194
  resolved: domainChain && domainChain.resolved === true,
953
1195
  source: domainChain && domainChain.source ? domainChain.source : 'none',
@@ -956,17 +1198,145 @@ function buildJobDomainChainMetadata(job = {}) {
956
1198
  reason: domainChain && domainChain.reason ? domainChain.reason : null,
957
1199
  decision_path_steps: summary ? Number(summary.decision_path_steps || 0) : 0,
958
1200
  risk_count: summary ? Number(summary.risk_count || 0) : 0,
1201
+ evidence_binding_count: summary ? Number(summary.evidence_binding_count || 0) : 0,
959
1202
  correction_triggers: summary && summary.correction_loop
960
1203
  ? normalizeChainList(summary.correction_loop.triggers, 10)
961
1204
  : [],
962
1205
  verification_gates: summary
963
1206
  ? normalizeChainList(summary.verification_gates, 10)
964
1207
  : [],
1208
+ problem_contract: problemContract,
965
1209
  summary: summary || null,
966
1210
  context: context || null
967
1211
  };
968
1212
  }
969
1213
 
1214
+ function summarizeProblemEvaluation(evaluation = {}) {
1215
+ return {
1216
+ passed: evaluation.passed === true,
1217
+ blocked: evaluation.blocked === true,
1218
+ confidence_score: Number(evaluation.confidence_score || 0),
1219
+ risk_level: normalizeString(evaluation?.dimensions?.risk?.level) || 'low',
1220
+ strategy: normalizeString(evaluation?.dimensions?.strategy?.strategy) || 'direct-execution',
1221
+ contract_score: Number(evaluation?.dimensions?.problem_contract?.score || 0),
1222
+ ontology_score: Number(evaluation?.dimensions?.ontology_alignment?.score || 0),
1223
+ convergence_score: Number(evaluation?.dimensions?.convergence?.score || 0),
1224
+ contract_missing: Array.isArray(evaluation?.dimensions?.problem_contract?.missing)
1225
+ ? evaluation.dimensions.problem_contract.missing
1226
+ : [],
1227
+ ontology_missing_axes: Array.isArray(evaluation?.dimensions?.ontology_alignment?.missing_axes)
1228
+ ? evaluation.dimensions.ontology_alignment.missing_axes
1229
+ : [],
1230
+ convergence_missing: Array.isArray(evaluation?.dimensions?.convergence?.missing)
1231
+ ? evaluation.dimensions.convergence.missing
1232
+ : [],
1233
+ blockers: Array.isArray(evaluation.blockers) ? evaluation.blockers : [],
1234
+ warnings: Array.isArray(evaluation.warnings) ? evaluation.warnings : [],
1235
+ recommendations: Array.isArray(evaluation.recommendations) ? evaluation.recommendations : [],
1236
+ report_file: normalizeString(evaluation.report_file) || null
1237
+ };
1238
+ }
1239
+
1240
+ function deriveGateSignals(steps = []) {
1241
+ const normalized = Array.isArray(steps) ? steps : [];
1242
+ const requiredTotal = normalized.filter((step) => step && step.required !== false).length;
1243
+ const requiredEnabled = normalized.filter((step) => step && step.required !== false && step.enabled !== false).length;
1244
+ return {
1245
+ required_total: requiredTotal,
1246
+ required_enabled: requiredEnabled,
1247
+ required_missing: Math.max(0, requiredTotal - requiredEnabled)
1248
+ };
1249
+ }
1250
+
1251
+ function buildStageReadiness(job = {}, stage = '', overrides = {}) {
1252
+ const normalizedStage = normalizeString(stage).toLowerCase();
1253
+ const patchBundleReady = normalizeString(job?.artifacts?.patch_bundle_id).length > 0;
1254
+ const verifyReportReady = normalizeString(job?.artifacts?.verify_report).length > 0;
1255
+ const readiness = {
1256
+ prerequisites_ready: true,
1257
+ rollback_ready: isStageCompleted(job, 'apply'),
1258
+ patch_bundle_ready: patchBundleReady,
1259
+ verify_report_ready: verifyReportReady
1260
+ };
1261
+
1262
+ if (normalizedStage === 'generate') {
1263
+ readiness.prerequisites_ready = isStageCompleted(job, 'plan');
1264
+ } else if (normalizedStage === 'apply') {
1265
+ readiness.prerequisites_ready = isStageCompleted(job, 'generate');
1266
+ } else if (normalizedStage === 'verify') {
1267
+ readiness.prerequisites_ready = isStageCompleted(job, 'apply');
1268
+ } else if (normalizedStage === 'release') {
1269
+ readiness.prerequisites_ready = isStageCompleted(job, 'verify');
1270
+ }
1271
+
1272
+ return {
1273
+ ...readiness,
1274
+ ...overrides
1275
+ };
1276
+ }
1277
+
1278
+ function assignProblemEvalArtifact(job = {}, stage = '', evaluation = {}) {
1279
+ const normalizedStage = normalizeString(stage).toLowerCase();
1280
+ if (!normalizedStage) {
1281
+ return;
1282
+ }
1283
+ job.artifacts = job.artifacts || {};
1284
+ const reports = job.artifacts.problem_eval_reports && typeof job.artifacts.problem_eval_reports === 'object'
1285
+ ? job.artifacts.problem_eval_reports
1286
+ : {};
1287
+ reports[normalizedStage] = normalizeString(evaluation.report_file) || null;
1288
+ job.artifacts.problem_eval_reports = reports;
1289
+ }
1290
+
1291
+ async function enforceProblemEvaluationForStage(job = {}, stage = '', context = {}, dependencies = {}) {
1292
+ const projectPath = dependencies.projectPath || process.cwd();
1293
+ const fileSystem = dependencies.fileSystem || fs;
1294
+ const evaluation = await runProblemEvaluation({
1295
+ stage,
1296
+ job_id: normalizeString(job.job_id),
1297
+ scene_id: normalizeString(context.scene_id || job?.scene?.id),
1298
+ spec_id: normalizeString(context.spec_id || job?.source?.spec_id || job?.scene?.spec_id),
1299
+ goal: normalizeString(context.goal || job?.source?.goal),
1300
+ release_channel: normalizeString(context.release_channel || ''),
1301
+ domain_chain: context.domain_chain || (job?.source?.domain_chain || {}),
1302
+ problem_contract: context.problem_contract || job?.source?.problem_contract || {},
1303
+ related_specs_count: Number(context.related_specs_count || job?.source?.related_specs?.total_candidates || 0),
1304
+ stage_readiness: context.stage_readiness || buildStageReadiness(job, stage),
1305
+ gate_signals: context.gate_signals || {}
1306
+ }, {
1307
+ projectPath,
1308
+ fileSystem,
1309
+ env: dependencies.env,
1310
+ writeReport: true
1311
+ });
1312
+ assignProblemEvalArtifact(job, stage, evaluation);
1313
+ return evaluation;
1314
+ }
1315
+
1316
+ async function markStudioStageBlockedByProblemEval(paths, job, stageName, evaluation, fileSystem = fs) {
1317
+ const summary = summarizeProblemEvaluation(evaluation);
1318
+ job.status = `${stageName}_blocked`;
1319
+ job.updated_at = nowIso();
1320
+ job.stages = job.stages || createStageState();
1321
+ job.stages[stageName] = {
1322
+ status: 'blocked',
1323
+ completed_at: null,
1324
+ metadata: {
1325
+ problem_evaluation: summary
1326
+ }
1327
+ };
1328
+ await saveJob(paths, job, fileSystem);
1329
+ await appendStudioEvent(paths, job, `stage.${stageName}.blocked`, {
1330
+ problem_evaluation: summary
1331
+ }, fileSystem);
1332
+ await writeLatestJob(paths, job.job_id, fileSystem);
1333
+
1334
+ const reason = summary.blockers.length > 0
1335
+ ? summary.blockers.join(', ')
1336
+ : 'problem-evaluation-policy';
1337
+ throw new Error(`studio ${stageName} blocked by problem evaluation: ${reason}`);
1338
+ }
1339
+
970
1340
  async function runStudioPlanCommand(options = {}, dependencies = {}) {
971
1341
  const projectPath = dependencies.projectPath || process.cwd();
972
1342
  const fileSystem = dependencies.fileSystem || fs;
@@ -1013,6 +1383,63 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1013
1383
 
1014
1384
  const jobId = normalizeString(options.job) || createJobId();
1015
1385
  const now = nowIso();
1386
+ const problemContract = normalizeProblemContract(
1387
+ domainChainBinding.problem_contract || {},
1388
+ {
1389
+ scene_id: sceneId,
1390
+ goal: normalizeString(options.goal),
1391
+ problem_statement: normalizeString(domainChainBinding?.summary?.problem_statement),
1392
+ verification_plan: normalizeString(domainChainBinding?.summary?.verification_plan)
1393
+ }
1394
+ );
1395
+ const planShadowJob = {
1396
+ job_id: jobId,
1397
+ scene: {
1398
+ id: sceneId,
1399
+ spec_id: domainChainBinding.spec_id || specId || null
1400
+ },
1401
+ source: {
1402
+ goal: normalizeString(options.goal) || null,
1403
+ spec_id: domainChainBinding.spec_id || specId || null,
1404
+ problem_contract: problemContract,
1405
+ problem_contract_path: domainChainBinding.problem_contract_path || null,
1406
+ domain_chain: {
1407
+ resolved: domainChainBinding.resolved === true,
1408
+ summary: domainChainBinding.summary || null
1409
+ },
1410
+ related_specs: {
1411
+ total_candidates: Number(relatedSpecLookup.total_candidates || 0)
1412
+ }
1413
+ },
1414
+ artifacts: {}
1415
+ };
1416
+ const planProblemEvaluation = await enforceProblemEvaluationForStage(planShadowJob, 'plan', {
1417
+ scene_id: sceneId,
1418
+ spec_id: domainChainBinding.spec_id || specId || null,
1419
+ goal: normalizeString(options.goal) || null,
1420
+ problem_contract: problemContract,
1421
+ domain_chain: {
1422
+ resolved: domainChainBinding.resolved === true,
1423
+ summary: domainChainBinding.summary || null
1424
+ },
1425
+ related_specs_count: Number(relatedSpecLookup.total_candidates || 0),
1426
+ stage_readiness: {
1427
+ prerequisites_ready: true,
1428
+ rollback_ready: false,
1429
+ patch_bundle_ready: false,
1430
+ verify_report_ready: false
1431
+ }
1432
+ }, {
1433
+ projectPath,
1434
+ fileSystem,
1435
+ env: dependencies.env
1436
+ });
1437
+ if (!planProblemEvaluation.passed) {
1438
+ const blockers = Array.isArray(planProblemEvaluation.blockers) && planProblemEvaluation.blockers.length > 0
1439
+ ? planProblemEvaluation.blockers.join(', ')
1440
+ : 'problem-evaluation-policy';
1441
+ throw new Error(`studio plan blocked by problem evaluation: ${blockers}`);
1442
+ }
1016
1443
  const stages = createStageState();
1017
1444
  const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
1018
1445
  const sceneSessionBinding = await sessionStore.beginSceneSession({
@@ -1035,6 +1462,8 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1035
1462
  domain_chain_path: domainChainBinding.chain_path || null,
1036
1463
  domain_chain_summary: domainChainBinding.summary || null,
1037
1464
  domain_chain_reason: domainChainBinding.reason || null,
1465
+ problem_contract: problemContract,
1466
+ problem_evaluation: summarizeProblemEvaluation(planProblemEvaluation),
1038
1467
  related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
1039
1468
  related_specs_top: relatedSpecItems
1040
1469
  }
@@ -1050,6 +1479,8 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1050
1479
  from_chat: fromChat,
1051
1480
  goal: normalizeString(options.goal) || null,
1052
1481
  spec_id: domainChainBinding.spec_id || specId || null,
1482
+ problem_contract: problemContract,
1483
+ problem_contract_path: domainChainBinding.problem_contract_path || null,
1053
1484
  domain_chain: {
1054
1485
  resolved: domainChainBinding.resolved === true,
1055
1486
  source: domainChainBinding.source || 'none',
@@ -1088,7 +1519,10 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1088
1519
  artifacts: {
1089
1520
  patch_bundle_id: null,
1090
1521
  verify_report: null,
1091
- release_ref: null
1522
+ release_ref: null,
1523
+ problem_eval_reports: {
1524
+ plan: normalizeString(planProblemEvaluation.report_file) || null
1525
+ }
1092
1526
  }
1093
1527
  };
1094
1528
 
@@ -1104,6 +1538,8 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1104
1538
  domain_chain_source: domainChainBinding.source || 'none',
1105
1539
  domain_chain_spec_id: domainChainBinding.spec_id || null,
1106
1540
  domain_chain_path: domainChainBinding.chain_path || null,
1541
+ problem_contract: problemContract,
1542
+ problem_evaluation: summarizeProblemEvaluation(planProblemEvaluation),
1107
1543
  related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
1108
1544
  related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
1109
1545
  }, fileSystem);
@@ -1138,6 +1574,21 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
1138
1574
  throw new Error(`Scene mismatch: planned scene is "${jobSceneId}" but --scene provided "${sceneArg}"`);
1139
1575
  }
1140
1576
  const sceneId = sceneArg || jobSceneId;
1577
+ const generateProblemEvaluation = await enforceProblemEvaluationForStage(job, 'generate', {
1578
+ scene_id: sceneId,
1579
+ spec_id: normalizeString(job?.source?.spec_id) || normalizeString(job?.scene?.spec_id) || null,
1580
+ goal: normalizeString(job?.source?.goal),
1581
+ domain_chain: job?.source?.domain_chain || {},
1582
+ related_specs_count: Number(job?.source?.related_specs?.total_candidates || 0),
1583
+ stage_readiness: buildStageReadiness(job, 'generate')
1584
+ }, {
1585
+ projectPath,
1586
+ fileSystem,
1587
+ env: dependencies.env
1588
+ });
1589
+ if (!generateProblemEvaluation.passed) {
1590
+ await markStudioStageBlockedByProblemEval(paths, job, 'generate', generateProblemEvaluation, fileSystem);
1591
+ }
1141
1592
  const patchBundleId = normalizeString(options.patchBundle) || `patch-${sceneId}-${Date.now()}`;
1142
1593
  const domainChainMetadata = buildJobDomainChainMetadata(job);
1143
1594
  const generateReportPath = `${STUDIO_REPORTS_DIR}/generate-${job.job_id}.json`;
@@ -1167,6 +1618,7 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
1167
1618
  scene_id: sceneId,
1168
1619
  target: job.target,
1169
1620
  patch_bundle_id: patchBundleId,
1621
+ problem_evaluation: summarizeProblemEvaluation(generateProblemEvaluation),
1170
1622
  domain_chain: domainChainMetadata,
1171
1623
  report: generateReportPath
1172
1624
  });
@@ -1176,6 +1628,7 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
1176
1628
  scene_id: sceneId,
1177
1629
  target: job.target,
1178
1630
  patch_bundle_id: patchBundleId,
1631
+ problem_evaluation: summarizeProblemEvaluation(generateProblemEvaluation),
1179
1632
  domain_chain: domainChainMetadata,
1180
1633
  report: generateReportPath
1181
1634
  }, fileSystem);
@@ -1211,6 +1664,23 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
1211
1664
  if (!patchBundleId) {
1212
1665
  throw new Error('--patch-bundle is required (or generate stage must provide one)');
1213
1666
  }
1667
+ const applyProblemEvaluation = await enforceProblemEvaluationForStage(job, 'apply', {
1668
+ scene_id: normalizeString(job?.scene?.id),
1669
+ spec_id: normalizeString(job?.source?.spec_id) || normalizeString(job?.scene?.spec_id) || null,
1670
+ goal: normalizeString(job?.source?.goal),
1671
+ domain_chain: job?.source?.domain_chain || {},
1672
+ related_specs_count: Number(job?.source?.related_specs?.total_candidates || 0),
1673
+ stage_readiness: buildStageReadiness(job, 'apply', {
1674
+ patch_bundle_ready: normalizeString(patchBundleId).length > 0
1675
+ })
1676
+ }, {
1677
+ projectPath,
1678
+ fileSystem,
1679
+ env: dependencies.env
1680
+ });
1681
+ if (!applyProblemEvaluation.passed) {
1682
+ await markStudioStageBlockedByProblemEval(paths, job, 'apply', applyProblemEvaluation, fileSystem);
1683
+ }
1214
1684
 
1215
1685
  job.status = 'applied';
1216
1686
  job.artifacts = job.artifacts || {};
@@ -1219,13 +1689,15 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
1219
1689
 
1220
1690
  ensureStageCompleted(job, 'apply', {
1221
1691
  patch_bundle_id: patchBundleId,
1222
- auth_required: authResult.required
1692
+ auth_required: authResult.required,
1693
+ problem_evaluation: summarizeProblemEvaluation(applyProblemEvaluation)
1223
1694
  });
1224
1695
 
1225
1696
  await saveJob(paths, job, fileSystem);
1226
1697
  await appendStudioEvent(paths, job, 'stage.apply.completed', {
1227
1698
  patch_bundle_id: patchBundleId,
1228
- auth_required: authResult.required
1699
+ auth_required: authResult.required,
1700
+ problem_evaluation: summarizeProblemEvaluation(applyProblemEvaluation)
1229
1701
  }, fileSystem);
1230
1702
  await writeLatestJob(paths, jobId, fileSystem);
1231
1703
 
@@ -1257,8 +1729,29 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1257
1729
  const autoErrorbookRecords = [];
1258
1730
  const gateSteps = await buildVerifyGateSteps({ profile }, {
1259
1731
  projectPath,
1260
- fileSystem
1732
+ fileSystem,
1733
+ specId: normalizeString(domainChainMetadata.spec_id) || null
1734
+ });
1735
+ const verifyProblemEvaluation = await enforceProblemEvaluationForStage(job, 'verify', {
1736
+ scene_id: normalizeString(job?.scene?.id),
1737
+ spec_id: normalizeString(domainChainMetadata.spec_id) || normalizeString(job?.source?.spec_id),
1738
+ goal: normalizeString(job?.source?.goal),
1739
+ domain_chain: job?.source?.domain_chain || {},
1740
+ problem_contract: job?.source?.problem_contract || {},
1741
+ related_specs_count: Number(job?.source?.related_specs?.total_candidates || 0),
1742
+ stage_readiness: buildStageReadiness(job, 'verify', {
1743
+ gate_required_ready: deriveGateSignals(gateSteps).required_missing === 0,
1744
+ convergence_strict: profile === 'strict'
1745
+ }),
1746
+ gate_signals: deriveGateSignals(gateSteps)
1747
+ }, {
1748
+ projectPath,
1749
+ fileSystem,
1750
+ env: dependencies.env
1261
1751
  });
1752
+ if (!verifyProblemEvaluation.passed) {
1753
+ await markStudioStageBlockedByProblemEval(paths, job, 'verify', verifyProblemEvaluation, fileSystem);
1754
+ }
1262
1755
  const gateResult = await executeGateSteps(gateSteps, {
1263
1756
  projectPath,
1264
1757
  commandRunner: dependencies.commandRunner,
@@ -1296,6 +1789,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1296
1789
  passed: gateResult.passed,
1297
1790
  steps: gateResult.steps,
1298
1791
  domain_chain: domainChainMetadata,
1792
+ problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1299
1793
  auto_errorbook_records: autoErrorbookRecords
1300
1794
  };
1301
1795
 
@@ -1314,6 +1808,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1314
1808
  profile,
1315
1809
  passed: false,
1316
1810
  report: verifyReportPath,
1811
+ problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1317
1812
  domain_chain: domainChainMetadata,
1318
1813
  auto_errorbook_records: autoErrorbookRecords
1319
1814
  }
@@ -1322,6 +1817,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1322
1817
  await appendStudioEvent(paths, job, 'stage.verify.failed', {
1323
1818
  profile,
1324
1819
  report: verifyReportPath,
1820
+ problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1325
1821
  domain_chain: domainChainMetadata,
1326
1822
  auto_errorbook_records: autoErrorbookRecords
1327
1823
  }, fileSystem);
@@ -1334,6 +1830,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1334
1830
  profile,
1335
1831
  passed: true,
1336
1832
  report: verifyReportPath,
1833
+ problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1337
1834
  domain_chain: domainChainMetadata,
1338
1835
  auto_errorbook_records: autoErrorbookRecords
1339
1836
  });
@@ -1343,6 +1840,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1343
1840
  profile,
1344
1841
  passed: true,
1345
1842
  report: verifyReportPath,
1843
+ problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1346
1844
  domain_chain: domainChainMetadata,
1347
1845
  auto_errorbook_records: autoErrorbookRecords
1348
1846
  }, fileSystem);
@@ -1386,10 +1884,46 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1386
1884
  const releaseStartedAt = nowIso();
1387
1885
  const domainChainMetadata = buildJobDomainChainMetadata(job);
1388
1886
  const autoErrorbookRecords = [];
1389
- const gateSteps = await buildReleaseGateSteps({ profile }, {
1887
+ const verifyReportSignals = await readVerifyReportSignals(
1390
1888
  projectPath,
1889
+ normalizeString(job?.artifacts?.verify_report),
1391
1890
  fileSystem
1891
+ );
1892
+ const governanceSignals = await readGovernanceSignals(projectPath, fileSystem);
1893
+ const gateSteps = await buildReleaseGateSteps({ profile }, {
1894
+ projectPath,
1895
+ fileSystem,
1896
+ specId: normalizeString(domainChainMetadata.spec_id) || null,
1897
+ verifyReportPath: normalizeString(job?.artifacts?.verify_report) || null
1392
1898
  });
1899
+ const releaseGateSignals = deriveGateSignals(gateSteps);
1900
+ const releaseProblemEvaluation = await enforceProblemEvaluationForStage(job, 'release', {
1901
+ scene_id: normalizeString(job?.scene?.id),
1902
+ spec_id: normalizeString(domainChainMetadata.spec_id) || normalizeString(job?.source?.spec_id),
1903
+ goal: normalizeString(job?.source?.goal),
1904
+ release_channel: channel,
1905
+ domain_chain: job?.source?.domain_chain || {},
1906
+ problem_contract: job?.source?.problem_contract || {},
1907
+ related_specs_count: Number(job?.source?.related_specs?.total_candidates || 0),
1908
+ stage_readiness: buildStageReadiness(job, 'release', {
1909
+ gate_required_ready: releaseGateSignals.required_missing === 0,
1910
+ convergence_strict: profile === 'strict',
1911
+ verify_stage_passed: isStageCompleted(job, 'verify'),
1912
+ verify_report_ready: verifyReportSignals.available,
1913
+ verify_report_passed: verifyReportSignals.passed,
1914
+ regression_passed: verifyReportSignals.passed && verifyReportSignals.failed_step_count === 0,
1915
+ governance_report_ready: governanceSignals.available,
1916
+ high_alert_count: Number(governanceSignals.high_breach_count || 0)
1917
+ }),
1918
+ gate_signals: releaseGateSignals
1919
+ }, {
1920
+ projectPath,
1921
+ fileSystem,
1922
+ env: dependencies.env
1923
+ });
1924
+ if (!releaseProblemEvaluation.passed) {
1925
+ await markStudioStageBlockedByProblemEval(paths, job, 'release', releaseProblemEvaluation, fileSystem);
1926
+ }
1393
1927
  const gateResult = await executeGateSteps(gateSteps, {
1394
1928
  projectPath,
1395
1929
  commandRunner: dependencies.commandRunner,
@@ -1429,6 +1963,9 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1429
1963
  passed: gateResult.passed,
1430
1964
  steps: gateResult.steps,
1431
1965
  domain_chain: domainChainMetadata,
1966
+ verify_signals: verifyReportSignals,
1967
+ governance_signals: governanceSignals,
1968
+ problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
1432
1969
  auto_errorbook_records: autoErrorbookRecords
1433
1970
  };
1434
1971
 
@@ -1450,6 +1987,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1450
1987
  passed: false,
1451
1988
  report: releaseReportPath,
1452
1989
  auth_required: authResult.required,
1990
+ problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
1453
1991
  domain_chain: domainChainMetadata,
1454
1992
  auto_errorbook_records: autoErrorbookRecords
1455
1993
  }
@@ -1460,6 +1998,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1460
1998
  release_ref: releaseRef,
1461
1999
  report: releaseReportPath,
1462
2000
  auth_required: authResult.required,
2001
+ problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
1463
2002
  domain_chain: domainChainMetadata,
1464
2003
  auto_errorbook_records: autoErrorbookRecords
1465
2004
  }, fileSystem);
@@ -1473,6 +2012,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1473
2012
  release_ref: releaseRef,
1474
2013
  report: releaseReportPath,
1475
2014
  auth_required: authResult.required,
2015
+ problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
1476
2016
  domain_chain: domainChainMetadata,
1477
2017
  auto_errorbook_records: autoErrorbookRecords
1478
2018
  });
@@ -1504,6 +2044,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1504
2044
  release_ref: releaseRef,
1505
2045
  report: releaseReportPath,
1506
2046
  auth_required: authResult.required,
2047
+ problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
1507
2048
  domain_chain: domainChainMetadata,
1508
2049
  auto_errorbook_records: autoErrorbookRecords
1509
2050
  }, fileSystem);