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.
@@ -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
  }