spec-gen-cli 1.2.4 → 1.2.6
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/README.md +51 -18
- package/dist/api/generate.d.ts.map +1 -1
- package/dist/api/generate.js +46 -32
- package/dist/api/generate.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +26 -12
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +225 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +116 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/refresh-stories.d.ts +10 -0
- package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
- package/dist/cli/commands/refresh-stories.js +314 -0
- package/dist/cli/commands/refresh-stories.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/analyzer/unified-search.d.ts +107 -0
- package/dist/core/analyzer/unified-search.d.ts.map +1 -0
- package/dist/core/analyzer/unified-search.js +237 -0
- package/dist/core/analyzer/unified-search.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts +17 -2
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -1
- package/dist/core/generator/openspec-format-generator.js +111 -10
- package/dist/core/generator/openspec-format-generator.js.map +1 -1
- package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
- package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
- package/dist/core/generator/rag-manifest-generator.js +134 -0
- package/dist/core/generator/rag-manifest-generator.js.map +1 -0
- package/dist/core/services/chat-tools.d.ts.map +1 -1
- package/dist/core/services/chat-tools.js +54 -6
- package/dist/core/services/chat-tools.js.map +1 -1
- package/dist/core/services/mcp-handlers/change.d.ts +14 -0
- package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/change.js +416 -0
- package/dist/core/services/mcp-handlers/change.js.map +1 -0
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/orient.js +108 -1
- package/dist/core/services/mcp-handlers/orient.js.map +1 -1
- package/dist/core/services/mcp-handlers/semantic.d.ts +4 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/semantic.js +101 -32
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
- package/package.json +4 -1
- package/src/viewer/InteractiveGraphViewer.jsx +46 -4
- package/src/viewer/components/ChatPanel.jsx +2 -3
- package/src/viewer/components/ClusterGraph.jsx +22 -6
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Search
|
|
3
|
+
*
|
|
4
|
+
* Combines code and spec indexes with cross-scoring to boost results
|
|
5
|
+
* that are linked through bidirectional mappings.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const results = await UnifiedSearch.unifiedSearch(
|
|
9
|
+
* outputDir,
|
|
10
|
+
* "validate user authentication",
|
|
11
|
+
* embedSvc,
|
|
12
|
+
* { limit: 10 }
|
|
13
|
+
* );
|
|
14
|
+
*/
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CONSTANTS
|
|
18
|
+
// ============================================================================
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
directMappingBoost: 0.3,
|
|
21
|
+
reverseMappingBoost: 0.3,
|
|
22
|
+
mutualMappingBoost: 0.5,
|
|
23
|
+
additionalLinkBoost: 0.1,
|
|
24
|
+
maxAdditionalBoost: 0.3,
|
|
25
|
+
};
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// MAPPING INDEX
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Build a bidirectional mapping index from mapping.json
|
|
31
|
+
*/
|
|
32
|
+
export function buildBidirectionalMapping(mappings) {
|
|
33
|
+
const functionToRequirements = new Map();
|
|
34
|
+
const requirementToFunctions = new Map();
|
|
35
|
+
for (const m of mappings) {
|
|
36
|
+
if (!m.requirement || !m.domain || !m.functions)
|
|
37
|
+
continue;
|
|
38
|
+
// Use dot+camelCase format to match spec vector index IDs (e.g. 'auth.validateToken')
|
|
39
|
+
const reqCamel = m.requirement.charAt(0).toLowerCase() + m.requirement.slice(1);
|
|
40
|
+
const requirementKey = `${m.domain}.${reqCamel}`;
|
|
41
|
+
// Build function → requirements mapping
|
|
42
|
+
for (const fn of m.functions) {
|
|
43
|
+
if (!fn.file || !fn.name)
|
|
44
|
+
continue;
|
|
45
|
+
const functionKey = `${fn.file}::${fn.name}`;
|
|
46
|
+
const existing = functionToRequirements.get(functionKey) ?? [];
|
|
47
|
+
existing.push({ domain: m.domain, requirement: m.requirement });
|
|
48
|
+
functionToRequirements.set(functionKey, existing);
|
|
49
|
+
}
|
|
50
|
+
// Build requirement → functions mapping
|
|
51
|
+
const functions = m.functions
|
|
52
|
+
.filter((f) => f.file && f.name)
|
|
53
|
+
.map((f) => ({ file: f.file, name: f.name }));
|
|
54
|
+
if (functions.length > 0) {
|
|
55
|
+
requirementToFunctions.set(requirementKey, functions);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { functionToRequirements, requirementToFunctions };
|
|
59
|
+
}
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// CROSS-SCORING ALGORITHM
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Calculate cross-scoring boost based on bidirectional mappings
|
|
65
|
+
*/
|
|
66
|
+
export function calculateCrossScore(result, mappingIndex, config) {
|
|
67
|
+
const linkedArtifacts = [];
|
|
68
|
+
let mappingBoost = 0;
|
|
69
|
+
if ('filePath' in result.record) {
|
|
70
|
+
// This is a code result - look for linked requirements
|
|
71
|
+
const rec = result.record;
|
|
72
|
+
const functionKey = `${rec.filePath}::${rec.name}`;
|
|
73
|
+
const requirements = mappingIndex.functionToRequirements.get(functionKey);
|
|
74
|
+
if (requirements && requirements.length > 0) {
|
|
75
|
+
mappingBoost += config.directMappingBoost;
|
|
76
|
+
// Add linked requirements as artifacts
|
|
77
|
+
for (const req of requirements) {
|
|
78
|
+
const reqCamel = req.requirement.charAt(0).toLowerCase() + req.requirement.slice(1);
|
|
79
|
+
const artifactId = `${req.domain}.${reqCamel}`;
|
|
80
|
+
linkedArtifacts.push({
|
|
81
|
+
type: 'spec',
|
|
82
|
+
id: artifactId,
|
|
83
|
+
score: result.score,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Additional boost for multiple links (capped)
|
|
87
|
+
const additionalBoost = Math.min((requirements.length - 1) * config.additionalLinkBoost, config.maxAdditionalBoost);
|
|
88
|
+
mappingBoost += additionalBoost;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// This is a spec result - look for linked functions
|
|
93
|
+
const specRec = result.record;
|
|
94
|
+
const requirementKey = specRec.id;
|
|
95
|
+
const functions = mappingIndex.requirementToFunctions.get(requirementKey);
|
|
96
|
+
if (functions && functions.length > 0) {
|
|
97
|
+
mappingBoost += config.reverseMappingBoost;
|
|
98
|
+
// Add linked functions as artifacts
|
|
99
|
+
for (const fn of functions) {
|
|
100
|
+
const artifactId = `${fn.file}::${fn.name}`;
|
|
101
|
+
linkedArtifacts.push({
|
|
102
|
+
type: 'code',
|
|
103
|
+
id: artifactId,
|
|
104
|
+
score: result.score,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Additional boost for multiple links (capped)
|
|
108
|
+
const additionalBoost = Math.min((functions.length - 1) * config.additionalLinkBoost, config.maxAdditionalBoost);
|
|
109
|
+
mappingBoost += additionalBoost;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { mappingBoost, linkedArtifacts };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Determine result type based on source and linked artifacts
|
|
116
|
+
*/
|
|
117
|
+
export function determineResultType(result, linkedArtifacts) {
|
|
118
|
+
const isCode = 'filePath' in result.record;
|
|
119
|
+
if (isCode && linkedArtifacts.some(art => art.type === 'spec'))
|
|
120
|
+
return 'both';
|
|
121
|
+
if (!isCode && linkedArtifacts.some(art => art.type === 'code'))
|
|
122
|
+
return 'both';
|
|
123
|
+
return isCode ? 'code' : 'spec';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extract source metadata from result
|
|
127
|
+
*/
|
|
128
|
+
export function extractSourceMetadata(result) {
|
|
129
|
+
if ('filePath' in result.record) {
|
|
130
|
+
const rec = result.record;
|
|
131
|
+
return {
|
|
132
|
+
filePath: rec.filePath,
|
|
133
|
+
functionName: rec.name,
|
|
134
|
+
className: rec.className || undefined,
|
|
135
|
+
language: rec.language,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const rec = result.record;
|
|
140
|
+
return {
|
|
141
|
+
domain: rec.domain,
|
|
142
|
+
section: rec.section,
|
|
143
|
+
title: rec.title,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// UNIFIED SEARCH
|
|
149
|
+
// ============================================================================
|
|
150
|
+
export class UnifiedSearch {
|
|
151
|
+
/**
|
|
152
|
+
* Unified search that combines code and spec indexes with cross-scoring
|
|
153
|
+
*/
|
|
154
|
+
static async unifiedSearch(outputDir, query, embedSvc, opts = {}) {
|
|
155
|
+
const { limit = 10, language, domain, section, config = {} } = opts;
|
|
156
|
+
const scoringConfig = { ...DEFAULT_CONFIG, ...config };
|
|
157
|
+
// Import index classes dynamically
|
|
158
|
+
const { VectorIndex } = await import('./vector-index.js');
|
|
159
|
+
const { SpecVectorIndex } = await import('./spec-vector-index.js');
|
|
160
|
+
// Load mapping index
|
|
161
|
+
const mappingJsonPath = join(outputDir, 'mapping.json');
|
|
162
|
+
let mappingIndex = {
|
|
163
|
+
functionToRequirements: new Map(),
|
|
164
|
+
requirementToFunctions: new Map(),
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
const { readFile } = await import('node:fs/promises');
|
|
168
|
+
const raw = JSON.parse(await readFile(mappingJsonPath, 'utf-8'));
|
|
169
|
+
mappingIndex = buildBidirectionalMapping(raw.mappings ?? []);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Non-fatal - proceed without cross-scoring
|
|
173
|
+
}
|
|
174
|
+
// Execute parallel searches
|
|
175
|
+
const svc = embedSvc ?? null;
|
|
176
|
+
const [codeResults, specResults] = await Promise.all([
|
|
177
|
+
VectorIndex.search(outputDir, query, svc, { limit: limit * 3, language }).catch(() => []),
|
|
178
|
+
svc
|
|
179
|
+
? SpecVectorIndex.search(outputDir, query, svc, { limit: limit * 3, domain, section }).catch(() => [])
|
|
180
|
+
: Promise.resolve([]),
|
|
181
|
+
]);
|
|
182
|
+
// Combine and score results
|
|
183
|
+
const allResults = [];
|
|
184
|
+
// Process code results
|
|
185
|
+
for (const result of codeResults) {
|
|
186
|
+
const { mappingBoost, linkedArtifacts } = calculateCrossScore(result, mappingIndex, scoringConfig);
|
|
187
|
+
const finalScore = result.score + mappingBoost;
|
|
188
|
+
const resultType = determineResultType(result, linkedArtifacts);
|
|
189
|
+
allResults.push({
|
|
190
|
+
result,
|
|
191
|
+
unified: {
|
|
192
|
+
id: result.record.id,
|
|
193
|
+
type: resultType,
|
|
194
|
+
score: finalScore,
|
|
195
|
+
baseScore: result.score,
|
|
196
|
+
mappingBoost,
|
|
197
|
+
source: extractSourceMetadata(result),
|
|
198
|
+
linkedArtifacts,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Process spec results
|
|
203
|
+
for (const result of specResults) {
|
|
204
|
+
const { mappingBoost, linkedArtifacts } = calculateCrossScore(result, mappingIndex, scoringConfig);
|
|
205
|
+
const finalScore = result.score + mappingBoost;
|
|
206
|
+
const resultType = determineResultType(result, linkedArtifacts);
|
|
207
|
+
allResults.push({
|
|
208
|
+
result,
|
|
209
|
+
unified: {
|
|
210
|
+
id: result.record.id,
|
|
211
|
+
type: resultType,
|
|
212
|
+
score: finalScore,
|
|
213
|
+
baseScore: result.score,
|
|
214
|
+
mappingBoost,
|
|
215
|
+
source: extractSourceMetadata(result),
|
|
216
|
+
linkedArtifacts,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// Sort by final score
|
|
221
|
+
allResults.sort((a, b) => b.unified.score - a.unified.score);
|
|
222
|
+
// Return top results
|
|
223
|
+
return allResults.slice(0, limit).map(r => r.unified);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// UTILITIES
|
|
228
|
+
// ============================================================================
|
|
229
|
+
/**
|
|
230
|
+
* Check if unified search is available
|
|
231
|
+
*/
|
|
232
|
+
export async function unifiedSearchAvailable(outputDir) {
|
|
233
|
+
const { VectorIndex } = await import('./vector-index.js');
|
|
234
|
+
const { SpecVectorIndex } = await import('./spec-vector-index.js');
|
|
235
|
+
return VectorIndex.exists(outputDir) && SpecVectorIndex.exists(outputDir);
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=unified-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unified-search.js","sourceRoot":"","sources":["../../../src/core/analyzer/unified-search.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* specification files.
|
|
6
6
|
*/
|
|
7
7
|
import type { PipelineResult } from './spec-pipeline.js';
|
|
8
|
+
import type { DependencyGraphResult } from '../analyzer/dependency-graph.js';
|
|
9
|
+
import type { MappingArtifact } from './mapping-generator.js';
|
|
8
10
|
/**
|
|
9
11
|
* Generated spec file
|
|
10
12
|
*/
|
|
@@ -28,6 +30,8 @@ export interface GeneratorOptions {
|
|
|
28
30
|
includeTechnicalNotes?: boolean;
|
|
29
31
|
/** Maximum line width for wrapping */
|
|
30
32
|
maxLineWidth?: number;
|
|
33
|
+
/** Dependency graph for cross-domain dependency sections */
|
|
34
|
+
depGraph?: DependencyGraphResult;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* OpenSpec Format Generator
|
|
@@ -36,9 +40,10 @@ export declare class OpenSpecFormatGenerator {
|
|
|
36
40
|
private options;
|
|
37
41
|
constructor(options?: GeneratorOptions);
|
|
38
42
|
/**
|
|
39
|
-
* Generate all spec files from pipeline result
|
|
43
|
+
* Generate all spec files from pipeline result.
|
|
44
|
+
* Pass mappingArtifact to annotate each Requirement with `> Implementation: file:line`.
|
|
40
45
|
*/
|
|
41
|
-
generateSpecs(result: PipelineResult): GeneratedSpec[];
|
|
46
|
+
generateSpecs(result: PipelineResult, mappingArtifact?: MappingArtifact): GeneratedSpec[];
|
|
42
47
|
/**
|
|
43
48
|
* Group entities, services, and endpoints by domain
|
|
44
49
|
*/
|
|
@@ -63,6 +68,16 @@ export declare class OpenSpecFormatGenerator {
|
|
|
63
68
|
* Generate the API spec
|
|
64
69
|
*/
|
|
65
70
|
private generateApiSpec;
|
|
71
|
+
/**
|
|
72
|
+
* Emit `> Implementation: \`file:line\`` after a Requirement header when a
|
|
73
|
+
* high-confidence mapping entry exists. Mutates `lines` in-place.
|
|
74
|
+
*/
|
|
75
|
+
private emitImplementationHint;
|
|
76
|
+
/**
|
|
77
|
+
* Build `## Dependencies` section for a domain spec using depGraph edges.
|
|
78
|
+
* Returns an empty array if no depGraph or no cross-domain edges found.
|
|
79
|
+
*/
|
|
80
|
+
private buildDependencySection;
|
|
66
81
|
/**
|
|
67
82
|
* Add a scenario to the lines array
|
|
68
83
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openspec-format-generator.d.ts","sourceRoot":"","sources":["../../../src/core/generator/openspec-format-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"openspec-format-generator.d.ts","sourceRoot":"","sources":["../../../src/core/generator/openspec-format-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAM9D;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,cAAc,GAAG,KAAK,GAAG,KAAK,CAAC;CAC9D;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB;IACnB,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IAC/B,oCAAoC;IACpC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,8BAA8B;IAC9B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC;AAgCD;;GAEG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,OAAO,CAAkB;gBAErB,OAAO,GAAE,gBAAqB;IAK1C;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,aAAa,EAAE;IAuBzF;;OAEG;IACH,OAAO,CAAC,aAAa;IAmGrB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA2G5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAiN1B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA+IhC;;OAEG;IACH,OAAO,CAAC,eAAe;IAwGvB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAqB9B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAyE9B;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,cAAc;IAiBtB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAuBjB;AAMD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CA2D9D;AAMD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,aAAa,EAAE,CAGjB"}
|
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
* Takes structured LLM outputs and formats them into clean OpenSpec-compatible
|
|
5
5
|
* specification files.
|
|
6
6
|
*/
|
|
7
|
-
// ============================================================================
|
|
8
|
-
// CONSTANTS
|
|
9
|
-
// ============================================================================
|
|
10
7
|
const DEFAULT_OPTIONS = {
|
|
11
8
|
version: '1.0.0',
|
|
12
9
|
style: 'detailed',
|
|
@@ -23,19 +20,21 @@ const DEFAULT_OPTIONS = {
|
|
|
23
20
|
export class OpenSpecFormatGenerator {
|
|
24
21
|
options;
|
|
25
22
|
constructor(options = {}) {
|
|
26
|
-
|
|
23
|
+
const { depGraph, ...rest } = options;
|
|
24
|
+
this.options = { ...DEFAULT_OPTIONS, ...rest, depGraph };
|
|
27
25
|
}
|
|
28
26
|
/**
|
|
29
|
-
* Generate all spec files from pipeline result
|
|
27
|
+
* Generate all spec files from pipeline result.
|
|
28
|
+
* Pass mappingArtifact to annotate each Requirement with `> Implementation: file:line`.
|
|
30
29
|
*/
|
|
31
|
-
generateSpecs(result) {
|
|
30
|
+
generateSpecs(result, mappingArtifact) {
|
|
32
31
|
const specs = [];
|
|
33
32
|
const domains = this.groupByDomain(result);
|
|
34
33
|
// 1. Overview spec
|
|
35
34
|
specs.push(this.generateOverviewSpec(result.survey, domains, result.architecture));
|
|
36
35
|
// 2. Domain specs
|
|
37
36
|
for (const domain of domains) {
|
|
38
|
-
specs.push(this.generateDomainSpec(domain, result.survey));
|
|
37
|
+
specs.push(this.generateDomainSpec(domain, result.survey, mappingArtifact));
|
|
39
38
|
}
|
|
40
39
|
// 3. Architecture spec
|
|
41
40
|
specs.push(this.generateArchitectureSpec(result.architecture, result.survey, domains));
|
|
@@ -250,7 +249,7 @@ export class OpenSpecFormatGenerator {
|
|
|
250
249
|
/**
|
|
251
250
|
* Generate a domain spec
|
|
252
251
|
*/
|
|
253
|
-
generateDomainSpec(domain, _survey) {
|
|
252
|
+
generateDomainSpec(domain, _survey, mappingArtifact) {
|
|
254
253
|
const lines = [];
|
|
255
254
|
const now = new Date();
|
|
256
255
|
const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
@@ -332,8 +331,10 @@ export class OpenSpecFormatGenerator {
|
|
|
332
331
|
// Service operation requirements
|
|
333
332
|
for (const service of domain.services) {
|
|
334
333
|
for (const operation of (service.operations ?? [])) {
|
|
335
|
-
|
|
334
|
+
const reqName = this.formatRequirementName(operation.name);
|
|
335
|
+
lines.push(`### Requirement: ${reqName}`);
|
|
336
336
|
lines.push('');
|
|
337
|
+
this.emitImplementationHint(lines, reqName, domain.name, mappingArtifact);
|
|
337
338
|
const opDesc = (operation.description ?? '').replace(/^\s*(shall|must|should|may)\s+/i, '');
|
|
338
339
|
lines.push(`The system SHALL ${opDesc.toLowerCase()}`);
|
|
339
340
|
lines.push('');
|
|
@@ -378,6 +379,7 @@ export class OpenSpecFormatGenerator {
|
|
|
378
379
|
const reqName = this.formatRequirementName(endpoint.purpose || `${endpoint.method}${endpoint.path}`);
|
|
379
380
|
lines.push(`### Requirement: ${reqName}`);
|
|
380
381
|
lines.push('');
|
|
382
|
+
this.emitImplementationHint(lines, reqName, domain.name, mappingArtifact);
|
|
381
383
|
const epPurpose = (endpoint.purpose ?? 'handle this endpoint').replace(/^\s*(shall|must|should|may)\s+/i, '');
|
|
382
384
|
lines.push(`The system SHALL ${epPurpose.toLowerCase()}`);
|
|
383
385
|
lines.push('');
|
|
@@ -392,6 +394,7 @@ export class OpenSpecFormatGenerator {
|
|
|
392
394
|
const reqName = this.formatRequirementName(`${domain.name}Overview`);
|
|
393
395
|
lines.push(`### Requirement: ${reqName}`);
|
|
394
396
|
lines.push('');
|
|
397
|
+
this.emitImplementationHint(lines, reqName, domain.name, mappingArtifact);
|
|
395
398
|
lines.push(`The ${domain.name} domain SHALL provide its documented functionality.`);
|
|
396
399
|
lines.push('');
|
|
397
400
|
lines.push(`#### Scenario: ${reqName}Works`);
|
|
@@ -420,6 +423,11 @@ export class OpenSpecFormatGenerator {
|
|
|
420
423
|
}
|
|
421
424
|
lines.push('');
|
|
422
425
|
}
|
|
426
|
+
// Cross-domain dependency section (requires depGraph)
|
|
427
|
+
const depSection = this.buildDependencySection(domain.name, domain.files);
|
|
428
|
+
if (depSection.length > 0) {
|
|
429
|
+
lines.push(...depSection);
|
|
430
|
+
}
|
|
423
431
|
return {
|
|
424
432
|
path: `openspec/specs/${domain.name.toLowerCase()}/spec.md`,
|
|
425
433
|
content: lines.join('\n'),
|
|
@@ -645,6 +653,99 @@ export class OpenSpecFormatGenerator {
|
|
|
645
653
|
type: 'api',
|
|
646
654
|
};
|
|
647
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Emit `> Implementation: \`file:line\`` after a Requirement header when a
|
|
658
|
+
* high-confidence mapping entry exists. Mutates `lines` in-place.
|
|
659
|
+
*/
|
|
660
|
+
emitImplementationHint(lines, reqName, domainName, mappingArtifact) {
|
|
661
|
+
if (!mappingArtifact)
|
|
662
|
+
return;
|
|
663
|
+
const normReq = reqName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
664
|
+
const match = mappingArtifact.mappings.find(m => {
|
|
665
|
+
const normM = m.requirement.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
666
|
+
return normM === normReq && m.domain.toLowerCase() === domainName.toLowerCase();
|
|
667
|
+
});
|
|
668
|
+
if (!match || match.functions.length === 0)
|
|
669
|
+
return;
|
|
670
|
+
const best = [...match.functions].sort((a, b) => {
|
|
671
|
+
const order = { llm: 0, semantic: 1, heuristic: 2 };
|
|
672
|
+
return (order[a.confidence] ?? 3) - (order[b.confidence] ?? 3);
|
|
673
|
+
})[0];
|
|
674
|
+
lines.push(`> Implementation: \`${best.name}\` in \`${best.file}\` · confidence: ${best.confidence}`);
|
|
675
|
+
lines.push('');
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Build `## Dependencies` section for a domain spec using depGraph edges.
|
|
679
|
+
* Returns an empty array if no depGraph or no cross-domain edges found.
|
|
680
|
+
*/
|
|
681
|
+
buildDependencySection(domainName, _domainFiles) {
|
|
682
|
+
const depGraph = this.options.depGraph;
|
|
683
|
+
if (!depGraph)
|
|
684
|
+
return [];
|
|
685
|
+
// Resolve the cluster for this domain from depGraph
|
|
686
|
+
const cluster = depGraph.clusters.find(c => c.suggestedDomain.toLowerCase() === domainName.toLowerCase());
|
|
687
|
+
if (!cluster || cluster.files.length === 0)
|
|
688
|
+
return [];
|
|
689
|
+
const domainFileSet = new Set(cluster.files);
|
|
690
|
+
// Build file → cluster mapping
|
|
691
|
+
const clusterByFile = new Map();
|
|
692
|
+
for (const c of depGraph.clusters) {
|
|
693
|
+
for (const f of c.files) {
|
|
694
|
+
if (!clusterByFile.has(f)) {
|
|
695
|
+
clusterByFile.set(f, { id: c.id, suggestedDomain: c.suggestedDomain });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Scan edges for cross-domain calls
|
|
700
|
+
const callsInto = new Map(); // target domain → imported names
|
|
701
|
+
const calledBy = new Map(); // source domain → imported names
|
|
702
|
+
for (const edge of depGraph.edges) {
|
|
703
|
+
const srcInDomain = domainFileSet.has(edge.source);
|
|
704
|
+
const tgtInDomain = domainFileSet.has(edge.target);
|
|
705
|
+
if (srcInDomain === tgtInDomain)
|
|
706
|
+
continue; // intra-domain or unrelated
|
|
707
|
+
if (srcInDomain) {
|
|
708
|
+
const tgtCluster = clusterByFile.get(edge.target);
|
|
709
|
+
const tgtDomain = tgtCluster?.suggestedDomain.toLowerCase();
|
|
710
|
+
if (tgtDomain && tgtDomain !== domainName.toLowerCase()) {
|
|
711
|
+
if (!callsInto.has(tgtDomain))
|
|
712
|
+
callsInto.set(tgtDomain, new Set());
|
|
713
|
+
for (const name of (edge.importedNames ?? []))
|
|
714
|
+
callsInto.get(tgtDomain).add(name);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const srcCluster = clusterByFile.get(edge.source);
|
|
719
|
+
const srcDomain = srcCluster?.suggestedDomain.toLowerCase();
|
|
720
|
+
if (srcDomain && srcDomain !== domainName.toLowerCase()) {
|
|
721
|
+
if (!calledBy.has(srcDomain))
|
|
722
|
+
calledBy.set(srcDomain, new Set());
|
|
723
|
+
for (const name of (edge.importedNames ?? []))
|
|
724
|
+
calledBy.get(srcDomain).add(name);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (callsInto.size === 0 && calledBy.size === 0)
|
|
729
|
+
return [];
|
|
730
|
+
const lines = ['## Dependencies', ''];
|
|
731
|
+
if (calledBy.size > 0) {
|
|
732
|
+
lines.push('### Called by this domain');
|
|
733
|
+
for (const [srcDomain, names] of [...calledBy.entries()].sort()) {
|
|
734
|
+
const nameList = [...names].slice(0, 3).map(n => `\`${n}\``).join(', ');
|
|
735
|
+
lines.push(`- \`${srcDomain}\`${nameList ? ` → ${nameList}` : ''}`);
|
|
736
|
+
}
|
|
737
|
+
lines.push('');
|
|
738
|
+
}
|
|
739
|
+
if (callsInto.size > 0) {
|
|
740
|
+
lines.push('### Calls into');
|
|
741
|
+
for (const [tgtDomain, names] of [...callsInto.entries()].sort()) {
|
|
742
|
+
const nameList = [...names].slice(0, 3).map(n => `\`${n}\``).join(', ');
|
|
743
|
+
lines.push(`- \`${tgtDomain}\`${nameList ? ` → ${nameList}` : ''}`);
|
|
744
|
+
}
|
|
745
|
+
lines.push('');
|
|
746
|
+
}
|
|
747
|
+
return lines;
|
|
748
|
+
}
|
|
648
749
|
/**
|
|
649
750
|
* Add a scenario to the lines array
|
|
650
751
|
*/
|
|
@@ -669,7 +770,7 @@ export class OpenSpecFormatGenerator {
|
|
|
669
770
|
return 'Unnamed';
|
|
670
771
|
return name
|
|
671
772
|
.split(/[\s_-]+/)
|
|
672
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1)
|
|
773
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
673
774
|
.join('');
|
|
674
775
|
}
|
|
675
776
|
/**
|