code-auditor-mcp 3.4.12 → 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/CHANGELOG.md +46 -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 +57 -50
- 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 +62 -2
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +443 -349
- 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 +110 -1
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +1714 -124
- 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 +17 -202
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
- 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 -214
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
- 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 -480
- 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 +89 -13
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +315 -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/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 +33 -4
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
- 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 +93 -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 -72
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/pipeline.d.ts +13 -5
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +381 -200
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +3 -8
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +149 -94
- 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.map +1 -1
- package/dist/styles/cssAstExtractor.js +78 -18
- package/dist/styles/cssAstExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.js +10 -1
- 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 +181 -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 +5 -3
- 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,135 +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';
|
|
16
17
|
import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
|
|
18
|
+
import { resetRuleTiming, getRuleTimingSortedDesc } from './analyzers/ruleTiming.js';
|
|
17
19
|
import { LanguageRegistry } from './languages/LanguageRegistry.js';
|
|
18
20
|
import { discoverFiles } from './utils/fileDiscovery.js';
|
|
19
21
|
import { resolvePathProfile } from './config/pathProfiles.js';
|
|
20
22
|
import { validateFactsDependencies } from './pipelineTypes.js';
|
|
21
23
|
// ── Stage 1: Traverse + Parse ──────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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) ─────────────────────────────────
|
|
27
36
|
const projectRoot = config.projectRoot;
|
|
28
37
|
let files;
|
|
29
38
|
if (config.explicitFiles !== undefined) {
|
|
30
39
|
files = config.explicitFiles;
|
|
31
40
|
}
|
|
32
41
|
else {
|
|
33
|
-
|
|
34
|
-
excludeDirs: ['node_modules', '.next', 'dist', 'build', '.git', 'coverage', '.turbo'],
|
|
35
|
-
});
|
|
42
|
+
// discovery runs lazily inside the generator below
|
|
36
43
|
}
|
|
37
44
|
// Group by language adapter
|
|
38
45
|
const registry = LanguageRegistry.getInstance();
|
|
39
|
-
|
|
40
|
-
const orphans = [];
|
|
41
|
-
for (const file of files) {
|
|
42
|
-
const adapter = registry.getAdapterForFile(file);
|
|
43
|
-
if (adapter) {
|
|
44
|
-
const list = groups.get(adapter) ?? [];
|
|
45
|
-
list.push(file);
|
|
46
|
-
groups.set(adapter, list);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
orphans.push(file);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// Parse each file once
|
|
53
|
-
const tuples = [];
|
|
46
|
+
let total;
|
|
54
47
|
let parsed = 0;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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);
|
|
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
|
+
}
|
|
60
111
|
}
|
|
112
|
+
}
|
|
113
|
+
// Orphan files (no LanguageAdapter)
|
|
114
|
+
for (const file of orphans) {
|
|
61
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
|
+
}
|
|
62
134
|
const r0 = performance.now();
|
|
63
135
|
const content = await readFile(file, 'utf-8');
|
|
64
136
|
readMs += performance.now() - r0;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
tuples.push({
|
|
69
|
-
kind: 'parsed',
|
|
137
|
+
parsed++;
|
|
138
|
+
yield {
|
|
139
|
+
kind: 'raw',
|
|
70
140
|
file,
|
|
71
|
-
ast,
|
|
72
|
-
adapter,
|
|
141
|
+
ast: null,
|
|
142
|
+
adapter: null,
|
|
73
143
|
sourceCode: content,
|
|
74
|
-
}
|
|
75
|
-
parsed++;
|
|
144
|
+
};
|
|
76
145
|
}
|
|
77
146
|
catch (err) {
|
|
78
|
-
|
|
79
|
-
// Individual files that fail to parse don't abort the entire run
|
|
147
|
+
unparsedFiles.push({ filePath: file, reason: `read error: ${err?.message ?? String(err)}` });
|
|
80
148
|
if (config.progressCallback) {
|
|
81
149
|
config.progressCallback({
|
|
82
|
-
current:
|
|
83
|
-
total:
|
|
150
|
+
current: orphans.indexOf(file),
|
|
151
|
+
total: orphans.length,
|
|
84
152
|
analyzer: 'pipeline',
|
|
85
153
|
phase: 'stage1',
|
|
86
154
|
file,
|
|
87
|
-
message: `
|
|
155
|
+
message: `Raw file skipped (read error): ${err.message}`,
|
|
88
156
|
});
|
|
89
157
|
}
|
|
90
158
|
}
|
|
91
159
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
tuples.push({
|
|
101
|
-
kind: 'raw',
|
|
102
|
-
file,
|
|
103
|
-
ast: null,
|
|
104
|
-
adapter: null,
|
|
105
|
-
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)`,
|
|
106
168
|
});
|
|
107
169
|
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
if (config.progressCallback) {
|
|
110
|
-
config.progressCallback({
|
|
111
|
-
current: orphans.indexOf(file),
|
|
112
|
-
total: orphans.length,
|
|
113
|
-
analyzer: 'pipeline',
|
|
114
|
-
phase: 'stage1',
|
|
115
|
-
file,
|
|
116
|
-
message: `Raw file skipped (read error): ${err.message}`,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const orphanIncluded = tuples.filter(t => t.kind === 'raw').length;
|
|
122
|
-
if (orphanIncluded > 0 && config.progressCallback) {
|
|
123
|
-
config.progressCallback({
|
|
124
|
-
current: orphanIncluded,
|
|
125
|
-
total: orphanIncluded,
|
|
126
|
-
analyzer: 'pipeline',
|
|
127
|
-
phase: 'stage1',
|
|
128
|
-
message: `${orphanIncluded} file(s) included as raw (no language adapter)`,
|
|
129
|
-
});
|
|
130
170
|
}
|
|
131
|
-
const duration = performance.now() - t0;
|
|
132
171
|
return {
|
|
133
|
-
|
|
134
|
-
fileCount
|
|
135
|
-
|
|
136
|
-
readDurationMs: readMs,
|
|
172
|
+
generator: generate(),
|
|
173
|
+
get fileCount() { return total; },
|
|
174
|
+
getUnparsedFiles: () => unparsedFiles,
|
|
175
|
+
getTiming: () => ({ parseDurationMs: parseMs, readDurationMs: readMs }),
|
|
137
176
|
};
|
|
138
177
|
}
|
|
139
178
|
// ── Stage 2: Per-file visitors ─────────────────────────────────────────────
|
|
140
|
-
async function runStage2(tuples, visitors, config) {
|
|
179
|
+
export async function runStage2(tuples, visitors, config, totalFiles) {
|
|
141
180
|
const t0 = performance.now();
|
|
142
181
|
const visitorResults = new Map();
|
|
143
182
|
const allFacts = new Map();
|
|
@@ -161,109 +200,112 @@ async function runStage2(tuples, visitors, config) {
|
|
|
161
200
|
const infra = rawConfig['_infra'] ?? {};
|
|
162
201
|
const pathProfiles = infra['pathProfiles'];
|
|
163
202
|
const severityOverrides = infra['severityOverrides'] ?? {};
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
// Stream tuples from stage 1
|
|
204
|
+
let i = 0;
|
|
205
|
+
for await (const tuple of tuples) {
|
|
167
206
|
// Check abort
|
|
168
207
|
if (config.abortSignal?.aborted) {
|
|
169
208
|
throw new AuditAbortedError('Audit aborted during stage 2');
|
|
170
209
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
config.progressCallback
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Resolve path profiles for this file (non-analyzer-specific)
|
|
182
|
-
let fileInfra = infra;
|
|
183
|
-
let fileProfileNames = [];
|
|
184
|
-
let fileSeverityCap;
|
|
185
|
-
if (pathProfiles && pathProfiles.length > 0) {
|
|
186
|
-
const resolved = resolvePathProfile(tuple.file, projectRoot, pathProfiles);
|
|
187
|
-
if (Object.keys(resolved.overrides).length > 0) {
|
|
188
|
-
fileInfra = { ...infra, ...resolved.overrides };
|
|
189
|
-
}
|
|
190
|
-
fileProfileNames = resolved.matchedProfileNames;
|
|
191
|
-
fileSeverityCap = resolved.severityCap;
|
|
192
|
-
}
|
|
193
|
-
// Fan out to all visitors for this file — filter by declared extensions
|
|
194
|
-
const fileExt = path.extname(tuple.file);
|
|
195
|
-
for (const visitor of visitors) {
|
|
196
|
-
// Dispatch check: backward-compat visitors see only parsed tuples.
|
|
197
|
-
// Visitors that declare extensions see only tuples whose extension they consume.
|
|
198
|
-
if (visitor.extensions) {
|
|
199
|
-
if (!visitor.extensions.includes(fileExt))
|
|
200
|
-
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
|
+
});
|
|
201
220
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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;
|
|
206
232
|
}
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
: v.profile,
|
|
228
|
-
severity: (severityOverrides[v.rule] ?? v.severity),
|
|
229
|
-
}))
|
|
230
|
-
// Filter out violations whose severity was overridden to 'off' (Spec-11 R5)
|
|
231
|
-
.filter((v) => v.severity !== 'off')
|
|
232
|
-
// Apply severity cap from path profiles (Spec-20)
|
|
233
|
-
// Applied AFTER severityOverrides so path-level caps beat global promotions
|
|
234
|
-
.map((v) => {
|
|
235
|
-
if (fileSeverityCap) {
|
|
236
|
-
const capIndex = severityOrder.indexOf(fileSeverityCap);
|
|
237
|
-
if (capIndex >= 0 && severityOrder.indexOf(v.severity) > capIndex) {
|
|
238
|
-
return { ...v, severity: fileSeverityCap };
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return v;
|
|
242
|
-
});
|
|
243
|
-
// Accumulate violations
|
|
244
|
-
const ar = visitorResults.get(visitor.name);
|
|
245
|
-
ar.violations.push(...processedViolations);
|
|
246
|
-
const prevFiles = ar.status.status === 'visitor-ran' ? ar.status.filesProcessed : 0;
|
|
247
|
-
ar.status = {
|
|
248
|
-
status: 'visitor-ran',
|
|
249
|
-
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,
|
|
250
253
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
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
|
+
}
|
|
255
291
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 });
|
|
259
296
|
}
|
|
260
297
|
}
|
|
261
|
-
catch (err) {
|
|
262
|
-
// Visitor error on this file — collect but don't abort
|
|
263
|
-
const errs = errors.get(visitor.name);
|
|
264
|
-
errs.push({ file: tuple.file, error: err.message });
|
|
265
|
-
}
|
|
266
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++;
|
|
267
309
|
}
|
|
268
310
|
// Finalize results
|
|
269
311
|
const totalDuration = performance.now() - t0;
|
|
@@ -296,7 +338,7 @@ async function runStage2(tuples, visitors, config) {
|
|
|
296
338
|
allFacts,
|
|
297
339
|
indexFacts,
|
|
298
340
|
visitorDurationMs: timingMap,
|
|
299
|
-
fileCount:
|
|
341
|
+
fileCount: i,
|
|
300
342
|
};
|
|
301
343
|
}
|
|
302
344
|
// ── Stage 3: Corpus reducers ───────────────────────────────────────────────
|
|
@@ -315,11 +357,22 @@ async function runStage3(allFacts, reducers, config, indexHandle) {
|
|
|
315
357
|
}
|
|
316
358
|
// Per-reducer namespaced config: analyzer namespace + infrastructure
|
|
317
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
|
+
};
|
|
318
370
|
const reducerContext = {
|
|
319
371
|
projectRoot: config.projectRoot,
|
|
320
372
|
config: reducerConfig,
|
|
321
373
|
indexHandle,
|
|
322
374
|
abortSignal: config.abortSignal,
|
|
375
|
+
readSource,
|
|
323
376
|
};
|
|
324
377
|
try {
|
|
325
378
|
const r0 = performance.now();
|
|
@@ -440,26 +493,39 @@ export async function runPipeline(config, indexHandle) {
|
|
|
440
493
|
const visitors = config.visitors ?? [];
|
|
441
494
|
const reducers = config.reducers ?? [];
|
|
442
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();
|
|
443
499
|
// ── Validate facts dependencies ──────────────────────────────────────────
|
|
444
500
|
const depErrors = validateFactsDependencies(visitors, reducers, derivedReducers);
|
|
445
501
|
if (depErrors.length > 0) {
|
|
446
502
|
throw new Error(`Pipeline facts dependency errors:\n${depErrors.map((e) => ` - ${e}`).join('\n')}`);
|
|
447
503
|
}
|
|
448
|
-
// ── Stage 1:
|
|
449
|
-
const
|
|
450
|
-
stageTiming['stage1-parse'] = stage1.parseDurationMs;
|
|
504
|
+
// ── Stage 1 setup: eager file discovery, lazy parse stream ──────────────
|
|
505
|
+
const s1 = runStage1(config);
|
|
451
506
|
if (config.progressCallback) {
|
|
452
507
|
config.progressCallback({
|
|
453
|
-
current:
|
|
454
|
-
total:
|
|
508
|
+
current: 0,
|
|
509
|
+
total: s1.fileCount,
|
|
455
510
|
analyzer: 'pipeline',
|
|
456
511
|
phase: 'stage1-complete',
|
|
457
|
-
message: `${
|
|
512
|
+
message: `${s1.fileCount} files discovered`,
|
|
458
513
|
});
|
|
459
514
|
}
|
|
460
|
-
// ── Stage 2:
|
|
461
|
-
|
|
462
|
-
|
|
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;
|
|
463
529
|
if (config.progressCallback) {
|
|
464
530
|
config.progressCallback({
|
|
465
531
|
current: stage2.fileCount,
|
|
@@ -529,18 +595,49 @@ export async function runPipeline(config, indexHandle) {
|
|
|
529
595
|
});
|
|
530
596
|
}
|
|
531
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);
|
|
532
601
|
// Spec 27 — build per-rule coverage from completed pipeline results
|
|
533
|
-
const coverage = buildCoverageReport(analyzerResults, config);
|
|
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
|
+
}
|
|
534
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();
|
|
535
627
|
return {
|
|
536
628
|
analyzerResults,
|
|
537
629
|
metadata: {
|
|
538
630
|
auditDuration: totalDuration,
|
|
539
|
-
filesAnalyzed:
|
|
631
|
+
filesAnalyzed: s1.fileCount,
|
|
540
632
|
stageTiming,
|
|
541
633
|
scoped: config.isScoped,
|
|
542
634
|
diagnostics,
|
|
543
635
|
coverage,
|
|
636
|
+
inputPresence,
|
|
637
|
+
tableCatalog,
|
|
638
|
+
...(skippedFiles.length > 0 && { skippedFiles }),
|
|
639
|
+
...(unparsedFiles.length > 0 && { unparsedFiles }),
|
|
640
|
+
...(ruleTiming.length > 0 && { ruleTiming }),
|
|
544
641
|
},
|
|
545
642
|
indexFacts: stage2.indexFacts,
|
|
546
643
|
};
|
|
@@ -645,6 +742,43 @@ export function isReducerStatus(status) {
|
|
|
645
742
|
return status.status === 'reducer-ran';
|
|
646
743
|
}
|
|
647
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
|
+
}
|
|
648
782
|
/**
|
|
649
783
|
* Build a per-rule coverage report from completed pipeline results.
|
|
650
784
|
*
|
|
@@ -657,15 +791,16 @@ export function isReducerStatus(status) {
|
|
|
657
791
|
* | visitor-ran, filesProcessed === 0 | `notApplicable` | "no matching source files" |
|
|
658
792
|
* | reducer-ran, factsConsumed === 0 | `notApplicable` | "no facts consumed from upstream visitors" |
|
|
659
793
|
* | analyzer ran with input, count > 0 | `fired` | (none) |
|
|
660
|
-
* | analyzer ran with input, count === 0 | `
|
|
794
|
+
* | analyzer ran with input, count === 0 | `clean`/`notApplicable` | per-rule input mapping |
|
|
661
795
|
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
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`.
|
|
664
799
|
*
|
|
665
800
|
* Rules whose analyzer was not enabled in `config` are omitted entirely
|
|
666
801
|
* (they were not part of this run — distinct from `notRun`).
|
|
667
802
|
*/
|
|
668
|
-
export function buildCoverageReport(analyzerResults, config) {
|
|
803
|
+
export function buildCoverageReport(analyzerResults, config, inputPresence) {
|
|
669
804
|
const coverage = [];
|
|
670
805
|
for (const [ruleId, entry] of Object.entries(RULE_REGISTRY)) {
|
|
671
806
|
const { analyzer: analyzerName, field } = entry;
|
|
@@ -750,14 +885,60 @@ export function buildCoverageReport(analyzerResults, config) {
|
|
|
750
885
|
return v.rule === ruleId;
|
|
751
886
|
});
|
|
752
887
|
const count = violations.length;
|
|
753
|
-
|
|
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 {
|
|
754
923
|
ruleId,
|
|
755
924
|
analyzer: analyzerName,
|
|
756
|
-
state:
|
|
757
|
-
count,
|
|
758
|
-
reason:
|
|
759
|
-
}
|
|
925
|
+
state: 'unassessed',
|
|
926
|
+
count: 0,
|
|
927
|
+
reason: 'applicability not assessed (no per-rule input mapping)',
|
|
928
|
+
};
|
|
760
929
|
}
|
|
761
|
-
|
|
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
|
+
};
|
|
762
943
|
}
|
|
763
944
|
//# sourceMappingURL=pipeline.js.map
|