@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.
- package/dist/config-files/env.d.ts +45 -0
- package/dist/config-files/env.js +235 -0
- package/dist/config-files/env.js.map +1 -0
- package/dist/config-files/json.d.ts +20 -0
- package/dist/config-files/json.js +220 -0
- package/dist/config-files/json.js.map +1 -0
- package/dist/config-files/markdown.d.ts +68 -0
- package/dist/config-files/markdown.js +262 -0
- package/dist/config-files/markdown.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -3
- package/dist/index.js.map +1 -1
- package/dist/rules/index.js +1 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/security-v2.js +1 -1
- package/dist/rules/security-v2.js.map +1 -1
- package/dist/rules/security-v4.js +4 -4
- package/dist/rules/security-v4.js.map +1 -1
- package/dist/rules/security.js +6 -4
- package/dist/rules/security.js.map +1 -1
- package/dist/taint-findings.js +7 -1
- package/dist/taint-findings.js.map +1 -1
- package/package.json +3 -2
|
@@ -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 `` 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 (`` `` ``) — 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: ``. 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
|
|
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')) {
|