spec-gen-cli 1.0.0
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/LICENSE +21 -0
- package/README.md +1078 -0
- package/dist/api/analyze.d.ts +17 -0
- package/dist/api/analyze.d.ts.map +1 -0
- package/dist/api/analyze.js +109 -0
- package/dist/api/analyze.js.map +1 -0
- package/dist/api/drift.d.ts +21 -0
- package/dist/api/drift.d.ts.map +1 -0
- package/dist/api/drift.js +145 -0
- package/dist/api/drift.js.map +1 -0
- package/dist/api/generate.d.ts +18 -0
- package/dist/api/generate.d.ts.map +1 -0
- package/dist/api/generate.js +251 -0
- package/dist/api/generate.js.map +1 -0
- package/dist/api/index.d.ts +39 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +32 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/init.d.ts +18 -0
- package/dist/api/init.d.ts.map +1 -0
- package/dist/api/init.js +82 -0
- package/dist/api/init.js.map +1 -0
- package/dist/api/run.d.ts +19 -0
- package/dist/api/run.d.ts.map +1 -0
- package/dist/api/run.js +291 -0
- package/dist/api/run.js.map +1 -0
- package/dist/api/specs.d.ts +49 -0
- package/dist/api/specs.d.ts.map +1 -0
- package/dist/api/specs.js +136 -0
- package/dist/api/specs.js.map +1 -0
- package/dist/api/types.d.ts +176 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +9 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/verify.d.ts +20 -0
- package/dist/api/verify.d.ts.map +1 -0
- package/dist/api/verify.js +117 -0
- package/dist/api/verify.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +27 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +485 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/drift.d.ts +9 -0
- package/dist/cli/commands/drift.d.ts.map +1 -0
- package/dist/cli/commands/drift.js +540 -0
- package/dist/cli/commands/drift.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +9 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +633 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +171 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +638 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +574 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/run.d.ts +24 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +546 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/verify.d.ts +9 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +417 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/commands/view.d.ts +9 -0
- package/dist/cli/commands/view.d.ts.map +1 -0
- package/dist/cli/commands/view.js +511 -0
- package/dist/cli/commands/view.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +83 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/analyzer/architecture-writer.d.ts +67 -0
- package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
- package/dist/core/analyzer/architecture-writer.js +209 -0
- package/dist/core/analyzer/architecture-writer.js.map +1 -0
- package/dist/core/analyzer/artifact-generator.d.ts +222 -0
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
- package/dist/core/analyzer/artifact-generator.js +726 -0
- package/dist/core/analyzer/artifact-generator.js.map +1 -0
- package/dist/core/analyzer/call-graph.d.ts +83 -0
- package/dist/core/analyzer/call-graph.d.ts.map +1 -0
- package/dist/core/analyzer/call-graph.js +827 -0
- package/dist/core/analyzer/call-graph.js.map +1 -0
- package/dist/core/analyzer/code-shaper.d.ts +33 -0
- package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
- package/dist/core/analyzer/code-shaper.js +149 -0
- package/dist/core/analyzer/code-shaper.js.map +1 -0
- package/dist/core/analyzer/dependency-graph.d.ts +179 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
- package/dist/core/analyzer/dependency-graph.js +574 -0
- package/dist/core/analyzer/dependency-graph.js.map +1 -0
- package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
- package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
- package/dist/core/analyzer/duplicate-detector.js +279 -0
- package/dist/core/analyzer/duplicate-detector.js.map +1 -0
- package/dist/core/analyzer/embedding-service.d.ts +50 -0
- package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
- package/dist/core/analyzer/embedding-service.js +104 -0
- package/dist/core/analyzer/embedding-service.js.map +1 -0
- package/dist/core/analyzer/file-walker.d.ts +78 -0
- package/dist/core/analyzer/file-walker.d.ts.map +1 -0
- package/dist/core/analyzer/file-walker.js +531 -0
- package/dist/core/analyzer/file-walker.js.map +1 -0
- package/dist/core/analyzer/import-parser.d.ts +91 -0
- package/dist/core/analyzer/import-parser.d.ts.map +1 -0
- package/dist/core/analyzer/import-parser.js +720 -0
- package/dist/core/analyzer/import-parser.js.map +1 -0
- package/dist/core/analyzer/index.d.ts +10 -0
- package/dist/core/analyzer/index.d.ts.map +1 -0
- package/dist/core/analyzer/index.js +10 -0
- package/dist/core/analyzer/index.js.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts +80 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.js +339 -0
- package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
- package/dist/core/analyzer/repository-mapper.d.ts +150 -0
- package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
- package/dist/core/analyzer/repository-mapper.js +731 -0
- package/dist/core/analyzer/repository-mapper.js.map +1 -0
- package/dist/core/analyzer/signature-extractor.d.ts +31 -0
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/signature-extractor.js +387 -0
- package/dist/core/analyzer/signature-extractor.js.map +1 -0
- package/dist/core/analyzer/significance-scorer.d.ts +79 -0
- package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
- package/dist/core/analyzer/significance-scorer.js +407 -0
- package/dist/core/analyzer/significance-scorer.js.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts +43 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.js +129 -0
- package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
- package/dist/core/analyzer/vector-index.d.ts +63 -0
- package/dist/core/analyzer/vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/vector-index.js +169 -0
- package/dist/core/analyzer/vector-index.js.map +1 -0
- package/dist/core/drift/drift-detector.d.ts +102 -0
- package/dist/core/drift/drift-detector.d.ts.map +1 -0
- package/dist/core/drift/drift-detector.js +597 -0
- package/dist/core/drift/drift-detector.js.map +1 -0
- package/dist/core/drift/git-diff.d.ts +55 -0
- package/dist/core/drift/git-diff.d.ts.map +1 -0
- package/dist/core/drift/git-diff.js +356 -0
- package/dist/core/drift/git-diff.js.map +1 -0
- package/dist/core/drift/index.d.ts +12 -0
- package/dist/core/drift/index.d.ts.map +1 -0
- package/dist/core/drift/index.js +9 -0
- package/dist/core/drift/index.js.map +1 -0
- package/dist/core/drift/spec-mapper.d.ts +73 -0
- package/dist/core/drift/spec-mapper.d.ts.map +1 -0
- package/dist/core/drift/spec-mapper.js +353 -0
- package/dist/core/drift/spec-mapper.js.map +1 -0
- package/dist/core/generator/adr-generator.d.ts +32 -0
- package/dist/core/generator/adr-generator.d.ts.map +1 -0
- package/dist/core/generator/adr-generator.js +192 -0
- package/dist/core/generator/adr-generator.js.map +1 -0
- package/dist/core/generator/index.d.ts +9 -0
- package/dist/core/generator/index.d.ts.map +1 -0
- package/dist/core/generator/index.js +12 -0
- package/dist/core/generator/index.js.map +1 -0
- package/dist/core/generator/mapping-generator.d.ts +54 -0
- package/dist/core/generator/mapping-generator.d.ts.map +1 -0
- package/dist/core/generator/mapping-generator.js +239 -0
- package/dist/core/generator/mapping-generator.js.map +1 -0
- package/dist/core/generator/openspec-compat.d.ts +160 -0
- package/dist/core/generator/openspec-compat.d.ts.map +1 -0
- package/dist/core/generator/openspec-compat.js +523 -0
- package/dist/core/generator/openspec-compat.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts +111 -0
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
- package/dist/core/generator/openspec-format-generator.js +817 -0
- package/dist/core/generator/openspec-format-generator.js.map +1 -0
- package/dist/core/generator/openspec-writer.d.ts +131 -0
- package/dist/core/generator/openspec-writer.d.ts.map +1 -0
- package/dist/core/generator/openspec-writer.js +379 -0
- package/dist/core/generator/openspec-writer.js.map +1 -0
- package/dist/core/generator/prompts.d.ts +35 -0
- package/dist/core/generator/prompts.d.ts.map +1 -0
- package/dist/core/generator/prompts.js +212 -0
- package/dist/core/generator/prompts.js.map +1 -0
- package/dist/core/generator/spec-pipeline.d.ts +94 -0
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
- package/dist/core/generator/spec-pipeline.js +474 -0
- package/dist/core/generator/spec-pipeline.js.map +1 -0
- package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
- package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
- package/dist/core/generator/stages/stage1-survey.js +105 -0
- package/dist/core/generator/stages/stage1-survey.js.map +1 -0
- package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
- package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
- package/dist/core/generator/stages/stage2-entities.js +67 -0
- package/dist/core/generator/stages/stage2-entities.js.map +1 -0
- package/dist/core/generator/stages/stage3-services.d.ts +11 -0
- package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
- package/dist/core/generator/stages/stage3-services.js +75 -0
- package/dist/core/generator/stages/stage3-services.js.map +1 -0
- package/dist/core/generator/stages/stage4-api.d.ts +11 -0
- package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
- package/dist/core/generator/stages/stage4-api.js +65 -0
- package/dist/core/generator/stages/stage4-api.js.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts +10 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.js +62 -0
- package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
- package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
- package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
- package/dist/core/generator/stages/stage6-adr.js +41 -0
- package/dist/core/generator/stages/stage6-adr.js.map +1 -0
- package/dist/core/services/chat-agent.d.ts +45 -0
- package/dist/core/services/chat-agent.d.ts.map +1 -0
- package/dist/core/services/chat-agent.js +310 -0
- package/dist/core/services/chat-agent.js.map +1 -0
- package/dist/core/services/chat-tools.d.ts +32 -0
- package/dist/core/services/chat-tools.d.ts.map +1 -0
- package/dist/core/services/chat-tools.js +270 -0
- package/dist/core/services/chat-tools.js.map +1 -0
- package/dist/core/services/config-manager.d.ts +61 -0
- package/dist/core/services/config-manager.d.ts.map +1 -0
- package/dist/core/services/config-manager.js +143 -0
- package/dist/core/services/config-manager.js.map +1 -0
- package/dist/core/services/gitignore-manager.d.ts +29 -0
- package/dist/core/services/gitignore-manager.d.ts.map +1 -0
- package/dist/core/services/gitignore-manager.js +106 -0
- package/dist/core/services/gitignore-manager.js.map +1 -0
- package/dist/core/services/index.d.ts +8 -0
- package/dist/core/services/index.d.ts.map +1 -0
- package/dist/core/services/index.js +8 -0
- package/dist/core/services/index.js.map +1 -0
- package/dist/core/services/llm-service.d.ts +336 -0
- package/dist/core/services/llm-service.d.ts.map +1 -0
- package/dist/core/services/llm-service.js +1155 -0
- package/dist/core/services/llm-service.js.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts +42 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.js +300 -0
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
- package/dist/core/services/mcp-handlers/graph.d.ts +65 -0
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/graph.js +509 -0
- package/dist/core/services/mcp-handlers/graph.js.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts +38 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.js +172 -0
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
- package/dist/core/services/mcp-handlers/utils.d.ts +21 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/utils.js +62 -0
- package/dist/core/services/mcp-handlers/utils.js.map +1 -0
- package/dist/core/services/project-detector.d.ts +32 -0
- package/dist/core/services/project-detector.d.ts.map +1 -0
- package/dist/core/services/project-detector.js +111 -0
- package/dist/core/services/project-detector.js.map +1 -0
- package/dist/core/verifier/index.d.ts +5 -0
- package/dist/core/verifier/index.d.ts.map +1 -0
- package/dist/core/verifier/index.js +5 -0
- package/dist/core/verifier/index.js.map +1 -0
- package/dist/core/verifier/verification-engine.d.ts +226 -0
- package/dist/core/verifier/verification-engine.d.ts.map +1 -0
- package/dist/core/verifier/verification-engine.js +681 -0
- package/dist/core/verifier/verification-engine.js.map +1 -0
- package/dist/types/index.d.ts +252 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/pipeline.d.ts +148 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +5 -0
- package/dist/types/pipeline.js.map +1 -0
- package/dist/utils/errors.d.ts +51 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +128 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +149 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +331 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/progress.d.ts +142 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +280 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/prompts.d.ts +53 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +199 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/shutdown.d.ts +89 -0
- package/dist/utils/shutdown.d.ts.map +1 -0
- package/dist/utils/shutdown.js +237 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/package.json +114 -0
- package/src/viewer/InteractiveGraphViewer.jsx +1486 -0
- package/src/viewer/app/index.html +17 -0
- package/src/viewer/app/main.jsx +13 -0
- package/src/viewer/components/ArchitectureView.jsx +177 -0
- package/src/viewer/components/ChatPanel.jsx +448 -0
- package/src/viewer/components/ClusterGraph.jsx +441 -0
- package/src/viewer/components/FilterBar.jsx +179 -0
- package/src/viewer/components/FlatGraph.jsx +275 -0
- package/src/viewer/components/MicroComponents.jsx +83 -0
- package/src/viewer/hooks/usePanZoom.js +79 -0
- package/src/viewer/utils/constants.js +47 -0
- package/src/viewer/utils/graph-helpers.js +291 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenSpec Format Generator
|
|
3
|
+
*
|
|
4
|
+
* Takes structured LLM outputs and formats them into clean OpenSpec-compatible
|
|
5
|
+
* specification files.
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// CONSTANTS
|
|
9
|
+
// ============================================================================
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
style: 'detailed',
|
|
13
|
+
includeConfidence: true,
|
|
14
|
+
includeTechnicalNotes: true,
|
|
15
|
+
maxLineWidth: 100,
|
|
16
|
+
};
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// OPENSPEC FORMAT GENERATOR
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* OpenSpec Format Generator
|
|
22
|
+
*/
|
|
23
|
+
export class OpenSpecFormatGenerator {
|
|
24
|
+
options;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate all spec files from pipeline result
|
|
30
|
+
*/
|
|
31
|
+
generateSpecs(result) {
|
|
32
|
+
const specs = [];
|
|
33
|
+
const domains = this.groupByDomain(result);
|
|
34
|
+
// 1. Overview spec
|
|
35
|
+
specs.push(this.generateOverviewSpec(result.survey, domains, result.architecture));
|
|
36
|
+
// 2. Domain specs
|
|
37
|
+
for (const domain of domains) {
|
|
38
|
+
specs.push(this.generateDomainSpec(domain, result.survey));
|
|
39
|
+
}
|
|
40
|
+
// 3. Architecture spec
|
|
41
|
+
specs.push(this.generateArchitectureSpec(result.architecture, result.survey, domains));
|
|
42
|
+
// 4. API spec (if endpoints exist)
|
|
43
|
+
if (result.endpoints.length > 0) {
|
|
44
|
+
specs.push(this.generateApiSpec(result.endpoints, result.survey));
|
|
45
|
+
}
|
|
46
|
+
return specs;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Group entities, services, and endpoints by domain
|
|
50
|
+
*/
|
|
51
|
+
groupByDomain(result) {
|
|
52
|
+
const domainMap = new Map();
|
|
53
|
+
// Initialize domains from survey suggestions
|
|
54
|
+
for (const domainName of result.survey.suggestedDomains) {
|
|
55
|
+
domainMap.set(domainName.toLowerCase(), {
|
|
56
|
+
name: domainName,
|
|
57
|
+
description: '',
|
|
58
|
+
entities: [],
|
|
59
|
+
services: [],
|
|
60
|
+
endpoints: [],
|
|
61
|
+
files: [],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Add entities to domains
|
|
65
|
+
for (const entity of result.entities) {
|
|
66
|
+
const domainName = this.inferDomain(entity.name, entity.location, result.survey.suggestedDomains);
|
|
67
|
+
let domain = domainMap.get(domainName.toLowerCase());
|
|
68
|
+
if (!domain) {
|
|
69
|
+
domain = {
|
|
70
|
+
name: domainName,
|
|
71
|
+
description: '',
|
|
72
|
+
entities: [],
|
|
73
|
+
services: [],
|
|
74
|
+
endpoints: [],
|
|
75
|
+
files: [],
|
|
76
|
+
};
|
|
77
|
+
domainMap.set(domainName.toLowerCase(), domain);
|
|
78
|
+
}
|
|
79
|
+
domain.entities.push(entity);
|
|
80
|
+
if (entity.location && !domain.files.includes(entity.location)) {
|
|
81
|
+
domain.files.push(entity.location);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Add services to domains
|
|
85
|
+
for (const service of result.services) {
|
|
86
|
+
const domainName = service.domain || this.inferDomain(service.name, '', result.survey.suggestedDomains);
|
|
87
|
+
let domain = domainMap.get(domainName.toLowerCase());
|
|
88
|
+
if (!domain) {
|
|
89
|
+
domain = {
|
|
90
|
+
name: domainName,
|
|
91
|
+
description: '',
|
|
92
|
+
entities: [],
|
|
93
|
+
services: [],
|
|
94
|
+
endpoints: [],
|
|
95
|
+
files: [],
|
|
96
|
+
};
|
|
97
|
+
domainMap.set(domainName.toLowerCase(), domain);
|
|
98
|
+
}
|
|
99
|
+
domain.services.push(service);
|
|
100
|
+
}
|
|
101
|
+
// Add endpoints to domains
|
|
102
|
+
for (const endpoint of result.endpoints) {
|
|
103
|
+
const domainName = endpoint.relatedEntity
|
|
104
|
+
? this.inferDomain(endpoint.relatedEntity, endpoint.path, result.survey.suggestedDomains)
|
|
105
|
+
: 'api';
|
|
106
|
+
let domain = domainMap.get(domainName.toLowerCase());
|
|
107
|
+
if (!domain) {
|
|
108
|
+
domain = {
|
|
109
|
+
name: domainName,
|
|
110
|
+
description: '',
|
|
111
|
+
entities: [],
|
|
112
|
+
services: [],
|
|
113
|
+
endpoints: [],
|
|
114
|
+
files: [],
|
|
115
|
+
};
|
|
116
|
+
domainMap.set(domainName.toLowerCase(), domain);
|
|
117
|
+
}
|
|
118
|
+
domain.endpoints.push(endpoint);
|
|
119
|
+
}
|
|
120
|
+
// Set descriptions based on content — prefer service purpose (descriptive) over entity list
|
|
121
|
+
for (const domain of domainMap.values()) {
|
|
122
|
+
if (domain.services.length > 0) {
|
|
123
|
+
const representative = domain.services.find(s => s.name.toLowerCase().includes(domain.name.toLowerCase())) ?? domain.services[0];
|
|
124
|
+
domain.description = representative.purpose;
|
|
125
|
+
}
|
|
126
|
+
else if (domain.entities.length > 0) {
|
|
127
|
+
const preview = domain.entities.slice(0, 3).map(e => e.name).join(', ');
|
|
128
|
+
const extra = domain.entities.length > 3 ? ` and ${domain.entities.length - 3} more` : '';
|
|
129
|
+
domain.description = `Defines core data models: ${preview}${extra}.`;
|
|
130
|
+
}
|
|
131
|
+
else if (domain.endpoints.length > 0) {
|
|
132
|
+
const firstPurpose = domain.endpoints[0]?.purpose;
|
|
133
|
+
domain.description = firstPurpose
|
|
134
|
+
? firstPurpose
|
|
135
|
+
: `Provides ${domain.endpoints.length} API endpoint${domain.endpoints.length > 1 ? 's' : ''}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Filter out empty domains
|
|
139
|
+
return Array.from(domainMap.values()).filter(d => d.entities.length > 0 || d.services.length > 0 || d.endpoints.length > 0);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Infer domain from name and location
|
|
143
|
+
*/
|
|
144
|
+
inferDomain(name, location, suggestedDomains) {
|
|
145
|
+
const nameLower = (name ?? '').toLowerCase();
|
|
146
|
+
const locationLower = (location ?? '').toLowerCase();
|
|
147
|
+
// Check suggested domains first
|
|
148
|
+
for (const domain of suggestedDomains) {
|
|
149
|
+
if (nameLower.includes(domain.toLowerCase()) || locationLower.includes(domain.toLowerCase())) {
|
|
150
|
+
return domain;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Fall back to first suggested domain rather than inventing one from the name prefix
|
|
154
|
+
return suggestedDomains[0] ?? 'core';
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Generate the overview spec
|
|
158
|
+
*/
|
|
159
|
+
generateOverviewSpec(survey, domains, architecture) {
|
|
160
|
+
const lines = [];
|
|
161
|
+
const now = new Date();
|
|
162
|
+
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')}`;
|
|
163
|
+
// Header
|
|
164
|
+
lines.push('# System Overview');
|
|
165
|
+
lines.push('');
|
|
166
|
+
lines.push(`> Generated by spec-gen v${this.options.version} on ${date}`);
|
|
167
|
+
if (this.options.includeConfidence) {
|
|
168
|
+
lines.push(`> Confidence: ${Math.round(survey.confidence * 100)}%`);
|
|
169
|
+
}
|
|
170
|
+
lines.push('');
|
|
171
|
+
// Purpose
|
|
172
|
+
lines.push('## Purpose');
|
|
173
|
+
lines.push('');
|
|
174
|
+
lines.push(this.wrapText(architecture.systemPurpose));
|
|
175
|
+
lines.push('');
|
|
176
|
+
// Domains
|
|
177
|
+
lines.push('## Domains');
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push('This system is organized into the following domains:');
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push('| Domain | Description | Spec |');
|
|
182
|
+
lines.push('|--------|-------------|------|');
|
|
183
|
+
for (const domain of domains) {
|
|
184
|
+
const specPath = `../${domain.name.toLowerCase()}/spec.md`;
|
|
185
|
+
lines.push(`| ${this.capitalize(domain.name)} | ${domain.description || 'No description'} | [spec.md](${specPath}) |`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
// Technical Stack
|
|
189
|
+
lines.push('## Technical Stack');
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push(`- **Type**: ${this.formatCategory(survey.projectCategory)}`);
|
|
192
|
+
lines.push(`- **Primary Language**: ${survey.primaryLanguage}`);
|
|
193
|
+
lines.push(`- **Key Frameworks**: ${survey.frameworks.join(', ') || 'None detected'}`);
|
|
194
|
+
lines.push(`- **Architecture**: ${this.formatArchitecture(survey.architecturePattern)}`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
// Requirements
|
|
197
|
+
lines.push('## Requirements');
|
|
198
|
+
lines.push('');
|
|
199
|
+
// Generate capabilities from architecture
|
|
200
|
+
if (architecture.keyDecisions.length > 0) {
|
|
201
|
+
lines.push('### Requirement: SystemCapabilities');
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push('The system SHALL provide the following capabilities:');
|
|
204
|
+
for (const decision of architecture.keyDecisions) {
|
|
205
|
+
lines.push(`- ${decision}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push('#### Scenario: CapabilitiesProvided');
|
|
209
|
+
lines.push('- **GIVEN** the system is operational');
|
|
210
|
+
lines.push('- **WHEN** a user interacts with the system');
|
|
211
|
+
lines.push('- **THEN** the system provides the documented capabilities');
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
// Data flow as a scenario
|
|
215
|
+
if (architecture.dataFlow && architecture.dataFlow !== 'Unknown') {
|
|
216
|
+
lines.push('### Requirement: DataFlow');
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('The system SHALL process data through defined layers:');
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push('#### Scenario: StandardDataFlow');
|
|
221
|
+
lines.push('- **GIVEN** an incoming request');
|
|
222
|
+
lines.push(`- **WHEN** the request is processed`);
|
|
223
|
+
lines.push(`- **THEN** data flows through: ${architecture.dataFlow}`);
|
|
224
|
+
lines.push('');
|
|
225
|
+
}
|
|
226
|
+
// Technical notes
|
|
227
|
+
if (this.options.includeTechnicalNotes) {
|
|
228
|
+
lines.push('## Technical Notes');
|
|
229
|
+
lines.push('');
|
|
230
|
+
const archStyleNote = typeof architecture.architectureStyle === 'string'
|
|
231
|
+
? architecture.architectureStyle
|
|
232
|
+
: architecture.architectureStyle?.pattern ?? architecture.architectureStyle?.name ?? JSON.stringify(architecture.architectureStyle);
|
|
233
|
+
lines.push(`- **Architecture Style**: ${archStyleNote}`);
|
|
234
|
+
if (architecture.securityModel && architecture.securityModel !== 'Unknown') {
|
|
235
|
+
lines.push(`- **Security Model**: ${architecture.securityModel}`);
|
|
236
|
+
}
|
|
237
|
+
if (architecture.integrations.length > 0) {
|
|
238
|
+
const integrationNames = architecture.integrations.map(i => typeof i === 'string' ? i : i.name ?? JSON.stringify(i));
|
|
239
|
+
lines.push(`- **External Integrations**: ${integrationNames.join(', ')}`);
|
|
240
|
+
}
|
|
241
|
+
lines.push('');
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
path: 'openspec/specs/overview/spec.md',
|
|
245
|
+
content: lines.join('\n'),
|
|
246
|
+
domain: 'overview',
|
|
247
|
+
type: 'overview',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Generate a domain spec
|
|
252
|
+
*/
|
|
253
|
+
generateDomainSpec(domain, _survey) {
|
|
254
|
+
const lines = [];
|
|
255
|
+
const now = new Date();
|
|
256
|
+
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')}`;
|
|
257
|
+
// Header
|
|
258
|
+
lines.push(`# ${this.capitalize(domain.name)} Specification`);
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push(`> Generated by spec-gen v${this.options.version} on ${date}`);
|
|
261
|
+
if (domain.files.length > 0) {
|
|
262
|
+
lines.push(`> Source files: ${domain.files.join(', ')}`);
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
// Purpose
|
|
266
|
+
lines.push('## Purpose');
|
|
267
|
+
lines.push('');
|
|
268
|
+
lines.push(this.wrapText(domain.description || `The ${domain.name} domain manages core business logic.`));
|
|
269
|
+
lines.push('');
|
|
270
|
+
// Entities section
|
|
271
|
+
if (domain.entities.length > 0) {
|
|
272
|
+
lines.push('## Entities');
|
|
273
|
+
lines.push('');
|
|
274
|
+
for (const entity of domain.entities) {
|
|
275
|
+
lines.push(`### ${entity.name}`);
|
|
276
|
+
lines.push('');
|
|
277
|
+
lines.push(this.wrapText(entity.description));
|
|
278
|
+
lines.push('');
|
|
279
|
+
// Properties table
|
|
280
|
+
if ((entity.properties ?? []).length > 0) {
|
|
281
|
+
lines.push('**Properties:**');
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push('| Name | Type | Description |');
|
|
284
|
+
lines.push('|------|------|-------------|');
|
|
285
|
+
for (const prop of (entity.properties ?? [])) {
|
|
286
|
+
const desc = prop.description || (prop.required ? 'Required' : 'Optional');
|
|
287
|
+
lines.push(`| ${prop.name} | ${prop.type} | ${desc} |`);
|
|
288
|
+
}
|
|
289
|
+
lines.push('');
|
|
290
|
+
}
|
|
291
|
+
// Relationships
|
|
292
|
+
if ((entity.relationships ?? []).length > 0) {
|
|
293
|
+
lines.push('**Relationships:**');
|
|
294
|
+
lines.push('');
|
|
295
|
+
for (const rel of (entity.relationships ?? [])) {
|
|
296
|
+
lines.push(`- ${this.formatRelationship(rel)}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push('');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Requirements section
|
|
303
|
+
lines.push('## Requirements');
|
|
304
|
+
lines.push('');
|
|
305
|
+
// Entity validation requirements
|
|
306
|
+
for (const entity of domain.entities) {
|
|
307
|
+
if ((entity.validations ?? []).length > 0) {
|
|
308
|
+
lines.push(`### Requirement: ${entity.name}Validation`);
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(`The system SHALL validate ${entity.name} according to these rules:`);
|
|
311
|
+
for (const rule of (entity.validations ?? [])) {
|
|
312
|
+
lines.push(`- ${rule}`);
|
|
313
|
+
}
|
|
314
|
+
lines.push('');
|
|
315
|
+
// Scenarios from entity
|
|
316
|
+
const entityScenarios = entity.scenarios ?? [];
|
|
317
|
+
if (entityScenarios.length > 0) {
|
|
318
|
+
for (const scenario of entityScenarios) {
|
|
319
|
+
this.addScenario(lines, scenario);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Validator requires at least one scenario per requirement
|
|
324
|
+
lines.push(`#### Scenario: Valid${entity.name}Accepted`);
|
|
325
|
+
lines.push(`- **GIVEN** A valid ${entity.name} object with all required fields`);
|
|
326
|
+
lines.push(`- **WHEN** The object is validated`);
|
|
327
|
+
lines.push(`- **THEN** Validation passes with no errors`);
|
|
328
|
+
lines.push('');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Service operation requirements
|
|
333
|
+
for (const service of domain.services) {
|
|
334
|
+
for (const operation of (service.operations ?? [])) {
|
|
335
|
+
lines.push(`### Requirement: ${this.formatRequirementName(operation.name)}`);
|
|
336
|
+
lines.push('');
|
|
337
|
+
const opDesc = (operation.description ?? '').replace(/^\s*(shall|must|should|may)\s+/i, '');
|
|
338
|
+
lines.push(`The system SHALL ${opDesc.toLowerCase()}`);
|
|
339
|
+
lines.push('');
|
|
340
|
+
// Operation scenarios
|
|
341
|
+
for (const scenario of (operation.scenarios ?? [])) {
|
|
342
|
+
this.addScenario(lines, scenario);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Sub-components for orchestrator services (god functions)
|
|
346
|
+
if (service.subSpecs && service.subSpecs.length > 0) {
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push('## Sub-components');
|
|
349
|
+
lines.push('');
|
|
350
|
+
lines.push(`> \`${service.name}\` is an orchestrator. Each sub-component below implements one logical block.`);
|
|
351
|
+
lines.push('');
|
|
352
|
+
for (const sub of service.subSpecs) {
|
|
353
|
+
lines.push(`### Sub-component: ${this.formatRequirementName(sub.name)}`);
|
|
354
|
+
lines.push('');
|
|
355
|
+
lines.push(`> Implements: \`${sub.callee}\``);
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push(sub.purpose);
|
|
358
|
+
lines.push('');
|
|
359
|
+
for (const op of (sub.operations ?? [])) {
|
|
360
|
+
lines.push(`#### Requirement: ${this.formatRequirementName(op.name)}`);
|
|
361
|
+
lines.push('');
|
|
362
|
+
const opDesc = (op.description ?? '').replace(/^\s*(shall|must|should|may)\s+/i, '');
|
|
363
|
+
lines.push(`The system SHALL ${opDesc.toLowerCase()}`);
|
|
364
|
+
lines.push('');
|
|
365
|
+
for (const scenario of (op.scenarios ?? [])) {
|
|
366
|
+
this.addScenario(lines, scenario);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Fallback: if no requirements were generated, add a placeholder
|
|
373
|
+
const hasRequirements = domain.entities.some(e => (e.validations ?? []).length > 0) ||
|
|
374
|
+
domain.services.some(s => (s.operations ?? []).length > 0);
|
|
375
|
+
if (!hasRequirements) {
|
|
376
|
+
if (domain.endpoints.length > 0) {
|
|
377
|
+
for (const endpoint of domain.endpoints) {
|
|
378
|
+
const reqName = this.formatRequirementName(endpoint.purpose || `${endpoint.method}${endpoint.path}`);
|
|
379
|
+
lines.push(`### Requirement: ${reqName}`);
|
|
380
|
+
lines.push('');
|
|
381
|
+
const epPurpose = (endpoint.purpose ?? 'handle this endpoint').replace(/^\s*(shall|must|should|may)\s+/i, '');
|
|
382
|
+
lines.push(`The system SHALL ${epPurpose.toLowerCase()}`);
|
|
383
|
+
lines.push('');
|
|
384
|
+
lines.push(`#### Scenario: ${reqName}Success`);
|
|
385
|
+
lines.push(`- **GIVEN** the system is operational`);
|
|
386
|
+
lines.push(`- **WHEN** ${endpoint.method} ${endpoint.path} is called`);
|
|
387
|
+
lines.push(`- **THEN** the request is processed successfully`);
|
|
388
|
+
lines.push('');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
const reqName = this.formatRequirementName(`${domain.name}Overview`);
|
|
393
|
+
lines.push(`### Requirement: ${reqName}`);
|
|
394
|
+
lines.push('');
|
|
395
|
+
lines.push(`The ${domain.name} domain SHALL provide its documented functionality.`);
|
|
396
|
+
lines.push('');
|
|
397
|
+
lines.push(`#### Scenario: ${reqName}Works`);
|
|
398
|
+
lines.push('- **GIVEN** the system is operational');
|
|
399
|
+
lines.push('- **WHEN** the domain functionality is invoked');
|
|
400
|
+
lines.push('- **THEN** the expected outcome is produced');
|
|
401
|
+
lines.push('');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Technical notes
|
|
405
|
+
if (this.options.includeTechnicalNotes && domain.services.length > 0) {
|
|
406
|
+
lines.push('## Technical Notes');
|
|
407
|
+
lines.push('');
|
|
408
|
+
const allFiles = new Set(domain.files);
|
|
409
|
+
const allDeps = new Set();
|
|
410
|
+
for (const service of domain.services) {
|
|
411
|
+
for (const dep of (service.dependencies ?? [])) {
|
|
412
|
+
allDeps.add(dep);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (allFiles.size > 0) {
|
|
416
|
+
lines.push(`- **Implementation**: \`${Array.from(allFiles).join(', ')}\``);
|
|
417
|
+
}
|
|
418
|
+
if (allDeps.size > 0) {
|
|
419
|
+
lines.push(`- **Dependencies**: ${Array.from(allDeps).join(', ')}`);
|
|
420
|
+
}
|
|
421
|
+
lines.push('');
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
path: `openspec/specs/${domain.name.toLowerCase()}/spec.md`,
|
|
425
|
+
content: lines.join('\n'),
|
|
426
|
+
domain: domain.name.toLowerCase(),
|
|
427
|
+
type: 'domain',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Generate the architecture spec
|
|
432
|
+
*/
|
|
433
|
+
generateArchitectureSpec(architecture, _survey, _domains) {
|
|
434
|
+
const lines = [];
|
|
435
|
+
const now = new Date();
|
|
436
|
+
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')}`;
|
|
437
|
+
// Header
|
|
438
|
+
lines.push('# Architecture Specification');
|
|
439
|
+
lines.push('');
|
|
440
|
+
lines.push(`> Generated by spec-gen v${this.options.version} on ${date}`);
|
|
441
|
+
lines.push('');
|
|
442
|
+
// Purpose
|
|
443
|
+
lines.push('## Purpose');
|
|
444
|
+
lines.push('');
|
|
445
|
+
lines.push('This document describes the architectural patterns and structure of the system.');
|
|
446
|
+
lines.push('');
|
|
447
|
+
// Architecture Style
|
|
448
|
+
lines.push('## Architecture Style');
|
|
449
|
+
lines.push('');
|
|
450
|
+
const archStyle = architecture.architectureStyle;
|
|
451
|
+
const archStyleStr = typeof archStyle === 'string'
|
|
452
|
+
? archStyle
|
|
453
|
+
: archStyle?.pattern ?? archStyle?.name ?? JSON.stringify(archStyle);
|
|
454
|
+
const archJustification = typeof archStyle === 'object' && archStyle !== null
|
|
455
|
+
? archStyle?.justification
|
|
456
|
+
: undefined;
|
|
457
|
+
lines.push(this.wrapText(archStyleStr));
|
|
458
|
+
if (archJustification) {
|
|
459
|
+
lines.push('');
|
|
460
|
+
lines.push(`*${archJustification}*`);
|
|
461
|
+
}
|
|
462
|
+
lines.push('');
|
|
463
|
+
// Requirements
|
|
464
|
+
lines.push('## Requirements');
|
|
465
|
+
lines.push('');
|
|
466
|
+
// Layered architecture requirement
|
|
467
|
+
if (architecture.layerMap.length > 0) {
|
|
468
|
+
lines.push('### Requirement: LayeredArchitecture');
|
|
469
|
+
lines.push('');
|
|
470
|
+
lines.push('The system SHALL maintain separation between:');
|
|
471
|
+
for (const layer of architecture.layerMap) {
|
|
472
|
+
lines.push(`- ${layer.name} (${layer.purpose})`);
|
|
473
|
+
}
|
|
474
|
+
lines.push('');
|
|
475
|
+
lines.push('#### Scenario: LayerSeparation');
|
|
476
|
+
lines.push('- **GIVEN** a request from the presentation layer');
|
|
477
|
+
lines.push('- **WHEN** business logic is needed');
|
|
478
|
+
lines.push('- **THEN** the presentation layer delegates to the business layer');
|
|
479
|
+
lines.push('- **AND** direct database access from presentation is prohibited');
|
|
480
|
+
lines.push('');
|
|
481
|
+
}
|
|
482
|
+
// Security requirement
|
|
483
|
+
if (architecture.securityModel && architecture.securityModel !== 'Unknown') {
|
|
484
|
+
lines.push('### Requirement: SecurityModel');
|
|
485
|
+
lines.push('');
|
|
486
|
+
lines.push(`The system SHALL implement security via: ${architecture.securityModel}`);
|
|
487
|
+
lines.push('');
|
|
488
|
+
lines.push('#### Scenario: AuthenticatedAccess');
|
|
489
|
+
lines.push('- **GIVEN** an unauthenticated request');
|
|
490
|
+
lines.push('- **WHEN** accessing protected resources');
|
|
491
|
+
lines.push('- **THEN** access is denied');
|
|
492
|
+
lines.push('');
|
|
493
|
+
}
|
|
494
|
+
// System Diagram (Mermaid)
|
|
495
|
+
lines.push('## System Diagram');
|
|
496
|
+
lines.push('');
|
|
497
|
+
lines.push('```mermaid');
|
|
498
|
+
lines.push('graph TB');
|
|
499
|
+
// Generate layer diagram
|
|
500
|
+
for (let i = 0; i < architecture.layerMap.length; i++) {
|
|
501
|
+
const layer = architecture.layerMap[i];
|
|
502
|
+
const layerId = layer.name.replace(/\s+/g, '');
|
|
503
|
+
lines.push(` ${layerId}[${layer.name}]`);
|
|
504
|
+
if (i < architecture.layerMap.length - 1) {
|
|
505
|
+
const nextLayerId = architecture.layerMap[i + 1].name.replace(/\s+/g, '');
|
|
506
|
+
lines.push(` ${layerId} --> ${nextLayerId}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
lines.push('```');
|
|
510
|
+
lines.push('');
|
|
511
|
+
// Layer Structure
|
|
512
|
+
lines.push('## Layer Structure');
|
|
513
|
+
lines.push('');
|
|
514
|
+
for (const layer of architecture.layerMap) {
|
|
515
|
+
lines.push(`### ${layer.name}`);
|
|
516
|
+
lines.push('');
|
|
517
|
+
lines.push(`**Purpose**: ${layer.purpose}`);
|
|
518
|
+
if (layer.components.length > 0) {
|
|
519
|
+
lines.push(`**Location**: \`${layer.components.join(', ')}\``);
|
|
520
|
+
}
|
|
521
|
+
lines.push('');
|
|
522
|
+
}
|
|
523
|
+
// Data Flow
|
|
524
|
+
lines.push('## Data Flow');
|
|
525
|
+
lines.push('');
|
|
526
|
+
if (architecture.dataFlow && architecture.dataFlow !== 'Unknown') {
|
|
527
|
+
lines.push(this.wrapText(architecture.dataFlow));
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
lines.push('Data flows through the defined layers in sequence.');
|
|
531
|
+
}
|
|
532
|
+
lines.push('');
|
|
533
|
+
// External Integrations
|
|
534
|
+
if (architecture.integrations.length > 0) {
|
|
535
|
+
lines.push('## External Integrations');
|
|
536
|
+
lines.push('');
|
|
537
|
+
lines.push('| System | Purpose |');
|
|
538
|
+
lines.push('|--------|---------|');
|
|
539
|
+
for (const integration of architecture.integrations) {
|
|
540
|
+
const name = typeof integration === 'string' ? integration : integration.name ?? String(integration);
|
|
541
|
+
const purpose = typeof integration === 'object' && integration !== null
|
|
542
|
+
? (integration.purpose ?? 'External integration')
|
|
543
|
+
: 'External integration';
|
|
544
|
+
lines.push(`| ${name} | ${purpose} |`);
|
|
545
|
+
}
|
|
546
|
+
lines.push('');
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
path: 'openspec/specs/architecture/spec.md',
|
|
550
|
+
content: lines.join('\n'),
|
|
551
|
+
domain: 'architecture',
|
|
552
|
+
type: 'architecture',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Generate the API spec
|
|
557
|
+
*/
|
|
558
|
+
generateApiSpec(endpoints, _survey) {
|
|
559
|
+
const lines = [];
|
|
560
|
+
const now = new Date();
|
|
561
|
+
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')}`;
|
|
562
|
+
// Header
|
|
563
|
+
lines.push('# API Specification');
|
|
564
|
+
lines.push('');
|
|
565
|
+
lines.push(`> Generated by spec-gen v${this.options.version} on ${date}`);
|
|
566
|
+
lines.push('');
|
|
567
|
+
// Purpose
|
|
568
|
+
lines.push('## Purpose');
|
|
569
|
+
lines.push('');
|
|
570
|
+
lines.push('This document specifies the HTTP API exposed by the system.');
|
|
571
|
+
lines.push('');
|
|
572
|
+
// Requirements section (always present)
|
|
573
|
+
lines.push('## Requirements');
|
|
574
|
+
lines.push('');
|
|
575
|
+
// Authentication requirement
|
|
576
|
+
const authMethods = new Set(endpoints.map(e => e.authentication).filter(Boolean));
|
|
577
|
+
if (authMethods.size > 0) {
|
|
578
|
+
lines.push('### Requirement: APIAuthentication');
|
|
579
|
+
lines.push('');
|
|
580
|
+
lines.push(`The API SHALL require authentication via: ${Array.from(authMethods).join(', ')}`);
|
|
581
|
+
lines.push('');
|
|
582
|
+
lines.push('#### Scenario: AuthenticatedRequest');
|
|
583
|
+
lines.push('- **GIVEN** a request with valid authentication credentials');
|
|
584
|
+
lines.push('- **WHEN** the request is processed');
|
|
585
|
+
lines.push('- **THEN** the request is authenticated successfully');
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push('#### Scenario: UnauthenticatedRequest');
|
|
588
|
+
lines.push('- **GIVEN** a request without authentication');
|
|
589
|
+
lines.push('- **WHEN** accessing a protected endpoint');
|
|
590
|
+
lines.push('- **THEN** the response status is 401 Unauthorized');
|
|
591
|
+
lines.push('');
|
|
592
|
+
}
|
|
593
|
+
// Group endpoints by related entity
|
|
594
|
+
const endpointsByResource = new Map();
|
|
595
|
+
for (const endpoint of endpoints) {
|
|
596
|
+
const resource = endpoint.relatedEntity || 'General';
|
|
597
|
+
const existing = endpointsByResource.get(resource) || [];
|
|
598
|
+
existing.push(endpoint);
|
|
599
|
+
endpointsByResource.set(resource, existing);
|
|
600
|
+
}
|
|
601
|
+
// Endpoint requirements (under ## Requirements, no separate ## Endpoints section)
|
|
602
|
+
for (const [resource, resourceEndpoints] of endpointsByResource) {
|
|
603
|
+
for (const endpoint of resourceEndpoints) {
|
|
604
|
+
const reqName = this.formatRequirementName(`${endpoint.method}${resource}`);
|
|
605
|
+
lines.push(`### Requirement: ${reqName}`);
|
|
606
|
+
lines.push('');
|
|
607
|
+
lines.push(`The API SHALL support \`${endpoint.method} ${endpoint.path}\` to ${(endpoint.purpose ?? '').toLowerCase()}`);
|
|
608
|
+
lines.push('');
|
|
609
|
+
// Request schema
|
|
610
|
+
if (endpoint.requestSchema && Object.keys(endpoint.requestSchema).length > 0) {
|
|
611
|
+
lines.push('**Request:**');
|
|
612
|
+
lines.push('');
|
|
613
|
+
lines.push('```json');
|
|
614
|
+
lines.push(JSON.stringify(endpoint.requestSchema, null, 2));
|
|
615
|
+
lines.push('```');
|
|
616
|
+
lines.push('');
|
|
617
|
+
}
|
|
618
|
+
// Response schema
|
|
619
|
+
if (endpoint.responseSchema && Object.keys(endpoint.responseSchema).length > 0) {
|
|
620
|
+
lines.push('**Response:**');
|
|
621
|
+
lines.push('');
|
|
622
|
+
lines.push('```json');
|
|
623
|
+
lines.push(JSON.stringify(endpoint.responseSchema, null, 2));
|
|
624
|
+
lines.push('```');
|
|
625
|
+
lines.push('');
|
|
626
|
+
}
|
|
627
|
+
// Scenarios
|
|
628
|
+
for (const scenario of (endpoint.scenarios ?? [])) {
|
|
629
|
+
this.addScenario(lines, scenario);
|
|
630
|
+
}
|
|
631
|
+
// Default success scenario if none provided
|
|
632
|
+
if ((endpoint.scenarios ?? []).length === 0) {
|
|
633
|
+
lines.push(`#### Scenario: ${reqName}Success`);
|
|
634
|
+
lines.push('- **GIVEN** an authenticated user');
|
|
635
|
+
lines.push(`- **WHEN** \`${endpoint.method} ${endpoint.path}\` is called with valid data`);
|
|
636
|
+
lines.push('- **THEN** the response status is 200 OK');
|
|
637
|
+
lines.push('');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
path: 'openspec/specs/api/spec.md',
|
|
643
|
+
content: lines.join('\n'),
|
|
644
|
+
domain: 'api',
|
|
645
|
+
type: 'api',
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Add a scenario to the lines array
|
|
650
|
+
*/
|
|
651
|
+
addScenario(lines, scenario) {
|
|
652
|
+
lines.push(`#### Scenario: ${this.formatRequirementName(scenario.name)}`);
|
|
653
|
+
lines.push(`- **GIVEN** ${this.wrapText(scenario.given ?? 'the system is in a valid state')}`);
|
|
654
|
+
lines.push(`- **WHEN** ${this.wrapText(scenario.when ?? 'the operation is invoked')}`);
|
|
655
|
+
lines.push(`- **THEN** ${this.wrapText(scenario.then ?? 'the expected outcome occurs')}`);
|
|
656
|
+
if (scenario.and && scenario.and.length > 0) {
|
|
657
|
+
const andClauses = Array.isArray(scenario.and) ? scenario.and : [scenario.and];
|
|
658
|
+
for (const andClause of andClauses) {
|
|
659
|
+
lines.push(`- **AND** ${this.wrapText(andClause)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
lines.push('');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Format a requirement name (PascalCase, no spaces)
|
|
666
|
+
*/
|
|
667
|
+
formatRequirementName(name) {
|
|
668
|
+
if (!name)
|
|
669
|
+
return 'Unnamed';
|
|
670
|
+
return name
|
|
671
|
+
.split(/[\s_-]+/)
|
|
672
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
673
|
+
.join('');
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Format a relationship for display
|
|
677
|
+
*/
|
|
678
|
+
formatRelationship(rel) {
|
|
679
|
+
const typeLabel = {
|
|
680
|
+
'one-to-one': 'has one',
|
|
681
|
+
'one-to-many': 'has many',
|
|
682
|
+
'many-to-many': 'has many',
|
|
683
|
+
'belongs-to': 'belongs to',
|
|
684
|
+
}[rel.type] || rel.type;
|
|
685
|
+
return `${typeLabel} ${rel.targetEntity}${rel.description ? ` (${rel.description})` : ''}`;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Format project category for display
|
|
689
|
+
*/
|
|
690
|
+
formatCategory(category) {
|
|
691
|
+
const labels = {
|
|
692
|
+
'web-frontend': 'Web Frontend Application',
|
|
693
|
+
'web-backend': 'Web Backend Service',
|
|
694
|
+
'api-service': 'API Service',
|
|
695
|
+
'cli-tool': 'Command Line Tool',
|
|
696
|
+
library: 'Library/Package',
|
|
697
|
+
'mobile-app': 'Mobile Application',
|
|
698
|
+
'desktop-app': 'Desktop Application',
|
|
699
|
+
'data-pipeline': 'Data Pipeline',
|
|
700
|
+
'ml-service': 'Machine Learning Service',
|
|
701
|
+
monorepo: 'Monorepo',
|
|
702
|
+
other: 'Other',
|
|
703
|
+
};
|
|
704
|
+
return labels[category] || category;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Format architecture pattern for display
|
|
708
|
+
*/
|
|
709
|
+
formatArchitecture(pattern) {
|
|
710
|
+
const labels = {
|
|
711
|
+
layered: 'Layered Architecture',
|
|
712
|
+
hexagonal: 'Hexagonal Architecture (Ports & Adapters)',
|
|
713
|
+
microservices: 'Microservices',
|
|
714
|
+
monolith: 'Monolithic',
|
|
715
|
+
serverless: 'Serverless',
|
|
716
|
+
'event-driven': 'Event-Driven Architecture',
|
|
717
|
+
mvc: 'Model-View-Controller (MVC)',
|
|
718
|
+
other: 'Custom Architecture',
|
|
719
|
+
};
|
|
720
|
+
return labels[pattern] || pattern;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Capitalize first letter
|
|
724
|
+
*/
|
|
725
|
+
capitalize(str) {
|
|
726
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Wrap text at max line width
|
|
730
|
+
*/
|
|
731
|
+
wrapText(text) {
|
|
732
|
+
if (!text)
|
|
733
|
+
return '';
|
|
734
|
+
const str = typeof text === 'string' ? text : JSON.stringify(text);
|
|
735
|
+
const words = str.split(/\s+/);
|
|
736
|
+
const lines = [];
|
|
737
|
+
let currentLine = '';
|
|
738
|
+
for (const word of words) {
|
|
739
|
+
if (currentLine.length + word.length + 1 > this.options.maxLineWidth) {
|
|
740
|
+
lines.push(currentLine);
|
|
741
|
+
currentLine = word;
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (currentLine) {
|
|
748
|
+
lines.push(currentLine);
|
|
749
|
+
}
|
|
750
|
+
return lines.join('\n');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Validate a generated spec against OpenSpec conventions
|
|
755
|
+
*/
|
|
756
|
+
export function validateSpec(content) {
|
|
757
|
+
const errors = [];
|
|
758
|
+
const warnings = [];
|
|
759
|
+
// Check for title
|
|
760
|
+
if (!content.match(/^#\s+.+/m)) {
|
|
761
|
+
errors.push('Missing title (# heading)');
|
|
762
|
+
}
|
|
763
|
+
// Check for Purpose section
|
|
764
|
+
if (!content.includes('## Purpose')) {
|
|
765
|
+
warnings.push('Missing Purpose section');
|
|
766
|
+
}
|
|
767
|
+
// Check for Requirements section (except overview)
|
|
768
|
+
if (!content.includes('## Requirements') && !content.includes('## Domains')) {
|
|
769
|
+
warnings.push('Missing Requirements section');
|
|
770
|
+
}
|
|
771
|
+
// Check requirement format (RFC 2119 keywords)
|
|
772
|
+
const requirements = content.match(/###\s+Requirement:\s+.+/g) || [];
|
|
773
|
+
for (const req of requirements) {
|
|
774
|
+
const reqSection = content.substring(content.indexOf(req));
|
|
775
|
+
const nextSection = reqSection.indexOf('\n### ');
|
|
776
|
+
const reqContent = nextSection > 0 ? reqSection.substring(0, nextSection) : reqSection;
|
|
777
|
+
if (!reqContent.match(/\b(SHALL|MUST|SHOULD|MAY)\b/)) {
|
|
778
|
+
warnings.push(`Requirement missing RFC 2119 keyword: ${req}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// Check scenario format
|
|
782
|
+
const scenarios = content.match(/####\s+Scenario:\s+.+/g) || [];
|
|
783
|
+
for (const scenario of scenarios) {
|
|
784
|
+
const scenarioSection = content.substring(content.indexOf(scenario));
|
|
785
|
+
const nextScenario = scenarioSection.indexOf('\n#### ');
|
|
786
|
+
const scenarioContent = nextScenario > 0 ? scenarioSection.substring(0, nextScenario) : scenarioSection;
|
|
787
|
+
if (!scenarioContent.includes('**GIVEN**')) {
|
|
788
|
+
errors.push(`Scenario missing GIVEN: ${scenario}`);
|
|
789
|
+
}
|
|
790
|
+
if (!scenarioContent.includes('**WHEN**')) {
|
|
791
|
+
errors.push(`Scenario missing WHEN: ${scenario}`);
|
|
792
|
+
}
|
|
793
|
+
if (!scenarioContent.includes('**THEN**')) {
|
|
794
|
+
errors.push(`Scenario missing THEN: ${scenario}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Check for delta markers (should not be in generated specs)
|
|
798
|
+
if (content.match(/\[ADDED\]|\[MODIFIED\]|\[REMOVED\]/)) {
|
|
799
|
+
errors.push('Generated specs should not contain delta markers');
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
valid: errors.length === 0,
|
|
803
|
+
errors,
|
|
804
|
+
warnings,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
// ============================================================================
|
|
808
|
+
// CONVENIENCE FUNCTIONS
|
|
809
|
+
// ============================================================================
|
|
810
|
+
/**
|
|
811
|
+
* Generate OpenSpec files from pipeline result
|
|
812
|
+
*/
|
|
813
|
+
export function generateOpenSpecs(result, options) {
|
|
814
|
+
const generator = new OpenSpecFormatGenerator(options);
|
|
815
|
+
return generator.generateSpecs(result);
|
|
816
|
+
}
|
|
817
|
+
//# sourceMappingURL=openspec-format-generator.js.map
|