Skip to content

Commit cce7864

Browse files
committed
fix(core): fix config contract gaps
1 parent c82839d commit cce7864

5 files changed

Lines changed: 125 additions & 14 deletions

File tree

packages/core/src/rules/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
* Built-in rules index.
33
*
44
* This is the registry of all rules that ship with @sentinel/core.
5-
* Third-party rules are NOT registered here — they are declared in
6-
* the user's sentinel.config.ts and passed to the runner directly.
5+
* Only built-in rules in BUILT_IN_RULES are executed today — the runner
6+
* resolves rule IDs from ScanConfig.rules via this map. Unknown rule IDs
7+
* produce a config-warning ScanDiagnostic and are skipped (not executed).
8+
*
9+
* Custom/third-party rule loading is not yet supported and is future work.
710
*/
811

912
import type { Rule } from '../model/rule.js'

packages/core/src/runner/scanner.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,50 @@ describe('scan', () => {
186186
rmSync(root, { recursive: true, force: true })
187187
}
188188
})
189+
190+
it('respects include patterns and excludes files outside them', async () => {
191+
const root = createFixture()
192+
193+
try {
194+
writeScanFixture(root, {
195+
'src/in-scope.ts': "fetch('/in')",
196+
'lib/out-of-scope.ts': "fetch('/out')",
197+
})
198+
199+
const result = await scan(
200+
resolveConfig({
201+
rootDir: root,
202+
include: ['src/**'],
203+
rules: { 'missing-error-handler': 'off', 'no-hardcoded-url': 'off' },
204+
}),
205+
)
206+
207+
expect(result.stats.filesScanned).toBe(1)
208+
expect(result.apiCalls.some((call) => call.url === '/in')).toBe(true)
209+
expect(result.apiCalls.some((call) => call.url === '/out')).toBe(false)
210+
} finally {
211+
rmSync(root, { recursive: true, force: true })
212+
}
213+
})
214+
215+
it('discovers and scans .mts files with default include config', async () => {
216+
const root = createFixture()
217+
218+
try {
219+
writeScanFixture(root, {
220+
'api.mts': "fetch('/mts')",
221+
})
222+
223+
const result = await scan(
224+
resolveConfig({
225+
rootDir: root,
226+
rules: { 'missing-error-handler': 'off', 'no-hardcoded-url': 'off' },
227+
}),
228+
)
229+
230+
expect(result.apiCalls.some((call) => call.url === '/mts')).toBe(true)
231+
} finally {
232+
rmSync(root, { recursive: true, force: true })
233+
}
234+
})
189235
})

packages/core/src/runner/scanner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ export async function scan(config: ScanConfig): Promise<ScanResult> {
9292
const allApiCalls: ApiCall[] = []
9393

9494
// ── Phase 1: File Discovery ──────────────────────────────────────────────
95-
scanLogger.debug('Discovering files', { exclude: config.exclude })
95+
scanLogger.debug('Discovering files', { include: config.include, exclude: config.exclude })
9696

9797
const discoveryResult = await scanFiles({
9898
rootDir: config.rootDir,
99+
include: config.include,
99100
extraIgnore: config.exclude,
100101
logger: scopedLogger(logger, 'sentinel:scan:files'),
101102
})

packages/core/src/scan/file-scanner.test.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,24 @@ function makeCapturingLogger(): { logger: Logger; entries: LogEntry[] } {
1818

1919
const logger: Logger = {
2020
debug(message, meta) {
21-
entries.push(meta === undefined ? { level: 'debug', message } : { level: 'debug', message, meta })
21+
entries.push(
22+
meta === undefined ? { level: 'debug', message } : { level: 'debug', message, meta },
23+
)
2224
},
2325
info(message, meta) {
24-
entries.push(meta === undefined ? { level: 'info', message } : { level: 'info', message, meta })
26+
entries.push(
27+
meta === undefined ? { level: 'info', message } : { level: 'info', message, meta },
28+
)
2529
},
2630
warn(message, meta) {
27-
entries.push(meta === undefined ? { level: 'warn', message } : { level: 'warn', message, meta })
31+
entries.push(
32+
meta === undefined ? { level: 'warn', message } : { level: 'warn', message, meta },
33+
)
2834
},
2935
error(message, meta) {
30-
entries.push(meta === undefined ? { level: 'error', message } : { level: 'error', message, meta })
36+
entries.push(
37+
meta === undefined ? { level: 'error', message } : { level: 'error', message, meta },
38+
)
3139
},
3240
}
3341

@@ -185,12 +193,33 @@ describe('scanFiles', () => {
185193

186194
expect(result.value).toHaveLength(0)
187195
expect(
188-
entries.some(
189-
(e) => e.level === 'info' && e.message === 'No matching source files found',
190-
),
196+
entries.some((e) => e.level === 'info' && e.message === 'No matching source files found'),
191197
).toBe(true)
192198
} finally {
193199
rmSync(root, { recursive: true, force: true })
194200
}
195201
})
202+
203+
it('discovers .mts and .cts source files', async () => {
204+
const root = createFixture()
205+
206+
try {
207+
writeFileSync(join(root, 'app.mts'), 'export {}')
208+
writeFileSync(join(root, 'app.cts'), 'export {}')
209+
210+
const result = await scanFiles({ rootDir: root })
211+
212+
expect(result.ok).toBe(true)
213+
if (!result.ok) return
214+
215+
expect(result.value).toEqual(
216+
expect.arrayContaining([
217+
expect.objectContaining({ relativePath: 'app.mts', extension: '.mts' }),
218+
expect.objectContaining({ relativePath: 'app.cts', extension: '.cts' }),
219+
]),
220+
)
221+
} finally {
222+
rmSync(root, { recursive: true, force: true })
223+
}
224+
})
196225
})

