scene-capability-engine 3.5.0 → 3.5.2

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.
@@ -1,6 +1,10 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { DOMAIN_CHAIN_RELATIVE_PATH } = require('./domain-modeling');
4
+ const {
5
+ loadSceneBindingOverrides,
6
+ resolveSceneIdFromOverrides
7
+ } = require('./scene-binding-overrides');
4
8
 
5
9
  function normalizeText(value) {
6
10
  if (typeof value !== 'string') {
@@ -70,6 +74,8 @@ async function resolveSpecSearchEntries(projectPath, fileSystem = fs) {
70
74
  return [];
71
75
  }
72
76
 
77
+ const overrideContext = await loadSceneBindingOverrides(projectPath, {}, fileSystem);
78
+ const overrides = overrideContext.overrides;
73
79
  const names = await fileSystem.readdir(specsRoot);
74
80
  const entries = [];
75
81
 
@@ -106,7 +112,10 @@ async function resolveSpecSearchEntries(projectPath, fileSystem = fs) {
106
112
  ]);
107
113
 
108
114
  const sceneId = normalizeText(
109
- (domainChain && domainChain.scene_id) || extractSceneIdFromSceneSpec(sceneSpecContent) || ''
115
+ (domainChain && domainChain.scene_id)
116
+ || extractSceneIdFromSceneSpec(sceneSpecContent)
117
+ || resolveSceneIdFromOverrides(specId, overrides)
118
+ || ''
110
119
  ) || null;
111
120
  const problemStatement = normalizeText(
112
121
  (domainChain && domainChain.problem && domainChain.problem.statement) || ''
@@ -257,4 +266,3 @@ module.exports = {
257
266
  calculateSpecRelevance,
258
267
  findRelatedSpecs
259
268
  };
260
-
@@ -0,0 +1,115 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+
4
+ const DEFAULT_SPEC_SCENE_OVERRIDE_PATH = '.sce/spec-governance/spec-scene-overrides.json';
5
+
6
+ function normalizeText(value) {
7
+ if (typeof value !== 'string') {
8
+ return '';
9
+ }
10
+ return value.trim();
11
+ }
12
+
13
+ function normalizeOverrideEntry(specId, payload = {}) {
14
+ const normalizedSpecId = normalizeText(specId);
15
+ if (!normalizedSpecId) {
16
+ return null;
17
+ }
18
+
19
+ if (typeof payload === 'string') {
20
+ const sceneId = normalizeText(payload);
21
+ if (!sceneId) {
22
+ return null;
23
+ }
24
+ return {
25
+ spec_id: normalizedSpecId,
26
+ scene_id: sceneId,
27
+ source: 'override',
28
+ rule_id: null,
29
+ updated_at: null
30
+ };
31
+ }
32
+
33
+ const sceneId = normalizeText(payload && payload.scene_id);
34
+ if (!sceneId) {
35
+ return null;
36
+ }
37
+ return {
38
+ spec_id: normalizedSpecId,
39
+ scene_id: sceneId,
40
+ source: normalizeText(payload.source) || 'override',
41
+ rule_id: normalizeText(payload.rule_id) || null,
42
+ updated_at: normalizeText(payload.updated_at) || null
43
+ };
44
+ }
45
+
46
+ function normalizeSceneBindingOverrides(raw = {}) {
47
+ const payload = raw && typeof raw === 'object' ? raw : {};
48
+ const mappingsRaw = payload.mappings && typeof payload.mappings === 'object'
49
+ ? payload.mappings
50
+ : {};
51
+ const mappings = {};
52
+ for (const [specId, entry] of Object.entries(mappingsRaw)) {
53
+ const normalized = normalizeOverrideEntry(specId, entry);
54
+ if (!normalized) {
55
+ continue;
56
+ }
57
+ mappings[normalized.spec_id] = {
58
+ scene_id: normalized.scene_id,
59
+ source: normalized.source,
60
+ rule_id: normalized.rule_id,
61
+ updated_at: normalized.updated_at
62
+ };
63
+ }
64
+ return {
65
+ schema_version: normalizeText(payload.schema_version) || '1.0',
66
+ generated_at: normalizeText(payload.generated_at) || null,
67
+ updated_at: normalizeText(payload.updated_at) || null,
68
+ mappings
69
+ };
70
+ }
71
+
72
+ async function loadSceneBindingOverrides(projectPath = process.cwd(), options = {}, fileSystem = fs) {
73
+ const overridePath = normalizeText(options.override_path || options.overridePath)
74
+ || DEFAULT_SPEC_SCENE_OVERRIDE_PATH;
75
+ const absolutePath = path.join(projectPath, overridePath);
76
+ let payload = {};
77
+ let loadedFrom = 'default';
78
+ if (await fileSystem.pathExists(absolutePath)) {
79
+ try {
80
+ payload = await fileSystem.readJson(absolutePath);
81
+ loadedFrom = 'file';
82
+ } catch (_error) {
83
+ payload = {};
84
+ loadedFrom = 'default';
85
+ }
86
+ }
87
+ return {
88
+ override_path: overridePath,
89
+ absolute_path: absolutePath,
90
+ loaded_from: loadedFrom,
91
+ overrides: normalizeSceneBindingOverrides(payload)
92
+ };
93
+ }
94
+
95
+ function resolveSceneIdFromOverrides(specId, overrides = {}) {
96
+ const normalizedSpecId = normalizeText(specId);
97
+ if (!normalizedSpecId) {
98
+ return null;
99
+ }
100
+ const mappings = overrides && typeof overrides === 'object' && overrides.mappings
101
+ ? overrides.mappings
102
+ : {};
103
+ const entry = mappings[normalizedSpecId];
104
+ if (!entry || typeof entry !== 'object') {
105
+ return null;
106
+ }
107
+ return normalizeText(entry.scene_id) || null;
108
+ }
109
+
110
+ module.exports = {
111
+ DEFAULT_SPEC_SCENE_OVERRIDE_PATH,
112
+ normalizeSceneBindingOverrides,
113
+ loadSceneBindingOverrides,
114
+ resolveSceneIdFromOverrides
115
+ };
@@ -2,17 +2,63 @@ const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { DraftGenerator } = require('../spec/bootstrap/draft-generator');
4
4
  const { ensureSpecDomainArtifacts } = require('../spec/domain-modeling');
5
+ const {
6
+ DEFAULT_SPEC_SCENE_OVERRIDE_PATH,
7
+ loadSceneBindingOverrides,
8
+ normalizeSceneBindingOverrides,
9
+ resolveSceneIdFromOverrides
10
+ } = require('../spec/scene-binding-overrides');
5
11
 
6
12
  const DEFAULT_STUDIO_INTAKE_POLICY_PATH = '.sce/config/studio-intake-policy.json';
7
13
  const DEFAULT_STUDIO_GOVERNANCE_DIR = '.sce/spec-governance';
8
14
  const DEFAULT_STUDIO_PORTFOLIO_REPORT = `${DEFAULT_STUDIO_GOVERNANCE_DIR}/scene-portfolio.latest.json`;
9
15
  const DEFAULT_STUDIO_SCENE_INDEX = `${DEFAULT_STUDIO_GOVERNANCE_DIR}/scene-index.json`;
16
+ const DEFAULT_STUDIO_SCENE_OVERRIDE_PATH = DEFAULT_SPEC_SCENE_OVERRIDE_PATH;
17
+
18
+ const DEFAULT_STUDIO_SCENE_BACKFILL_RULES = Object.freeze([
19
+ {
20
+ id: 'moqui-core',
21
+ scene_id: 'scene.moqui-core',
22
+ keywords: ['moqui']
23
+ },
24
+ {
25
+ id: 'orchestration',
26
+ scene_id: 'scene.sce-orchestration',
27
+ keywords: ['orchestrate', 'runtime', 'controller', 'batch', 'parallel']
28
+ },
29
+ {
30
+ id: 'template-registry',
31
+ scene_id: 'scene.sce-template-registry',
32
+ keywords: ['template', 'scene-package', 'registry', 'catalog', 'scene-template']
33
+ },
34
+ {
35
+ id: 'spec-governance',
36
+ scene_id: 'scene.sce-spec-governance',
37
+ keywords: ['spec', 'gate', 'ontology', 'governance', 'policy']
38
+ },
39
+ {
40
+ id: 'quality',
41
+ scene_id: 'scene.sce-quality',
42
+ keywords: ['test', 'quality', 'stability', 'jest', 'coverage']
43
+ },
44
+ {
45
+ id: 'docs',
46
+ scene_id: 'scene.sce-docs',
47
+ keywords: ['document', 'documentation', 'onboarding', 'guide']
48
+ },
49
+ {
50
+ id: 'platform',
51
+ scene_id: 'scene.sce-platform',
52
+ keywords: ['adopt', 'upgrade', 'workspace', 'repo', 'environment', 'devops', 'release', 'github', 'npm']
53
+ }
54
+ ]);
10
55
 
11
56
  const DEFAULT_STUDIO_INTAKE_POLICY = Object.freeze({
12
57
  schema_version: '1.0',
13
58
  enabled: true,
14
59
  auto_create_spec: true,
15
60
  force_spec_for_studio_plan: true,
61
+ allow_manual_spec_override: false,
16
62
  prefer_existing_scene_spec: true,
17
63
  related_spec_min_score: 45,
18
64
  allow_new_spec_when_goal_diverges: true,
@@ -33,9 +79,17 @@ const DEFAULT_STUDIO_INTAKE_POLICY = Object.freeze({
33
79
  },
34
80
  governance: {
35
81
  auto_run_on_plan: true,
82
+ require_auto_on_plan: true,
36
83
  max_active_specs_per_scene: 3,
37
84
  stale_days: 14,
38
85
  duplicate_similarity_threshold: 0.66
86
+ },
87
+ backfill: {
88
+ enabled: true,
89
+ active_only_default: true,
90
+ default_scene_id: 'scene.sce-core',
91
+ override_file: DEFAULT_STUDIO_SCENE_OVERRIDE_PATH,
92
+ rules: DEFAULT_STUDIO_SCENE_BACKFILL_RULES
39
93
  }
40
94
  });
41
95
 
@@ -87,6 +141,30 @@ function normalizeTextList(value = []) {
87
141
  .filter(Boolean);
88
142
  }
89
143
 
144
+ function normalizeBackfillRules(value = []) {
145
+ if (!Array.isArray(value)) {
146
+ return [];
147
+ }
148
+ const rules = [];
149
+ for (const item of value) {
150
+ if (!item || typeof item !== 'object') {
151
+ continue;
152
+ }
153
+ const id = normalizeText(item.id);
154
+ const sceneId = normalizeText(item.scene_id || item.sceneId);
155
+ const keywords = normalizeTextList(item.keywords || item.match_any_keywords || item.matchAnyKeywords);
156
+ if (!id || !sceneId || keywords.length === 0) {
157
+ continue;
158
+ }
159
+ rules.push({
160
+ id,
161
+ scene_id: sceneId,
162
+ keywords
163
+ });
164
+ }
165
+ return rules;
166
+ }
167
+
90
168
  function toRelativePosix(projectPath, absolutePath) {
91
169
  return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
92
170
  }
@@ -169,6 +247,8 @@ function normalizeStudioIntakePolicy(raw = {}) {
169
247
  const payload = raw && typeof raw === 'object' ? raw : {};
170
248
  const specId = payload.spec_id && typeof payload.spec_id === 'object' ? payload.spec_id : {};
171
249
  const governance = payload.governance && typeof payload.governance === 'object' ? payload.governance : {};
250
+ const backfill = payload.backfill && typeof payload.backfill === 'object' ? payload.backfill : {};
251
+ const normalizedBackfillRules = normalizeBackfillRules(backfill.rules);
172
252
 
173
253
  return {
174
254
  schema_version: normalizeText(payload.schema_version) || DEFAULT_STUDIO_INTAKE_POLICY.schema_version,
@@ -178,6 +258,10 @@ function normalizeStudioIntakePolicy(raw = {}) {
178
258
  payload.force_spec_for_studio_plan,
179
259
  DEFAULT_STUDIO_INTAKE_POLICY.force_spec_for_studio_plan
180
260
  ),
261
+ allow_manual_spec_override: normalizeBoolean(
262
+ payload.allow_manual_spec_override,
263
+ DEFAULT_STUDIO_INTAKE_POLICY.allow_manual_spec_override
264
+ ),
181
265
  prefer_existing_scene_spec: normalizeBoolean(
182
266
  payload.prefer_existing_scene_spec,
183
267
  DEFAULT_STUDIO_INTAKE_POLICY.prefer_existing_scene_spec
@@ -224,6 +308,10 @@ function normalizeStudioIntakePolicy(raw = {}) {
224
308
  governance.auto_run_on_plan,
225
309
  DEFAULT_STUDIO_INTAKE_POLICY.governance.auto_run_on_plan
226
310
  ),
311
+ require_auto_on_plan: normalizeBoolean(
312
+ governance.require_auto_on_plan,
313
+ DEFAULT_STUDIO_INTAKE_POLICY.governance.require_auto_on_plan
314
+ ),
227
315
  max_active_specs_per_scene: normalizeInteger(
228
316
  governance.max_active_specs_per_scene,
229
317
  DEFAULT_STUDIO_INTAKE_POLICY.governance.max_active_specs_per_scene,
@@ -243,6 +331,23 @@ function normalizeStudioIntakePolicy(raw = {}) {
243
331
  DEFAULT_STUDIO_INTAKE_POLICY.governance.duplicate_similarity_threshold
244
332
  ))
245
333
  )
334
+ },
335
+ backfill: {
336
+ enabled: normalizeBoolean(
337
+ backfill.enabled,
338
+ DEFAULT_STUDIO_INTAKE_POLICY.backfill.enabled
339
+ ),
340
+ active_only_default: normalizeBoolean(
341
+ backfill.active_only_default,
342
+ DEFAULT_STUDIO_INTAKE_POLICY.backfill.active_only_default
343
+ ),
344
+ default_scene_id: normalizeText(backfill.default_scene_id)
345
+ || DEFAULT_STUDIO_INTAKE_POLICY.backfill.default_scene_id,
346
+ override_file: normalizeText(backfill.override_file)
347
+ || DEFAULT_STUDIO_INTAKE_POLICY.backfill.override_file,
348
+ rules: normalizedBackfillRules.length > 0
349
+ ? normalizedBackfillRules
350
+ : normalizeBackfillRules(DEFAULT_STUDIO_INTAKE_POLICY.backfill.rules)
246
351
  }
247
352
  };
248
353
  }
@@ -611,6 +716,12 @@ async function runStudioAutoIntake(options = {}, dependencies = {}) {
611
716
  created_spec: null
612
717
  };
613
718
 
719
+ if (skip && policy.allow_manual_spec_override !== true) {
720
+ throw new Error(
721
+ 'manual spec override is disabled by studio intake policy (allow_manual_spec_override=false)'
722
+ );
723
+ }
724
+
614
725
  if (skip) {
615
726
  payload.enabled = false;
616
727
  payload.decision = {
@@ -705,6 +816,10 @@ async function scanSpecPortfolio(projectPath = process.cwd(), options = {}, depe
705
816
  return [];
706
817
  }
707
818
  const staleDays = normalizeInteger(options.stale_days, 14, 1, 3650);
819
+ const overrideContext = await loadSceneBindingOverrides(projectPath, {
820
+ overridePath: options.override_file || DEFAULT_STUDIO_SCENE_OVERRIDE_PATH
821
+ }, fileSystem);
822
+ const sceneOverrides = normalizeSceneBindingOverrides(overrideContext.overrides || {});
708
823
  const entries = await fileSystem.readdir(specsRoot);
709
824
  const records = [];
710
825
 
@@ -733,9 +848,12 @@ async function scanSpecPortfolio(projectPath = process.cwd(), options = {}, depe
733
848
  readFileSafe(tasksPath, fileSystem)
734
849
  ]);
735
850
 
736
- const sceneId = normalizeText(
737
- chain && chain.scene_id ? chain.scene_id : ''
738
- ) || 'scene.unassigned';
851
+ const sceneFromChain = normalizeText(chain && chain.scene_id ? chain.scene_id : '');
852
+ const sceneFromOverride = resolveSceneIdFromOverrides(entry, sceneOverrides);
853
+ const sceneId = sceneFromChain || sceneFromOverride || 'scene.unassigned';
854
+ const sceneSource = sceneFromChain
855
+ ? 'domain-chain'
856
+ : (sceneFromOverride ? 'override' : 'unassigned');
739
857
  const problemStatement = normalizeText(
740
858
  (chain && chain.problem && chain.problem.statement)
741
859
  || (contract && contract.issue_statement)
@@ -767,6 +885,7 @@ async function scanSpecPortfolio(projectPath = process.cwd(), options = {}, depe
767
885
  tasks_progress: taskProgress.ratio,
768
886
  lifecycle_state: lifecycle.state,
769
887
  age_days: lifecycle.age_days,
888
+ scene_source: sceneSource,
770
889
  tokens
771
890
  });
772
891
  }
@@ -889,6 +1008,202 @@ function buildSceneGovernanceReport(records = [], policy = DEFAULT_STUDIO_INTAKE
889
1008
  };
890
1009
  }
891
1010
 
1011
+ function classifyBackfillRule(record = {}, backfillPolicy = {}) {
1012
+ const rules = Array.isArray(backfillPolicy.rules) ? backfillPolicy.rules : [];
1013
+ const defaultSceneId = normalizeText(backfillPolicy.default_scene_id) || 'scene.sce-core';
1014
+ const searchText = [
1015
+ normalizeText(record.spec_id).toLowerCase(),
1016
+ normalizeText(record.problem_statement).toLowerCase()
1017
+ ].join(' ');
1018
+ const searchTokens = new Set(tokenizeText(searchText));
1019
+ let bestMatch = null;
1020
+
1021
+ for (const rule of rules) {
1022
+ const ruleId = normalizeText(rule.id);
1023
+ const sceneId = normalizeText(rule.scene_id);
1024
+ const keywords = normalizeTextList(rule.keywords).map((item) => item.toLowerCase());
1025
+ if (!ruleId || !sceneId || keywords.length === 0) {
1026
+ continue;
1027
+ }
1028
+
1029
+ const matchedKeywords = [];
1030
+ for (const keyword of keywords) {
1031
+ if (!keyword) {
1032
+ continue;
1033
+ }
1034
+ if (searchText.includes(keyword) || searchTokens.has(keyword)) {
1035
+ matchedKeywords.push(keyword);
1036
+ }
1037
+ }
1038
+ if (matchedKeywords.length === 0) {
1039
+ continue;
1040
+ }
1041
+
1042
+ const score = matchedKeywords.length;
1043
+ if (!bestMatch || score > bestMatch.score) {
1044
+ bestMatch = {
1045
+ rule_id: ruleId,
1046
+ scene_id: sceneId,
1047
+ matched_keywords: matchedKeywords,
1048
+ score
1049
+ };
1050
+ }
1051
+ }
1052
+
1053
+ if (!bestMatch) {
1054
+ return {
1055
+ scene_id: defaultSceneId,
1056
+ rule_id: 'default',
1057
+ matched_keywords: [],
1058
+ confidence: 'low',
1059
+ source: 'default'
1060
+ };
1061
+ }
1062
+
1063
+ return {
1064
+ scene_id: bestMatch.scene_id,
1065
+ rule_id: bestMatch.rule_id,
1066
+ matched_keywords: bestMatch.matched_keywords,
1067
+ confidence: bestMatch.score >= 2 ? 'high' : 'medium',
1068
+ source: 'rule'
1069
+ };
1070
+ }
1071
+
1072
+ function clampBackfillLimit(value, fallback = 0, max = 1000) {
1073
+ if (value === undefined || value === null || value === '') {
1074
+ return fallback;
1075
+ }
1076
+ const parsed = Number.parseInt(String(value), 10);
1077
+ if (!Number.isFinite(parsed) || parsed < 0) {
1078
+ return fallback;
1079
+ }
1080
+ return Math.min(parsed, max);
1081
+ }
1082
+
1083
+ async function runStudioSceneBackfill(options = {}, dependencies = {}) {
1084
+ const projectPath = dependencies.projectPath || process.cwd();
1085
+ const fileSystem = dependencies.fileSystem || fs;
1086
+ const loaded = options.policy && typeof options.policy === 'object'
1087
+ ? { policy: normalizeStudioIntakePolicy(options.policy), policy_path: '(inline)', loaded_from: 'inline' }
1088
+ : await loadStudioIntakePolicy(projectPath, fileSystem);
1089
+ const policy = loaded.policy;
1090
+ const backfillPolicy = policy.backfill || DEFAULT_STUDIO_INTAKE_POLICY.backfill;
1091
+ const apply = options.apply === true;
1092
+ const refreshGovernance = options.refresh_governance !== false && options.refreshGovernance !== false;
1093
+ const sourceScene = normalizeText(options.source_scene || options.sourceScene || options.scene) || 'scene.unassigned';
1094
+ const includeAll = options.all === true || options.active_only === false || options.activeOnly === false;
1095
+ const activeOnly = includeAll ? false : (options.active_only === true || options.activeOnly === true || backfillPolicy.active_only_default !== false);
1096
+ const limit = clampBackfillLimit(options.limit, 0, 2000);
1097
+ const overrideFile = normalizeText(backfillPolicy.override_file) || DEFAULT_STUDIO_SCENE_OVERRIDE_PATH;
1098
+
1099
+ const records = await scanSpecPortfolio(projectPath, {
1100
+ stale_days: policy.governance && policy.governance.stale_days,
1101
+ override_file: overrideFile
1102
+ }, {
1103
+ fileSystem
1104
+ });
1105
+
1106
+ let candidates = records.filter((item) => normalizeText(item.scene_id) === sourceScene);
1107
+ if (activeOnly) {
1108
+ candidates = candidates.filter((item) => item.lifecycle_state === 'active');
1109
+ }
1110
+ if (limit > 0) {
1111
+ candidates = candidates.slice(0, limit);
1112
+ }
1113
+
1114
+ const assignmentPlan = candidates.map((record) => {
1115
+ const decision = classifyBackfillRule(record, backfillPolicy);
1116
+ return {
1117
+ spec_id: record.spec_id,
1118
+ from_scene_id: sourceScene,
1119
+ to_scene_id: decision.scene_id,
1120
+ lifecycle_state: record.lifecycle_state,
1121
+ rule_id: decision.rule_id,
1122
+ source: decision.source,
1123
+ confidence: decision.confidence,
1124
+ matched_keywords: decision.matched_keywords
1125
+ };
1126
+ });
1127
+
1128
+ const overrideContext = await loadSceneBindingOverrides(projectPath, {
1129
+ overridePath: overrideFile
1130
+ }, fileSystem);
1131
+ const existingOverrides = normalizeSceneBindingOverrides(overrideContext.overrides || {});
1132
+ const nextOverrides = normalizeSceneBindingOverrides(existingOverrides);
1133
+ const now = new Date().toISOString();
1134
+ let changedCount = 0;
1135
+
1136
+ for (const item of assignmentPlan) {
1137
+ const existing = existingOverrides.mappings[item.spec_id];
1138
+ const currentScene = normalizeText(existing && existing.scene_id);
1139
+ if (currentScene === item.to_scene_id) {
1140
+ continue;
1141
+ }
1142
+ nextOverrides.mappings[item.spec_id] = {
1143
+ scene_id: item.to_scene_id,
1144
+ source: 'scene-backfill',
1145
+ rule_id: item.rule_id,
1146
+ updated_at: now
1147
+ };
1148
+ changedCount += 1;
1149
+ }
1150
+
1151
+ const totalsByTargetScene = {};
1152
+ for (const item of assignmentPlan) {
1153
+ totalsByTargetScene[item.to_scene_id] = (totalsByTargetScene[item.to_scene_id] || 0) + 1;
1154
+ }
1155
+
1156
+ const payload = {
1157
+ mode: 'studio-scene-backfill',
1158
+ success: true,
1159
+ generated_at: now,
1160
+ policy_path: loaded.policy_path,
1161
+ policy_loaded_from: loaded.loaded_from,
1162
+ source_scene: sourceScene,
1163
+ active_only: activeOnly,
1164
+ apply,
1165
+ refresh_governance: refreshGovernance,
1166
+ override_file: overrideFile,
1167
+ summary: {
1168
+ candidate_count: assignmentPlan.length,
1169
+ changed_count: changedCount,
1170
+ target_scene_count: Object.keys(totalsByTargetScene).length
1171
+ },
1172
+ targets: totalsByTargetScene,
1173
+ assignments: assignmentPlan
1174
+ };
1175
+
1176
+ if (apply) {
1177
+ const overrideAbsolutePath = path.join(projectPath, overrideFile);
1178
+ await fileSystem.ensureDir(path.dirname(overrideAbsolutePath));
1179
+ const serialized = {
1180
+ schema_version: '1.0',
1181
+ generated_at: nextOverrides.generated_at || now,
1182
+ updated_at: now,
1183
+ source: 'studio-scene-backfill',
1184
+ mappings: nextOverrides.mappings
1185
+ };
1186
+ await fileSystem.writeJson(overrideAbsolutePath, serialized, { spaces: 2 });
1187
+ payload.override_written = overrideFile;
1188
+ if (refreshGovernance) {
1189
+ const refreshed = await runStudioSpecGovernance({
1190
+ apply: true
1191
+ }, {
1192
+ projectPath,
1193
+ fileSystem
1194
+ });
1195
+ payload.governance = {
1196
+ status: refreshed.summary ? refreshed.summary.status : null,
1197
+ alert_count: refreshed.summary ? Number(refreshed.summary.alert_count || 0) : 0,
1198
+ report_file: refreshed.report_file || null,
1199
+ scene_index_file: refreshed.scene_index_file || null
1200
+ };
1201
+ }
1202
+ }
1203
+
1204
+ return payload;
1205
+ }
1206
+
892
1207
  async function runStudioSpecGovernance(options = {}, dependencies = {}) {
893
1208
  const projectPath = dependencies.projectPath || process.cwd();
894
1209
  const fileSystem = dependencies.fileSystem || fs;
@@ -897,11 +1212,14 @@ async function runStudioSpecGovernance(options = {}, dependencies = {}) {
897
1212
  : await loadStudioIntakePolicy(projectPath, fileSystem);
898
1213
  const policy = loaded.policy;
899
1214
  const governance = policy.governance || DEFAULT_STUDIO_INTAKE_POLICY.governance;
1215
+ const backfill = policy.backfill || DEFAULT_STUDIO_INTAKE_POLICY.backfill;
900
1216
  const apply = options.apply !== false;
901
1217
  const sceneFilter = normalizeText(options.scene_id || options.sceneId || options.scene);
1218
+ const overrideFile = normalizeText(backfill.override_file) || DEFAULT_STUDIO_SCENE_OVERRIDE_PATH;
902
1219
 
903
1220
  const records = await scanSpecPortfolio(projectPath, {
904
- stale_days: governance.stale_days
1221
+ stale_days: governance.stale_days,
1222
+ override_file: overrideFile
905
1223
  }, {
906
1224
  fileSystem
907
1225
  });
@@ -920,7 +1238,10 @@ async function runStudioSpecGovernance(options = {}, dependencies = {}) {
920
1238
  policy_path: loaded.policy_path,
921
1239
  policy_loaded_from: loaded.loaded_from,
922
1240
  policy: {
923
- governance
1241
+ governance,
1242
+ backfill: {
1243
+ override_file: overrideFile
1244
+ }
924
1245
  },
925
1246
  summary: {
926
1247
  scene_count: summary.scene_count,
@@ -974,6 +1295,7 @@ async function runStudioSpecGovernance(options = {}, dependencies = {}) {
974
1295
  module.exports = {
975
1296
  DEFAULT_STUDIO_INTAKE_POLICY_PATH,
976
1297
  DEFAULT_STUDIO_INTAKE_POLICY,
1298
+ DEFAULT_STUDIO_SCENE_OVERRIDE_PATH,
977
1299
  DEFAULT_STUDIO_PORTFOLIO_REPORT,
978
1300
  DEFAULT_STUDIO_SCENE_INDEX,
979
1301
  normalizeStudioIntakePolicy,
@@ -986,6 +1308,7 @@ module.exports = {
986
1308
  parseTasksProgress,
987
1309
  scanSpecPortfolio,
988
1310
  buildSceneGovernanceReport,
1311
+ runStudioSceneBackfill,
989
1312
  runStudioSpecGovernance,
990
1313
  tokenizeText,
991
1314
  computeJaccard
@@ -92,6 +92,7 @@ const STUDIO_INTAKE_POLICY_DEFAULTS = Object.freeze({
92
92
  enabled: true,
93
93
  auto_create_spec: true,
94
94
  force_spec_for_studio_plan: true,
95
+ allow_manual_spec_override: false,
95
96
  prefer_existing_scene_spec: true,
96
97
  related_spec_min_score: 45,
97
98
  allow_new_spec_when_goal_diverges: true,
@@ -112,9 +113,25 @@ const STUDIO_INTAKE_POLICY_DEFAULTS = Object.freeze({
112
113
  },
113
114
  governance: {
114
115
  auto_run_on_plan: true,
116
+ require_auto_on_plan: true,
115
117
  max_active_specs_per_scene: 3,
116
118
  stale_days: 14,
117
119
  duplicate_similarity_threshold: 0.66
120
+ },
121
+ backfill: {
122
+ enabled: true,
123
+ active_only_default: true,
124
+ default_scene_id: 'scene.sce-core',
125
+ override_file: '.sce/spec-governance/spec-scene-overrides.json',
126
+ rules: [
127
+ { id: 'moqui-core', scene_id: 'scene.moqui-core', keywords: ['moqui'] },
128
+ { id: 'orchestration', scene_id: 'scene.sce-orchestration', keywords: ['orchestrate', 'runtime', 'controller', 'batch', 'parallel'] },
129
+ { id: 'template-registry', scene_id: 'scene.sce-template-registry', keywords: ['template', 'scene-package', 'registry', 'catalog', 'scene-template'] },
130
+ { id: 'spec-governance', scene_id: 'scene.sce-spec-governance', keywords: ['spec', 'gate', 'ontology', 'governance', 'policy'] },
131
+ { id: 'quality', scene_id: 'scene.sce-quality', keywords: ['test', 'quality', 'stability', 'jest', 'coverage'] },
132
+ { id: 'docs', scene_id: 'scene.sce-docs', keywords: ['document', 'documentation', 'onboarding', 'guide'] },
133
+ { id: 'platform', scene_id: 'scene.sce-platform', keywords: ['adopt', 'upgrade', 'workspace', 'repo', 'environment', 'devops', 'release', 'github', 'npm'] }
134
+ ]
118
135
  }
119
136
  });
120
137
 
@@ -172,6 +189,7 @@ const TAKEOVER_DEFAULTS = Object.freeze({
172
189
  enabled: true,
173
190
  auto_create_spec: true,
174
191
  force_spec_for_studio_plan: true,
192
+ allow_manual_spec_override: false,
175
193
  prefer_existing_scene_spec: true,
176
194
  related_spec_min_score: 45,
177
195
  allow_new_spec_when_goal_diverges: true,
@@ -179,9 +197,16 @@ const TAKEOVER_DEFAULTS = Object.freeze({
179
197
  goal_missing_strategy: 'create_for_tracking',
180
198
  governance: {
181
199
  auto_run_on_plan: true,
200
+ require_auto_on_plan: true,
182
201
  max_active_specs_per_scene: 3,
183
202
  stale_days: 14,
184
203
  duplicate_similarity_threshold: 0.66
204
+ },
205
+ backfill: {
206
+ enabled: true,
207
+ active_only_default: true,
208
+ default_scene_id: 'scene.sce-core',
209
+ override_file: '.sce/spec-governance/spec-scene-overrides.json'
185
210
  }
186
211
  },
187
212
  debug_policy: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
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": {