scene-capability-engine 3.6.5 → 3.6.7

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.7] - 2026-03-05
11
+
12
+ ### Fixed
13
+ - `state-migration-reconciliation-gate` now treats `blocking` checks as advisory by default and only fails when `--fail-on-blocking` is explicitly set.
14
+ - Release workflow reconciliation step now adds `--fail-on-blocking` only in enforce mode, preventing advisory mode from failing npm publish.
15
+
16
+ ## [3.6.6] - 2026-03-05
17
+
18
+ ### Added
19
+ - Stage-4 SQLite migration components for release evidence indexes:
20
+ - `release.evidence-runs-index` (`.sce/reports/release-evidence/handoff-runs.json`)
21
+ - `release.gate-history-index` (`.sce/reports/release-evidence/release-gate-history.json`)
22
+ - New SQLite index tables in state store:
23
+ - `release_evidence_run_registry`
24
+ - `release_gate_history_registry`
25
+ - New `SceStateStore` APIs:
26
+ - `upsert/listReleaseEvidenceRunRecords`
27
+ - `upsert/listReleaseGateHistoryRecords`
28
+ - Release workflow gate integration:
29
+ - new step `state_migration_reconciliation`
30
+ - new release asset `.sce/reports/release-evidence/state-migration-reconciliation-<tag>.json`
31
+
32
+ ### Changed
33
+ - `sce state plan/doctor/migrate/export` now includes release-evidence components in migration scope and parity summaries.
34
+
10
35
  ## [3.6.5] - 2026-03-05
11
36
 
12
37
  ### Added
package/README.md CHANGED
@@ -153,7 +153,7 @@ Studio task-stream output contract (default):
153
153
  - reconciliation gate: `npm run gate:state-migration-reconciliation`
154
154
  - runtime reads now prefer sqlite indexes for timeline/scene-session views when indexed data exists
155
155
  - `state doctor` now emits `summary` and runtime diagnostics (`runtime.timeline`, `runtime.scene_session`) with read-source and consistency status
156
- - migratable components now include errorbook + spec-governance indexes (`errorbook.entry-index`, `errorbook.incident-index`, `governance.spec-scene-overrides`, `governance.scene-index`) in addition to runtime registries
156
+ - migratable components now include runtime + errorbook + spec-governance + release evidence indexes (`errorbook.entry-index`, `errorbook.incident-index`, `governance.spec-scene-overrides`, `governance.scene-index`, `release.evidence-runs-index`, `release.gate-history-index`)
157
157
  - Write authorization lease model (SQLite-backed):
158
158
  - policy file: `.sce/config/authorization-policy.json`
159
159
  - grant lease: `sce auth grant --scope studio:* --reason "<reason>" --auth-password <password> --json`
package/README.zh.md CHANGED
@@ -153,7 +153,7 @@ Studio 任务流输出契约(默认):
153
153
  - 对账门禁:`npm run gate:state-migration-reconciliation`
154
154
  - 运行时读取在存在索引数据时优先使用 SQLite(timeline/scene-session 视图)
155
155
  - `state doctor` 新增 `summary` 与运行时诊断(`runtime.timeline`、`runtime.scene_session`),可直接读取读源与一致性状态
156
- - 可迁移组件扩展到 errorbook + spec-governance 索引(`errorbook.entry-index`、`errorbook.incident-index`、`governance.spec-scene-overrides`、`governance.scene-index`)
156
+ - 可迁移组件扩展到 runtime + errorbook + spec-governance + release evidence 索引(`errorbook.entry-index`、`errorbook.incident-index`、`governance.spec-scene-overrides`、`governance.scene-index`、`release.evidence-runs-index`、`release.gate-history-index`)
157
157
  - 写入授权租约模型(SQLite 持久化):
158
158
  - 策略文件:`.sce/config/authorization-policy.json`
159
159
  - 申请租约:`sce auth grant --scope studio:* --reason "<原因>" --auth-password <密码> --json`
@@ -727,7 +727,7 @@ sce state export --out .sce/reports/state-migration/state-export.latest.json --j
727
727
 
728
728
  # reconciliation gate (non-blocking by default; choose strict flags as needed)
729
729
  npm run gate:state-migration-reconciliation