packages/core/src/scan/file-scanner.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export interface ScannedFile {
3232
export interface FileScannerOptions {
3333
/** Root directory to scan (must exist and be a directory). */
3434
readonly rootDir: string
35+
/**
36+
* Glob patterns a file must match at least one of to be included.
37+
* When omitted or empty, all discovered source extensions are included.
38+
*/
39+
readonly include?: readonly string[]
3540
/** Additional glob patterns to exclude (merged with defaults). */
3641
readonly extraIgnore?: readonly string[]
3742
/** When true (default), merge patterns from .gitignore files found during walk. */
@@ -52,7 +57,7 @@ export class FileScannerError extends Error {
5257

5358
// ─── Constants ────────────────────────────────────────────────────────────────
5459

55-
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
60+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
5661

5762
const DEFAULT_IGNORED_DIRS = new Set([
5863
'node_modules',
@@ -68,7 +73,10 @@ const DEFAULT_IGNORED_DIRS = new Set([
6873
// ─── Public API ───────────────────────────────────────────────────────────────
6974

7075
/**
71-
* Recursively discover .ts, .tsx, .js, and .jsx files under rootDir.
76+
* Recursively discover .ts, .tsx, .js, .jsx, .mts, and .cts files under rootDir.
77+
*
78+
* When `include` patterns are provided, a file must match at least one to be
79+
* returned. Exclude patterns reject files that match any ignore glob.
7280
*
7381
* Non-fatal errors (permission denied on a subdirectory) warn and continue.
7482
* Fatal errors (root unreadable) return err().
@@ -80,10 +88,19 @@ export async function scanFiles(
8088
const logger = options.logger ?? noopLogger
8189
const respectGitignore = options.respectGitignore !== false
8290
const ignorePatterns = [...(options.extraIgnore ?? [])]
91+
const includePatterns = [...(options.include ?? [])]
8392

8493
try {
8594
const files: ScannedFile[] = []
86-
await walkDir(rootDir, rootDir, ignorePatterns, respectGitignore, logger, files)
95+
await walkDir(
96+
rootDir,
97+
rootDir,
98+
ignorePatterns,
99+
includePatterns,
100+
respectGitignore,
101+
logger,
102+
files,
103+
)
87104

88105
if (files.length === 0) {
89106
logger.info('No matching source files found', { rootDir })
@@ -101,6 +118,7 @@ async function walkDir(
101118
dir: string,
102119
rootDir: string,
103120
ignorePatterns: readonly string[],
121+
includePatterns: readonly string[],
104122
respectGitignore: boolean,
105123
logger: Logger,
106124
accumulator: ScannedFile[],
@@ -146,7 +164,15 @@ async function walkDir(
146164
if (entry.isDirectory()) {
147165
if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue
148166
if (isIgnoredPath(relativePath, localIgnorePatterns, true)) continue
149-
await walkDir(absolutePath, rootDir, localIgnorePatterns, respectGitignore, logger, accumulator)
167+
await walkDir(
168+
absolutePath,
169+
rootDir,
170+
localIgnorePatterns,
171+
includePatterns,
172+
respectGitignore,
173+
logger,
174+
accumulator,
175+
)
150176
continue
151177
}
152178

@@ -155,6 +181,7 @@ async function walkDir(
155181

156182
const extension = extname(relativePath).toLowerCase()
157183
if (!SOURCE_EXTENSIONS.has(extension)) continue
184+
if (!isIncludedPath(relativePath, includePatterns)) continue
158185

159186
accumulator.push({
160187
absolutePath,
@@ -190,6 +217,11 @@ function isIgnoredPath(
190217
return false
191218
}
192219

220+
function isIncludedPath(relativePath: string, includePatterns: readonly string[]): boolean {
221+
if (includePatterns.length === 0) return true
222+
return matchesAnyGlob(relativePath, includePatterns)
223+
}
224+
193225
function normalizeRelativePath(path: string): string {
194226
return path.replace(/\\/g, '/')
195227
}

0 commit comments

Comments
 (0)