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.
Files changed (51) hide show
  1. package/README.md +51 -18
  2. package/dist/api/generate.d.ts.map +1 -1
  3. package/dist/api/generate.js +46 -32
  4. package/dist/api/generate.js.map +1 -1
  5. package/dist/cli/commands/generate.d.ts.map +1 -1
  6. package/dist/cli/commands/generate.js +26 -12
  7. package/dist/cli/commands/generate.js.map +1 -1
  8. package/dist/cli/commands/mcp.d.ts +225 -0
  9. package/dist/cli/commands/mcp.d.ts.map +1 -1
  10. package/dist/cli/commands/mcp.js +116 -1
  11. package/dist/cli/commands/mcp.js.map +1 -1
  12. package/dist/cli/commands/refresh-stories.d.ts +10 -0
  13. package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
  14. package/dist/cli/commands/refresh-stories.js +314 -0
  15. package/dist/cli/commands/refresh-stories.js.map +1 -0
  16. package/dist/cli/index.js +2 -0
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/constants.d.ts +1 -0
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +1 -0
  21. package/dist/constants.js.map +1 -1
  22. package/dist/core/analyzer/unified-search.d.ts +107 -0
  23. package/dist/core/analyzer/unified-search.d.ts.map +1 -0
  24. package/dist/core/analyzer/unified-search.js +237 -0
  25. package/dist/core/analyzer/unified-search.js.map +1 -0
  26. package/dist/core/generator/openspec-format-generator.d.ts +17 -2
  27. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -1
  28. package/dist/core/generator/openspec-format-generator.js +111 -10
  29. package/dist/core/generator/openspec-format-generator.js.map +1 -1
  30. package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
  31. package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
  32. package/dist/core/generator/rag-manifest-generator.js +134 -0
  33. package/dist/core/generator/rag-manifest-generator.js.map +1 -0
  34. package/dist/core/services/chat-tools.d.ts.map +1 -1
  35. package/dist/core/services/chat-tools.js +54 -6
  36. package/dist/core/services/chat-tools.js.map +1 -1
  37. package/dist/core/services/mcp-handlers/change.d.ts +14 -0
  38. package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
  39. package/dist/core/services/mcp-handlers/change.js +416 -0
  40. package/dist/core/services/mcp-handlers/change.js.map +1 -0
  41. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
  42. package/dist/core/services/mcp-handlers/orient.js +108 -1
  43. package/dist/core/services/mcp-handlers/orient.js.map +1 -1
  44. package/dist/core/services/mcp-handlers/semantic.d.ts +4 -0
  45. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
  46. package/dist/core/services/mcp-handlers/semantic.js +101 -32
  47. package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
  48. package/package.json +4 -1
  49. package/src/viewer/InteractiveGraphViewer.jsx +46 -4
  50. package/src/viewer/components/ChatPanel.jsx +2 -3
  51. 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;AAM5B;;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;CACvB;AA8BD;;GAEG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,OAAO,CAA6B;gBAEhC,OAAO,GAAE,gBAAqB;IAI1C;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE;IAuBtD;;OAEG;IACH,OAAO,CAAC,aAAa;IAmGrB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA2G5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuM1B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA+IhC;;OAEG;IACH,OAAO,CAAC,eAAe;IAwGvB;;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"}
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
- this.options = { ...DEFAULT_OPTIONS, ...options };
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
- lines.push(`### Requirement: ${this.formatRequirementName(operation.name)}`);
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).toLowerCase())
773
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
673
774
  .join('');
674
775
  }
675
776
  /**