@nifrajs/cli 2.12.1 → 2.14.1

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 (74) hide show
  1. package/dist/capabilities-tool.d.ts +54 -1
  2. package/dist/capabilities-tool.d.ts.map +1 -1
  3. package/dist/capabilities-tool.js +146 -18
  4. package/dist/capabilities-tool.js.map +1 -1
  5. package/dist/check.d.ts +11 -11
  6. package/dist/check.d.ts.map +1 -1
  7. package/dist/check.js +235 -47
  8. package/dist/check.js.map +1 -1
  9. package/dist/cli.d.ts +11 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +96 -406
  12. package/dist/cli.js.map +1 -1
  13. package/dist/command-catalog.d.ts +244 -0
  14. package/dist/command-catalog.d.ts.map +1 -0
  15. package/dist/command-catalog.js +1014 -0
  16. package/dist/command-catalog.js.map +1 -0
  17. package/dist/contracts.d.ts +9 -0
  18. package/dist/contracts.d.ts.map +1 -1
  19. package/dist/contracts.js +22 -6
  20. package/dist/contracts.js.map +1 -1
  21. package/dist/doctor.d.ts +18 -11
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +30 -230
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/load.d.ts.map +1 -1
  26. package/dist/load.js +22 -5
  27. package/dist/load.js.map +1 -1
  28. package/dist/manifest-tool.d.ts +15 -1
  29. package/dist/manifest-tool.d.ts.map +1 -1
  30. package/dist/manifest-tool.js +23 -9
  31. package/dist/manifest-tool.js.map +1 -1
  32. package/dist/mcp-http.js +1 -1
  33. package/dist/mcp.d.ts +8 -0
  34. package/dist/mcp.d.ts.map +1 -1
  35. package/dist/mcp.js +59 -349
  36. package/dist/mcp.js.map +1 -1
  37. package/dist/project-facts.d.ts +40 -0
  38. package/dist/project-facts.d.ts.map +1 -0
  39. package/dist/project-facts.js +36 -0
  40. package/dist/project-facts.js.map +1 -0
  41. package/dist/release-verification.d.ts +45 -0
  42. package/dist/release-verification.d.ts.map +1 -0
  43. package/dist/release-verification.js +303 -0
  44. package/dist/release-verification.js.map +1 -0
  45. package/dist/rules/comment-markers.d.ts +8 -0
  46. package/dist/rules/comment-markers.d.ts.map +1 -1
  47. package/dist/rules/comment-markers.js +36 -0
  48. package/dist/rules/comment-markers.js.map +1 -1
  49. package/dist/rules/index.d.ts +2 -3
  50. package/dist/rules/index.d.ts.map +1 -1
  51. package/dist/rules/index.js.map +1 -1
  52. package/dist/rules/routes.d.ts.map +1 -1
  53. package/dist/rules/routes.js +12 -14
  54. package/dist/rules/routes.js.map +1 -1
  55. package/dist/rules/security.js +14 -14
  56. package/dist/rules/security.js.map +1 -1
  57. package/docs/types.json +28 -15
  58. package/package.json +9 -9
  59. package/src/capabilities-tool.ts +229 -21
  60. package/src/check.ts +286 -64
  61. package/src/cli.ts +106 -399
  62. package/src/command-catalog.ts +1415 -0
  63. package/src/contracts.ts +28 -7
  64. package/src/doctor.ts +56 -264
  65. package/src/load.ts +25 -6
  66. package/src/manifest-tool.ts +43 -12
  67. package/src/mcp-http.ts +1 -1
  68. package/src/mcp.ts +82 -414
  69. package/src/project-facts.ts +86 -0
  70. package/src/release-verification.ts +377 -0
  71. package/src/rules/comment-markers.ts +36 -0
  72. package/src/rules/index.ts +3 -4
  73. package/src/rules/routes.ts +13 -14
  74. package/src/rules/security.ts +14 -14
