scene-capability-engine 3.3.23 → 3.3.25
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 +58 -0
- package/bin/scene-capability-engine.js +20 -0
- package/docs/adoption-guide.md +8 -0
- package/docs/autonomous-control-guide.md +8 -8
- package/docs/command-reference.md +41 -2
- package/docs/errorbook-registry.md +12 -0
- package/lib/auto/config-schema.js +7 -7
- package/lib/commands/auto.js +2 -2
- package/lib/commands/errorbook.js +258 -0
- package/lib/commands/spec-bootstrap.js +17 -2
- package/lib/commands/spec-domain.js +217 -0
- package/lib/commands/spec-related.js +70 -0
- package/lib/commands/studio.js +345 -9
- package/lib/spec/domain-modeling.js +439 -0
- package/lib/spec/related-specs.js +260 -0
- package/lib/spec-gate/policy/default-policy.js +1 -0
- package/lib/spec-gate/rules/default-rules.js +8 -0
- package/package.json +3 -2
- package/template/.sce/steering/CORE_PRINCIPLES.md +30 -1
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DOMAIN_MAP_RELATIVE_PATH = path.join('custom', 'problem-domain-map.md');
|
|
5
|
+
const SCENE_SPEC_RELATIVE_PATH = path.join('custom', 'scene-spec.md');
|
|
6
|
+
const DOMAIN_CHAIN_RELATIVE_PATH = path.join('custom', 'problem-domain-chain.json');
|
|
7
|
+
const DOMAIN_CHAIN_API_VERSION = 'sce.problem-domain-chain/v0.1';
|
|
8
|
+
|
|
9
|
+
function normalizeText(value) {
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveSpecPaths(projectPath, specId) {
|
|
17
|
+
const specPath = path.join(projectPath, '.sce', 'specs', specId);
|
|
18
|
+
return {
|
|
19
|
+
specPath,
|
|
20
|
+
domainMapPath: path.join(specPath, DOMAIN_MAP_RELATIVE_PATH),
|
|
21
|
+
sceneSpecPath: path.join(specPath, SCENE_SPEC_RELATIVE_PATH),
|
|
22
|
+
domainChainPath: path.join(specPath, DOMAIN_CHAIN_RELATIVE_PATH)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildProblemDomainMindMap(specId, options = {}) {
|
|
27
|
+
const sceneId = normalizeText(options.sceneId) || `scene.${specId.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()}`;
|
|
28
|
+
const problemStatement = normalizeText(options.problemStatement) || 'TBD: describe the primary business problem';
|
|
29
|
+
const primaryFlow = normalizeText(options.primaryFlow) || 'TBD: define core user/business flow';
|
|
30
|
+
const verificationPlan = normalizeText(options.verificationPlan) || 'TBD: define validation and rollback criteria';
|
|
31
|
+
|
|
32
|
+
return `# Problem Domain Mind Map
|
|
33
|
+
|
|
34
|
+
> Mandatory artifact: use this map to expand the problem domain before implementation.
|
|
35
|
+
> Policy: after two failed fix rounds, diagnostics must be added before the next patch round.
|
|
36
|
+
|
|
37
|
+
## Root Problem
|
|
38
|
+
|
|
39
|
+
- Scene: \`${sceneId}\`
|
|
40
|
+
- Spec: \`${specId}\`
|
|
41
|
+
- Problem Statement: ${problemStatement}
|
|
42
|
+
- Primary Flow: ${primaryFlow}
|
|
43
|
+
|
|
44
|
+
## Domain Mind Map
|
|
45
|
+
|
|
46
|
+
\`\`\`mermaid
|
|
47
|
+
mindmap
|
|
48
|
+
root((${specId}))
|
|
49
|
+
Problem
|
|
50
|
+
Symptom
|
|
51
|
+
Root Cause Hypothesis
|
|
52
|
+
Constraints
|
|
53
|
+
Ontology
|
|
54
|
+
Entity
|
|
55
|
+
Relation
|
|
56
|
+
Business Rule
|
|
57
|
+
Decision Policy
|
|
58
|
+
Execution Flow
|
|
59
|
+
Stakeholders
|
|
60
|
+
User
|
|
61
|
+
Operator
|
|
62
|
+
Maintainer
|
|
63
|
+
Risk
|
|
64
|
+
Wrong Direction
|
|
65
|
+
Data Integrity
|
|
66
|
+
Security
|
|
67
|
+
Rollback
|
|
68
|
+
Validation
|
|
69
|
+
Test Evidence
|
|
70
|
+
Runtime Signal
|
|
71
|
+
Gate Criteria
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
## Layered Exploration Chain
|
|
75
|
+
|
|
76
|
+
1. Clarify symptom scope and affected boundaries.
|
|
77
|
+
2. Enumerate entities, relations, and rule constraints.
|
|
78
|
+
3. Identify decision points and execution paths.
|
|
79
|
+
4. Produce candidate fixes and risk tradeoffs.
|
|
80
|
+
5. Define verification path and measurable acceptance.
|
|
81
|
+
|
|
82
|
+
## Correction Loop
|
|
83
|
+
|
|
84
|
+
- Expected Wrong-Direction Signals:
|
|
85
|
+
- requirement drift
|
|
86
|
+
- ontology mismatch
|
|
87
|
+
- repeated failed remediation
|
|
88
|
+
- Correction Actions:
|
|
89
|
+
- update this map
|
|
90
|
+
- add debug evidence
|
|
91
|
+
- adjust scene-spec contract before coding
|
|
92
|
+
|
|
93
|
+
## Verification Plan
|
|
94
|
+
|
|
95
|
+
- ${verificationPlan}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildSceneSpec(specId, options = {}) {
|
|
100
|
+
const sceneId = normalizeText(options.sceneId) || `scene.${specId.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()}`;
|
|
101
|
+
const problemStatement = normalizeText(options.problemStatement) || 'TBD';
|
|
102
|
+
const primaryFlow = normalizeText(options.primaryFlow) || 'TBD';
|
|
103
|
+
const verificationPlan = normalizeText(options.verificationPlan) || 'TBD';
|
|
104
|
+
|
|
105
|
+
return `# Scene Spec
|
|
106
|
+
|
|
107
|
+
> Mandatory artifact: scene-oriented contract for implementation and gating.
|
|
108
|
+
|
|
109
|
+
## Scene Definition
|
|
110
|
+
|
|
111
|
+
- Scene ID: \`${sceneId}\`
|
|
112
|
+
- Spec ID: \`${specId}\`
|
|
113
|
+
- Objective: ${problemStatement}
|
|
114
|
+
- Primary Flow: ${primaryFlow}
|
|
115
|
+
|
|
116
|
+
## Scope & Boundaries
|
|
117
|
+
|
|
118
|
+
- In Scope:
|
|
119
|
+
- core scene behavior
|
|
120
|
+
- required integrations
|
|
121
|
+
- Out of Scope:
|
|
122
|
+
- unrelated legacy refactors
|
|
123
|
+
- uncontrolled workaround paths
|
|
124
|
+
|
|
125
|
+
## Ontology Coverage
|
|
126
|
+
|
|
127
|
+
| Layer | Required Mapping |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| Entity | list key domain entities |
|
|
130
|
+
| Relation | list key relations |
|
|
131
|
+
| Business Rule | list enforceable rules |
|
|
132
|
+
| Decision Policy | list decision points |
|
|
133
|
+
| Execution Flow | list end-to-end action chain |
|
|
134
|
+
|
|
135
|
+
## Decision & Execution Path
|
|
136
|
+
|
|
137
|
+
1. Trigger condition and entry point.
|
|
138
|
+
2. Decision policy branch(es).
|
|
139
|
+
3. Service/tool execution sequence.
|
|
140
|
+
4. Expected outputs and side effects.
|
|
141
|
+
5. Failure path and rollback criteria.
|
|
142
|
+
|
|
143
|
+
## Acceptance & Gate
|
|
144
|
+
|
|
145
|
+
- Functional acceptance: define testable behaviors.
|
|
146
|
+
- Technical acceptance: define gate/test requirements.
|
|
147
|
+
- Verification Plan: ${verificationPlan}
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildProblemDomainChain(specId, options = {}) {
|
|
152
|
+
const sceneId = normalizeText(options.sceneId) || `scene.${specId.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()}`;
|
|
153
|
+
const problemStatement = normalizeText(options.problemStatement) || 'TBD: describe the primary business problem';
|
|
154
|
+
const primaryFlow = normalizeText(options.primaryFlow) || 'TBD: define core user/business flow';
|
|
155
|
+
const verificationPlan = normalizeText(options.verificationPlan) || 'TBD: define validation and rollback criteria';
|
|
156
|
+
const now = new Date().toISOString();
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
api_version: DOMAIN_CHAIN_API_VERSION,
|
|
160
|
+
generated_at: now,
|
|
161
|
+
scene_id: sceneId,
|
|
162
|
+
spec_id: specId,
|
|
163
|
+
problem: {
|
|
164
|
+
statement: problemStatement,
|
|
165
|
+
scope: 'TBD: define boundary and excluded domains',
|
|
166
|
+
symptom: 'TBD: observable symptom and impact'
|
|
167
|
+
},
|
|
168
|
+
ontology: {
|
|
169
|
+
entity: ['TBD: primary entity'],
|
|
170
|
+
relation: ['TBD: key relation'],
|
|
171
|
+
business_rule: ['TBD: enforceable business rule'],
|
|
172
|
+
decision_policy: ['TBD: decision condition and policy'],
|
|
173
|
+
execution_flow: ['TBD: action chain and side effects']
|
|
174
|
+
},
|
|
175
|
+
hypotheses: [
|
|
176
|
+
{
|
|
177
|
+
id: 'H1',
|
|
178
|
+
statement: 'TBD: root-cause hypothesis',
|
|
179
|
+
evidence: ['TBD: evidence or signal'],
|
|
180
|
+
confidence: 'low'
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
risks: [
|
|
184
|
+
{
|
|
185
|
+
id: 'R1',
|
|
186
|
+
type: 'wrong-direction',
|
|
187
|
+
statement: 'TBD: direction drift risk',
|
|
188
|
+
mitigation: 'TBD: correction checkpoint'
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
decision_execution_path: [
|
|
192
|
+
{
|
|
193
|
+
step: 1,
|
|
194
|
+
action: 'entry',
|
|
195
|
+
decision: 'TBD: trigger condition',
|
|
196
|
+
expected_result: 'TBD: expected output'
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
step: 2,
|
|
200
|
+
action: 'route',
|
|
201
|
+
decision: 'TBD: policy branch',
|
|
202
|
+
expected_result: 'TBD: branch result'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
step: 3,
|
|
206
|
+
action: 'execute',
|
|
207
|
+
decision: 'TBD: execution rule',
|
|
208
|
+
expected_result: 'TBD: side effect and data change'
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
correction_loop: {
|
|
212
|
+
triggers: [
|
|
213
|
+
'gate failure',
|
|
214
|
+
'ontology mismatch',
|
|
215
|
+
'two failed fix rounds'
|
|
216
|
+
],
|
|
217
|
+
actions: [
|
|
218
|
+
'refresh domain map',
|
|
219
|
+
'attach debug evidence',
|
|
220
|
+
'rebuild scene spec contract'
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
verification: {
|
|
224
|
+
plan: verificationPlan,
|
|
225
|
+
gates: [
|
|
226
|
+
'spec-gate',
|
|
227
|
+
'tests',
|
|
228
|
+
'release preflight'
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function validateProblemDomainMapContent(content = '') {
|
|
235
|
+
const checks = {
|
|
236
|
+
hasRootProblem: /##\s+Root Problem/i.test(content),
|
|
237
|
+
hasMindMapBlock: /```mermaid[\s\S]*mindmap/i.test(content),
|
|
238
|
+
hasLayeredExplorationChain: /##\s+Layered Exploration Chain/i.test(content),
|
|
239
|
+
hasCorrectionLoop: /##\s+Correction Loop/i.test(content)
|
|
240
|
+
};
|
|
241
|
+
const passed = Object.values(checks).every(Boolean);
|
|
242
|
+
return { passed, checks };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateSceneSpecContent(content = '') {
|
|
246
|
+
const checks = {
|
|
247
|
+
hasSceneDefinition: /##\s+Scene Definition/i.test(content),
|
|
248
|
+
hasOntologyCoverage: /##\s+Ontology Coverage/i.test(content),
|
|
249
|
+
hasDecisionExecutionPath: /##\s+Decision & Execution Path/i.test(content),
|
|
250
|
+
hasAcceptanceGate: /##\s+Acceptance & Gate/i.test(content)
|
|
251
|
+
};
|
|
252
|
+
const passed = Object.values(checks).every(Boolean);
|
|
253
|
+
return { passed, checks };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isNonEmptyString(value) {
|
|
257
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasNonEmptyStringArray(value) {
|
|
261
|
+
return Array.isArray(value) && value.some((item) => isNonEmptyString(item));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validateProblemDomainChainPayload(payload = {}, specId = '') {
|
|
265
|
+
const checks = {
|
|
266
|
+
apiVersion: isNonEmptyString(payload.api_version),
|
|
267
|
+
sceneId: isNonEmptyString(payload.scene_id),
|
|
268
|
+
specId: isNonEmptyString(payload.spec_id) && (!specId || payload.spec_id === specId),
|
|
269
|
+
problemStatement: isNonEmptyString(payload?.problem?.statement),
|
|
270
|
+
ontologyEntity: hasNonEmptyStringArray(payload?.ontology?.entity),
|
|
271
|
+
ontologyRelation: hasNonEmptyStringArray(payload?.ontology?.relation),
|
|
272
|
+
ontologyBusinessRule: hasNonEmptyStringArray(payload?.ontology?.business_rule),
|
|
273
|
+
ontologyDecisionPolicy: hasNonEmptyStringArray(payload?.ontology?.decision_policy),
|
|
274
|
+
ontologyExecutionFlow: hasNonEmptyStringArray(payload?.ontology?.execution_flow),
|
|
275
|
+
hasHypotheses: Array.isArray(payload.hypotheses) && payload.hypotheses.length > 0,
|
|
276
|
+
hasRisks: Array.isArray(payload.risks) && payload.risks.length > 0,
|
|
277
|
+
hasDecisionPath: Array.isArray(payload.decision_execution_path) && payload.decision_execution_path.length >= 3,
|
|
278
|
+
hasCorrectionTriggers: hasNonEmptyStringArray(payload?.correction_loop?.triggers),
|
|
279
|
+
hasCorrectionActions: hasNonEmptyStringArray(payload?.correction_loop?.actions),
|
|
280
|
+
hasVerificationGates: hasNonEmptyStringArray(payload?.verification?.gates)
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
passed: Object.values(checks).every(Boolean),
|
|
284
|
+
checks
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function ensureSpecDomainArtifacts(projectPath, specId, options = {}) {
|
|
289
|
+
const fileSystem = options.fileSystem || fs;
|
|
290
|
+
const dryRun = options.dryRun === true;
|
|
291
|
+
const force = options.force === true;
|
|
292
|
+
|
|
293
|
+
const paths = resolveSpecPaths(projectPath, specId);
|
|
294
|
+
const domainMapContent = buildProblemDomainMindMap(specId, options);
|
|
295
|
+
const sceneSpecContent = buildSceneSpec(specId, options);
|
|
296
|
+
const domainChainPayload = buildProblemDomainChain(specId, options);
|
|
297
|
+
|
|
298
|
+
const created = {
|
|
299
|
+
domain_map: false,
|
|
300
|
+
scene_spec: false,
|
|
301
|
+
domain_chain: false
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (!dryRun) {
|
|
305
|
+
await fileSystem.ensureDir(path.dirname(paths.domainMapPath));
|
|
306
|
+
|
|
307
|
+
const hasDomainMap = await fileSystem.pathExists(paths.domainMapPath);
|
|
308
|
+
if (force || !hasDomainMap) {
|
|
309
|
+
await fileSystem.writeFile(paths.domainMapPath, domainMapContent, 'utf8');
|
|
310
|
+
created.domain_map = true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const hasSceneSpec = await fileSystem.pathExists(paths.sceneSpecPath);
|
|
314
|
+
if (force || !hasSceneSpec) {
|
|
315
|
+
await fileSystem.writeFile(paths.sceneSpecPath, sceneSpecContent, 'utf8');
|
|
316
|
+
created.scene_spec = true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const hasDomainChain = await fileSystem.pathExists(paths.domainChainPath);
|
|
320
|
+
if (force || !hasDomainChain) {
|
|
321
|
+
await fileSystem.writeJson(paths.domainChainPath, domainChainPayload, { spaces: 2 });
|
|
322
|
+
created.domain_chain = true;
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
created.domain_map = true;
|
|
326
|
+
created.scene_spec = true;
|
|
327
|
+
created.domain_chain = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
paths: {
|
|
332
|
+
domain_map: paths.domainMapPath,
|
|
333
|
+
scene_spec: paths.sceneSpecPath,
|
|
334
|
+
domain_chain: paths.domainChainPath
|
|
335
|
+
},
|
|
336
|
+
created,
|
|
337
|
+
preview: {
|
|
338
|
+
domain_map: domainMapContent,
|
|
339
|
+
scene_spec: sceneSpecContent,
|
|
340
|
+
domain_chain: domainChainPayload
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function validateSpecDomainArtifacts(projectPath, specId, fileSystem = fs) {
|
|
346
|
+
const paths = resolveSpecPaths(projectPath, specId);
|
|
347
|
+
const warnings = [];
|
|
348
|
+
const details = {
|
|
349
|
+
domain_map: {
|
|
350
|
+
path: paths.domainMapPath,
|
|
351
|
+
exists: false,
|
|
352
|
+
checks: {}
|
|
353
|
+
},
|
|
354
|
+
scene_spec: {
|
|
355
|
+
path: paths.sceneSpecPath,
|
|
356
|
+
exists: false,
|
|
357
|
+
checks: {}
|
|
358
|
+
},
|
|
359
|
+
domain_chain: {
|
|
360
|
+
path: paths.domainChainPath,
|
|
361
|
+
exists: false,
|
|
362
|
+
checks: {}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
let passedChecks = 0;
|
|
367
|
+
const totalChecks = 3;
|
|
368
|
+
|
|
369
|
+
const hasDomainMap = await fileSystem.pathExists(paths.domainMapPath);
|
|
370
|
+
details.domain_map.exists = hasDomainMap;
|
|
371
|
+
if (!hasDomainMap) {
|
|
372
|
+
warnings.push(`Missing required artifact: ${DOMAIN_MAP_RELATIVE_PATH}`);
|
|
373
|
+
} else {
|
|
374
|
+
const content = await fileSystem.readFile(paths.domainMapPath, 'utf8');
|
|
375
|
+
const evaluation = validateProblemDomainMapContent(content);
|
|
376
|
+
details.domain_map.checks = evaluation.checks;
|
|
377
|
+
if (evaluation.passed) {
|
|
378
|
+
passedChecks += 1;
|
|
379
|
+
} else {
|
|
380
|
+
warnings.push(`Invalid ${DOMAIN_MAP_RELATIVE_PATH}: missing mandatory sections`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const hasSceneSpec = await fileSystem.pathExists(paths.sceneSpecPath);
|
|
385
|
+
details.scene_spec.exists = hasSceneSpec;
|
|
386
|
+
if (!hasSceneSpec) {
|
|
387
|
+
warnings.push(`Missing required artifact: ${SCENE_SPEC_RELATIVE_PATH}`);
|
|
388
|
+
} else {
|
|
389
|
+
const content = await fileSystem.readFile(paths.sceneSpecPath, 'utf8');
|
|
390
|
+
const evaluation = validateSceneSpecContent(content);
|
|
391
|
+
details.scene_spec.checks = evaluation.checks;
|
|
392
|
+
if (evaluation.passed) {
|
|
393
|
+
passedChecks += 1;
|
|
394
|
+
} else {
|
|
395
|
+
warnings.push(`Invalid ${SCENE_SPEC_RELATIVE_PATH}: missing mandatory sections`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const hasDomainChain = await fileSystem.pathExists(paths.domainChainPath);
|
|
400
|
+
details.domain_chain.exists = hasDomainChain;
|
|
401
|
+
if (!hasDomainChain) {
|
|
402
|
+
warnings.push(`Missing required artifact: ${DOMAIN_CHAIN_RELATIVE_PATH}`);
|
|
403
|
+
} else {
|
|
404
|
+
let payload = null;
|
|
405
|
+
try {
|
|
406
|
+
payload = await fileSystem.readJson(paths.domainChainPath);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
warnings.push(`Invalid ${DOMAIN_CHAIN_RELATIVE_PATH}: malformed JSON (${error.message})`);
|
|
409
|
+
}
|
|
410
|
+
if (payload) {
|
|
411
|
+
const evaluation = validateProblemDomainChainPayload(payload, specId);
|
|
412
|
+
details.domain_chain.checks = evaluation.checks;
|
|
413
|
+
if (evaluation.passed) {
|
|
414
|
+
passedChecks += 1;
|
|
415
|
+
} else {
|
|
416
|
+
warnings.push(`Invalid ${DOMAIN_CHAIN_RELATIVE_PATH}: missing mandatory chain fields`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
passed: passedChecks === totalChecks,
|
|
423
|
+
ratio: passedChecks / totalChecks,
|
|
424
|
+
details,
|
|
425
|
+
warnings
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
DOMAIN_MAP_RELATIVE_PATH,
|
|
431
|
+
SCENE_SPEC_RELATIVE_PATH,
|
|
432
|
+
DOMAIN_CHAIN_RELATIVE_PATH,
|
|
433
|
+
DOMAIN_CHAIN_API_VERSION,
|
|
434
|
+
buildProblemDomainMindMap,
|
|
435
|
+
buildSceneSpec,
|
|
436
|
+
buildProblemDomainChain,
|
|
437
|
+
ensureSpecDomainArtifacts,
|
|
438
|
+
validateSpecDomainArtifacts
|
|
439
|
+
};
|
|
@@ -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
|
+
|
|
@@ -11,6 +11,7 @@ const DEFAULT_GATE_POLICY = {
|
|
|
11
11
|
mandatory: { enabled: true, weight: 30, hard_fail: true },
|
|
12
12
|
tests: { enabled: true, weight: 25, hard_fail: true },
|
|
13
13
|
docs: { enabled: true, weight: 15, hard_fail: false },
|
|
14
|
+
domain_scene_modeling: { enabled: true, weight: 25, hard_fail: true },
|
|
14
15
|
config_consistency: { enabled: true, weight: 15, hard_fail: false },
|
|
15
16
|
traceability: { enabled: true, weight: 15, hard_fail: false }
|
|
16
17
|
}
|