730
- node scripts/state-migration-reconciliation-gate.js --fail-on-alert --fail-on-pending --json
730
+ node scripts/state-migration-reconciliation-gate.js --fail-on-blocking --fail-on-alert --fail-on-pending --json
731
731
  ```
732
732
 
733
733
  Current migratable components:
@@ -738,6 +738,8 @@ Current migratable components:
738
738
  - `errorbook.incident-index` (`.sce/errorbook/staging/index.json`)
739
739
  - `governance.spec-scene-overrides` (`.sce/spec-governance/spec-scene-overrides.json`)
740
740
  - `governance.scene-index` (`.sce/spec-governance/scene-index.json`)
741
+ - `release.evidence-runs-index` (`.sce/reports/release-evidence/handoff-runs.json`)
742
+ - `release.gate-history-index` (`.sce/reports/release-evidence/release-gate-history.json`)
741
743
 
742
744
  SQLite index tables introduced for gradual migration:
743
745
  - `agent_runtime_registry`
@@ -747,6 +749,8 @@ SQLite index tables introduced for gradual migration:
747
749
  - `errorbook_incident_index_registry`
748
750
  - `governance_spec_scene_override_registry`
749
751
  - `governance_scene_index_registry`
752
+ - `release_evidence_run_registry`
753
+ - `release_gate_history_registry`
750
754
  - `state_migration_registry`
751
755
 
752
756
  Runtime read preference:
@@ -137,6 +137,8 @@ class SceStateStore {
137
137
  errorbook_incident_index: {},
138
138
  governance_spec_scene_override: {},
139
139
  governance_scene_index: {},
140
+ release_evidence_run: {},
141
+ release_gate_history: {},
140
142
  migration_records: {},
141
143
  auth_leases: {},
142
144
  auth_events: [],
@@ -407,6 +409,54 @@ class SceStateStore {
407
409
 
408
410
  CREATE INDEX IF NOT EXISTS idx_governance_scene_index_registry_counts
409
411
  ON governance_scene_index_registry(total_specs DESC, active_specs DESC);
412
+
413
+ CREATE TABLE IF NOT EXISTS release_evidence_run_registry (
414
+ session_id TEXT PRIMARY KEY,
415
+ merged_at TEXT,
416
+ status TEXT,
417
+ gate_passed INTEGER,
418
+ spec_success_rate_percent REAL,
419
+ risk_level TEXT,
420
+ ontology_quality_score REAL,
421
+ capability_coverage_percent REAL,
422
+ capability_coverage_passed INTEGER,
423
+ scene_package_batch_passed INTEGER,
424
+ scene_package_batch_failure_count INTEGER,
425
+ failed_goals INTEGER,
426
+ release_gate_preflight_available INTEGER,
427
+ release_gate_preflight_blocked INTEGER,
428
+ source_updated_at TEXT,
429
+ source TEXT,
430
+ indexed_at TEXT NOT NULL
431
+ );
432
+
433
+ CREATE INDEX IF NOT EXISTS idx_release_evidence_run_registry_status_merged
434
+ ON release_evidence_run_registry(status, merged_at DESC);
435
+
436
+ CREATE TABLE IF NOT EXISTS release_gate_history_registry (
437
+ tag TEXT PRIMARY KEY,
438
+ evaluated_at TEXT,
439
+ gate_passed INTEGER,
440
+ enforce INTEGER,
441
+ risk_level TEXT,
442
+ spec_success_rate_percent REAL,
443
+ scene_package_batch_passed INTEGER,
444
+ scene_package_batch_failure_count INTEGER,
445
+ capability_expected_unknown_count INTEGER,
446
+ capability_provided_unknown_count INTEGER,
447
+ release_gate_preflight_available INTEGER,
448
+ release_gate_preflight_blocked INTEGER,
449
+ require_release_gate_preflight INTEGER,
450
+ drift_alert_count INTEGER,
451
+ drift_blocked INTEGER,
452
+ weekly_ops_blocked INTEGER,
453
+ source_updated_at TEXT,
454
+ source TEXT,
455
+ indexed_at TEXT NOT NULL
456
+ );
457
+
458
+ CREATE INDEX IF NOT EXISTS idx_release_gate_history_registry_eval
459
+ ON release_gate_history_registry(evaluated_at DESC);
410
460
  `);
411
461
  }
412
462
 
@@ -678,6 +728,66 @@ class SceStateStore {
678
728
  };
679
729
  }
680
730
 