@@ -0,0 +1,377 @@
1
+ /**
2
+ * The repository release verification gate.
3
+ *
4
+ * This module is intentionally independent from app loading. The root package scripts and the
5
+ * `nifra verify` command both call this runner, so the local and CI paths share one gate plan.
6
+ */
7
+
8
+ import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs"
9
+ import { tmpdir } from "node:os"
10
+ import { dirname, join, relative, resolve, sep } from "node:path"
11
+
12
+ export type ReleaseVerificationMode = "default" | "release"
13
+ export type ReleaseGateStatus = "pass" | "fail" | "skipped"
14
+
15
+ export interface ReleaseGateResult {
16
+ readonly id: string
17
+ readonly status: ReleaseGateStatus
18
+ readonly commands: readonly string[]
19
+ readonly exitCode?: number
20
+ readonly message?: string
21
+ readonly remediation: string
22
+ }
23
+
24
+ export interface ReleaseVerificationResult {
25
+ readonly ok: boolean
26
+ readonly mode: ReleaseVerificationMode
27
+ readonly gates: readonly ReleaseGateResult[]
28
+ }
29
+
30
+ export interface ReleaseCommandResult {
31
+ readonly exitCode: number
32
+ readonly stdout?: string
33
+ readonly stderr?: string
34
+ }
35
+
36
+ export interface ReleaseCommandSpec {
37
+ readonly args: readonly string[]
38
+ readonly cwd: string
39
+ readonly env: Record<string, string | undefined>
40
+ }
41
+
42
+ export interface ReleaseVerificationOptions {
43
+ readonly mode?: ReleaseVerificationMode
44
+ readonly runCommand?: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>
45
+ }
46
+
47
+ interface ReleaseGateSpec {
48
+ readonly id: string
49
+ readonly commands: readonly (readonly string[])[]
50
+ readonly remediation: string
51
+ }
52
+
53
+ const DEFAULT_GATES: readonly ReleaseGateSpec[] = [
54
+ {
55
+ id: "lint",
56
+ commands: [["run", "lint"]],
57
+ remediation: "Run `bun run lint` and fix the reported lint findings.",
58
+ },
59
+ {
60
+ id: "typecheck",
61
+ commands: [["run", "typecheck"]],
62
+ remediation: "Run `bun run typecheck` and fix the reported TypeScript errors.",
63
+ },
64
+ {
65
+ id: "tests",
66
+ commands: [["run", "test"]],
67
+ remediation: "Run `bun run test` and fix the first failing test.",
68
+ },
69
+ {
70
+ id: "docs",
71
+ commands: [["run", "check:docs"]],
72
+ remediation: "Run `bun run check:docs` and update the failing documentation example.",
73
+ },
74
+ {
75
+ id: "api-corpus",
76
+ commands: [["run", "check:api"]],
77
+ remediation: "Run `bun run gen:api` and review the generated API reference.",
78
+ },
79
+ {
80
+ id: "cards-corpus",
81
+ commands: [["run", "check:cards"]],
82
+ remediation: "Run `bun run gen:cards` and review the generated package cards.",
83
+ },
84
+ {
85
+ id: "node-outcome-corpus",
86
+ commands: [["run", "check:node-outcome"]],
87
+ remediation: "Run `bun run gen:node-outcome` and review the generated Node outcome contract.",
88
+ },
89
+ {
90
+ id: "sitemap",
91
+ commands: [["run", "check:sitemap"]],
92
+ remediation: "Run `bun run gen:sitemap` and review the generated sitemap.",
93
+ },
94
+ {
95
+ id: "public-boundary",
96
+ commands: [["run", "check:public-boundary"]],
97
+ remediation:
98
+ "Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
99
+ },
100
+ {
101
+ id: "size",
102
+ commands: [["run", "check:size"]],
103
+ remediation:
104
+ "Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
105
+ },
106
+ ]
107
+
108
+ const RELEASE_GATES: readonly ReleaseGateSpec[] = [
109
+ {
110
+ id: "build",
111
+ commands: [["run", "build"]],
112
+ remediation: "Run `bun run build` and fix the first package build failure.",
113
+ },
114
+ ...DEFAULT_GATES.slice(0, 3),
115
+ {
116
+ id: "coverage",
117
+ commands: [
118
+ ["run", "test:coverage"],
119
+ ["run", "check:coverage"],
120
+ ],
121
+ remediation:
122
+ "Run `bun run test:coverage` first, then `bun run check:coverage`, and fix the reported coverage regression.",
123
+ },
124
+ {
125
+ id: "corpus",
126
+ commands: [["run", "check:corpus"]],
127
+ remediation:
128
+ "Run `bun run gen:llms`, `bun run gen:api`, and `bun run gen:cards`, then rerun the corpus gate.",
129
+ },
130
+ DEFAULT_GATES[3] as ReleaseGateSpec,
131
+ {
132
+ id: "public-boundary",
133
+ commands: [["run", "check:public-boundary"]],
134
+ remediation:
135
+ "Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
136
+ },
137
+ {
138
+ id: "size",
139
+ commands: [["run", "check:size"]],
140
+ remediation:
141
+ "Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
142
+ },
143
+ {
144
+ id: "core-performance",
145
+ commands: [["run", "check:core-performance"]],
146
+ remediation:
147
+ "Run `bun run check:core-performance` and investigate the measured performance regression.",
148
+ },
149
+ {
150
+ id: "publish",
151
+ commands: [["run", "check:publish"]],
152
+ remediation:
153
+ "Run `bun run check:publish` and fix the publish-consumer metadata or type-surface failure.",
154
+ },
155
+ {
156
+ id: "consumer",
157
+ commands: [["run", "check:consumers"]],
158
+ remediation: "Run `bun run check:consumers` and fix the isolated consumer failure.",
159
+ },
160
+ {
161
+ id: "cold-start",
162
+ commands: [["run", "check:cold-start"]],
163
+ remediation:
164
+ "Run `bun run check:cold-start` and fix the fresh scaffold install or build failure.",
165
+ },
166
+ {
167
+ id: "cross-runtime-deno",
168
+ commands: [
169
+ ["run", "test:deno"],
170
+ ["run", "check:deno-tarball"],
171
+ ],
172
+ remediation:
173
+ "Run `bun run test:deno` and `bun run check:deno-tarball`, then fix the first Deno compatibility failure.",
174
+ },
175
+ {
176
+ id: "cross-runtime-node",
177
+ commands: [["run", "test:node"]],
178
+ remediation: "Run `bun run test:node` and fix the Node runtime adapter failure.",
179
+ },
180
+ {
181
+ id: "pipeline-parity",
182
+ commands: [["run", "check:pipeline-parity"]],
183
+ remediation:
184
+ "Run `bun run check:pipeline-parity` and fix the development and production manifest drift.",
185
+ },
186
+ ]
187
+
188
+ const runBunCommand = async (spec: ReleaseCommandSpec): Promise<ReleaseCommandResult> => {
189
+ const child = Bun.spawn([process.execPath, ...spec.args], {
190
+ cwd: spec.cwd,
191
+ env: spec.env,
192
+ stdout: "pipe",
193
+ stderr: "pipe",
194
+ })
195
+ const [exitCode, stdout, stderr] = await Promise.all([
196
+ child.exited,
197
+ new Response(child.stdout).text(),
198
+ new Response(child.stderr).text(),
199
+ ])
200
+ return { exitCode, stdout, stderr }
201
+ }
202
+
203
+ const parsePackage = (path: string): Record<string, unknown> | undefined => {
204
+ try {
205
+ const value: unknown = JSON.parse(readFileSync(path, "utf8"))
206
+ return value !== null && typeof value === "object" && !Array.isArray(value)
207
+ ? (value as Record<string, unknown>)
208
+ : undefined
209
+ } catch {
210
+ return undefined
211
+ }
212
+ }
213
+
214
+ const workspacePatterns = (pkg: Record<string, unknown>): readonly string[] => {
215
+ const workspaces = pkg.workspaces
216
+ if (Array.isArray(workspaces))
217
+ return workspaces.filter((item): item is string => typeof item === "string")
218
+ if (workspaces !== null && typeof workspaces === "object") {
219
+ const packages = (workspaces as Record<string, unknown>).packages
220
+ if (Array.isArray(packages))
221
+ return packages.filter((item): item is string => typeof item === "string")
222
+ }
223
+ return []
224
+ }
225
+
226
+ const workspaceContains = async (
227
+ root: string,
228
+ patterns: readonly string[],
229
+ target: string,
230
+ ): Promise<boolean> => {
231
+ if (root === target) return patterns.length > 0
232
+ for (const pattern of patterns) {
233
+ const packagePattern = `${pattern.replace(/\/$/, "")}/package.json`
234
+ for await (const match of new Bun.Glob(packagePattern).scan({ cwd: root, dot: false })) {
235
+ if (match.split(/[\\/]/).includes("node_modules")) continue
236
+ const packageRoot = resolve(root, dirname(match))
237
+ const targetRelative = relative(packageRoot, target)
238
+ if (
239
+ targetRelative === "" ||
240
+ (!targetRelative.startsWith(`..${sep}`) && targetRelative !== "..")
241
+ )
242
+ return true
243
+ }
244
+ }
245
+ return false
246
+ }
247
+
248
+ /** Resolve the repository root even when the command starts in a workspace package. */
249
+ export async function resolveVerificationRoot(start: string): Promise<string> {
250
+ const original = realpathSync(resolve(start))
251
+ let current = original
252
+ for (;;) {
253
+ const pkgPath = join(current, "package.json")
254
+ const pkg = parsePackage(pkgPath)
255
+ if (pkg !== undefined && (await workspaceContains(current, workspacePatterns(pkg), original))) {
256
+ return current
257
+ }
258
+ const parent = dirname(current)
259
+ if (parent === current) return original
260
+ current = parent
261
+ }
262
+ }
263
+
264
+ const gatePlan = (mode: ReleaseVerificationMode): readonly ReleaseGateSpec[] =>
265
+ mode === "release" ? RELEASE_GATES : DEFAULT_GATES
266
+
267
+ const commandLabel = (args: readonly string[]): string => `bun ${args.join(" ")}`
268
+
269
+ const runGate = async (
270
+ root: string,
271
+ gate: ReleaseGateSpec,
272
+ runCommand: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>,
273
+ fixtureRoot: string,
274
+ ): Promise<ReleaseGateResult> => {
275
+ let lastExitCode: number | undefined
276
+ for (const [index, args] of gate.commands.entries()) {
277
+ const fixture = mkdtempSync(join(fixtureRoot, `${gate.id}-${index}-`))
278
+ const result = await runCommand({
279
+ args,
280
+ cwd: root,
281
+ env: {
282
+ ...process.env,
283
+ TMPDIR: fixture,
284
+ NIFRA_VERIFY_GATE: gate.id,
285
+ },
286
+ })
287
+ lastExitCode = result.exitCode
288
+ if (result.exitCode !== 0) {
289
+ return {
290
+ id: gate.id,
291
+ status: "fail",
292
+ commands: gate.commands.map(commandLabel),
293
+ exitCode: result.exitCode,
294
+ message: `${commandLabel(args)} exited with code ${result.exitCode}`,
295
+ remediation: gate.remediation,
296
+ }
297
+ }
298
+ }
299
+ return {
300
+ id: gate.id,
301
+ status: "pass",
302
+ commands: gate.commands.map(commandLabel),
303
+ ...(lastExitCode === undefined ? {} : { exitCode: lastExitCode }),
304
+ remediation: gate.remediation,
305
+ }
306
+ }
307
+
308
+ const skippedGate = (gate: ReleaseGateSpec, failedId: string): ReleaseGateResult => ({
309
+ id: gate.id,
310
+ status: "skipped",
311
+ commands: gate.commands.map(commandLabel),
312
+ message: `not run because the ${failedId} gate failed`,
313
+ remediation: `Fix the ${failedId} gate first, then rerun verification.`,
314
+ })
315
+
316
+ /** Collect the same gate result rendered by the CLI and used by repository scripts. */
317
+ export async function collectReleaseVerification(
318
+ start: string,
319
+ options: ReleaseVerificationOptions = {},
320
+ ): Promise<ReleaseVerificationResult> {
321
+ const mode = options.mode ?? "default"
322
+ const root = await resolveVerificationRoot(start)
323
+ const runCommand = options.runCommand ?? runBunCommand
324
+ const fixtureRoot = mkdtempSync(join(realpathSync(tmpdir()), "nifra-verify-"))
325
+ const gates: ReleaseGateResult[] = []
326
+ try {
327
+ let failedId: string | undefined
328
+ for (const gate of gatePlan(mode)) {
329
+ if (failedId !== undefined) {
330
+ gates.push(skippedGate(gate, failedId))
331
+ continue
332
+ }
333
+ const result = await runGate(root, gate, runCommand, fixtureRoot)
334
+ gates.push(result)
335
+ if (result.status === "fail") failedId = result.id
336
+ }
337
+ } finally {
338
+ rmSync(fixtureRoot, { recursive: true, force: true })
339
+ }
340
+ return {
341
+ ok: gates.every((gate) => gate.status === "pass"),
342
+ mode,
343
+ gates,
344
+ }
345
+ }
346
+
347
+ export function renderReleaseVerification(result: ReleaseVerificationResult): string {
348
+ const lines = [`nifra verify --${result.mode}`, ""]
349
+ for (const gate of result.gates) {
350
+ const marker = gate.status === "pass" ? "✓" : gate.status === "fail" ? "✗" : "-"
351
+ lines.push(`${marker} ${gate.id}: ${gate.status}`)
352
+ if (gate.message !== undefined) lines.push(` ${gate.message}`)
353
+ if (gate.status !== "pass") lines.push(` fix: ${gate.remediation}`)
354
+ }
355
+ lines.push("", result.ok ? "✓ verification passed" : "✗ verification failed")
356
+ return lines.join("\n")
357
+ }
358
+
359
+ /** Run verification and render either the human or machine interface. */
360
+ export async function runReleaseVerification(
361
+ cwd: string,
362
+ options: ReleaseVerificationOptions & { readonly json?: boolean } = {},
363
+ ): Promise<boolean> {
364
+ const result = await collectReleaseVerification(cwd, options)
365
+ if (options.json) console.log(JSON.stringify(result, null, 2))
366
+ else console.log(renderReleaseVerification(result))
367
+ return result.ok
368
+ }
369
+
370
+ if (import.meta.main) {
371
+ const mode: ReleaseVerificationMode = process.argv.includes("--release") ? "release" : "default"
372
+ const ok = await runReleaseVerification(process.cwd(), {
373
+ mode,
374
+ json: process.argv.includes("--json"),
375
+ })
376
+ if (!ok) process.exitCode = 1
377
+ }
@@ -24,3 +24,39 @@ export function commentBlockHasMarker(
24
24
  }
