@kernlang/review 3.3.8 → 3.4.0

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 (116) hide show
  1. package/dist/cache.js +1 -1
  2. package/dist/call-graph.d.ts +10 -0
  3. package/dist/call-graph.js +138 -9
  4. package/dist/call-graph.js.map +1 -1
  5. package/dist/concept-rules/auth-drift.js +2 -0
  6. package/dist/concept-rules/auth-drift.js.map +1 -1
  7. package/dist/concept-rules/auth-propagation-drift.d.ts +10 -0
  8. package/dist/concept-rules/auth-propagation-drift.js +85 -0
  9. package/dist/concept-rules/auth-propagation-drift.js.map +1 -0
  10. package/dist/concept-rules/body-shape-drift.d.ts +32 -0
  11. package/dist/concept-rules/body-shape-drift.js +98 -0
  12. package/dist/concept-rules/body-shape-drift.js.map +1 -0
  13. package/dist/concept-rules/contract-drift.js +3 -1
  14. package/dist/concept-rules/contract-drift.js.map +1 -1
  15. package/dist/concept-rules/contract-method-drift.js +2 -0
  16. package/dist/concept-rules/contract-method-drift.js.map +1 -1
  17. package/dist/concept-rules/cross-stack-utils.d.ts +24 -0
  18. package/dist/concept-rules/cross-stack-utils.js +123 -29
  19. package/dist/concept-rules/cross-stack-utils.js.map +1 -1
  20. package/dist/concept-rules/index.d.ts +4 -2
  21. package/dist/concept-rules/index.js +22 -3
  22. package/dist/concept-rules/index.js.map +1 -1
  23. package/dist/concept-rules/mutation-without-idempotency.d.ts +10 -0
  24. package/dist/concept-rules/mutation-without-idempotency.js +47 -0
  25. package/dist/concept-rules/mutation-without-idempotency.js.map +1 -0
  26. package/dist/concept-rules/request-validation-drift.d.ts +11 -0
  27. package/dist/concept-rules/request-validation-drift.js +99 -0
  28. package/dist/concept-rules/request-validation-drift.js.map +1 -0
  29. package/dist/concept-rules/root-cause.d.ts +4 -0
  30. package/dist/concept-rules/root-cause.js +31 -0
  31. package/dist/concept-rules/root-cause.js.map +1 -0
  32. package/dist/concept-rules/unbounded-collection-query.d.ts +10 -0
  33. package/dist/concept-rules/unbounded-collection-query.js +58 -0
  34. package/dist/concept-rules/unbounded-collection-query.js.map +1 -0
  35. package/dist/concept-rules/unhandled-api-error-shape.d.ts +10 -0
  36. package/dist/concept-rules/unhandled-api-error-shape.js +59 -0
  37. package/dist/concept-rules/unhandled-api-error-shape.js.map +1 -0
  38. package/dist/default-export.d.ts +41 -0
  39. package/dist/default-export.js +76 -0
  40. package/dist/default-export.js.map +1 -0
  41. package/dist/eval.d.ts +67 -0
  42. package/dist/eval.js +177 -0
  43. package/dist/eval.js.map +1 -0
  44. package/dist/external-tools.js +52 -3
  45. package/dist/external-tools.js.map +1 -1
  46. package/dist/file-context.js +32 -13
  47. package/dist/file-context.js.map +1 -1
  48. package/dist/file-role.d.ts +6 -0
  49. package/dist/file-role.js +27 -0
  50. package/dist/file-role.js.map +1 -1
  51. package/dist/framework-seeds.d.ts +46 -0
  52. package/dist/framework-seeds.js +245 -0
  53. package/dist/framework-seeds.js.map +1 -0
  54. package/dist/git-env.d.ts +1 -0
  55. package/dist/git-env.js +25 -0
  56. package/dist/git-env.js.map +1 -0
  57. package/dist/graph.js +246 -21
  58. package/dist/graph.js.map +1 -1
  59. package/dist/index.d.ts +12 -3
  60. package/dist/index.js +314 -96
  61. package/dist/index.js.map +1 -1
  62. package/dist/mappers/ts-concepts.js +730 -1
  63. package/dist/mappers/ts-concepts.js.map +1 -1
  64. package/dist/path-canonical.d.ts +34 -0
  65. package/dist/path-canonical.js +85 -0
  66. package/dist/path-canonical.js.map +1 -0
  67. package/dist/policy.d.ts +22 -0
  68. package/dist/policy.js +47 -0
  69. package/dist/policy.js.map +1 -0
  70. package/dist/project-context.d.ts +135 -0
  71. package/dist/project-context.js +563 -0
  72. package/dist/project-context.js.map +1 -0
  73. package/dist/public-api.d.ts +21 -0
  74. package/dist/public-api.js +17 -2
  75. package/dist/public-api.js.map +1 -1
  76. package/dist/python-fallback.d.ts +2 -0
  77. package/dist/python-fallback.js +506 -0
  78. package/dist/python-fallback.js.map +1 -0
  79. package/dist/reporter.js +106 -1
  80. package/dist/reporter.js.map +1 -1
  81. package/dist/rule-quality.d.ts +58 -0
  82. package/dist/rule-quality.js +357 -0
  83. package/dist/rule-quality.js.map +1 -0
  84. package/dist/rules/base.js +21 -3
  85. package/dist/rules/base.js.map +1 -1
  86. package/dist/rules/dead-code.d.ts +2 -2
  87. package/dist/rules/dead-code.js +88 -4
  88. package/dist/rules/dead-code.js.map +1 -1
  89. package/dist/rules/index.d.ts +22 -0
  90. package/dist/rules/index.js +72 -0
  91. package/dist/rules/index.js.map +1 -1
  92. package/dist/rules/kern-source.d.ts +4 -0
  93. package/dist/rules/kern-source.js +184 -0
  94. package/dist/rules/kern-source.js.map +1 -1
  95. package/dist/rules/react.js +52 -3
  96. package/dist/rules/react.js.map +1 -1
  97. package/dist/rules/suggest-kern-primitive.js +0 -1
  98. package/dist/rules/suggest-kern-primitive.js.map +1 -1
  99. package/dist/semantic-diff.js +2 -0
  100. package/dist/semantic-diff.js.map +1 -1
  101. package/dist/suppression/apply-suppression.js +2 -0
  102. package/dist/suppression/apply-suppression.js.map +1 -1
  103. package/dist/suppression/parse-directives.d.ts +13 -5
  104. package/dist/suppression/parse-directives.js +62 -8
  105. package/dist/suppression/parse-directives.js.map +1 -1
  106. package/dist/suppression/types.d.ts +9 -0
  107. package/dist/suppression/types.js +6 -1
  108. package/dist/suppression/types.js.map +1 -1
  109. package/dist/taint-crossfile.js +15 -8
  110. package/dist/taint-crossfile.js.map +1 -1
  111. package/dist/telemetry.d.ts +126 -0
  112. package/dist/telemetry.js +303 -0
  113. package/dist/telemetry.js.map +1 -0
  114. package/dist/types.d.ts +172 -2
  115. package/dist/types.js.map +1 -1
  116. package/package.json +4 -3
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Project-Context — repo-level signals for the review pipeline.
3
+ *
4
+ * Reads project configs (tsconfig.json, package.json) and .gitignore so rules
5
+ * can gate on what the user already enforces elsewhere. Designed to be SAFE on
6
+ * adversarial inputs:
7
+ *
8
+ * - **JSON-only.** No executable config readers (no eslint.config.js eval).
9
+ * Phase 2 may add YAML/TOML but only via safeLoad; never `require()` of a
10
+ * user-controlled file.
11
+ * - **Realpath containment.** Any path resolved from a config (extends, etc.)
12
+ * must live under the project root after `realpathSync` — otherwise it is
13
+ * ignored. Defends against `extends: '../../../etc/passwd'` and symlink
14
+ * traversal attacks surfaced by the Phase 1 red-team.
15
+ * - **Content-hash cache.** Cache key is hashed file content + extends chain
16
+ * (reuses the cache.ts pattern). mtime is unsound on second-resolution
17
+ * filesystems and on TOCTOU windows where bytes change but mtime does not.
18
+ * - **LRU eviction.** Max 128 cached project roots — defense against the
19
+ * long-running Guard bot accumulating per-PR worktree paths until OOM.
20
+ * - **Pattern-length cap on .gitignore.** Discards any individual pattern
21
+ * longer than 256 chars. Defense against ReDoS via crafted negation +
22
+ * quantifier patterns from the red-team.
23
+ */
24
+ /** What `getProjectContext` returns. Extended in later phases. */
25
+ export interface ProjectContext {
26
+ /** Absolute, realpath-resolved project root. */
27
+ root: string;
28
+ /** Parsed root package.json — restricted to fields we care about. */
29
+ packageJson?: ProjectPackageJson;
30
+ /** Parsed tsconfig (top-level only — extends chain is hashed but not deep-merged here). */
31
+ tsconfig?: ProjectTsconfig;
32
+ /** Compiled .gitignore matchers, in walk order (root first, deeper later). */
33
+ gitignore: GitignoreMatchers;
34
+ /**
35
+ * External linter configurations — used to demote kern findings that overlap
36
+ * with rules the project already enforces. Phase 2 is intentionally limited
37
+ * to JSON-only readers: `.eslintrc.json`, `package.json` `eslintConfig`, and
38
+ * `biome.json`. `eslint.config.js` is **never** evaluated (RCE risk surfaced
39
+ * by Phase 1 red-team). Per-file `overrides` resolution requires the async
40
+ * ESLint API and is left to callers via a future pre-warm path.
41
+ */
42
+ external: ExternalLinterConfig;
43
+ /**
44
+ * Set of POSIX-relative paths that `git ls-files` reports as tracked. A file
45
+ * being tracked overrides .gitignore for skip-list purposes — published
46
+ * artifacts (e.g. packages/sdk/dist/client.gen.ts) get reviewed even when
47
+ * the directory matches `.gitignore`. Empty set if not a git repo.
48
+ */
49
+ gitTrackedFiles: Set<string>;
50
+ /**
51
+ * Stable hash of every config input that contributed to this context. Used as
52
+ * the cache key; if any config file changes, the hash changes and the entry
53
+ * is recomputed.
54
+ */
55
+ contentHash: string;
56
+ }
57
+ export interface ProjectPackageJson {
58
+ name?: string;
59
+ type?: 'module' | 'commonjs';
60
+ workspaces?: string[];
61
+ bin?: Record<string, string> | string;
62
+ exports?: unknown;
63
+ private?: boolean;
64
+ }
65
+ export interface ProjectTsconfig {
66
+ /** True iff `compilerOptions.strict === true` is set on the resolved config. */
67
+ strict?: boolean;
68
+ /** Per-flag — overrides composite `strict`. Future phases dial confidence per flag. */
69
+ strictNullChecks?: boolean;
70
+ noImplicitAny?: boolean;
71
+ noUnusedLocals?: boolean;
72
+ noUnusedParameters?: boolean;
73
+ }
74
+ /** External linter rule IDs that are enabled at error/warn. */
75
+ export interface ExternalLinterConfig {
76
+ /** Rule IDs from `.eslintrc.json` / `package.json` eslintConfig that are at error or warn. */
77
+ eslintEnabledRules: Set<string>;
78
+ /** Rule IDs from `biome.json` linter.rules that are at error or warn. */
79
+ biomeEnabledRules: Set<string>;
80
+ }
81
+ /** Compiled gitignore matchers. Use `isPathIgnored` to query. */
82
+ export interface GitignoreMatchers {
83
+ /** Patterns from the project root's .gitignore, in declaration order. */
84
+ rootPatterns: GitignorePattern[];
85
+ }
86
+ export interface GitignorePattern {
87
+ /** Original line as written, post-trim. */
88
+ raw: string;
89
+ /** Compiled regex. Pattern-length capped at 256 to avoid ReDoS. */
90
+ regex: RegExp;
91
+ /** Negation rule (`!foo`) — re-includes a previously-ignored path. */
92
+ negate: boolean;
93
+ /** Pattern is anchored to repo root (no slash in middle of pattern). */
94
+ matchDirsOnly: boolean;
95
+ }
96
+ /**
97
+ * Walk up from a starting directory looking for the nearest `package.json`.
98
+ * Returns undefined if none is found before the filesystem root. Used by
99
+ * per-file review entry points that need a project context but only have a
100
+ * file path.
101
+ */
102
+ export declare function findProjectRoot(startDir: string): string | undefined;
103
+ /**
104
+ * Get the project context for a project root. Cached by content hash —
105
+ * a config file edit invalidates the entry on next call.
106
+ */
107
+ export declare function getProjectContext(projectRoot: string): ProjectContext;
108
+ /** Test-only: clear cache between tests. */
109
+ export declare function _resetProjectContextCache(): void;
110
+ /** Test-only: report current cache size. */
111
+ export declare function _projectContextCacheSize(): number;
112
+ /**
113
+ * Returns true iff the file is matched by the project's .gitignore. Use
114
+ * `isReviewable` for the full skip-list semantics — this is the gitignore
115
+ * predicate alone.
116
+ */
117
+ export declare function isPathIgnored(filePath: string, ctx: ProjectContext): boolean;
118
+ /**
119
+ * Per-flag tsconfig strictness — phase 2.3 from the red-team. The composite
120
+ * `strict: true` is shorthand for several flags; users frequently enable
121
+ * `strictNullChecks` alone without the umbrella. Rules should query the
122
+ * specific flag they depend on, not `strict`, so that `strict:false` does
123
+ * not over-debuff a finding whose underlying guarantee is in fact present.
124
+ */
125
+ export declare function isStrictFlagEffective(flag: keyof Required<ProjectTsconfig>, ctx: ProjectContext): boolean;
126
+ /**
127
+ * The full skip-list predicate. A file is reviewable iff it is NOT
128
+ * (gitignored AND not git-tracked).
129
+ *
130
+ * This is the Phase 1 red-team's finding #4 fix: a tracked artifact that lives
131
+ * inside a gitignored directory (the classic `packages/sdk/dist/client.gen.ts`
132
+ * case) must remain reviewable. Suppression-by-skip-list is reserved for
133
+ * truly-untracked outputs.
134
+ */
135
+ export declare function isReviewable(filePath: string, ctx: ProjectContext): boolean;
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Project-Context — repo-level signals for the review pipeline.
3
+ *
4
+ * Reads project configs (tsconfig.json, package.json) and .gitignore so rules
5
+ * can gate on what the user already enforces elsewhere. Designed to be SAFE on
6
+ * adversarial inputs:
7
+ *
8
+ * - **JSON-only.** No executable config readers (no eslint.config.js eval).
9
+ * Phase 2 may add YAML/TOML but only via safeLoad; never `require()` of a
10
+ * user-controlled file.
11
+ * - **Realpath containment.** Any path resolved from a config (extends, etc.)
12
+ * must live under the project root after `realpathSync` — otherwise it is
13
+ * ignored. Defends against `extends: '../../../etc/passwd'` and symlink
14
+ * traversal attacks surfaced by the Phase 1 red-team.
15
+ * - **Content-hash cache.** Cache key is hashed file content + extends chain
16
+ * (reuses the cache.ts pattern). mtime is unsound on second-resolution
17
+ * filesystems and on TOCTOU windows where bytes change but mtime does not.
18
+ * - **LRU eviction.** Max 128 cached project roots — defense against the
19
+ * long-running Guard bot accumulating per-PR worktree paths until OOM.
20
+ * - **Pattern-length cap on .gitignore.** Discards any individual pattern
21
+ * longer than 256 chars. Defense against ReDoS via crafted negation +
22
+ * quantifier patterns from the red-team.
23
+ */
24
+ import { execFileSync } from 'child_process';
25
+ import { createHash } from 'crypto';
26
+ import { existsSync, readFileSync, realpathSync } from 'fs';
27
+ import { dirname, isAbsolute, relative, resolve, sep } from 'path';
28
+ import { withoutLocalGitEnv } from './git-env.js';
29
+ /** Maximum pattern length for a single .gitignore entry (red-team P1 ReDoS guard). */
30
+ const MAX_GITIGNORE_PATTERN_LENGTH = 256;
31
+ /** LRU cap for cached project contexts (red-team P1 OOM guard for Guard bot). */
32
+ const CONTEXT_CACHE_CAP = 128;
33
+ /** Map iteration order is insertion order; deletes + re-inserts give LRU. */
34
+ const contextCache = new Map();
35
+ /**
36
+ * Walk up from a starting directory looking for the nearest `package.json`.
37
+ * Returns undefined if none is found before the filesystem root. Used by
38
+ * per-file review entry points that need a project context but only have a
39
+ * file path.
40
+ */
41
+ export function findProjectRoot(startDir) {
42
+ let cur = resolve(startDir);
43
+ while (true) {
44
+ if (existsSync(resolve(cur, 'package.json')))
45
+ return cur;
46
+ const parent = dirname(cur);
47
+ if (parent === cur)
48
+ return undefined;
49
+ cur = parent;
50
+ }
51
+ }
52
+ /**
53
+ * Get the project context for a project root. Cached by content hash —
54
+ * a config file edit invalidates the entry on next call.
55
+ */
56
+ export function getProjectContext(projectRoot) {
57
+ const root = safeRealpath(projectRoot);
58
+ if (!root) {
59
+ return emptyContext(projectRoot);
60
+ }
61
+ const probe = computeContentHash(root);
62
+ const cached = contextCache.get(root);
63
+ if (cached && cached.hash === probe) {
64
+ // LRU touch: delete + re-insert moves it to most-recently-used.
65
+ contextCache.delete(root);
66
+ contextCache.set(root, cached);
67
+ return cached.context;
68
+ }
69
+ const context = buildContext(root, probe);
70
+ contextCache.set(root, { hash: probe, context });
71
+ // LRU eviction.
72
+ while (contextCache.size > CONTEXT_CACHE_CAP) {
73
+ const oldestKey = contextCache.keys().next().value;
74
+ if (oldestKey === undefined)
75
+ break;
76
+ contextCache.delete(oldestKey);
77
+ }
78
+ return context;
79
+ }
80
+ /** Test-only: clear cache between tests. */
81
+ export function _resetProjectContextCache() {
82
+ contextCache.clear();
83
+ }
84
+ /** Test-only: report current cache size. */
85
+ export function _projectContextCacheSize() {
86
+ return contextCache.size;
87
+ }
88
+ /**
89
+ * Returns true iff the file is matched by the project's .gitignore. Use
90
+ * `isReviewable` for the full skip-list semantics — this is the gitignore
91
+ * predicate alone.
92
+ */
93
+ export function isPathIgnored(filePath, ctx) {
94
+ const rel = toRelative(ctx.root, filePath);
95
+ if (rel === undefined)
96
+ return false;
97
+ let ignored = false;
98
+ for (const pattern of ctx.gitignore.rootPatterns) {
99
+ if (pattern.regex.test(rel)) {
100
+ ignored = !pattern.negate;
101
+ }
102
+ }
103
+ return ignored;
104
+ }
105
+ /**
106
+ * Per-flag tsconfig strictness — phase 2.3 from the red-team. The composite
107
+ * `strict: true` is shorthand for several flags; users frequently enable
108
+ * `strictNullChecks` alone without the umbrella. Rules should query the
109
+ * specific flag they depend on, not `strict`, so that `strict:false` does
110
+ * not over-debuff a finding whose underlying guarantee is in fact present.
111
+ */
112
+ export function isStrictFlagEffective(flag, ctx) {
113
+ const ts = ctx.tsconfig;
114
+ if (!ts)
115
+ return false;
116
+ if (ts[flag] === true)
117
+ return true;
118
+ // The umbrella `strict: true` enables strictNullChecks, noImplicitAny, and
119
+ // a handful of others. Treat it as setting them when not explicitly false.
120
+ if (ts.strict === true && (flag === 'strictNullChecks' || flag === 'noImplicitAny') && ts[flag] !== false) {
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+ /**
126
+ * The full skip-list predicate. A file is reviewable iff it is NOT
127
+ * (gitignored AND not git-tracked).
128
+ *
129
+ * This is the Phase 1 red-team's finding #4 fix: a tracked artifact that lives
130
+ * inside a gitignored directory (the classic `packages/sdk/dist/client.gen.ts`
131
+ * case) must remain reviewable. Suppression-by-skip-list is reserved for
132
+ * truly-untracked outputs.
133
+ */
134
+ export function isReviewable(filePath, ctx) {
135
+ const rel = toRelative(ctx.root, filePath);
136
+ if (rel === undefined)
137
+ return true; // Outside project root — caller decides.
138
+ if (ctx.gitTrackedFiles.has(rel))
139
+ return true;
140
+ return !isPathIgnored(filePath, ctx);
141
+ }
142
+ // ── Implementation ─────────────────────────────────────────────────────────
143
+ function buildContext(root, contentHash) {
144
+ const packageJson = readJson(root, 'package.json');
145
+ return {
146
+ root,
147
+ packageJson,
148
+ tsconfig: readTsconfig(root),
149
+ gitignore: readGitignore(root),
150
+ gitTrackedFiles: readGitTrackedFiles(root),
151
+ external: readExternalLinterConfig(root, packageJson),
152
+ contentHash,
153
+ };
154
+ }
155
+ function emptyContext(projectRoot) {
156
+ return {
157
+ root: resolve(projectRoot),
158
+ gitignore: { rootPatterns: [] },
159
+ gitTrackedFiles: new Set(),
160
+ external: { eslintEnabledRules: new Set(), biomeEnabledRules: new Set() },
161
+ contentHash: '',
162
+ };
163
+ }
164
+ /**
165
+ * Read project-level external linter configs. JSON-only and bounded:
166
+ *
167
+ * - **No eslint.config.js eval.** Phase 1 red-team flagged require() of an
168
+ * attacker-controlled config file as straight RCE. We read `.eslintrc.json`,
169
+ * `.eslintrc` (assumed JSON), and `package.json` `eslintConfig` only. If the
170
+ * project uses flat config (eslint.config.js), this reader returns empty;
171
+ * overlap calibration just doesn't fire — fail-safe.
172
+ * - **Relative-only extends.** Same containment rule as tsconfig: a string
173
+ * starting with `.` is followed if it resolves under the project root.
174
+ * Package refs (`eslint:recommended`, `@scope/eslint-config`) are NOT
175
+ * resolved — we don't reach into node_modules.
176
+ * - **Single tier of overrides ignored.** Only the top-level `rules` block is
177
+ * consumed. Per-file `overrides` requires the async ESLint API; reading them
178
+ * out of context here would be incorrect (different files would see the
179
+ * same merged set), so we skip entirely.
180
+ */
181
+ function readExternalLinterConfig(root, packageJson) {
182
+ const eslintEnabledRules = new Set();
183
+ const biomeEnabledRules = new Set();
184
+ // ── ESLint ──
185
+ // Priority: .eslintrc.json → .eslintrc → package.json#eslintConfig.
186
+ const eslintRoots = [];
187
+ for (const name of ['.eslintrc.json', '.eslintrc']) {
188
+ const data = readJson(root, name);
189
+ if (data) {
190
+ eslintRoots.push(data);
191
+ break;
192
+ }
193
+ }
194
+ if (packageJson?.eslintConfig)
195
+ eslintRoots.push(packageJson.eslintConfig);
196
+ for (const r of eslintRoots) {
197
+ collectEslintRules(root, r, eslintEnabledRules, new Set(), 0);
198
+ }
199
+ // ── Biome ──
200
+ const biome = readJson(root, 'biome.json');
201
+ if (biome)
202
+ collectBiomeRules(root, biome, biomeEnabledRules, new Set(), 0);
203
+ return { eslintEnabledRules, biomeEnabledRules };
204
+ }
205
+ function collectEslintRules(root, config, out, seen, depth) {
206
+ if (depth > 10)
207
+ return;
208
+ if (!config || typeof config !== 'object')
209
+ return;
210
+ const cfg = config;
211
+ // Walk relative `extends`. Package refs are skipped (see header doc).
212
+ const extendsList = Array.isArray(cfg.extends) ? cfg.extends : typeof cfg.extends === 'string' ? [cfg.extends] : [];
213
+ for (const ext of extendsList) {
214
+ if (typeof ext !== 'string' || !ext.startsWith('.'))
215
+ continue;
216
+ const candidate = resolve(root, ext);
217
+ const real = safeRealpath(candidate);
218
+ if (!real || !isWithin(root, real) || seen.has(real))
219
+ continue;
220
+ seen.add(real);
221
+ if (!existsSync(real))
222
+ continue;
223
+ let extData;
224
+ try {
225
+ extData = JSON.parse(readFileSync(real, 'utf-8').replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ''));
226
+ }
227
+ catch {
228
+ continue;
229
+ }
230
+ collectEslintRules(root, extData, out, seen, depth + 1);
231
+ }
232
+ // Pull rule levels from this config.
233
+ const rules = cfg.rules;
234
+ if (rules && typeof rules === 'object') {
235
+ for (const [ruleId, raw] of Object.entries(rules)) {
236
+ if (isEnabledLevel(raw))
237
+ out.add(ruleId);
238
+ }
239
+ }
240
+ }
241
+ function collectBiomeRules(root, config, out, seen, depth) {
242
+ if (depth > 10)
243
+ return;
244
+ if (!config || typeof config !== 'object')
245
+ return;
246
+ const cfg = config;
247
+ const extendsList = Array.isArray(cfg.extends) ? cfg.extends : typeof cfg.extends === 'string' ? [cfg.extends] : [];
248
+ for (const ext of extendsList) {
249
+ if (typeof ext !== 'string' || !ext.startsWith('.'))
250
+ continue;
251
+ const candidate = resolve(root, ext);
252
+ const real = safeRealpath(candidate);
253
+ if (!real || !isWithin(root, real) || seen.has(real))
254
+ continue;
255
+ seen.add(real);
256
+ if (!existsSync(real))
257
+ continue;
258
+ let extData;
259
+ try {
260
+ extData = JSON.parse(readFileSync(real, 'utf-8').replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ''));
261
+ }
262
+ catch {
263
+ continue;
264
+ }
265
+ collectBiomeRules(root, extData, out, seen, depth + 1);
266
+ }
267
+ // biome.json shape: linter.rules.<group>.<ruleName>: 'error' | 'warn' | 'off' | { level, options }
268
+ const linter = cfg.linter;
269
+ const groups = linter?.rules;
270
+ if (!groups || typeof groups !== 'object')
271
+ return;
272
+ for (const [groupName, group] of Object.entries(groups)) {
273
+ if (!group || typeof group !== 'object')
274
+ continue;
275
+ if (groupName === 'recommended' || groupName === 'all')
276
+ continue;
277
+ for (const [ruleName, raw] of Object.entries(group)) {
278
+ if (isEnabledLevel(raw))
279
+ out.add(ruleName);
280
+ }
281
+ }
282
+ }
283
+ function isEnabledLevel(raw) {
284
+ if (raw === 'error' || raw === 'warn' || raw === 1 || raw === 2)
285
+ return true;
286
+ if (Array.isArray(raw) && raw.length > 0)
287
+ return isEnabledLevel(raw[0]);
288
+ if (raw && typeof raw === 'object') {
289
+ const lvl = raw.level;
290
+ return isEnabledLevel(lvl);
291
+ }
292
+ return false;
293
+ }
294
+ /**
295
+ * Shells out to `git ls-files -c -z` to get the set of tracked paths. Returns
296
+ * an empty set if not a git repo or if git is unavailable. Bounded execution
297
+ * (10s timeout, 100 MB buffer) so a misbehaving git can't wedge the review.
298
+ */
299
+ function readGitTrackedFiles(root) {
300
+ try {
301
+ const buf = execFileSync('git', ['ls-files', '-c', '-z'], {
302
+ cwd: root,
303
+ env: withoutLocalGitEnv(),
304
+ timeout: 10_000,
305
+ maxBuffer: 100 * 1024 * 1024,
306
+ encoding: 'utf-8',
307
+ stdio: ['ignore', 'pipe', 'ignore'],
308
+ });
309
+ const set = new Set();
310
+ for (const path of buf.split('\0')) {
311
+ if (path)
312
+ set.add(path);
313
+ }
314
+ return set;
315
+ }
316
+ catch {
317
+ return new Set();
318
+ }
319
+ }
320
+ function safeRealpath(p) {
321
+ try {
322
+ return realpathSync(resolve(p));
323
+ }
324
+ catch {
325
+ return undefined;
326
+ }
327
+ }
328
+ function isWithin(root, candidate) {
329
+ const rel = relative(root, candidate);
330
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
331
+ }
332
+ function toRelative(root, filePath) {
333
+ const abs = realpathOrResolve(filePath);
334
+ if (!isWithin(root, abs))
335
+ return undefined;
336
+ const rel = relative(root, abs);
337
+ // Normalize to POSIX separators for consistent gitignore matching.
338
+ return rel.split(sep).join('/');
339
+ }
340
+ /**
341
+ * Realpath the candidate (resolves symlinks). Falls back to plain `resolve` if
342
+ * the file does not yet exist or realpath fails. Required because the project
343
+ * root is realpath'd at cache time, so file paths must be compared in the same
344
+ * symlink-resolved form (e.g. macOS `/var → /private/var`).
345
+ */
346
+ function realpathOrResolve(p) {
347
+ const abs = resolve(p);
348
+ try {
349
+ return realpathSync(abs);
350
+ }
351
+ catch {
352
+ // File doesn't exist yet — walk up to deepest existing ancestor and
353
+ // realpath that, then append the missing tail.
354
+ const parts = [];
355
+ let cur = abs;
356
+ while (true) {
357
+ const parent = dirname(cur);
358
+ if (parent === cur)
359
+ return abs;
360
+ try {
361
+ const real = realpathSync(parent);
362
+ return resolve(real, ...parts.reverse(), basenameOf(cur));
363
+ }
364
+ catch {
365
+ parts.push(basenameOf(cur));
366
+ cur = parent;
367
+ }
368
+ }
369
+ }
370
+ }
371
+ function basenameOf(p) {
372
+ const idx = p.lastIndexOf(sep);
373
+ return idx === -1 ? p : p.slice(idx + 1);
374
+ }
375
+ function computeContentHash(root) {
376
+ const hash = createHash('sha256');
377
+ for (const file of ['package.json', 'tsconfig.json', '.gitignore', '.eslintrc.json', '.eslintrc', 'biome.json']) {
378
+ const abs = resolve(root, file);
379
+ hash.update(file);
380
+ if (existsSync(abs)) {
381
+ try {
382
+ hash.update(readFileSync(abs, 'utf-8'));
383
+ }
384
+ catch {
385
+ // unreadable file — included as length-0 contribution
386
+ }
387
+ }
388
+ }
389
+ return hash.digest('hex');
390
+ }
391
+ function readJson(root, name) {
392
+ const abs = resolve(root, name);
393
+ if (!existsSync(abs))
394
+ return undefined;
395
+ if (!isWithin(root, abs))
396
+ return undefined;
397
+ try {
398
+ const raw = readFileSync(abs, 'utf-8');
399
+ // Strip JSONC comments — common in tsconfig.
400
+ const stripped = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '');
401
+ return JSON.parse(stripped);
402
+ }
403
+ catch {
404
+ return undefined;
405
+ }
406
+ }
407
+ function readTsconfig(root) {
408
+ const merged = readTsconfigChain(root, resolve(root, 'tsconfig.json'), new Set(), 0);
409
+ if (!merged)
410
+ return undefined;
411
+ const opts = merged.compilerOptions ?? {};
412
+ return {
413
+ strict: opts.strict,
414
+ strictNullChecks: opts.strictNullChecks,
415
+ noImplicitAny: opts.noImplicitAny,
416
+ noUnusedLocals: opts.noUnusedLocals,
417
+ noUnusedParameters: opts.noUnusedParameters,
418
+ };
419
+ }
420
+ function readTsconfigChain(root, abs, seen, depth) {
421
+ if (depth > 10)
422
+ return undefined;
423
+ if (!isWithin(root, abs))
424
+ return undefined;
425
+ const real = safeRealpath(abs);
426
+ if (!real || !isWithin(root, real))
427
+ return undefined;
428
+ if (seen.has(real))
429
+ return undefined;
430
+ seen.add(real);
431
+ if (!existsSync(real))
432
+ return undefined;
433
+ let raw;
434
+ try {
435
+ const text = readFileSync(real, 'utf-8').replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '');
436
+ raw = JSON.parse(text);
437
+ }
438
+ catch {
439
+ return undefined;
440
+ }
441
+ // Merge extends shallowly: extended config provides defaults; current overrides.
442
+ const extendsList = Array.isArray(raw?.extends)
443
+ ? raw?.extends
444
+ : typeof raw?.extends === 'string'
445
+ ? [raw.extends]
446
+ : [];
447
+ let merged = {};
448
+ for (const ext of extendsList ?? []) {
449
+ if (typeof ext !== 'string')
450
+ continue;
451
+ // Only relative extends are walked. Package refs (`@scope/tsconfig`) live in
452
+ // node_modules; resolving them is overkill for our purposes and would re-open
453
+ // the eval-arbitrary-code surface.
454
+ if (!ext.startsWith('.'))
455
+ continue;
456
+ const candidate = resolve(dirname(real), ext);
457
+ const withJson = candidate.endsWith('.json') ? candidate : `${candidate}.json`;
458
+ const sub = readTsconfigChain(root, withJson, seen, depth + 1);
459
+ if (sub) {
460
+ merged = {
461
+ ...merged,
462
+ ...sub,
463
+ compilerOptions: { ...merged.compilerOptions, ...sub.compilerOptions },
464
+ };
465
+ }
466
+ }
467
+ return {
468
+ ...merged,
469
+ ...raw,
470
+ compilerOptions: { ...merged.compilerOptions, ...raw?.compilerOptions },
471
+ };
472
+ }
473
+ function readGitignore(root) {
474
+ const abs = resolve(root, '.gitignore');
475
+ if (!existsSync(abs))
476
+ return { rootPatterns: [] };
477
+ if (!isWithin(root, abs))
478
+ return { rootPatterns: [] };
479
+ let text = '';
480
+ try {
481
+ text = readFileSync(abs, 'utf-8');
482
+ }
483
+ catch {
484
+ return { rootPatterns: [] };
485
+ }
486
+ const patterns = [];
487
+ for (const rawLine of text.split(/\r?\n/)) {
488
+ const trimmed = rawLine.trim();
489
+ if (!trimmed || trimmed.startsWith('#'))
490
+ continue;
491
+ if (trimmed.length > MAX_GITIGNORE_PATTERN_LENGTH)
492
+ continue; // ReDoS guard.
493
+ const negate = trimmed.startsWith('!');
494
+ const body = negate ? trimmed.slice(1) : trimmed;
495
+ const matchDirsOnly = body.endsWith('/');
496
+ const cleaned = matchDirsOnly ? body.slice(0, -1) : body;
497
+ const regex = compileGitignoreRegex(cleaned, matchDirsOnly);
498
+ if (!regex)
499
+ continue;
500
+ patterns.push({ raw: trimmed, regex, negate, matchDirsOnly });
501
+ }
502
+ return { rootPatterns: patterns };
503
+ }
504
+ function compileGitignoreRegex(pattern, matchDirsOnly) {
505
+ // Hand-rolled minimal gitignore-style glob → regex. Supports:
506
+ // * → [^/]*
507
+ // **/ → (anything-or-nothing)
508
+ // /xxx → root-anchored
509
+ // xxx → match anywhere in path (with leading dir boundary)
510
+ // xxx/ → directory match (handled via matchDirsOnly param + trailing match)
511
+ // Other extended globs (?, [], **) intentionally limited — keeps the regex
512
+ // shapes bounded and ReDoS-safe.
513
+ let body = pattern;
514
+ const anchored = body.startsWith('/');
515
+ if (anchored)
516
+ body = body.slice(1);
517
+ let regex = '';
518
+ for (let i = 0; i < body.length; i++) {
519
+ const ch = body[i];
520
+ if (ch === '*') {
521
+ if (body[i + 1] === '*' && body[i + 2] === '/') {
522
+ regex += '(?:.*/)?';
523
+ i += 2;
524
+ }
525
+ else if (body[i + 1] === '*') {
526
+ regex += '.*';
527
+ i += 1;
528
+ }
529
+ else {
530
+ regex += '[^/]*';
531
+ }
532
+ }
533
+ else if (ch === '?') {
534
+ regex += '[^/]';
535
+ }
536
+ else if (ch === '.' ||
537
+ ch === '+' ||
538
+ ch === '(' ||
539
+ ch === ')' ||
540
+ ch === '|' ||
541
+ ch === '^' ||
542
+ ch === '$' ||
543
+ ch === '{' ||
544
+ ch === '}' ||
545
+ ch === '[' ||
546
+ ch === ']' ||
547
+ ch === '\\') {
548
+ regex += `\\${ch}`;
549
+ }
550
+ else {
551
+ regex += ch;
552
+ }
553
+ }
554
+ const prefix = anchored ? '^' : '^(?:.*/)?';
555
+ const suffix = matchDirsOnly ? '(?:/.*)?$' : '(?:$|/.*$)';
556
+ try {
557
+ return new RegExp(prefix + regex + suffix);
558
+ }
559
+ catch {
560
+ return undefined;
561
+ }
562
+ }
563
+ //# sourceMappingURL=project-context.js.map