scene-capability-engine 3.3.24 → 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 +4 -0
- package/bin/scene-capability-engine.js +10 -0
- package/docs/command-reference.md +5 -0
- package/lib/commands/spec-related.js +70 -0
- package/lib/commands/studio.js +34 -3
- package/lib/spec/related-specs.js +260 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -42,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
42
42
|
- when `--spec` is omitted, `plan` auto-selects the latest scene-matching chain (`scene_id`)
|
|
43
43
|
- `sce studio generate` writes chain-aware metadata and `generate` stage report at `.sce/reports/studio/generate-<job-id>.json`
|
|
44
44
|
- `sce studio verify` / `sce studio release` now include domain-chain metadata in reports and pass `spec_id` into auto errorbook failure capture
|
|
45
|
+
- Added historical related-spec retrieval for faster new-problem analysis:
|
|
46
|
+
- new command: `sce spec-related` (alias route: `sce spec related`)
|
|
47
|
+
- supports query/scene/spec-seeded lookup and relevance ranking
|
|
48
|
+
- `sce studio plan` now auto-loads related historical specs into job metadata (`source.related_specs`)
|
|
45
49
|
|
|
46
50
|
## [3.3.23] - 2026-02-27
|
|
47
51
|
|
|
@@ -20,6 +20,7 @@ const { registerSpecBootstrapCommand } = require('../lib/commands/spec-bootstrap
|
|
|
20
20
|
const { registerSpecPipelineCommand } = require('../lib/commands/spec-pipeline');
|
|
21
21
|
const { registerSpecGateCommand } = require('../lib/commands/spec-gate');
|
|
22
22
|
const { registerSpecDomainCommand } = require('../lib/commands/spec-domain');
|
|
23
|
+
const { registerSpecRelatedCommand } = require('../lib/commands/spec-related');
|
|
23
24
|
const { registerValueCommands } = require('../lib/commands/value');
|
|
24
25
|
const VersionChecker = require('../lib/version/version-checker');
|
|
25
26
|
const {
|
|
@@ -57,6 +58,7 @@ const program = new Command();
|
|
|
57
58
|
* - `sce spec pipeline ...` -> `sce spec-pipeline ...`
|
|
58
59
|
* - `sce spec gate ...` -> `sce spec-gate ...`
|
|
59
60
|
* - `sce spec domain ...` -> `sce spec-domain ...`
|
|
61
|
+
* - `sce spec related ...` -> `sce spec-related ...`
|
|
60
62
|
* - `sce spec create <name> ...` -> `sce create-spec <name> ...`
|
|
61
63
|
* - `sce spec <name> ...` -> `sce create-spec <name> ...` (legacy)
|
|
62
64
|
*
|
|
@@ -96,6 +98,11 @@ function normalizeSpecCommandArgs(argv) {
|
|
|
96
98
|
return normalized;
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
if (commandToken === 'related') {
|
|
102
|
+
normalized.splice(commandIndex, 2, 'spec-related');
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
if (commandToken === 'create') {
|
|
100
107
|
normalized.splice(commandIndex, 2, 'create-spec');
|
|
101
108
|
return normalized;
|
|
@@ -342,6 +349,9 @@ registerSpecGateCommand(program);
|
|
|
342
349
|
// Spec domain modeling command
|
|
343
350
|
registerSpecDomainCommand(program);
|
|
344
351
|
|
|
352
|
+
// Spec related lookup command
|
|
353
|
+
registerSpecRelatedCommand(program);
|
|
354
|
+
|
|
345
355
|
// 系统诊断命令
|
|
346
356
|
program
|
|
347
357
|
.command('doctor')
|
|
@@ -73,6 +73,10 @@ sce spec domain init --spec 01-00-feature-name --scene scene.customer-order-inve
|
|
|
73
73
|
sce spec domain validate --spec 01-00-feature-name --fail-on-error --json
|
|
74
74
|
sce spec domain refresh --spec 01-00-feature-name --scene scene.customer-order-inventory --json
|
|
75
75
|
|
|
76
|
+
# Find related historical specs before starting a new analysis
|
|
77
|
+
sce spec related --query "customer order inventory reconciliation drift" --scene scene.customer-order-inventory --json
|
|
78
|
+
sce spec related --spec 01-00-feature-name --limit 8 --json
|
|
79
|
+
|
|
76
80
|
# Multi-Spec mode defaults to orchestrate routing
|
|
77
81
|
sce spec bootstrap --specs "spec-a,spec-b" --max-parallel 3
|
|
78
82
|
sce spec pipeline run --specs "spec-a,spec-b" --max-parallel 3
|
|
@@ -491,6 +495,7 @@ Stage guardrails are enforced by default:
|
|
|
491
495
|
- `plan` requires `--scene`; SCE binds one active primary session per scene
|
|
492
496
|
- `plan --spec <id>` (recommended) ingests `.sce/specs/<spec>/custom/problem-domain-chain.json` into studio job context
|
|
493
497
|
- when `--spec` is omitted, `plan` auto-resolves the latest matching spec chain by `scene_id` when available
|
|
498
|
+
- `plan` auto-searches related historical specs by `scene + goal` and writes top candidates into job metadata (`source.related_specs`)
|
|
494
499
|
- successful `release` auto-archives current scene session and auto-opens the next scene cycle session
|
|
495
500
|
- `generate` requires `plan`
|
|
496
501
|
- `generate` consumes the plan-stage domain-chain context and writes chain-aware metadata/report (`.sce/reports/studio/generate-<job-id>.json`)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { findRelatedSpecs } = require('../spec/related-specs');
|
|
3
|
+
|
|
4
|
+
function normalizeText(value) {
|
|
5
|
+
if (typeof value !== 'string') {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function runSpecRelatedCommand(options = {}, dependencies = {}) {
|
|
12
|
+
const query = normalizeText(options.query);
|
|
13
|
+
const sceneId = normalizeText(options.scene);
|
|
14
|
+
const specId = normalizeText(options.spec);
|
|
15
|
+
|
|
16
|
+
if (!query && !sceneId && !specId) {
|
|
17
|
+
throw new Error('At least one selector is required: --query or --scene or --spec');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const payload = await findRelatedSpecs({
|
|
21
|
+
query,
|
|
22
|
+
sceneId,
|
|
23
|
+
sourceSpecId: specId,
|
|
24
|
+
limit: options.limit
|
|
25
|
+
}, dependencies);
|
|
26
|
+
|
|
27
|
+
if (options.json) {
|
|
28
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.blue('Related Specs'));
|
|
31
|
+
console.log(` Query: ${payload.query || '(none)'}`);
|
|
32
|
+
console.log(` Scene: ${payload.scene_id || '(none)'}`);
|
|
33
|
+
console.log(` Source Spec: ${payload.source_spec_id || '(none)'}`);
|
|
34
|
+
console.log(` Candidates: ${payload.total_candidates}`);
|
|
35
|
+
for (const item of payload.related_specs) {
|
|
36
|
+
console.log(` - ${item.spec_id} | score=${item.score} | scene=${item.scene_id || 'n/a'}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function registerSpecRelatedCommand(program) {
|
|
44
|
+
program
|
|
45
|
+
.command('spec-related')
|
|
46
|
+
.description('Find previously related Specs by query/scene context')
|
|
47
|
+
.option('--query <text>', 'Problem statement or search query')
|
|
48
|
+
.option('--scene <scene-id>', 'Scene id for scene-aligned lookup')
|
|
49
|
+
.option('--spec <spec-id>', 'Use existing spec as query seed')
|
|
50
|
+
.option('--limit <n>', 'Maximum related specs to return', '5')
|
|
51
|
+
.option('--json', 'Output machine-readable JSON')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
try {
|
|
54
|
+
await runSpecRelatedCommand(options);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
58
|
+
} else {
|
|
59
|
+
console.error(chalk.red('❌ spec-related failed:'), error.message);
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
runSpecRelatedCommand,
|
|
68
|
+
registerSpecRelatedCommand
|
|
69
|
+
};
|
|
70
|
+
|
package/lib/commands/studio.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
DOMAIN_CHAIN_RELATIVE_PATH,
|
|
9
9
|
ensureSpecDomainArtifacts
|
|
10
10
|
} = require('../spec/domain-modeling');
|
|
11
|
+
const { findRelatedSpecs } = require('../spec/related-specs');
|
|
11
12
|
|
|
12
13
|
const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
|
|
13
14
|
const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
|
|
@@ -986,6 +987,25 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
986
987
|
projectPath,
|
|
987
988
|
fileSystem
|
|
988
989
|
});
|
|
990
|
+
const relatedSpecLookup = await findRelatedSpecs({
|
|
991
|
+
query: normalizeString(options.goal),
|
|
992
|
+
sceneId,
|
|
993
|
+
limit: 8,
|
|
994
|
+
excludeSpecId: domainChainBinding.spec_id || specId || null
|
|
995
|
+
}, {
|
|
996
|
+
projectPath,
|
|
997
|
+
fileSystem
|
|
998
|
+
});
|
|
999
|
+
const relatedSpecItems = Array.isArray(relatedSpecLookup.related_specs)
|
|
1000
|
+
? relatedSpecLookup.related_specs.map((item) => ({
|
|
1001
|
+
spec_id: item.spec_id,
|
|
1002
|
+
scene_id: item.scene_id || null,
|
|
1003
|
+
score: Number(item.score || 0),
|
|
1004
|
+
reasons: Array.isArray(item.reasons) ? item.reasons : [],
|
|
1005
|
+
matched_tokens: Array.isArray(item.matched_tokens) ? item.matched_tokens : [],
|
|
1006
|
+
updated_at: item.updated_at || null
|
|
1007
|
+
}))
|
|
1008
|
+
: [];
|
|
989
1009
|
|
|
990
1010
|
const paths = resolveStudioPaths(projectPath);
|
|
991
1011
|
await ensureStudioDirectories(paths, fileSystem);
|
|
@@ -1013,7 +1033,9 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1013
1033
|
domain_chain_spec_id: domainChainBinding.spec_id || null,
|
|
1014
1034
|
domain_chain_path: domainChainBinding.chain_path || null,
|
|
1015
1035
|
domain_chain_summary: domainChainBinding.summary || null,
|
|
1016
|
-
domain_chain_reason: domainChainBinding.reason || null
|
|
1036
|
+
domain_chain_reason: domainChainBinding.reason || null,
|
|
1037
|
+
related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
|
|
1038
|
+
related_specs_top: relatedSpecItems
|
|
1017
1039
|
}
|
|
1018
1040
|
};
|
|
1019
1041
|
|
|
@@ -1040,11 +1062,18 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1040
1062
|
summary: domainChainBinding.summary || null,
|
|
1041
1063
|
context: domainChainBinding.context || null,
|
|
1042
1064
|
updated_at: domainChainBinding.updated_at || null
|
|
1065
|
+
},
|
|
1066
|
+
related_specs: {
|
|
1067
|
+
query: relatedSpecLookup.query || '',
|
|
1068
|
+
scene_id: relatedSpecLookup.scene_id || null,
|
|
1069
|
+
total_candidates: Number(relatedSpecLookup.total_candidates || 0),
|
|
1070
|
+
items: relatedSpecItems
|
|
1043
1071
|
}
|
|
1044
1072
|
},
|
|
1045
1073
|
scene: {
|
|
1046
1074
|
id: sceneId,
|
|
1047
|
-
spec_id: domainChainBinding.spec_id || specId || null
|
|
1075
|
+
spec_id: domainChainBinding.spec_id || specId || null,
|
|
1076
|
+
related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
|
|
1048
1077
|
},
|
|
1049
1078
|
session: {
|
|
1050
1079
|
policy: 'mandatory.scene-primary',
|
|
@@ -1073,7 +1102,9 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1073
1102
|
domain_chain_resolved: domainChainBinding.resolved === true,
|
|
1074
1103
|
domain_chain_source: domainChainBinding.source || 'none',
|
|
1075
1104
|
domain_chain_spec_id: domainChainBinding.spec_id || null,
|
|
1076
|
-
domain_chain_path: domainChainBinding.chain_path || null
|
|
1105
|
+
domain_chain_path: domainChainBinding.chain_path || null,
|
|
1106
|
+
related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
|
|
1107
|
+
related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
|
|
1077
1108
|
}, fileSystem);
|
|
1078
1109
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1079
1110
|
|
|
@@ -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