scene-capability-engine 3.4.5 → 3.5.0

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.
@@ -6,6 +6,7 @@ const DEFAULT_POLICY_PATH = '.sce/config/problem-eval-policy.json';
6
6
  const DEFAULT_REPORT_DIR = '.sce/reports/problem-eval';
7
7
  const STUDIO_STAGES = Object.freeze(['plan', 'generate', 'apply', 'verify', 'release']);
8
8
  const DEBUG_EVIDENCE_TAGS = Object.freeze(['debug-evidence', 'diagnostic-evidence', 'debug-log']);
9
+ const ONTOLOGY_AXES = Object.freeze(['entity', 'relation', 'business_rule', 'decision_policy', 'execution_flow']);
9
10
 
10
11
  const DEFAULT_PROBLEM_EVAL_POLICY = Object.freeze({
11
12
  schema_version: '1.0',
@@ -32,7 +33,19 @@ const DEFAULT_PROBLEM_EVAL_POLICY = Object.freeze({
32
33
  'compliance',
33
34
  'data-loss'
34
35
  ],
35
- recommendation_limit: 6
36
+ recommendation_limit: 6,
37
+ max_failed_rounds_before_debug: 2,
38
+ problem_contract_required_stages: [...STUDIO_STAGES],
39
+ problem_contract_block_stages: ['plan', 'apply', 'release'],
40
+ ontology_alignment_required_stages: [...STUDIO_STAGES],
41
+ ontology_alignment_block_stages: ['apply', 'release'],
42
+ ontology_required_axes: [...ONTOLOGY_AXES],
43
+ require_ontology_evidence_binding: true,
44
+ ontology_evidence_min_bindings: 1,
45
+ convergence_required_stages: ['verify', 'release'],
46
+ convergence_block_stages: ['release'],
47
+ release_block_on_high_alerts: true,
48
+ release_require_governance_report: false
36
49
  });
37
50
 
38
51
  function normalizeText(value) {
@@ -84,6 +97,36 @@ function normalizeArray(value = []) {
84
97
  return value.map((item) => normalizeText(item)).filter(Boolean);
85
98
  }
86
99
 
100
+ function normalizeTextList(value = [], limit = 20) {
101
+ if (!Array.isArray(value)) {
102
+ return [];
103
+ }
104
+ return value
105
+ .map((item) => {
106
+ if (typeof item === 'string') {
107
+ return normalizeText(item);
108
+ }
109
+ if (item && typeof item === 'object') {
110
+ return normalizeText(item.step || item.description || item.id || item.name || '');
111
+ }
112
+ return '';
113
+ })
114
+ .filter(Boolean)
115
+ .slice(0, limit);
116
+ }
117
+
118
+ function normalizeStageArray(value, fallback = []) {
119
+ const candidates = normalizeArray(value).map((item) => item.toLowerCase());
120
+ const filtered = candidates.filter((item, index) => STUDIO_STAGES.includes(item) && candidates.indexOf(item) === index);
121
+ return filtered.length > 0 ? filtered : [...fallback];
122
+ }
123
+
124
+ function normalizeOntologyAxisArray(value, fallback = []) {
125
+ const candidates = normalizeArray(value).map((item) => item.toLowerCase());
126
+ const filtered = candidates.filter((item, index) => ONTOLOGY_AXES.includes(item) && candidates.indexOf(item) === index);
127
+ return filtered.length > 0 ? filtered : [...fallback];
128
+ }
129
+
87
130
  function normalizeIncidentState(value, fallback = 'open') {
88
131
  const normalized = normalizeLowerText(value);
89
132
  if (!normalized) {
@@ -133,12 +176,14 @@ function normalizePolicy(policy = {}, env = process.env) {
133
176
  schema_version: normalizeText(policy.schema_version) || DEFAULT_PROBLEM_EVAL_POLICY.schema_version,
134
177
  enabled,
135
178
  mode: mode || DEFAULT_PROBLEM_EVAL_POLICY.mode,
136
- enforce_on_stages: normalizeArray(policy.enforce_on_stages).length > 0
137
- ? normalizeArray(policy.enforce_on_stages).map((item) => item.toLowerCase())
138
- : [...DEFAULT_PROBLEM_EVAL_POLICY.enforce_on_stages],
139
- block_on_stages: normalizeArray(policy.block_on_stages).length > 0
140
- ? normalizeArray(policy.block_on_stages).map((item) => item.toLowerCase())
141
- : [...DEFAULT_PROBLEM_EVAL_POLICY.block_on_stages],
179
+ enforce_on_stages: normalizeStageArray(
180
+ policy.enforce_on_stages,
181
+ DEFAULT_PROBLEM_EVAL_POLICY.enforce_on_stages
182
+ ),
183
+ block_on_stages: normalizeStageArray(
184
+ policy.block_on_stages,
185
+ DEFAULT_PROBLEM_EVAL_POLICY.block_on_stages
186
+ ),
142
187
  min_confidence_by_stage: {
143
188
  plan: normalizeInteger(minByStage.plan, DEFAULT_PROBLEM_EVAL_POLICY.min_confidence_by_stage.plan, 0, 100),
144
189
  generate: normalizeInteger(minByStage.generate, DEFAULT_PROBLEM_EVAL_POLICY.min_confidence_by_stage.generate, 0, 100),
@@ -158,6 +203,58 @@ function normalizePolicy(policy = {}, env = process.env) {
158
203
  DEFAULT_PROBLEM_EVAL_POLICY.recommendation_limit,
159
204
  1,
160
205
  20
206
+ ),
207
+ max_failed_rounds_before_debug: normalizeInteger(
208
+ policy.max_failed_rounds_before_debug,
209
+ DEFAULT_PROBLEM_EVAL_POLICY.max_failed_rounds_before_debug,
210
+ 1,
211
+ 10
212
+ ),
213
+ problem_contract_required_stages: normalizeStageArray(
214
+ policy.problem_contract_required_stages,
215
+ DEFAULT_PROBLEM_EVAL_POLICY.problem_contract_required_stages
216
+ ),
217
+ problem_contract_block_stages: normalizeStageArray(
218
+ policy.problem_contract_block_stages,
219
+ DEFAULT_PROBLEM_EVAL_POLICY.problem_contract_block_stages
220
+ ),
221
+ ontology_alignment_required_stages: normalizeStageArray(
222
+ policy.ontology_alignment_required_stages,
223
+ DEFAULT_PROBLEM_EVAL_POLICY.ontology_alignment_required_stages
224
+ ),
225
+ ontology_alignment_block_stages: normalizeStageArray(
226
+ policy.ontology_alignment_block_stages,
227
+ DEFAULT_PROBLEM_EVAL_POLICY.ontology_alignment_block_stages
228
+ ),
229
+ ontology_required_axes: normalizeOntologyAxisArray(
230
+ policy.ontology_required_axes,
231
+ DEFAULT_PROBLEM_EVAL_POLICY.ontology_required_axes
232
+ ),
233
+ require_ontology_evidence_binding: normalizeBoolean(
234
+ policy.require_ontology_evidence_binding,
235
+ DEFAULT_PROBLEM_EVAL_POLICY.require_ontology_evidence_binding
236
+ ),
237
+ ontology_evidence_min_bindings: normalizeInteger(
238
+ policy.ontology_evidence_min_bindings,
239
+ DEFAULT_PROBLEM_EVAL_POLICY.ontology_evidence_min_bindings,
240
+ 0,
241
+ 20
242
+ ),
243
+ convergence_required_stages: normalizeStageArray(
244
+ policy.convergence_required_stages,
245
+ DEFAULT_PROBLEM_EVAL_POLICY.convergence_required_stages
246
+ ),
247
+ convergence_block_stages: normalizeStageArray(
248
+ policy.convergence_block_stages,
249
+ DEFAULT_PROBLEM_EVAL_POLICY.convergence_block_stages
250
+ ),
251
+ release_block_on_high_alerts: normalizeBoolean(
252
+ policy.release_block_on_high_alerts,
253
+ DEFAULT_PROBLEM_EVAL_POLICY.release_block_on_high_alerts
254
+ ),
255
+ release_require_governance_report: normalizeBoolean(
256
+ policy.release_require_governance_report,
257
+ DEFAULT_PROBLEM_EVAL_POLICY.release_require_governance_report
161
258
  )
162
259
  };
163
260
 
@@ -226,7 +323,8 @@ function scoreRisk(stage, text, policy, incidentSignals = {}, releaseChannel = '
226
323
  score += Math.min(20, openIncidents * 3);
227
324
  signals.push(`open-incidents:${openIncidents}`);
228
325
  }
229
- if (maxAttempts >= 3) {
326
+ const debugRoundThreshold = Number(policy.max_failed_rounds_before_debug || 2) + 1;
327
+ if (maxAttempts >= debugRoundThreshold) {
230
328
  score += 16;
231
329
  signals.push(`repeat-attempts:${maxAttempts}`);
232
330
  }
@@ -242,15 +340,262 @@ function scoreRisk(stage, text, policy, incidentSignals = {}, releaseChannel = '
242
340
  return { score, level, signals };
243
341
  }
244
342
 
343
+ function stageInPolicy(stage, list = []) {
344
+ return Array.isArray(list) && list.includes(stage);
345
+ }
346
+
347
+ function countOntologyEvidenceBindings(domainChain = {}, summary = {}) {
348
+ const summaryCount = Number(summary.evidence_binding_count || 0);
349
+ if (Number.isFinite(summaryCount) && summaryCount > 0) {
350
+ return summaryCount;
351
+ }
352
+
353
+ let total = 0;
354
+ const explicitBindings = Array.isArray(domainChain.evidence_bindings)
355
+ ? domainChain.evidence_bindings.length
356
+ : 0;
357
+ total += explicitBindings;
358
+
359
+ const ontologyEvidence = domainChain.ontology_evidence && typeof domainChain.ontology_evidence === 'object'
360
+ ? domainChain.ontology_evidence
361
+ : {};
362
+ for (const axis of ONTOLOGY_AXES) {
363
+ total += normalizeTextList(ontologyEvidence[axis], 50).length;
364
+ }
365
+
366
+ const hypotheses = Array.isArray(domainChain.hypotheses) ? domainChain.hypotheses : [];
367
+ for (const hypothesis of hypotheses.slice(0, 30)) {
368
+ total += normalizeTextList(hypothesis && hypothesis.evidence, 20).length;
369
+ }
370
+
371
+ return total;
372
+ }
373
+
374
+ function extractDomainChainSummary(domainChain = {}) {
375
+ const summary = domainChain.summary && typeof domainChain.summary === 'object'
376
+ ? domainChain.summary
377
+ : {};
378
+ const payloadOntology = domainChain.ontology && typeof domainChain.ontology === 'object'
379
+ ? domainChain.ontology
380
+ : {};
381
+
382
+ const fallbackCounts = {
383
+ entity: normalizeTextList(payloadOntology.entity, 50).length,
384
+ relation: normalizeTextList(payloadOntology.relation, 50).length,
385
+ business_rule: normalizeTextList(payloadOntology.business_rule, 50).length,
386
+ decision_policy: normalizeTextList(payloadOntology.decision_policy, 50).length,
387
+ execution_flow: normalizeTextList(payloadOntology.execution_flow, 50).length
388
+ };
389
+
390
+ const summaryCounts = summary.ontology_counts && typeof summary.ontology_counts === 'object'
391
+ ? summary.ontology_counts
392
+ : {};
393
+ const ontologyCounts = {
394
+ entity: Number(summaryCounts.entity || fallbackCounts.entity || 0),
395
+ relation: Number(summaryCounts.relation || fallbackCounts.relation || 0),
396
+ business_rule: Number(summaryCounts.business_rule || fallbackCounts.business_rule || 0),
397
+ decision_policy: Number(summaryCounts.decision_policy || fallbackCounts.decision_policy || 0),
398
+ execution_flow: Number(summaryCounts.execution_flow || fallbackCounts.execution_flow || 0)
399
+ };
400
+
401
+ return {
402
+ ontology_counts: ontologyCounts,
403
+ decision_path_steps: Number(
404
+ summary.decision_path_steps
405
+ || (Array.isArray(domainChain.decision_execution_path) ? domainChain.decision_execution_path.length : 0)
406
+ || 0
407
+ ),
408
+ verification_gates: Array.isArray(summary.verification_gates)
409
+ ? summary.verification_gates
410
+ : (domainChain?.verification?.gates && Array.isArray(domainChain.verification.gates) ? domainChain.verification.gates : []),
411
+ hypothesis_count: Number(summary.hypothesis_count || (Array.isArray(domainChain.hypotheses) ? domainChain.hypotheses.length : 0) || 0),
412
+ risk_count: Number(summary.risk_count || (Array.isArray(domainChain.risks) ? domainChain.risks.length : 0) || 0),
413
+ evidence_binding_count: countOntologyEvidenceBindings(domainChain, summary)
414
+ };
415
+ }
416
+
417
+ function deriveProblemContract(context = {}) {
418
+ const domainChain = context.domain_chain && typeof context.domain_chain === 'object'
419
+ ? context.domain_chain
420
+ : {};
421
+ const chainContext = domainChain.context && typeof domainChain.context === 'object'
422
+ ? domainChain.context
423
+ : {};
424
+ const contractRaw = context.problem_contract && typeof context.problem_contract === 'object'
425
+ ? context.problem_contract
426
+ : {};
427
+
428
+ const issueStatement = normalizeText(
429
+ contractRaw.issue_statement
430
+ || contractRaw.issue
431
+ || contractRaw.problem_statement
432
+ || chainContext?.problem?.statement
433
+ || domainChain?.problem?.statement
434
+ || context.goal
435
+ || (context.scene_id ? `Stabilize scene ${normalizeText(context.scene_id)} execution.` : '')
436
+ );
437
+ const expectedOutcome = normalizeText(
438
+ contractRaw.expected_outcome
439
+ || contractRaw.expected
440
+ || contractRaw.success_criteria
441
+ || chainContext?.verification?.plan
442
+ || domainChain?.verification?.plan
443
+ || (context.scene_id ? `Scene ${normalizeText(context.scene_id)} reaches deterministic verification gates.` : '')
444
+ );
445
+ const reproductionStepsRaw = normalizeTextList(
446
+ contractRaw.reproduction_steps
447
+ || contractRaw.repro_steps
448
+ || contractRaw.steps,
449
+ 20
450
+ );
451
+ const reproductionSteps = reproductionStepsRaw.length > 0
452
+ ? reproductionStepsRaw
453
+ : [
454
+ normalizeText(context.goal) || 'Reproduce the reported failure path in the target scene.',
455
+ 'Capture execution trace and gate evidence for the failing path.'
456
+ ].filter(Boolean);
457
+ const impactScope = normalizeText(
458
+ contractRaw.impact_scope
459
+ || contractRaw.scope
460
+ || chainContext?.problem?.scope
461
+ || domainChain?.problem?.scope
462
+ || context.scene_id
463
+ );
464
+ const forbiddenWorkaroundsRaw = normalizeTextList(
465
+ contractRaw.forbidden_workarounds
466
+ || contractRaw.prohibited_workarounds
467
+ || contractRaw.disallowed_workarounds,
468
+ 20
469
+ );
470
+ const forbiddenWorkarounds = forbiddenWorkaroundsRaw.length > 0
471
+ ? forbiddenWorkaroundsRaw
472
+ : [
473
+ 'Do not bypass mandatory gates or tests.',
474
+ 'Do not silence errors without root-cause remediation.'
475
+ ];
476
+ return {
477
+ issue_statement: issueStatement,
478
+ expected_outcome: expectedOutcome,
479
+ reproduction_steps: reproductionSteps,
480
+ impact_scope: impactScope,
481
+ forbidden_workarounds: forbiddenWorkarounds
482
+ };
483
+ }
484
+
485
+ function evaluateProblemContract(context = {}) {
486
+ const contract = deriveProblemContract(context);
487
+ const checks = {
488
+ issue_statement: normalizeText(contract.issue_statement).length > 0,
489
+ expected_outcome: normalizeText(contract.expected_outcome).length > 0,
490
+ reproduction_steps: Array.isArray(contract.reproduction_steps) && contract.reproduction_steps.length > 0,
491
+ impact_scope: normalizeText(contract.impact_scope).length > 0,
492
+ forbidden_workarounds: Array.isArray(contract.forbidden_workarounds) && contract.forbidden_workarounds.length > 0
493
+ };
494
+ const total = Object.keys(checks).length;
495
+ const covered = Object.values(checks).filter(Boolean).length;
496
+ return {
497
+ contract,
498
+ checks,
499
+ total,
500
+ covered,
501
+ missing: Object.keys(checks).filter((key) => !checks[key]),
502
+ score: Math.round((covered / total) * 100),
503
+ passed: covered === total
504
+ };
505
+ }
506
+
507
+ function evaluateOntologyAlignment(context = {}, policy = DEFAULT_PROBLEM_EVAL_POLICY) {
508
+ const domainChain = context.domain_chain && typeof context.domain_chain === 'object'
509
+ ? context.domain_chain
510
+ : {};
511
+ const summary = extractDomainChainSummary(domainChain);
512
+ const requiredAxes = Array.isArray(policy.ontology_required_axes) && policy.ontology_required_axes.length > 0
513
+ ? policy.ontology_required_axes
514
+ : [...ONTOLOGY_AXES];
515
+ const missingAxes = requiredAxes.filter((axis) => Number(summary?.ontology_counts?.[axis] || 0) <= 0);
516
+ const evidenceBindingCount = Number(summary.evidence_binding_count || 0);
517
+ const minBindings = Number(policy.ontology_evidence_min_bindings || 0);
518
+ const hasDomainMaterial = domainChain.resolved === true
519
+ || ONTOLOGY_AXES.some((axis) => Number(summary?.ontology_counts?.[axis] || 0) > 0)
520
+ || normalizeText(context.spec_id).length > 0;
521
+ if (!hasDomainMaterial) {
522
+ return {
523
+ required_axes: requiredAxes,
524
+ missing_axes: [],
525
+ ontology_counts: summary.ontology_counts,
526
+ evidence_binding_count: evidenceBindingCount,
527
+ required_evidence_bindings: minBindings,
528
+ evidence_satisfied: true,
529
+ score: 100,
530
+ passed: true,
531
+ skipped: true
532
+ };
533
+ }
534
+ const evidenceSatisfied = policy.require_ontology_evidence_binding !== true
535
+ ? true
536
+ : evidenceBindingCount >= minBindings;
537
+
538
+ let score = 0;
539
+ if (requiredAxes.length > 0) {
540
+ score += Math.round(((requiredAxes.length - missingAxes.length) / requiredAxes.length) * 80);
541
+ }
542
+ if (evidenceSatisfied) {
543
+ score += 20;
544
+ }
545
+ score = Math.max(0, Math.min(100, score));
546
+
547
+ return {
548
+ required_axes: requiredAxes,
549
+ missing_axes: missingAxes,
550
+ ontology_counts: summary.ontology_counts,
551
+ evidence_binding_count: evidenceBindingCount,
552
+ required_evidence_bindings: minBindings,
553
+ evidence_satisfied: evidenceSatisfied,
554
+ score,
555
+ passed: missingAxes.length === 0 && evidenceSatisfied
556
+ };
557
+ }
558
+
559
+ function evaluateConvergence(context = {}, stage = '', policy = DEFAULT_PROBLEM_EVAL_POLICY) {
560
+ const readiness = context.stage_readiness && typeof context.stage_readiness === 'object'
561
+ ? context.stage_readiness
562
+ : {};
563
+ const checks = {
564
+ prerequisites_ready: readiness.prerequisites_ready === true
565
+ };
566
+ if (stage === 'release') {
567
+ checks.verify_report_ready = readiness.verify_report_ready === true;
568
+ checks.verify_stage_passed = readiness.verify_stage_passed !== false;
569
+ checks.regression_passed = readiness.regression_passed !== false;
570
+ const highAlertCount = Number(readiness.high_alert_count || 0);
571
+ checks.high_alerts_clear = policy.release_block_on_high_alerts === true
572
+ ? highAlertCount <= 0
573
+ : true;
574
+ if (policy.release_require_governance_report === true) {
575
+ checks.governance_report_ready = readiness.governance_report_ready === true;
576
+ }
577
+ }
578
+
579
+ const total = Object.keys(checks).length;
580
+ const covered = Object.values(checks).filter(Boolean).length;
581
+ const missing = Object.keys(checks).filter((key) => !checks[key]);
582
+ const score = total > 0 ? Math.round((covered / total) * 100) : 100;
583
+
584
+ return {
585
+ checks,
586
+ missing,
587
+ score,
588
+ passed: missing.length === 0
589
+ };
590
+ }
591
+
245
592
  function scoreEvidence(context = {}, incidentSignals = {}) {
246
593
  const signals = [];
247
594
  let score = 0;
248
595
  const domainChain = context.domain_chain && typeof context.domain_chain === 'object'
249
596
  ? context.domain_chain
250
597
  : {};
251
- const summary = domainChain.summary && typeof domainChain.summary === 'object'
252
- ? domainChain.summary
253
- : {};
598
+ const summary = extractDomainChainSummary(domainChain);
254
599
 
255
600
  if (domainChain.resolved === true) {
256
601
  score += 20;
@@ -293,6 +638,11 @@ function scoreEvidence(context = {}, incidentSignals = {}) {
293
638
  score += 6;
294
639
  signals.push('required-gates-available');
295
640
  }
641
+ const evidenceBindingCount = Number(summary.evidence_binding_count || 0);
642
+ if (evidenceBindingCount > 0) {
643
+ score += Math.min(10, evidenceBindingCount);
644
+ signals.push(`ontology-evidence-bindings:${evidenceBindingCount}`);
645
+ }
296
646
 
297
647
  score = Math.max(0, Math.min(100, Math.round(score)));
298
648
  return { score, signals };
@@ -347,7 +697,8 @@ function scoreReadiness(context = {}) {
347
697
  function deriveStrategy(stage, risk, evidence, confidence, incidentSignals = {}, policy = DEFAULT_PROBLEM_EVAL_POLICY) {
348
698
  const reasons = [];
349
699
  let strategy = 'direct-execution';
350
- if (Number(incidentSignals.max_attempt_count || 0) >= 3
700
+ const debugAttemptThreshold = Number(policy.max_failed_rounds_before_debug || 2) + 1;
701
+ if (Number(incidentSignals.max_attempt_count || 0) >= debugAttemptThreshold
351
702
  && policy.high_risk_requires_debug_evidence
352
703
  && incidentSignals.has_debug_evidence !== true) {
353
704
  strategy = 'debug-first';
@@ -393,8 +744,16 @@ function evaluateProblemContext(context = {}, policy = DEFAULT_PROBLEM_EVAL_POLI
393
744
  const risk = scoreRisk(stage, textForRisk, policy, incidentSignals, context.release_channel);
394
745
  const evidence = scoreEvidence(context, incidentSignals);
395
746
  const readiness = scoreReadiness(context);
747
+ const problemContract = evaluateProblemContract(context);
748
+ const ontologyAlignment = evaluateOntologyAlignment(context, policy);
749
+ const convergence = evaluateConvergence(context, stage, policy);
396
750
  const confidenceScore = Math.max(0, Math.min(100, Math.round(
397
- evidence.score * 0.45 + readiness.score * 0.35 + (100 - risk.score) * 0.20
751
+ evidence.score * 0.32
752
+ + readiness.score * 0.24
753
+ + (100 - risk.score) * 0.14
754
+ + problemContract.score * 0.15
755
+ + ontologyAlignment.score * 0.10
756
+ + convergence.score * 0.05
398
757
  )));
399
758
 
400
759
  const minConfidence = Number(policy?.min_confidence_by_stage?.[stage] || 0);
@@ -413,9 +772,10 @@ function evaluateProblemContext(context = {}, policy = DEFAULT_PROBLEM_EVAL_POLI
413
772
  }
414
773
  }
415
774
 
775
+ const debugAttemptThreshold = Number(policy.max_failed_rounds_before_debug || 2) + 1;
416
776
  if (policy.high_risk_requires_debug_evidence
417
777
  && risk.level === 'high'
418
- && Number(incidentSignals.max_attempt_count || 0) >= 3
778
+ && Number(incidentSignals.max_attempt_count || 0) >= debugAttemptThreshold
419
779
  && incidentSignals.has_debug_evidence !== true) {
420
780
  warnings.push('high risk with repeated failed attempts and no debug evidence');
421
781
  if (blockStage) {
@@ -430,6 +790,47 @@ function evaluateProblemContext(context = {}, policy = DEFAULT_PROBLEM_EVAL_POLI
430
790
  }
431
791
  }
432
792
 
793
+ const problemContractRequired = stageInPolicy(stage, policy.problem_contract_required_stages);
794
+ const problemContractBlockedStage = stageInPolicy(stage, policy.problem_contract_block_stages);
795
+ if (problemContractRequired && !problemContract.passed) {
796
+ warnings.push(`problem contract incomplete: ${problemContract.missing.join(', ')}`);
797
+ if (problemContractBlockedStage) {
798
+ blockers.push(`problem-contract-incomplete:${problemContract.missing.join('|')}`);
799
+ }
800
+ }
801
+
802
+ const ontologyRequired = stageInPolicy(stage, policy.ontology_alignment_required_stages);
803
+ const ontologyBlockedStage = stageInPolicy(stage, policy.ontology_alignment_block_stages);
804
+ if (ontologyRequired && !ontologyAlignment.passed) {
805
+ if (ontologyAlignment.missing_axes.length > 0) {
806
+ warnings.push(`ontology alignment missing axes: ${ontologyAlignment.missing_axes.join(', ')}`);
807
+ }
808
+ if (!ontologyAlignment.evidence_satisfied) {
809
+ warnings.push(
810
+ `ontology evidence binding below threshold: ${ontologyAlignment.evidence_binding_count}<${ontologyAlignment.required_evidence_bindings}`
811
+ );
812
+ }
813
+ if (ontologyBlockedStage) {
814
+ if (ontologyAlignment.missing_axes.length > 0) {
815
+ blockers.push(`ontology-alignment-missing:${ontologyAlignment.missing_axes.join('|')}`);
816
+ }
817
+ if (!ontologyAlignment.evidence_satisfied) {
818
+ blockers.push(
819
+ `ontology-evidence-binding-low:${ontologyAlignment.evidence_binding_count}<${ontologyAlignment.required_evidence_bindings}`
820
+ );
821
+ }
822
+ }
823
+ }
824
+
825
+ const convergenceRequired = stageInPolicy(stage, policy.convergence_required_stages);
826
+ const convergenceBlockedStage = stageInPolicy(stage, policy.convergence_block_stages);
827
+ if (convergenceRequired && !convergence.passed) {
828
+ warnings.push(`convergence checks pending: ${convergence.missing.join(', ')}`);
829
+ if (convergenceBlockedStage) {
830
+ blockers.push(`convergence-gate-missing:${convergence.missing.join('|')}`);
831
+ }
832
+ }
833
+
433
834
  const recommendations = [];
434
835
  if (strategy.strategy === 'debug-first') {
435
836
  recommendations.push('Capture debug trace/log evidence before the next patch attempt.');
@@ -444,12 +845,22 @@ function evaluateProblemContext(context = {}, policy = DEFAULT_PROBLEM_EVAL_POLI
444
845
  if (Number(incidentSignals.open_incident_count || 0) > 0) {
445
846
  recommendations.push('Review staging incident attempts to avoid repeating failed actions.');
446
847
  }
848
+ if (!problemContract.passed) {
849
+ recommendations.push('Complete the problem contract: issue, expected outcome, reproduction steps, impact scope, and forbidden workarounds.');
850
+ }
851
+ if (!ontologyAlignment.passed) {
852
+ recommendations.push('Fill missing ontology axes and bind evidence references before further remediation.');
853
+ }
854
+ if (!convergence.passed) {
855
+ recommendations.push('Close convergence checks (verify pass, regression pass, high-alert clear) before release.');
856
+ }
447
857
  if (recommendations.length === 0) {
448
858
  recommendations.push('Proceed with direct execution and keep gate verification enabled.');
449
859
  }
450
860
 
451
861
  const cappedRecommendations = recommendations.slice(0, policy.recommendation_limit || 6);
452
- const blocked = enforced && blockStage && !advisoryMode && blockers.length > 0;
862
+ const hardBlockStage = blockStage || problemContractBlockedStage || ontologyBlockedStage || convergenceBlockedStage;
863
+ const blocked = enforced && hardBlockStage && !advisoryMode && blockers.length > 0;
453
864
 
454
865
  return {
455
866
  mode: 'problem-eval',
@@ -464,13 +875,17 @@ function evaluateProblemContext(context = {}, policy = DEFAULT_PROBLEM_EVAL_POLI
464
875
  mode: policy.mode,
465
876
  enforced,
466
877
  block_stage: blockStage,
878
+ hard_block_stage: hardBlockStage,
467
879
  min_confidence: minConfidence
468
880
  },
469
881
  dimensions: {
470
882
  risk,
471
883
  evidence,
472
884
  readiness,
473
- strategy
885
+ strategy,
886
+ problem_contract: problemContract,
887
+ ontology_alignment: ontologyAlignment,
888
+ convergence
474
889
  },
475
890
  incident_signals: {
476
891
  ...incidentSignals
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const DOMAIN_MAP_RELATIVE_PATH = path.join('custom', 'problem-domain-map.md');
5
5
  const SCENE_SPEC_RELATIVE_PATH = path.join('custom', 'scene-spec.md');
6
6
  const DOMAIN_CHAIN_RELATIVE_PATH = path.join('custom', 'problem-domain-chain.json');
7
+ const PROBLEM_CONTRACT_RELATIVE_PATH = path.join('custom', 'problem-contract.json');
7
8
  const DOMAIN_CHAIN_API_VERSION = 'sce.problem-domain-chain/v0.1';
8
9
  const DOMAIN_RESEARCH_DIMENSIONS = Object.freeze([
9
10
  'scene_boundary',
@@ -30,7 +31,30 @@ function resolveSpecPaths(projectPath, specId) {
30
31
  specPath,
31
32
  domainMapPath: path.join(specPath, DOMAIN_MAP_RELATIVE_PATH),
32
33
  sceneSpecPath: path.join(specPath, SCENE_SPEC_RELATIVE_PATH),
33
- domainChainPath: path.join(specPath, DOMAIN_CHAIN_RELATIVE_PATH)
34
+ domainChainPath: path.join(specPath, DOMAIN_CHAIN_RELATIVE_PATH),
35
+ problemContractPath: path.join(specPath, PROBLEM_CONTRACT_RELATIVE_PATH)
36
+ };
37
+ }
38
+
39
+ function buildProblemContract(specId, options = {}) {
40
+ const sceneId = normalizeText(options.sceneId) || `scene.${specId.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()}`;
41
+ const problemStatement = normalizeText(options.problemStatement) || 'TBD: describe the primary business problem';
42
+ const verificationPlan = normalizeText(options.verificationPlan) || 'TBD: define validation and rollback criteria';
43
+ return {
44
+ schema_version: '1.0',
45
+ spec_id: specId,
46
+ scene_id: sceneId,
47
+ issue_statement: problemStatement,
48
+ expected_outcome: verificationPlan,
49
+ reproduction_steps: [
50
+ 'Reproduce the issue along the failing scene path.',
51
+ 'Capture gate/test/runtime evidence for failed behavior.'
52
+ ],
53
+ impact_scope: `scene=${sceneId}`,
54
+ forbidden_workarounds: [
55
+ 'Do not bypass mandatory gates or tests.',
56
+ 'Do not silence runtime errors without root-cause remediation.'
57
+ ]
34
58
  };
35
59
  }
36
60
 
@@ -274,6 +298,14 @@ function buildProblemDomainChain(specId, options = {}) {
274
298
  'tests',
275
299
  'release preflight'
276
300
  ]
301
+ },
302
+ problem_contract: buildProblemContract(specId, options),
303
+ ontology_evidence: {
304
+ entity: ['Entity mapping evidence: model/schema reference'],
305
+ relation: ['Relation mapping evidence: join/foreign-key or service linkage'],
306
+ business_rule: ['Rule evidence: validation/policy check reference'],
307
+ decision_policy: ['Decision evidence: branch condition and policy source'],
308
+ execution_flow: ['Execution evidence: service/screen/runtime trace path']
277
309
  }
278
310
  };
279
311
  }
@@ -479,11 +511,13 @@ async function ensureSpecDomainArtifacts(projectPath, specId, options = {}) {
479
511
  const domainMapContent = buildProblemDomainMindMap(specId, options);
480
512
  const sceneSpecContent = buildSceneSpec(specId, options);
481
513
  const domainChainPayload = buildProblemDomainChain(specId, options);
514
+ const problemContractPayload = buildProblemContract(specId, options);
482
515
 
483
516
  const created = {
484
517
  domain_map: false,
485
518
  scene_spec: false,
486
- domain_chain: false
519
+ domain_chain: false,
520
+ problem_contract: false
487
521
  };
488
522
 
489
523
  if (!dryRun) {
@@ -506,23 +540,32 @@ async function ensureSpecDomainArtifacts(projectPath, specId, options = {}) {
506
540
  await fileSystem.writeJson(paths.domainChainPath, domainChainPayload, { spaces: 2 });
507
541
  created.domain_chain = true;
508
542
  }
543
+
544
+ const hasProblemContract = await fileSystem.pathExists(paths.problemContractPath);
545
+ if (force || !hasProblemContract) {
546
+ await fileSystem.writeJson(paths.problemContractPath, problemContractPayload, { spaces: 2 });
547
+ created.problem_contract = true;
548
+ }
509
549
  } else {
510
550
  created.domain_map = true;
511
551
  created.scene_spec = true;
512
552
  created.domain_chain = true;
553
+ created.problem_contract = true;
513
554
  }
514
555
 
515
556
  return {
516
557
  paths: {
517
558
  domain_map: paths.domainMapPath,
518
559
  scene_spec: paths.sceneSpecPath,
519
- domain_chain: paths.domainChainPath
560
+ domain_chain: paths.domainChainPath,
561
+ problem_contract: paths.problemContractPath
520
562
  },
521
563
  created,
522
564
  preview: {
523
565
  domain_map: domainMapContent,
524
566
  scene_spec: sceneSpecContent,
525
- domain_chain: domainChainPayload
567
+ domain_chain: domainChainPayload,
568
+ problem_contract: problemContractPayload
526
569
  }
527
570
  };
528
571
  }
@@ -644,11 +687,13 @@ module.exports = {
644
687
  DOMAIN_MAP_RELATIVE_PATH,
645
688
  SCENE_SPEC_RELATIVE_PATH,
646
689
  DOMAIN_CHAIN_RELATIVE_PATH,
690
+ PROBLEM_CONTRACT_RELATIVE_PATH,
647
691
  DOMAIN_CHAIN_API_VERSION,
648
692
  DOMAIN_RESEARCH_DIMENSIONS,
649
693
  buildProblemDomainMindMap,
650
694
  buildSceneSpec,
651
695
  buildProblemDomainChain,
696
+ buildProblemContract,
652
697
  ensureSpecDomainArtifacts,
653
698
  validateSpecDomainArtifacts,
654
699
  analyzeSpecDomainCoverage