@kernlang/review 3.5.6-canary.202.1.31706b95 → 3.5.7

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `.env` analyzer — duplicate variables, malformed assignments, and a
3
+ * conservative committed-secret heuristic. Parallel to json.ts and
4
+ * markdown.ts: a non-ts-morph path that participates in the same
5
+ * ReviewFinding pipeline so kern-sight and kern-guard consume findings
6
+ * without API changes. No parser dep — the format is line-oriented and
7
+ * regex-tractable.
8
+ *
9
+ * Format conventions (dotenv-style, conservative subset):
10
+ *
11
+ * • One assignment per line: `KEY=VALUE` or `KEY="quoted value"`.
12
+ * • Comments start with `#`. Inline comments (`KEY=val # note`) are
13
+ * accepted; the comment is stripped before secret-likeness checks.
14
+ * • Blank lines and pure-comment lines are ignored.
15
+ * • `export KEY=VALUE` is accepted (bash-style).
16
+ * • Continuation across lines is NOT supported. Multi-line values
17
+ * inside quotes are NOT supported (rare in real env files; out of
18
+ * scope for a hygiene scanner).
19
+ *
20
+ * Rules:
21
+ *
22
+ * • `env/duplicate-key` — same KEY assigned more than once in the file.
23
+ * The second value wins on `process.env` parse. Fingerprint by key
24
+ * name (NOT line) so kern-guard dedup is stable; 3rd+ occurrences
25
+ * append `#N`.
26
+ * • `env/malformed` — a non-blank, non-comment line that does not
27
+ * match the assignment shape. Fingerprint by line content hash so
28
+ * formatting fixes upstream don't perturb downstream findings.
29
+ * • `env/possible-secret` — KEY matches `SECRET|TOKEN|API_KEY|PASSWORD|
30
+ * PRIVATE|ACCESS_KEY` AND value is non-empty AND is not an obvious
31
+ * placeholder (`changeme`, `example`, `<…>`, `${…}`, `your_…_here`,
32
+ * pure `*` masks, etc.). Warning severity — conservative on purpose;
33
+ * this should not fire on placeholders in `.env.example`.
34
+ *
35
+ * Files routed here are: `.env`, `.env.local`, `.env.development`,
36
+ * `.env.production`, `.env.test`, `.env.<anything>`. Files named
37
+ * `.env.example`, `.env.sample`, `.env.template`, `.env.defaults` are
38
+ * scanned but the secret-likeness rule is SUPPRESSED — those are by
39
+ * convention for committed placeholders.
40
+ */
41
+ import type { ReviewFinding } from '../types.js';
42
+ /** Files this analyzer claims. Routed by basename, not extension: `.env`
43
+ * has no extension in the usual sense. */
44
+ export declare function isEnvFile(filePath: string): boolean;
45
+ export declare function reviewEnvFile(source: string, filePath: string): ReviewFinding[];
@@ -0,0 +1,235 @@
1
+ /**
2
+ * `.env` analyzer — duplicate variables, malformed assignments, and a
3
+ * conservative committed-secret heuristic. Parallel to json.ts and
4
+ * markdown.ts: a non-ts-morph path that participates in the same
5
+ * ReviewFinding pipeline so kern-sight and kern-guard consume findings
6
+ * without API changes. No parser dep — the format is line-oriented and
7
+ * regex-tractable.
8
+ *
9
+ * Format conventions (dotenv-style, conservative subset):
10
+ *
11
+ * • One assignment per line: `KEY=VALUE` or `KEY="quoted value"`.
12
+ * • Comments start with `#`. Inline comments (`KEY=val # note`) are
13
+ * accepted; the comment is stripped before secret-likeness checks.
14
+ * • Blank lines and pure-comment lines are ignored.
15
+ * • `export KEY=VALUE` is accepted (bash-style).
16
+ * • Continuation across lines is NOT supported. Multi-line values
17
+ * inside quotes are NOT supported (rare in real env files; out of
18
+ * scope for a hygiene scanner).
19
+ *
20
+ * Rules:
21
+ *
22
+ * • `env/duplicate-key` — same KEY assigned more than once in the file.
23
+ * The second value wins on `process.env` parse. Fingerprint by key
24
+ * name (NOT line) so kern-guard dedup is stable; 3rd+ occurrences
25
+ * append `#N`.
26
+ * • `env/malformed` — a non-blank, non-comment line that does not
27
+ * match the assignment shape. Fingerprint by line content hash so
28
+ * formatting fixes upstream don't perturb downstream findings.
29
+ * • `env/possible-secret` — KEY matches `SECRET|TOKEN|API_KEY|PASSWORD|
30
+ * PRIVATE|ACCESS_KEY` AND value is non-empty AND is not an obvious
31
+ * placeholder (`changeme`, `example`, `<…>`, `${…}`, `your_…_here`,
32
+ * pure `*` masks, etc.). Warning severity — conservative on purpose;
33
+ * this should not fire on placeholders in `.env.example`.
34
+ *
35
+ * Files routed here are: `.env`, `.env.local`, `.env.development`,
36
+ * `.env.production`, `.env.test`, `.env.<anything>`. Files named
37
+ * `.env.example`, `.env.sample`, `.env.template`, `.env.defaults` are
38
+ * scanned but the secret-likeness rule is SUPPRESSED — those are by
39
+ * convention for committed placeholders.
40
+ */
41
+ import { basename } from 'node:path';
42
+ // Matches `KEY=value`, `export KEY=value`, with optional spaces around `=`
43
+ // (dotenv accepts both `KEY=value` and `KEY = value` — discouraged but
44
+ // common). Captures:
45
+ // [1] = key
46
+ // [2] = value (everything after `=`, before any trailing comment)
47
+ const ASSIGNMENT_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
48
+ // Keys that suggest a secret. Case-insensitive. Conservative set.
49
+ const SECRET_KEY_RE = /(?:^|_)(?:SECRET|TOKEN|API[_]?KEY|PASSWORD|PRIVATE[_]?KEY|ACCESS[_]?KEY|AUTH[_]?KEY|CLIENT[_]?SECRET)(?:$|_)/i;
50
+ // Placeholder shapes — if the value is one of these, do NOT fire the
51
+ // secret-likeness rule. The list is intentionally generous because the
52
+ // cost of a false positive on .env.example is much higher than missing
53
+ // one real secret (kern-guard would post the warning every PR).
54
+ const PLACEHOLDER_PATTERNS = [
55
+ /^\s*$/,
56
+ /^(changeme|change-me|change_me|example|sample|placeholder|todo|tbd|xxx+|none|null|undefined|fill[_-]?me[_-]?in)\s*$/i,
57
+ /^your[_-]?.+[_-]?here$/i, // `your_api_key_here`
58
+ /^<[^>]+>$/, // `<your-token>`
59
+ /^\$\{[^}]+\}$/, // `${VAR}`
60
+ /^\*+$/, // pure asterisk mask
61
+ /^[*x]+(?:[-_][*x]+)*$/i, // `xxx-xxx-xxx`
62
+ ];
63
+ /** Strip surrounding single/double quotes if balanced. */
64
+ function unquote(s) {
65
+ if (s.length >= 2) {
66
+ const first = s.charAt(0);
67
+ const last = s.charAt(s.length - 1);
68
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
69
+ return s.slice(1, -1);
70
+ }
71
+ }
72
+ return s;
73
+ }
74
+ /** Produce the "value for secret-likeness check" from a raw post-`=` slice.
75
+ * Order matters: a quoted value with a trailing inline comment looks like
76
+ * `"changeme" # production`. Stripping the inline comment FIRST without
77
+ * quote-awareness would either bail (old behavior — bug) or chop a `#`
78
+ * inside the quotes. Walking forward through the string, tracking the
79
+ * initial quote char if any, gives the right answer in one pass:
80
+ *
81
+ * 1. trim
82
+ * 2. if starts with `"` or `'`, find the matching closing quote and
83
+ * drop everything from the FIRST whitespace+`#` AFTER it
84
+ * 3. otherwise, drop everything from the first whitespace+`#`
85
+ * 4. unquote whatever remains
86
+ *
87
+ * Used only for the secret-likeness check; duplicate detection compares
88
+ * full raw values to keep error spans precise. */
89
+ function valueForSecretCheck(raw) {
90
+ const trimmed = raw.trim();
91
+ if (trimmed === '')
92
+ return '';
93
+ let body = trimmed;
94
+ const first = trimmed.charAt(0);
95
+ if (first === '"' || first === "'") {
96
+ // Find the matching unescaped closing quote.
97
+ let endIdx = -1;
98
+ for (let i = 1; i < trimmed.length; i++) {
99
+ const c = trimmed.charAt(i);
100
+ if (c === '\\') {
101
+ i++; // skip the escaped char
102
+ continue;
103
+ }
104
+ if (c === first) {
105
+ endIdx = i;
106
+ break;
107
+ }
108
+ }
109
+ if (endIdx !== -1) {
110
+ // Everything past the closing quote, after optional whitespace, may
111
+ // be an inline `# comment`. Take only the quoted portion (inclusive
112
+ // of the quotes — `unquote` strips them next).
113
+ body = trimmed.slice(0, endIdx + 1);
114
+ }
115
+ // If no closing quote was found, fall through and let the # stripper
116
+ // handle the malformed input.
117
+ }
118
+ else {
119
+ // Unquoted value — strip a `# comment` preceded by whitespace.
120
+ const hashIdx = trimmed.indexOf(' #');
121
+ if (hashIdx !== -1)
122
+ body = trimmed.slice(0, hashIdx);
123
+ }
124
+ return unquote(body.trim());
125
+ }
126
+ function isPlaceholder(value) {
127
+ for (const re of PLACEHOLDER_PATTERNS) {
128
+ if (re.test(value))
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+ /** Treat files named `.env.example`, `.env.sample`, `.env.template`,
134
+ * `.env.defaults`, `.env.dist` as committed-placeholder files where the
135
+ * secret-likeness rule is suppressed. */
136
+ function isPlaceholderFile(filePath) {
137
+ const base = basename(filePath).toLowerCase();
138
+ return /^\.env\.(example|sample|template|defaults|dist)$/.test(base);
139
+ }
140
+ /** Files this analyzer claims. Routed by basename, not extension: `.env`
141
+ * has no extension in the usual sense. */
142
+ export function isEnvFile(filePath) {
143
+ const base = basename(filePath);
144
+ return base === '.env' || base.startsWith('.env.');
145
+ }
146
+ /** Fast non-cryptographic hash for malformed-line fingerprints. djb2. */
147
+ function hashShort(s) {
148
+ let h = 5381;
149
+ for (let i = 0; i < s.length; i++) {
150
+ h = ((h << 5) + h + s.charCodeAt(i)) | 0;
151
+ }
152
+ return (h >>> 0).toString(36);
153
+ }
154
+ function makeSpan(filePath, line, startCol, endCol) {
155
+ return { file: filePath, startLine: line, startCol, endLine: line, endCol };
156
+ }
157
+ export function reviewEnvFile(source, filePath) {
158
+ const findings = [];
159
+ const suppressSecretRule = isPlaceholderFile(filePath);
160
+ // Track first-occurrence + dup count per key for fingerprint stability.
161
+ const seen = new Map();
162
+ const lines = source.split(/\r?\n/);
163
+ for (let i = 0; i < lines.length; i++) {
164
+ const raw = lines[i];
165
+ const trimmed = raw.trim();
166
+ if (trimmed === '')
167
+ continue;
168
+ if (trimmed.startsWith('#'))
169
+ continue;
170
+ const m = raw.match(ASSIGNMENT_RE);
171
+ if (!m) {
172
+ // Malformed line — non-blank, non-comment, not parseable.
173
+ const ruleId = 'env/malformed';
174
+ findings.push({
175
+ source: 'kern',
176
+ ruleId,
177
+ severity: 'warning',
178
+ category: 'style',
179
+ message: 'Line does not look like a `KEY=VALUE` assignment. dotenv parsers may skip or misread this line.',
180
+ primarySpan: makeSpan(filePath, i + 1, 1, raw.length + 1),
181
+ confidence: 85,
182
+ // Fingerprint by content shape — same broken line on a different
183
+ // line number stays the same finding. Different broken lines get
184
+ // distinct fingerprints.
185
+ fingerprint: `${ruleId}:${hashShort(trimmed)}`,
186
+ });
187
+ continue;
188
+ }
189
+ const key = m[1];
190
+ const rawValue = m[2] ?? '';
191
+ // ── Duplicate key ──────────────────────────────────────────────────
192
+ const prior = seen.get(key);
193
+ if (prior) {
194
+ prior.dupCount += 1;
195
+ const ruleId = 'env/duplicate-key';
196
+ const suffix = prior.dupCount === 1 ? '' : `#${prior.dupCount}`;
197
+ findings.push({
198
+ source: 'kern',
199
+ ruleId,
200
+ severity: 'warning',
201
+ category: 'bug',
202
+ message: `Duplicate variable "${key}". The last assignment wins; earlier lines are silently overridden when dotenv loads the file.`,
203
+ primarySpan: makeSpan(filePath, i + 1, 1, raw.length + 1),
204
+ relatedSpans: [makeSpan(filePath, prior.firstLine, 1, 1)],
205
+ confidence: 100,
206
+ fingerprint: `${ruleId}:${key}${suffix}`,
207
+ });
208
+ }
209
+ else {
210
+ seen.set(key, { firstLine: i + 1, dupCount: 0 });
211
+ }
212
+ // ── Possible committed secret ──────────────────────────────────────
213
+ if (!suppressSecretRule && SECRET_KEY_RE.test(key)) {
214
+ const valueUnquoted = valueForSecretCheck(rawValue);
215
+ if (valueUnquoted.length > 0 && !isPlaceholder(valueUnquoted)) {
216
+ const ruleId = 'env/possible-secret';
217
+ findings.push({
218
+ source: 'kern',
219
+ ruleId,
220
+ severity: 'warning',
221
+ category: 'bug',
222
+ message: `"${key}" looks like a secret with a real-looking value committed. If this is a placeholder, rename the file to \`.env.example\` (or similar) or use \`<placeholder>\` / \`\${VAR}\` syntax.`,
223
+ primarySpan: makeSpan(filePath, i + 1, 1, raw.length + 1),
224
+ confidence: 70,
225
+ // Fingerprint by key only — same key flagged across edits stays
226
+ // the same finding. We do NOT include the value (changes are
227
+ // exactly the signal we want to NOT suppress).
228
+ fingerprint: `${ruleId}:${key}`,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ return findings;
234
+ }
235
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config-files/env.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * JSON / JSONC analyzer — emits ReviewFindings for parse errors, duplicate
3
+ * keys, and trailing commas in strict JSON. Parallel to python-fallback.ts:
4
+ * a non-ts-morph analysis path that participates in the same finding pipeline
5
+ * so kern-sight (editor diagnostics) and kern-guard (PR Check annotations)
6
+ * both consume it without changes.
7
+ *
8
+ * Dialect detection:
9
+ * .jsonc → JSONC (comments + trailing commas allowed)
10
+ * tsconfig.json / jsconfig.json / .vscode/*.json → JSONC (de facto)
11
+ * everything else .json → strict JSON
12
+ *
13
+ * Fingerprint policy: NOT line-based. Parse errors fingerprint by error code
14
+ * + dialect; duplicate keys fingerprint by key-path. Line numbers shift under
15
+ * unrelated edits — line-based fingerprints would make kern-guard re-post
16
+ * the same finding as "new" on every PR that touched whitespace above it.
17
+ */
18
+ import type { ReviewFinding } from '../types.js';
19
+ /** Entry point for the engine dispatcher. */
20
+ export declare function reviewJsonFile(source: string, filePath: string): ReviewFinding[];
@@ -0,0 +1,220 @@
1
+ /**
2
+ * JSON / JSONC analyzer — emits ReviewFindings for parse errors, duplicate
3
+ * keys, and trailing commas in strict JSON. Parallel to python-fallback.ts:
4
+ * a non-ts-morph analysis path that participates in the same finding pipeline
5
+ * so kern-sight (editor diagnostics) and kern-guard (PR Check annotations)
6
+ * both consume it without changes.
7
+ *
8
+ * Dialect detection:
9
+ * .jsonc → JSONC (comments + trailing commas allowed)
10
+ * tsconfig.json / jsconfig.json / .vscode/*.json → JSONC (de facto)
11
+ * everything else .json → strict JSON
12
+ *
13
+ * Fingerprint policy: NOT line-based. Parse errors fingerprint by error code
14
+ * + dialect; duplicate keys fingerprint by key-path. Line numbers shift under
15
+ * unrelated edits — line-based fingerprints would make kern-guard re-post
16
+ * the same finding as "new" on every PR that touched whitespace above it.
17
+ */
18
+ import { basename } from 'node:path';
19
+ import { parseTree } from 'jsonc-parser';
20
+ function dialectForPath(filePath) {
21
+ if (filePath.endsWith('.jsonc'))
22
+ return 'jsonc';
23
+ // Files inside a `.vscode/` directory (settings.json, launch.json,
24
+ // tasks.json, extensions.json, …) accept JSONC per VS Code convention.
25
+ // Normalize separators so the check works on Windows paths too.
26
+ const normalized = filePath.replace(/\\/g, '/');
27
+ if (normalized.includes('/.vscode/'))
28
+ return 'jsonc';
29
+ // tsconfig + jsconfig allow comments per TS spec — matches `tsconfig.json`,
30
+ // `tsconfig.base.json`, etc. (startsWith already covers `tsconfig.json`.)
31
+ const base = basename(filePath).toLowerCase();
32
+ if (base.startsWith('tsconfig.') || base.startsWith('jsconfig.'))
33
+ return 'jsonc';
34
+ return 'json';
35
+ }
36
+ // jsonc-parser ParseErrorCode is a `const enum` — values are inlined at
37
+ // compile time and there is no runtime reverse-lookup table. Mirror the
38
+ // numeric enum to a stable kebab-case slug here. If jsonc-parser ever
39
+ // renumbers (it has been stable since 1.x) the analyzer will degrade to
40
+ // `unknown-<n>` rather than crash.
41
+ const ERROR_CODE_SLUGS = {
42
+ 1: 'invalid-symbol',
43
+ 2: 'invalid-number-format',
44
+ 3: 'property-name-expected',
45
+ 4: 'value-expected',
46
+ 5: 'colon-expected',
47
+ 6: 'comma-expected',
48
+ 7: 'close-brace-expected',
49
+ 8: 'close-bracket-expected',
50
+ 9: 'end-of-file-expected',
51
+ 10: 'invalid-comment-token',
52
+ 11: 'unexpected-end-of-comment',
53
+ 12: 'unexpected-end-of-string',
54
+ 13: 'unexpected-end-of-number',
55
+ 14: 'invalid-unicode',
56
+ 15: 'invalid-escape-character',
57
+ 16: 'invalid-character',
58
+ };
59
+ function errorCodeSlug(code) {
60
+ return ERROR_CODE_SLUGS[code] ?? `unknown-${code}`;
61
+ }
62
+ function humanize(err, dialect) {
63
+ const slug = errorCodeSlug(err.error);
64
+ if (slug === 'invalid-comment-token' && dialect === 'json') {
65
+ return 'Comments are not allowed in strict JSON. Rename to .jsonc or remove the comment.';
66
+ }
67
+ if (slug === 'value-expected')
68
+ return 'Expected a JSON value (string, number, object, array, boolean, or null).';
69
+ if (slug === 'colon-expected')
70
+ return "Expected ':' between property name and value.";
71
+ if (slug === 'comma-expected')
72
+ return "Expected ',' between elements.";
73
+ if (slug === 'close-brace-expected')
74
+ return "Expected '}' to close object.";
75
+ if (slug === 'close-bracket-expected')
76
+ return "Expected ']' to close array.";
77
+ if (slug === 'invalid-character')
78
+ return 'Invalid character in JSON.';
79
+ if (slug === 'invalid-escape-character')
80
+ return 'Invalid escape sequence in string.';
81
+ if (slug === 'invalid-unicode')
82
+ return 'Invalid Unicode escape sequence.';
83
+ if (slug === 'invalid-number-format')
84
+ return 'Invalid number format.';
85
+ if (slug === 'property-name-expected')
86
+ return 'Expected a property name (a quoted string).';
87
+ if (slug === 'end-of-file-expected')
88
+ return 'Trailing content after end of JSON value.';
89
+ if (slug === 'unexpected-end-of-comment')
90
+ return 'Unclosed comment — reached end of file before `*/`.';
91
+ if (slug === 'unexpected-end-of-string')
92
+ return 'Unclosed string — reached end of file before closing quote.';
93
+ if (slug === 'unexpected-end-of-number')
94
+ return 'Unterminated number literal.';
95
+ if (slug === 'invalid-symbol')
96
+ return 'Unexpected token in JSON.';
97
+ return `JSON parse error: ${slug}.`;
98
+ }
99
+ /** Convert a character offset into the source to a 1-based (line, col) pair. */
100
+ function offsetToLineCol(source, offset) {
101
+ const clamped = Math.max(0, Math.min(offset, source.length));
102
+ let line = 1;
103
+ let lineStart = 0;
104
+ for (let i = 0; i < clamped; i++) {
105
+ if (source.charCodeAt(i) === 0x0a /* \n */) {
106
+ line++;
107
+ lineStart = i + 1;
108
+ }
109
+ }
110
+ return { line, col: clamped - lineStart + 1 };
111
+ }
112
+ function spanAt(source, filePath, offset, length) {
113
+ const start = offsetToLineCol(source, offset);
114
+ const end = offsetToLineCol(source, offset + Math.max(1, length));
115
+ return { file: filePath, startLine: start.line, startCol: start.col, endLine: end.line, endCol: end.col };
116
+ }
117
+ /**
118
+ * Walk the AST collecting duplicate property names within the same object.
119
+ * Builds a structural key-path (e.g. `compilerOptions.paths`) so the
120
+ * fingerprint stays stable across unrelated formatting changes.
121
+ */
122
+ function detectDuplicateKeys(tree, source, filePath) {
123
+ const out = [];
124
+ function visit(node, path) {
125
+ if (node.type === 'object' && node.children) {
126
+ // Track first-occurrence node AND how many duplicates we've already
127
+ // emitted for that key. The count lets us disambiguate fingerprints
128
+ // when the same key appears 3+ times in one object — without a
129
+ // counter, dedup in kern-guard would collapse all but one into the
130
+ // baseline and a later edit could silently drop a real finding.
131
+ const seen = new Map();
132
+ for (const prop of node.children) {
133
+ // jsonc-parser 'property' node: children[0] = key, children[1] = value
134
+ if (prop.type !== 'property' || !prop.children || prop.children.length < 1)
135
+ continue;
136
+ const keyNode = prop.children[0];
137
+ if (!keyNode || keyNode.type !== 'string' || typeof keyNode.value !== 'string')
138
+ continue;
139
+ const key = keyNode.value;
140
+ const childPath = path ? `${path}.${key}` : key;
141
+ const entry = seen.get(key);
142
+ if (entry) {
143
+ entry.dupCount += 1;
144
+ const ruleId = 'json/duplicate-key';
145
+ // Second duplicate keeps the path-only fingerprint (most common
146
+ // case, stable across formatting). Third+ append `#N` so each
147
+ // additional occurrence is independently dedup-able.
148
+ const suffix = entry.dupCount === 1 ? '' : `#${entry.dupCount}`;
149
+ out.push({
150
+ source: 'kern',
151
+ ruleId,
152
+ severity: 'error',
153
+ category: 'bug',
154
+ message: `Duplicate property "${key}" in object. The second value silently overrides the first in JSON.parse.`,
155
+ primarySpan: spanAt(source, filePath, keyNode.offset, keyNode.length),
156
+ relatedSpans: [spanAt(source, filePath, entry.firstNode.offset, entry.firstNode.length)],
157
+ confidence: 100,
158
+ fingerprint: `${ruleId}:${childPath}${suffix}`,
159
+ });
160
+ }
161
+ else {
162
+ seen.set(key, { firstNode: keyNode, dupCount: 0 });
163
+ }
164
+ const valueNode = prop.children[1];
165
+ if (valueNode)
166
+ visit(valueNode, childPath);
167
+ }
168
+ }
169
+ else if (node.type === 'array' && node.children) {
170
+ for (let i = 0; i < node.children.length; i++) {
171
+ const child = node.children[i];
172
+ if (child)
173
+ visit(child, `${path}[${i}]`);
174
+ }
175
+ }
176
+ }
177
+ visit(tree, '');
178
+ return out;
179
+ }
180
+ /** Entry point for the engine dispatcher. */
181
+ export function reviewJsonFile(source, filePath) {
182
+ const dialect = dialectForPath(filePath);
183
+ const errors = [];
184
+ const tree = parseTree(source, errors, {
185
+ disallowComments: dialect === 'json',
186
+ allowTrailingComma: dialect === 'jsonc',
187
+ });
188
+ const findings = [];
189
+ // Track per-(ruleId) occurrence count so multiple parse errors of the
190
+ // same kind (e.g. two separate `invalid-comment-token` in a strict JSON)
191
+ // produce distinct fingerprints. Without this, kern-guard's baseline
192
+ // dedup collapses N independent errors into one and silently hides the
193
+ // others on the next PR.
194
+ const perRuleCount = new Map();
195
+ for (const err of errors) {
196
+ const slug = errorCodeSlug(err.error);
197
+ const ruleId = `json/parse/${slug}`;
198
+ const idx = perRuleCount.get(ruleId) ?? 0;
199
+ perRuleCount.set(ruleId, idx + 1);
200
+ // First occurrence keeps the cleanest fingerprint (most common case
201
+ // is exactly one parse error per file); 2nd+ append `#N` so they are
202
+ // independently dedup-able. Still line-independent.
203
+ const suffix = idx === 0 ? '' : `#${idx}`;
204
+ findings.push({
205
+ source: 'kern',
206
+ ruleId,
207
+ severity: 'error',
208
+ category: 'bug',
209
+ message: humanize(err, dialect),
210
+ primarySpan: spanAt(source, filePath, err.offset, err.length),
211
+ confidence: 100,
212
+ fingerprint: `${ruleId}:${dialect}${suffix}`,
213
+ });
214
+ }
215
+ if (tree) {
216
+ findings.push(...detectDuplicateKeys(tree, source, filePath));
217
+ }
218
+ return findings;
219
+ }
220
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/config-files/json.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Markdown analyzer + outline extractor — self-contained line scanner.
3
+ *
4
+ * Replaces the previous `mdast-util-from-markdown` dependency (which pulled
5
+ * ~30 transitive `micromark-*` packages) with a focused state machine. The
6
+ * tradeoff is deliberate: this is NOT a CommonMark parser. It is a config /
7
+ * docs hygiene scanner that covers exactly what kern-sight and kern-guard
8
+ * need today —
9
+ *
10
+ * • ATX headings (`#` through `######`) outside fenced code
11
+ * • Image syntax `![alt](url)` on a non-code line
12
+ * • Fenced-code awareness (backtick and tilde fences, length-matched)
13
+ * • Outline tree built from heading levels + slugs
14
+ *
15
+ * What we deliberately do NOT handle, because feature surface stays small:
16
+ *
17
+ * • Setext headings (`===` / `---` underlines) — uncommon in this codebase
18
+ * • Inline HTML headings (`<h1>…</h1>`)
19
+ * • Reference-style images (`![alt][label]` + `[label]: url`)
20
+ * • Indented (4-space) code blocks
21
+ * • Tab handling beyond the obvious cases
22
+ * • Inline code spans (`` `![](url)` ``) — image syntax inside backtick
23
+ * spans on an otherwise non-fenced line can trip the image-alt rule.
24
+ * False positives here surface as `md/image-missing-alt` on the URL
25
+ * inside the span. Acceptable for a hygiene scanner; if it becomes
26
+ * a real noise source, suppress with `kern-ignore` directives.
27
+ *
28
+ * If a doc uses those forms, findings on it are best-effort. The point is
29
+ * predictable diagnostics for the common 95% case, not full CommonMark
30
+ * fidelity, and to keep `@kernlang/review` trending toward zero dependencies.
31
+ *
32
+ * Two outputs from a single pass:
33
+ *
34
+ * 1. ReviewFinding[] — structural issues (skipped heading levels, missing
35
+ * image alt text). Flow through the engine's standard pipeline so both
36
+ * kern-sight (editor diagnostics) and kern-guard (Check annotations)
37
+ * consume them without API changes.
38
+ *
39
+ * 2. MarkdownOutline — heading tree shaped for kern-sight's Current File
40
+ * webview. Exported separately because kern-guard has no use for it
41
+ * (only findings get posted to GitHub); keeping it off the engine's
42
+ * ReviewReport keeps the worker-side surface minimal.
43
+ *
44
+ * Fingerprint policy: structural keys (heading path, image alt-text URL),
45
+ * NEVER line numbers, so kern-guard's baseline dedup does not re-post on
46
+ * whitespace edits.
47
+ */
48
+ import type { ReviewFinding } from '../types.js';
49
+ /** A single heading in the outline tree, with nesting. */
50
+ export interface MarkdownOutlineHeading {
51
+ level: 1 | 2 | 3 | 4 | 5 | 6;
52
+ text: string;
53
+ /** GitHub-style slug (lowercase, hyphenated, alphanumerics + hyphens). */
54
+ slug: string;
55
+ /** 1-based start line of the heading marker. */
56
+ line: number;
57
+ children: MarkdownOutlineHeading[];
58
+ }
59
+ export interface MarkdownOutline {
60
+ /** Flat list of headings in source order. */
61
+ flat: MarkdownOutlineHeading[];
62
+ /** Nested heading tree (each heading owns the higher-level headings under it). */
63
+ tree: MarkdownOutlineHeading[];
64
+ }
65
+ /** Entry point for the engine dispatcher — findings only. */
66
+ export declare function reviewMarkdownFile(source: string, filePath: string): ReviewFinding[];
67
+ /** Public outline extractor — kern-sight only. */
68
+ export declare function extractMarkdownOutline(source: string): MarkdownOutline;