25
25
  return false
26
26
  }
27
+
28
+ /**
29
+ * The justification a reason-carrying marker (`<marker>: <reason>`) supplies, or `undefined` when the
30
+ * marker is absent or written with no reason after its colon. Searches the same trailing-comment /
31
+ * comment-block-above region as {@link commentBlockHasMarker}, so a hatch that demands a written reason
32
+ * can require this to be non-empty before it silences anything - an empty `<marker>:` never suppresses,
33
+ * which is how "reason mandatory" is enforced without a second diagnostic.
34
+ */
35
+ export function commentBlockMarkerReason(
36
+ lines: readonly string[],
37
+ line: number,
38
+ marker: string,
39
+ ): string | undefined {
40
+ const reasonOn = (text: string): string | undefined => {
41
+ const at = text.indexOf(marker)
42
+ if (at < 0) return undefined
43
+ const rest = text.slice(at + marker.length)
44
+ const colon = rest.indexOf(":")
45
+ if (colon < 0) return undefined
46
+ // A block comment's `*/` closer is not part of the reason.
47
+ const reason = rest
48
+ .slice(colon + 1)
49
+ .replace(/\*\/\s*$/, "")
50
+ .trim()
51
+ return reason.length > 0 ? reason : undefined
52
+ }
53
+ const onLine = reasonOn(lines[line - 1] ?? "")
54
+ if (onLine !== undefined) return onLine
55
+ for (let i = line - 2; i >= 0; i--) {
56
+ const text = lines[i] ?? ""
57
+ if (!COMMENT_LINE.test(text)) return undefined
58
+ const reason = reasonOn(text)
59
+ if (reason !== undefined) return reason
60
+ }
61
+ return undefined
62
+ }
@@ -1,6 +1,9 @@
1
1
  import type { Diagnostic } from "../diagnostics.ts"
