scene-capability-engine 3.6.38 → 3.6.39
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 +18 -0
- package/docs/command-reference.md +27 -0
- package/docs/document-governance.md +22 -2
- package/docs/releases/README.md +1 -0
- package/docs/releases/v3.6.39.md +24 -0
- package/docs/state-migration-reconciliation-runbook.md +76 -0
- package/docs/state-storage-tiering.md +104 -0
- package/docs/zh/releases/README.md +1 -0
- package/docs/zh/releases/v3.6.39.md +24 -0
- package/lib/commands/docs.js +8 -2
- package/lib/commands/scene.js +78 -18
- package/lib/commands/watch.js +10 -1
- package/lib/governance/config-manager.js +16 -0
- package/lib/governance/diagnostic-engine.js +2 -1
- package/lib/governance/validation-engine.js +3 -2
- package/lib/runtime/session-store.js +8 -0
- package/lib/state/sce-state-store.js +265 -0
- package/lib/state/state-migration-manager.js +27 -2
- package/lib/state/state-storage-policy.js +179 -0
- package/lib/watch/action-executor.js +10 -1
- package/lib/watch/event-debouncer.js +3 -0
- package/lib/watch/file-watcher.js +51 -10
- package/lib/watch/watch-manager.js +10 -1
- package/lib/workspace/takeover-baseline.js +11 -0
- package/package.json +5 -1
- package/template/.sce/config/state-storage-policy.json +165 -0
|
@@ -41,6 +41,9 @@ class FileWatcher extends EventEmitter {
|
|
|
41
41
|
};
|
|
42
42
|
this.retryCount = 0;
|
|
43
43
|
this.lastError = null;
|
|
44
|
+
this.basePath = process.cwd();
|
|
45
|
+
this.initializationTimer = null;
|
|
46
|
+
this.recoveryTimer = null;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -93,6 +96,8 @@ class FileWatcher extends EventEmitter {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
try {
|
|
99
|
+
this.basePath = basePath;
|
|
100
|
+
|
|
96
101
|
// 将相对路径转换为绝对路径
|
|
97
102
|
const absolutePatterns = this.config.patterns.map(pattern => {
|
|
98
103
|
if (path.isAbsolute(pattern)) {
|
|
@@ -120,19 +125,22 @@ class FileWatcher extends EventEmitter {
|
|
|
120
125
|
|
|
121
126
|
// 等待 watcher 准备就绪
|
|
122
127
|
await new Promise((resolve, reject) => {
|
|
123
|
-
|
|
128
|
+
this._clearInitializationTimer();
|
|
129
|
+
this.initializationTimer = setTimeout(() => {
|
|
130
|
+
this.initializationTimer = null;
|
|
124
131
|
reject(new Error('FileWatcher initialization timeout'));
|
|
125
132
|
}, 10000);
|
|
133
|
+
this._unrefTimer(this.initializationTimer);
|
|
126
134
|
|
|
127
135
|
this.watcher.once('ready', () => {
|
|
128
|
-
|
|
136
|
+
this._clearInitializationTimer();
|
|
129
137
|
this.isWatching = true;
|
|
130
138
|
this.stats.startedAt = new Date();
|
|
131
139
|
resolve();
|
|
132
140
|
});
|
|
133
141
|
|
|
134
142
|
this.watcher.once('error', (error) => {
|
|
135
|
-
|
|
143
|
+
this._clearInitializationTimer();
|
|
136
144
|
reject(error);
|
|
137
145
|
});
|
|
138
146
|
});
|
|
@@ -140,6 +148,7 @@ class FileWatcher extends EventEmitter {
|
|
|
140
148
|
this.emit('started', { patterns: this.config.patterns });
|
|
141
149
|
} catch (error) {
|
|
142
150
|
this.isWatching = false;
|
|
151
|
+
await this._disposeWatcher();
|
|
143
152
|
throw error;
|
|
144
153
|
}
|
|
145
154
|
}
|
|
@@ -150,15 +159,14 @@ class FileWatcher extends EventEmitter {
|
|
|
150
159
|
* @returns {Promise<void>}
|
|
151
160
|
*/
|
|
152
161
|
async stop() {
|
|
153
|
-
if (!this.isWatching) {
|
|
162
|
+
if (!this.isWatching && !this.watcher && !this.initializationTimer && !this.recoveryTimer) {
|
|
154
163
|
return;
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
167
|
+
this._clearInitializationTimer();
|
|
168
|
+
this._clearRecoveryTimer();
|
|
169
|
+
await this._disposeWatcher();
|
|
162
170
|
|
|
163
171
|
this.isWatching = false;
|
|
164
172
|
this.watchedFiles.clear();
|
|
@@ -300,10 +308,12 @@ class FileWatcher extends EventEmitter {
|
|
|
300
308
|
});
|
|
301
309
|
|
|
302
310
|
// 延迟后重试
|
|
303
|
-
|
|
311
|
+
this._clearRecoveryTimer();
|
|
312
|
+
this.recoveryTimer = setTimeout(async () => {
|
|
313
|
+
this.recoveryTimer = null;
|
|
304
314
|
try {
|
|
305
315
|
// 重启 watcher
|
|
306
|
-
const basePath = this.
|
|
316
|
+
const basePath = this.basePath || process.cwd();
|
|
307
317
|
|
|
308
318
|
await this.stop();
|
|
309
319
|
await this.start(basePath);
|
|
@@ -326,6 +336,7 @@ class FileWatcher extends EventEmitter {
|
|
|
326
336
|
await this._attemptRecovery(recoveryError);
|
|
327
337
|
}
|
|
328
338
|
}, this.config.retryDelay);
|
|
339
|
+
this._unrefTimer(this.recoveryTimer);
|
|
329
340
|
}
|
|
330
341
|
|
|
331
342
|
/**
|
|
@@ -494,6 +505,36 @@ class FileWatcher extends EventEmitter {
|
|
|
494
505
|
lastError: this.lastError ? this.lastError.message : null
|
|
495
506
|
};
|
|
496
507
|
}
|
|
508
|
+
|
|
509
|
+
_unrefTimer(timer) {
|
|
510
|
+
if (timer && typeof timer.unref === 'function') {
|
|
511
|
+
timer.unref();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
_clearInitializationTimer() {
|
|
516
|
+
if (this.initializationTimer) {
|
|
517
|
+
clearTimeout(this.initializationTimer);
|
|
518
|
+
this.initializationTimer = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
_clearRecoveryTimer() {
|
|
523
|
+
if (this.recoveryTimer) {
|
|
524
|
+
clearTimeout(this.recoveryTimer);
|
|
525
|
+
this.recoveryTimer = null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async _disposeWatcher() {
|
|
530
|
+
if (!this.watcher) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const watcher = this.watcher;
|
|
535
|
+
this.watcher = null;
|
|
536
|
+
await watcher.close();
|
|
537
|
+
}
|
|
497
538
|
}
|
|
498
539
|
|
|
499
540
|
module.exports = FileWatcher;
|
|
@@ -6,6 +6,15 @@ const EventDebouncer = require('./event-debouncer');
|
|
|
6
6
|
const ActionExecutor = require('./action-executor');
|
|
7
7
|
const ExecutionLogger = require('./execution-logger');
|
|
8
8
|
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise(resolve => {
|
|
11
|
+
const timer = setTimeout(resolve, ms);
|
|
12
|
+
if (typeof timer.unref === 'function') {
|
|
13
|
+
timer.unref();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* WatchManager - Watch 模式管理器
|
|
11
20
|
*
|
|
@@ -137,7 +146,7 @@ class WatchManager extends EventEmitter {
|
|
|
137
146
|
*/
|
|
138
147
|
async restart() {
|
|
139
148
|
await this.stop();
|
|
140
|
-
await
|
|
149
|
+
await sleep(1000);
|
|
141
150
|
await this.start();
|
|
142
151
|
}
|
|
143
152
|
|
|
@@ -7,6 +7,9 @@ const {
|
|
|
7
7
|
MANIFEST_FILENAME,
|
|
8
8
|
SCE_STEERING_DIR,
|
|
9
9
|
} = require('../runtime/steering-contract');
|
|
10
|
+
const {
|
|
11
|
+
cloneStateStoragePolicyDefaults
|
|
12
|
+
} = require('../state/state-storage-policy');
|
|
10
13
|
|
|
11
14
|
const TAKEOVER_BASELINE_SCHEMA_VERSION = '1.0';
|
|
12
15
|
|
|
@@ -479,6 +482,7 @@ async function applyTakeoverBaseline(projectPath = process.cwd(), options = {})
|
|
|
479
482
|
const problemEvalPolicyPath = path.join(sceRoot, 'config', 'problem-eval-policy.json');
|
|
480
483
|
const problemClosurePolicyPath = path.join(sceRoot, 'config', 'problem-closure-policy.json');
|
|
481
484
|
const studioIntakePolicyPath = path.join(sceRoot, 'config', 'studio-intake-policy.json');
|
|
485
|
+
const stateStoragePolicyPath = path.join(sceRoot, 'config', 'state-storage-policy.json');
|
|
482
486
|
const reportPath = path.join(sceRoot, 'reports', 'takeover-baseline-latest.json');
|
|
483
487
|
|
|
484
488
|
const existingAdoption = await _readJsonSafe(adoptionPath, fileSystem);
|
|
@@ -489,6 +493,7 @@ async function applyTakeoverBaseline(projectPath = process.cwd(), options = {})
|
|
|
489
493
|
const existingProblemEvalPolicy = await _readJsonSafe(problemEvalPolicyPath, fileSystem);
|
|
490
494
|
const existingProblemClosurePolicy = await _readJsonSafe(problemClosurePolicyPath, fileSystem);
|
|
491
495
|
const existingStudioIntakePolicy = await _readJsonSafe(studioIntakePolicyPath, fileSystem);
|
|
496
|
+
const existingStateStoragePolicy = await _readJsonSafe(stateStoragePolicyPath, fileSystem);
|
|
492
497
|
|
|
493
498
|
const desiredAdoption = _buildAdoptionConfig(existingAdoption, nowIso, sceVersion);
|
|
494
499
|
const desiredAutoConfig = _buildAutoConfig(existingAuto);
|
|
@@ -498,6 +503,7 @@ async function applyTakeoverBaseline(projectPath = process.cwd(), options = {})
|
|
|
498
503
|
const desiredProblemEvalPolicy = _deepMerge(existingProblemEvalPolicy || {}, PROBLEM_EVAL_POLICY_DEFAULTS);
|
|
499
504
|
const desiredProblemClosurePolicy = _deepMerge(existingProblemClosurePolicy || {}, PROBLEM_CLOSURE_POLICY_DEFAULTS);
|
|
500
505
|
const desiredStudioIntakePolicy = _deepMerge(existingStudioIntakePolicy || {}, STUDIO_INTAKE_POLICY_DEFAULTS);
|
|
506
|
+
const desiredStateStoragePolicy = _deepMerge(existingStateStoragePolicy || {}, cloneStateStoragePolicyDefaults());
|
|
501
507
|
|
|
502
508
|
const fileResults = [];
|
|
503
509
|
fileResults.push(await _reconcileJsonFile(adoptionPath, desiredAdoption, {
|
|
@@ -540,6 +546,11 @@ async function applyTakeoverBaseline(projectPath = process.cwd(), options = {})
|
|
|
540
546
|
apply,
|
|
541
547
|
fileSystem
|
|
542
548
|
}));
|
|
549
|
+
fileResults.push(await _reconcileJsonFile(stateStoragePolicyPath, desiredStateStoragePolicy, {
|
|
550
|
+
projectPath,
|
|
551
|
+
apply,
|
|
552
|
+
fileSystem
|
|
553
|
+
}));
|
|
543
554
|
fileResults.push(await _reconcileSteeringContract(projectPath, {
|
|
544
555
|
apply,
|
|
545
556
|
fileSystem
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scene-capability-engine",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.39",
|
|
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": {
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"test:sce-tracking": "node scripts/check-sce-tracking.js",
|
|
40
40
|
"test:brand-consistency": "node scripts/check-branding-consistency.js",
|
|
41
41
|
"audit:steering": "node scripts/steering-content-audit.js --fail-on-error",
|
|
42
|
+
"audit:state-storage": "node scripts/state-storage-tiering-audit.js",
|
|
42
43
|
"report:steering-audit": "node scripts/steering-content-audit.js --json",
|
|
44
|
+
"report:state-storage": "node scripts/state-storage-tiering-audit.js --json",
|
|
45
|
+
"report:interactive-approval-projection": "node scripts/interactive-approval-event-projection.js --action doctor --json",
|
|
46
|
+
"audit:interactive-approval-projection": "node scripts/interactive-approval-event-projection.js --action doctor --fail-on-drift --fail-on-parse-error",
|
|
43
47
|
"test:watch": "npx jest --watch",
|
|
44
48
|
"coverage": "npx jest --coverage",
|
|
45
49
|
"report:moqui-metadata": "node scripts/moqui-metadata-extract.js --json",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"strategy": "selective-sqlite-advancement",
|
|
4
|
+
"tiers": {
|
|
5
|
+
"file-source": {
|
|
6
|
+
"description": "Canonical file-backed storage for low-cardinality config, raw evidence, audit streams, and recovery-oriented payloads."
|
|
7
|
+
},
|
|
8
|
+
"sqlite-index": {
|
|
9
|
+
"description": "SQLite index/registry layer for file-backed resources that need high-frequency filtering, sorting, and cross-run aggregation."
|
|
10
|
+
},
|
|
11
|
+
"derived-sqlite-projection": {
|
|
12
|
+
"description": "Disposable SQLite projection rebuilt from canonical files for append-only streams with query pressure."
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"admission": {
|
|
16
|
+
"required_signals": [
|
|
17
|
+
"cross-run or cross-session query pressure is proven",
|
|
18
|
+
"file scans are materially weaker than indexed filtering/sorting",
|
|
19
|
+
"sqlite content remains rebuildable from a canonical source",
|
|
20
|
+
"operator diagnostics and reconcile path are defined before rollout"
|
|
21
|
+
],
|
|
22
|
+
"deny_if_any": [
|
|
23
|
+
"resource is the only copy of raw audit or evidence payload",
|
|
24
|
+
"resource is low-cardinality personal workspace or preference state",
|
|
25
|
+
"human-readable diff and manual recovery are more valuable than query speed",
|
|
26
|
+
"migration would introduce silent source-of-truth cutover"
|
|
27
|
+
],
|
|
28
|
+
"future_candidate_checklist": [
|
|
29
|
+
"identify canonical source path or stream",
|
|
30
|
+
"document expected query patterns and consumers",
|
|
31
|
+
"define rebuild and reconcile semantics",
|
|
32
|
+
"define release gate or audit behavior for drift states",
|
|
33
|
+
"document why existing file-only storage is insufficient"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"component_scope": [
|
|
37
|
+
{
|
|
38
|
+
"component_id": "collab.agent-registry",
|
|
39
|
+
"tier": "sqlite-index",
|
|
40
|
+
"canonical_source": "file",
|
|
41
|
+
"source_path": ".sce/config/agent-registry.json",
|
|
42
|
+
"sqlite_tables": ["agent_runtime_registry"],
|
|
43
|
+
"rationale": "Registry-style lookup with repeated status and capability queries."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"component_id": "runtime.timeline-index",
|
|
47
|
+
"tier": "sqlite-index",
|
|
48
|
+
"canonical_source": "file",
|
|
49
|
+
"source_path": ".sce/timeline/index.json",
|
|
50
|
+
"sqlite_tables": ["timeline_snapshot_registry"],
|
|
51
|
+
"rationale": "Timeline index benefits from filtered and cross-session reads while file snapshots remain recoverable source artifacts."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"component_id": "runtime.scene-session-index",
|
|
55
|
+
"tier": "sqlite-index",
|
|
56
|
+
"canonical_source": "file",
|
|
57
|
+
"source_path": ".sce/session-governance/scene-index.json",
|
|
58
|
+
"sqlite_tables": ["scene_session_cycle_registry"],
|
|
59
|
+
"rationale": "Scene/session lookups have query pressure and consistency checks but still rely on file session payloads."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"component_id": "errorbook.entry-index",
|
|
63
|
+
"tier": "sqlite-index",
|
|
64
|
+
"canonical_source": "file",
|
|
65
|
+
"source_path": ".sce/errorbook/index.json",
|
|
66
|
+
"sqlite_tables": ["errorbook_entry_index_registry"],
|
|
67
|
+
"rationale": "Promoted errorbook registry queries benefit from indexed status and quality filtering."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"component_id": "errorbook.incident-index",
|
|
71
|
+
"tier": "sqlite-index",
|
|
72
|
+
"canonical_source": "file",
|
|
73
|
+
"source_path": ".sce/errorbook/staging/index.json",
|
|
74
|
+
"sqlite_tables": ["errorbook_incident_index_registry"],
|
|
75
|
+
"rationale": "Incident staging state requires queryable triage views without replacing raw incident artifacts."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"component_id": "governance.spec-scene-overrides",
|
|
79
|
+
"tier": "sqlite-index",
|
|
80
|
+
"canonical_source": "file",
|
|
81
|
+
"source_path": ".sce/spec-governance/spec-scene-overrides.json",
|
|
82
|
+
"sqlite_tables": ["governance_spec_scene_override_registry"],
|
|
83
|
+
"rationale": "Override lookups are registry-like and join naturally with other governance indexes."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"component_id": "governance.scene-index",
|
|
87
|
+
"tier": "sqlite-index",
|
|
88
|
+
"canonical_source": "file",
|
|
89
|
+
"source_path": ".sce/spec-governance/scene-index.json",
|
|
90
|
+
"sqlite_tables": ["governance_scene_index_registry"],
|
|
91
|
+
"rationale": "Scene governance summaries are better served by indexed counts and status filters."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"component_id": "release.evidence-runs-index",
|
|
95
|
+
"tier": "sqlite-index",
|
|
96
|
+
"canonical_source": "file",
|
|
97
|
+
"source_path": ".sce/reports/release-evidence/handoff-runs.json",
|
|
98
|
+
"sqlite_tables": ["release_evidence_run_registry"],
|
|
99
|
+
"rationale": "Release evidence run summaries need fast historical querying while release assets remain file-backed."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"component_id": "release.gate-history-index",
|
|
103
|
+
"tier": "sqlite-index",
|
|
104
|
+
"canonical_source": "file",
|
|
105
|
+
"source_path": ".sce/reports/release-evidence/release-gate-history.json",
|
|
106
|
+
"sqlite_tables": ["release_gate_history_registry"],
|
|
107
|
+
"rationale": "Gate history is registry-shaped and queried by tag, pass/fail, and drift metrics."
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"resource_rules": [
|
|
111
|
+
{
|
|
112
|
+
"rule_id": "workspace-personal-state",
|
|
113
|
+
"tier": "file-source",
|
|
114
|
+
"explicit_paths": ["~/.sce/workspace-state.json"],
|
|
115
|
+
"derived_projection_allowed": false,
|
|
116
|
+
"source_replacement_allowed": false,
|
|
117
|
+
"rationale": "Personal workspace selection and preferences are low-cardinality, atomic, and not worth migrating into SQLite."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"rule_id": "append-only-report-streams",
|
|
121
|
+
"tier": "file-source",
|
|
122
|
+
"path_patterns": [".sce/reports/**/*.jsonl"],
|
|
123
|
+
"derived_projection_allowed": true,
|
|
124
|
+
"source_replacement_allowed": false,
|
|
125
|
+
"rationale": "Raw governance and evidence streams must stay append-only files; projection is allowed only for query acceleration."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"rule_id": "append-only-audit-streams",
|
|
129
|
+
"tier": "file-source",
|
|
130
|
+
"path_patterns": [".sce/audit/**/*.jsonl"],
|
|
131
|
+
"derived_projection_allowed": true,
|
|
132
|
+
"source_replacement_allowed": false,
|
|
133
|
+
"rationale": "Audit streams remain canonical evidence and should never become SQLite-only write paths."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"rule_id": "timeline-snapshot-payloads",
|
|
137
|
+
"tier": "file-source",
|
|
138
|
+
"path_patterns": [".sce/timeline/snapshots/**"],
|
|
139
|
+
"derived_projection_allowed": false,
|
|
140
|
+
"source_replacement_allowed": false,
|
|
141
|
+
"rationale": "Timeline snapshots are recovery-oriented payload artifacts, not registry data."
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"rule_id": "session-payload-artifacts",
|
|
145
|
+
"tier": "file-source",
|
|
146
|
+
"path_patterns": [".sce/session-governance/sessions/**"],
|
|
147
|
+
"derived_projection_allowed": false,
|
|
148
|
+
"source_replacement_allowed": false,
|
|
149
|
+
"rationale": "Session payloads must stay file-backed for recovery, archive, and manual debugging."
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"rule_id": "release-evidence-assets",
|
|
153
|
+
"tier": "file-source",
|
|
154
|
+
"path_patterns": [
|
|
155
|
+
".sce/reports/release-evidence/**/*.json",
|
|
156
|
+
".sce/reports/release-evidence/**/*.md",
|
|
157
|
+
".sce/reports/release-evidence/**/*.jsonl",
|
|
158
|
+
".sce/reports/release-evidence/**/*.lines"
|
|
159
|
+
],
|
|
160
|
+
"derived_projection_allowed": true,
|
|
161
|
+
"source_replacement_allowed": false,
|
|
162
|
+
"rationale": "Release evidence assets remain portable files even when selected summary indexes are mirrored into SQLite."
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|