code-auditor-mcp 3.4.11 → 3.4.13
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +119 -1
- package/dist/analyzers/__verify_self_probe.d.ts +2 -0
- package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
- package/dist/analyzers/__verify_self_probe.js +5 -0
- package/dist/analyzers/__verify_self_probe.js.map +1 -0
- package/dist/analyzers/analyzerUtils.d.ts +34 -1
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +61 -22
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +259 -198
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +59 -46
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
- package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.js +10 -0
- package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
- package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.js +42 -44
- package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
- package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.js +30 -0
- package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +72 -2
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +462 -338
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +11 -0
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +188 -125
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +118 -1
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +1717 -104
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/ruleTiming.d.ts +54 -0
- package/dist/analyzers/ruleTiming.d.ts.map +1 -0
- package/dist/analyzers/ruleTiming.js +71 -0
- package/dist/analyzers/ruleTiming.js.map +1 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +24 -186
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1081 -1061
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -212
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1518
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -472
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
- package/dist/analyzers/universal/schema/config.d.ts +31 -0
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/config.js +57 -0
- package/dist/analyzers/universal/schema/config.js.map +1 -0
- package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
- package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/discovery.js +616 -0
- package/dist/analyzers/universal/schema/discovery.js.map +1 -0
- package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
- package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
- package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/migrations.js +238 -0
- package/dist/analyzers/universal/schema/migrations.js.map +1 -0
- package/dist/analyzers/universal/schema/types.d.ts +131 -0
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/types.js +10 -0
- package/dist/analyzers/universal/schema/types.js.map +1 -0
- package/dist/analyzers/universal/schema/violations.d.ts +44 -0
- package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/violations.js +54 -0
- package/dist/analyzers/universal/schema/violations.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +101 -39
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +348 -40
- package/dist/cli.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +9 -7
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +12 -3
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +15 -5
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts +75 -0
- package/dist/config/effectiveConfig.d.ts.map +1 -0
- package/dist/config/effectiveConfig.js +213 -0
- package/dist/config/effectiveConfig.js.map +1 -0
- package/dist/config/pathProfiles.d.ts +7 -4
- package/dist/config/pathProfiles.d.ts.map +1 -1
- package/dist/config/pathProfiles.js +8 -10
- package/dist/config/pathProfiles.js.map +1 -1
- package/dist/config/thresholdRationales.d.ts +45 -0
- package/dist/config/thresholdRationales.d.ts.map +1 -0
- package/dist/config/thresholdRationales.js +67 -0
- package/dist/config/thresholdRationales.js.map +1 -0
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +6 -2
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/enforcement/diffGate.d.ts +55 -0
- package/dist/enforcement/diffGate.d.ts.map +1 -0
- package/dist/enforcement/diffGate.js +136 -0
- package/dist/enforcement/diffGate.js.map +1 -0
- package/dist/enforcement/gate.d.ts +36 -0
- package/dist/enforcement/gate.d.ts.map +1 -0
- package/dist/enforcement/gate.js +72 -0
- package/dist/enforcement/gate.js.map +1 -0
- package/dist/enforcement/suppressions.d.ts +41 -0
- package/dist/enforcement/suppressions.d.ts.map +1 -0
- package/dist/enforcement/suppressions.js +170 -0
- package/dist/enforcement/suppressions.js.map +1 -0
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +4 -1
- package/dist/fingerprint.js.map +1 -1
- package/dist/grammars/tree-sitter-scss.wasm +0 -0
- package/dist/invariants/ruleEngine.d.ts +3 -4
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +2 -2
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +14 -0
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +93 -55
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/LanguageRegistry.d.ts +8 -0
- package/dist/languages/LanguageRegistry.d.ts.map +1 -1
- package/dist/languages/LanguageRegistry.js +8 -0
- package/dist/languages/LanguageRegistry.js.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +82 -60
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +404 -349
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +51 -6
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +127 -88
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.d.ts +36 -0
- package/dist/languages/adapterBridge.d.ts.map +1 -1
- package/dist/languages/adapterBridge.js +89 -35
- package/dist/languages/adapterBridge.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts +66 -23
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +553 -474
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +38 -9
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +111 -74
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/converter.d.ts +18 -0
- package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/converter.js +18 -0
- package/dist/languages/tree-sitter/converter.js.map +1 -1
- package/dist/languages/tree-sitter/parser.d.ts +39 -2
- package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
- package/dist/languages/tree-sitter/parser.js +95 -12
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/types.d.ts +67 -2
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -62
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1461 -791
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +1 -0
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +1 -0
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +30 -1
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +527 -203
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +5 -12
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +175 -100
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/presets/presets.d.ts +54 -0
- package/dist/presets/presets.d.ts.map +1 -0
- package/dist/presets/presets.js +181 -0
- package/dist/presets/presets.js.map +1 -0
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +0 -2
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/ruleAliases.d.ts +67 -0
- package/dist/ruleAliases.d.ts.map +1 -0
- package/dist/ruleAliases.js +79 -0
- package/dist/ruleAliases.js.map +1 -0
- package/dist/styles/cssAstExtractor.d.ts +20 -5
- package/dist/styles/cssAstExtractor.d.ts.map +1 -1
- package/dist/styles/cssAstExtractor.js +247 -9
- package/dist/styles/cssAstExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.js +13 -4
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindConfigLoader.d.ts +8 -6
- package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
- package/dist/styles/tailwindConfigLoader.js +61 -15
- package/dist/styles/tailwindConfigLoader.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +13 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/types.d.ts +201 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/scripts/hook-audit.sh +25 -7
- package/plugin/skills/code-auditor/SKILL.md +5 -5
package/dist/pipeline.js
CHANGED
|
@@ -9,134 +9,174 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Stage position IS the dependency declaration — no topological sort needed.
|
|
11
11
|
*/
|
|
12
|
-
import { readFile } from 'fs/promises';
|
|
12
|
+
import { readFile, stat } from 'fs/promises';
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
13
14
|
import path from 'path';
|
|
14
15
|
import { performance } from 'perf_hooks';
|
|
15
|
-
import { AuditAbortedError, } from './types.js';
|
|
16
|
+
import { AuditAbortedError, MAX_ORPHAN_SOURCE_BYTES, } from './types.js';
|
|
17
|
+
import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
|
|
18
|
+
import { resetRuleTiming, getRuleTimingSortedDesc } from './analyzers/ruleTiming.js';
|
|
16
19
|
import { LanguageRegistry } from './languages/LanguageRegistry.js';
|
|
17
20
|
import { discoverFiles } from './utils/fileDiscovery.js';
|
|
18
21
|
import { resolvePathProfile } from './config/pathProfiles.js';
|
|
19
22
|
import { validateFactsDependencies } from './pipelineTypes.js';
|
|
20
23
|
// ── Stage 1: Traverse + Parse ──────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Stream files one-at-a-time into stage 2 so per-file memory can be freed
|
|
26
|
+
* before the next file is parsed.
|
|
27
|
+
*
|
|
28
|
+
* File discovery is eager (separate from parsing) so the total count is
|
|
29
|
+
* available for progress reporting throughout the parse + visit loop.
|
|
30
|
+
*
|
|
31
|
+
* @returns generator (yields tuples), total file count, and a closure to
|
|
32
|
+
* retrieve aggregate parse/read timing after the generator completes.
|
|
33
|
+
*/
|
|
34
|
+
function runStage1(config) {
|
|
35
|
+
// ── Eager discovery (same as before) ─────────────────────────────────
|
|
26
36
|
const projectRoot = config.projectRoot;
|
|
27
37
|
let files;
|
|
28
38
|
if (config.explicitFiles !== undefined) {
|
|
29
39
|
files = config.explicitFiles;
|
|
30
40
|
}
|
|
31
41
|
else {
|
|
32
|
-
|
|
33
|
-
excludeDirs: ['node_modules', '.next', 'dist', 'build', '.git', 'coverage', '.turbo'],
|
|
34
|
-
});
|
|
42
|
+
// discovery runs lazily inside the generator below
|
|
35
43
|
}
|
|
36
44
|
// Group by language adapter
|
|
37
45
|
const registry = LanguageRegistry.getInstance();
|
|
38
|
-
|
|
39
|
-
const orphans = [];
|
|
40
|
-
for (const file of files) {
|
|
41
|
-
const adapter = registry.getAdapterForFile(file);
|
|
42
|
-
if (adapter) {
|
|
43
|
-
const list = groups.get(adapter) ?? [];
|
|
44
|
-
list.push(file);
|
|
45
|
-
groups.set(adapter, list);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
orphans.push(file);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Parse each file once
|
|
52
|
-
const tuples = [];
|
|
46
|
+
let total;
|
|
53
47
|
let parsed = 0;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
let readMs = 0;
|
|
49
|
+
let parseMs = 0;
|
|
50
|
+
// Spec 32 — files that failed to parse (or be read) during stage 1, with the
|
|
51
|
+
// reason. A non-empty list means the audit was incomplete; surfaced in coverage
|
|
52
|
+
// and forces a non-zero exit so a plausible-but-wrong report is never silent.
|
|
53
|
+
const unparsedFiles = [];
|
|
54
|
+
async function* generate() {
|
|
55
|
+
// Lazy file discovery (only if not explicit)
|
|
56
|
+
const fileList = files ?? await discoverFiles(projectRoot, {
|
|
57
|
+
excludeDirs: ['node_modules', '.next', 'dist', 'build', '.git', 'coverage', '.turbo'],
|
|
58
|
+
});
|
|
59
|
+
total = fileList.length;
|
|
60
|
+
const groups = new Map();
|
|
61
|
+
const orphans = [];
|
|
62
|
+
for (const file of fileList) {
|
|
63
|
+
const adapter = registry.getAdapterForFile(file);
|
|
64
|
+
if (adapter) {
|
|
65
|
+
const list = groups.get(adapter) ?? [];
|
|
66
|
+
list.push(file);
|
|
67
|
+
groups.set(adapter, list);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
orphans.push(file);
|
|
59
71
|
}
|
|
72
|
+
}
|
|
73
|
+
// Parse each file and yield immediately
|
|
74
|
+
for (const [adapter, adapterFiles] of groups) {
|
|
75
|
+
for (const file of adapterFiles) {
|
|
76
|
+
if (config.abortSignal?.aborted) {
|
|
77
|
+
throw new AuditAbortedError('Audit aborted during stage 1');
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const r0 = performance.now();
|
|
81
|
+
const content = await readFile(file, 'utf-8');
|
|
82
|
+
readMs += performance.now() - r0;
|
|
83
|
+
const p0 = performance.now();
|
|
84
|
+
const ast = await adapter.parse(file, content);
|
|
85
|
+
parseMs += performance.now() - p0;
|
|
86
|
+
parsed++;
|
|
87
|
+
yield {
|
|
88
|
+
kind: 'parsed',
|
|
89
|
+
file,
|
|
90
|
+
ast,
|
|
91
|
+
adapter,
|
|
92
|
+
sourceCode: content,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// Spec 32 — a parse failure (including a WASM abort that survived
|
|
97
|
+
// parseWithRecovery) must never be silent: record it so it lands in
|
|
98
|
+
// coverage and forces a non-zero exit.
|
|
99
|
+
unparsedFiles.push({ filePath: file, reason: err?.message ?? String(err) });
|
|
100
|
+
if (config.progressCallback) {
|
|
101
|
+
config.progressCallback({
|
|
102
|
+
current: parsed,
|
|
103
|
+
total,
|
|
104
|
+
analyzer: 'pipeline',
|
|
105
|
+
phase: 'stage1',
|
|
106
|
+
file,
|
|
107
|
+
message: `Skipped: ${err.message}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Orphan files (no LanguageAdapter)
|
|
114
|
+
for (const file of orphans) {
|
|
60
115
|
try {
|
|
116
|
+
// Spec 31 — oversized orphan .sql dumps are not materialized into a
|
|
117
|
+
// source string; the schema-sql visitor streams them on demand (and
|
|
118
|
+
// skips them entirely when they contain no DDL). Restricted to .sql:
|
|
119
|
+
// JSON/CSS orphans must still be materialized — their visitors parse
|
|
120
|
+
// the content, and an empty source would silently change behavior for
|
|
121
|
+
// large-but-valid files (e.g. a multi-MB audit-report.json).
|
|
122
|
+
const st = await stat(file);
|
|
123
|
+
if (file.endsWith('.sql') && st.size > MAX_ORPHAN_SOURCE_BYTES) {
|
|
124
|
+
parsed++;
|
|
125
|
+
yield {
|
|
126
|
+
kind: 'raw',
|
|
127
|
+
file,
|
|
128
|
+
ast: null,
|
|
129
|
+
adapter: null,
|
|
130
|
+
sourceCode: '',
|
|
131
|
+
};
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
61
134
|
const r0 = performance.now();
|
|
62
135
|
const content = await readFile(file, 'utf-8');
|
|
63
136
|
readMs += performance.now() - r0;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
tuples.push({
|
|
68
|
-
kind: 'parsed',
|
|
137
|
+
parsed++;
|
|
138
|
+
yield {
|
|
139
|
+
kind: 'raw',
|
|
69
140
|
file,
|
|
70
|
-
ast,
|
|
71
|
-
adapter,
|
|
141
|
+
ast: null,
|
|
142
|
+
adapter: null,
|
|
72
143
|
sourceCode: content,
|
|
73
|
-
}
|
|
74
|
-
parsed++;
|
|
144
|
+
};
|
|
75
145
|
}
|
|
76
146
|
catch (err) {
|
|
77
|
-
|
|
78
|
-
// Individual files that fail to parse don't abort the entire run
|
|
147
|
+
unparsedFiles.push({ filePath: file, reason: `read error: ${err?.message ?? String(err)}` });
|
|
79
148
|
if (config.progressCallback) {
|
|
80
149
|
config.progressCallback({
|
|
81
|
-
current:
|
|
82
|
-
total:
|
|
150
|
+
current: orphans.indexOf(file),
|
|
151
|
+
total: orphans.length,
|
|
83
152
|
analyzer: 'pipeline',
|
|
84
153
|
phase: 'stage1',
|
|
85
154
|
file,
|
|
86
|
-
message: `
|
|
155
|
+
message: `Raw file skipped (read error): ${err.message}`,
|
|
87
156
|
});
|
|
88
157
|
}
|
|
89
158
|
}
|
|
90
159
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
tuples.push({
|
|
100
|
-
kind: 'raw',
|
|
101
|
-
file,
|
|
102
|
-
ast: null,
|
|
103
|
-
adapter: null,
|
|
104
|
-
sourceCode: content,
|
|
160
|
+
const orphanIncluded = fileList.filter(f => !registry.getAdapterForFile(f)).length;
|
|
161
|
+
if (orphanIncluded > 0 && config.progressCallback) {
|
|
162
|
+
config.progressCallback({
|
|
163
|
+
current: orphanIncluded,
|
|
164
|
+
total: orphanIncluded,
|
|
165
|
+
analyzer: 'pipeline',
|
|
166
|
+
phase: 'stage1',
|
|
167
|
+
message: `${orphanIncluded} file(s) included as raw (no language adapter)`,
|
|
105
168
|
});
|
|
106
169
|
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
if (config.progressCallback) {
|
|
109
|
-
config.progressCallback({
|
|
110
|
-
current: orphans.indexOf(file),
|
|
111
|
-
total: orphans.length,
|
|
112
|
-
analyzer: 'pipeline',
|
|
113
|
-
phase: 'stage1',
|
|
114
|
-
file,
|
|
115
|
-
message: `Raw file skipped (read error): ${err.message}`,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
170
|
}
|
|
120
|
-
const orphanIncluded = tuples.filter(t => t.kind === 'raw').length;
|
|
121
|
-
if (orphanIncluded > 0 && config.progressCallback) {
|
|
122
|
-
config.progressCallback({
|
|
123
|
-
current: orphanIncluded,
|
|
124
|
-
total: orphanIncluded,
|
|
125
|
-
analyzer: 'pipeline',
|
|
126
|
-
phase: 'stage1',
|
|
127
|
-
message: `${orphanIncluded} file(s) included as raw (no language adapter)`,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
const duration = performance.now() - t0;
|
|
131
171
|
return {
|
|
132
|
-
|
|
133
|
-
fileCount
|
|
134
|
-
|
|
135
|
-
readDurationMs: readMs,
|
|
172
|
+
generator: generate(),
|
|
173
|
+
get fileCount() { return total; },
|
|
174
|
+
getUnparsedFiles: () => unparsedFiles,
|
|
175
|
+
getTiming: () => ({ parseDurationMs: parseMs, readDurationMs: readMs }),
|
|
136
176
|
};
|
|
137
177
|
}
|
|
138
178
|
// ── Stage 2: Per-file visitors ─────────────────────────────────────────────
|
|
139
|
-
async function runStage2(tuples, visitors, config) {
|
|
179
|
+
export async function runStage2(tuples, visitors, config, totalFiles) {
|
|
140
180
|
const t0 = performance.now();
|
|
141
181
|
const visitorResults = new Map();
|
|
142
182
|
const allFacts = new Map();
|
|
@@ -160,109 +200,112 @@ async function runStage2(tuples, visitors, config) {
|
|
|
160
200
|
const infra = rawConfig['_infra'] ?? {};
|
|
161
201
|
const pathProfiles = infra['pathProfiles'];
|
|
162
202
|
const severityOverrides = infra['severityOverrides'] ?? {};
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
// Stream tuples from stage 1
|
|
204
|
+
let i = 0;
|
|
205
|
+
for await (const tuple of tuples) {
|
|
166
206
|
// Check abort
|
|
167
207
|
if (config.abortSignal?.aborted) {
|
|
168
208
|
throw new AuditAbortedError('Audit aborted during stage 2');
|
|
169
209
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
config.progressCallback
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Resolve path profiles for this file (non-analyzer-specific)
|
|
181
|
-
let fileInfra = infra;
|
|
182
|
-
let fileProfileNames = [];
|
|
183
|
-
let fileSeverityCap;
|
|
184
|
-
if (pathProfiles && pathProfiles.length > 0) {
|
|
185
|
-
const resolved = resolvePathProfile(tuple.file, projectRoot, pathProfiles);
|
|
186
|
-
if (Object.keys(resolved.overrides).length > 0) {
|
|
187
|
-
fileInfra = { ...infra, ...resolved.overrides };
|
|
188
|
-
}
|
|
189
|
-
fileProfileNames = resolved.matchedProfileNames;
|
|
190
|
-
fileSeverityCap = resolved.severityCap;
|
|
191
|
-
}
|
|
192
|
-
// Fan out to all visitors for this file — filter by declared extensions
|
|
193
|
-
const fileExt = path.extname(tuple.file);
|
|
194
|
-
for (const visitor of visitors) {
|
|
195
|
-
// Dispatch check: backward-compat visitors see only parsed tuples.
|
|
196
|
-
// Visitors that declare extensions see only tuples whose extension they consume.
|
|
197
|
-
if (visitor.extensions) {
|
|
198
|
-
if (!visitor.extensions.includes(fileExt))
|
|
199
|
-
continue;
|
|
210
|
+
try {
|
|
211
|
+
// Progress
|
|
212
|
+
if (config.progressCallback && i % 10 === 0) {
|
|
213
|
+
config.progressCallback({
|
|
214
|
+
current: i,
|
|
215
|
+
total: totalFiles,
|
|
216
|
+
analyzer: 'pipeline',
|
|
217
|
+
phase: 'stage2',
|
|
218
|
+
file: tuple.file,
|
|
219
|
+
});
|
|
200
220
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
221
|
+
// Resolve path profiles for this file (non-analyzer-specific)
|
|
222
|
+
let fileInfra = infra;
|
|
223
|
+
let fileProfileNames = [];
|
|
224
|
+
let fileGateExcluded;
|
|
225
|
+
if (pathProfiles && pathProfiles.length > 0) {
|
|
226
|
+
const resolved = resolvePathProfile(tuple.file, projectRoot, pathProfiles);
|
|
227
|
+
if (Object.keys(resolved.overrides).length > 0) {
|
|
228
|
+
fileInfra = { ...infra, ...resolved.overrides };
|
|
229
|
+
}
|
|
230
|
+
fileProfileNames = resolved.matchedProfileNames;
|
|
231
|
+
fileGateExcluded = resolved.excludeFromGate;
|
|
205
232
|
}
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
: v.profile,
|
|
227
|
-
severity: (severityOverrides[v.rule] ?? v.severity),
|
|
228
|
-
}))
|
|
229
|
-
// Filter out violations whose severity was overridden to 'off' (Spec-11 R5)
|
|
230
|
-
.filter((v) => v.severity !== 'off')
|
|
231
|
-
// Apply severity cap from path profiles (Spec-20)
|
|
232
|
-
// Applied AFTER severityOverrides so path-level caps beat global promotions
|
|
233
|
-
.map((v) => {
|
|
234
|
-
if (fileSeverityCap) {
|
|
235
|
-
const capIndex = severityOrder.indexOf(fileSeverityCap);
|
|
236
|
-
if (capIndex >= 0 && severityOrder.indexOf(v.severity) > capIndex) {
|
|
237
|
-
return { ...v, severity: fileSeverityCap };
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return v;
|
|
241
|
-
});
|
|
242
|
-
// Accumulate violations
|
|
243
|
-
const ar = visitorResults.get(visitor.name);
|
|
244
|
-
ar.violations.push(...processedViolations);
|
|
245
|
-
const prevFiles = ar.status.status === 'visitor-ran' ? ar.status.filesProcessed : 0;
|
|
246
|
-
ar.status = {
|
|
247
|
-
status: 'visitor-ran',
|
|
248
|
-
filesProcessed: prevFiles + 1,
|
|
233
|
+
// Fan out to all visitors for this file — filter by declared extensions
|
|
234
|
+
const fileExt = path.extname(tuple.file);
|
|
235
|
+
for (const visitor of visitors) {
|
|
236
|
+
// Dispatch check: backward-compat visitors see only parsed tuples.
|
|
237
|
+
// Visitors that declare extensions see only tuples whose extension they consume.
|
|
238
|
+
if (visitor.extensions) {
|
|
239
|
+
if (!visitor.extensions.includes(fileExt))
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// No extensions declared → parsed tuples only (backward compat)
|
|
244
|
+
if (tuple.kind !== 'parsed')
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const visitorConfig = { ...(rawConfig[visitor.name] ?? {}), ...fileInfra };
|
|
248
|
+
const visitorContext = {
|
|
249
|
+
projectRoot,
|
|
250
|
+
filePath: tuple.file,
|
|
251
|
+
config: visitorConfig,
|
|
252
|
+
abortSignal: config.abortSignal,
|
|
249
253
|
};
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
+
try {
|
|
255
|
+
const v0 = performance.now();
|
|
256
|
+
const result = await visitor.visit(tuple.ast, tuple.adapter, visitorContext, tuple.sourceCode);
|
|
257
|
+
const vMs = performance.now() - v0;
|
|
258
|
+
// Accumulate timing
|
|
259
|
+
timingMap.set(visitor.name, (timingMap.get(visitor.name) ?? 0) + vMs);
|
|
260
|
+
// Attach profile, severity overrides, analyzer name, and gate exclusion
|
|
261
|
+
// (Spec 36 R4). Severity is left untouched — a path profile excludes a
|
|
262
|
+
// file from the blocking gate, it never softens a finding within it.
|
|
263
|
+
const processedViolations = result.violations
|
|
264
|
+
.map((v) => ({
|
|
265
|
+
...v,
|
|
266
|
+
profile: fileProfileNames.length > 0
|
|
267
|
+
? fileProfileNames[fileProfileNames.length - 1]
|
|
268
|
+
: v.profile,
|
|
269
|
+
severity: (severityOverrides[v.rule] ?? v.severity),
|
|
270
|
+
...(fileGateExcluded ? { gateExcluded: true } : {}),
|
|
271
|
+
}))
|
|
272
|
+
// Filter out violations whose severity was overridden to 'off' (Spec-11 R5)
|
|
273
|
+
.filter((v) => v.severity !== 'off');
|
|
274
|
+
// Accumulate violations
|
|
275
|
+
const ar = visitorResults.get(visitor.name);
|
|
276
|
+
ar.violations.push(...processedViolations);
|
|
277
|
+
const prevFiles = ar.status.status === 'visitor-ran' ? ar.status.filesProcessed : 0;
|
|
278
|
+
ar.status = {
|
|
279
|
+
status: 'visitor-ran',
|
|
280
|
+
filesProcessed: prevFiles + 1,
|
|
281
|
+
};
|
|
282
|
+
// Accumulate facts
|
|
283
|
+
if (result.facts && Object.keys(result.facts).length > 0) {
|
|
284
|
+
const existing = allFacts.get(visitor.name) ?? {};
|
|
285
|
+
allFacts.set(visitor.name, { ...existing, ...result.facts });
|
|
286
|
+
}
|
|
287
|
+
// Collect index facts
|
|
288
|
+
if (result.indexFacts && result.indexFacts.length > 0) {
|
|
289
|
+
indexFacts.push(...result.indexFacts);
|
|
290
|
+
}
|
|
254
291
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
catch (err) {
|
|
293
|
+
// Visitor error on this file — collect but don't abort
|
|
294
|
+
const errs = errors.get(visitor.name);
|
|
295
|
+
errs.push({ file: tuple.file, error: err.message });
|
|
258
296
|
}
|
|
259
297
|
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
// Visitor error on this file — collect but don't abort
|
|
262
|
-
const errs = errors.get(visitor.name);
|
|
263
|
-
errs.push({ file: tuple.file, error: err.message });
|
|
264
|
-
}
|
|
265
298
|
}
|
|
299
|
+
finally {
|
|
300
|
+
// Free per-file memory on every exit path (Spec 32 Fix 3): reclaim the WASM
|
|
301
|
+
// tree + drop the source string, even if a visitor or path-profile resolve
|
|
302
|
+
// throws. A leaked tree keeps the Emscripten arena pinned at its high-water
|
|
303
|
+
// mark and is the trigger for the Aborted() ceiling.
|
|
304
|
+
const ast = tuple.ast;
|
|
305
|
+
ast?.dispose?.();
|
|
306
|
+
tuple.sourceCode = '';
|
|
307
|
+
}
|
|
308
|
+
i++;
|
|
266
309
|
}
|
|
267
310
|
// Finalize results
|
|
268
311
|
const totalDuration = performance.now() - t0;
|
|
@@ -295,7 +338,7 @@ async function runStage2(tuples, visitors, config) {
|
|
|
295
338
|
allFacts,
|
|
296
339
|
indexFacts,
|
|
297
340
|
visitorDurationMs: timingMap,
|
|
298
|
-
fileCount:
|
|
341
|
+
fileCount: i,
|
|
299
342
|
};
|
|
300
343
|
}
|
|
301
344
|
// ── Stage 3: Corpus reducers ───────────────────────────────────────────────
|
|
@@ -314,24 +357,47 @@ async function runStage3(allFacts, reducers, config, indexHandle) {
|
|
|
314
357
|
}
|
|
315
358
|
// Per-reducer namespaced config: analyzer namespace + infrastructure
|
|
316
359
|
const reducerConfig = { ...(rawConfig[reducer.name] ?? {}), ...infra };
|
|
360
|
+
// On-demand source reader: reducers pull file text lazily via readFileSync
|
|
361
|
+
// instead of retaining every file's source as a fact through stage 4.
|
|
362
|
+
const readSource = (filePath) => {
|
|
363
|
+
try {
|
|
364
|
+
return readFileSync(filePath, 'utf-8');
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
317
370
|
const reducerContext = {
|
|
318
371
|
projectRoot: config.projectRoot,
|
|
319
372
|
config: reducerConfig,
|
|
320
373
|
indexHandle,
|
|
321
374
|
abortSignal: config.abortSignal,
|
|
375
|
+
readSource,
|
|
322
376
|
};
|
|
323
377
|
try {
|
|
324
378
|
const r0 = performance.now();
|
|
325
379
|
const result = await reducer.reduce(factsObj, reducerContext);
|
|
326
380
|
const rMs = performance.now() - r0;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
381
|
+
// Allow reducers to signal notRun (e.g. invariants auto-disabled when no
|
|
382
|
+
// rules are configured). The reducer sets notRunReason in its result.
|
|
383
|
+
if (result.notRunReason) {
|
|
384
|
+
reducerResults.set(reducer.name, {
|
|
385
|
+
violations: [],
|
|
386
|
+
status: { status: 'notRun', reason: result.notRunReason },
|
|
387
|
+
executionTime: rMs,
|
|
388
|
+
analyzerName: reducer.name,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
reducerResults.set(reducer.name, {
|
|
393
|
+
violations: result.violations,
|
|
394
|
+
status: { status: 'reducer-ran', factsConsumed: result.factsConsumed ?? factsConsumed },
|
|
395
|
+
executionTime: rMs,
|
|
396
|
+
analyzerName: reducer.name,
|
|
397
|
+
});
|
|
398
|
+
if (result.facts && Object.keys(result.facts).length > 0) {
|
|
399
|
+
reducerFacts.set(reducer.name, result.facts);
|
|
400
|
+
}
|
|
335
401
|
}
|
|
336
402
|
}
|
|
337
403
|
catch (err) {
|
|
@@ -377,12 +443,23 @@ async function runStage4(allFacts, derivedReducers, config, indexHandle) {
|
|
|
377
443
|
const r0 = performance.now();
|
|
378
444
|
const result = await dr.reduce(allFacts, reducerContext);
|
|
379
445
|
const rMs = performance.now() - r0;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
446
|
+
// Allow derived reducers to signal notRun (same mechanism as Stage 3)
|
|
447
|
+
if (result.notRunReason) {
|
|
448
|
+
derivedResults.set(dr.name, {
|
|
449
|
+
violations: [],
|
|
450
|
+
status: { status: 'notRun', reason: result.notRunReason },
|
|
451
|
+
executionTime: rMs,
|
|
452
|
+
analyzerName: dr.name,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
derivedResults.set(dr.name, {
|
|
457
|
+
violations: result.violations,
|
|
458
|
+
status: { status: 'reducer-ran', factsConsumed: result.factsConsumed ?? factsConsumed },
|
|
459
|
+
executionTime: rMs,
|
|
460
|
+
analyzerName: dr.name,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
386
463
|
}
|
|
387
464
|
catch (err) {
|
|
388
465
|
derivedResults.set(dr.name, {
|
|
@@ -416,26 +493,39 @@ export async function runPipeline(config, indexHandle) {
|
|
|
416
493
|
const visitors = config.visitors ?? [];
|
|
417
494
|
const reducers = config.reducers ?? [];
|
|
418
495
|
const derivedReducers = config.derivedReducers ?? [];
|
|
496
|
+
// Spec 38 R2 — per-rule timing accumulator is module-global (spans stages 2+3),
|
|
497
|
+
// so reset it at pipeline entry to avoid cross-run contamination.
|
|
498
|
+
resetRuleTiming();
|
|
419
499
|
// ── Validate facts dependencies ──────────────────────────────────────────
|
|
420
500
|
const depErrors = validateFactsDependencies(visitors, reducers, derivedReducers);
|
|
421
501
|
if (depErrors.length > 0) {
|
|
422
502
|
throw new Error(`Pipeline facts dependency errors:\n${depErrors.map((e) => ` - ${e}`).join('\n')}`);
|
|
423
503
|
}
|
|
424
|
-
// ── Stage 1:
|
|
425
|
-
const
|
|
426
|
-
stageTiming['stage1-parse'] = stage1.parseDurationMs;
|
|
504
|
+
// ── Stage 1 setup: eager file discovery, lazy parse stream ──────────────
|
|
505
|
+
const s1 = runStage1(config);
|
|
427
506
|
if (config.progressCallback) {
|
|
428
507
|
config.progressCallback({
|
|
429
|
-
current:
|
|
430
|
-
total:
|
|
508
|
+
current: 0,
|
|
509
|
+
total: s1.fileCount,
|
|
431
510
|
analyzer: 'pipeline',
|
|
432
511
|
phase: 'stage1-complete',
|
|
433
|
-
message: `${
|
|
512
|
+
message: `${s1.fileCount} files discovered`,
|
|
434
513
|
});
|
|
435
514
|
}
|
|
436
|
-
// ── Stage 2:
|
|
437
|
-
|
|
438
|
-
|
|
515
|
+
// ── Stage 2: Stream parse + per-file visitors ────────────────────────────
|
|
516
|
+
// The generator lazily reads/parses files as stage 2 consumes them.
|
|
517
|
+
// Stage 1 parse time is accumulated inside the generator closure and
|
|
518
|
+
// retrieved via getTiming() after the stream exhausts.
|
|
519
|
+
const streamT0 = performance.now();
|
|
520
|
+
const stage2 = await runStage2(s1.generator, visitors, config, s1.fileCount);
|
|
521
|
+
const { parseDurationMs, readDurationMs } = s1.getTiming();
|
|
522
|
+
// Streaming interleaves parse + visit per file, so there is no clean
|
|
523
|
+
// "stage 1 then stage 2" wall-clock split. Report honest figures instead:
|
|
524
|
+
// two CPU accumulators (measured inside the generator) plus one combined
|
|
525
|
+
// wall-clock for the whole parse+visit stream.
|
|
526
|
+
stageTiming['parse-cpu'] = parseDurationMs;
|
|
527
|
+
stageTiming['read-cpu'] = readDurationMs;
|
|
528
|
+
stageTiming['stream-parse-visit'] = performance.now() - streamT0;
|
|
439
529
|
if (config.progressCallback) {
|
|
440
530
|
config.progressCallback({
|
|
441
531
|
current: stage2.fileCount,
|
|
@@ -505,15 +595,49 @@ export async function runPipeline(config, indexHandle) {
|
|
|
505
595
|
});
|
|
506
596
|
}
|
|
507
597
|
}
|
|
598
|
+
// Spec 33 Item 14 — per-rule input presence, computed once from the merged
|
|
599
|
+
// facts + index tables, then used to promote zero-violation rules.
|
|
600
|
+
const inputPresence = computeInputPresence(combinedFacts, indexHandle);
|
|
601
|
+
// Spec 27 — build per-rule coverage from completed pipeline results
|
|
602
|
+
const coverage = buildCoverageReport(analyzerResults, config, inputPresence);
|
|
603
|
+
// Spec 29: Extract table catalog from schema reducer facts for metadata
|
|
604
|
+
const schemaFacts = combinedFacts['schema'];
|
|
605
|
+
const tableCatalog = schemaFacts?.tableCatalog;
|
|
606
|
+
// Spec 31: surface oversized orphan files skipped by stage-1 streaming.
|
|
607
|
+
// The schema-sql visitor marks them with `skipped: true`; here they are lifted
|
|
608
|
+
// into metadata so a skipped file is visible in coverage without adding a
|
|
609
|
+
// violation (which would break the exact baseline counts).
|
|
610
|
+
const schemaSqlFacts = stage2.allFacts.get('schema-sql');
|
|
611
|
+
const skippedFiles = [];
|
|
612
|
+
if (schemaSqlFacts) {
|
|
613
|
+
for (const [filePath, fact] of Object.entries(schemaSqlFacts)) {
|
|
614
|
+
const f = fact;
|
|
615
|
+
if (f.skipped) {
|
|
616
|
+
skippedFiles.push({ filePath, bytes: f.bytes ?? 0, reason: 'oversized-orphan-no-ddl' });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
508
620
|
const totalDuration = performance.now() - totalT0;
|
|
621
|
+
// Spec 32: files that failed to parse (or be read) in stage 1. Surfaced in
|
|
622
|
+
// coverage and used by the CLI to force a non-zero exit — a run that silently
|
|
623
|
+
// skipped files must never report as clean.
|
|
624
|
+
const unparsedFiles = s1.getUnparsedFiles();
|
|
625
|
+
// Spec 38 R2 — surface per-rule timing, slowest first, only when opt-in.
|
|
626
|
+
const ruleTiming = getRuleTimingSortedDesc();
|
|
509
627
|
return {
|
|
510
628
|
analyzerResults,
|
|
511
629
|
metadata: {
|
|
512
630
|
auditDuration: totalDuration,
|
|
513
|
-
filesAnalyzed:
|
|
631
|
+
filesAnalyzed: s1.fileCount,
|
|
514
632
|
stageTiming,
|
|
515
633
|
scoped: config.isScoped,
|
|
516
634
|
diagnostics,
|
|
635
|
+
coverage,
|
|
636
|
+
inputPresence,
|
|
637
|
+
tableCatalog,
|
|
638
|
+
...(skippedFiles.length > 0 && { skippedFiles }),
|
|
639
|
+
...(unparsedFiles.length > 0 && { unparsedFiles }),
|
|
640
|
+
...(ruleTiming.length > 0 && { ruleTiming }),
|
|
517
641
|
},
|
|
518
642
|
indexFacts: stage2.indexFacts,
|
|
519
643
|
};
|
|
@@ -617,4 +741,204 @@ export function isVisitorStatus(status) {
|
|
|
617
741
|
export function isReducerStatus(status) {
|
|
618
742
|
return status.status === 'reducer-ran';
|
|
619
743
|
}
|
|
744
|
+
// ── Coverage reporting (Spec 27) ────────────────────────────────────────────
|
|
745
|
+
/**
|
|
746
|
+
* Spec 33 Item 14 — snapshot which rule-input sources are present this run.
|
|
747
|
+
*
|
|
748
|
+
* - `factKeys`: visitor/reducer names whose merged facts (stage 2 + stage 3)
|
|
749
|
+
* are non-empty. A reducer reading e.g. `function-index` facts has input
|
|
750
|
+
* only when that key is present.
|
|
751
|
+
* - `indexTables`: index tables (referenced by a rule's `input`) that held
|
|
752
|
+
* ≥1 row when coverage was built. Non-existent tables are skipped.
|
|
753
|
+
*/
|
|
754
|
+
function computeInputPresence(combinedFacts, indexHandle) {
|
|
755
|
+
const factKeys = [];
|
|
756
|
+
for (const [name, facts] of Object.entries(combinedFacts)) {
|
|
757
|
+
if (facts && typeof facts === 'object' && Object.keys(facts).length > 0) {
|
|
758
|
+
factKeys.push(name);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const indexTables = [];
|
|
762
|
+
if (indexHandle) {
|
|
763
|
+
const candidates = new Set();
|
|
764
|
+
for (const entry of Object.values(RULE_REGISTRY)) {
|
|
765
|
+
for (const source of entry.input ?? []) {
|
|
766
|
+
if (source !== 'files')
|
|
767
|
+
candidates.add(source);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const table of candidates) {
|
|
771
|
+
try {
|
|
772
|
+
if (indexHandle.tableHasRows(table))
|
|
773
|
+
indexTables.push(table);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
// Table doesn't exist (e.g. a fact-key name) — not an index table.
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return { factKeys, indexTables };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Build a per-rule coverage report from completed pipeline results.
|
|
784
|
+
*
|
|
785
|
+
* Iterates every rule in the canonical {@link RULE_REGISTRY}, cross-references
|
|
786
|
+
* with the analyzer-results map to derive a {@link RuleCoverageState} per rule:
|
|
787
|
+
*
|
|
788
|
+
* | Analyzer status | Rule state | Reason |
|
|
789
|
+
* |----------------------------------------|-----------------|---------------------------------|
|
|
790
|
+
* | notRun | `notApplicable` | `notRun.reason` |
|
|
791
|
+
* | visitor-ran, filesProcessed === 0 | `notApplicable` | "no matching source files" |
|
|
792
|
+
* | reducer-ran, factsConsumed === 0 | `notApplicable` | "no facts consumed from upstream visitors" |
|
|
793
|
+
* | analyzer ran with input, count > 0 | `fired` | (none) |
|
|
794
|
+
* | analyzer ran with input, count === 0 | `clean`/`notApplicable` | per-rule input mapping |
|
|
795
|
+
*
|
|
796
|
+
* Spec 33 Item 14: zero-violation rules are promoted from `unassessed` to
|
|
797
|
+
* `clean` (mapped input present) or `notApplicable` (mapped input absent). Only
|
|
798
|
+
* rules with no `input` mapping (non-pipeline analyzers) remain `unassessed`.
|
|
799
|
+
*
|
|
800
|
+
* Rules whose analyzer was not enabled in `config` are omitted entirely
|
|
801
|
+
* (they were not part of this run — distinct from `notRun`).
|
|
802
|
+
*/
|
|
803
|
+
export function buildCoverageReport(analyzerResults, config, inputPresence) {
|
|
804
|
+
const coverage = [];
|
|
805
|
+
for (const [ruleId, entry] of Object.entries(RULE_REGISTRY)) {
|
|
806
|
+
const { analyzer: analyzerName, field } = entry;
|
|
807
|
+
// Skip rules whose analyzer wasn't configured for this run
|
|
808
|
+
if (!(analyzerName in (config.config ?? {}))) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
const result = analyzerResults[analyzerName];
|
|
812
|
+
// Analyzer not in results → notRun-equivalent
|
|
813
|
+
if (!result) {
|
|
814
|
+
coverage.push({
|
|
815
|
+
ruleId,
|
|
816
|
+
analyzer: analyzerName,
|
|
817
|
+
state: 'notApplicable',
|
|
818
|
+
count: 0,
|
|
819
|
+
reason: `analyzer "${analyzerName}" not in results`,
|
|
820
|
+
});
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const status = result.status;
|
|
824
|
+
// notRun: all rules notApplicable
|
|
825
|
+
if (status.status !== 'visitor-ran' && status.status !== 'reducer-ran') {
|
|
826
|
+
const reason = status.reason;
|
|
827
|
+
coverage.push({
|
|
828
|
+
ruleId,
|
|
829
|
+
analyzer: analyzerName,
|
|
830
|
+
state: 'notApplicable',
|
|
831
|
+
count: 0,
|
|
832
|
+
reason,
|
|
833
|
+
});
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
// Visitor with zero input files
|
|
837
|
+
if (isVisitorStatus(status) && getFilesProcessed(status) === 0) {
|
|
838
|
+
coverage.push({
|
|
839
|
+
ruleId,
|
|
840
|
+
analyzer: analyzerName,
|
|
841
|
+
state: 'notApplicable',
|
|
842
|
+
count: 0,
|
|
843
|
+
reason: 'no matching source files',
|
|
844
|
+
});
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
// Reducer with zero facts consumed
|
|
848
|
+
if (isReducerStatus(status) && getFactsConsumed(status) === 0) {
|
|
849
|
+
coverage.push({
|
|
850
|
+
ruleId,
|
|
851
|
+
analyzer: analyzerName,
|
|
852
|
+
state: 'notApplicable',
|
|
853
|
+
count: 0,
|
|
854
|
+
reason: 'no facts consumed from upstream visitors',
|
|
855
|
+
});
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
// Check per-rule config gate (explicitly disabled by config)
|
|
859
|
+
if (entry.configGate) {
|
|
860
|
+
const analyzerNs = (config.config ?? {})[analyzerName];
|
|
861
|
+
const gateValue = analyzerNs?.[entry.configGate];
|
|
862
|
+
if (gateValue === false) {
|
|
863
|
+
coverage.push({
|
|
864
|
+
ruleId,
|
|
865
|
+
analyzer: analyzerName,
|
|
866
|
+
state: 'notApplicable',
|
|
867
|
+
count: 0,
|
|
868
|
+
reason: `disabled by config (${analyzerName}.${entry.configGate}: false)`,
|
|
869
|
+
});
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// Analyzer ran with input — count violations for this rule
|
|
874
|
+
const violations = (result.violations ?? []).filter((v) => {
|
|
875
|
+
if (field === 'type')
|
|
876
|
+
return v.type === ruleId;
|
|
877
|
+
if (field === 'contractType')
|
|
878
|
+
return v.contractType === ruleId;
|
|
879
|
+
if (field === 'principle')
|
|
880
|
+
return v.principle === ruleId;
|
|
881
|
+
if (field === 'violationType')
|
|
882
|
+
return v.violationType === ruleId;
|
|
883
|
+
if (field === 'ruleId')
|
|
884
|
+
return v.ruleId === ruleId;
|
|
885
|
+
return v.rule === ruleId;
|
|
886
|
+
});
|
|
887
|
+
const count = violations.length;
|
|
888
|
+
if (count > 0) {
|
|
889
|
+
coverage.push({
|
|
890
|
+
ruleId,
|
|
891
|
+
analyzer: analyzerName,
|
|
892
|
+
state: 'fired',
|
|
893
|
+
count,
|
|
894
|
+
});
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
// Spec 33 Item 14 — zero-violation rules are now promoted from `unassessed`
|
|
898
|
+
// to `clean`/`notApplicable` based on whether the rule's mapped input was
|
|
899
|
+
// present this run. Rules with no mapping (non-pipeline analyzers) stay
|
|
900
|
+
// `unassessed`.
|
|
901
|
+
coverage.push(resolveZeroViolationState(ruleId, analyzerName, entry.input, inputPresence));
|
|
902
|
+
}
|
|
903
|
+
return coverage;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Spec 33 Item 14 — classify a zero-violation rule by its declared input.
|
|
907
|
+
*
|
|
908
|
+
* - No `input` mapping → `unassessed` (input provenance unknown; non-pipeline
|
|
909
|
+
* analyzers only).
|
|
910
|
+
* - Any input source present → `clean` (the analyzer ran over the rule's real
|
|
911
|
+
* input and found nothing to flag).
|
|
912
|
+
* - All input sources absent → `notApplicable` (the input this rule reads was
|
|
913
|
+
* never produced this run).
|
|
914
|
+
*
|
|
915
|
+
* An input source is "present" when it is the literal `'files'` (always present
|
|
916
|
+
* at this branch — earlier checks already excluded empty-input analyzers), a
|
|
917
|
+
* fact-key in {@link InputPresence.factKeys}, or an index table in
|
|
918
|
+
* {@link InputPresence.indexTables}.
|
|
919
|
+
*/
|
|
920
|
+
function resolveZeroViolationState(ruleId, analyzerName, input, inputPresence) {
|
|
921
|
+
if (!input || input.length === 0) {
|
|
922
|
+
return {
|
|
923
|
+
ruleId,
|
|
924
|
+
analyzer: analyzerName,
|
|
925
|
+
state: 'unassessed',
|
|
926
|
+
count: 0,
|
|
927
|
+
reason: 'applicability not assessed (no per-rule input mapping)',
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const factKeys = new Set(inputPresence?.factKeys ?? []);
|
|
931
|
+
const indexTables = new Set(inputPresence?.indexTables ?? []);
|
|
932
|
+
const anyPresent = input.some((source) => source === 'files' || factKeys.has(source) || indexTables.has(source));
|
|
933
|
+
if (anyPresent) {
|
|
934
|
+
return { ruleId, analyzer: analyzerName, state: 'clean', count: 0 };
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
ruleId,
|
|
938
|
+
analyzer: analyzerName,
|
|
939
|
+
state: 'notApplicable',
|
|
940
|
+
count: 0,
|
|
941
|
+
reason: `rule input absent (none of: ${input.join(', ')})`,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
620
944
|
//# sourceMappingURL=pipeline.js.map
|