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.
- package/CHANGELOG.md +30 -0
- package/README.md +16 -2
- package/README.zh.md +16 -2
- package/docs/command-reference.md +32 -4
- package/lib/commands/studio.js +862 -28
- package/lib/spec/related-specs.js +10 -2
- package/lib/spec/scene-binding-overrides.js +115 -0
- package/lib/studio/spec-intake-governor.js +328 -5
- package/lib/workspace/takeover-baseline.js +25 -0
- package/package.json +1 -1
- package/template/.sce/config/studio-intake-policy.json +86 -0
|
@@ -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)
|
|
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
|
|
737
|
-
|
|
738
|
-
|
|
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