@kernlang/review 3.2.3 → 3.3.5

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 (92) hide show
  1. package/dist/cache.js +140 -3
  2. package/dist/cache.js.map +1 -1
  3. package/dist/call-graph.d.ts +4 -1
  4. package/dist/call-graph.js +290 -25
  5. package/dist/call-graph.js.map +1 -1
  6. package/dist/concept-rules/contract-drift.d.ts +21 -0
  7. package/dist/concept-rules/contract-drift.js +66 -0
  8. package/dist/concept-rules/contract-drift.js.map +1 -0
  9. package/dist/concept-rules/cross-stack-utils.d.ts +50 -0
  10. package/dist/concept-rules/cross-stack-utils.js +98 -0
  11. package/dist/concept-rules/cross-stack-utils.js.map +1 -0
  12. package/dist/concept-rules/index.js +12 -1
  13. package/dist/concept-rules/index.js.map +1 -1
  14. package/dist/concept-rules/tainted-across-wire.d.ts +33 -0
  15. package/dist/concept-rules/tainted-across-wire.js +98 -0
  16. package/dist/concept-rules/tainted-across-wire.js.map +1 -0
  17. package/dist/concept-rules/untyped-api-response.d.ts +30 -0
  18. package/dist/concept-rules/untyped-api-response.js +71 -0
  19. package/dist/concept-rules/untyped-api-response.js.map +1 -0
  20. package/dist/external-tools.d.ts +36 -4
  21. package/dist/external-tools.js +79 -12
  22. package/dist/external-tools.js.map +1 -1
  23. package/dist/graph.js +149 -39
  24. package/dist/graph.js.map +1 -1
  25. package/dist/index.d.ts +29 -4
  26. package/dist/index.js +329 -47
  27. package/dist/index.js.map +1 -1
  28. package/dist/inferrer.d.ts +5 -0
  29. package/dist/inferrer.js +1 -1
  30. package/dist/inferrer.js.map +1 -1
  31. package/dist/llm-bridge.d.ts +26 -1
  32. package/dist/llm-bridge.js +42 -6
  33. package/dist/llm-bridge.js.map +1 -1
  34. package/dist/llm-review.js +29 -11
  35. package/dist/llm-review.js.map +1 -1
  36. package/dist/mappers/ts-concepts.js +278 -7
  37. package/dist/mappers/ts-concepts.js.map +1 -1
  38. package/dist/public-api.d.ts +73 -0
  39. package/dist/public-api.js +351 -0
  40. package/dist/public-api.js.map +1 -0
  41. package/dist/reporter.d.ts +5 -0
  42. package/dist/reporter.js +119 -84
  43. package/dist/reporter.js.map +1 -1
  44. package/dist/review-health.d.ts +38 -0
  45. package/dist/review-health.js +60 -0
  46. package/dist/review-health.js.map +1 -0
  47. package/dist/rules/async.js +4 -16
  48. package/dist/rules/async.js.map +1 -1
  49. package/dist/rules/base.js +112 -87
  50. package/dist/rules/base.js.map +1 -1
  51. package/dist/rules/confidence.d.ts +2 -2
  52. package/dist/rules/confidence.js +32 -15
  53. package/dist/rules/confidence.js.map +1 -1
  54. package/dist/rules/dead-code.d.ts +2 -1
  55. package/dist/rules/dead-code.js +49 -3
  56. package/dist/rules/dead-code.js.map +1 -1
  57. package/dist/rules/index.js +131 -0
  58. package/dist/rules/index.js.map +1 -1
  59. package/dist/rules/kern-source-cross-file.d.ts +2 -0
  60. package/dist/rules/kern-source-cross-file.js +102 -0
  61. package/dist/rules/kern-source-cross-file.js.map +1 -0
  62. package/dist/rules/kern-source.js +86 -9
  63. package/dist/rules/kern-source.js.map +1 -1
  64. package/dist/rules/nextjs-app-router.js +936 -31
  65. package/dist/rules/nextjs-app-router.js.map +1 -1
  66. package/dist/rules/nextjs.js +193 -10
  67. package/dist/rules/nextjs.js.map +1 -1
  68. package/dist/rules/react-composition.js +442 -61
  69. package/dist/rules/react-composition.js.map +1 -1
  70. package/dist/rules/react-hooks.js +51 -2
  71. package/dist/rules/react-hooks.js.map +1 -1
  72. package/dist/rules/react.js +265 -49
  73. package/dist/rules/react.js.map +1 -1
  74. package/dist/rules/utils.d.ts +37 -2
  75. package/dist/rules/utils.js +113 -0
  76. package/dist/rules/utils.js.map +1 -1
  77. package/dist/semantic-diff.js +1 -1
  78. package/dist/semantic-diff.js.map +1 -1
  79. package/dist/taint-ast.js +228 -4
  80. package/dist/taint-ast.js.map +1 -1
  81. package/dist/taint-crossfile.d.ts +30 -2
  82. package/dist/taint-crossfile.js +280 -59
  83. package/dist/taint-crossfile.js.map +1 -1
  84. package/dist/taint-types.d.ts +2 -1
  85. package/dist/taint-types.js +32 -2
  86. package/dist/taint-types.js.map +1 -1
  87. package/dist/taint.d.ts +1 -1
  88. package/dist/taint.js +1 -1
  89. package/dist/taint.js.map +1 -1
  90. package/dist/types.d.ts +80 -0
  91. package/dist/types.js.map +1 -1
  92. package/package.json +3 -3
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Public API resolver — decides whether an exported symbol is part of a package's
3
+ * intentional public API, so dead-export doesn't flag symbols consumed outside the
4
+ * analyzed graph.
5
+ *
6
+ * Sources of truth, in order:
7
+ * 1. package.json `exports` (string / object / conditional)
8
+ * 2. package.json `main` / `module` / `types`
9
+ * 3. package.json `bin`
10
+ * 4. Conservative barrel fallback: `src/index.ts(x)`, `index.ts(x)`
11
+ * 5. kern.config `review.publicApi` — explicit escape hatch
12
+ *
13
+ * A file listed as a package entry has ALL its named exports treated as public.
14
+ * Re-exports resolved through call-graph are already kept live by existing logic;
15
+ * this rule covers exports consumed by EXTERNAL callers who never show up in
16
+ * the graph (library consumers, dynamic loaders, platform entry points).
17
+ */
18
+ import { existsSync, readFileSync } from 'fs';
19
+ import { dirname, isAbsolute, join, resolve } from 'path';
20
+ import { SyntaxKind } from 'ts-morph';
21
+ const SRC_EXTS = ['.ts', '.tsx'];
22
+ /** Characters that, when present in a pattern, trigger glob expansion. */
23
+ const GLOB_CHARS = /[*?[]/;
24
+ function hasGlobChars(pattern) {
25
+ return GLOB_CHARS.test(pattern);
26
+ }
27
+ /**
28
+ * Convert a POSIX-style glob pattern to a `RegExp` that matches full paths.
29
+ *
30
+ * Supported syntax:
31
+ * - `*` — any run of non-separator chars
32
+ * - `**` — any path fragment, including separators (zero or more segments)
33
+ * - `?` — a single non-separator char
34
+ * - `[abc]` / `[a-z]` — character class
35
+ * - `[!abc]` — negated character class (POSIX-style; translated to regex `[^abc]`)
36
+ *
37
+ * All other regex metacharacters are escaped. Brace expansion (`{a,b}`) is
38
+ * NOT supported — keep config patterns simple; split into multiple entries.
39
+ *
40
+ * The pattern is expected to be POSIX-separated (forward slashes). The caller
41
+ * normalizes Windows backslashes to `/` before calling this. Consecutive `*`
42
+ * runs are collapsed first to prevent catastrophic backtracking on patterns
43
+ * like `**\/**\/**`.
44
+ */
45
+ function globToRegex(pattern) {
46
+ const squashed = pattern.replace(/\*{2,}/g, '**').replace(/(?:\*\*\/)+/g, '**/');
47
+ let out = '';
48
+ let i = 0;
49
+ while (i < squashed.length) {
50
+ const c = squashed[i];
51
+ if (c === '*') {
52
+ if (squashed[i + 1] === '*') {
53
+ if (squashed[i + 2] === '/') {
54
+ out += '(?:.*/)?';
55
+ i += 3;
56
+ }
57
+ else {
58
+ out += '.*';
59
+ i += 2;
60
+ }
61
+ }
62
+ else {
63
+ out += '[^/]*';
64
+ i++;
65
+ }
66
+ }
67
+ else if (c === '?') {
68
+ out += '[^/]';
69
+ i++;
70
+ }
71
+ else if (c === '[') {
72
+ const end = squashed.indexOf(']', i + 1);
73
+ if (end === -1) {
74
+ out += '\\[';
75
+ i++;
76
+ }
77
+ else {
78
+ let inner = squashed.substring(i + 1, end);
79
+ if (inner.startsWith('!'))
80
+ inner = `^${inner.slice(1)}`;
81
+ out += `[${inner}]`;
82
+ i = end + 1;
83
+ }
84
+ }
85
+ else if ('.+^$|(){}\\'.includes(c)) {
86
+ out += `\\${c}`;
87
+ i++;
88
+ }
89
+ else {
90
+ out += c;
91
+ i++;
92
+ }
93
+ }
94
+ return new RegExp(`^${out}$`);
95
+ }
96
+ /** Normalize path separators so glob matching works on Windows. */
97
+ function toPosix(p) {
98
+ return p.replace(/\\/g, '/');
99
+ }
100
+ function collectSpecifiers(value) {
101
+ if (typeof value === 'string')
102
+ return [value];
103
+ if (!value || typeof value !== 'object')
104
+ return [];
105
+ if (Array.isArray(value))
106
+ return value.flatMap(collectSpecifiers);
107
+ const out = [];
108
+ for (const v of Object.values(value)) {
109
+ out.push(...collectSpecifiers(v));
110
+ }
111
+ return out;
112
+ }
113
+ /**
114
+ * Resolve a package.json specifier (e.g. `./dist/index.js`) to the source file
115
+ * the review operates on (e.g. `./src/index.ts`). Returns undefined if nothing
116
+ * plausible exists on disk.
117
+ */
118
+ export function resolveSpecifierToSrc(packageRoot, specifier, fileExists = existsSync) {
119
+ if (typeof specifier !== 'string' || !specifier.startsWith('.'))
120
+ return undefined;
121
+ const abs = resolve(packageRoot, specifier);
122
+ const stemMatch = abs.match(/^(.+?)(\.d\.ts|\.js|\.cjs|\.mjs|\.ts|\.tsx)$/);
123
+ const stem = stemMatch ? stemMatch[1] : abs;
124
+ const candidates = [];
125
+ if (abs.endsWith('.ts') || abs.endsWith('.tsx'))
126
+ candidates.push(abs);
127
+ for (const ext of SRC_EXTS)
128
+ candidates.push(`${stem}${ext}`);
129
+ // dist → src swap
130
+ const withSrc = candidates.flatMap((c) => (c.includes('/dist/') ? [c.replace('/dist/', '/src/')] : []));
131
+ candidates.push(...withSrc);
132
+ // Directory entry — try index.{ts,tsx}
133
+ for (const base of [abs, stem]) {
134
+ for (const ext of SRC_EXTS)
135
+ candidates.push(join(base, `index${ext}`));
136
+ }
137
+ for (const c of candidates) {
138
+ if (fileExists(c))
139
+ return c;
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * For a parsed package.json at packageRoot, return all source files that act as
145
+ * public entry points. Missing files are silently dropped.
146
+ */
147
+ export function resolvePackageEntryFiles(packageRoot, pkg, fileExists = existsSync) {
148
+ const specs = new Set();
149
+ if (pkg.exports !== undefined) {
150
+ for (const s of collectSpecifiers(pkg.exports))
151
+ specs.add(s);
152
+ }
153
+ if (typeof pkg.main === 'string')
154
+ specs.add(pkg.main);
155
+ if (typeof pkg.module === 'string')
156
+ specs.add(pkg.module);
157
+ if (typeof pkg.types === 'string')
158
+ specs.add(pkg.types);
159
+ if (typeof pkg.bin === 'string') {
160
+ specs.add(pkg.bin);
161
+ }
162
+ else if (pkg.bin && typeof pkg.bin === 'object') {
163
+ for (const v of Object.values(pkg.bin)) {
164
+ if (typeof v === 'string')
165
+ specs.add(v);
166
+ }
167
+ }
168
+ // Conservative barrel fallback — only contributes if the file actually exists.
169
+ for (const ext of SRC_EXTS) {
170
+ specs.add(`./src/index${ext}`);
171
+ specs.add(`./index${ext}`);
172
+ }
173
+ const resolved = new Set();
174
+ for (const s of specs) {
175
+ const p = resolveSpecifierToSrc(packageRoot, s, fileExists);
176
+ if (p)
177
+ resolved.add(p);
178
+ }
179
+ return [...resolved];
180
+ }
181
+ function findPackageRoot(startFile, cache) {
182
+ const startDir = dirname(startFile);
183
+ if (cache.has(startDir))
184
+ return cache.get(startDir) ?? null;
185
+ const visited = [];
186
+ let dir = startDir;
187
+ for (let i = 0; i < 30; i++) {
188
+ if (cache.has(dir)) {
189
+ const hit = cache.get(dir) ?? null;
190
+ for (const v of visited)
191
+ cache.set(v, hit);
192
+ return hit;
193
+ }
194
+ visited.push(dir);
195
+ if (existsSync(join(dir, 'package.json'))) {
196
+ for (const v of visited)
197
+ cache.set(v, dir);
198
+ return dir;
199
+ }
200
+ const parent = dirname(dir);
201
+ if (parent === dir)
202
+ break;
203
+ dir = parent;
204
+ }
205
+ for (const v of visited)
206
+ cache.set(v, null);
207
+ return null;
208
+ }
209
+ /**
210
+ * Build a public-API map by walking up from each file to its nearest package.json
211
+ * and collecting declared entry points. Applies config overrides on top.
212
+ */
213
+ export function buildPublicApiMap(filePaths, overrides) {
214
+ const rootCache = new Map();
215
+ const roots = new Set();
216
+ for (const fp of filePaths) {
217
+ const r = findPackageRoot(fp, rootCache);
218
+ if (r)
219
+ roots.add(r);
220
+ }
221
+ const entryFiles = new Set();
222
+ for (const root of roots) {
223
+ const pkgPath = join(root, 'package.json');
224
+ let pkg;
225
+ try {
226
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
227
+ }
228
+ catch {
229
+ continue;
230
+ }
231
+ for (const f of resolvePackageEntryFiles(root, pkg))
232
+ entryFiles.add(f);
233
+ }
234
+ const explicitSymbols = new Set();
235
+ const projectRoot = overrides?.projectRoot ?? process.cwd();
236
+ if (overrides?.files) {
237
+ for (const pattern of overrides.files) {
238
+ if (typeof pattern !== 'string' || pattern.length === 0)
239
+ continue;
240
+ const abs = isAbsolute(pattern) ? pattern : resolve(projectRoot, pattern);
241
+ // Always add the literal resolved path. This preserves the pre-glob
242
+ // behavior ("files listed here are public even if absent from the
243
+ // reviewed graph") AND gives the right answer for Next.js-style
244
+ // literal brackets like "src/app/[slug]/page.tsx" — the bracketed
245
+ // segment looks like a glob character class but is actually part of
246
+ // the filename. Glob expansion still runs below, so users who meant
247
+ // [...] as a class get that behavior too.
248
+ entryFiles.add(abs);
249
+ if (hasGlobChars(pattern)) {
250
+ const regex = globToRegex(toPosix(abs));
251
+ for (const fp of filePaths) {
252
+ if (regex.test(toPosix(fp)))
253
+ entryFiles.add(fp);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (overrides?.symbols) {
259
+ for (const spec of overrides.symbols) {
260
+ if (typeof spec !== 'string')
261
+ continue;
262
+ const idx = spec.lastIndexOf('#');
263
+ if (idx <= 0 || idx === spec.length - 1)
264
+ continue;
265
+ const rawPath = spec.slice(0, idx);
266
+ const name = spec.slice(idx + 1);
267
+ const abs = isAbsolute(rawPath) ? rawPath : resolve(projectRoot, rawPath);
268
+ explicitSymbols.add(`${abs}#${name}`);
269
+ }
270
+ }
271
+ return { entryFiles, explicitSymbols };
272
+ }
273
+ export const EMPTY_PUBLIC_API = {
274
+ entryFiles: new Set(),
275
+ explicitSymbols: new Set(),
276
+ };
277
+ export function isPublicApi(map, filePath, exportName) {
278
+ if (map.entryFiles.has(filePath))
279
+ return true;
280
+ if (map.explicitSymbols.has(`${filePath}#${exportName}`))
281
+ return true;
282
+ return false;
283
+ }
284
+ // Re-export propagation.
285
+ //
286
+ // A curated barrel (`export { foo } from './worker.js'`) makes worker.ts#foo
287
+ // part of the package's public API even if no file in the graph imports
288
+ // worker.ts directly. Without this, dead-export would fire on every symbol
289
+ // a package re-exports but never uses internally: the exact FP that Agon
290
+ // hit before Phase 1, and which the single-file buildPublicApiMap cannot
291
+ // catch on its own.
292
+ function getDeclName(decl, fallback) {
293
+ const maybeNamed = decl;
294
+ if (typeof maybeNamed.getName === 'function') {
295
+ const name = maybeNamed.getName();
296
+ if (name)
297
+ return name;
298
+ }
299
+ if (decl.getKindName() === 'ExportAssignment') {
300
+ const expr = decl.getExpression?.();
301
+ if (expr?.getKind() === SyntaxKind.Identifier)
302
+ return expr.getText();
303
+ }
304
+ return fallback;
305
+ }
306
+ /**
307
+ * Expand `map.entryFiles` through re-export chains: for each public entry,
308
+ * walk `getExportedDeclarations()` and mark every upstream `(file, symbol)`
309
+ * that the entry re-exports as also public. Returns a new map; the input is
310
+ * not mutated.
311
+ *
312
+ * `sourceFileFor(path)` should return the ts-morph SourceFile for an
313
+ * absolute path (or undefined when the file is outside the analyzed graph).
314
+ */
315
+ export function expandPublicApiThroughReExports(map, sourceFileFor) {
316
+ const entryFiles = new Set(map.entryFiles);
317
+ const explicitSymbols = new Set(map.explicitSymbols);
318
+ // BFS through re-export chains so a barrel-of-barrels propagates too.
319
+ const frontier = [...map.entryFiles];
320
+ const seen = new Set(map.entryFiles);
321
+ while (frontier.length > 0) {
322
+ const entry = frontier.shift();
323
+ const sf = sourceFileFor(entry);
324
+ if (!sf)
325
+ continue;
326
+ let exported;
327
+ try {
328
+ exported = sf.getExportedDeclarations();
329
+ }
330
+ catch {
331
+ continue;
332
+ }
333
+ for (const [exportName, decls] of exported) {
334
+ for (const decl of decls) {
335
+ const declFile = decl.getSourceFile().getFilePath();
336
+ if (declFile === entry)
337
+ continue;
338
+ explicitSymbols.add(`${declFile}#${getDeclName(decl, exportName)}`);
339
+ if (!seen.has(declFile)) {
340
+ seen.add(declFile);
341
+ // An upstream barrel's own re-exports should also be followed;
342
+ // don't mark it as an entry (only declared entries get that), but
343
+ // do walk it so transitive symbols are captured.
344
+ frontier.push(declFile);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ return { entryFiles, explicitSymbols };
350
+ }
351
+ //# sourceMappingURL=public-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"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"}
@@ -28,6 +28,11 @@ export declare function formatReportJSON(report: ReviewReport, options?: {
28
28
  includeLLMPrompt?: boolean;
29
29
  }): string;
30
30
  export declare function formatSARIF(reports: ReviewReport[]): string;
31
+ export interface SARIFMetadataOptions {
32
+ suppressedFindings?: ReviewFinding[];
33
+ getBaselineStatus?: (report: ReviewReport, finding: ReviewFinding) => 'new' | 'existing' | undefined;
34
+ }
35
+ export declare function formatSARIFWithMetadata(reports: ReviewReport[], options?: SARIFMetadataOptions): string;
31
36
  /**
32
37
  * Format SARIF with suppression metadata.
33
38
  * Suppressed findings appear with a `suppressions` array per SARIF v2.1.0 section 3.35.
package/dist/reporter.js CHANGED
@@ -191,6 +191,17 @@ export function formatReport(report, config) {
191
191
  const lines = [];
192
192
  lines.push(` @kernlang/review — analyzing ${report.filePath}`);
193
193
  lines.push('');
194
+ // Render health banner BEFORE findings. Users need to know which subsystems skipped/fell
195
+ // back before they interpret "0 findings" as "the file is clean" — a clean report and a
196
+ // report where half the checks didn't run used to look identical in the CLI.
197
+ if (report.health && report.health.entries.length > 0) {
198
+ const label = report.health.status === 'partial' ? 'PARTIAL' : 'DEGRADED';
199
+ lines.push(` [${label}] Review ran in ${report.health.status} mode:`);
200
+ for (const entry of report.health.entries) {
201
+ lines.push(` - ${entry.subsystem} (${entry.kind}): ${entry.message}`);
202
+ }
203
+ lines.push('');
204
+ }
194
205
  if (report.inferred.length > 0) {
195
206
  lines.push(` KERN-expressible (${report.inferred.length} constructs):`);
196
207
  for (const r of report.inferred) {
@@ -308,6 +319,10 @@ export function formatReportJSON(report, options) {
308
319
  }
309
320
  // ── SARIF Format ─────────────────────────────────────────────────────────
310
321
  export function formatSARIF(reports) {
322
+ return formatSARIFWithMetadata(reports);
323
+ }
324
+ export function formatSARIFWithMetadata(reports, options = {}) {
325
+ const { suppressedFindings, getBaselineStatus } = options;
311
326
  const sarif = {
312
327
  $schema: 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json',
313
328
  version: '2.1.0',
@@ -321,118 +336,138 @@ export function formatSARIF(reports) {
321
336
  },
322
337
  },
323
338
  results: [],
324
- },
325
- ],
326
- };
327
- const rules = new Set();
328
- for (const report of reports) {
329
- for (const f of report.findings) {
330
- if (!rules.has(f.ruleId)) {
331
- rules.add(f.ruleId);
332
- sarif.runs[0].tool.driver.rules.push({
333
- id: f.ruleId,
334
- shortDescription: { text: f.ruleId },
335
- helpUri: `https://github.com/kern-lang/kern-lang/blob/main/docs/rules.md#${f.ruleId}`,
336
- });
337
- }
338
- const sarifLevel = f.severity === 'error' ? 'error' : f.severity === 'warning' ? 'warning' : 'note';
339
- const result = {
340
- ruleId: f.ruleId,
341
- level: sarifLevel,
342
- message: { text: f.message },
343
- locations: [
339
+ // SARIF spec 3.20: invocations describe tool-execution events. toolExecutionNotifications
340
+ // carries messages FROM the tool (not findings ABOUT the code), which is exactly where
341
+ // "ESLint skipped" / "call graph failed" belong. Without this, enterprise consumers of
342
+ // SARIF (GitHub code scanning, Azure DevOps) see 0 results and assume "clean" when the
343
+ // truth is "half the analyzers didn't run."
344
+ invocations: [
344
345
  {
345
- physicalLocation: {
346
- artifactLocation: { uri: f.primarySpan.file },
347
- region: {
348
- startLine: f.primarySpan.startLine,
349
- startColumn: f.primarySpan.startCol,
350
- endLine: f.primarySpan.endLine,
351
- endColumn: f.primarySpan.endCol,
352
- },
353
- },
346
+ executionSuccessful: true,
347
+ toolExecutionNotifications: [],
354
348
  },
355
349
  ],
356
- };
357
- // SARIF result.rank is 0.0–100.0 per spec; kern/confidence stays 0–1
358
- if (f.confidence !== undefined) {
359
- result.rank = f.confidence * 100;
360
- result.properties = { 'kern/confidence': f.confidence };
361
- }
362
- sarif.runs[0].results.push(result);
363
- }
364
- }
365
- return JSON.stringify(sarif, null, 2);
366
- }
367
- /**
368
- * Format SARIF with suppression metadata.
369
- * Suppressed findings appear with a `suppressions` array per SARIF v2.1.0 section 3.35.
370
- */
371
- export function formatSARIFWithSuppressions(reports, suppressedFindings) {
372
- const sarif = {
373
- $schema: 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json',
374
- version: '2.1.0',
375
- runs: [
376
- {
377
- tool: {
378
- driver: {
379
- name: '@kernlang/review',
380
- version: '2.0.0',
381
- rules: [],
382
- },
383
- },
384
- results: [],
385
350
  },
386
351
  ],
387
352
  };
353
+ // Aggregate + dedupe health entries across every report. The in-process ReviewHealthBuilder
354
+ // already dedupes within one review, but reports from different reviewFile calls can each
355
+ // carry their own builders — dedupe again here by (subsystem, kind) so SARIF output emits one
356
+ // notification per distinct failure mode, not one per report.
357
+ const healthSeen = new Set();
358
+ let anyError = false;
359
+ for (const report of reports) {
360
+ for (const entry of report.health?.entries ?? []) {
361
+ const key = `${entry.subsystem}:${entry.kind}`;
362
+ if (healthSeen.has(key))
363
+ continue;
364
+ healthSeen.add(key);
365
+ // SARIF notification levels map to health kinds:
366
+ // skipped -> note (optional subsystem absent — nothing wrong)
367
+ // fallback -> warning (analysis degraded but still ran)
368
+ // error -> error (subsystem failed outright; findings may be missing)
369
+ const level = entry.kind === 'error' ? 'error' : entry.kind === 'fallback' ? 'warning' : 'note';
370
+ if (entry.kind === 'error')
371
+ anyError = true;
372
+ sarif.runs[0].invocations[0].toolExecutionNotifications.push({
373
+ descriptor: { id: `kern/health/${entry.subsystem}` },
374
+ level,
375
+ message: { text: entry.message },
376
+ properties: {
377
+ 'kern/subsystem': entry.subsystem,
378
+ 'kern/kind': entry.kind,
379
+ ...(entry.detail !== undefined ? { 'kern/detail': entry.detail } : {}),
380
+ },
381
+ });
382
+ }
383
+ }
384
+ // Spec 3.20.6: executionSuccessful=false means the tool raised any error-level notification.
385
+ // Setting this correctly lets CI systems distinguish "tool ran cleanly, zero findings" from
386
+ // "tool errored on a subsystem, zero findings from that subsystem don't mean safe code."
387
+ if (anyError) {
388
+ sarif.runs[0].invocations[0].executionSuccessful = false;
389
+ }
388
390
  const rules = new Set();
389
- // Include file path in suppression key to avoid cross-file fingerprint collisions
390
391
  const suppressedSet = new Set(suppressedFindings?.map((f) => `${f.primarySpan.file}:${f.fingerprint}`) ?? []);
391
- const allFindings = [...reports.flatMap((r) => r.findings), ...(suppressedFindings ?? [])];
392
- for (const f of allFindings) {
393
- if (!rules.has(f.ruleId)) {
394
- rules.add(f.ruleId);
392
+ function pushResult(finding, report, overrides = {}) {
393
+ if (!rules.has(finding.ruleId)) {
394
+ rules.add(finding.ruleId);
395
395
  sarif.runs[0].tool.driver.rules.push({
396
- id: f.ruleId,
397
- shortDescription: { text: f.ruleId },
398
- helpUri: `https://github.com/kern-lang/kern-lang/blob/main/docs/rules.md#${f.ruleId}`,
396
+ id: finding.ruleId,
397
+ shortDescription: { text: finding.ruleId },
398
+ helpUri: `https://github.com/kern-lang/kern-lang/blob/main/docs/rules.md#${finding.ruleId}`,
399
399
  });
400
400
  }
401
- const sarifLevel = f.severity === 'error' ? 'error' : f.severity === 'warning' ? 'warning' : 'note';
401
+ const sarifLevel = finding.severity === 'error' ? 'error' : finding.severity === 'warning' ? 'warning' : 'note';
402
+ const baselineStatus = report ? getBaselineStatus?.(report, finding) : undefined;
403
+ const properties = {};
402
404
  const result = {
403
- ruleId: f.ruleId,
405
+ ruleId: finding.ruleId,
404
406
  level: sarifLevel,
405
- message: { text: f.message },
407
+ message: { text: finding.message },
406
408
  locations: [
407
409
  {
408
410
  physicalLocation: {
409
- artifactLocation: { uri: f.primarySpan.file },
411
+ artifactLocation: { uri: finding.primarySpan.file },
410
412
  region: {
411
- startLine: f.primarySpan.startLine,
412
- startColumn: f.primarySpan.startCol,
413
- endLine: f.primarySpan.endLine,
414
- endColumn: f.primarySpan.endCol,
413
+ startLine: finding.primarySpan.startLine,
414
+ startColumn: finding.primarySpan.startCol,
415
+ endLine: finding.primarySpan.endLine,
416
+ endColumn: finding.primarySpan.endCol,
415
417
  },
416
418
  },
417
419
  },
418
420
  ],
419
421
  };
420
- if (f.confidence !== undefined) {
421
- result.rank = f.confidence * 100;
422
- result.properties = { 'kern/confidence': f.confidence };
422
+ // SARIF result.rank is 0.0–100.0 per spec; kern/confidence stays 0–1
423
+ if (finding.confidence !== undefined) {
424
+ result.rank = finding.confidence * 100;
425
+ properties['kern/confidence'] = finding.confidence;
423
426
  }
424
- if (suppressedSet.has(`${f.primarySpan.file}:${f.fingerprint}`)) {
425
- result.suppressions = [
426
- {
427
- kind: 'inSource',
428
- justification: `kern-ignore directive`,
429
- },
430
- ];
427
+ if (baselineStatus) {
428
+ properties['kern/baselineStatus'] = baselineStatus;
429
+ }
430
+ if (Object.keys(properties).length > 0) {
431
+ result.properties = properties;
432
+ }
433
+ const suppressions = [];
434
+ if (overrides.isSuppressedInSource || suppressedSet.has(`${finding.primarySpan.file}:${finding.fingerprint}`)) {
435
+ suppressions.push({
436
+ kind: 'inSource',
437
+ justification: 'kern-ignore directive',
438
+ });
439
+ }
440
+ if (baselineStatus === 'existing') {
441
+ suppressions.push({
442
+ kind: 'external',
443
+ justification: 'Present in review baseline',
444
+ });
445
+ }
446
+ if (suppressions.length > 0) {
447
+ result.suppressions = suppressions;
431
448
  }
432
449
  sarif.runs[0].results.push(result);
433
450
  }
451
+ for (const report of reports) {
452
+ for (const finding of report.findings) {
453
+ pushResult(finding, report);
454
+ }
455
+ for (const finding of report.suppressedFindings ?? []) {
456
+ pushResult(finding, report, { isSuppressedInSource: true });
457
+ }
458
+ }
459
+ for (const finding of suppressedFindings ?? []) {
460
+ pushResult(finding, undefined, { isSuppressedInSource: true });
461
+ }
434
462
  return JSON.stringify(sarif, null, 2);
435
463
  }
464
+ /**
465
+ * Format SARIF with suppression metadata.
466
+ * Suppressed findings appear with a `suppressions` array per SARIF v2.1.0 section 3.35.
467
+ */
468
+ export function formatSARIFWithSuppressions(reports, suppressedFindings) {
469
+ return formatSARIFWithMetadata(reports, { suppressedFindings });
470
+ }
436
471
  // ── Multi-file Summary ───────────────────────────────────────────────────
437
472
  export function formatSummary(reports) {
438
473
  const lines = [];