731
+ _mapReleaseEvidenceRunRow(row) {
732
+ if (!row) {
733
+ return null;
734
+ }
735
+ return {
736
+ session_id: normalizeString(row.session_id),
737
+ merged_at: normalizeIsoTimestamp(row.merged_at, '') || null,
738
+ status: normalizeString(row.status) || null,
739
+ gate_passed: normalizeBooleanValue(row.gate_passed, false),
740
+ spec_success_rate_percent: Number.isFinite(Number(row.spec_success_rate_percent))
741
+ ? Number(row.spec_success_rate_percent)
742
+ : null,
743
+ risk_level: normalizeString(row.risk_level) || null,
744
+ ontology_quality_score: Number.isFinite(Number(row.ontology_quality_score))
745
+ ? Number(row.ontology_quality_score)
746
+ : null,
747
+ capability_coverage_percent: Number.isFinite(Number(row.capability_coverage_percent))
748
+ ? Number(row.capability_coverage_percent)
749
+ : null,
750
+ capability_coverage_passed: normalizeBooleanValue(row.capability_coverage_passed, false),
751
+ scene_package_batch_passed: normalizeBooleanValue(row.scene_package_batch_passed, false),
752
+ scene_package_batch_failure_count: normalizeNonNegativeInteger(row.scene_package_batch_failure_count, 0),
753
+ failed_goals: normalizeNonNegativeInteger(row.failed_goals, 0),
754
+ release_gate_preflight_available: normalizeBooleanValue(row.release_gate_preflight_available, false),
755
+ release_gate_preflight_blocked: normalizeBooleanValue(row.release_gate_preflight_blocked, false),
756
+ source_updated_at: normalizeIsoTimestamp(row.source_updated_at, '') || null,
757
+ source: normalizeString(row.source) || null,
758
+ indexed_at: normalizeIsoTimestamp(row.indexed_at, '') || null
759
+ };
760
+ }
761
+
762
+ _mapReleaseGateHistoryRow(row) {
763
+ if (!row) {
764
+ return null;
765
+ }
766
+ return {
767
+ tag: normalizeString(row.tag),
768
+ evaluated_at: normalizeIsoTimestamp(row.evaluated_at, '') || null,
769
+ gate_passed: normalizeBooleanValue(row.gate_passed, false),
770
+ enforce: normalizeBooleanValue(row.enforce, false),
771
+ risk_level: normalizeString(row.risk_level) || null,
772
+ spec_success_rate_percent: Number.isFinite(Number(row.spec_success_rate_percent))
773
+ ? Number(row.spec_success_rate_percent)
774
+ : null,
775
+ scene_package_batch_passed: normalizeBooleanValue(row.scene_package_batch_passed, false),
776
+ scene_package_batch_failure_count: normalizeNonNegativeInteger(row.scene_package_batch_failure_count, 0),
777
+ capability_expected_unknown_count: normalizeNonNegativeInteger(row.capability_expected_unknown_count, 0),
778
+ capability_provided_unknown_count: normalizeNonNegativeInteger(row.capability_provided_unknown_count, 0),
779
+ release_gate_preflight_available: normalizeBooleanValue(row.release_gate_preflight_available, false),
780
+ release_gate_preflight_blocked: normalizeBooleanValue(row.release_gate_preflight_blocked, false),
781
+ require_release_gate_preflight: normalizeBooleanValue(row.require_release_gate_preflight, false),
782
+ drift_alert_count: normalizeNonNegativeInteger(row.drift_alert_count, 0),
783
+ drift_blocked: normalizeBooleanValue(row.drift_blocked, false),
784
+ weekly_ops_blocked: normalizeBooleanValue(row.weekly_ops_blocked, false),
785
+ source_updated_at: normalizeIsoTimestamp(row.source_updated_at, '') || null,
786
+ source: normalizeString(row.source) || null,
787
+ indexed_at: normalizeIsoTimestamp(row.indexed_at, '') || null
788
+ };
789
+ }
790
+
681
791
  async resolveOrCreateTaskRef(options = {}) {
682
792
  const sceneId = normalizeString(options.sceneId);
683
793
  const specId = normalizeString(options.specId);
@@ -2165,6 +2275,322 @@ class SceStateStore {
2165
2275
  .filter(Boolean);
2166
2276
  }
2167
2277
 
2278
+ async upsertReleaseEvidenceRunRecords(records = [], options = {}) {
2279
+ const source = normalizeString(options.source) || 'file.release-evidence.handoff-runs';
2280
+ const nowIso = this.now();
2281
+ const normalizedRecords = Array.isArray(records)
2282
+ ? records.map((item) => ({
2283
+ session_id: normalizeString(item && item.session_id),
2284
+ merged_at: normalizeIsoTimestamp(item && item.merged_at, '') || null,
2285
+ status: normalizeString(item && item.status) || null,
2286
+ gate_passed: normalizeBooleanValue(item && item.gate_passed, false),
2287
+ spec_success_rate_percent: Number.isFinite(Number(item && item.spec_success_rate_percent))
2288
+ ? Number(item.spec_success_rate_percent)
2289
+ : null,
2290
+ risk_level: normalizeString(item && item.risk_level) || null,
2291
+ ontology_quality_score: Number.isFinite(Number(item && item.ontology_quality_score))
2292
+ ? Number(item.ontology_quality_score)
2293
+ : null,
2294
+ capability_coverage_percent: Number.isFinite(Number(item && item.capability_coverage_percent))
2295
+ ? Number(item.capability_coverage_percent)
2296
+ : null,
2297
+ capability_coverage_passed: normalizeBooleanValue(item && item.capability_coverage_passed, false),
2298
+ scene_package_batch_passed: normalizeBooleanValue(item && item.scene_package_batch_passed, false),
2299
+ scene_package_batch_failure_count: normalizeNonNegativeInteger(item && item.scene_package_batch_failure_count, 0),
2300
+ failed_goals: normalizeNonNegativeInteger(item && item.failed_goals, 0),
2301
+ release_gate_preflight_available: normalizeBooleanValue(item && item.release_gate_preflight_available, false),
2302
+ release_gate_preflight_blocked: normalizeBooleanValue(item && item.release_gate_preflight_blocked, false),
2303
+ source_updated_at: normalizeIsoTimestamp(item && item.source_updated_at, '') || null,
2304
+ source: normalizeString(item && item.source) || source,
2305
+ indexed_at: nowIso
2306
+ }))
2307
+ .filter((item) => item.session_id)
2308
+ : [];
2309
+
2310
+ if (this._useMemoryBackend()) {
2311
+ for (const item of normalizedRecords) {
2312
+ this._memory.release_evidence_run[item.session_id] = { ...item };
2313
+ }
2314
+ return {
2315
+ success: true,
2316
+ written: normalizedRecords.length,
2317
+ total: Object.keys(this._memory.release_evidence_run || {}).length
2318
+ };
2319
+ }
2320
+
2321
+ if (!await this.ensureReady()) {
2322
+ return null;
2323
+ }
2324
+
2325
+ const statement = this._db.prepare(`
2326
+ INSERT OR REPLACE INTO release_evidence_run_registry(
2327
+ session_id, merged_at, status, gate_passed, spec_success_rate_percent, risk_level,
2328
+ ontology_quality_score, capability_coverage_percent, capability_coverage_passed,
2329
+ scene_package_batch_passed, scene_package_batch_failure_count, failed_goals,
2330
+ release_gate_preflight_available, release_gate_preflight_blocked,
2331
+ source_updated_at, source, indexed_at
2332
+ )
2333
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2334
+ `);
2335
+
2336
+ this._withTransaction(() => {
2337
+ for (const item of normalizedRecords) {
2338
+ statement.run(
2339
+ item.session_id,
2340
+ item.merged_at,
2341
+ item.status,
2342
+ item.gate_passed ? 1 : 0,
2343
+ item.spec_success_rate_percent,
2344
+ item.risk_level,
2345
+ item.ontology_quality_score,
2346
+ item.capability_coverage_percent,
2347
+ item.capability_coverage_passed ? 1 : 0,
2348
+ item.scene_package_batch_passed ? 1 : 0,
2349
+ item.scene_package_batch_failure_count,
2350
+ item.failed_goals,
2351
+ item.release_gate_preflight_available ? 1 : 0,
2352
+ item.release_gate_preflight_blocked ? 1 : 0,
2353
+ item.source_updated_at,
2354
+ item.source,
2355
+ item.indexed_at
2356
+ );
2357
+ }
2358
+ });
2359
+
2360
+ const totalRow = this._db
2361
+ .prepare('SELECT COUNT(*) AS total FROM release_evidence_run_registry')
2362
+ .get();
2363
+
2364
+ return {
2365
+ success: true,
2366
+ written: normalizedRecords.length,
2367
+ total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
2368
+ };
2369
+ }
2370
+
2371
+ async listReleaseEvidenceRunRecords(options = {}) {
2372
+ const limit = normalizeInteger(options.limit, 100);
2373
+ const status = normalizeString(options.status);
2374
+ const riskLevel = normalizeString(options.riskLevel || options.risk_level);
2375
+
2376
+ if (this._useMemoryBackend()) {
2377
+ let rows = Object.values(this._memory.release_evidence_run || {}).map((item) => ({ ...item }));
2378
+ if (status) {
2379
+ rows = rows.filter((item) => normalizeString(item.status) === status);
2380
+ }
2381
+ if (riskLevel) {
2382
+ rows = rows.filter((item) => normalizeString(item.risk_level) === riskLevel);
2383
+ }
2384
+ rows.sort((left, right) => {
2385
+ const leftTs = Date.parse(left.merged_at || '') || 0;
2386
+ const rightTs = Date.parse(right.merged_at || '') || 0;
2387
+ if (rightTs !== leftTs) {
2388
+ return rightTs - leftTs;
2389
+ }
2390
+ return `${right.session_id}`.localeCompare(`${left.session_id}`);
2391
+ });
2392
+ if (limit > 0) {
2393
+ rows = rows.slice(0, limit);
2394
+ }
2395
+ return rows.map((row) => this._mapReleaseEvidenceRunRow(row));
2396
+ }
2397
+
2398
+ if (!await this.ensureReady()) {
2399
+ return null;
2400
+ }
2401
+
2402
+ let query = `
2403
+ SELECT session_id, merged_at, status, gate_passed, spec_success_rate_percent, risk_level,
2404
+ ontology_quality_score, capability_coverage_percent, capability_coverage_passed,
2405
+ scene_package_batch_passed, scene_package_batch_failure_count, failed_goals,
2406
+ release_gate_preflight_available, release_gate_preflight_blocked, source_updated_at,
2407
+ source, indexed_at
2408
+ FROM release_evidence_run_registry
2409
+ `;
2410
+ const clauses = [];
2411
+ const params = [];
2412
+ if (status) {
2413
+ clauses.push('status = ?');
2414
+ params.push(status);
2415
+ }
2416
+ if (riskLevel) {
2417
+ clauses.push('risk_level = ?');
2418
+ params.push(riskLevel);
2419
+ }
2420
+ if (clauses.length > 0) {
2421
+ query += ` WHERE ${clauses.join(' AND ')}`;
2422
+ }
2423
+ query += ' ORDER BY merged_at DESC, session_id DESC';
2424
+ if (limit > 0) {
2425
+ query += ' LIMIT ?';
2426
+ params.push(limit);
2427
+ }
2428
+
2429
+ const rows = this._db.prepare(query).all(...params);
2430
+ return rows
2431
+ .map((row) => this._mapReleaseEvidenceRunRow(row))
2432
+ .filter(Boolean);
2433
+ }
2434
+
2435
+ async upsertReleaseGateHistoryRecords(records = [], options = {}) {
2436
+ const source = normalizeString(options.source) || 'file.release-evidence.gate-history';
2437
+ const nowIso = this.now();
2438
+ const normalizedRecords = Array.isArray(records)
2439
+ ? records.map((item) => ({
2440
+ tag: normalizeString(item && item.tag),
2441
+ evaluated_at: normalizeIsoTimestamp(item && item.evaluated_at, '') || null,
2442
+ gate_passed: normalizeBooleanValue(item && item.gate_passed, false),
2443
+ enforce: normalizeBooleanValue(item && item.enforce, false),
2444
+ risk_level: normalizeString(item && item.risk_level) || null,
2445
+ spec_success_rate_percent: Number.isFinite(Number(item && item.spec_success_rate_percent))
2446
+ ? Number(item.spec_success_rate_percent)
2447
+ : null,
2448
+ scene_package_batch_passed: normalizeBooleanValue(item && item.scene_package_batch_passed, false),
2449
+ scene_package_batch_failure_count: normalizeNonNegativeInteger(item && item.scene_package_batch_failure_count, 0),
2450
+ capability_expected_unknown_count: normalizeNonNegativeInteger(item && item.capability_expected_unknown_count, 0),
2451
+ capability_provided_unknown_count: normalizeNonNegativeInteger(item && item.capability_provided_unknown_count, 0),
2452
+ release_gate_preflight_available: normalizeBooleanValue(item && item.release_gate_preflight_available, false),
2453
+ release_gate_preflight_blocked: normalizeBooleanValue(item && item.release_gate_preflight_blocked, false),
2454
+ require_release_gate_preflight: normalizeBooleanValue(item && item.require_release_gate_preflight, false),
2455
+ drift_alert_count: normalizeNonNegativeInteger(item && item.drift_alert_count, 0),
2456
+ drift_blocked: normalizeBooleanValue(item && item.drift_blocked, false),
2457
+ weekly_ops_blocked: normalizeBooleanValue(item && item.weekly_ops_blocked, false),
2458
+ source_updated_at: normalizeIsoTimestamp(item && item.source_updated_at, '') || null,
2459
+ source: normalizeString(item && item.source) || source,
2460
+ indexed_at: nowIso
2461
+ }))
2462
+ .filter((item) => item.tag)
2463
+ : [];
2464
+
2465
+ if (this._useMemoryBackend()) {
2466
+ for (const item of normalizedRecords) {
2467
+ this._memory.release_gate_history[item.tag] = { ...item };
2468
+ }
2469
+ return {
2470
+ success: true,
2471
+ written: normalizedRecords.length,
2472
+ total: Object.keys(this._memory.release_gate_history || {}).length
2473
+ };
2474
+ }
2475
+
2476
+ if (!await this.ensureReady()) {
2477
+ return null;
2478
+ }
2479
+
2480
+ const statement = this._db.prepare(`
2481
+ INSERT OR REPLACE INTO release_gate_history_registry(
2482
+ tag, evaluated_at, gate_passed, enforce, risk_level, spec_success_rate_percent,
2483
+ scene_package_batch_passed, scene_package_batch_failure_count,
2484
+ capability_expected_unknown_count, capability_provided_unknown_count,
2485
+ release_gate_preflight_available, release_gate_preflight_blocked,
2486
+ require_release_gate_preflight, drift_alert_count, drift_blocked,
2487
+ weekly_ops_blocked, source_updated_at, source, indexed_at
2488
+ )
2489
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2490
+ `);
2491
+
2492
+ this._withTransaction(() => {
2493
+ for (const item of normalizedRecords) {
2494
+ statement.run(
2495
+ item.tag,
2496
+ item.evaluated_at,
2497
+ item.gate_passed ? 1 : 0,
2498
+ item.enforce ? 1 : 0,
2499
+ item.risk_level,
2500
+ item.spec_success_rate_percent,
2501
+ item.scene_package_batch_passed ? 1 : 0,
2502
+ item.scene_package_batch_failure_count,
2503
+ item.capability_expected_unknown_count,
2504
+ item.capability_provided_unknown_count,
2505
+ item.release_gate_preflight_available ? 1 : 0,
2506
+ item.release_gate_preflight_blocked ? 1 : 0,
2507
+ item.require_release_gate_preflight ? 1 : 0,
2508
+ item.drift_alert_count,
2509
+ item.drift_blocked ? 1 : 0,
2510
+ item.weekly_ops_blocked ? 1 : 0,
2511
+ item.source_updated_at,
2512
+ item.source,
2513
+ item.indexed_at
2514
+ );
2515
+ }
2516
+ });
2517
+
2518
+ const totalRow = this._db
2519
+ .prepare('SELECT COUNT(*) AS total FROM release_gate_history_registry')
2520
+ .get();
2521
+
2522
+ return {
2523
+ success: true,
2524
+ written: normalizedRecords.length,
2525
+ total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
2526
+ };
2527
+ }
2528
+
2529
+ async listReleaseGateHistoryRecords(options = {}) {
2530
+ const limit = normalizeInteger(options.limit, 100);
2531
+ const tag = normalizeString(options.tag);
2532
+ const riskLevel = normalizeString(options.riskLevel || options.risk_level);
2533
+
2534
+ if (this._useMemoryBackend()) {
2535
+ let rows = Object.values(this._memory.release_gate_history || {}).map((item) => ({ ...item }));
2536
+ if (tag) {
2537
+ rows = rows.filter((item) => normalizeString(item.tag) === tag);
2538
+ }
2539
+ if (riskLevel) {
2540
+ rows = rows.filter((item) => normalizeString(item.risk_level) === riskLevel);
2541
+ }
2542
+ rows.sort((left, right) => {
2543
+ const leftTs = Date.parse(left.evaluated_at || '') || 0;
2544
+ const rightTs = Date.parse(right.evaluated_at || '') || 0;
2545
+ if (rightTs !== leftTs) {
2546
+ return rightTs - leftTs;
2547
+ }
2548
+ return `${right.tag}`.localeCompare(`${left.tag}`);
2549
+ });
2550
+ if (limit > 0) {
2551
+ rows = rows.slice(0, limit);
2552
+ }
2553
+ return rows.map((row) => this._mapReleaseGateHistoryRow(row));
2554
+ }
2555
+
2556
+ if (!await this.ensureReady()) {
2557
+ return null;
2558
+ }
2559
+
2560
+ let query = `
2561
+ SELECT tag, evaluated_at, gate_passed, enforce, risk_level, spec_success_rate_percent,
2562
+ scene_package_batch_passed, scene_package_batch_failure_count,
2563
+ capability_expected_unknown_count, capability_provided_unknown_count,
2564
+ release_gate_preflight_available, release_gate_preflight_blocked,
2565
+ require_release_gate_preflight, drift_alert_count, drift_blocked,
2566
+ weekly_ops_blocked, source_updated_at, source, indexed_at
2567
+ FROM release_gate_history_registry
2568
+ `;
2569
+ const clauses = [];
2570
+ const params = [];
2571
+ if (tag) {
2572
+ clauses.push('tag = ?');
2573
+ params.push(tag);
2574
+ }
2575
+ if (riskLevel) {
2576
+ clauses.push('risk_level = ?');
2577
+ params.push(riskLevel);
2578
+ }
2579
+ if (clauses.length > 0) {
2580
+ query += ` WHERE ${clauses.join(' AND ')}`;
2581
+ }
2582
+ query += ' ORDER BY evaluated_at DESC, tag DESC';
2583
+ if (limit > 0) {
2584
+ query += ' LIMIT ?';
2585
+ params.push(limit);
2586
+ }
2587
+
2588
+ const rows = this._db.prepare(query).all(...params);
2589
+ return rows
2590
+ .map((row) => this._mapReleaseGateHistoryRow(row))
2591
+ .filter(Boolean);
2592
+ }
2593
+
2168
2594
  _resolveOrCreateTaskRefInMemory(options = {}) {
2169
2595
  const sceneId = normalizeString(options.sceneId);
2170
2596
  const specId = normalizeString(options.specId);
@@ -11,6 +11,8 @@ const COMPONENT_ERRORBOOK_ENTRY_INDEX = 'errorbook.entry-index';
11
11
  const COMPONENT_ERRORBOOK_INCIDENT_INDEX = 'errorbook.incident-index';
12
12
  const COMPONENT_SPEC_SCENE_OVERRIDES = 'governance.spec-scene-overrides';
13
13
  const COMPONENT_SPEC_SCENE_INDEX = 'governance.scene-index';
14
+ const COMPONENT_RELEASE_EVIDENCE_RUNS = 'release.evidence-runs-index';
15
+ const COMPONENT_RELEASE_GATE_HISTORY = 'release.gate-history-index';
14
16
  const DEFAULT_STATE_EXPORT_PATH = '.sce/reports/state-migration/state-export.latest.json';
15
17
 
16
18
  const COMPONENT_DEFINITIONS = Object.freeze([
@@ -41,6 +43,14 @@ const COMPONENT_DEFINITIONS = Object.freeze([
41
43
  {
42
44
  id: COMPONENT_SPEC_SCENE_INDEX,
43
45
  source_path: '.sce/spec-governance/scene-index.json'
46
+ },
47
+ {
48
+ id: COMPONENT_RELEASE_EVIDENCE_RUNS,
49
+ source_path: '.sce/reports/release-evidence/handoff-runs.json'
50
+ },
51
+ {
52
+ id: COMPONENT_RELEASE_GATE_HISTORY,
53
+ source_path: '.sce/reports/release-evidence/release-gate-history.json'
44
54
  }
45
55
  ]);
46
56
 
@@ -280,6 +290,106 @@ function mapSpecSceneIndexPayload(payload = {}) {
280
290
  .filter((item) => item.scene_id);
281
291
  }
282
292
 
293
+ function mapReleaseEvidenceRunsPayload(payload = {}) {
294
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.sessions)) {
295
+ return [];
296
+ }
297
+ const sourceUpdatedAt = normalizeString(payload.updated_at) || normalizeString(payload.generated_at);
298
+ return payload.sessions
299
+ .map((session) => {
300
+ const gate = session && session.gate && typeof session.gate === 'object' ? session.gate : {};
301
+ const gateActual = gate && gate.actual && typeof gate.actual === 'object' ? gate.actual : {};
302
+ const coverage = session && session.capability_coverage && typeof session.capability_coverage === 'object'
303
+ ? session.capability_coverage
304
+ : {};
305
+ const coverageSummary = coverage && coverage.summary && typeof coverage.summary === 'object'
306
+ ? coverage.summary
307
+ : {};
308
+ const scenePackageBatch = session && session.scene_package_batch && typeof session.scene_package_batch === 'object'
309
+ ? session.scene_package_batch
310
+ : {};
311
+ const scenePackageBatchSummary = scenePackageBatch && scenePackageBatch.summary && typeof scenePackageBatch.summary === 'object'
312
+ ? scenePackageBatch.summary
313
+ : {};
314
+ const batchSummary = session && session.batch_summary && typeof session.batch_summary === 'object'
315
+ ? session.batch_summary
316
+ : {};
317
+ const preflight = session && session.release_gate_preflight && typeof session.release_gate_preflight === 'object'
318
+ ? session.release_gate_preflight
319
+ : {};
320
+ return {
321
+ session_id: normalizeString(session && session.session_id),
322
+ merged_at: normalizeString(session && session.merged_at) || normalizeString(session && session.generated_at),
323
+ status: normalizeString(session && session.status),
324
+ gate_passed: gate && gate.passed === true,
325
+ spec_success_rate_percent: Number.isFinite(Number(gateActual.spec_success_rate_percent))
326
+ ? Number(gateActual.spec_success_rate_percent)
327
+ : null,
328
+ risk_level: normalizeString(gateActual.risk_level),
329
+ ontology_quality_score: Number.isFinite(Number(gateActual.ontology_quality_score))
330
+ ? Number(gateActual.ontology_quality_score)
331
+ : null,
332
+ capability_coverage_percent: Number.isFinite(Number(coverageSummary.coverage_percent))
333
+ ? Number(coverageSummary.coverage_percent)
334
+ : null,
335
+ capability_coverage_passed: coverageSummary.passed === true,
336
+ scene_package_batch_passed: scenePackageBatchSummary.batch_gate_passed === true,
337
+ scene_package_batch_failure_count: Number.isFinite(Number(scenePackageBatchSummary.batch_gate_failure_count))
338
+ ? Number.parseInt(`${scenePackageBatchSummary.batch_gate_failure_count}`, 10)
339
+ : (
340
+ Number.isFinite(Number(scenePackageBatchSummary.failed))
341
+ ? Number.parseInt(`${scenePackageBatchSummary.failed}`, 10)
342
+ : 0
343
+ ),
344
+ failed_goals: Number.isFinite(Number(batchSummary.failed_goals))
345
+ ? Number.parseInt(`${batchSummary.failed_goals}`, 10)
346
+ : 0,
347
+ release_gate_preflight_available: preflight && preflight.available === true,
348
+ release_gate_preflight_blocked: preflight && preflight.blocked === true,
349
+ source_updated_at: sourceUpdatedAt
350
+ };
351
+ })
352
+ .filter((item) => item.session_id);
353
+ }
354
+
355
+ function mapReleaseGateHistoryPayload(payload = {}) {
356
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.entries)) {
357
+ return [];
358
+ }
359
+ const sourceUpdatedAt = normalizeString(payload.updated_at) || normalizeString(payload.generated_at);
360
+ return payload.entries
361
+ .map((entry) => ({
362
+ tag: normalizeString(entry && entry.tag),
363
+ evaluated_at: normalizeString(entry && entry.evaluated_at),
364
+ gate_passed: entry && entry.gate_passed === true,
365
+ enforce: entry && entry.enforce === true,
366
+ risk_level: normalizeString(entry && entry.risk_level),
367
+ spec_success_rate_percent: Number.isFinite(Number(entry && entry.spec_success_rate_percent))
368
+ ? Number(entry.spec_success_rate_percent)
369
+ : null,
370
+ scene_package_batch_passed: entry && entry.scene_package_batch_passed === true,
371
+ scene_package_batch_failure_count: Number.isFinite(Number(entry && entry.scene_package_batch_failure_count))
372
+ ? Number.parseInt(`${entry.scene_package_batch_failure_count}`, 10)
373
+ : 0,
374
+ capability_expected_unknown_count: Number.isFinite(Number(entry && entry.capability_expected_unknown_count))
375
+ ? Number.parseInt(`${entry.capability_expected_unknown_count}`, 10)
376
+ : 0,
377
+ capability_provided_unknown_count: Number.isFinite(Number(entry && entry.capability_provided_unknown_count))
378
+ ? Number.parseInt(`${entry.capability_provided_unknown_count}`, 10)
379
+ : 0,
380
+ release_gate_preflight_available: entry && entry.release_gate_preflight_available === true,
381
+ release_gate_preflight_blocked: entry && entry.release_gate_preflight_blocked === true,
382
+ require_release_gate_preflight: entry && entry.require_release_gate_preflight === true,
383
+ drift_alert_count: Number.isFinite(Number(entry && entry.drift_alert_count))
384
+ ? Number.parseInt(`${entry.drift_alert_count}`, 10)
385
+ : 0,
386
+ drift_blocked: entry && entry.drift_blocked === true,
387
+ weekly_ops_blocked: entry && entry.weekly_ops_blocked === true,
388
+ source_updated_at: sourceUpdatedAt
389
+ }))
390
+ .filter((item) => item.tag);
391
+ }
392
+
283
393
  async function readJsonSource(absolutePath, fileSystem = fs) {
284
394
  if (!await fileSystem.pathExists(absolutePath)) {
285
395
  return {
@@ -325,6 +435,10 @@ async function readComponentSnapshot(component = {}, projectPath = process.cwd()
325
435
  records = mapSpecSceneOverridesPayload(source.payload);
326
436
  } else if (component.id === COMPONENT_SPEC_SCENE_INDEX) {
327
437
  records = mapSpecSceneIndexPayload(source.payload);
438
+ } else if (component.id === COMPONENT_RELEASE_EVIDENCE_RUNS) {
439
+ records = mapReleaseEvidenceRunsPayload(source.payload);
440
+ } else if (component.id === COMPONENT_RELEASE_GATE_HISTORY) {
441
+ records = mapReleaseGateHistoryPayload(source.payload);
328
442
  }
329
443
  }
330
444
 
@@ -434,6 +548,16 @@ async function writeComponentToStateStore(componentSnapshot = {}, stateStore, co
434
548
  source: componentId
435
549
  });
436
550
  }
