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 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
+
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.3.24",
3
+ "version": "3.3.25",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {