@toolbaux/guardian 0.1.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 +366 -0
- package/dist/adapters/csharp-adapter.js +149 -0
- package/dist/adapters/go-adapter.js +96 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/java-adapter.js +122 -0
- package/dist/adapters/python-adapter.js +183 -0
- package/dist/adapters/runner.js +69 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/typescript-adapter.js +179 -0
- package/dist/benchmarking/framework.js +91 -0
- package/dist/cli.js +343 -0
- package/dist/commands/analyze-depth.js +43 -0
- package/dist/commands/api-spec-extractor.js +52 -0
- package/dist/commands/breaking-change-analyzer.js +334 -0
- package/dist/commands/config-compliance.js +219 -0
- package/dist/commands/constraints.js +221 -0
- package/dist/commands/context.js +101 -0
- package/dist/commands/data-flow-tracer.js +291 -0
- package/dist/commands/dependency-impact-analyzer.js +27 -0
- package/dist/commands/diff.js +146 -0
- package/dist/commands/discrepancy.js +71 -0
- package/dist/commands/doc-generate.js +163 -0
- package/dist/commands/doc-html.js +120 -0
- package/dist/commands/drift.js +88 -0
- package/dist/commands/extract.js +16 -0
- package/dist/commands/feature-context.js +116 -0
- package/dist/commands/generate.js +339 -0
- package/dist/commands/guard.js +182 -0
- package/dist/commands/init.js +209 -0
- package/dist/commands/intel.js +20 -0
- package/dist/commands/license-dependency-auditor.js +33 -0
- package/dist/commands/performance-hotspot-profiler.js +42 -0
- package/dist/commands/search.js +314 -0
- package/dist/commands/security-boundary-auditor.js +359 -0
- package/dist/commands/simulate.js +294 -0
- package/dist/commands/summary.js +27 -0
- package/dist/commands/test-coverage-mapper.js +264 -0
- package/dist/commands/verify-drift.js +62 -0
- package/dist/config.js +441 -0
- package/dist/extract/ai-context-hints.js +107 -0
- package/dist/extract/analyzers/backend.js +1704 -0
- package/dist/extract/analyzers/depth.js +264 -0
- package/dist/extract/analyzers/frontend.js +2221 -0
- package/dist/extract/api-usage-tracker.js +19 -0
- package/dist/extract/cache.js +53 -0
- package/dist/extract/codebase-intel.js +190 -0
- package/dist/extract/compress.js +452 -0
- package/dist/extract/context-block.js +356 -0
- package/dist/extract/contracts.js +183 -0
- package/dist/extract/discrepancies.js +233 -0
- package/dist/extract/docs-loader.js +110 -0
- package/dist/extract/docs.js +2379 -0
- package/dist/extract/drift.js +1578 -0
- package/dist/extract/duplicates.js +435 -0
- package/dist/extract/feature-arcs.js +138 -0
- package/dist/extract/graph.js +76 -0
- package/dist/extract/html-doc.js +1409 -0
- package/dist/extract/ignore.js +45 -0
- package/dist/extract/index.js +455 -0
- package/dist/extract/llm-client.js +159 -0
- package/dist/extract/pattern-registry.js +141 -0
- package/dist/extract/product-doc.js +497 -0
- package/dist/extract/python.js +1202 -0
- package/dist/extract/runtime.js +193 -0
- package/dist/extract/schema-evolution-validator.js +35 -0
- package/dist/extract/test-gap-analyzer.js +20 -0
- package/dist/extract/tests.js +74 -0
- package/dist/extract/types.js +1 -0
- package/dist/extract/validate-backend.js +30 -0
- package/dist/extract/writer.js +11 -0
- package/dist/output-layout.js +37 -0
- package/dist/project-discovery.js +309 -0
- package/dist/schema/architecture.js +350 -0
- package/dist/schema/feature-spec.js +89 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/ux.js +46 -0
- package/package.json +75 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FEATURE 3: TEST COVERAGE MAPPER
|
|
3
|
+
*
|
|
4
|
+
* Visual heatmap showing test coverage per module, endpoint, and data flow
|
|
5
|
+
* Identifies untested high-coupling files and recommends tests
|
|
6
|
+
*
|
|
7
|
+
* Benchmarking: Medium complexity
|
|
8
|
+
* Problem Domain: Quality Assurance, Test Planning
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
/**
|
|
13
|
+
* Main function: Generate coverage heatmap
|
|
14
|
+
*/
|
|
15
|
+
export async function generateCoverageHeatmap(options) {
|
|
16
|
+
const { srcRoot, testRoot, output, highCouplingThreshold = 0.5 } = options;
|
|
17
|
+
// Scan source files
|
|
18
|
+
const sourceFiles = await scanSourceFiles(srcRoot);
|
|
19
|
+
// Scan test files
|
|
20
|
+
const testFiles = await scanTestFiles(testRoot);
|
|
21
|
+
// Map tests to source files
|
|
22
|
+
const coverage = mapTestCoverage(sourceFiles, testFiles);
|
|
23
|
+
// Identify high-coupling files (from coupling scores in architecture context)
|
|
24
|
+
const highCouplingFiles = identifyHighCouplingFiles(sourceFiles, highCouplingThreshold);
|
|
25
|
+
// Find gaps
|
|
26
|
+
const gaps = identifyGaps(coverage, highCouplingFiles);
|
|
27
|
+
// Calculate overall metrics
|
|
28
|
+
const totalLines = coverage.reduce((sum, c) => sum + c.totalLines, 0);
|
|
29
|
+
const coveredLines = coverage.reduce((sum, c) => sum + c.linesCovered, 0);
|
|
30
|
+
const overallCoverage = totalLines > 0 ? (coveredLines / totalLines) * 100 : 0;
|
|
31
|
+
const heatmap = {
|
|
32
|
+
modules: coverage,
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
overallCoverage,
|
|
35
|
+
criticalGaps: gaps.filter((g) => g.severity === "critical").length,
|
|
36
|
+
riskScore: calculateRiskScore(coverage, overallCoverage),
|
|
37
|
+
recommendations: generateRecommendations(coverage, gaps),
|
|
38
|
+
};
|
|
39
|
+
if (output) {
|
|
40
|
+
await writeHeatmapReport(heatmap, output);
|
|
41
|
+
}
|
|
42
|
+
return heatmap;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Helper: Scan source files
|
|
46
|
+
*/
|
|
47
|
+
async function scanSourceFiles(srcRoot) {
|
|
48
|
+
const files = new Map();
|
|
49
|
+
async function walkDir(dir) {
|
|
50
|
+
try {
|
|
51
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const fullPath = path.join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory() &&
|
|
55
|
+
[".git", "node_modules", "dist", "build", "coverage"].includes(entry.name)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
await walkDir(fullPath);
|
|
60
|
+
}
|
|
61
|
+
else if (entry.isFile() &&
|
|
62
|
+
[".ts", ".tsx", ".js", ".jsx"].some((ext) => entry.name.endsWith(ext))) {
|
|
63
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
64
|
+
const relPath = path.relative(srcRoot, fullPath);
|
|
65
|
+
files.set(relPath, content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Skip inaccessible directories
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await walkDir(srcRoot);
|
|
74
|
+
return files;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Helper: Scan test files
|
|
78
|
+
*/
|
|
79
|
+
async function scanTestFiles(testRoot) {
|
|
80
|
+
const files = new Map();
|
|
81
|
+
async function walkDir(dir) {
|
|
82
|
+
try {
|
|
83
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const fullPath = path.join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory() && [".git", "node_modules"].includes(entry.name)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
await walkDir(fullPath);
|
|
91
|
+
}
|
|
92
|
+
else if (entry.isFile() &&
|
|
93
|
+
(entry.name.endsWith(".test.ts") ||
|
|
94
|
+
entry.name.endsWith(".test.js") ||
|
|
95
|
+
entry.name.endsWith(".spec.ts") ||
|
|
96
|
+
entry.name.endsWith(".spec.js"))) {
|
|
97
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
98
|
+
const relPath = path.relative(testRoot, fullPath);
|
|
99
|
+
files.set(relPath, content);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Skip inaccessible directories
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await walkDir(testRoot);
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Helper: Map test coverage to source files
|
|
112
|
+
*/
|
|
113
|
+
function mapTestCoverage(sourceFiles, testFiles) {
|
|
114
|
+
const coverage = [];
|
|
115
|
+
for (const [sourcePath, sourceContent] of sourceFiles.entries()) {
|
|
116
|
+
const lineCount = sourceContent.split("\n").length;
|
|
117
|
+
const testers = [];
|
|
118
|
+
// Find tests that reference this file
|
|
119
|
+
for (const [testPath, testContent] of testFiles.entries()) {
|
|
120
|
+
const sourceFileName = path.basename(sourcePath, path.extname(sourcePath));
|
|
121
|
+
if (testContent.includes(sourceFileName) ||
|
|
122
|
+
testContent.includes(sourcePath) ||
|
|
123
|
+
testPath.includes(sourceFileName)) {
|
|
124
|
+
testers.push(testPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Estimate coverage based on test presence
|
|
128
|
+
const estimatedCoverage = testers.length > 0 ? calculateEstimatedCoverage(sourceContent) : 0;
|
|
129
|
+
coverage.push({
|
|
130
|
+
module: sourcePath,
|
|
131
|
+
linesCovered: Math.floor((estimatedCoverage / 100) * lineCount),
|
|
132
|
+
totalLines: lineCount,
|
|
133
|
+
coverage: estimatedCoverage,
|
|
134
|
+
level: getCoverageLevel(estimatedCoverage),
|
|
135
|
+
testers,
|
|
136
|
+
gaps: [],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return coverage;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Helper: Calculate estimated coverage by analysing function complexity
|
|
143
|
+
*/
|
|
144
|
+
function calculateEstimatedCoverage(content) {
|
|
145
|
+
// Simple heuristic: count exported functions/classes
|
|
146
|
+
const exports = (content.match(/export\s+(function|class|interface|type)/g) || [])
|
|
147
|
+
.length;
|
|
148
|
+
const complexFunctions = (content.match(/function\s+\w+\s*\([^)]*\)\s*\{[^}]*if|for|switch/g) || []).length;
|
|
149
|
+
// If we have exports, assume 60% base coverage + complexity bonus
|
|
150
|
+
if (exports > 0) {
|
|
151
|
+
return Math.min(90, 60 + complexFunctions * 5);
|
|
152
|
+
}
|
|
153
|
+
return Math.min(60, complexFunctions * 10);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Helper: Determine coverage level
|
|
157
|
+
*/
|
|
158
|
+
function getCoverageLevel(coverage) {
|
|
159
|
+
if (coverage >= 80)
|
|
160
|
+
return "full";
|
|
161
|
+
if (coverage >= 60)
|
|
162
|
+
return "partial";
|
|
163
|
+
if (coverage >= 30)
|
|
164
|
+
return "minimal";
|
|
165
|
+
return "none";
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Helper: Identify high-coupling files
|
|
169
|
+
*/
|
|
170
|
+
function identifyHighCouplingFiles(sourceFiles, threshold) {
|
|
171
|
+
// Known high-coupling patterns
|
|
172
|
+
const highCouplingPatterns = /extract|adapter|index|types|config/i;
|
|
173
|
+
const highCoupling = new Set();
|
|
174
|
+
for (const filePath of sourceFiles.keys()) {
|
|
175
|
+
if (highCouplingPatterns.test(filePath)) {
|
|
176
|
+
highCoupling.add(filePath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return highCoupling;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Helper: Identify coverage gaps
|
|
183
|
+
*/
|
|
184
|
+
function identifyGaps(coverage, highCouplingFiles) {
|
|
185
|
+
const gaps = [];
|
|
186
|
+
for (const metrics of coverage) {
|
|
187
|
+
const isHighCoupling = highCouplingFiles.has(metrics.module);
|
|
188
|
+
if (metrics.coverage < 50) {
|
|
189
|
+
gaps.push({
|
|
190
|
+
location: metrics.module,
|
|
191
|
+
severity: isHighCoupling ? "critical" : "high",
|
|
192
|
+
reason: `Low coverage on ${isHighCoupling ? "high-coupling" : "important"} module`,
|
|
193
|
+
suggestedTest: `Add integration tests for ${path.basename(metrics.module)}`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (metrics.coverage < 70 && isHighCoupling) {
|
|
197
|
+
gaps.push({
|
|
198
|
+
location: metrics.module,
|
|
199
|
+
severity: "high",
|
|
200
|
+
reason: `Partial coverage on high-coupling file`,
|
|
201
|
+
suggestedTest: `Increase coverage: add edge case tests`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return gaps;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Helper: Calculate risk score (0-100)
|
|
209
|
+
*/
|
|
210
|
+
function calculateRiskScore(coverage, overallCoverage) {
|
|
211
|
+
// Find untested high-coupling files
|
|
212
|
+
const untested = coverage.filter((c) => c.coverage === 0 && c.module.match(/extract|adapter|index|types/i)).length;
|
|
213
|
+
// Score based on overall coverage + untested critical files
|
|
214
|
+
let score = Math.max(0, 100 - overallCoverage + untested * 10);
|
|
215
|
+
return Math.min(100, score);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Helper: Generate recommendations
|
|
219
|
+
*/
|
|
220
|
+
function generateRecommendations(coverage, gaps) {
|
|
221
|
+
const recommendations = [];
|
|
222
|
+
const criticalGapsCount = gaps.filter((g) => g.severity === "critical").length;
|
|
223
|
+
if (criticalGapsCount > 0) {
|
|
224
|
+
recommendations.push(`🚨 ${criticalGapsCount} critical test gaps: High-coupling files lack tests`);
|
|
225
|
+
}
|
|
226
|
+
const untestedCount = coverage.filter((c) => c.coverage === 0).length;
|
|
227
|
+
if (untestedCount > 0) {
|
|
228
|
+
recommendations.push(`⚠️ ${untestedCount} modules with zero test coverage`);
|
|
229
|
+
}
|
|
230
|
+
const avgCoverage = coverage.reduce((sum, c) => sum + c.coverage, 0) / coverage.length;
|
|
231
|
+
if (avgCoverage < 60) {
|
|
232
|
+
recommendations.push(`💡 Average coverage ${avgCoverage.toFixed(1)}% is below 60%`);
|
|
233
|
+
}
|
|
234
|
+
if (gaps.length === 0) {
|
|
235
|
+
recommendations.push(`✅ No major test coverage gaps detected`);
|
|
236
|
+
}
|
|
237
|
+
return recommendations;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Helper: Write heatmap report to markdown
|
|
241
|
+
*/
|
|
242
|
+
async function writeHeatmapReport(heatmap, outputPath) {
|
|
243
|
+
let md = `# Test Coverage Heatmap Report\n\n`;
|
|
244
|
+
md += `Generated: ${heatmap.timestamp}\n\n`;
|
|
245
|
+
md += `## Summary\n`;
|
|
246
|
+
md += `- **Overall Coverage:** ${heatmap.overallCoverage.toFixed(1)}%\n`;
|
|
247
|
+
md += `- **Critical Gaps:** ${heatmap.criticalGaps}\n`;
|
|
248
|
+
md += `- **Risk Score:** ${heatmap.riskScore}/100\n\n`;
|
|
249
|
+
// Coverage heatmap
|
|
250
|
+
md += `## Coverage Heatmap\n`;
|
|
251
|
+
md += `| Module | Coverage | Level | Tests |\n`;
|
|
252
|
+
md += `|--------|----------|-------|-------|\n`;
|
|
253
|
+
for (const m of heatmap.modules) {
|
|
254
|
+
const heat = m.coverage >= 80 ? "🟢" : m.coverage >= 60 ? "🟡" : m.coverage >= 30 ? "🟠" : "🔴";
|
|
255
|
+
md += `| ${m.module} | ${m.coverage.toFixed(1)}% | ${heat} ${m.level} | ${m.testers.length} |\n`;
|
|
256
|
+
}
|
|
257
|
+
md += `\n## Recommendations\n`;
|
|
258
|
+
for (const rec of heatmap.recommendations) {
|
|
259
|
+
md += `- ${rec}\n`;
|
|
260
|
+
}
|
|
261
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
262
|
+
await fs.writeFile(outputPath, md, "utf8");
|
|
263
|
+
}
|
|
264
|
+
export default generateCoverageHeatmap;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { computeProjectDrift } from "../extract/drift.js";
|
|
4
|
+
import { logResolvedProjectPaths, resolveProjectPaths } from "../project-discovery.js";
|
|
5
|
+
export async function runVerifyDrift(options) {
|
|
6
|
+
const resolved = await resolveProjectPaths({
|
|
7
|
+
projectRoot: options.projectRoot,
|
|
8
|
+
backendRoot: options.backendRoot,
|
|
9
|
+
frontendRoot: options.frontendRoot,
|
|
10
|
+
configPath: options.configPath
|
|
11
|
+
});
|
|
12
|
+
logResolvedProjectPaths(resolved);
|
|
13
|
+
const drift = await computeProjectDrift({
|
|
14
|
+
backendRoot: resolved.backendRoot,
|
|
15
|
+
frontendRoot: resolved.frontendRoot,
|
|
16
|
+
configPath: options.configPath
|
|
17
|
+
});
|
|
18
|
+
const config = resolved.config;
|
|
19
|
+
const projectRoot = resolved.workspaceRoot;
|
|
20
|
+
const baselinePath = options.baseline || config.drift?.baselinePath || "specs-out/baseline.json";
|
|
21
|
+
const resolvedBaseline = path.isAbsolute(baselinePath)
|
|
22
|
+
? baselinePath
|
|
23
|
+
: path.resolve(projectRoot, baselinePath);
|
|
24
|
+
let baselinePayload = null;
|
|
25
|
+
try {
|
|
26
|
+
const raw = await fs.readFile(resolvedBaseline, "utf8");
|
|
27
|
+
baselinePayload = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.log(`[Warning] Could not load baseline at ${resolvedBaseline}`);
|
|
31
|
+
}
|
|
32
|
+
const threshold = options.strictThreshold ? parseFloat(options.strictThreshold) : 0.15;
|
|
33
|
+
console.log("=========================================");
|
|
34
|
+
console.log("SpecGuard Drift Verification");
|
|
35
|
+
console.log("=========================================\n");
|
|
36
|
+
console.log(`Current Status: ${drift.status}`);
|
|
37
|
+
console.log(`Current Delta: ${drift.delta.toFixed(4)}`);
|
|
38
|
+
console.log(`Current K_t: ${drift.K_t.toFixed(4)}`);
|
|
39
|
+
let failed = false;
|
|
40
|
+
if (drift.status === "critical") {
|
|
41
|
+
console.error(`\n[ERROR] Drift status is "critical". Reached critical capacity limits.`);
|
|
42
|
+
failed = true;
|
|
43
|
+
}
|
|
44
|
+
if (baselinePayload && baselinePayload.drift) {
|
|
45
|
+
const baselineDelta = baselinePayload.drift.delta ?? 0;
|
|
46
|
+
const shift = Math.abs(drift.delta - baselineDelta);
|
|
47
|
+
console.log(`\nBaseline Delta: ${baselineDelta.toFixed(4)}`);
|
|
48
|
+
console.log(`Coupling Shift: ${shift.toFixed(4)} (Threshold: ${threshold})`);
|
|
49
|
+
if (shift > threshold) {
|
|
50
|
+
console.error(`\n[ERROR] Architectural coupling shift (${shift.toFixed(4)}) exceeded strict threshold (${threshold}).`);
|
|
51
|
+
failed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
console.log("=========================================");
|
|
55
|
+
if (failed) {
|
|
56
|
+
console.error("Verification FAILED. Architectural drift guardrails breached.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log("Verification PASSED. Architecture remains within acceptable limits.");
|
|
61
|
+
}
|
|
62
|
+
}
|