2
+ import type { ProjectFacts } from "../project-facts.ts"
2
3
  import { isBuiltInCode } from "./codes.ts"
3
4
 
5
+ export type { ProjectFacts } from "../project-facts.ts"
6
+
4
7
  export interface SourceFile {
5
8
  readonly file: string
6
9
  readonly content: string
@@ -11,10 +14,6 @@ export interface SourceIndex {
11
14
  read(file: string): string | undefined
12
15
  }
13
16
 
14
- export interface ProjectFacts {
15
- readonly [key: string]: unknown
16
- }
17
-
18
17
  export interface RuleContext {
19
18
  readonly root: string
20
19
  readonly sources: SourceIndex
@@ -3,8 +3,8 @@ import { commentBlockHasMarker } from "./comment-markers.ts"
3
3
  import type { CheckRule, RuleContext } from "./index.ts"
4
4
 
5
5
  /**
6
- * Route-table lints over the statically collected route registrations (`scanStaticRouteText` in
7
- * ../check.ts fills `ctx.project.staticRoutes` during the source walk - no second scan here).
6
+ * Route-table lints over the statically collected route registrations (`ProjectFacts.routes` is built
7
+ * once before the registry runs - no rule re-walks source).
8
8
  *
9
9
  * NF-C018 exists because the typed client's proxy intercepts a fixed set of property names before
10
10
  * path resolution (`resolveSegment` + the thenable guard in @nifrajs/client): the seven HTTP verbs
@@ -41,20 +41,19 @@ interface StaticRouteFact {
41
41
 
42
42
  /** Parse-don't-cast over the project fact: a malformed entry is dropped, never trusted. */
43
43
  function routeFacts(ctx: RuleContext): StaticRouteFact[] {
44
- const raw = ctx.project.staticRoutes
45
- if (!Array.isArray(raw)) return []
44
+ const raw: readonly unknown[] = ctx.project.routes
46
45
  const out: StaticRouteFact[] = []
47
46
  for (const item of raw) {
48
- if (typeof item !== "object" || item === null) continue
49
- const record = item as Record<string, unknown>
47
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue
48
+ const record = Object.fromEntries(Object.entries(item))
50
49
  if (
51
- typeof record.file === "string" &&
52
- typeof record.line === "number" &&
53
- typeof record.method === "string" &&
54
- typeof record.path === "string"
55
- ) {
56
- out.push({ file: record.file, line: record.line, method: record.method, path: record.path })
57
- }
50
+ typeof record.file !== "string" ||
51
+ typeof record.line !== "number" ||
52
+ typeof record.method !== "string" ||
53
+ typeof record.path !== "string"
54
+ )
55
+ continue
56
+ out.push({ file: record.file, line: record.line, method: record.method, path: record.path })
58
57
  }
59
58
  return out
60
59
  }
@@ -91,7 +90,7 @@ export const reservedSegmentRule: CheckRule = {
91
90
  if (collision === undefined) continue
92
91
  let lines = linesByFile.get(route.file)
93
92
  if (lines === undefined) {
94
- lines = (ctx.sources.read(route.file) ?? "").split("\n")
93
+ lines = (ctx.project.source.read(route.file) ?? "").split("\n")
95
94
  linesByFile.set(route.file, lines)
96
95
  }
97
96
  if (commentBlockHasMarker(lines, route.line, RESERVED_SEGMENT_PRAGMA)) continue
@@ -108,8 +108,8 @@ export const secretComparisonRule: CheckRule = {
108
108
  const ts = await importProjectTypeScript(ctx.root)
109
109
  if (ts === undefined) return didNotRun("NF-S002", "Non-constant-time secret comparison scan")
110
110
  const findings: Diagnostic[] = []
111
- for (const file of ctx.sources.files) {
112
- const parsed = parsedFile(ts, ctx.sources, file)
111
+ for (const file of ctx.project.source.files) {
112
+ const parsed = parsedFile(ts, ctx.project.source, file)
113
113
  if (parsed === undefined) continue
114
114
  const { tree, lines } = parsed
115
115
  const visit = (node: TSApi.Node): void => {
@@ -169,8 +169,8 @@ export const piiLogRule: CheckRule = {
169
169
  const ts = await importProjectTypeScript(ctx.root)
170
170
  if (ts === undefined) return didNotRun("NF-S003", "Sensitive-value-in-log scan")
171
171
  const findings: Diagnostic[] = []
172
- for (const file of ctx.sources.files) {
173
- const parsed = parsedFile(ts, ctx.sources, file)
172
+ for (const file of ctx.project.source.files) {
173
+ const parsed = parsedFile(ts, ctx.project.source, file)
174
174
  if (parsed === undefined) continue
175
175
  const { tree, lines } = parsed
176
176
  const visit = (node: TSApi.Node): void => {
@@ -217,8 +217,8 @@ export const failOpenGateRule: CheckRule = {
217
217
  const ts = await importProjectTypeScript(ctx.root)
218
218
  if (ts === undefined) return didNotRun("NF-S001", "Fail-open gate scan")
219
219
  const findings: Diagnostic[] = []
220
- for (const file of ctx.sources.files) {
221
- const parsed = parsedFile(ts, ctx.sources, file)
220
+ for (const file of ctx.project.source.files) {
221
+ const parsed = parsedFile(ts, ctx.project.source, file)
222
222
  if (parsed === undefined) continue
223
223
  const { tree, lines } = parsed
224
224
  const visit = (node: TSApi.Node): void => {
@@ -284,8 +284,8 @@ export const corsOriginPredicateRule: CheckRule = {
284
284
  const ts = await importProjectTypeScript(ctx.root)
285
285
  if (ts === undefined) return didNotRun("NF-S004", "CORS origin predicate scan")
286
286
  const findings: Diagnostic[] = []
287
- for (const file of ctx.sources.files) {
288
- const parsed = parsedFile(ts, ctx.sources, file)
287
+ for (const file of ctx.project.source.files) {
288
+ const parsed = parsedFile(ts, ctx.project.source, file)
289
289
  if (parsed === undefined) continue
290
290
  const { tree, lines } = parsed
291
291
  const visit = (node: TSApi.Node): void => {
@@ -345,8 +345,8 @@ export const externalRedirectRule: CheckRule = {
345
345
  const ts = await importProjectTypeScript(ctx.root)
346
346
  if (ts === undefined) return didNotRun("NF-S005", "External redirect opt-out scan")
347
347
  const findings: Diagnostic[] = []
348
- for (const file of ctx.sources.files) {
349
- const parsed = parsedFile(ts, ctx.sources, file)
348
+ for (const file of ctx.project.source.files) {
349
+ const parsed = parsedFile(ts, ctx.project.source, file)
350
350
  if (parsed === undefined) continue
351
351
  const { tree, lines } = parsed
352
352
  const visit = (node: TSApi.Node): void => {
@@ -402,8 +402,8 @@ export const assuranceEscapeHatchRule: CheckRule = {
402
402
  const ts = await importProjectTypeScript(ctx.root)
403
403
  if (ts === undefined) return didNotRun("NF-S006", "Security escape hatch scan")
404
404
  const findings: Diagnostic[] = []
405
- for (const file of ctx.sources.files) {
406
- const parsed = parsedFile(ts, ctx.sources, file)
405
+ for (const file of ctx.project.source.files) {
406
+ const parsed = parsedFile(ts, ctx.project.source, file)
407
407
  if (parsed === undefined) continue
408
408
  const { tree, lines } = parsed
409
409
  const visit = (node: TSApi.Node): void => {
@@ -445,8 +445,8 @@ export const unprefixedSecureCookieRule: CheckRule = {
445
445
  const ts = await importProjectTypeScript(ctx.root)
446
446
  if (ts === undefined) return didNotRun("NF-S007", "Secure cookie prefix scan")
447
447
  const findings: Diagnostic[] = []
448
- for (const file of ctx.sources.files) {
449
- const parsed = parsedFile(ts, ctx.sources, file)
448
+ for (const file of ctx.project.source.files) {
449
+ const parsed = parsedFile(ts, ctx.project.source, file)
450
450
  if (parsed === undefined) continue
451
451
  const { tree } = parsed
452
452
  const visit = (node: TSApi.Node): void => {