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,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Verification Engine
|
|
3
|
+
*
|
|
4
|
+
* Tests whether generated specs accurately describe the codebase by using
|
|
5
|
+
* the specs to predict code behavior and comparing against actual files.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile, mkdir, access, readdir } from 'node:fs/promises';
|
|
8
|
+
import { join, basename, relative } from 'node:path';
|
|
9
|
+
import logger from '../../utils/logger.js';
|
|
10
|
+
import { ImportExportParser } from '../analyzer/import-parser.js';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// SYSTEM PROMPTS
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const PREDICTION_SYSTEM_PROMPT = `You are testing the accuracy of OpenSpec specifications.
|
|
15
|
+
|
|
16
|
+
You will be given:
|
|
17
|
+
1. A set of specifications describing a software system (in OpenSpec format)
|
|
18
|
+
2. A file path within that system
|
|
19
|
+
|
|
20
|
+
Your task: Based ONLY on the specifications, predict:
|
|
21
|
+
- What this file likely does (purpose)
|
|
22
|
+
- What modules/files it probably imports
|
|
23
|
+
- What it probably exports (functions, classes, etc.)
|
|
24
|
+
- Key logic patterns you'd expect to see based on the spec requirements
|
|
25
|
+
|
|
26
|
+
Be specific. If the specs don't provide enough info, say so.
|
|
27
|
+
|
|
28
|
+
Respond with valid JSON only.`;
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// VERIFICATION ENGINE
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Spec Verification Engine
|
|
34
|
+
*/
|
|
35
|
+
export class SpecVerificationEngine {
|
|
36
|
+
llm;
|
|
37
|
+
options;
|
|
38
|
+
specs = [];
|
|
39
|
+
parser;
|
|
40
|
+
constructor(llm, options) {
|
|
41
|
+
this.llm = llm;
|
|
42
|
+
this.parser = new ImportExportParser();
|
|
43
|
+
this.options = {
|
|
44
|
+
rootPath: options.rootPath,
|
|
45
|
+
openspecPath: options.openspecPath,
|
|
46
|
+
outputDir: options.outputDir,
|
|
47
|
+
minComplexity: options.minComplexity ?? 50,
|
|
48
|
+
maxComplexity: options.maxComplexity ?? 500,
|
|
49
|
+
filesPerDomain: options.filesPerDomain ?? 3,
|
|
50
|
+
passThreshold: options.passThreshold ?? 0.6,
|
|
51
|
+
generationContext: options.generationContext ?? [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run full verification
|
|
56
|
+
*/
|
|
57
|
+
async verify(depGraph, specVersion) {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
// Load all specs
|
|
60
|
+
await this.loadSpecs();
|
|
61
|
+
if (this.specs.length === 0) {
|
|
62
|
+
throw new Error('No specs found to verify against');
|
|
63
|
+
}
|
|
64
|
+
logger.analysis(`Loaded ${this.specs.length} spec(s) for verification`);
|
|
65
|
+
// Select verification candidates
|
|
66
|
+
const candidates = this.selectCandidates(depGraph);
|
|
67
|
+
logger.discovery(`Selected ${candidates.length} candidate file(s) for verification`);
|
|
68
|
+
if (candidates.length === 0) {
|
|
69
|
+
throw new Error('No suitable verification candidates found');
|
|
70
|
+
}
|
|
71
|
+
// Run verification for each candidate
|
|
72
|
+
const results = [];
|
|
73
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
74
|
+
const candidate = candidates[i];
|
|
75
|
+
logger.analysis(`Verifying ${i + 1}/${candidates.length}: ${candidate.path}`);
|
|
76
|
+
try {
|
|
77
|
+
const result = await this.verifyFile(candidate);
|
|
78
|
+
results.push(result);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.warning(`Failed to verify ${candidate.path}: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Generate report
|
|
85
|
+
const report = this.generateReport(results, specVersion);
|
|
86
|
+
// Save report
|
|
87
|
+
await this.saveReport(report);
|
|
88
|
+
const duration = Date.now() - startTime;
|
|
89
|
+
logger.success(`Verification complete in ${(duration / 1000).toFixed(1)}s`);
|
|
90
|
+
return report;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Load all specs from openspec directory
|
|
94
|
+
*/
|
|
95
|
+
async loadSpecs() {
|
|
96
|
+
this.specs = [];
|
|
97
|
+
const specsDir = join(this.options.openspecPath, 'specs');
|
|
98
|
+
try {
|
|
99
|
+
await access(specsDir);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const entries = await readdir(specsDir, { withFileTypes: true });
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory())
|
|
107
|
+
continue;
|
|
108
|
+
const specPath = join(specsDir, entry.name, 'spec.md');
|
|
109
|
+
try {
|
|
110
|
+
const content = await readFile(specPath, 'utf-8');
|
|
111
|
+
this.specs.push({
|
|
112
|
+
domain: entry.name,
|
|
113
|
+
path: relative(this.options.rootPath, specPath),
|
|
114
|
+
content,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Spec doesn't exist for this domain
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Select verification candidate files
|
|
124
|
+
*/
|
|
125
|
+
selectCandidates(depGraph) {
|
|
126
|
+
const candidates = [];
|
|
127
|
+
const usedPaths = new Set(this.options.generationContext);
|
|
128
|
+
// Group files by domain
|
|
129
|
+
const filesByDomain = new Map();
|
|
130
|
+
for (const node of depGraph.nodes) {
|
|
131
|
+
// Skip files used in generation
|
|
132
|
+
if (usedPaths.has(node.file.path))
|
|
133
|
+
continue;
|
|
134
|
+
// Skip test files
|
|
135
|
+
if (node.file.isTest)
|
|
136
|
+
continue;
|
|
137
|
+
// Skip generated files
|
|
138
|
+
if (node.file.isGenerated)
|
|
139
|
+
continue;
|
|
140
|
+
// Skip files outside complexity range
|
|
141
|
+
if (node.file.lines < this.options.minComplexity)
|
|
142
|
+
continue;
|
|
143
|
+
if (node.file.lines > this.options.maxComplexity)
|
|
144
|
+
continue;
|
|
145
|
+
// Determine domain from path
|
|
146
|
+
const domain = this.inferDomain(node.file.path);
|
|
147
|
+
if (!filesByDomain.has(domain)) {
|
|
148
|
+
filesByDomain.set(domain, []);
|
|
149
|
+
}
|
|
150
|
+
filesByDomain.get(domain).push(node);
|
|
151
|
+
}
|
|
152
|
+
// Select files from each domain
|
|
153
|
+
for (const [domain, nodes] of filesByDomain) {
|
|
154
|
+
// Prefer leaf nodes (low connectivity)
|
|
155
|
+
const sorted = nodes.sort((a, b) => {
|
|
156
|
+
const aConnectivity = a.metrics.inDegree + a.metrics.outDegree;
|
|
157
|
+
const bConnectivity = b.metrics.inDegree + b.metrics.outDegree;
|
|
158
|
+
return aConnectivity - bConnectivity;
|
|
159
|
+
});
|
|
160
|
+
// Take up to filesPerDomain
|
|
161
|
+
const selected = sorted.slice(0, this.options.filesPerDomain);
|
|
162
|
+
for (const node of selected) {
|
|
163
|
+
candidates.push({
|
|
164
|
+
path: node.file.path,
|
|
165
|
+
absolutePath: node.file.absolutePath,
|
|
166
|
+
domain,
|
|
167
|
+
usedInGeneration: false,
|
|
168
|
+
complexity: node.file.lines,
|
|
169
|
+
lines: node.file.lines,
|
|
170
|
+
imports: node.metrics.outDegree,
|
|
171
|
+
exports: node.exports.length,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return candidates;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Infer domain from file path
|
|
179
|
+
*/
|
|
180
|
+
inferDomain(filePath) {
|
|
181
|
+
const parts = filePath.split('/');
|
|
182
|
+
// Look for known domain indicators
|
|
183
|
+
for (const part of parts) {
|
|
184
|
+
const lower = part.toLowerCase();
|
|
185
|
+
// Skip common non-domain directories
|
|
186
|
+
if (['src', 'lib', 'app', 'core', 'utils', 'helpers', 'common', 'shared'].includes(lower)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Return first meaningful directory
|
|
190
|
+
if (part.length > 1 && !part.startsWith('.')) {
|
|
191
|
+
return lower;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return 'misc';
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Verify a single file
|
|
198
|
+
*/
|
|
199
|
+
async verifyFile(candidate) {
|
|
200
|
+
// Get prediction from LLM
|
|
201
|
+
const prediction = await this.getPrediction(candidate);
|
|
202
|
+
// Analyze actual file
|
|
203
|
+
const fileContent = await readFile(candidate.absolutePath, 'utf-8');
|
|
204
|
+
const fileAnalysis = await this.parser.parseFile(candidate.absolutePath);
|
|
205
|
+
// Compare prediction to actual
|
|
206
|
+
const purposeMatch = this.comparePurpose(prediction.predictedPurpose, fileContent);
|
|
207
|
+
const importMatch = this.compareImports(prediction.predictedImports, fileAnalysis.imports.map(i => i.source));
|
|
208
|
+
const exportMatch = this.compareExports(prediction.predictedExports, fileAnalysis.exports.map(e => e.name));
|
|
209
|
+
const requirementCoverage = this.analyzeRequirementCoverage(prediction.relatedRequirements, fileContent);
|
|
210
|
+
// Calculate overall score
|
|
211
|
+
const overallScore = this.calculateOverallScore(purposeMatch, importMatch, exportMatch, requirementCoverage);
|
|
212
|
+
// Generate feedback
|
|
213
|
+
const feedback = this.generateFeedback(candidate, prediction, purposeMatch, importMatch, exportMatch, requirementCoverage);
|
|
214
|
+
return {
|
|
215
|
+
filePath: candidate.path,
|
|
216
|
+
domain: candidate.domain,
|
|
217
|
+
purposeMatch,
|
|
218
|
+
importMatch,
|
|
219
|
+
exportMatch,
|
|
220
|
+
requirementCoverage,
|
|
221
|
+
overallScore,
|
|
222
|
+
llmConfidence: prediction.confidence,
|
|
223
|
+
feedback,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get prediction from LLM
|
|
228
|
+
*/
|
|
229
|
+
async getPrediction(candidate) {
|
|
230
|
+
// Build specs context
|
|
231
|
+
const specsContent = this.specs
|
|
232
|
+
.map(s => `=== ${s.domain} (${s.path}) ===\n${s.content}`)
|
|
233
|
+
.join('\n\n');
|
|
234
|
+
const userPrompt = `Here are the specifications:
|
|
235
|
+
|
|
236
|
+
${specsContent}
|
|
237
|
+
|
|
238
|
+
Predict the contents of: ${candidate.path}
|
|
239
|
+
|
|
240
|
+
Respond in JSON:
|
|
241
|
+
{
|
|
242
|
+
"predictedPurpose": "...",
|
|
243
|
+
"predictedImports": ["...", "..."],
|
|
244
|
+
"predictedExports": ["...", "..."],
|
|
245
|
+
"predictedLogic": ["...", "..."],
|
|
246
|
+
"relatedRequirements": ["RequirementName1", "RequirementName2"],
|
|
247
|
+
"confidence": 0.0-1.0,
|
|
248
|
+
"reasoning": "..."
|
|
249
|
+
}`;
|
|
250
|
+
try {
|
|
251
|
+
const prediction = await this.llm.completeJSON({
|
|
252
|
+
systemPrompt: PREDICTION_SYSTEM_PROMPT,
|
|
253
|
+
userPrompt,
|
|
254
|
+
temperature: 0.3,
|
|
255
|
+
maxTokens: 1000,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
predictedPurpose: prediction.predictedPurpose ?? '',
|
|
259
|
+
predictedImports: prediction.predictedImports ?? [],
|
|
260
|
+
predictedExports: prediction.predictedExports ?? [],
|
|
261
|
+
predictedLogic: prediction.predictedLogic ?? [],
|
|
262
|
+
relatedRequirements: prediction.relatedRequirements ?? [],
|
|
263
|
+
confidence: prediction.confidence ?? 0.5,
|
|
264
|
+
reasoning: prediction.reasoning ?? '',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
logger.warning(`Prediction failed for ${candidate.path}: ${error.message}`);
|
|
269
|
+
return {
|
|
270
|
+
predictedPurpose: '',
|
|
271
|
+
predictedImports: [],
|
|
272
|
+
predictedExports: [],
|
|
273
|
+
predictedLogic: [],
|
|
274
|
+
relatedRequirements: [],
|
|
275
|
+
confidence: 0,
|
|
276
|
+
reasoning: 'Prediction failed',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Compare predicted purpose to actual file content
|
|
282
|
+
*/
|
|
283
|
+
comparePurpose(predicted, fileContent) {
|
|
284
|
+
// Extract actual purpose from file comments
|
|
285
|
+
const actual = this.extractPurpose(fileContent);
|
|
286
|
+
// Calculate similarity using keyword overlap
|
|
287
|
+
const similarity = this.calculateSimilarity(predicted, actual);
|
|
288
|
+
return { predicted, actual, similarity };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Extract purpose from file content (comments, docstrings)
|
|
292
|
+
*/
|
|
293
|
+
extractPurpose(content) {
|
|
294
|
+
const lines = content.split('\n');
|
|
295
|
+
const purposeLines = [];
|
|
296
|
+
// Look for JSDoc/TSDoc comment at top of file
|
|
297
|
+
let inBlockComment = false;
|
|
298
|
+
for (const line of lines.slice(0, 30)) {
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (trimmed.startsWith('/**')) {
|
|
301
|
+
inBlockComment = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (trimmed.startsWith('*/') || trimmed.endsWith('*/')) {
|
|
305
|
+
inBlockComment = false;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (inBlockComment) {
|
|
309
|
+
const comment = trimmed.replace(/^\*\s*/, '').trim();
|
|
310
|
+
if (comment && !comment.startsWith('@')) {
|
|
311
|
+
purposeLines.push(comment);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Single line comments at top
|
|
315
|
+
if (trimmed.startsWith('//') && !inBlockComment && purposeLines.length < 3) {
|
|
316
|
+
purposeLines.push(trimmed.replace(/^\/\/\s*/, ''));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return purposeLines.join(' ').slice(0, 500);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Calculate text similarity using keyword overlap
|
|
323
|
+
*/
|
|
324
|
+
calculateSimilarity(text1, text2) {
|
|
325
|
+
if (!text1 || !text2)
|
|
326
|
+
return 0;
|
|
327
|
+
const words1 = this.extractKeywords(text1);
|
|
328
|
+
const words2 = this.extractKeywords(text2);
|
|
329
|
+
if (words1.size === 0 || words2.size === 0)
|
|
330
|
+
return 0;
|
|
331
|
+
let matches = 0;
|
|
332
|
+
for (const word of words1) {
|
|
333
|
+
if (words2.has(word))
|
|
334
|
+
matches++;
|
|
335
|
+
}
|
|
336
|
+
// Jaccard similarity
|
|
337
|
+
const union = new Set([...words1, ...words2]);
|
|
338
|
+
return matches / union.size;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Extract keywords from text
|
|
342
|
+
*/
|
|
343
|
+
extractKeywords(text) {
|
|
344
|
+
const words = text
|
|
345
|
+
.toLowerCase()
|
|
346
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
347
|
+
.split(/\s+/)
|
|
348
|
+
.filter(w => w.length > 2);
|
|
349
|
+
// Filter out common words
|
|
350
|
+
const stopwords = new Set(['the', 'and', 'for', 'this', 'that', 'with', 'are', 'from', 'has', 'have', 'will', 'can', 'all', 'each', 'which', 'when', 'there', 'been', 'being', 'their', 'would', 'could', 'should']);
|
|
351
|
+
return new Set(words.filter(w => !stopwords.has(w)));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Compare predicted imports to actual
|
|
355
|
+
*/
|
|
356
|
+
compareImports(predicted, actual) {
|
|
357
|
+
return this.calculateSetMatch(predicted.map(p => this.normalizeImport(p)), actual.map(a => this.normalizeImport(a)));
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Normalize import path for comparison
|
|
361
|
+
*/
|
|
362
|
+
normalizeImport(importPath) {
|
|
363
|
+
// Remove extensions
|
|
364
|
+
const normalized = importPath
|
|
365
|
+
.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '')
|
|
366
|
+
.replace(/^\.\//, '')
|
|
367
|
+
.replace(/^\.\.\//, '');
|
|
368
|
+
// Extract module name from path
|
|
369
|
+
const parts = normalized.split('/');
|
|
370
|
+
return parts[parts.length - 1].toLowerCase();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Compare predicted exports to actual
|
|
374
|
+
*/
|
|
375
|
+
compareExports(predicted, actual) {
|
|
376
|
+
return this.calculateSetMatch(predicted.map(p => p.toLowerCase()), actual.map(a => a.toLowerCase()));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Calculate precision, recall, F1 for set comparison
|
|
380
|
+
*/
|
|
381
|
+
calculateSetMatch(predicted, actual) {
|
|
382
|
+
const predictedSet = new Set(predicted);
|
|
383
|
+
const actualSet = new Set(actual);
|
|
384
|
+
let truePositives = 0;
|
|
385
|
+
for (const p of predictedSet) {
|
|
386
|
+
if (actualSet.has(p))
|
|
387
|
+
truePositives++;
|
|
388
|
+
}
|
|
389
|
+
const precision = predictedSet.size > 0 ? truePositives / predictedSet.size : 0;
|
|
390
|
+
const recall = actualSet.size > 0 ? truePositives / actualSet.size : 0;
|
|
391
|
+
const f1Score = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
|
|
392
|
+
return {
|
|
393
|
+
predicted,
|
|
394
|
+
actual,
|
|
395
|
+
precision,
|
|
396
|
+
recall,
|
|
397
|
+
f1Score,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Analyze requirement coverage
|
|
402
|
+
*/
|
|
403
|
+
analyzeRequirementCoverage(relatedRequirements, fileContent) {
|
|
404
|
+
const actuallyImplements = [];
|
|
405
|
+
const contentLower = fileContent.toLowerCase();
|
|
406
|
+
for (const req of relatedRequirements) {
|
|
407
|
+
// Check if requirement keywords appear in the file
|
|
408
|
+
const reqWords = req.toLowerCase().split(/[\s-_]+/);
|
|
409
|
+
const matches = reqWords.filter(w => w.length > 3 && contentLower.includes(w));
|
|
410
|
+
if (matches.length >= Math.min(2, reqWords.length)) {
|
|
411
|
+
actuallyImplements.push(req);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const coverage = relatedRequirements.length > 0
|
|
415
|
+
? actuallyImplements.length / relatedRequirements.length
|
|
416
|
+
: 0;
|
|
417
|
+
return {
|
|
418
|
+
relatedRequirements,
|
|
419
|
+
actuallyImplements,
|
|
420
|
+
coverage,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Calculate overall score (weighted combination)
|
|
425
|
+
*/
|
|
426
|
+
calculateOverallScore(purposeMatch, importMatch, exportMatch, requirementCoverage) {
|
|
427
|
+
// Weights from spec:
|
|
428
|
+
// - Purpose: 25%
|
|
429
|
+
// - Imports: 30%
|
|
430
|
+
// - Exports: 30%
|
|
431
|
+
// - Requirements: 15%
|
|
432
|
+
return (purposeMatch.similarity * 0.25 +
|
|
433
|
+
importMatch.f1Score * 0.30 +
|
|
434
|
+
exportMatch.f1Score * 0.30 +
|
|
435
|
+
requirementCoverage.coverage * 0.15);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Generate feedback for gaps
|
|
439
|
+
*/
|
|
440
|
+
generateFeedback(candidate, prediction, purposeMatch, importMatch, exportMatch, requirementCoverage) {
|
|
441
|
+
const feedback = [];
|
|
442
|
+
// Low purpose match
|
|
443
|
+
if (purposeMatch.similarity < 0.3) {
|
|
444
|
+
feedback.push(`Purpose mismatch: specs don't clearly describe what ${basename(candidate.path)} does`);
|
|
445
|
+
}
|
|
446
|
+
// Missing imports
|
|
447
|
+
const missingImports = importMatch.actual.filter(a => !importMatch.predicted.includes(a));
|
|
448
|
+
if (missingImports.length > 0) {
|
|
449
|
+
feedback.push(`Missing dependencies: specs don't mention ${missingImports.slice(0, 3).join(', ')}`);
|
|
450
|
+
}
|
|
451
|
+
// Missing exports
|
|
452
|
+
const missingExports = exportMatch.actual.filter(a => !exportMatch.predicted.includes(a));
|
|
453
|
+
if (missingExports.length > 0) {
|
|
454
|
+
feedback.push(`Undocumented exports: ${missingExports.slice(0, 3).join(', ')} not described in specs`);
|
|
455
|
+
}
|
|
456
|
+
// Low requirement coverage
|
|
457
|
+
if (requirementCoverage.coverage < 0.5 && prediction.relatedRequirements.length > 0) {
|
|
458
|
+
const missing = prediction.relatedRequirements.filter(r => !requirementCoverage.actuallyImplements.includes(r));
|
|
459
|
+
if (missing.length > 0) {
|
|
460
|
+
feedback.push(`Requirements ${missing.slice(0, 2).join(', ')} don't appear to be implemented in this file`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Low confidence from LLM
|
|
464
|
+
if (prediction.confidence < 0.5) {
|
|
465
|
+
feedback.push(`LLM had low confidence: "${prediction.reasoning}"`);
|
|
466
|
+
}
|
|
467
|
+
return feedback;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Generate verification report
|
|
471
|
+
*/
|
|
472
|
+
generateReport(results, specVersion) {
|
|
473
|
+
const passedFiles = results.filter(r => r.overallScore >= this.options.passThreshold).length;
|
|
474
|
+
const overallConfidence = results.length > 0
|
|
475
|
+
? results.reduce((sum, r) => sum + r.overallScore, 0) / results.length
|
|
476
|
+
: 0;
|
|
477
|
+
// Domain breakdown
|
|
478
|
+
const domainResults = new Map();
|
|
479
|
+
for (const result of results) {
|
|
480
|
+
if (!domainResults.has(result.domain)) {
|
|
481
|
+
domainResults.set(result.domain, []);
|
|
482
|
+
}
|
|
483
|
+
domainResults.get(result.domain).push(result);
|
|
484
|
+
}
|
|
485
|
+
const domainBreakdown = [];
|
|
486
|
+
for (const [domain, domainRes] of domainResults) {
|
|
487
|
+
const avgScore = domainRes.reduce((sum, r) => sum + r.overallScore, 0) / domainRes.length;
|
|
488
|
+
// Find weakest area
|
|
489
|
+
const avgPurpose = domainRes.reduce((sum, r) => sum + r.purposeMatch.similarity, 0) / domainRes.length;
|
|
490
|
+
const avgImport = domainRes.reduce((sum, r) => sum + r.importMatch.f1Score, 0) / domainRes.length;
|
|
491
|
+
const avgExport = domainRes.reduce((sum, r) => sum + r.exportMatch.f1Score, 0) / domainRes.length;
|
|
492
|
+
const avgReq = domainRes.reduce((sum, r) => sum + r.requirementCoverage.coverage, 0) / domainRes.length;
|
|
493
|
+
const areas = [
|
|
494
|
+
{ name: 'purpose', score: avgPurpose },
|
|
495
|
+
{ name: 'imports', score: avgImport },
|
|
496
|
+
{ name: 'exports', score: avgExport },
|
|
497
|
+
{ name: 'requirements', score: avgReq },
|
|
498
|
+
];
|
|
499
|
+
const weakest = areas.sort((a, b) => a.score - b.score)[0];
|
|
500
|
+
domainBreakdown.push({
|
|
501
|
+
domain,
|
|
502
|
+
specPath: `openspec/specs/${domain}/spec.md`,
|
|
503
|
+
filesVerified: domainRes.length,
|
|
504
|
+
averageScore: avgScore,
|
|
505
|
+
weakestArea: weakest.name,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// Common gaps
|
|
509
|
+
const allFeedback = results.flatMap(r => r.feedback);
|
|
510
|
+
const feedbackCounts = new Map();
|
|
511
|
+
for (const fb of allFeedback) {
|
|
512
|
+
// Normalize feedback for grouping
|
|
513
|
+
const key = fb.split(':')[0];
|
|
514
|
+
feedbackCounts.set(key, (feedbackCounts.get(key) ?? 0) + 1);
|
|
515
|
+
}
|
|
516
|
+
const commonGaps = Array.from(feedbackCounts.entries())
|
|
517
|
+
.filter(([_, count]) => count >= 2)
|
|
518
|
+
.sort((a, b) => b[1] - a[1])
|
|
519
|
+
.slice(0, 5)
|
|
520
|
+
.map(([gap, _]) => gap);
|
|
521
|
+
// Suggested improvements
|
|
522
|
+
const suggestedImprovements = [];
|
|
523
|
+
for (const breakdown of domainBreakdown) {
|
|
524
|
+
if (breakdown.averageScore < 0.7) {
|
|
525
|
+
suggestedImprovements.push({
|
|
526
|
+
domain: breakdown.domain,
|
|
527
|
+
issue: `Low verification score (${(breakdown.averageScore * 100).toFixed(0)}%)`,
|
|
528
|
+
suggestion: `Review and enhance ${breakdown.specPath}, especially ${breakdown.weakestArea} descriptions`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Recommendation
|
|
533
|
+
let recommendation;
|
|
534
|
+
if (overallConfidence >= 0.75) {
|
|
535
|
+
recommendation = 'ready';
|
|
536
|
+
}
|
|
537
|
+
else if (overallConfidence >= 0.5) {
|
|
538
|
+
recommendation = 'needs-review';
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
recommendation = 'regenerate';
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
timestamp: new Date().toISOString(),
|
|
545
|
+
specVersion,
|
|
546
|
+
sampledFiles: results.length,
|
|
547
|
+
passedFiles,
|
|
548
|
+
overallConfidence,
|
|
549
|
+
domainBreakdown,
|
|
550
|
+
commonGaps,
|
|
551
|
+
recommendation,
|
|
552
|
+
suggestedImprovements,
|
|
553
|
+
results,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Save verification report
|
|
558
|
+
*/
|
|
559
|
+
async saveReport(report) {
|
|
560
|
+
await mkdir(this.options.outputDir, { recursive: true });
|
|
561
|
+
// Save JSON report
|
|
562
|
+
const jsonPath = join(this.options.outputDir, 'report.json');
|
|
563
|
+
await writeFile(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
564
|
+
logger.discovery(`Saved JSON report to ${relative(this.options.rootPath, jsonPath)}`);
|
|
565
|
+
// Save Markdown report
|
|
566
|
+
const mdPath = join(this.options.outputDir, 'REPORT.md');
|
|
567
|
+
const markdown = this.generateMarkdownReport(report);
|
|
568
|
+
await writeFile(mdPath, markdown, 'utf-8');
|
|
569
|
+
logger.discovery(`Saved Markdown report to ${relative(this.options.rootPath, mdPath)}`);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Generate markdown report
|
|
573
|
+
*/
|
|
574
|
+
generateMarkdownReport(report) {
|
|
575
|
+
const lines = [];
|
|
576
|
+
lines.push('# Spec Verification Report');
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push(`Generated: ${report.timestamp}`);
|
|
579
|
+
lines.push(`Spec Version: ${report.specVersion}`);
|
|
580
|
+
lines.push('');
|
|
581
|
+
// Summary
|
|
582
|
+
lines.push('## Summary');
|
|
583
|
+
lines.push('');
|
|
584
|
+
lines.push(`| Metric | Value |`);
|
|
585
|
+
lines.push(`|--------|-------|`);
|
|
586
|
+
lines.push(`| Files Verified | ${report.sampledFiles} |`);
|
|
587
|
+
lines.push(`| Files Passed | ${report.passedFiles} (${((report.passedFiles / report.sampledFiles) * 100).toFixed(0)}%) |`);
|
|
588
|
+
lines.push(`| Overall Confidence | ${(report.overallConfidence * 100).toFixed(1)}% |`);
|
|
589
|
+
lines.push(`| Recommendation | **${report.recommendation}** |`);
|
|
590
|
+
lines.push('');
|
|
591
|
+
// Recommendation explanation
|
|
592
|
+
lines.push('### Recommendation');
|
|
593
|
+
if (report.recommendation === 'ready') {
|
|
594
|
+
lines.push('✅ Specs accurately describe the codebase and are ready for use.');
|
|
595
|
+
}
|
|
596
|
+
else if (report.recommendation === 'needs-review') {
|
|
597
|
+
lines.push('⚠️ Specs need review. Some gaps were identified that should be addressed.');
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
lines.push('❌ Specs have significant gaps. Consider regenerating with improved context.');
|
|
601
|
+
}
|
|
602
|
+
lines.push('');
|
|
603
|
+
// Domain breakdown
|
|
604
|
+
lines.push('## Domain Breakdown');
|
|
605
|
+
lines.push('');
|
|
606
|
+
lines.push('| Domain | Spec Path | Files | Avg Score | Weakest Area |');
|
|
607
|
+
lines.push('|--------|-----------|-------|-----------|--------------|');
|
|
608
|
+
for (const domain of report.domainBreakdown) {
|
|
609
|
+
const scorePercent = (domain.averageScore * 100).toFixed(0);
|
|
610
|
+
lines.push(`| ${domain.domain} | ${domain.specPath} | ${domain.filesVerified} | ${scorePercent}% | ${domain.weakestArea} |`);
|
|
611
|
+
}
|
|
612
|
+
lines.push('');
|
|
613
|
+
// Common gaps
|
|
614
|
+
if (report.commonGaps.length > 0) {
|
|
615
|
+
lines.push('## Common Gaps');
|
|
616
|
+
lines.push('');
|
|
617
|
+
for (const gap of report.commonGaps) {
|
|
618
|
+
lines.push(`- ${gap}`);
|
|
619
|
+
}
|
|
620
|
+
lines.push('');
|
|
621
|
+
}
|
|
622
|
+
// Suggested improvements
|
|
623
|
+
if (report.suggestedImprovements.length > 0) {
|
|
624
|
+
lines.push('## Suggested Improvements');
|
|
625
|
+
lines.push('');
|
|
626
|
+
for (const improvement of report.suggestedImprovements) {
|
|
627
|
+
lines.push(`### ${improvement.domain}`);
|
|
628
|
+
lines.push(`- **Issue**: ${improvement.issue}`);
|
|
629
|
+
lines.push(`- **Suggestion**: ${improvement.suggestion}`);
|
|
630
|
+
lines.push('');
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Detailed results
|
|
634
|
+
lines.push('## Detailed Results');
|
|
635
|
+
lines.push('');
|
|
636
|
+
for (const result of report.results) {
|
|
637
|
+
const scorePercent = (result.overallScore * 100).toFixed(0);
|
|
638
|
+
const status = result.overallScore >= 0.6 ? '✅' : '❌';
|
|
639
|
+
lines.push(`### ${status} ${result.filePath}`);
|
|
640
|
+
lines.push('');
|
|
641
|
+
lines.push(`- **Domain**: ${result.domain}`);
|
|
642
|
+
lines.push(`- **Overall Score**: ${scorePercent}%`);
|
|
643
|
+
lines.push(`- **LLM Confidence**: ${(result.llmConfidence * 100).toFixed(0)}%`);
|
|
644
|
+
lines.push('');
|
|
645
|
+
lines.push('| Category | Score |');
|
|
646
|
+
lines.push('|----------|-------|');
|
|
647
|
+
lines.push(`| Purpose Match | ${(result.purposeMatch.similarity * 100).toFixed(0)}% |`);
|
|
648
|
+
lines.push(`| Import Match (F1) | ${(result.importMatch.f1Score * 100).toFixed(0)}% |`);
|
|
649
|
+
lines.push(`| Export Match (F1) | ${(result.exportMatch.f1Score * 100).toFixed(0)}% |`);
|
|
650
|
+
lines.push(`| Requirement Coverage | ${(result.requirementCoverage.coverage * 100).toFixed(0)}% |`);
|
|
651
|
+
lines.push('');
|
|
652
|
+
if (result.feedback.length > 0) {
|
|
653
|
+
lines.push('**Feedback:**');
|
|
654
|
+
for (const fb of result.feedback) {
|
|
655
|
+
lines.push(`- ${fb}`);
|
|
656
|
+
}
|
|
657
|
+
lines.push('');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
lines.push('---');
|
|
661
|
+
lines.push('*Generated by spec-gen verify*');
|
|
662
|
+
return lines.join('\n');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Get list of domains
|
|
666
|
+
*/
|
|
667
|
+
getDomains() {
|
|
668
|
+
return this.specs.map(s => s.domain);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// ============================================================================
|
|
672
|
+
// CONVENIENCE FUNCTIONS
|
|
673
|
+
// ============================================================================
|
|
674
|
+
/**
|
|
675
|
+
* Run verification on a project
|
|
676
|
+
*/
|
|
677
|
+
export async function verifySpecs(llm, depGraph, options, specVersion) {
|
|
678
|
+
const engine = new SpecVerificationEngine(llm, options);
|
|
679
|
+
return engine.verify(depGraph, specVersion);
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=verification-engine.js.map
|