scene-capability-engine 3.3.24 → 3.3.26
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 +10 -0
- package/bin/scene-capability-engine.js +12 -0
- package/docs/command-reference.md +37 -1
- package/lib/commands/session.js +27 -0
- package/lib/commands/spec-related.js +70 -0
- package/lib/commands/studio.js +66 -12
- package/lib/commands/timeline.js +287 -0
- package/lib/runtime/project-timeline.js +598 -0
- package/lib/spec/related-specs.js +260 -0
- package/package.json +1 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { DOMAIN_CHAIN_RELATIVE_PATH } = require('./domain-modeling');
|
|
4
|
+
|
|
5
|
+
function normalizeText(value) {
|
|
6
|
+
if (typeof value !== 'string') {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function clampPositiveInteger(value, fallback, max = 100) {
|
|
13
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
return Math.min(parsed, max);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tokenizeText(value) {
|
|
21
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return Array.from(new Set(
|
|
26
|
+
normalized
|
|
27
|
+
.split(/[^a-z0-9\u4e00-\u9fff]+/i)
|
|
28
|
+
.map((item) => item.trim())
|
|
29
|
+
.filter((item) => item.length >= 2 || /[\u4e00-\u9fff]/.test(item))
|
|
30
|
+
));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractSceneIdFromSceneSpec(markdown) {
|
|
34
|
+
const content = normalizeText(markdown);
|
|
35
|
+
if (!content) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const match = content.match(/Scene ID:\s*`([^`]+)`/i);
|
|
39
|
+
if (!match || !match[1]) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return match[1].trim() || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function safeReadJson(filePath, fileSystem = fs) {
|
|
46
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return await fileSystem.readJson(filePath);
|
|
51
|
+
} catch (_error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function safeReadFile(filePath, fileSystem = fs) {
|
|
57
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await fileSystem.readFile(filePath, 'utf8');
|
|
62
|
+
} catch (_error) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resolveSpecSearchEntries(projectPath, fileSystem = fs) {
|
|
68
|
+
const specsRoot = path.join(projectPath, '.sce', 'specs');
|
|
69
|
+
if (!await fileSystem.pathExists(specsRoot)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const names = await fileSystem.readdir(specsRoot);
|
|
74
|
+
const entries = [];
|
|
75
|
+
|
|
76
|
+
for (const specId of names) {
|
|
77
|
+
const specRoot = path.join(specsRoot, specId);
|
|
78
|
+
let stat = null;
|
|
79
|
+
try {
|
|
80
|
+
stat = await fileSystem.stat(specRoot);
|
|
81
|
+
} catch (_error) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!stat || !stat.isDirectory()) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const domainChainPath = path.join(specRoot, DOMAIN_CHAIN_RELATIVE_PATH);
|
|
89
|
+
const sceneSpecPath = path.join(specRoot, 'custom', 'scene-spec.md');
|
|
90
|
+
const domainMapPath = path.join(specRoot, 'custom', 'problem-domain-map.md');
|
|
91
|
+
const requirementsPath = path.join(specRoot, 'requirements.md');
|
|
92
|
+
const designPath = path.join(specRoot, 'design.md');
|
|
93
|
+
|
|
94
|
+
const [
|
|
95
|
+
domainChain,
|
|
96
|
+
sceneSpecContent,
|
|
97
|
+
domainMapContent,
|
|
98
|
+
requirementsContent,
|
|
99
|
+
designContent
|
|
100
|
+
] = await Promise.all([
|
|
101
|
+
safeReadJson(domainChainPath, fileSystem),
|
|
102
|
+
safeReadFile(sceneSpecPath, fileSystem),
|
|
103
|
+
safeReadFile(domainMapPath, fileSystem),
|
|
104
|
+
safeReadFile(requirementsPath, fileSystem),
|
|
105
|
+
safeReadFile(designPath, fileSystem)
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const sceneId = normalizeText(
|
|
109
|
+
(domainChain && domainChain.scene_id) || extractSceneIdFromSceneSpec(sceneSpecContent) || ''
|
|
110
|
+
) || null;
|
|
111
|
+
const problemStatement = normalizeText(
|
|
112
|
+
(domainChain && domainChain.problem && domainChain.problem.statement) || ''
|
|
113
|
+
) || null;
|
|
114
|
+
|
|
115
|
+
const ontologyText = domainChain && domainChain.ontology
|
|
116
|
+
? [
|
|
117
|
+
...(Array.isArray(domainChain.ontology.entity) ? domainChain.ontology.entity : []),
|
|
118
|
+
...(Array.isArray(domainChain.ontology.relation) ? domainChain.ontology.relation : []),
|
|
119
|
+
...(Array.isArray(domainChain.ontology.business_rule) ? domainChain.ontology.business_rule : []),
|
|
120
|
+
...(Array.isArray(domainChain.ontology.decision_policy) ? domainChain.ontology.decision_policy : []),
|
|
121
|
+
...(Array.isArray(domainChain.ontology.execution_flow) ? domainChain.ontology.execution_flow : [])
|
|
122
|
+
].join(' ')
|
|
123
|
+
: '';
|
|
124
|
+
|
|
125
|
+
const searchableText = [
|
|
126
|
+
specId,
|
|
127
|
+
sceneId || '',
|
|
128
|
+
problemStatement || '',
|
|
129
|
+
ontologyText,
|
|
130
|
+
sceneSpecContent.slice(0, 4000),
|
|
131
|
+
domainMapContent.slice(0, 3000),
|
|
132
|
+
requirementsContent.slice(0, 3000),
|
|
133
|
+
designContent.slice(0, 3000)
|
|
134
|
+
].join('\n');
|
|
135
|
+
|
|
136
|
+
entries.push({
|
|
137
|
+
spec_id: specId,
|
|
138
|
+
scene_id: sceneId,
|
|
139
|
+
problem_statement: problemStatement,
|
|
140
|
+
updated_at: stat.mtime ? stat.mtime.toISOString() : null,
|
|
141
|
+
searchable_text: searchableText.toLowerCase()
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return entries;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function calculateSpecRelevance(entry, queryTokens = [], sceneId = '') {
|
|
149
|
+
let score = 0;
|
|
150
|
+
const reasons = [];
|
|
151
|
+
const matchedTokens = [];
|
|
152
|
+
const normalizedSceneId = normalizeText(sceneId).toLowerCase();
|
|
153
|
+
const entrySceneId = normalizeText(entry.scene_id).toLowerCase();
|
|
154
|
+
const haystack = entry.searchable_text || '';
|
|
155
|
+
|
|
156
|
+
if (normalizedSceneId && entrySceneId) {
|
|
157
|
+
if (entrySceneId === normalizedSceneId) {
|
|
158
|
+
score += 90;
|
|
159
|
+
reasons.push('scene_exact');
|
|
160
|
+
} else if (entrySceneId.includes(normalizedSceneId) || normalizedSceneId.includes(entrySceneId)) {
|
|
161
|
+
score += 35;
|
|
162
|
+
reasons.push('scene_partial');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const token of queryTokens) {
|
|
167
|
+
if (!token || token.length < 2) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (haystack.includes(token)) {
|
|
171
|
+
score += 9;
|
|
172
|
+
matchedTokens.push(token);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (matchedTokens.length > 0) {
|
|
177
|
+
reasons.push('query_overlap');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
score,
|
|
182
|
+
reasons,
|
|
183
|
+
matched_tokens: Array.from(new Set(matchedTokens)).slice(0, 20)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function buildDerivedQueryFromSpec(projectPath, specId, fileSystem = fs) {
|
|
188
|
+
const specs = await resolveSpecSearchEntries(projectPath, fileSystem);
|
|
189
|
+
const selected = specs.find((item) => item.spec_id === specId);
|
|
190
|
+
if (!selected) {
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
return [
|
|
194
|
+
selected.problem_statement || '',
|
|
195
|
+
selected.scene_id || '',
|
|
196
|
+
selected.spec_id || ''
|
|
197
|
+
].join(' ').trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function findRelatedSpecs(options = {}, dependencies = {}) {
|
|
201
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
202
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
203
|
+
const limit = clampPositiveInteger(options.limit, 5, 50);
|
|
204
|
+
const sceneId = normalizeText(options.sceneId || options.scene);
|
|
205
|
+
const excludeSpecId = normalizeText(options.excludeSpecId);
|
|
206
|
+
const sourceSpecId = normalizeText(options.sourceSpecId || options.spec);
|
|
207
|
+
|
|
208
|
+
let query = normalizeText(options.query);
|
|
209
|
+
if (!query && sourceSpecId) {
|
|
210
|
+
query = await buildDerivedQueryFromSpec(projectPath, sourceSpecId, fileSystem);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const queryTokens = tokenizeText(query);
|
|
214
|
+
const entries = await resolveSpecSearchEntries(projectPath, fileSystem);
|
|
215
|
+
const ranked = [];
|
|
216
|
+
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if (excludeSpecId && entry.spec_id === excludeSpecId) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const relevance = calculateSpecRelevance(entry, queryTokens, sceneId);
|
|
222
|
+
if (relevance.score <= 0) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
ranked.push({
|
|
226
|
+
spec_id: entry.spec_id,
|
|
227
|
+
scene_id: entry.scene_id,
|
|
228
|
+
problem_statement: entry.problem_statement,
|
|
229
|
+
updated_at: entry.updated_at,
|
|
230
|
+
score: relevance.score,
|
|
231
|
+
reasons: relevance.reasons,
|
|
232
|
+
matched_tokens: relevance.matched_tokens
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ranked.sort((left, right) => {
|
|
237
|
+
if (right.score !== left.score) {
|
|
238
|
+
return right.score - left.score;
|
|
239
|
+
}
|
|
240
|
+
return String(right.updated_at || '').localeCompare(String(left.updated_at || ''));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
mode: 'spec-related',
|
|
245
|
+
success: true,
|
|
246
|
+
query: query || '',
|
|
247
|
+
scene_id: sceneId || null,
|
|
248
|
+
source_spec_id: sourceSpecId || null,
|
|
249
|
+
total_candidates: ranked.length,
|
|
250
|
+
related_specs: ranked.slice(0, limit)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
tokenizeText,
|
|
256
|
+
resolveSpecSearchEntries,
|
|
257
|
+
calculateSpecRelevance,
|
|
258
|
+
findRelatedSpecs
|
|
259
|
+
};
|
|
260
|
+
|
package/package.json
CHANGED