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.
Files changed (245) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  3. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  4. package/dist/analyzers/__verify_self_probe.js +5 -0
  5. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  6. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  7. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  8. package/dist/analyzers/analyzerUtils.js +61 -22
  9. package/dist/analyzers/analyzerUtils.js.map +1 -1
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  18. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  20. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  21. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  26. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  27. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/documentationAnalyzer.js +259 -198
  29. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  30. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.js +57 -50
  32. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  33. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  34. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  35. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  36. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  37. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  39. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  40. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  41. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  42. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  43. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  44. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  45. package/dist/analyzers/provenance.d.ts +62 -2
  46. package/dist/analyzers/provenance.d.ts.map +1 -1
  47. package/dist/analyzers/provenance.js +443 -349
  48. package/dist/analyzers/provenance.js.map +1 -1
  49. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  50. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  51. package/dist/analyzers/reactAnalyzer.js +188 -125
  52. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  53. package/dist/analyzers/ruleRegistry.d.ts +110 -1
  54. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  55. package/dist/analyzers/ruleRegistry.js +1714 -124
  56. package/dist/analyzers/ruleRegistry.js.map +1 -1
  57. package/dist/analyzers/ruleTiming.d.ts +54 -0
  58. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  59. package/dist/analyzers/ruleTiming.js +71 -0
  60. package/dist/analyzers/ruleTiming.js.map +1 -0
  61. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  65. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  69. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +17 -202
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  73. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  77. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  81. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -214
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  85. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -480
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  89. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  93. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  94. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  95. package/dist/analyzers/universal/schema/config.js +57 -0
  96. package/dist/analyzers/universal/schema/config.js.map +1 -0
  97. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  99. package/dist/analyzers/universal/schema/discovery.js +616 -0
  100. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  101. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  105. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  107. package/dist/analyzers/universal/schema/migrations.js +238 -0
  108. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  109. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  110. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  111. package/dist/analyzers/universal/schema/types.js +10 -0
  112. package/dist/analyzers/universal/schema/types.js.map +1 -0
  113. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  115. package/dist/analyzers/universal/schema/violations.js +54 -0
  116. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  117. package/dist/auditRunner.d.ts.map +1 -1
  118. package/dist/auditRunner.js +89 -13
  119. package/dist/auditRunner.js.map +1 -1
  120. package/dist/cli.js +315 -40
  121. package/dist/cli.js.map +1 -1
  122. package/dist/config/configLoader.d.ts.map +1 -1
  123. package/dist/config/configLoader.js +9 -7
  124. package/dist/config/configLoader.js.map +1 -1
  125. package/dist/config/defaults.d.ts +12 -3
  126. package/dist/config/defaults.d.ts.map +1 -1
  127. package/dist/config/defaults.js +15 -5
  128. package/dist/config/defaults.js.map +1 -1
  129. package/dist/config/effectiveConfig.d.ts +75 -0
  130. package/dist/config/effectiveConfig.d.ts.map +1 -0
  131. package/dist/config/effectiveConfig.js +213 -0
  132. package/dist/config/effectiveConfig.js.map +1 -0
  133. package/dist/config/pathProfiles.d.ts +7 -4
  134. package/dist/config/pathProfiles.d.ts.map +1 -1
  135. package/dist/config/pathProfiles.js +8 -10
  136. package/dist/config/pathProfiles.js.map +1 -1
  137. package/dist/config/thresholdRationales.d.ts +45 -0
  138. package/dist/config/thresholdRationales.d.ts.map +1 -0
  139. package/dist/config/thresholdRationales.js +67 -0
  140. package/dist/config/thresholdRationales.js.map +1 -0
  141. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  142. package/dist/conventions/conventionMiner.js +6 -2
  143. package/dist/conventions/conventionMiner.js.map +1 -1
  144. package/dist/enforcement/diffGate.d.ts +55 -0
  145. package/dist/enforcement/diffGate.d.ts.map +1 -0
  146. package/dist/enforcement/diffGate.js +136 -0
  147. package/dist/enforcement/diffGate.js.map +1 -0
  148. package/dist/enforcement/gate.d.ts +36 -0
  149. package/dist/enforcement/gate.d.ts.map +1 -0
  150. package/dist/enforcement/gate.js +72 -0
  151. package/dist/enforcement/gate.js.map +1 -0
  152. package/dist/enforcement/suppressions.d.ts +41 -0
  153. package/dist/enforcement/suppressions.d.ts.map +1 -0
  154. package/dist/enforcement/suppressions.js +170 -0
  155. package/dist/enforcement/suppressions.js.map +1 -0
  156. package/dist/fingerprint.d.ts.map +1 -1
  157. package/dist/fingerprint.js +4 -1
  158. package/dist/fingerprint.js.map +1 -1
  159. package/dist/invariants/ruleEngine.d.ts +3 -4
  160. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  161. package/dist/invariants/ruleEngine.js +2 -2
  162. package/dist/invariants/ruleEngine.js.map +1 -1
  163. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  164. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.js +93 -55
  166. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  167. package/dist/languages/LanguageRegistry.d.ts +8 -0
  168. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  169. package/dist/languages/LanguageRegistry.js +8 -0
  170. package/dist/languages/LanguageRegistry.js.map +1 -1
  171. package/dist/languages/RuntimeManager.d.ts +82 -60
  172. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  173. package/dist/languages/RuntimeManager.js +404 -349
  174. package/dist/languages/RuntimeManager.js.map +1 -1
  175. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  176. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.js +127 -88
  178. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  179. package/dist/languages/adapterBridge.d.ts +36 -0
  180. package/dist/languages/adapterBridge.d.ts.map +1 -1
  181. package/dist/languages/adapterBridge.js +89 -35
  182. package/dist/languages/adapterBridge.js.map +1 -1
  183. package/dist/languages/go/GoAdapter.d.ts +66 -23
  184. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  185. package/dist/languages/go/GoAdapter.js +553 -474
  186. package/dist/languages/go/GoAdapter.js.map +1 -1
  187. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +33 -4
  188. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  191. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  192. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  193. package/dist/languages/tree-sitter/converter.js +18 -0
  194. package/dist/languages/tree-sitter/converter.js.map +1 -1
  195. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  196. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  197. package/dist/languages/tree-sitter/parser.js +93 -12
  198. package/dist/languages/tree-sitter/parser.js.map +1 -1
  199. package/dist/languages/types.d.ts +67 -2
  200. package/dist/languages/types.d.ts.map +1 -1
  201. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -72
  202. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  205. package/dist/mcp-tools-shared.d.ts.map +1 -1
  206. package/dist/mcp-tools-shared.js.map +1 -1
  207. package/dist/pipeline.d.ts +13 -5
  208. package/dist/pipeline.d.ts.map +1 -1
  209. package/dist/pipeline.js +381 -200
  210. package/dist/pipeline.js.map +1 -1
  211. package/dist/pipelineAdapters.d.ts +3 -8
  212. package/dist/pipelineAdapters.d.ts.map +1 -1
  213. package/dist/pipelineAdapters.js +149 -94
  214. package/dist/pipelineAdapters.js.map +1 -1
  215. package/dist/presets/presets.d.ts +54 -0
  216. package/dist/presets/presets.d.ts.map +1 -0
  217. package/dist/presets/presets.js +181 -0
  218. package/dist/presets/presets.js.map +1 -0
  219. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  220. package/dist/reporting/sarifReportGenerator.js +0 -2
  221. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  222. package/dist/ruleAliases.d.ts +67 -0
  223. package/dist/ruleAliases.d.ts.map +1 -0
  224. package/dist/ruleAliases.js +79 -0
  225. package/dist/ruleAliases.js.map +1 -0
  226. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  227. package/dist/styles/cssAstExtractor.js +78 -18
  228. package/dist/styles/cssAstExtractor.js.map +1 -1
  229. package/dist/styles/styleIndexer.js +10 -1
  230. package/dist/styles/styleIndexer.js.map +1 -1
  231. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  232. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  233. package/dist/styles/tailwindConfigLoader.js +61 -15
  234. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  235. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  236. package/dist/styles/tailwindUtilityExpander.js +13 -1
  237. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  238. package/dist/types.d.ts +181 -1
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +8 -0
  241. package/dist/types.js.map +1 -1
  242. package/package.json +5 -3
  243. package/plugin/hooks/hooks.json +1 -1
  244. package/plugin/scripts/hook-audit.sh +25 -7
  245. 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