551
+ if (componentId === COMPONENT_RELEASE_EVIDENCE_RUNS) {
552
+ return stateStore.upsertReleaseEvidenceRunRecords(componentSnapshot.records, {
553
+ source: componentId
554
+ });
555
+ }
556
+ if (componentId === COMPONENT_RELEASE_GATE_HISTORY) {
557
+ return stateStore.upsertReleaseGateHistoryRecords(componentSnapshot.records, {
558
+ source: componentId
559
+ });
560
+ }
437
561
  return null;
438
562
  }
439
563
 
@@ -561,6 +685,8 @@ async function runStateDoctor(options = {}, dependencies = {}) {
561
685
  errorbookIncidentRows,
562
686
  governanceOverrideRows,
563
687
  governanceSceneRows,
688
+ releaseEvidenceRows,
689
+ releaseGateHistoryRows,
564
690
  migrations
565
691
  ] = await Promise.all([
566
692
  stateStore.listAgentRuntimeRecords({ limit: 0 }),
@@ -570,6 +696,8 @@ async function runStateDoctor(options = {}, dependencies = {}) {
570
696
  stateStore.listErrorbookIncidentIndexRecords({ limit: 0 }),
571
697
  stateStore.listGovernanceSpecSceneOverrideRecords({ limit: 0 }),
572
698
  stateStore.listGovernanceSceneIndexRecords({ limit: 0 }),
699
+ stateStore.listReleaseEvidenceRunRecords({ limit: 0 }),
700
+ stateStore.listReleaseGateHistoryRecords({ limit: 0 }),
573
701
  stateStore.listStateMigrations({ limit: 20 })
574
702
  ]);
575
703
 
@@ -580,7 +708,9 @@ async function runStateDoctor(options = {}, dependencies = {}) {
580
708
  [COMPONENT_ERRORBOOK_ENTRY_INDEX, Array.isArray(errorbookEntryRows) ? errorbookEntryRows.length : 0],
581
709
  [COMPONENT_ERRORBOOK_INCIDENT_INDEX, Array.isArray(errorbookIncidentRows) ? errorbookIncidentRows.length : 0],
582
710
  [COMPONENT_SPEC_SCENE_OVERRIDES, Array.isArray(governanceOverrideRows) ? governanceOverrideRows.length : 0],
583
- [COMPONENT_SPEC_SCENE_INDEX, Array.isArray(governanceSceneRows) ? governanceSceneRows.length : 0]
711
+ [COMPONENT_SPEC_SCENE_INDEX, Array.isArray(governanceSceneRows) ? governanceSceneRows.length : 0],
712
+ [COMPONENT_RELEASE_EVIDENCE_RUNS, Array.isArray(releaseEvidenceRows) ? releaseEvidenceRows.length : 0],
713
+ [COMPONENT_RELEASE_GATE_HISTORY, Array.isArray(releaseGateHistoryRows) ? releaseGateHistoryRows.length : 0]
584
714
  ]);
