@kernlang/review 3.5.6 → 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,262 @@
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
+ /** GitHub-style heading slug — lowercase, strip non-letter / non-digit
49
+ * punctuation across the full Unicode range (so `Café`, `中文`, `Привет`
50
+ * survive), then replace each whitespace char (not runs) with one hyphen.
51
+ * Per-char (not collapsed) replacement matches GitHub's behavior: "API &
52
+ * Usage" becomes "api--usage" because `&` is dropped while both
53
+ * surrounding spaces survive as separate hyphens. */
54
+ function slugify(text) {
55
+ return (text
56
+ .toLowerCase()
57
+ // Allow Unicode letters, digits, underscores (GitHub keeps `_` in slugs),
58
+ // whitespace (collapsed to hyphens below), and hyphens.
59
+ .replace(/[^\p{L}\p{N}_\s-]/gu, '')
60
+ .trim()
61
+ .replace(/\s/g, '-'));
62
+ }
63
+ // Image syntax: `![alt](url)`. Allowed: empty alt, alt with spaces and
64
+ // most punctuation EXCEPT `]`, URL with most chars EXCEPT `)`. Reference-style
65
+ // images (`![alt][label]`) are intentionally not matched — see header comment.
66
+ const IMAGE_RE = /!\[([^\]\n]*)\]\(([^)\n]*)\)/g;
67
+ /**
68
+ * Single-pass scanner. Walks source line by line, tracks open fenced code
69
+ * blocks (backtick or tilde fences), collects ATX headings + image syntax
70
+ * from non-code lines.
71
+ */
72
+ function scanMarkdown(source) {
73
+ const headings = [];
74
+ const images = [];
75
+ // Fenced code state. When inside a fence, both heading and image
76
+ // detection are suppressed. The closing fence must match the opening
77
+ // character AND be at least as long as the opener (CommonMark §4.5).
78
+ let inFence = false;
79
+ let fenceChar = null;
80
+ let fenceLen = 0;
81
+ // Split keeping line numbers 1-based. \r\n is normalized to \n via split
82
+ // on /\r?\n/ so Windows line endings don't break anything.
83
+ const lines = source.split(/\r?\n/);
84
+ for (let i = 0; i < lines.length; i++) {
85
+ const line = lines[i];
86
+ const lineNo = i + 1;
87
+ // Fence detection per CommonMark §4.5:
88
+ // - up to 3 spaces of leading indent (NOT arbitrary tabs/whitespace —
89
+ // a 4-space-indented `` ``` `` is part of an indented code block, not
90
+ // a fence, so it must NOT trigger fence state)
91
+ // - opening fences may carry an info string after the marker run
92
+ // (`` ```js ``); closing fences may have ONLY trailing whitespace.
93
+ //
94
+ // Match opening and closing with different regexes so a line like
95
+ // ` ```text ` while already in a fence doesn't bogusly close it. Both
96
+ // anchor on `^ {0,3}` so a 4-space indent is left to the
97
+ // indented-code-block class (which we don't model — those lines simply
98
+ // don't satisfy heading/image detection either, so they're safe).
99
+ const openFenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
100
+ const closeFenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
101
+ if (inFence) {
102
+ if (closeFenceMatch) {
103
+ const ch = closeFenceMatch[1].charAt(0);
104
+ if (ch === fenceChar && closeFenceMatch[1].length >= fenceLen) {
105
+ inFence = false;
106
+ fenceChar = null;
107
+ fenceLen = 0;
108
+ }
109
+ }
110
+ // Anything else inside a fence is ignored for headings/images.
111
+ continue;
112
+ }
113
+ if (openFenceMatch) {
114
+ inFence = true;
115
+ fenceChar = openFenceMatch[1].charAt(0);
116
+ fenceLen = openFenceMatch[1].length;
117
+ continue;
118
+ }
119
+ // ATX heading: optional ≤3 spaces of indent, 1-6 `#`, REQUIRED space
120
+ // after (per CommonMark) unless the line is just `#` chars. We also
121
+ // strip optional trailing `# …` decoration.
122
+ const headingMatch = line.match(/^ {0,3}(#{1,6})(?:\s+(.*?))?(?:\s+#+\s*)?\s*$/);
123
+ if (headingMatch) {
124
+ const level = headingMatch[1].length;
125
+ // Use the raw heading text as-is — do NOT globally strip ``, *, _
126
+ // characters. That would turn `API_KEY` into `APIKEY` (breaking
127
+ // outline labels) and was a regression vs the prior mdast-backed
128
+ // implementation. A proper inline-span parser would distinguish
129
+ // paired delimiters from literal underscores; we accept that the
130
+ // outline shows raw markdown for headings with inline emphasis and
131
+ // leave true delimiter handling out of scope (matches the module's
132
+ // "hygiene scanner, not CommonMark" contract).
133
+ const text = (headingMatch[2] ?? '').trim();
134
+ const slug = slugify(text) || `heading-${lineNo}`;
135
+ // startCol = where the first `#` sits.
136
+ const startCol = line.length - line.replace(/^ */, '').length + 1;
137
+ headings.push({
138
+ level,
139
+ text,
140
+ slug,
141
+ line: lineNo,
142
+ startCol,
143
+ endCol: line.length + 1,
144
+ });
145
+ continue;
146
+ }
147
+ // Image syntax. matchAll gives us all occurrences on the line with
148
+ // their positions; we collect each as a ScanImage.
149
+ for (const m of line.matchAll(IMAGE_RE)) {
150
+ const idx = m.index ?? 0;
151
+ images.push({
152
+ alt: m[1] ?? '',
153
+ url: (m[2] ?? '').trim(),
154
+ line: lineNo,
155
+ startCol: idx + 1,
156
+ endCol: idx + m[0].length + 1,
157
+ });
158
+ }
159
+ }
160
+ return { headings, images };
161
+ }
162
+ function makeSpan(filePath, line, startCol, endCol) {
163
+ return { file: filePath, startLine: line, startCol, endLine: line, endCol };
164
+ }
165
+ /**
166
+ * Parse markdown once and emit both findings and outline. Internal — public
167
+ * entry points (`reviewMarkdownFile`, `extractMarkdownOutline`) share this.
168
+ */
169
+ function analyze(source, filePath) {
170
+ const { headings, images } = scanMarkdown(source);
171
+ const findings = [];
172
+ // ── Skipped heading levels ──────────────────────────────────────────
173
+ // h1 → h3 is a structural smell (screen-readers, TOC generators get confused).
174
+ // The first heading sets the baseline; after that, level must not jump by
175
+ // more than 1 deeper. Going shallower (h3 → h2) is always fine.
176
+ //
177
+ // The running heading path tracks (level, slug) tuples — NOT just slugs by
178
+ // index. The naive `length >= depth` pop is wrong when levels are skipped:
179
+ // after `# A` + `### B`, the stack length is 2 but B is at depth 3, so a
180
+ // subsequent sibling `### B2` would not pop B and would instead nest
181
+ // *under* B. That would make B2's fingerprint depend on B's text, so
182
+ // renaming B would change B2's fingerprint — kern-guard would re-post B2
183
+ // as a "new" finding on the next PR. Comparing on `.level` avoids that.
184
+ let prevLevel = null;
185
+ const headingPath = [];
186
+ for (const h of headings) {
187
+ while (headingPath.length > 0 && headingPath[headingPath.length - 1].level >= h.level) {
188
+ headingPath.pop();
189
+ }
190
+ headingPath.push({ level: h.level, slug: h.slug });
191
+ if (prevLevel !== null && h.level > prevLevel + 1) {
192
+ const ruleId = 'md/skipped-heading-level';
193
+ const path = headingPath.map((p) => p.slug).join('/');
194
+ findings.push({
195
+ source: 'kern',
196
+ ruleId,
197
+ severity: 'warning',
198
+ category: 'structure',
199
+ message: `Heading jumps from h${prevLevel} to h${h.level} — skipping levels breaks document outline and assistive tech navigation.`,
200
+ primarySpan: makeSpan(filePath, h.line, h.startCol, h.endCol),
201
+ confidence: 95,
202
+ fingerprint: `${ruleId}:${path}`,
203
+ });
204
+ }
205
+ prevLevel = h.level;
206
+ }
207
+ // ── Images missing alt text ─────────────────────────────────────────
208
+ // Empty alt text on an image is an a11y red flag. Decorative images
209
+ // should use alt="" intentionally; the scanner can't distinguish, so we
210
+ // flag all empty-alt images and let the author confirm/suppress.
211
+ for (let i = 0; i < images.length; i++) {
212
+ const img = images[i];
213
+ const alt = img.alt.trim();
214
+ if (alt.length === 0) {
215
+ const ruleId = 'md/image-missing-alt';
216
+ // Fingerprint by URL (stable across line shifts); falls back to a
217
+ // sequence-based key only if the image has no URL at all.
218
+ const key = img.url || `idx-${i}`;
219
+ findings.push({
220
+ source: 'kern',
221
+ ruleId,
222
+ severity: 'warning',
223
+ category: 'structure',
224
+ message: `Image is missing alt text${img.url ? ` (\`${img.url}\`)` : ''}. Provide a description, or use \`![](…)\` only for purely decorative images.`,
225
+ primarySpan: makeSpan(filePath, img.line, img.startCol, img.endCol),
226
+ confidence: 90,
227
+ fingerprint: `${ruleId}:${key}`,
228
+ });
229
+ }
230
+ }
231
+ // ── Build outline ───────────────────────────────────────────────────
232
+ const flat = headings.map((h) => ({
233
+ level: h.level,
234
+ text: h.text,
235
+ slug: h.slug,
236
+ line: h.line,
237
+ children: [],
238
+ }));
239
+ // Nest into a tree by level — each heading owns subsequent deeper-level
240
+ // headings until a shallower heading closes the chain. Standard outline algo.
241
+ const outlineTree = [];
242
+ const stack = [];
243
+ for (const h of flat) {
244
+ while (stack.length > 0 && stack[stack.length - 1].level >= h.level)
245
+ stack.pop();
246
+ if (stack.length === 0)
247
+ outlineTree.push(h);
248
+ else
249
+ stack[stack.length - 1].children.push(h);
250
+ stack.push(h);
251
+ }
252
+ return { findings, outline: { flat, tree: outlineTree } };
253
+ }
254
+ /** Entry point for the engine dispatcher — findings only. */
255
+ export function reviewMarkdownFile(source, filePath) {
256
+ return analyze(source, filePath).findings;
257
+ }
258
+ /** Public outline extractor — kern-sight only. */
259
+ export function extractMarkdownOutline(source) {
260
+ return analyze(source, '<inline>').outline;
261
+ }
262
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/config-files/markdown.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export type { ConceptRule, ConceptRuleContext } from './concept-rules/index.js';
17
17
  export { runConceptRules } from './concept-rules/index.js';
18
18
  export type { ConfidenceGraph, ConfidenceNode, ConfidenceSpec, ConfidenceSummary, DuplicateNameEntry, MultiFileConfidenceGraph, NeedsEntry, SerializedConfidenceGraph, } from './confidence.js';
19
19
  export { buildConfidenceGraph, buildMultiFileConfidenceGraph, computeConfidenceSummary, parseConfidence, propagateConfidence, resolveBaseConfidence, serializeGraph, } from './confidence.js';
20
+ export { extractMarkdownOutline, type MarkdownOutline, type MarkdownOutlineHeading } from './config-files/markdown.js';
20
21
  export { structuralDiff } from './differ.js';
21
22
  export { evaluateReviewReports, formatReviewEvalSummary, normalizeReviewEvalManifest, type ReviewEvalCase, type ReviewEvalCaseConfig, type ReviewEvalCaseResult, type ReviewEvalExpectations, type ReviewEvalFindingExpectation, type ReviewEvalManifest, type ReviewEvalRunMetadata, type ReviewEvalSummary, summarizeReviewEvalResults, } from './eval.js';
22
23
  export { linkToNodes, runESLint, runTSCDiagnostics, runTSCDiagnosticsFromPaths } from './external-tools.js';
@@ -104,7 +105,7 @@ export declare function extractConceptsForGraph(filePaths: string[]): Map<string
104
105
  /**
105
106
  * Review a single file. Auto-detects language from extension.
106
107
  * Uses a filesystem-backed ts-morph Project for type-aware analysis.
107
- * Supports: .ts, .tsx, .py, .kern
108
+ * Supports: .ts, .tsx, .py, .kern, .json, .jsonc
108
109
  */
109
110
  export declare function reviewFile(filePath: string, config?: ReviewConfig): ReviewReport;
110
111
  /**
package/dist/index.js CHANGED
@@ -34,6 +34,9 @@ import { buildCallGraph } from './call-graph.js';
34
34
  import { runConceptRules } from './concept-rules/index.js';
35
35
  import { isAuthEndpointTarget, isWorkerContextFile } from './concept-rules/unguarded-effect.js';
36
36
  import { isTransportPrimitiveCarveOut } from './concept-rules/unrecovered-effect.js';
37
+ import { isEnvFile, reviewEnvFile } from './config-files/env.js';
38
+ import { reviewJsonFile } from './config-files/json.js';
39
+ import { reviewMarkdownFile } from './config-files/markdown.js';
37
40
  import { structuralDiff } from './differ.js';
38
41
  import { runTSCDiagnostics } from './external-tools.js';
39
42
  import { buildFileContextMap } from './file-context.js';
@@ -150,6 +153,7 @@ export { buildCallGraph } from './call-graph.js';
150
153
  export { runConceptRules } from './concept-rules/index.js';
151
154
  // Confidence layer
152
155
  export { buildConfidenceGraph, buildMultiFileConfidenceGraph, computeConfidenceSummary, parseConfidence, propagateConfidence, resolveBaseConfidence, serializeGraph, } from './confidence.js';
156
+ export { extractMarkdownOutline } from './config-files/markdown.js';
153
157
  export { structuralDiff } from './differ.js';
154
158
  export { evaluateReviewReports, formatReviewEvalSummary, normalizeReviewEvalManifest, summarizeReviewEvalResults, } from './eval.js';
155
159
  export { linkToNodes, runESLint, runTSCDiagnostics, runTSCDiagnosticsFromPaths } from './external-tools.js';
@@ -321,8 +325,24 @@ const REVIEWABLE_EXTENSIONS = new Set([
321
325
  '.kern',
322
326
  '.py',
323
327
  '.vue',
328
+ // Config-file analyzers — parallel non-ts-morph path (config-files/*.ts).
329
+ // Findings flow through the same ReviewFinding pipeline so kern-sight and
330
+ // kern-guard consume them without API changes.
331
+ '.json',
332
+ '.jsonc',
333
+ '.md',
324
334
  ]);
335
+ /** Files routed to the config-files analyzers, bypassing ts-morph entirely.
336
+ * Includes extension-keyed analyzers (.json/.jsonc/.md) plus basename-keyed
337
+ * analyzers (`.env`, `.env.local`, `.env.production`, …). */
338
+ function isConfigFile(filePath) {
339
+ return filePath.endsWith('.json') || filePath.endsWith('.jsonc') || filePath.endsWith('.md') || isEnvFile(filePath);
340
+ }
325
341
  export function isReviewableFile(filePath) {
342
+ // Basename-keyed analyzers come first — `.env.local` would yield extension
343
+ // `.local` under the simple lastIndexOf shortcut and be misclassified.
344
+ if (isEnvFile(filePath))
345
+ return true;
326
346
  const dot = filePath.lastIndexOf('.');
327
347
  if (dot === -1)
328
348
  return false;
@@ -380,10 +400,48 @@ function emptyReport(filePath) {
380
400
  },
381
401
  };
382
402
  }
403
+ /**
404
+ * Build a ReviewReport for a config-file (.json / .jsonc) source. Bypasses
405
+ * ts-morph entirely — config analyzers emit ReviewFindings directly. Stats
406
+ * are minimal because IR/token reduction is meaningless for these files.
407
+ */
408
+ function reviewConfigFileSource(source, filePath) {
409
+ let findings = [];
410
+ // Basename-keyed env check FIRST — `.env.md` / `.env.json` would otherwise
411
+ // route to the markdown/JSON analyzer because `endsWith` would match. This
412
+ // mirrors the basename-first ordering already used in `isReviewableFile`.
413
+ if (isEnvFile(filePath)) {
414
+ findings = reviewEnvFile(source, filePath);
415
+ }
416
+ else if (filePath.endsWith('.jsonc') || filePath.endsWith('.json')) {
417
+ // Check `.jsonc` before `.json` — `.jsonc` also satisfies `endsWith('.json')`
418
+ // and we want the JSONC dispatch to be explicit rather than rely on the
419
+ // dialect-detection re-check inside `reviewJsonFile` to bail us out.
420
+ findings = reviewJsonFile(source, filePath);
421
+ }
422
+ else if (filePath.endsWith('.md')) {
423
+ findings = reviewMarkdownFile(source, filePath);
424
+ }
425
+ return {
426
+ filePath,
427
+ inferred: [],
428
+ templateMatches: [],
429
+ findings,
430
+ stats: {
431
+ totalLines: source.split('\n').length,
432
+ coveredLines: 0,
433
+ coveragePct: 0,
434
+ totalTsTokens: 0,
435
+ totalKernTokens: 0,
436
+ reductionPct: 0,
437
+ constructCount: 0,
438
+ },
439
+ };
440
+ }
383
441
  /**
384
442
  * Review a single file. Auto-detects language from extension.
385
443
  * Uses a filesystem-backed ts-morph Project for type-aware analysis.
386
- * Supports: .ts, .tsx, .py, .kern
444
+ * Supports: .ts, .tsx, .py, .kern, .json, .jsonc
387
445
  */
388
446
  export function reviewFile(filePath, config) {
389
447
  if (!isReviewableFile(filePath))
@@ -392,7 +450,7 @@ export function reviewFile(filePath, config) {
392
450
  // Resolve the effective tsconfig up-front so both the cache key and the ts-morph Project see the
393
451
  // same path. If we only discovered it later inside reviewSourceWithProject, adding or changing the
394
452
  // nearest tsconfig without editing the source would serve stale cached findings.
395
- const effectiveConfig = config?.tsConfigFilePath || filePath.endsWith('.kern') || filePath.endsWith('.py')
453
+ const effectiveConfig = config?.tsConfigFilePath || filePath.endsWith('.kern') || filePath.endsWith('.py') || isConfigFile(filePath)
396
454
  ? config
397
455
  : { ...(config ?? {}), tsConfigFilePath: findTsConfig(dirname(filePath)) };
398
456
  let key;
@@ -403,7 +461,10 @@ export function reviewFile(filePath, config) {
403
461
  return cached;
404
462
  }
405
463
  let report;
406
- if (filePath.endsWith('.kern')) {
464
+ if (isConfigFile(filePath)) {
465
+ report = reviewConfigFileSource(source, filePath);
466
+ }
467
+ else if (filePath.endsWith('.kern')) {
407
468
  report = reviewKernSource(source, filePath, effectiveConfig);
408
469
  }
409
470
  else if (filePath.endsWith('.py')) {