- async function runStage1(config) {
23
- const t0 = performance.now();
24
- let readMs = 0;
25
- let parseMs = 0;
26
- // Discover files (or use explicit)
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
- files = await discoverFiles(projectRoot, {
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
- const groups = new Map();
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
- for (const [adapter, adapterFiles] of groups) {
56
- for (const file of adapterFiles) {
57
- // Check abort
58
- if (config.abortSignal?.aborted) {
59
- throw new AuditAbortedError('Audit aborted during stage 1');
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
- const p0 = performance.now();
66
- const ast = await adapter.parse(file, content);
67
- parseMs += performance.now() - p0;
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
- // File read/parse errors skip this file
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: parsed,
83
- total: files.length,
150
+ current: orphans.indexOf(file),
151
+ total: orphans.length,
84
152
  analyzer: 'pipeline',
85
153
  phase: 'stage1',
86
154
  file,
87
- message: `Skipped: ${err.message}`,
155
+ message: `Raw file skipped (read error): ${err.message}`,
88
156
  });
89
157
  }
90
158
  }
91
159
  }
92
- }
93
- // Include orphan files as raw tuples — no LanguageAdapter available,
94
- // but visitors that consume raw files can read sourceCode directly.
95
- for (const file of orphans) {
96
- try {
97
- const r0 = performance.now();
98
- const content = await readFile(file, 'utf-8');
99
- readMs += performance.now() - r0;
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
- tuples,
134
- fileCount: tuples.length,
135
- parseDurationMs: duration,
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
- // Iterate tuples
165
- for (let i = 0; i < tuples.length; i++) {
166
- const tuple = tuples[i];
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
- // Progress
172
- if (config.progressCallback && i % 10 === 0) {
173
- config.progressCallback({
174
- current: i,
175
- total: tuples.length,
176
- analyzer: 'pipeline',
177
- phase: 'stage2',
178
- file: tuple.file,
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
- else {
203
- // No extensions declared → parsed tuples only (backward compat)
204
- if (tuple.kind !== 'parsed')
205
- continue;
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
- const visitorConfig = { ...(rawConfig[visitor.name] ?? {}), ...fileInfra };
208
- const visitorContext = {
209
- projectRoot,
210
- filePath: tuple.file,
211
- config: visitorConfig,
212
- abortSignal: config.abortSignal,
213
- };
214
- try {
215
- const v0 = performance.now();
216
- const result = await visitor.visit(tuple.ast, tuple.adapter, visitorContext, tuple.sourceCode);
217
- const vMs = performance.now() - v0;
218
- // Accumulate timing
219
- timingMap.set(visitor.name, (timingMap.get(visitor.name) ?? 0) + vMs);
220
- // Attach profile, severity overrides, analyzer name
221
- const severityOrder = ['suggestion', 'warning', 'critical'];
222
- const processedViolations = result.violations
223
- .map((v) => ({
224
- ...v,
225
- profile: fileProfileNames.length > 0
226
- ? fileProfileNames[fileProfileNames.length - 1]
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
- // Accumulate facts
252
- if (result.facts && Object.keys(result.facts).length > 0) {
253
- const existing = allFacts.get(visitor.name) ?? {};
254
- allFacts.set(visitor.name, { ...existing, ...result.facts });
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
- // Collect index facts
257
- if (result.indexFacts && result.indexFacts.length > 0) {
258
- indexFacts.push(...result.indexFacts);
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: tuples.length,
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: Traverse + Parse ────────────────────────────────────────────
449
- const stage1 = await runStage1(config);
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: stage1.fileCount,
454
- total: stage1.fileCount,
508
+ current: 0,
509
+ total: s1.fileCount,
455
510
  analyzer: 'pipeline',
456
511
  phase: 'stage1-complete',
457
- message: `${stage1.fileCount} files parsed in ${stage1.parseDurationMs.toFixed(0)}ms`,
512
+ message: `${s1.fileCount} files discovered`,
458
513
  });
459
514
  }
460
- // ── Stage 2: Per-file visitors ───────────────────────────────────────────
461
- const stage2 = await runStage2(stage1.tuples, visitors, config);
462
- stageTiming['stage2-visitors'] = performance.now() - totalT0 - stage1.parseDurationMs;
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: stage1.fileCount,
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 | `unassessed` | per-rule input mapping NYI |
794
+ * | analyzer ran with input, count === 0 | `clean`/`notApplicable` | per-rule input mapping |
661
795
  *
662
- * `clean` is never emitted in v1 it requires per-rule input mapping to
663
- * confirm that a rule's specific input was present and checked.
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
- coverage.push({
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: count > 0 ? 'fired' : 'unassessed',
757
- count,
758
- reason: count === 0 ? 'applicability not assessed (per-rule input mapping NYI)' : undefined,
759
- });
925
+ state: 'unassessed',
926
+ count: 0,
927
+ reason: 'applicability not assessed (no per-rule input mapping)',
928
+ };
760
929
  }
761
- return coverage;
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