585
715
 
586
716
  const checks = plan.components.map((component) => {
@@ -788,6 +918,8 @@ async function runStateExport(options = {}, dependencies = {}) {
788
918
  errorbookIncidentRows,
789
919
  governanceOverrideRows,
790
920
  governanceSceneRows,
921
+ releaseEvidenceRows,
922
+ releaseGateHistoryRows,
791
923
  migrations
792
924
  ] = await Promise.all([
793
925
  stateStore.listAgentRuntimeRecords({ limit: 0 }),
@@ -797,6 +929,8 @@ async function runStateExport(options = {}, dependencies = {}) {
797
929
  stateStore.listErrorbookIncidentIndexRecords({ limit: 0 }),
798
930
  stateStore.listGovernanceSpecSceneOverrideRecords({ limit: 0 }),
799
931
  stateStore.listGovernanceSceneIndexRecords({ limit: 0 }),
932
+ stateStore.listReleaseEvidenceRunRecords({ limit: 0 }),
933
+ stateStore.listReleaseGateHistoryRecords({ limit: 0 }),
800
934
  stateStore.listStateMigrations({ limit: 0 })
801
935
  ]);
802
936
 
@@ -813,6 +947,8 @@ async function runStateExport(options = {}, dependencies = {}) {
813
947
  errorbook_incident_index_registry: Array.isArray(errorbookIncidentRows) ? errorbookIncidentRows : [],
814
948
  governance_spec_scene_override_registry: Array.isArray(governanceOverrideRows) ? governanceOverrideRows : [],
815
949
  governance_scene_index_registry: Array.isArray(governanceSceneRows) ? governanceSceneRows : [],
950
+ release_evidence_run_registry: Array.isArray(releaseEvidenceRows) ? releaseEvidenceRows : [],
951
+ release_gate_history_registry: Array.isArray(releaseGateHistoryRows) ? releaseGateHistoryRows : [],
816
952
  state_migration_registry: Array.isArray(migrations) ? migrations : []
817
953
  },
818
954
  summary: {
@@ -823,6 +959,8 @@ async function runStateExport(options = {}, dependencies = {}) {
823
959
  errorbook_incident_index_registry: Array.isArray(errorbookIncidentRows) ? errorbookIncidentRows.length : 0,
824
960
  governance_spec_scene_override_registry: Array.isArray(governanceOverrideRows) ? governanceOverrideRows.length : 0,
825
961
  governance_scene_index_registry: Array.isArray(governanceSceneRows) ? governanceSceneRows.length : 0,
962
+ release_evidence_run_registry: Array.isArray(releaseEvidenceRows) ? releaseEvidenceRows.length : 0,
963
+ release_gate_history_registry: Array.isArray(releaseGateHistoryRows) ? releaseGateHistoryRows.length : 0,
826
964
  state_migration_registry: Array.isArray(migrations) ? migrations.length : 0
827
965
  },
828
966
  out_file: path.relative(projectPath, outPath).replace(/\\/g, '/')
@@ -841,6 +979,8 @@ module.exports = {
841
979
  COMPONENT_ERRORBOOK_INCIDENT_INDEX,
842
980
  COMPONENT_SPEC_SCENE_OVERRIDES,
843
981
  COMPONENT_SPEC_SCENE_INDEX,
982
+ COMPONENT_RELEASE_EVIDENCE_RUNS,
983
+ COMPONENT_RELEASE_GATE_HISTORY,
844
984
  COMPONENT_DEFINITIONS,
845
985
  DEFAULT_STATE_EXPORT_PATH,
846
986
  buildStateMigrationPlan,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.6.5",
3
+ "version": "3.6.7",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {